1. 在树莓派中打开I2C

树莓派的内核支持 I2C 驱动,但是需要先打开。打开方法如下:
在树莓派屏幕的左上角选择Preferences,选择Raspberry Pi Configuration 然后选择Interfaces,Enable I2C ,完成之后,需要重启树莓派,就可以使用I2C了。

image.png image.png
连线部分

传感器引脚 功能 树莓派引脚
SDA 数据线 SDA.1
SCL 时钟线 SCL.1
VCC 3.3V 3.3V
GND 接地 GND

image.png


2. QT中使用WiringPi控制I2C

2.1 添加头文件

WiringPi 库里面也实现了I2C 相关的接口,在需要用的地方,引入下面两个头文件

  1. #include "wiringPi.h"
  2. #include "wiringPiI2C.h"

2.2 声明fd

在.h 文件中,声明一个fd,类型是int
fd其实代表的是文件描述符号( file descriptor ),在linux系统里面,硬件设备在操作系统里面都以fd来标识和索引,可以通过fd来对一个设备进行操作,一般就是一个数字。I2C在树莓派的操作系统中,也是作为一个fd来进行识别。因此需要在初始化的时候声明一个int类型保存fd。

  1. class MainWindow : public QMainWindow
  2. {
  3. Q_OBJECT
  4. public:
  5. explicit MainWindow(QWidget *parent = nullptr);
  6. ~MainWindow();
  7. int fd // i2c设备的文件描述符
  8. private:
  9. Ui::MainWindow *ui;
  10. };

2.3 初始化

在初始化函数内, 初始化wiringPi和wiringPiI2C这两个模块。其中wiringPiI2CSetup要传入一个参数,是传感器的识别地址。ADXL345的默认地址是0x53 ,所以加了一个宏定义,下面是初始化的代码

  1. #define I2CADDR 0x53
  2. MainWindow::MainWindow(QWidget *parent) :
  3. QMainWindow(parent),
  4. ui(new Ui::MainWindow)
  5. {
  6. ui->setupUi(this);
  7. wiringPiSetup();
  8. fd = wiringPiI2CSetup(I2CADDR);
  9. }

2.3 写数据

i2c写入数据的api如下:

API 解释
int** wiringPiI2CWrite(int fd, int **data) 向设备连续写入数据,部分设备支持
int **wiringPiI2CWriteReg8(int fd, int reg, int **data) ; 向寄存器写8位数据,reg地址,data是数据
int **wiringPiI2CWriteReg16(int fd, int reg, int **data) ; 向寄存器写16**数据,reg地址**,data是数据

2.4 读数据

API 解释
int** wiringPiI2CRead(int **fd) 向设备连续写入数据,部分设备支持
int **wiringPiI2CReadReg8(int fd, int **reg) ; 向寄存器读8位数据,reg地址,返回值是数据
int **wiringPiI2CReadReg16(int fd, int **reg) ; 向寄存器读16**数据,reg地址**,返回值是数据

3. ADXL345详细介绍

3.1 ADXL345数据手册

以下是ADXL345的中文数据手册。数据手册比较复杂,下面的其他点会有对数据手册的简单总结。

ADXL345_cn.pdf

3.2 ADXL345的使用流程

3.3 ADXL345的关键寄存器

ADXL345传感器有几个关键参数,需要配置对应**地址**的寄存器,来调整传感器的参数。
接下来会对几个关键从寄存器进行介绍:

3.3.1 设备ID寄存器:0x00

这个寄存器是只读寄存器,保存了设备id,每个ADXL345传感器都有相同的**设备ID,是0XE5**,因此在程序初始化的时候,经常会先读取一下设备id,看一下传感器状态是否正常。

  1. void MainWindow::on_detectButton_clicked()
  2. {
  3. int id = wiringPiI2CReadReg8(fd, 0x00); // 0x00,是设备id的寄存器
  4. if(id == 0xe5){
  5. // 检测到0xe5,传感器正常工作
  6. ui->idEdit->setText("0x" + QString::number(id, 16));
  7. }else {
  8. // 没有检测到id,传感器工作不正常
  9. ui->idEdit->setText("none");
  10. }
  11. }

