简介:惯性测量单元(IMU)由加速度计与陀螺仪构成,广泛集成于六轴传感器中,用于运动检测与姿态感知。其核心应用之一是位移测算,原理上通过对加速度信号进行双重积分获得速度与位置,但实际工程中直接积分会因重力分量、噪声累积和零漂发散而失效。为此需要先通过姿态解算去除重力影响,再借助低通滤波、零速修正等手段抑制积分漂移。在STM32、ESP32等嵌入式平台上,基于Mahony互补滤波的加速度补偿方案能够有效提升短时低速运动场景下的测算精度,适用于步态分析、设备定位与机器人里程估计等场景。本文围绕MPU6050位移测算的完整流程,给出从寄存器配置、数据读取到滤波与误差校正的源码实现,并提供参数调试技巧,帮助开发者在真实项目中落地可靠的距离估算方案。
1. MPU6050位移测算为什么难,难在哪
MPU6050位移测算是惯性测量单元(IMU)应用里最常被问、也最容易翻车的方向。很多人拿着六轴传感器拿到手的第一个念头是“加速度积分两次不就是位移吗”,但实际跑起来会发现数据漂移得离谱,静止时位移能跑到几十米,动起来反而更没法看。原因不复杂:MPU6050 的加速度计噪声大、零偏不稳定,陀螺仪也有温漂,双重积分会让误差按时间的平方累积。这个问题不是代码能彻底解决的,但用对方法和滤波,把误差压到可接受范围是能做到的。这篇文章面向想用MPU6050 源码做位移测算的嵌入式开发者、学生和机器人爱好者,我会从加速度积分的基本公式讲起,给出一套能在 STM32、ESP32 上跑的完整方案,并说明哪些参数必须调、哪些坑必须避。
2. 从加速度到位移:MPU6050位移测算的基础公式与滤波选型
2.1 为什么不能直接对加速度积分两次
加速度计输出的是比力(specific force),包含了重力分量。静止放在桌面上时,MPU6050 读到的加速度模值约等于 1g,而不是 0。如果直接把原始数据拿来做双重积分,第一轮积分就会把重力当作运动加速度,得到的“速度”直线上升,第二轮积分得到的“位移”更是直接爆炸。所以第一步不是写积分函数,而是先把重力从测量值里剥掉。
剥掉重力有两个路径。第一个是假设传感器姿态不变,用静止时测量到的重力方向作为参考,做一次坐标旋转补偿。第二个是结合陀螺仪做姿态解算,实时求出旋转矩阵,再把加速度从机体坐标系变换到世界坐标系。六轴传感器姿态解算的常见做法是用 Mahony 或 Madgwick 互补滤波,把加速度计和陀螺仪的数据融合,输出四元数,然后通过四元数把加速度转到导航系。实测下来,只要载体不是长时间高速旋转,Mahony 的精度和资源开销已经足够。
位移测算的误差来源还隐藏在采样阶段。加速度计输出本身带有高频噪声,假如采样频率是 100Hz,噪声会混叠进信号里,积分时逐点累加。所以加速度原始数据必须先经过低通滤波。滤波器的选择我一般先用一阶低通,频率设为 20Hz 左右。原因是实验台架的往返运动通常集中在 0.5Hz 到 5Hz 之间,20Hz 截止频率可以保留有效运动分量,同时把高频震动滤掉。如果做的是机器人关节这类低频运动,频率还可以再往下压,甚至用 10Hz,但潜在大幅摆动场景下需要提高截止频率以避免相位滞后过大。
2.2 位移测算常用公式:双重积分的离散实现
位移测算的数学基础是运动学公式,即速度等于加速度对时间积分,位移等于速度对时间积分。MPU6050 输出的是离散采样序列,实际代码里用的是离散累加:
- 速度 v[n] = v[n-1] + a[n] * dt
- 位移 s[n] = s[n-1] + v[n] * dt
dt是采样周期,由定时器决定,不能直接用1/HZ去近似。因为定时器中断本身有抖动,周期不一致会让积分结果产生随机误差。我通常用一个 32 位计数器,在每次中断里读取计数值再换算成实际间隔,这样即使中断轻微抖动,积分步长也符合真实时间。
这是基础的积分函数,一个典型的实现如下:
typedef struct { float dt; // 采样周期,单位秒 float vel; // 上一时刻速度 float disp; // 上一时刻位移 float acc_prev; // 上一时刻加速度 float vel_prev; // 上一时刻速度 } MotionIntegrator; void MotionIntegrator_Init(MotionIntegrator *mi, float dt) { mi->dt = dt; mi->vel = 0.0f; mi->disp = 0.0f; mi->acc_prev = 0.0f; mi->vel_prev = 0.0f; } float MotionIntegrator_Update(MotionIntegrator *mi, float acc) { // 梯形积分比矩形积分更稳,减小步长突变带来的误差 float vel_new = mi->vel_prev + (mi->acc_prev + acc) * 0.5f * mi->dt; float disp_new = mi->disp + (mi->vel_prev + vel_new) * 0.5f * mi->dt; mi->vel_prev = vel_new; mi->disp = disp_new; mi->acc_prev = acc; return disp_new; }这个函数用梯形积分替代矩形积分,因为梯形积分默认加速度在两个采样点之间线性变化,更贴近物理过程。参数需要注意两点:acc必须是扣除重力后的加速度值,单位是m/s^2,不是原始 LSB 值;vel_prev和disp必须用volatile或结构体保存,因为中断和主循环都要访问它们。积分结果会随着时间越漂越远,这是双重积分的本性,后面第 4 章会讲怎么校正。
2.3 位移测算前选好滤波方案:均值滤波还是一阶低通
滤波方案直接影响后面所有计算结果。我见过很多人在这一步直接上卡尔曼滤波,其实没必要。MPU6050 的加速度噪声在 1kHz 输出下比较明显,但经过一阶低通后已经够用。卡尔曼滤波更适合做数据融合而不是单纯去噪,且调参数成本高,先用简单方案跑通,再决定要不要增强。
一阶低通的离散形式是y[n] = alpha * x[n] + (1 - alpha) * y[n-1],alpha 的计算公式是alpha = dt / (RC + dt),RC 是时间常数,等于1 / (2 * PI * fc)。比如采样频率 100Hz、截止频率 20Hz,RC 约 0.00796 秒,alpha 约 0.44。这个参数在代码里放出来,方便你在不同运动频率下现场改。一个常见误区是 alpha 太小,导致真实运动信号被严重衰减,位移计算结果偏小;alpha 太大又滤不干净,积分轨迹毛刺明显。建议先用 30Hz 截止频率起步,观察波形再调。
均值滤波也可以作为备选。滑动窗口均值滤波器实现简单,缓存 N 个采样值求平均,代码量小,但会引入固定的延迟,延迟等于(N-1)/2 * dt。如果后续要做实时控制和闭环,延迟会带来相位问题,所以我更推荐一阶低通。喷气式实验平台通常用一阶低通就够,而机器人足端受力分析这种需要高保真加速度的场合,才需要更高级的滤波。
3. 用 MPU6050 源码在本地跑通位移测算的最小固件
3.1 硬件连接与 I2C 初始化的关键参数
做位移测算,MPU6050芯片本身不区分硬件平台,I2C 接口是通用方案。这里以 STM32 HAL 库为例写初始化步骤,ESP32 和 Arduino 的差别主要是 I2C 句柄不同,寄存器配置完全一致。
接线非常简单:VCC 接 3.3V,GND 接地,SCL 和 SDA 分别接 I2C 引脚,AD0 接地时器件地址是 0x68,接高电平是 0x69。很多板子默认把 AD0 拉低,代码里直接写 0x68 即可。I2C 速率我一般在 400kHz 模式下运行,MPU6050 支持到 400kHz,100kHz 在读取高频数据时容易丢数。
初始化时最关键的寄存器是电源管理 1(0x6B),必须把 DEVICE_RESET 位置位再清零,然后设置时钟源为 PLL,否则陀螺仪数据不稳定。配置代码核心部分是:
uint8_t data; // 唤醒 MPU6050,退出休眠模式 uint8_t reset_val = 0x80; HAL_I2C_Mem_Write(&hi2c1, 0x68 << 1, 0x6B, 1, &reset_val, 1, 100); HAL_Delay(100); // 设置时钟源为 PLL,禁用休眠 uint8_t pwr_val = 0x01; HAL_I2C_Mem_Write(&hi2c1, 0x68 << 1, 0x6B, 1, &pwr_val, 1, 100); // 加速度量程 ±4g,输出 16384 LSB/g uint8_t accel_cfg = 0x08; HAL_I2C_Mem_Write(&hi2c1, 0x68 << 1, 0x1C, 1, &accel_cfg, 1, 100); // 陀螺仪量程 ±1000dps,输出 32.8 LSB/dps uint8_t gyro_cfg = 0x10; HAL_I2C_Mem_Write(&hi2c1, 0x68 << 1, 0x1B, 1, &gyro_cfg, 1, 100); // 配置数字低通滤波器,带宽 21Hz,同时设置 1kHz 采样率 uint8_t dlpf_cfg = 0x05; HAL_I2C_Mem_Write(&hi2c1, 0x68 << 1, 0x1A, 1, &dlpf_cfg, 1, 100);量程选择需要解释一下。测量位移时通常运动加速度不大,±2g 和 ±4g 的量化精度不同。±2g 的分辨率是 16384 LSB/g,±4g 是 8192 LSB/g。如果做的是室内人员定位,±2g 完全够;如果做的是机械臂末端抛掷动作,瞬时加速度可能超过 3g,用 ±2g 会饱和,这时再换 ±4g 或 ±8g。MPU6050 分辨率与量程的关系是很多初学者忽略的点,量程越大分辨率越低,小位移信号会被量化噪声淹没。
DMP 配置项在这里不碰,因为 DMP 输出的是四元数,只做姿态融合,不做位移。我们的位移测算完全在 MCU 侧完成,不依赖 DMP。
3.2 原始数据读取与单位换算
初始化完成后,加速度数据通过 0x3B 到 0x40 这 6 个寄存器读出。每个轴 16 位,高低字节分开存储。读取时最好一次性读 6 个字节,避免多次 I2C 通信之间数据被更新。HAL 库的读取函数和换算逻辑如下:
uint8_t buf[6]; int16_t ax_raw, ay_raw, az_raw; float ax, ay, az; HAL_I2C_Mem_Read(&hi2c1, 0x68 << 1, 0x3B, 1, buf, 6, 100); ax_raw = (int16_t)((buf[0] << 8) | buf[1]); ay_raw = (int16_t)((buf[2] << 8) | buf[3]); az_raw = (int16_t)((buf[4] << 8) | buf[5]); // 按 ±4g 量程换算,8192 LSB/g,重力加速度取 9.8 m/s^2 float accel_scale = 9.8f / 8192.0f; ax = (float)ax_raw * accel_scale; ay = (float)ay_raw * accel_scale; az = (float)az_raw * accel_scale;注意ax_raw右移时带符号,不能直接赋值给 uint16_t,否则负数会变成正数。换算系数取决于量程寄存器的配置,如果是 ±2g 就改成 16384。这个细节在嵌入式内核源码里很容易被忽略,但出来的结果会错得莫名其妙。
3.3 姿态解算:用四元数提取重力并补偿
加速度值换算完成后,还不能直接进积分器,必须先做重力补偿。这里用的是经典 Mahony 互补滤波。这个滤波器通过修正陀螺仪积分姿态来跟踪重力方向,然后输出四元数。核心更新函数是:
void Mahony_Update(Mahony_t *imu, float gx, float gy, float gz, float ax, float ay, float az, float dt) { float q0 = imu->q0, q1 = imu->q1, q2 = imu->q2, q3 = imu->q3; float norm; float vx, vy, vz; float ex, ey, ez; // 归一化加速度计测量值 norm = sqrtf(ax * ax + ay * ay + az * az); if (norm < 0.001f) return; ax /= norm; ay /= norm; az /= norm; // 从当前四元数预测重力方向 vx = 2.0f * (q1 * q3 - q0 * q2); vy = 2.0f * (q0 * q1 + q2 * q3); vz = q0 * q0 - q1 * q1 - q2 * q2 + q3 * q3; // 叉积误差 ex = ay * vz - az * vy; ey = az * vx - ax * vz; ez = ax * vy - ay * vx; // 比例积分修正陀螺仪零漂 imu->integral_fb_x += imu->Ki * ex * dt; imu->integral_fb_y += imu->Ki * ey * dt; imu->integral_fb_z += imu->Ki * ez * dt; gx += imu->Kp * ex + imu->integral_fb_x; gy += imu->Kp * ey + imu->integral_fb_y; gz += imu->Kp * ez + imu->integral_fb_z; // 四元数微分方程更新 q0 += 0.5f * dt * (-q1 * gx - q2 * gy - q3 * gz); q1 += 0.5f * dt * ( q0 * gx + q2 * gz - q3 * gy); q2 += 0.5f * dt * ( q0 * gy - q1 * gz + q3 * gx); q3 += 0.5f * dt * ( q0 * gz + q1 * gy - q2 * gx); // 归一化四元数,防止误差累积 norm = sqrtf(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3); q0 /= norm; q1 /= norm; q2 /= norm; q3 /= norm; imu->q0 = q0; imu->q1 = q1; imu->q2 = q2; imu->q3 = q3; }这个函数的参数直接影响姿态精度。Kp是比例增益,一般取 0.5 到 1.0。运动剧烈时增大Kp可以更快跟踪真实姿态,但会把加速度计的振动噪声引入姿态估计,导致补偿后的加速度更加不干净。Ki用于消除陀螺仪常值零漂,取 0.05 到 0.2 之间,太大容易引起振荡。我把Kp=0.6、Ki=0.1作为默认值,静止场景够用,运动场景再调。
补偿加速度时,需要把世界坐标系里的重力向量[0, 0, 1]变换回机体坐标系,然后从加速度测量值中减去。这个操作在代码里的表达是:
float gx_w = 2.0f * (q1 * q3 - q0 * q2); float gy_w = 2.0f * (q0 * q1 + q2 * q3); float gz_w = q0 * q0 - q1 * q1 - q2 * q2 + q3 * q3; // 世界系重力分量(约等于 1g),变换到机体系后减去 float g_norm = sqrtf(gx_w * gx_w + gy_w * gy_w + gz_w * gz_w); float gravity_bx = gx_w / g_norm * 9.8f; float gravity_by = gy_w / g_norm * 9.8f; float gravity_bz = gz_w / g_norm * 9.8f; float acc_world_x = ax - gravity_bx; float acc_world_y = ay - gravity_by; float acc_world_z = az - gravity_bz;acc_world_*已经是扣除重力后的运动加速度,单位是 m/s^2,可以直接喂给积分器。这个步骤是位移测算源码里最容易算错的地方,因为重力补偿的坐标方向一定要和四元数定义一致。我调试时先在静止状态下看补偿后的加速度,三个轴都应该在 0 附近小幅波动,波动幅度超过 ±0.05g 说明姿态解算有问题。
4. MPU6050位移测算的误差校正:零偏、倾斜与积分漂移处理
4.1 静止零偏校正与温度漂移
MPU6050陀螺仪使用方法里,零偏校正通常被一句话带过,但它对位移测算的影响非常大。陀螺仪的零偏误差在积分后会变成姿态角误差,姿态角误差又会让重力补偿方向偏掉,导致最终加速度里残留重力分量。这个残留重力分量被双重积分后,会以0.5 * g * t^2的速度产生虚假位移。静止 30 秒,哪怕陀螺仪只有 0.1 dps 的零偏,姿态误差也能累积到 3 度,重力分量残留超过 0.05g,位移结果直接不可信。
常见的做法是上电后让传感器静止 2 秒,采集 200 个陀螺仪采样值求平均,作为陀螺仪零偏。代码片段:
float gyro_offset[3] = {0}; int sample_count = 200; for (int i = 0; i < sample_count; i++) { gyro_offset[0] += gx_raw; gyro_offset[1] += gy_raw; gyro_offset[2] += gz_raw; HAL_Delay(10); } gyro_offset[0] /= sample_count; gyro_offset[1] /= sample_count; gyro_offset[2] /= sample_count;这个校正必须在传感器完全静止时进行,探头带着电机振动时校准会把振动分量当零偏,结果更糟。另外温度漂移问题也不能忽略,MPU6050 内部虽然有温度传感器,但校准是在常温下做的,如果设备从空调房间拿到户外,零偏会变化。对位移测算来说,温度漂移通常不会在几秒内影响太大,但长时间运行后需要周期性重新校准。可以在主循环里检测温度变化超过 5 度时暂停测量,提示重新校准。
加速度计的零偏同样需要校正。静止时三轴加速度的理论值应该是[0, 0, 1g],但因为安装应力和芯片制造误差,读到的值会偏离。校正方式是静止采集后求平均,把实际值减理论值得到偏置。这个偏置保存在 Flash 或 EEPROM 里,下次上电直接加载。
4.2 零速检测与积分漂移的约束方法
双重积分最显著的坑是漂移。即使重力补偿完全正确,加速度计残余噪声也会在积分后形成随机游走,导致静止时速度不为零、位移不断增大。解决思路是在载体静止时把速度硬置为零,这就是零速检测。原理是当加速度方差小于阈值时判定为静止,然后冻结速度积分。
零速检测的代码实现:
float acc_var = 0.0f; float acc_mean = (ax + ay + az) / 3.0f; for (int i = 0; i < 10; i++) { acc_var += powf(acc_buf[i] - acc_mean, 2); } acc_var /= 10.0f; if (acc_var < 0.001f) { mi.vel_prev = 0.0f; }判定阈值要根据安装环境调。放在桌面上,加速度方差很小,阈值 0.001 可以正常工作。如果传感器安装在机械臂末端,机械臂静止时电机的微振动也会让加速度方差超过阈值,这时要么提高阈值,要么在机械臂控制器的使能信号里直接给出静止标志,比阈值判断更可靠。这个做法在足式机器人步态估计里叫零速修正,专业术语 Zero Velocity Update。
位移漂移的另一种约束是高通滤波,但这会引入新的问题。对速度做高通滤波可以滤掉低速漂移分量,但同时会滤掉真实的匀速运动信息,因为匀速运动在速度曲线里是直流分量。所以位移测算不能用速度高通滤波,只能在零速时段做修正,这是四轴飞行器和机器人领域常用的经验法则。
4.3 加速度二次积分前的数据质量检查
积分器运行前要做数据质量检查,过滤掉明显的坏点。常见坏点来源包括:I2C 读取偶发错误导致数据跳变、电机启动瞬间的电磁干扰、以及雷击般的冲击振动。一个简单有效的异常值剔除策略是计算当前采样与上一采样之间的差值,超过设定阈值就用上一个值替代:
float acc_diff = fabsf(ax - ax_prev); if (acc_diff > 4.0f) { ax = ax_prev; }阈值 4 m/s^2 表示相邻两个采样点之间加速度变化不超过 0.4g,正常运动的加速度变化率不会这么大,但冲击载荷会触发。这个策略能防止积分器被一个坏点放大成巨大的速度尖峰。注意如果运动本身很剧烈,比如摔落测试,阈值要相应调大,否则会把真实运动当成坏点滤掉,得不偿失。
数据质量检查还应该包括陀螺仪和加速度计的量程检查。若读到的加速度接近量程上限,说明发生了饱和,饱和后的数据是平台值,积分的物理意义已经不存在。此时应该记录事件并暂停积分,等加速度回落后重新初始化积分器,而不是塞入无效数据。这种做法在冲击测量和碰撞检测里很常见,但很多 MPU6050 位移测算的示例代码没有处理。
4.4 动态姿态下的位移误差边界
即使做了上述所有补偿,动态姿态下的位移误差依然存在。原因在于 Mahony 滤波器用加速度计修正姿态时,如果载体本身在加速运动,加速度计会感知到运动加速度,这会被误认为是姿态误差的一部分,导致姿态估计偏斜。这是一个根本性矛盾:姿态解算需要加速度计,但加速度计里包含运动加速度。
缓解办法是在检测到较大运动加速度时降低姿态修正系数,让陀螺仪主导姿态估计。具体做法是动态调整Kp,当加速度模长远离 1g 时减小Kp:
float acc_magnitude = sqrtf(ax * ax + ay * ay + az * az); float scale = 1.0f; if (fabsf(acc_magnitude - 9.8f) > 2.0f) { scale = 0.1f; } imu.Kp = base_kp * scale;这个策略在一些四轴姿态解算源码里叫自适应增益。它能显著减少剧烈运动时的姿态误差,但不能完全消除。所以必须明确 MPU6050 位移测算的适用边界:短时间、低动态、慢变化的运动场景,比如步行计数、电梯运行、手动推车位移,误差可以控制在 5% 到 10%;而剧烈甩动、高频振动、长时间连续运动,误差会快速累积,单纯靠算法已经无力回天。这个边界不能写到标题外面去,但每个用的人心里要有数。
5. 位移测算值得一试的技巧:滑动窗口基线补偿与触发式积分
位移测算做到最后,比拼的往往是细节。这里分享两个我在调试中实际受用的技巧,它们不复杂,但对位移曲线的可用性有质的提升。
第一个技巧是滑动窗口基线补偿。加速度计的零偏会随着温度和时间缓慢变化,固定补偿值不够用。我维护一个 200 个采样点的环形缓冲区,只记录静止时刻的加速度值,持续计算其均值。当检测到零速时,用当前均值更新零偏。这样即使温度漂移导致零偏缓慢变化,补偿值也会跟着走。实现要点是缓冲区在静止时更新,运动时不更新,避免把运动加速度混进基线。代码上只需在零速检测分支里调用:
if (is_stationary) { baseline_buf[baseline_idx] = ax; baseline_idx = (baseline_idx + 1) % BASELINE_WIN; baseline_ax = average(baseline_buf, BASELINE_WIN); }第二个技巧是触发式积分,也叫起始点对齐。很多位移应用关心的是从按下按键到运动结束这一段相对位移,而不是从开机开始的绝对位移。如果在运动开始前就持续积分,零速修正前的微小漂移已经给位移叠加了一个未知常数,后面的结果会带着这个偏差。我的做法是设定加速度阈值触发,当补偿后加速度超过 0.1 m/s^2 时清零速度积分器并开始记录位移,同时在上升沿打一个时间戳。这样得到的就是从静止到静止的净位移,不受历史漂移影响。注意触发后要留几毫秒的预采样,以便准确捕捉触发点。
第三个值得一试的是把位移测算的结果做可视化验证。最常见的验证方法是让传感器在导轨上匀速往返,用激光测距仪或编码器作为参考,把估算位移和真实位移同时画出来。如果没有检测设备,可以做一个简单的甩臂实验:摆臂从静止开始加速到最高点再回到静止,理论上净位移为零,观察积分位移的终点漂移量。这个实验不需要额外硬件,只需要串口把位移数据打印出来就行,我每次调参数都会先跑这个实验。位移曲线终点越过零点的幅度越小,说明参数越合理。最后提醒一点,位移数据流打印到串口时注意波特率,115200 在 100Hz 下输出三个浮点数基本够用,更高频率建议用二进制传输,否则阻塞严重会拖慢采样周期。
把这两套技巧和前面几章的滤波、姿态解算、零速修正组合起来,一套完整的MPU6050位移测算源码就能落地了。调试时先静态后动态,先低速后高速,先短距离后长距离,一步一步把每个环节的误差量化出来,才能真正做到“完整下载即用”。
本文还有配套的精品资源,点击获取