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方式,因为更通用。
读取流程其实就三步:
- 初始化: 配置电源管理寄存器,唤醒传感器。
- 配置量程: 设置加速度计和陀螺仪的量程(比如±2g、±250°/s)。
- 读取数据: 连续读取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。
校准方法很简单:采集静止时的数据,取平均,然后减去这个平均值。
具体步骤:
- 把设备水平静止放置(最好用水平仪确认)。
- 采集N组数据(N建议取100-500,采样率100Hz的话,1-5秒就够了)。
- 对每个轴分别求平均值,得到零偏值。
- 后续每个读数都减去这个零偏值。
代码实现:
// 零偏校准函数
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秒,被骂惨了。正确的做法是:出厂前做一次完整校准,把参数写死,用户拿到手直接用。
好了,这一章就到这儿。下一章咱们聊聊怎么把这些校准后的数据变成姿态角——也就是传说中的「四元数和欧拉角」。到时候你会发现,校准做得好,后面全是坦途。