3.3.2 速度/功率寄存器:0x2C

这个寄存器可以配置传感器的工作状态是低功耗模式还是正常模式,以及配置读取的速度

D7 D6 D5 D4 D3 D2 D1 D0
0 0 0 LOW_POWER 速率
  • D4 位如果是0,为正常模式;D4如果是1,是低功耗模式
  • D3到D0这4位数的部分是指定读取的速度,下面是不同速率的对照表

image.png
常见默认的配置,是将速度设置为50Hz,模式为正常模式,可以写入0x0a

D7 D6 D5 D4 D3 D2 D1 D0
0 0 0 0 1 1 0 0

3.3.3 数据格式寄存器:0x31

这个寄存器是用来配置ADXL345的输出数据格式。这个寄存器主要配置以下两个方面的内容:

  • 通信格式(4线SPI or 3线SPI)
  • 测量分辨率(测量精度)
  • 测量范围(量程)

寄存器的数据格式如下:

D7 D6 D5 D4 D3 D2 D1 D0
自测力 SPI 中断电平 0 全分辨率 对齐 范围
  • D6:1为3线spi,0为4线spi
  • D3:1全分辨率模式,代表数据精度为13位,精度最高;010位模式,精度较低
  • D1 D0:设置量程,对应值如下,其中g是重力加速度,1g=9.8m/s** | 设置 | | 范围 | | —- | —- | —- | | D1 | D0 | -2g ~ 2g | | 0 | 0 | -4g ~ 4g | | 0 | 1 | -8g ~ 8g | | 1 | 0 | -16g ~ 16g |

常见默认的配置是,全分辨率,量程最大到16g:

D7 D6 D5 D4 D3 D2 D1 D0
0 0 0 0 1 0 1 0

3.3.4 数据寄存器0x32 ~ 0x37

这六组寄存器存储了加速度的原始值,下面先介绍寄存器的组成

0x32 DATAX0
0x33 DATAX1
0x34 DATAY0
0x35 DATAY1
0x36 DATAZ0
0x37 DATAZ1

组合原始数据

0分量是数据的低8位,1分量是数据的高8位。最终组合的数字以补码的形式组成。
即X轴的数据格式为

DATAX1(8位) DATAX0(8位)

形式是补码,即可以直接将数据转换成成带符号的int16类型。即可以得到.
组合方式是,将高8位的数据左移动8位,例如11111111 变成 1111111100000000,然后在将低8位的数据与高八位的数据进行 | 操作,例如:
1111111100000000 | 10010001 = 1111111110010001

  1. 原始输出数据 = (int)((DATAX1 << 8) | DATAX1)

缩放分量

从数据手册可以得到,原始输出数据,除以对应的比例系数,才能得到真实的加速度值。
当选择全分辨率时候,比例系数都为 256LSB/g LSB就代表原始输出值
image.png
因此将原始输出值除以256,就可以得到加速度(单位为g)

  1. 加速度 = (float)((int)((DATAX1 << 8) | DATAX0)) / 256

默认数据中,在平放时,X轴和Y轴都没有加速度,Z轴有一个加速度,因此g轴的数据趋近为1,其他两个轴的数据趋近为0:
image.png

3.3.5 偏移寄存器:0X1E 0X1F 0X20

这三个寄存器是用来进行校准。由于加速度传感器随着使用可能存在偏差。ADXL345 提供了校准寄存器用来教程
校准流程如下
在数据手册中写到:

在无调头或单点校准方案中,器件调整为:一个轴通常为 z轴在1 g重力场,其余轴,通常是x和y轴在0 g场。然后取一 系列样本的平均值,测量其输出。系统设计人员可选择平 均样本数,但建议100 Hz或更高数据率的起点为0.1 sec。这 相当于100 Hz的数据速率10个样本。对于低于100 Hz的数据 速率,建议平均至少有10个样本。x和y轴上0 g测量和Z轴的 1 g测量的值分别存储为X0g、Y0g和Z+1g 。

