2、IMU数据读取与校准:I2C/SPI接口读取原始数据,零偏校准、灵敏度校准

好,咱们进入第二章。这一章,说白了就是让传感器开口说话。

IMU(惯性测量单元)是手环、智能眼镜、运动追踪器的核心。你算法再牛,拿到的原始数据是错的,后面全白搭。我见过太多项目,花几个月调滤波、搞融合,最后发现是I2C时序没配好,数据全是乱码。嗯,这种坑,咱们今天一次填平。

2.1 接口选型:I2C还是SPI?

先聊接口。IMU芯片通常同时支持I2C和SPI。怎么选?

核心原则: 要速度,选SPI;要省引脚,选I2C。

我个人习惯,在可穿戴设备里,只要主控引脚够用,我首选SPI。为什么?

  • 速度优势: SPI可以跑到10MHz以上,I2C通常只有400kHz(快速模式)。读取6轴原始数据时,SPI能轻松达到1kHz以上的输出速率,I2C在高频率下容易受总线电容影响。
  • 时序简单: SPI是全双工,没有I2C那套地址应答、时钟拉伸的麻烦。调试起来,逻辑分析仪一看就懂。
  • 功耗: 很多人以为I2C省电,其实SPI在高速传输时,总线占用时间更短,整体功耗反而更低。

当然,I2C也有它的主场。比如你只需要100Hz的加速度数据,而且主控引脚紧张(像某些蓝牙SoC),那I2C完全够用。我在一个智能戒指项目里就用了I2C,因为PCB空间实在太小,多一根线都走不了。

避坑指南: 我曾经遇到过I2C上拉电阻选错导致通信间歇性失败。记住:标准模式下上拉电阻4.7kΩ,快速模式下用2.2kΩ。别偷懒用10kΩ,总线电容一大,波形就圆了。

2.2 读取原始数据:以MPU6050为例

咱们拿最经典的MPU6050来演示。它支持I2C和SPI,这里我讲I2C方式,因为更通用。

读取流程其实就三步:

  1. 初始化: 配置电源管理寄存器,唤醒传感器。
  2. 配置量程: 设置加速度计和陀螺仪的量程(比如±2g、±250°/s)。
  3. 读取数据: 连续读取6个轴的数据寄存器(每个轴2字节,共12字节)。

代码长这样(伪代码,但逻辑完全可用):

// 初始化MPU6050
void MPU6050_Init() {
    // 1. 唤醒传感器:写0x00到寄存器0x6B(电源管理)
    I2C_Write(0x68, 0x6B, 0x00);
    
    // 2. 配置加速度计量程为±2g:写0x00到寄存器0x1C
    I2C_Write(0x68, 0x1C, 0x00);
    
    // 3. 配置陀螺仪量程为±250°/s:写0x00到寄存器0x1B
    I2C_Write(0x68, 0x1B, 0x00);
}

// 读取原始数据
void MPU6050_ReadRaw(int16_t* accel, int16_t* gyro) {
    uint8_t buf[14];
    // 从寄存器0x3B开始连续读取14字节
    I2C_Read(0x68, 0x3B, buf, 14);
    
    // 解析加速度计数据(高字节在前)
    accel[0] = (buf[0] << 8) | buf[1];  // X轴
    accel[1] = (buf[2] << 8) | buf[3];  // Y轴
    accel[2] = (buf[4] << 8) | buf[5];  // Z轴
    
    // 解析陀螺仪数据
    gyro[0] = (buf[8] << 8) | buf[9];   // X轴
    gyro[1] = (buf[10] << 8) | buf[11]; // Y轴
    gyro[2] = (buf[12] << 8) | buf[13]; // Z轴
}

注意: 读取时一定要连续读,不要分多次单字节读。因为IMU内部数据是原子更新的,你读一半时新数据覆盖了,就会得到错位的值。我见过有人踩这个坑,数据跳变像过山车。

2.3 零偏校准(Bias Calibration)

原始数据读出来了,但能用吗?不能。因为每个传感器都有零偏。

什么叫零偏?就是传感器静止时,输出值不为0。比如陀螺仪,你放在桌上不动,它应该输出0°/s,但实际可能是+2.5°/s。加速度计更明显,静止时Z轴应该是1g(16384 LSB,如果量程±2g),但实际可能是16200或16500。

校准方法很简单:采集静止时的数据,取平均,然后减去这个平均值。

具体步骤:

  1. 把设备水平静止放置(最好用水平仪确认)。
  2. 采集N组数据(N建议取100-500,采样率100Hz的话,1-5秒就够了)。
  3. 对每个轴分别求平均值,得到零偏值。
  4. 后续每个读数都减去这个零偏值。

代码实现:

// 零偏校准函数
void CalibrateBias(int16_t* accel_bias, int16_t* gyro_bias) {
    int32_t accel_sum[3] = {0, 0, 0};
    int32_t gyro_sum[3] = {0, 0, 0};
    int16_t accel_raw[3], gyro_raw[3];
    
    // 采集100组数据
    for(int i = 0; i < 100; i++) {
        MPU6050_ReadRaw(accel_raw, gyro_raw);
        accel_sum[0] += accel_raw[0];
        accel_sum[1] += accel_raw[1];
        accel_sum[2] += accel_raw[2];
        gyro_sum[0] += gyro_raw[0];
        gyro_sum[1] += gyro_raw[1];
        gyro_sum[2] += gyro_raw[2];
        delay(10); // 10ms间隔
    }
    
    // 计算平均值作为零偏
    accel_bias[0] = accel_sum[0] / 100;
    accel_bias[1] = accel_sum[1] / 100;
    accel_bias[2] = accel_sum[2] / 100 - 16384; // 减去重力加速度的理论值
    gyro_bias[0] = gyro_sum[0] / 100;
    gyro_bias[1] = gyro_sum[1] / 100;
    gyro_bias[2] = gyro_sum[2] / 100;
}

个人经验: 零偏校准最好在设备上电后等待几秒再做。因为IMU内部有温度漂移,刚上电时芯片温度还没稳定,零偏会慢慢变化。我习惯先让设备跑5秒,再开始校准。另外,温度变化大的场景(比如从室内到室外),建议做温度补偿,这个后面章节会讲。

2.4 灵敏度校准(Scale Factor Calibration)

零偏搞定了,但还有第二个问题:灵敏度误差。

什么意思?就是传感器输出的数值和真实物理量之间的比例关系不准。比如加速度计,理论上±2g量程时,1g对应16384 LSB。但实际芯片可能只有16200或16500。陀螺仪也一样,你转90°/s,它可能输出95°/s或85°/s。

校准方法:用已知的物理量去标定。

对于加速度计:

  • 把设备分别朝6个方向静止(+X, -X, +Y, -Y, +Z, -Z),每个方向采集数据。
  • 理论上,每个方向的重力分量应该是±1g(或±16384 LSB)。
  • 用实际读数和理论值做线性拟合,得到每个轴的缩放因子。

对于陀螺仪:

  • 用转台提供精确的角速度(比如100°/s)。
  • 或者更简单:把设备快速旋转180°,用积分算角度,和实际角度对比。

这里给一个简化的加速度计灵敏度校准代码(假设只校准Z轴):

// 灵敏度校准(简化版,仅Z轴)
float accel_scale_z = 1.0; // 默认缩放因子

void CalibrateScale() {
    int16_t accel_raw[3], gyro_raw[3];
    int32_t sum_z_up = 0, sum_z_down = 0;
    
    // 1. 设备水平朝上(Z轴朝上,感受+1g)
    for(int i = 0; i < 50; i++) {
        MPU6050_ReadRaw(accel_raw, gyro_raw);
        sum_z_up += accel_raw[2];
        delay(10);
    }
    float avg_z_up = sum_z_up / 50.0;
    
    // 2. 设备水平朝下(Z轴朝下,感受-1g)
    // 这里需要手动翻转设备,实际项目中可以用夹具
    for(int i = 0; i < 50; i++) {
        MPU6050_ReadRaw(accel_raw, gyro_raw);
        sum_z_down += accel_raw[2];
        delay(10);
    }
    float avg_z_down = sum_z_down / 50.0;
    
    // 3. 计算缩放因子
    // 理论差值:+1g - (-1g) = 2g = 32768 LSB
    // 实际差值:avg_z_up - avg_z_down
    accel_scale_z = 32768.0 / (avg_z_up - avg_z_down);
}

重要提醒: 灵敏度校准对温度非常敏感。我曾经在25°C的实验室里校准好,拿到户外35°C的场地测试,误差直接翻倍。所以,如果产品工作温度范围宽,建议做多温度点的标定,然后查表插值。

2.5 校准后的数据使用

校准完成后,每次读取原始数据后,做两步处理:

// 应用校准参数
void ApplyCalibration(int16_t* accel_raw, int16_t* gyro_raw, 
                      float* accel_cal, float* gyro_cal) {
    // 1. 减去零偏
    accel_cal[0] = (accel_raw[0] - accel_bias[0]) * accel_scale_x;
    accel_cal[1] = (accel_raw[1] - accel_bias[1]) * accel_scale_y;
    accel_cal[2] = (accel_raw[2] - accel_bias[2]) * accel_scale_z;
    
    // 2. 陀螺仪一般只做零偏校准(灵敏度校准需要转台)
    gyro_cal[0] = (gyro_raw[0] - gyro_bias[0]) * gyro_scale_x;
    gyro_cal[1] = (gyro_raw[1] - gyro_bias[1]) * gyro_scale_y;
    gyro_cal[2] = (gyro_raw[2] - gyro_bias[2]) * gyro_scale_z;
}

校准后的数据,单位就是物理单位了:加速度计是g,陀螺仪是°/s。后面做姿态解算、步数检测,用的都是这个校准后的数据。

最后说一句: 校准参数一定要存到非易失性存储器里(比如Flash或EEPROM)。每次上电都重新校准?那用户体验太差了。我见过一个产品,每次开机都要用户把设备放平等5秒,被骂惨了。正确的做法是:出厂前做一次完整校准,把参数写死,用户拿到手直接用。

好了,这一章就到这儿。下一章咱们聊聊怎么把这些校准后的数据变成姿态角——也就是传说中的「四元数和欧拉角」。到时候你会发现,校准做得好,后面全是坦途。