尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32+MPU6050数据滤波指南:从滑动窗口到互补滤波,稳住你的传感器数据

STM32+MPU6050数据滤波指南:从滑动窗口到互补滤波,稳住你的传感器数据 MPU6050这芯片在嵌入式圈子里几乎是无人不知做平衡车、四轴、手势识别、计步器十个项目里八个都能看到它的身影。但很多人第一次调它的时候都会遇到同一个灵魂拷问为什么我读出来的数据抖得像帕金森一样加速度计静止放着数值还在上下乱跳陀螺仪不动的时候角速度也时不时给你飘出一个非零值。这时候就需要滤波了而且是必须滤波。这篇教程我基于自己实际调试的经验把STM32MPU6050的滤波思路从头到尾捋一遍。不讲废话直接从“为什么有噪声”“用什么算法”“代码怎么写”“参数怎么调”四个维度展开把常用的滑动窗口滤波、一阶低通滤波、互补滤波全部讲透最后补充一些你很难在文档里查到的调试心得。搞完这一套你手里的MPU6050基本就能做到“稳如老狗”。1. 为什么MPU6050的数据必须滤波1.1 原始数据里的五种魔鬼先搞清楚一个问题MPU6050本身是一颗六轴惯性测量单元内部集成了三轴MEMS加速度计和三轴MEMS陀螺仪外加一个温度传感器。它输出的数据不是出厂就干净整洁的里面至少夹杂着以下几种噪声和干扰第一是机械振动。这几乎是所有MEMS传感器都躲不掉的。芯片内部的微机械结构在振动环境下会产生共振或者附加加速度反映到读数上就是高频抖动。你拿手轻轻敲一下桌面看串口打印出来的加速度值能明显看到毛刺。第二是电源纹波。MPU6050的模拟电源和数字电源如果没有做好去耦电源上的高频噪声会直接耦合到传感器输出上。尤其是你如果用STM32的3.3V引脚直接给模块供电而主板上还有电机、舵机这种大功率外设那电源纹波绝对会让你的MPU6050读数雪上加霜。第三是ADC量化噪声。MPU6050内部是16位ADC量程设成±2g的时候分辨率大约0.000061g/LSB听着很高对吧但实际读出来的数据末位几位经常在不稳定地跳变这就是量化噪声和参考电压波动共同作用的结果。第四是温度漂移。这个主要影响陀螺仪。MEMS陀螺仪的零偏会随温度变化开机那一会儿和运行半小时之后静止状态的陀螺仪读数可能差了十几个LSB。很多人问为什么校准完还是有漂移大概率就是因为没考虑温度因素。第五是采样过程中的随机噪声。I2C读取时序不稳定、DMA传输打断、外部电磁干扰这些都可能在数据链路上引入随机错误。那是不是说MPU6050就不好用恰恰相反这颗芯片的性价比和稳定性在消费级IMU里算相当优秀的。只是它输出的原始数据是“物理量噪声”的混合体需要用软件手段把噪声剥离掉。1.2 你要的到底是滤波还是姿态解算在学习滤波之前先想明白一个问题你拿到MPU6050的数据之后要做什么如果只是做简单的角度测量或者震动检测那加速度计的原始数据经过低通滤波就能直接用。如果你是做平衡车、四轴飞行器、机器人姿态控制那你的核心诉求不是滤波而是“姿态解算”——从加速度计和陀螺仪的原始数据中提取出当前设备的横滚角、俯仰角、偏航角。这种情况下滤波只是姿态解算过程中的一个环节你真正需要的是融合算法比如互补滤波、卡尔曼滤波、Mahony算法。我给你打个比方滤波就是戴降噪耳机听音乐姿态解算是从混合了雨声、风声、脚步声的录音里判断那个人是朝你走过来还是走远。降噪耳机解决的是音质问题声源定位解决的是方向问题。很多人拿着滤波算法去做姿态解算或者拿着姿态解算算法去处理原始数据方向完全搞反了效果自然不理想。所以这篇文章我分两条线走一条线讲数据滤波用于减少原始数据的噪声一条线讲姿态解算用于从滤波后的数据中提取姿态角。两条线配合使用才能发挥MPU6050这个六轴传感器的完整价值。2. 滤波算法怎么选从滑动窗口到互补滤波2.1 滑动窗口均值滤波最简单但别小看滑动窗口均值滤波也叫移动平均滤波算法逻辑一句话就能说清维护一个固定长度的数据缓冲区每来一个新数据就把它加入队尾同时移除队首最老的数据然后对当前缓冲区里的所有数据取平均作为本次的输出值。这个算法最大的优点就是“简单直接、不费脑、跑得贼快”。在STM32这种主频72MHzF103或者168MHzF407的单片机上开一个长度为10~20的窗口CPU开销几乎可以忽略。对于加速度计这种高频随机噪声滑动窗口滤波可以把毛刺削减得非常明显。但它的缺点也很明显一是延迟窗口越长输出信号的滞后越严重实时性变差。你在做平衡车的时候如果姿态角滞后了50毫秒那整个控制环路的相位裕度就被吃掉了系统很容易振荡。二是它对突发性异常值非常敏感一个极大的尖峰脉冲会让整个窗口的平均值被拉偏。我个人的习惯是滑动窗口滤波只用于短窗口的预处理比如长度取4~8用来把高频毛刺先压一下后面再配合一阶低通或者姿态解算算法做进一步处理而不是单独依赖它。另外滑动窗口还有一个变体叫“中值滤波”把窗口内的数据排序后取中间值对脉冲噪声的抑制效果比平均值好得多。如果你发现数据里偶发几个特别离谱的突变点优先考虑中值滤波。2.2 一阶低通滤波性价比之王一阶低通滤波的公式就一行output alpha * input (1 - alpha) * previous_output;alpha的取值在0到1之间决定了对新数据的信任程度。alpha越大滤波越“轻”响应越快但滤噪效果弱alpha越小滤波越“重”数据越平滑但滞后越大。这个算法的本质是对历史输出和当前输入做加权平均相当于给信号加了一个RC低通滤波器。它的好处是只需要两个变量、一次乘法、一次加法代码量极小而且每次有新的采样数据就能实时更新不需要维护一个数组。在STM32上做高频采样比如1kHz的时候极其合适。alpha怎么选可以通过截止频率来换算。一阶低通滤波器的截止频率和alpha的关系近似为alpha 1 - exp(-2 * PI * fc / fs)其中fs是采样频率fc是你期望的截止频率。采样频率固定之后你只需要决定想把多少Hz以上的噪声滤掉。比如采样频率500Hz想滤掉20Hz以上的振动噪声代入公式算出来alpha大约0.23。不过实际调试中我不会死磕公式一般是先用公式算个初值然后直接看波形调。这个后面章节详细说。2.3 卡尔曼与互补滤波姿态解算的进阶玩法如果你要的是姿态角那光滤原始数据是不够的必须做传感器融合。这里我重点说两种方案互补滤波和卡尔曼滤波。卡尔曼滤波是理论上的“最优估计”算法通过建立系统的状态方程和观测方程融合陀螺仪的高频特性和加速度计的低频特性估算出当前的真实姿态。它效果好但实现起来公式复杂需要调协方差矩阵对初学者不太友好。STM32上跑一个简化版本的卡尔曼滤波是没问题的但调试成本比较高。互补滤波则巧妙得多。它利用陀螺仪和加速度计在频域上的互补特性——陀螺仪动态响应快但会漂移低频噪声严重加速度计没有长期漂移但高频噪声大——将两者按比例融合陀螺仪的积分结果经过高通滤波加速度计的测量值经过低通滤波相加得到最终的姿态角估计。互补滤波的经典形式是angle 0.98 * (angle gyro_dt * gyro_rate) 0.02 * accel_angle;这里的0.98和0.02就是权重系数也叫互补系数决定了你是更相信陀螺仪还是更相信加速度计。这个算法实现简单实时性好在STM32上跑毫无压力是中小型嵌入式项目的首选姿态解算方案。老实说对绝大多数STM32项目来说互补滤波的性能已经足够了。卡尔曼滤波的“最优”确实在理论上更漂亮但在实际嵌入式环境中系统模型不准、噪声统计特性未知卡尔曼反而不如调好参数的互补滤波稳。2.4 滤波算法选型对比算法复杂度延迟实时性适用场景滑动窗口均值极低随窗口长度增加高原始数据预处理中值滤波低随窗口长度增加中抑制脉冲尖峰一阶低通极低低极高实时数据平滑互补滤波低低高姿态角解算卡尔曼滤波高低中高精度姿态估计3. STM32实战代码一步一步写3.1 数据读取与原始数据预处理在写滤波代码之前先确保你能通过I2C正常读取MPU6050的原始数据。这里我以STM32F103C8T6搭配HAL库为例用的是标准I2C接口读取MPU6050的加速度和角速度原始值然后转换成物理单位。#include i2c.h #include mpu6050.h #define MPU6050_ADDR 0xD0 // 模块通常为0xD0根据AD0引脚不同可为0xD2 #define MPU6050_ACCEL_XOUT_H 0x3B #define MPU6050_GYRO_XOUT_H 0x43 int16_t accel_raw[3]; int16_t gyro_raw[3]; void MPU6050_Init(void) { uint8_t data 0x00; // 退出睡眠模式 data 0x00; HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR, 0x6B, I2C_MEMADD_SIZE_8BIT, data, 1, 100); // 设置加速度计量程为±4g寄存器1C对应加速度分辨率16384 LSB/g data 0x08; HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR, 0x1C, I2C_MEMADD_SIZE_8BIT, data, 1, 100); // 设置陀螺仪量程为±500dps寄存器1B对应角速度分辨率65.5 LSB/dps data 0x08; HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR, 0x1B, I2C_MEMADD_SIZE_8BIT, data, 1, 100); // 设置数字低通滤波(DLPF)带宽为94Hz寄存器1A 0x02 data 0x02; HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR, 0x1A, I2C_MEMADD_SIZE_8BIT, data, 1, 100); } void MPU6050_ReadRaw(void) { uint8_t buf[14]; HAL_I2C_Mem_Read(hi2c1, MPU6050_ADDR, MPU6050_ACCEL_XOUT_H, I2C_MEMADD_SIZE_8BIT, buf, 14, 100); accel_raw[0] (int16_t)((buf[0] 8) | buf[1]); accel_raw[1] (int16_t)((buf[2] 8) | buf[3]); accel_raw[2] (int16_t)((buf[4] 8) | buf[5]); gyro_raw[0] (int16_t)((buf[8] 8) | buf[9]); gyro_raw[1] (int16_t)((buf[10] 8) | buf[11]); gyro_raw[2] (int16_t)((buf[12] 8) | buf[13]); }这里提醒三个细节第一I2C读取尽量一次读出14个字节不要一个字一个字读。MPU6050内部的寄存器地址是自动递增的从0x3B开始一次读14字节一次就能把加速度、温度、陀螺仪全部读回来效率高得多。第二加速度和陀螺仪的量程配置直接决定后续计算公式里的缩放系数。我用±4g和±500dps加速度满量程对应的LSB值是8192角速度满量程对应的LSB值是65.5。这个系数换算错一个后面所有的仪表数据都是错的。第三MPU6050内部其实自带一个数字低通滤波器寄存器的配置会改变传感器输出的噪声特性。初始阶段可以把它设为94Hz带宽滤掉大部分高频噪声后面的软件滤波压力就小很多。3.2 滑动窗口滤波的实现滑动窗口滤波的实现有两种方式一种是循环队列一种是“数组整体移位”。循环队列效率高但代码稍微绕一点数组移位写起来直观但每次插入删除都要移动数据耗时长。在STM32这种资源有限的MCU上我推荐用环形缓冲区实现。#define WINDOW_SIZE 8 typedef struct { int16_t buffer[WINDOW_SIZE]; uint8_t index; uint8_t count; int32_t sum; } SlidingWindow_t; void SlidingWindow_Init(SlidingWindow_t *sw) { sw-index 0; sw-count 0; sw-sum 0; memset(sw-buffer, 0, sizeof(sw-buffer)); } int16_t SlidingWindow_Filter(SlidingWindow_t *sw, int16_t new_value) { // 如果窗口已满先减去被覆盖的旧值 if (sw-count WINDOW_SIZE) { sw-sum - sw-buffer[sw-index]; } else { sw-count; } // 写入新值并更新和 sw-buffer[sw-index] new_value; sw-sum new_value; sw-index (sw-index 1) % WINDOW_SIZE; // 返回平均值 return (int16_t)(sw-sum / sw-count); }这个实现的核心是维护了一个sum变量每次只做一次减法和一次加法不需要每次重新遍历整个缓冲区求和效率很高。用取模运算实现环形索引写起来简单读起来也直观。窗口长度WINDOW_SIZE的选择要根据实际应用来定如果你只需要平滑显示窗口可以取到16甚至32如果你要用于闭环控制窗口尽量控制在4~8否则滞后太大。还有一个容易被忽略的细节这个滑动窗口滤波是基于整数运算的sum累加时用了int32_t防止多个int16_t数据相加时溢出。初学的人很容易在这里踩坑——窗口一长sum直接爆掉输出的数据全是乱的。3.3 一阶低通滤波的实现一阶低通滤波的代码更简单但是有一个关键变量需要处理好那就是上次输出值的初始化。typedef struct { float alpha; float previous_output; } LowPass_t; void LowPass_Init(LowPass_t *lp, float alpha) { lp-alpha alpha; lp-previous_output 0.0f; } float LowPass_Process(LowPass_t *lp, float input) { lp-previous_output lp-alpha * input (1.0f - lp-alpha) * lp-previous_output; return lp-previous_output; }alpha取值建议在0.1到0.5之间。小于0.1滤波太重数据被削得几乎没有动态响应大于0.5滤波太轻噪声压不住。我实际测试中常用的采样频率是500Hzalpha取0.2左右已经能把MPU6050加速度计的高频抖动压制到±0.02g以内。这里要特别强调一个工程经验这个滤波器处理浮点运算是没问题的STM32F103虽然没有硬件FPU但三轴数据各做一次一阶滤波才几十个周期CPU完全可以扛住。如果你用的是更高端的F4系列那更是毫无压力。另外一阶低通滤波器可以用来处理角速度积分前的数据。陀螺仪积分很容易漂移先用一阶低通滤掉高频噪声再去做积分能明显降低静态漂移量。3.4 互补滤波的姿态解算核心姿态解算是MPU6050滤波的终极目的之一。我直接贴一套经过实测的互补滤波代码用于计算横滚角roll和俯仰角pitch。typedef struct { float roll; float pitch; float yaw; } EulerAngle_t; EulerAngle_t att_angle; float dt 0.002f; // 采样周期 2ms对应500Hz float complementary_coefficient 0.98f; void Attitude_Update(float gx, float gy, float gz, float ax, float ay, float az) { // 从加速度计计算角度 float accel_roll atan2f(ay, az) * 180.0f / PI; float accel_pitch atan2f(-ax, sqrtf(ay * ay az * az)) * 180.0f / PI; // 陀螺仪积分 att_angle.roll gx * dt; att_angle.pitch gy * dt; // 互补融合陀螺仪积分占主导加速度计修正低频漂移 att_angle.roll complementary_coefficient * att_angle.roll (1.0f - complementary_coefficient) * accel_roll; att_angle.pitch complementary_coefficient * att_angle.pitch (1.0f - complementary_coefficient) * accel_pitch; }注意几个细节第一陀螺仪数据gx、gy必须已经转成度每秒dps也就是原始值除以灵敏度系数65.5。加速度数据ax、ay、az必须已经转成g单位也就是原始值除以8192。如果不做单位换算融合出来的角度完全是错的。第二atan2f函数在数学库中需要链接libm在使用STM32CubeIDE时默认会包含但如果用Makefile或者裸机工程记得在链接阶段加上-lm选项。第三互补系数0.98表示最终角度98%来自陀螺仪积分2%来自加速度计解算。这意味着只要加速度计不被大幅加速干扰陀螺仪的短期积分漂移就会被持续拉回真实值。第四这个简单的互补滤波没有处理偏航角yaw。原因很简单MPU6050没有磁力计仅靠陀螺仪积分出来的yaw会随着时间持续漂移加速度计无法提供水平方向的绝对参照。如果你的应用需要稳定的yaw必须再加一颗磁力计或者用MPU9250这种九轴芯片。4. 调参心法让波形说话4.1 用串口把数据导出来写完了滤波算法下一步就是验证效果。在嵌入式世界里没有示波器的时候串口就是你的示波器。我建议把滤波前后的数据一起打印出来用上位机画成波形对比这是最直观的调试方式。在STM32上我通常用DMA串口空闲中断的方式以100Hz的频率上报数据。打印格式用CSV方便直接用串口助手或者Python脚本解析。printf(%d,%d,%d,%d\r\n, accel_raw[0], accel_raw[1], filtered_ax, filtered_ay);如果你手头有匿名上位机或者其他支持波形显示的工具也可以直接把数据打包往上发实时观察波形变化效率更高。我个人习惯先用串口把数据导到PC上用Python脚本画图确认滤波效果再决定要不要调参。4.2 常见问题速查表下面这几个问题是我在做STM32MPU6050过程中遇到的也经常被网友问到整理成一个速查表供你排查现象常见原因解决办法I2C读不到MPU6050地址配置错误、接线松动检查AD0引脚电平扫描I2C地址加速度数值一直为0未退出睡眠模式写0x6B寄存器为0滤波后数据依然很乱采样频率和滤波参数不匹配降低截止频率或增大窗口滤波后角度滞后明显窗口过长或alpha过小缩短窗口长度、增加alpha静止时roll/pitch缓慢漂移陀螺仪零偏未校准上电后静态采样求均值减去零偏剧烈运动时角度跳变加速度计受运动加速度干扰增大互补系数或加入运动检测逻辑还有一个我踩过很多次的坑MPU6050模块板载的稳压芯片如果质量一般3.3V供电电压纹波会比较大导致测量数据漂浮不定。这种情况下你换什么滤波算法都白搭。解决方法是给MPU6050单独加一颗LDO或者至少在供电引脚旁边并联10uF0.1uF的去耦电容效果立竿见影。4.3 我的调参心得和一些经验总结最后分享一些我自己实际调试出来的经验这些在芯片手册和教程里很难找到第一滤波参数永远不要孤立调试一定要结合采样频率来定。很多同学在500Hz采样下用alpha0.5的滤波效果一塌糊涂然后怀疑算法有问题。实际上采样频率一改同样的alpha对应的截止频率就完全变了。你只有先确定采样频率才有讨论滤波参数的意义。第二在做姿态解算的时候一定要先用低速旋转测试验证算法的正确性再上平衡车或者四轴。怎么验证把MPU6050固定在一个转台上手动旋转一个已知角度比如90度看解算出来的姿态角是否准确。如果在低速下角度都测不准高速动态下更不会准。第三滤波算法不是越多越好。有些人一上来就是“滑动窗口一阶低通中值卡尔曼”四件套全上结果数据延迟大得根本没法控制。合理的设计是先通过硬件和传感器内部滤波把大部分高频噪声去掉软件里最多叠两级滤波保证数据质量和实时性的平衡。第四如果你用的是MPU6050模块模块上通常会有一个SCL/SDA上拉电阻和旁路电容。但你要是直接买裸芯片自己画板上拉电阻一定要加默认I2C总线需要上拉到VCC否则通信极不稳定。我用过一次没加上拉的板子I2C时好时坏排查了很久才找到原因。第五别忘了定期校零。陀螺仪的零偏并不是永远不变的温度变化、电源波动都会让零偏飘移。比较实用的做法是每次上电初始化时让设备静止1秒钟采样几十组陀螺仪数据求平均把这个平均值作为零偏存下来之后每次读取都减去这个零偏。这个操作只需要几行代码但对漂移的抑制效果非常好。我在实际使用中最大的感受是滤波这件事功夫在算法之外。硬件设计、供电质量、采样时序、单位换算每一个环节都会影响最终效果。很多人在滤波算法上反复折腾却忽略了源头上的问题。先保证原始数据质量合格再谈软件滤波这样才能事半功倍。如果你也正在调MPU6050滤波按照这篇文章的思路一步步来应该能少走不少弯路。
返回列表