使用偏移寄存器(寄存器0x1E、寄存器0x1F和寄存器 0x20),ADXL345可以自动补偿偏移输出。这些寄存器包含 8位二进制补码值,为自动添加到所有测得的加速度值, 其结果随后置入到DATA寄存器。因为置于偏移寄存器的 值为附加值,负值置于寄存器,消除正偏移,相反则消除 负偏移。该寄存器比例因子为15.6 mg/LSB,与选定的g范围 无关。

例子如下:

  • step1: 读一组原始数取平均

在平放状态下,取了一组原始数平均值

  • X=0.001g=10mg
  • Y=0.0017g=17mg
  • Z=1.021g=1021mg
    • step2:计算差值

X,Y两个坐标轴-去0,差值就是它本身
Z轴需要减去1g(1000mg),因为有重力加速度

  • deltaX = 10mg
  • deltaY = 17mg
  • deltaZ = 21mg
    • step3:缩放比例

当全分辨率的时候,比例因子为 1/256 = 3.9mg/LSB,而偏移寄存器的固定比例因子为15.6mg/LSB
其关系是4倍,因此将所有的delta除以4,并取负数(因为要补偿这些差的值)

  • offsetX = -(10/4)mg = -2.5mg
  • offsetY = -(17/4)mg = -4.25mg
  • offserZ = -(21/4)mg = -5.25mg
    • step4:写回

将所有数值写回对应的偏移寄存器当中
以上过程就完成了所有的校准。在下一章中会有对应实现的代码。

3.3.6 电源状态传感器 0x2d

该寄存器指定是否进行电源自动休眠,也可以强制指定为待机模式或工作模式

D7 D6 D5 D4 D3 D2 D1 D0
0 0 链接位 自动休眠位 测量位 休眠位 唤醒位

使传感器正常工作需要将休眠位置0,常用值是0x08


4.代码实现

4.1 头文件以及宏定义

引入wiringPi 相关的头文件,并用宏定义的方式,定义常用寄存器的地址。并定义一个结构体,统一管理三个方向的加速度。

  1. #include "wiringPi.h"
  2. #include "wiringPiI2C.h"
  3. #define REG_DEVID 0x00 // 设备id寄存器
  4. #define REG_BW_RATE 0x2c // 速度_功率寄存器
  5. #define REG_DATA_FORMAT 0x31 //数据格式寄存器
  6. #define REG_DATAX0 0x32 // x0
  7. #define REG_DATAX1 0x33 // x1
  8. #define REG_DATAY0 0x34 // y0
  9. #define REG_DATAY1 0x35 // y1
  10. #define REG_DATAZ0 0x36 // z0
  11. #define REG_DATAZ1 0x37 // z1
  12. #define REG_POWER_CTL 0x2d //工作模式寄存器
  13. #define REG_OFSX 0x1e // x偏移寄存器
  14. #define REG_OFSY 0x1f // y偏移寄存器
  15. #define REG_OFSZ 0x20 // z偏移寄存器
  16. struct acc_dat{
  17. double x;
  18. double y;
  19. double z;
  20. };

4.2 传感器初始化代码

传感器初始化主要需要配置3.3 提到的几个关键寄存器

  1. void MainWindow::adxl345_init(){
  2. wiringPiI2CWriteReg8(fd, REG_DATA_FORMAT, 0x0b);//设置数据格式为全分辨率,范围最大
  3. wiringPiI2CWriteReg8(fd, REG_BW_RATE, 0x0a);//设置速度为50hz
  4. wiringPiI2CWriteReg8(fd, REG_OFSX, 0x00);//清空x偏移设置
  5. wiringPiI2CWriteReg8(fd, REG_OFSY, 0x00);//清空y偏移设置
  6. wiringPiI2CWriteReg8(fd, REG_OFSZ, 0x00);//清空z偏移设置
  7. wiringPiI2CWriteReg8(fd, REG_POWER_CTL, 0x08); //配置为唤醒模式
  8. }

4.3 检测设备id代码

  1. void MainWindow::on_detectButton_clicked()
  2. {
  3. int id = wiringPiI2CReadReg8(fd, 0x00);
  4. if(id == 0xe5){
  5. ui->idEdit->setText("0x" + QString::number(id, 16)); // 检测到id,并显示在文本框
  6. }else {
  7. ui->idEdit->setText("none");
  8. }
  9. }

