3、传感器驱动开发(上):IMU(加速度计/陀螺仪)SPI/I2C驱动编写与数据校验

各位同学,今天咱们来聊聊飞控系统里最基础、也最关键的一环——IMU驱动开发。说白了,就是怎么让主控芯片跟加速度计、陀螺仪这些传感器“说上话”,并且确保它们报上来的数据是靠谱的。

我个人习惯把传感器驱动比作飞控的“眼睛”和“耳朵”。眼睛看不清,耳朵听不准,后面姿态解算、控制算法做得再漂亮也是白搭。我在项目中遇到过好几次,飞控调参调了大半个月,结果发现是传感器数据有毛刺,白白浪费了时间。所以,这一章咱们把基础打牢。

3.1 通信接口选型:SPI vs I2C

先解决第一个问题:用SPI还是I2C?

很多同学刚入门时会纠结。我直接说结论:飞控场景下,首选SPI。为什么?

  • 速度:SPI通常是几MHz到几十MHz,I2C一般只有400kHz(快速模式)。飞控里IMU数据更新率动辄1kHz以上,SPI能轻松应对。
  • 实时性:SPI是全双工,主控可以随时发起通信。I2C有总线仲裁,万一挂个慢速设备,整个总线都得等。
  • 可靠性:SPI没有I2C那种“时钟拉伸”问题,在强干扰环境下更稳定。

核心原则:对于IMU这类高频、实时性要求高的传感器,优先用SPI。I2C更适合温湿度计、气压计这类慢速设备。

当然,I2C也有它的优势:引脚少(只需两根线),接线简单。如果你做的是小型飞控,PCB空间紧张,I2C也是个选项。但记住,别把IMU和磁力计挂在同一条I2C总线上——我吃过这个亏,磁力计的数据更新会把IMU的时序搅乱。

3.2 SPI驱动编写实战(以ICM-20602为例)

好,咱们直接上代码。以InvenSense的ICM-20602为例,这是一颗六轴IMU,支持SPI和I2C。咱们用SPI模式。

3.2.1 初始化流程

初始化其实就三步:复位、配置、校验。我习惯把这三步写成一个函数,清晰明了。

// ICM-20602 SPI初始化
bool icm20602_init(void)
{
    uint8_t who_am_i;
    
    // 1. 硬件复位
    spi_write_reg(ICM20602_REG_PWR_MGMT_1, 0x80);  // 复位
    delay_ms(10);
    
    // 2. 唤醒并配置时钟源
    spi_write_reg(ICM20602_REG_PWR_MGMT_1, 0x01);  // 选择陀螺仪PLL
    delay_ms(5);
    
    // 3. 配置量程
    spi_write_reg(ICM20602_REG_GYRO_CONFIG, 0x00); // 陀螺仪±250dps
    spi_write_reg(ICM20602_REG_ACCEL_CONFIG, 0x00);// 加速度计±2g
    
    // 4. 配置数字低通滤波器
    spi_write_reg(ICM20602_REG_CONFIG, 0x03);      // DLPF带宽约41Hz
    
    // 5. 校验WHO_AM_I
    who_am_i = spi_read_reg(ICM20602_REG_WHO_AM_I);
    if (who_am_i != ICM20602_WHO_AM_I_VALUE) {
        return false;  // 传感器未响应或地址错误
    }
    
    return true;
}

这里有个细节:复位后一定要加延时。我记得有一次调试,复位后立刻写寄存器,结果写不进去。查了半天手册才发现,复位后传感器内部还在初始化,需要等10ms左右。嗯,这个坑我替你们踩过了。

3.2.2 数据读取

读取数据时,我建议用批量读取,而不是一个个寄存器读。SPI每次通信都有片选和字节开销,批量读能大幅提升效率。

// 批量读取加速度计和陀螺仪数据
void icm20602_read_all(float *accel, float *gyro)
{
    uint8_t buf[14];
    int16_t raw_acc[3], raw_gyro[3];
    
    // 从ACCEL_XOUT_H开始,连续读14个字节
    spi_read_burst(ICM20602_REG_ACCEL_XOUT_H, buf, 14);
    
    // 解析原始数据(大端模式)
    raw_acc[0] = (buf[0] << 8) | buf[1];
    raw_acc[1] = (buf[2] << 8) | buf[3];
    raw_acc[2] = (buf[4] << 8) | buf[5];
    raw_gyro[0] = (buf[8] << 8) | buf[9];
    raw_gyro[1] = (buf[10] << 8) | buf[11];
    raw_gyro[2] = (buf[12] << 8) | buf[13];
    
    // 转换为物理量(±2g对应16384 LSB/g,±250dps对应131 LSB/dps)
    accel[0] = raw_acc[0] / 16384.0f;
    accel[1] = raw_acc[1] / 16384.0f;
    accel[2] = raw_acc[2] / 16384.0f;
    gyro[0] = raw_gyro[0] / 131.0f;
    gyro[1] = raw_gyro[1] / 131.0f;
    gyro[2] = raw_gyro[2] / 131.0f;
}

小技巧:SPI读取时,第一个字节是寄存器地址+读标志位(通常最高位置1),后面才是数据。写代码时别忘了这个细节,否则读回来的全是0。

3.3 I2C驱动编写(以MPU6050为例)

虽然我推荐SPI,但I2C在某些场景下还是有用武之地的。比如MPU6050这颗经典传感器,很多开源飞控都在用。咱们简单过一下I2C的写法。

