简介:面向MPU6500六轴惯性测量单元(IMU)的驱动与姿态解算工程,适用于嵌入式开发者、无人机与机器人等运动控制场景,解决传感器原始数据读取、校准、融合及欧拉角/四元数输出等关键问题。内容完整覆盖MPU6500寄存器配置、工作模式设定、滤波参数调节,以及基于互补滤波或Madgwick算法的数据融合过程,帮助理解如何将加速度计与陀螺仪数据转化为稳定的姿态估计。压缩包共157个文件,容量730KB,以C源码和头文件为主,辅以编译配置、Python脚本与PDF说明文档,包含从底层寄存器操作到DMP运动驱动的多级代码,可直接在CCS等IDE工程中加载分析。已有1039人学习下载。读者可从中获得完整的驱动框架与实际工程参考,包括中断设计、传感器校准和姿态解算的常用实现,适合作为机器人、无人机或可穿戴设备中运动感知模块的开发起点。
1. 为什么 MPU6500 的驱动和算法不能分开看:一个被数据噪声坑惨的案例
"MPU6500 驱动算法"这个搜索词,通常指向两种人:一种人刚把寄存器配通,原始数据能读出来,接下来不知道滤波和姿态解算该从哪里下手;另一种人已经在跑姿态算法,但角度总是一动就抖、一停就飘。这两种情况实际上是同一个问题——驱动层和算法层的边界被切错了。驱动不止是把寄存器写对,还要负责量程选择、数字滤波、零偏估计和采样节拍;算法也不只是算四元数,还要知道输入数据是从什么带宽、什么噪声环境里来的。常见翻车场景是:把现成姿态代码直接搬到新板子上,陀螺仪原始数据看上去正常,但角度一分钟漂十几度,查了一圈发现内部数字低通滤波器没配置,上电后也没做零偏估计。这篇笔记按"寄存器初始化 → 数据清洗 → 姿态解算 → 验证标定"的顺序,把每步的参数、代码和坑位一起讲,适合给无人机、平衡车、机器人和手持云台做姿态测量的嵌入式开发者。
2. 先把底子打好:MPU6500 的通信选型与寄存器初始化
2.1 选 I2C 还是 SPI:接管脚资源与数据更新率
MPU6500 这颗 6 轴传感器同时提供 I2C 和 SPI 接口,很多开发板默认走 I2C,因为引脚少、接线省事。但 I2C 的速率上限是 400kHz,读取一次完整的 14 字节数据大概要 280us 以上的总线占用,姿态更新率如果做到 1kHz,留给其他传感器和计算的时间就很紧。SPI 可以跑到 20MHz,读同样 14 字节只需要几十微秒,代价是至少占用 CS、SCLK、MOSI、MISO 四根线。
选择逻辑可以这样看:如果板子上引脚紧张,并且姿态更新率只要 200Hz 左右,I2C 完全够用。如果系统里已经有 SPI 总线且速率高,或者需要 1kHz 的姿态闭环,SPI 是更可靠的选择。还有一个容易被忽略的点:I2C 地址由 AD0 引脚电平决定,AD0 接地时地址是 0x68,接 VDD 时地址是 0x69。SPI 模式下 AD0 引脚多数板子被复用为片选,不能当成普通 I2C 地址脚用。我第一次布板时把 AD0 悬空,结果 I2C 扫描偶尔能发现设备,偶尔又丢失,后来才发现是引脚电平不定。
| 维度 | I2C | SPI |
|---|---|---|
| 引脚占用 | 2 根(SDA、SCL) | 4 根(CS、SCLK、MOSI、MISO) |
| 时钟上限 | 400kHz | 20MHz 左右 |
| 单次读 14 字节占用 | 约 280us | 几十微秒 |
| 适用场景 | 引脚紧张、200Hz 姿态更新 | 1kHz 姿态环、多传感器共享 SPI |
这个对比表不是让你一定选 SPI,而是要你根据主循环的调度情况做预算。我一般会先把 I2C 版本跑通,原型阶段节省调试时间;等系统负载上来以后再迁移到 SPI。迁移时只需要把底层 read/write 函数换成 SPI 版,初始化逻辑可以保持一致,这也是驱动分层的价值。
2.2 最小初始化序列:从复位到配置量程与数字低通滤波器的代码
初始化代码是所有 MPU6500 工程的地基。第一次上电时芯片可能处于未知状态,必须复位寄存器,等内部电源建立以后再配置量程和滤波。下面这段 C 代码是 I2C 版本的最小初始化序列,read/write 函数由你的 BSP 提供,重点关注寄存器地址和写入值。
#include <stdint.h> #define MPU6500_I2C_ADDR 0x68 #define REG_WHO_AM_I 0x75 #define REG_PWR_MGMT_1 0x6B #define REG_GYRO_CONFIG 0x1B #define REG_ACCEL_CONFIG 0x1C #define REG_CONFIG 0x1A #define REG_SMPLRT_DIV 0x19 static int mpu6500_init(void) { uint8_t who = 0; /* 第一步:确认 I2C 地址和芯片型号 */ if (i2c_read_reg(MPU6500_I2C_ADDR, REG_WHO_AM_I, &who) != 0) return -1; if (who != 0x70) /* MPU6500 的 WHO_AM_I 复位值 */ return -1; /* 第二步:先复位再唤醒 */ i2c_write_reg(MPU6500_I2C_ADDR, REG_PWR_MGMT_1, 0x80); delay_ms(100); /* 等待内部电源建立 */ i2c_write_reg(MPU6500_I2C_ADDR, REG_PWR_MGMT_1, 0x01); /* PLL参考X轴陀螺 */ /* 第三步:配置量程 */ i2c_write_reg(MPU6500_I2C_ADDR, REG_GYRO_CONFIG, 0x18); /* ±2000 dps */ i2c_write_reg(MPU6500_I2C_ADDR, REG_ACCEL_CONFIG, 0x10); /* ±8 g */ /* 第四步:数字低通滤波器 DLPF 配置 */ i2c_write_reg(MPU6500_I2C_ADDR, REG_CONFIG, 0x03); /* DLPF_CFG=3 */ i2c_write_reg(MPU6500_I2C_ADDR, REG_SMPLRT_DIV, 0x00); /* 输出率取上限 */ delay_ms(50); return 0; }这段代码的顺序有讲究。先读 WHO_AM_I 不是形式主义,它能直接暴露地址错误、焊接问题、上电时序没满足这三类故障;如果读出来不对,后面写寄存器都没有意义。复位写入 0x80 后必须等待,有些板子 10ms 就够了,但 100ms 更稳妥,因为不同电源芯片的跌落恢复时间不一样。
量程选择直接影响后续的灵敏度换算:陀螺仪 ±2000dps 时原始数据灵敏度是 16.4 LSB/dps,加速度计 ±8g 时是 4096 LSB/g。如果只做小幅姿态,陀螺仪量程可以降到 ±250dps,分辨率会提高到 131 LSB/dps。但如果设备会有快速翻转,还是留足裕量,否则数据会削顶,姿态瞬间乱掉。DLPF 这里写 3,对应的是把陀螺仪和加速度计的带宽都压在几十赫兹量级,具体截止频率表不同批次数据手册略有差异,建议以芯片丝印对应批次的手册为准。SMPLRT_DIV=0 表示不额外分频,让内部滤波后的输出率直接进驱动。
注意:如果初始化后读取原始数据全是 0,优先检查是否进入了睡眠模式。PWR_MGMT_1 的最高位是设备复位,第六位是睡眠位,唤醒时要写 0x01 把这两位置成正确状态。
2.3 自检与零偏粗校准:把芯片的出厂误差先在驱动层压住
陀螺仪出厂零偏通常不会太大,但换到小封装 PCB 上,贴片应力和供电电压偏差会让静止输出在 ±1dps 级别波动。如果不校准,这些数值会直接进入积分,几分钟后角度漂移就到不可接受的程度。上电后的第一次粗校准一般在初始化末尾完成:让设备静止,连续读几百次陀螺仪输出,做平均值。
#define BIAS_SAMPLE_CNT 200 #define REG_ACCEL_XOUT_H 0x3B static void mpu6500_read_raw(int16_t raw[6]) { uint8_t buf[14]; /* 从加速度计高字节开始连续读 14 字节,前 6 字节是加速度计,后 6 字节是陀螺仪 */ i2c_read_regs(MPU6500_I2C_ADDR, REG_ACCEL_XOUT_H, buf, 14); for (int i = 0; i < 6; i++) { raw[i] = (int16_t)((buf[i * 2] << 8) | buf[i * 2 + 1]); } } static void mpu6500_gyro_bias(float bias[3]) { int64_t sum[3] = {0, 0, 0}; int16_t raw[6]; for (int i = 0; i < BIAS_SAMPLE_CNT; i++) { mpu6500_read_raw(raw); sum[0] += raw[3]; /* 陀螺仪 X 在 raw[3..5] */ sum[1] += raw[4]; sum[2] += raw[5]; delay_ms(2); } bias[0] = (float)sum[0] / BIAS_SAMPLE_CNT; bias[1] = (float)sum[1] / BIAS_SAMPLE_CNT; bias[2] = (float)sum[2] / BIAS_SAMPLE_CNT; }注意 raw 数组的顺序:从 0x3B 开始连续读 14 字节,加速度计 X/Y/Z 对应 raw[0..2],陀螺仪 X/Y/Z 对应 raw[3..5]。有些人直接拿网上的 6050 代码,把顺序想成"陀螺仪在前",后面用四元数更新时三轴就会乱。零偏校准只对陀螺仪做,加速度计的零偏在静止水平面上读 ax 和 ay 也可以粗估,但更完整的是下一章的六面标定。粗校准的局限是它假设上电后器件温度不变,实际焊接应力和温漂会让零偏随时间变化,所以这一章只是"压住",后面还要靠在线修正。
3. 把原始数据变成干净数据:加速度计与陀螺仪的滤波和标定算法
3.1 加速度计为什么需要低通滤波:静止检测与重力参考
加速度计在姿态解算里的角色是"重力方向参考",但它对线振动和机械噪声非常敏感。电机转动、齿轮啮合、路面颠簸都会产生几十赫兹以上的加速度能量。如果这些毛刺直接进互补滤波,姿态解算结果会跟着抖动,甚至在静止时看到角度在 1° 到 2° 之间来回跳。所以加速度计通道需要比陀螺仪更积极的低通滤波。
常见做法是软件一阶低通,或者滑动平均。一阶低通的 alpha 参数需要按采样周期计算,不是随便拍脑袋。下面是一个三轴一阶低通实现:
typedef struct { float alpha; /* 0~1:越接近 1,当前采样权重越高,滤波越弱 */ float out[3]; } lpf3_t; static void lpf3_init(lpf3_t *f, float alpha) { f->alpha = alpha; f->out[0] = f->out[1] = f->out[2] = 0.0f; } static void lpf3_run(lpf3_t *f, const float in[3]) { for (int i = 0; i < 3; i++) { f->out[i] = f->alpha * in[i] + (1.0f - f->alpha) * f->out[i]; } }alpha 的取值公式是 alpha = dt / (dt + tau),其中 dt 是采样周期,tau 是滤波时间常数。比如采样频率 1kHz,想得到约 20Hz 的截止频率,tau = 1/(2π×20) ≈ 8ms,所以 alpha ≈ 0.001/(0.001+0.008) = 0.11。如果采样频率是 500Hz,alpha 要重新算,不是沿用 0.11。这一点是很多人滤波越调越糊的根源:改了采样率,alpha 没跟着改。
注意这里说的滤波是在内部 DLPF 之外的软件通道。我的习惯是 DLPF 选一个较宽的带宽挡住高频混叠,软件低通再按控制环需求收紧一点。如果内部 DLPF 已经压到 10Hz 左右,软件就不必再叠一级,否则相位滞后会大到让姿态环不稳定。判断标准很简单:用手快速翻转传感器,观察输出角度跟不跟手,如果明显拖泥带水,就把滤波带宽放宽。
3.2 陀螺仪零偏温漂的在线修正:滑动均值与一阶互补
陀螺仪零偏不是常数,温度变化、芯片自热、电源波动都会让它缓慢移动。上电粗校准只能保证开机时的状态,设备运行十分钟后零偏可能又偏出去。许多工程做法是在运行中做静止检测:当去偏后的角速度模值低于某个阈值时,认为设备处于静止,用这段时间的平均值不断修正零偏。
实现上要注意判断逻辑的顺序。应该先用当前估计的零偏把角速度修正掉,再判断是否静止;如果用原始角速度判断,零偏本身就会把静止判成运动。下面这段代码放在姿态更新之前,每个采样周期调用一次:
#define STATIC_GYRO_THRESH 0.5f /* 去偏后角速度小于 0.5 dps 认为是静止 */ #define STATIC_HOLD_CNT 100 /* 连续静止 100 帧才开始更新偏置 */ static void gyro_base_update(float gx, float gy, float gz, float *bx, float *by, float *bz, uint16_t *static_cnt) { float wx = gx - *bx; float wy = gy - *by; float wz = gz - *bz; float mag = sqrtf(wx * wx + wy * wy + wz * wz); if (mag < STATIC_GYRO_THRESH) { (*static_cnt)++; if (*static_cnt >= STATIC_HOLD_CNT) { /* 缓慢跟踪:等效时间常数到秒级,避免把真实角速度吃掉 */ *bx += 0.005f * wx; *by += 0.005f * wy; *bz += 0.005f * wz; *static_cnt = STATIC_HOLD_CNT; /* 防止计数器溢出 */ } } else { *static_cnt = 0; } }把更新系数设为 0.005 而不是直接把平均值赋进去,是为了防止短暂静止的角速度被完全吸收。如果设备频繁在"静止→运动→静止"之间切换,更新系数太大会让真实的小角度运动被偏置吃掉,恢复时反而出现反向漂移。阈值 0.5dps 要看应用场景:高动态无人机可以放宽到 1 到 2dps,精密转台必须收到 0.1dps 以下。这个函数本质是一种慢速低通跟踪,和 3.1 的一阶低通不同,它跟踪的是零偏这个慢变量,而不是姿态信号本身。
3.3 六面标定法:让加速度计的尺度与零偏不再是黑匣子
加速度计的轴间误差和零偏,影响最大的是互补滤波里的重力参考方向。如果加速度计在一个水平面上读出某个轴不是 1g 而是 1.03g,姿态角就会有一个固定偏置。六面标定不复杂:把传感器的六个面依次朝上,每面记录该轴的平均原始读数,然后按以下模型计算。
标定模型设为 a_cal = scale * a_raw + bias,忽略轴间交叉耦合。X 轴朝上时感受 +1g,朝下时感受 -1g,所以 scale_x = 2 / (x_up - x_down),bias_x = -(x_up + x_down) / 2。其余轴同理。
typedef struct { float scale[3]; float bias[3]; } accel_cal_t; static void accel_cal_6face(float x_up, float x_down, float y_up, float y_down, float z_up, float z_down, accel_cal_t *cal) { cal->scale[0] = 2.0f / (x_up - x_down); cal->bias[0] = -(x_up + x_down) / 2.0f; cal->scale[1] = 2.0f / (y_up - y_down); cal->bias[1] = -(y_up + y_down) / 2.0f; cal->scale[2] = 2.0f / (z_up - z_down); cal->bias[2] = -(z_up + z_down) / 2.0f; } static void accel_cal_apply(const accel_cal_t *cal, float a[3]) { for (int i = 0; i < 3; i++) { a[i] = cal->scale[i] * a[i] + cal->bias[i]; } }这段代码的输入是六个平均原始读数,单位是 LSB。标定完成后,再乘上量程灵敏度系数转成 g。实际操作里最大的坑是"没放平":如果用来找平的基准面本身倾斜 0.5°,标定结果里就会有 0.5° 的姿态误差。我的习惯是先找一个工作面,用水平仪确认,再用双面胶固定一个小 PCB,确保六个面翻转时没有偏移。每面采样 100 次求平均,能压掉一部分振动影响。对于只关心相对姿态的平衡车,六面标定不是必须的;但要做绝对姿态输出、要跟机械轴对齐,这一步省不得。
4. 从轴数据到姿态:互补滤波与四元数更新
4.1 为什么选互补滤波而不是直接积分:欧拉角在工程现场的失效场景
陀螺仪角速度积分短期精度高,但零偏和积分误差会随时间累积,几分钟就能看到角度漂移。加速度计能给出绝对重力方向,不会漂移,但动态响应慢且受振动污染。互补滤波的思路是在频域合并两者:陀螺仪通过积分给出高频运动和短时姿态,加速度计长期修正这个姿态的漂移,两者用一个比例增益缝合。
这里用四元数而不是欧拉角,是为了避免万向锁。欧拉角的 roll/pitch/yaw 在 pitch 接近 ±90° 时,roll 和 yaw 的微分方程会退化,控制回路会突然抖动甚至翻转。四元数没有奇异点,代价是调试者要理解它的物理含义。在民用无人机、机器人云台这类动态范围比较大的场景,四元数是更稳妥的选择。
4.2 互补滤波实现:加速度计修正陀螺仪漂移的完整 C 代码
下面这段代码是互补滤波的一种常用实现,用加速度计向量与四元数推算出的重力向量的叉积作为修正误差,反馈到陀螺仪角速度上。这里只保留了比例项 Kp,没有加积分项,适合先验证平台。
typedef struct { float q0, q1, q2, q3; /* 单位四元数 */ } quat_t; #define Kp 0.8f /* 修正增益:越大收敛越快,噪声也越明显 */ #define dt 0.002f /* 采样周期 2ms,必须和实际调用频率一致 */ static void imu_attitude_update(quat_t *q, float gx, float gy, float gz, float ax, float ay, float az) { float norm, vx, vy, vz, ex, ey, ez; /* 加速度计归一化 */ norm = sqrtf(ax * ax + ay * ay + az * az); if (norm < 1e-6f) return; ax /= norm; ay /= norm; az /= norm; /* 由四元数推算重力估计方向 */ vx = 2.0f * (q->q1 * q->q3 - q->q0 * q->q2); vy = 2.0f * (q->q0 * q->q1 + q->q2 * q->q3); vz = q->q0 * q->q0 - q->q1 * q->q1 - q->q2 * q->q2 + q->q3 * q->q3; /* 叉积:测得重力与估计重力之间的方向误差 */ ex = ay * vz - az * vy; ey = az * vx - ax * vz; ez = ax * vy - ay * vx; /* 误差反馈到陀螺仪角速度 */ gx += Kp * ex; gy += Kp * ey; gz += Kp * ez; /* 一阶龙格-库塔更新四元数 */ q->q0 += 0.5f * dt * (-q->q1 * gx - q->q2 * gy - q->q3 * gz); q->q1 += 0.5f * dt * ( q->q0 * gx + q->q2 * gz - q->q3 * gy); q->q2 += 0.5f * dt * ( q->q0 * gy - q->q1 * gz + q->q3 * gx); q->q3 += 0.5f * dt * ( q->q0 * gz + q->q1 * gy - q->q2 * gx); /* 单位化,防止数值漂移积累 */ norm = sqrtf(q->q0 * q->q0 + q->q1 * q->q1 + q->q2 * q->q2 + q->q3 * q->q3); if (norm < 1e-6f) norm = 1.0f; q->q0 /= norm; q->q1 /= norm; q->q2 /= norm; q->q3 /= norm; }这个实现的性能很大程度上取决于加速度计是否被滤波干净。如果加速度计噪声很大,Kp 就得调小,否则修正项会把噪声灌进陀螺仪积分;如果 Kp 太小,静态漂移收敛又太慢。我一般从 Kp=0.5 起步,观察静态噪声和阶跃响应:太软就加大到 0.8,太抖就减到 0.3。更关键的是 dt,如果实际调用周期不是 2ms,四元数更新会忽快忽慢,这是很多姿态乱跳的根源。
注意:dt 应该用实测的采样间隔,而不是代码里写的常量。先用定时器测量两个采样中断之间的实际时间,再填进这个常量。很多平台用 RTOS,任务调度抖动很大,直接用标称 2ms 会让积分步长匹配不上。
4.3 四元数转欧拉角与坐标系对齐:让输出角度对得上你的机械零位
四元数用于内部计算,输出给上层控制或可视化时通常要转成欧拉角。转换公式需要做参数保护,否则 asinf 输入超过 1 会得到 NaN。
typedef struct { float roll, pitch, yaw; /* 单位:弧度 */ } euler_t; static float clampf(float x, float lo, float hi) { return x < lo ? lo : (x > hi ? hi : x); } static void quat_to_euler(const quat_t *q, euler_t *e) { float a = q->q0 * q->q0; float b = q->q1 * q->q1; float c = q->q2 * q->q2; float d = q->q3 * q->q3; e->roll = atan2f(2.0f * (q->q2 * q->q3 + q->q0 * q->q1), a - b - c + d); e->pitch = asinf(clampf(2.0f * (q->q0 * q->q2 - q->q1 * q->q3), -1.0f, 1.0f)); e->yaw = atan2f(2.0f * (q->q1 * q->q2 + q->q0 * q->q3), a + b - c - d); }欧拉角的定义顺序不是唯一的,代码里是 Z-Y-X 的内旋顺序,对应 roll→pitch→yaw。不同应用如果采用不同的旋转顺序,转换公式会不一样,不要直接抄别的项目的输出再原样用。坐标系对齐也在这里处理:如果芯片安装方向与外壳机械轴不一致,可以在进入姿态算法之前对原始三轴做一次固定旋转。比如传感器平放但产品需要竖放,常见做法是绕 Z 轴旋转 90° 或 180°,旋转后的数据再进四元数,就省得每次输出角度再换算。
5. MPU6500 驱动避坑与排查:5 个让姿态算法翻车的真实问题
5.1 现象:静止时角度慢慢漂移,一分钟能漂好几度
原因:陀螺仪零偏没有在每次上电后重新估计,或者只校准了一次就不再更新。芯片温度起来后零偏移动,旧偏置失效。解决:上电后做 200 次静止平均作为初始偏置,运行时用静止检测做慢速跟踪。注意判断静止要用去偏后的角速度,否则零偏本身会把静止判成运动。如果已经做了这两步,角度还在漂,就要检查加速度计六面标定是不是引入了固定误差。
5.2 现象:加速度计低通滤波后响应变肉,快速倾斜时姿态明显滞后
原因:软件滤波的 alpha 没按采样周期重算,或者滑动平均窗口太长。比如 1kHz 采样时用了 100 点平均,等效滞后约 50ms,对飞行器来说已经很大。解决:用 alpha = dt / (dt + tau) 计算,选 tau 之前先明确姿态环需要的响应时间。做完后在手里快速翻转传感器,观察输出角度与真实动作的视觉滞后:如果慢半拍,把截止频率调高。另一个隐蔽原因是内部 DLPF 和软件滤波叠加,两级低通让相位滞后翻倍,此时应该把其中一级放宽。
5.3 现象:四元数更新频率忽高忽低,角度输出像在跳格子
原因:把传感器读取放在主循环里,其他任务导致读取间隔波动,但代码里 dt 是固定值。积分用错了步长,姿态每一拍都会引入额外误差。解决:用定时器中断或 MPU6500 的数据就绪 INT 引脚触发采样,保证更新频率固定;如果仍要放在主循环,至少用计数器实测两帧间隔,把真实的 dt 传给姿态更新函数。不要用 delay 硬等,因为中断和任务抢占会让间隔不可控。
5.4 现象:pitch 接近 ±90° 时 roll 和 yaw 乱跳
原因:这是欧拉角固有的万向锁表现,也可能来自四元数转欧拉角时 atan2 参数顺序写反。后者会让 roll 和 yaw 在静止时互相串扰。解决:先写一个单元测试,让传感器绕 X 轴从 0° 转到 90°,观察 roll 是否对应变化、yaw 是否保持不动;如果不满足,回到转换公式检查参数顺序。如果应用场景确实会接近 ±90°,建议控制环直接用四元数而不是欧拉角。
5.5 现象:SPI 读 WHO_AM_I 返回 0x00 或 0xFF,I2C 却正常
原因:SPI 模式下,主控的 CPOL/CPHA 与芯片不匹配,或者读寄存器时地址字节的最高位没有设为 1。MPU6500 的 SPI 读取要求寄存器地址字节最高位为 1,很多通用 SPI 驱动默认按普通外设处理,没有这个位。解决:把 SPI 模式调整为数据手册时序图对应的极性,并在读函数里对地址做| 0x80操作。如果还是 0xFF,检查 CS 是否在整个读事务期间保持拉低,CS 在字节间抖动也会导致读取失败。还要确认 MISO 引脚没被其他外设复用,否则读回来的数据会被拉低或拉高。
6. 进阶验证:把姿态输出和真实角度对照,不靠肉眼判断
6.1 离线回放法
把原始数据录下来,同时在 PC 上实现一套完全相同的算法,比对着找参数问题。我会在嵌入式里每隔 2ms 把 6 轴原始整数值通过串口打印到文本文件,然后写一个脚本按同样的顺序做滤波和四元数更新。离线跑的好处是参数可以随便改,不会把板子调坏,也能快速定位问题发生在哪个环节。如果 PC 复算出来的角度正常,而目标板输出不正常,问题多半在采样节拍或数值截断,而不是算法本身。
6.2 静态基准对照
用数字倾角仪或分度头给出几个已知角度,把传感器装在一个刚性块上,分别记录算法输出的 roll/pitch 与基准值的偏差。测试点不需要多,-60°、-30°、0°、30°、60° 五档足够看出线性度和零偏。记录成表格后,最大偏差超过 1° 时要往回查标定和滤波参数。这个测试最重要的是夹具刚性:用手扶住或者用泡沫固定,会在角度上叠加微小的机械形变,测出来的误差不是传感器的真实表现。
6.3 用 Allan 方差评估零偏不稳定性
如果要做长时间运行的自主姿态,光看静态角度还不够,最好做一次 Allan 方差分析。把陀螺仪放在静止无振动的桌面上连续采集一个小时,用脚本计算不同时间间隔下的方差曲线。曲线的最低点对应零偏不稳定性,这是判断这颗传感器在无人车、机器狗这类场景里够不够用的核心指标。这个分析不需要实时系统,离线录数据后处理即可。
我自己的习惯是:任何新板子首先跑离线回放,把算法和驱动分隔开,再进场做静态对照,最后才看动态响应。以前我跳过标定直接调 Kp,结果把 Kp 调到很大才勉强压住漂移,姿态噪声却糟到没法用于闭环;后来发现加速度计六面标定根本没做,重力方向本身偏了。这些经验希望能帮到你,也欢迎你在实际项目中反复验证这些参数,毕竟每一块板子的机械布局和噪声环境都不一样。
本文还有配套的精品资源,点击获取