4.4 校准代码

校准部分,按照3.3.5中的步骤,编写下列代码:

  1. void MainWindow::on_modifyButton_clicked()
  2. {
  3. char x0, y0, z0, x1, y1, z1;//
  4. qint16 x = 0;
  5. qint16 y = 0;
  6. qint16 z = 0;
  7. for(int i = 0;i < 10;i++){
  8. // 读取分量
  9. x0 = (char)wiringPiI2CReadReg8(fd, REG_DATAX0);
  10. x1 = (char)wiringPiI2CReadReg8(fd, REG_DATAX1);
  11. y0 = (char)wiringPiI2CReadReg8(fd, REG_DATAY0);
  12. y1 = (char)wiringPiI2CReadReg8(fd, REG_DATAY1);
  13. z0 = (char)wiringPiI2CReadReg8(fd, REG_DATAZ0);
  14. z1 = (char)wiringPiI2CReadReg8(fd, REG_DATAZ1);
  15. // 合成原始数据并累加
  16. x += (qint16)((quint16)(x1 << 8) | x0); // 高8位左移后,与第八位相与,结合成最终结果
  17. y += (qint16)((quint16)(y1 << 8) | y0);
  18. z += (qint16)((quint16)(z1-1 << 8) | z0);
  19. delay(100);
  20. }
  21. //求平均值
  22. x = x/10;
  23. y = y/10;
  24. z = z/10;
  25. // 求差并缩放
  26. qint16 xoffset = -(x/4);
  27. qint16 yoffset = -(y/4);
  28. qint16 zoffset = -((z-1)/4);
  29. // 写回寄存器
  30. wiringPiI2CWriteReg8(fd, REG_OFSX, xoffset);
  31. wiringPiI2CWriteReg8(fd, REG_OFSY, yoffset);
  32. wiringPiI2CWriteReg8(fd, REG_OFSZ, zoffset);
  33. }

4.5 读取数据代码

读取数据整体流程和校准类似,因为全分辨率情况下,比例为256:1(在3.3.4中有详细解释),故代码如下:

  1. struct acc_dat MainWindow::adxl345_read_xyz()
  2. {
  3. char x0, y0, z0, x1, y1, z1;//
  4. struct acc_dat acc_xyz;
  5. x0 = (char)wiringPiI2CReadReg8(fd, REG_DATAX0);
  6. x1 = (char)wiringPiI2CReadReg8(fd, REG_DATAX1);
  7. y0 = (char)wiringPiI2CReadReg8(fd, REG_DATAY0);
  8. y1 = (char)wiringPiI2CReadReg8(fd, REG_DATAY1);
  9. z0 = (char)wiringPiI2CReadReg8(fd, REG_DATAZ0);
  10. z1 = (char)wiringPiI2CReadReg8(fd, REG_DATAZ1);
  11. acc_xyz.x = ((quint16)(x1 << 8) | x0) /(double)256; //缩放
  12. acc_xyz.y = ((quint16)(y1 << 8) | y0) /(double)256 ; //缩放
  13. acc_xyz.z = ((quint16)(z1 << 8) | z0) /(double)256; //缩放
  14. return acc_xyz;
  15. }

5.三轴传感器的姿态倾角计算

ADXL345 可以测3轴的加速度,但没有检测地磁的功能。只能通过借助Z轴的重力加速度在其他两轴上的投影,获取俯仰角与翻滚角(Pitch and Roll),不能检测纯水平方向的角度变化(Yaw)
_Yaw-Pitch-Roll.png
数学计算公式可见这3篇博客:
https://wiki.dfrobot.com/How_to_Use_a_Three-Axis_Accelerometer_for_Tilt_Sensing#Introduction
https://www.cnblogs.com/lifexy/archive/2019/04/13/10699502.html
https://blog.csdn.net/qcopter/article/details/51848544
通过c语言定义的atan2,可以直接得到对应两个角的结果,需要引入头文件:

  1. #include <math.h>
  2. //calculate the Roll&Pitch
  3. void RP_calculate(){
  4. double x_Buff = float(x);
  5. double y_Buff = float(y);
  6. double z_Buff = float(z);
  7. roll = atan2(y_Buff , z_Buff) * 57.3;
  8. pitch = atan2((- x_Buff) , sqrt(y_Buff * y_Buff + z_Buff * z_Buff)) * 57.3;
  9. }
  1. acc_xyz.roll = atan2(acc_xyz.x , acc_xyz.y) * 57.3;
  2. acc_xyz.pitch = atan2((- acc_xyz.x) , sqrt(acc_xyz.y * acc_xyz.y + acc_xyz.z * acc_xyz.z)) * 57.3;

