3、加速度传感器驱动开发:ADXL345/MPU6050驱动编写、寄存器配置、数据读取与解析、Linux内核驱动框架
好,咱们今天来啃一块硬骨头——加速度传感器的驱动开发。说实话,我当年第一次接触ADXL345的时候,也被那一堆寄存器搞得有点懵。但后来我发现,只要你摸清了套路,这些传感器其实都差不多。今天咱们就拿ADXL345和MPU6050这两个经典芯片开刀,把驱动开发的底裤扒干净。
3.1 传感器选型与硬件接口
先说说这两个传感器的区别。ADXL345是ADI公司的三轴加速度计,精度高、功耗低,我特别喜欢用它做倾角检测。MPU6050呢,是InvenSense的六轴传感器,集成了加速度计和陀螺仪,做姿态解算的时候特别香。
硬件接口方面,两者都支持I2C和SPI。我个人习惯用I2C,因为连线少,就两根线。但要注意,ADXL345的I2C地址是0x53(SDO接地)或0x1D(SDO接VCC),MPU6050的地址是0x68或0x69。嗯,这里有个坑——我遇到过好几次因为地址搞错导致通信失败的情况,后来我都是直接拿逻辑分析仪抓波形确认。
| 参数 | ADXL345 | MPU6050 |
|---|---|---|
| 测量范围 | ±2g/±4g/±8g/±16g | ±2g/±4g/±8g/±16g |
| 分辨率 | 13位(4mg/LSB) | 16位(16384 LSB/g) |
| 接口 | I2C/SPI | I2C |
| 内置功能 | 仅加速度 | 加速度+陀螺仪 |
3.2 寄存器配置——从零开始初始化
初始化传感器,说白了就是往寄存器里写值。你想想看,传感器上电后默认是待机状态,你得告诉它「嘿,醒醒,开始干活了」。
以ADXL345为例,核心寄存器就这几个:
- 0x2D(POWER_CTL):电源控制寄存器。写0x08进入测量模式。
- 0x31(DATA_FORMAT):数据格式寄存器。设置量程和分辨率。
- 0x32-0x37(DATAX0-DATAZ1):数据输出寄存器,共6个字节。
我一般这样初始化:
// ADXL345初始化代码片段
void adxl345_init(void) {
// 进入测量模式
i2c_write_reg(ADXL345_ADDR, 0x2D, 0x08);
// 设置±4g量程,13位分辨率
i2c_write_reg(ADXL345_ADDR, 0x31, 0x01);
// 设置输出数据速率100Hz
i2c_write_reg(ADXL345_ADDR, 0x2C, 0x0A);
printf("ADXL345初始化完成\n");
}
MPU6050的初始化稍微复杂一点,因为它还有个陀螺仪。我记得第一次调MPU6050的时候,忘了配置陀螺仪的量程,结果读出来的角速度全是乱码。后来我养成了习惯——初始化完一定要回读寄存器确认。
// MPU6050初始化代码片段
void mpu6050_init(void) {
// 唤醒传感器(默认是睡眠模式)
i2c_write_reg(MPU6050_ADDR, 0x6B, 0x00);
// 设置加速度计量程±4g
i2c_write_reg(MPU6050_ADDR, 0x1C, 0x08);
// 设置陀螺仪量程±500°/s
i2c_write_reg(MPU6050_ADDR, 0x1B, 0x08);
// 配置数字低通滤波器,带宽42Hz
i2c_write_reg(MPU6050_ADDR, 0x1A, 0x03);
}
3.3 数据读取与解析——把原始数据变成物理量
数据读取其实很简单,就是连续读6个字节。但解析的时候要注意——原始数据是补码形式,需要做符号扩展。
以ADXL345为例,它的数据格式是:
- DATAX0(0x32):X轴低字节
- DATAX1(0x33):X轴高字节
- DATAY0(0x34):Y轴低字节
- DATAY1(0x35):Y轴高字节
- DATAZ0(0x36):Z轴低字节
- DATAZ1(0x37):Z轴高字节
解析代码长这样:
// 读取并解析ADXL345数据
void adxl345_read_data(float *x, float *y, float *z) {
uint8_t buf[6];
int16_t raw_x, raw_y, raw_z;
// 连续读取6个字节
i2c_read_regs(ADXL345_ADDR, 0x32, buf, 6);
// 组合成16位有符号整数
raw_x = (int16_t)((buf[1] << 8) | buf[0]);
raw_y = (int16_t)((buf[3] << 8) | buf[2]);
raw_z = (int16_t)((buf[5] << 8) | buf[4]);
// 转换为g值(±4g量程下,1LSB = 8mg)
*x = raw_x * 0.008f;
*y = raw_y * 0.008f;
*z = raw_z * 0.008f;
}
这里有个细节——为什么是乘以0.008?因为±4g量程下,13位分辨率对应的是8192 LSB/g,取倒数就是0.000122g/LSB。但ADXL345内部做了左对齐,实际有效位是13位,所以1LSB = 8mg。嗯,这个换算关系我当年也是查了半天手册才搞明白。
3.4 Linux内核驱动框架——把传感器挂到系统里
在Linux下写驱动,说白了就是实现一个字符设备或者输入设备。我个人更喜欢用输入子系统,因为上层应用可以直接通过/dev/input/eventX读取数据,省事。
驱动框架的核心结构:
- 设备树匹配:告诉内核你的传感器挂在哪条I2C总线上。
- probe函数:初始化硬件,注册中断,创建设备节点。
- 数据读取函数:定时器或中断触发,读取传感器数据。
- 上报数据:通过input子系统上报ABS_X、ABS_Y、ABS_Z事件。
一个简化的驱动框架示例:
// ADXL345 Linux驱动框架(简化版)
static const struct i2c_device_id adxl345_id[] = {
{ "adxl345", 0 },
{ }
};
static struct i2c_driver adxl345_driver = {
.driver = {
.name = "adxl345",
.of_match_table = adxl345_of_match,
},
.probe = adxl345_probe,
.remove = adxl345_remove,
.id_table = adxl345_id,
};
static int adxl345_probe(struct i2c_client *client,
const struct i2c_device_id *id) {
struct adxl345_data *data;
struct input_dev *input_dev;
// 分配内存
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
// 初始化输入设备
input_dev = devm_input_allocate_device(&client->dev);
input_dev->name = "ADXL345 Accelerometer";
input_set_abs_params(input_dev, ABS_X, -16000, 16000, 0, 0);
input_set_abs_params(input_dev, ABS_Y, -16000, 16000, 0, 0);
input_set_abs_params(input_dev, ABS_Z, -16000, 16000, 0, 0);
// 注册输入设备
input_register_device(input_dev);
// 创建定时器,周期性读取数据
timer_setup(&data->timer, adxl345_timer_callback, 0);
mod_timer(&data->timer, jiffies + msecs_to_jiffies(10));
return 0;
}
你可能会问,为什么要用定时器而不是中断?嗯,ADXL345虽然支持中断,但它的中断引脚配置比较麻烦。我一般用定时器轮询,10ms读一次,100Hz的采样率够用了。如果你需要低功耗,那就得用中断唤醒。
3.5 实战中的避坑指南
最后,分享几个我实战中遇到的坑:
- I2C时钟延展问题:有些传感器在转换数据时会拉低SCL,如果主控不支持时钟延展,通信就会卡死。解决办法是在I2C控制器里开启时钟延展支持。
- 数据对齐问题:MPU6050的数据是16位大端模式,而ARM处理器是小端。读回来之后需要做字节交换,不然数据全是错的。
- 滤波的重要性:原始数据噪声很大,尤其是电机启动的时候。我一般加一个滑动平均滤波,窗口大小取4-8个点,效果不错。
- 初始化三步走:唤醒、设量程、配速率
- 数据解析注意补码和单位换算
- Linux驱动用输入子系统,省心省力
- 调试时先读WHO_AM_I,确认通信正常
好了,加速度传感器的驱动开发就讲到这里。下一章咱们聊聊陀螺仪和磁力计,把九轴姿态解算的底子打好。有什么问题,咱们群里见。