// MPU6050 I2C初始化
bool mpu6050_init(void)
{
    uint8_t id;
    
    // 唤醒传感器
    i2c_write_reg(MPU6050_ADDR, MPU6050_REG_PWR_MGMT_1, 0x00);
    delay_ms(10);
    
    // 配置采样率
    i2c_write_reg(MPU6050_ADDR, MPU6050_REG_SMPLRT_DIV, 0x07); // 1kHz/(1+7)=125Hz
    
    // 读取WHO_AM_I
    i2c_read_reg(MPU6050_ADDR, MPU6050_REG_WHO_AM_I, &id);
    if (id != MPU6050_WHO_AM_I_VALUE) {
        return false;
    }
    
    return true;
}

I2C和SPI最大的区别在于:I2C有设备地址。每次通信都要先发地址,再发寄存器地址,最后才是数据。所以I2C的代码看起来多了一层封装,但底层逻辑是一样的。

注意:I2C总线上的上拉电阻不能省!我见过有人为了省两个电阻,直接把SDA和SCL接到MCU引脚上,结果通信时好时坏。上拉电阻一般选4.7kΩ,总线电容大时选2.2kΩ。

3.4 数据校验:别让飞控“睁眼瞎”

驱动写好了,数据能读回来了,是不是就完事了?远远不够。传感器数据本身可能就有问题——比如受到振动、温度漂移、甚至硬件故障。所以,数据校验是必须的。

3.4.1 校验方法

我常用的校验手段有四种,列个表大家看得清楚:

校验方法 检测目标 实现方式
范围检查 数据是否超出物理极限 加速度计±16g,陀螺仪±2000dps
一致性检查 连续数据是否突变 相邻采样差值超过阈值则丢弃
CRC校验 通信过程中数据是否被篡改 部分传感器支持硬件CRC
自检(SELF_TEST) 传感器内部是否正常 写入自检寄存器,读取响应

我曾经在一个项目中,飞控在空中突然翻滚。排查了三天,最后发现是陀螺仪数据偶尔跳变到满量程。加上一致性检查后,问题立刻解决。你想想看,如果当时没有做数据校验,后果不堪设想。

3.4.2 代码实现

// 数据校验函数
bool sensor_data_check(float accel[3], float gyro[3])
{
    // 1. 范围检查
    for (int i = 0; i < 3; i++) {
        if (fabs(accel[i]) > 16.0f || fabs(gyro[i]) > 2000.0f) {
            return false;  // 超出物理范围
        }
    }
    
    // 2. 一致性检查(与上一帧比较)
    static float prev_accel[3] = {0};
    static float prev_gyro[3] = {0};
    float delta_accel, delta_gyro;
    
    for (int i = 0; i < 3; i++) {
        delta_accel = fabs(accel[i] - prev_accel[i]);
        delta_gyro = fabs(gyro[i] - prev_gyro[i]);
        
        // 阈值:加速度计变化不超过4g,陀螺仪不超过500dps
        if (delta_accel > 4.0f || delta_gyro > 500.0f) {
            return false;  // 数据突变,可能为异常
        }
    }
    
    // 更新上一帧数据
    memcpy(prev_accel, accel, sizeof(prev_accel));
    memcpy(prev_gyro, gyro, sizeof(prev_gyro));
    
    return true;
}

重要:数据校验不能只在初始化时做一次,必须在每个采样周期都做。我建议把校验函数放在读取函数之后、姿态解算之前,形成一个“读取-校验-使用”的流水线。

3.5 本章知识体系

说了这么多,咱们用一张图来总结一下IMU驱动开发的整体流程。这张图是我自己画的,涵盖了从硬件接口到数据校验的完整链路。

IMU驱动开发知识体系 通信接口选型 SPI(高速/实时) I2C(低速/简单) 传感器选型 驱动开发核心步骤 初始化(复位→配置→校验) → 数据读取(单次/批量) → 数据解析(原始值→物理量) 数据校验(四道防线) 范围检查 → 一致性检查 → CRC校验 → 自检(SELF_TEST) 可靠的数据 → 姿态解算

从图上可以看得很清楚:通信接口是基础,驱动开发是核心,数据校验是保障。这三层缺一不可。很多新手只关注驱动怎么写,忽略了数据校验,结果飞控飞起来就炸。嗯,希望你们能记住这个教训。

3.6 避坑指南

最后,我把自己这些年踩过的坑总结一下,你们直接拿去用:

  • SPI的片选信号别乱拉:多个SPI设备共用一个片选?别这么干。每个设备独立片选,否则数据会串扰。
  • I2C的时钟频率别设太高:有些传感器标称支持400kHz,但实际布线长了就跑不动。我一般设300kHz,稳如老狗。
  • 数据校验的阈值别拍脑袋:加速度计变化阈值设4g?你得先看看你的飞行器最大加速度是多少。固定翼一般不超过3g,多旋翼可能到5g。根据实际场景调。
  • 别忘了温度补偿:陀螺仪的零偏会随温度漂移。如果你做的是工业级飞控,建议加一个温度补偿表。我曾在零下20度的环境里试飞,陀螺仪零偏直接翻了一倍。

我的习惯:每次写完驱动,我都会用逻辑分析仪抓一下SPI/I2C波形,确认时序没问题。别太相信代码,示波器不会骗人。

好了,这一章的内容就到这里。IMU驱动是飞控软件的基石,你们一定要亲手写一遍,别光看不练。下一章咱们继续聊传感器驱动,到时候会讲磁力计和气压计的驱动,以及传感器融合的初步概念。


公众号:蓝海资料掘金营,微信deep3321