6.附:完整测试代码

mainwindow.h

  1. #ifndef MAINWINDOW_H
  2. #define MAINWINDOW_H
  3. #include <QMainWindow>
  4. #include <QTimer>
  5. #include "wiringPi.h"
  6. #include "wiringPiI2C.h"
  7. #define REG_DEVID 0x00 // 设备id寄存器
  8. #define REG_BW_RATE 0x2c // 速度_功率寄存器
  9. #define REG_DATA_FORMAT 0x31 //数据格式寄存器
  10. #define REG_DATAX0 0x32 // x0
  11. #define REG_DATAX1 0x33 // x1
  12. #define REG_DATAY0 0x34 // y0
  13. #define REG_DATAY1 0x35 // y1
  14. #define REG_DATAZ0 0x36 // z0
  15. #define REG_DATAZ1 0x37 // z1
  16. #define REG_OFSX 0x1e // x偏移寄存器
  17. #define REG_OFSY 0x1f // y偏移寄存器
  18. #define REG_OFSZ 0x20 // z偏移寄存器
  19. typedef struct acc_dat{
  20. double x;
  21. double y;
  22. double z;
  23. double pitch;
  24. double roll;
  25. }info;
  26. namespace Ui {
  27. class MainWindow;
  28. }
  29. class MainWindow : public QMainWindow
  30. {
  31. Q_OBJECT
  32. public:
  33. explicit MainWindow(QWidget *parent = nullptr);
  34. ~MainWindow();
  35. int fd;
  36. bool sta;
  37. QTimer *timer;
  38. void adxl345_init();
  39. info adxl345_read_xyz();
  40. private slots:
  41. void on_modifyButton_clicked();
  42. void on_startButton_clicked();
  43. void on_detectButton_clicked();
  44. void onReadData();
  45. private:
  46. Ui::MainWindow *ui;
  47. };
  48. #endif // MAINWINDOW_H

