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);
}
我的小技巧:初始化完成后,读一下WHO_AM_I寄存器(ADXL345是0x00,MPU6050是0x75)。如果读回来的值和芯片ID一致,说明I2C通信没问题。这个步骤能帮你省下至少半小时的调试时间。

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。嗯,这个换算关系我当年也是查了半天手册才搞明白。

我曾经踩过的坑:读MPU6050数据时,加速度计和陀螺仪的数据寄存器地址是连续的。加速度计从0x3B开始,陀螺仪从0x43开始。如果你一次性读太多字节,可能会读到错误的数据。我建议分两次读,一次读加速度计,一次读陀螺仪。

3.4 Linux内核驱动框架——把传感器挂到系统里

在Linux下写驱动,说白了就是实现一个字符设备或者输入设备。我个人更喜欢用输入子系统,因为上层应用可以直接通过/dev/input/eventX读取数据,省事。

驱动框架的核心结构:

  1. 设备树匹配:告诉内核你的传感器挂在哪条I2C总线上。
  2. probe函数:初始化硬件,注册中断,创建设备节点。
  3. 数据读取函数:定时器或中断触发,读取传感器数据。
  4. 上报数据:通过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,确认通信正常

好了,加速度传感器的驱动开发就讲到这里。下一章咱们聊聊陀螺仪和磁力计,把九轴姿态解算的底子打好。有什么问题,咱们群里见。