mainwindow.cpp

  1. #include "mainwindow.h"
  2. #include "ui_mainwindow.h"
  3. #include <QMessageBox>
  4. #include <QDebug>
  5. #include <math.h>
  6. #define I2CADDR 0x53
  7. MainWindow::MainWindow(QWidget *parent) :
  8. QMainWindow(parent),
  9. ui(new Ui::MainWindow)
  10. {
  11. ui->setupUi(this);
  12. timer = new QTimer;
  13. connect(timer,SIGNAL(timeout()), this, SLOT(onReadData()));
  14. sta = false;
  15. wiringPiSetup();
  16. fd = wiringPiI2CSetup(I2CADDR);
  17. adxl345_init();
  18. }
  19. MainWindow::~MainWindow()
  20. {
  21. delete ui;
  22. }
  23. void MainWindow::adxl345_init(){
  24. wiringPiI2CWriteReg8(fd, REG_DATA_FORMAT, 0x0b);//
  25. wiringPiI2CWriteReg8(fd, REG_BW_RATE, 0x0a);//设置正常模式(不低功耗)
  26. wiringPiI2CWriteReg8(fd, REG_OFSX, 0x00);//清空x偏移设置
  27. wiringPiI2CWriteReg8(fd, REG_OFSY, 0x00);//清空y偏移设置
  28. wiringPiI2CWriteReg8(fd, REG_OFSZ, 0x00);//清空z偏移设置
  29. wiringPiI2CWriteReg8(fd, REG_POWER_CTL, 0x08); //配置为唤醒模式
  30. }
  31. void MainWindow::onReadData(){
  32. auto res = adxl345_read_xyz();
  33. ui->textBrowser->append("x:" + QString("%1").arg(res.x));
  34. ui->textBrowser->append("y:" + QString("%1").arg(res.y));
  35. ui->textBrowser->append("z:" + QString("%1").arg(res.z));
  36. ui->textBrowser->append("pitch:" + QString("%1").arg(res.pitch));
  37. ui->textBrowser->append("roll:" + QString("%1").arg(res.roll));
  38. ui->textBrowser->append("---------------------------");
  39. }
  40. info MainWindow::adxl345_read_xyz()
  41. {
  42. char x0, y0, z0, x1, y1, z1;//
  43. info acc_xyz;
  44. x0 = (char)wiringPiI2CReadReg8(fd, REG_DATAX0);
  45. x1 = (char)wiringPiI2CReadReg8(fd, REG_DATAX1);
  46. y0 = (char)wiringPiI2CReadReg8(fd, REG_DATAY0);
  47. y1 = (char)wiringPiI2CReadReg8(fd, REG_DATAY1);
  48. z0 = (char)wiringPiI2CReadReg8(fd, REG_DATAZ0);
  49. z1 = (char)wiringPiI2CReadReg8(fd, REG_DATAZ1);
  50. acc_xyz.x = (qint16)((quint16)(x1 << 8) | x0) /(double)256;
  51. acc_xyz.y = (qint16)((quint16)(y1 << 8) | y0) /(double)256 ;
  52. acc_xyz.z = (qint16)((quint16)(z1 << 8) | z0) /(double)256;
  53. acc_xyz.roll = atan2(acc_xyz.x , acc_xyz.y) * 57.3;
  54. acc_xyz.pitch = atan2((- acc_xyz.x) , sqrt(acc_xyz.y * acc_xyz.y + acc_xyz.z * acc_xyz.z)) * 57.3;
  55. return acc_xyz;
  56. }
  57. void MainWindow::on_modifyButton_clicked()
  58. {
  59. char x0, y0, z0, x1, y1, z1;//
  60. qint16 x = 0;
  61. qint16 y = 0;
  62. qint16 z = 0;
  63. for(int i = 0;i < 10;i++){
  64. x0 = (char)wiringPiI2CReadReg8(fd, REG_DATAX0);
  65. x1 = (char)wiringPiI2CReadReg8(fd, REG_DATAX1);
  66. y0 = (char)wiringPiI2CReadReg8(fd, REG_DATAY0);
  67. y1 = (char)wiringPiI2CReadReg8(fd, REG_DATAY1);
  68. z0 = (char)wiringPiI2CReadReg8(fd, REG_DATAZ0);
  69. z1 = (char)wiringPiI2CReadReg8(fd, REG_DATAZ1);
  70. x += (qint16)((quint16)(x1 << 8) | x0);
  71. y += (qint16)((quint16)(y1 << 8) | y0);
  72. z += (qint16)((quint16)(z1-1 << 8) | z0);
  73. delay(100);
  74. }
  75. x = x/10;
  76. y = y/10;
  77. z = z/10;
  78. qint16 xoffset = -(x/4);
  79. qint16 yoffset = -(y/4);
  80. qint16 zoffset = -((z-1)/4);
  81. wiringPiI2CWriteReg8(fd, REG_OFSX, xoffset);
  82. wiringPiI2CWriteReg8(fd, REG_OFSY, yoffset);
  83. wiringPiI2CWriteReg8(fd, REG_OFSZ, zoffset);
  84. QMessageBox::information(this,tr("Finish"),QStringLiteral("modify finish!"),QMessageBox::Ok);
  85. }
  86. void MainWindow::on_startButton_clicked()
  87. {
  88. timer->start(500);
  89. }
  90. void MainWindow::on_detectButton_clicked()
  91. {
  92. int id = wiringPiI2CReadReg8(fd, 0x00);
  93. if(id == 0xe5){
  94. ui->idEdit->setText("0x" + QString::number(id, 16));
  95. }else {
  96. ui->idEdit->setText("none");
  97. }
  98. }

二、在QT中用I2C读取ADXL345.docx