拓冰建站拓冰建站
首页 / 资讯中心 / 正文

stm32+mpu6050滤波教程:从滑动窗口到互补滤波的完整实践

做stm32和mpu6050项目的朋友十有八九都会被一个问题折磨——明明传感器买的是正经模块读取流程也照着例程写了串口打印出来的数据就是抖得像帕金森。尤其是加速度计和陀螺仪原始值静止放在桌面上波形图都能画出一片毛刺山。这篇博文就围绕stm32_mpu6050_滤波教程这个主题把我从零开始做滤波的完整思路、代码实现、踩坑记录一次讲清楚。内容覆盖滑动窗口滤波、一阶低通滤波、互补滤波姿态解算以及工程实战中的调试方法适配刚接触姿态传感器的新手也给正在调参的老手提供一些可参考的排查思路。先说清楚这篇文章能解决什么。很多人拿到mpu6050第一件事就是直接读原始数据算角度结果发现角度值跳得离谱于是开始怀疑硬件坏了、I2C时序不对、电源纹波大。实际上绝大部分情况下问题出在数据没做滤波处理。传感器输出的信号里既有高频噪声也有运动产生的有效信号不加区分地全盘接收自然拿不到干净的数据。本文会从滤波原理讲起逐步给出可复现的代码和调试方法最终目标是你照着做能让mpu6050的输出在静止时纹波小于正负0.5度运动时跟手不滞后。1. 为什么mpu6050的数据必须做滤波处理1.1 传感器数据特征与主要噪声来源mpu6050内部集成了三轴加速度计和三轴陀螺仪加速度计测量比力陀螺仪测量角速度。听起来很直接但真实输出远没有理想中干净。加速度计对振动极其敏感电机转动、桌面震动、甚至手轻轻敲一下外壳都会在数据上叠加明显的高频毛刺。陀螺仪则有零偏漂移问题静止时输出不为零积分之后角度会缓慢飘走。从信号构成的角度看可以简单分两类一类是运动产生的低频有效信号一类是振动、电磁干扰、量化误差带来的高频噪声。滤波的任务就是尽可能保留前者、抑制后者。这里最关键的一点是滤波不是越多越好、越平越好过度滤波会让数据变得“迟钝”运动时滞后明显反而影响姿态反馈的实时性。1.2 不滤波直接使用的典型后果拿我早期做的一个两轮平衡车项目举例。当时偷懒没加滤波直接把加速度计数据算成倾角送去PID控制结果是车子静止时电机高频抖动噪声被控制环路放大之后整个系统都在“发抖”调了好几天的PID参数都没救回来。后来把原始数据打出来看才明白静止时加速度计输出波动可以达到正负0.1g换算成角度就是好几度的偏差。姿态解算场景更明显。用陀螺仪积分算角度原始数据里带零偏噪声积分误差随时间累积几秒钟之后角度就开始乱飘。用加速度计算角度替代又会被振动噪声带偏。这类问题的标准解法就是滤波先对原始信号做低通处理再做姿态融合各取所长。1.3 滤波方案的整体选型思路现阶段主流的做法大致分成两条路。第一类是纯软件滤波算法比如滑动窗口滤波、一阶低通滤波、限幅滤波这些算法轻量、容易理解、适合在stm32这类单片机上裸跑。第二类是姿态融合算法比如互补滤波、Mahony滤波、卡尔曼滤波它们把加速度计和陀螺仪的数据融合起来直接输出姿态角。选哪种取决于项目需求。如果你只是读取原始加速度或角速度做触发判断用一阶低通或者滑动窗口就够了。如果你要得到稳定的角度值做闭环控制推荐互补滤波计算量小、效果稳定。卡尔曼滤波理论上最优但在资源有限的stm32上实现起来代码量大参数调起来也费时优先级可以往后放。2. 项目准备与环境搭建2.1 硬件选型与接线方案做滤波实验的硬件要求不高。主控用最常见的stm32f103c8t6最小系统板价格便宜、资料多完全满足运算需求。传感器用mpu6050模块市面上带稳压和电平转换的GY-521模块即可注意买的时候选供电3.3V到5V兼容的版本避免电平不匹配烧坏传感器。接线非常简单。mpu6050默认走I2C总线只需要四根线VCC接电源、GND接地、SCL接PB6、SDA接PB7。我用的是stm32f103c8t6的硬件I2C1对应引脚正好是PB6和PB7。如果你更习惯软件模拟I2C任意两个GPIO口都能工作只是时序稳定性稍差。另外建议在VCC和GND之间并联一个100nF去耦电容实测能减小一部分电源高频噪声。2.2 开发环境与工程配置工程模板我用的是STM32CubeMX生成的HAL库工程配合Keil MDK编译。选择HAL库的原因是代码可读性好不需要像标准库那样手动管理寄存器适合把精力集中在滤波算法上。CubeMX配置时只需要开启I2C1、USART1用于打印数据再配置一个定时器用于定时采样。有一个容易被忽略的细节I2C速率不要拉太高默认100KHz标准模式就行。mpu6050虽然支持400KHz快速模式但很多模块的引脚上拉电阻值不太合适高速模式容易通信失败白费半天排查时间。2.3 基础读取工程实现在写滤波算法之前先确保能稳定读到原始数据。mpu6050的寄存器操作比较规律初始化时依次写入电源管理寄存器、采样率分频寄存器、配置寄存器和各量程寄存器。读取时通过I2C连续读取加速度计和陀螺仪的六个数据寄存器即可。// 初始化mpu6050基本配置 void MPU6050_Init(void) { uint8_t val; // 唤醒传感器退出睡眠模式 val 0x00; HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR, MPU6050_PWR_MGMT_1, 1, val, 1, 100); // 配置采样率分频采样率1kHz val 0x07; HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR, MPU6050_SMPLRT_DIV, 1, val, 1, 100); // 配置加速度计量程为正负4g val 0x08; HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR, MPU6050_ACCEL_CONFIG, 1, val, 1, 100); // 配置陀螺仪量程为正负500度/秒 val 0x08; HAL_I2C_Mem_Write(hi2c1, MPU6050_ADDR, MPU6050_GYRO_CONFIG, 1, val, 1, 100); }数据读取注意一点寄存器里存的是16位有符号数高字节在前。读出来之后要合并成int16_t类型然后根据量程换算成实际物理值。加速度计量程正负4g时灵敏度为8192 LSB/g陀螺仪量程正负500度/秒时灵敏度为65.5 LSB/度/秒。换算这一步别省滤波参数和后续分析都依赖真实物理值。3. 从零手写四种主流滤波算法3.1 滑动窗口滤波的原理与代码实现滑动窗口滤波是我最先尝试的算法原理非常朴素维护一个固定长度的缓冲区每来一个新数据就丢弃最旧的数据取窗口内所有数据的平均值作为输出。它本质上是FIR低通滤波的一种简化形式能有效平滑周期性噪声和随机毛刺。窗口长度是核心参数。窗口太短滤波效果不明显窗口太长数据延迟变大。针对mpu6050在1kHz采样率下的输出我测试下来窗口长度取10到20比较合适。以10点窗口为例引入的延迟大约是10毫秒对于大部分姿态控制应用完全能接受。// 滑动窗口滤波适用长度为10的窗口 #define FILTER_WINDOW_SIZE 10 float SlidingWindowFilter(float newValue) { static float buffer[FILTER_WINDOW_SIZE] {0}; static uint8_t index 0; static uint8_t count 0; float sum 0.0f; uint8_t i; buffer[index] newValue; index (index 1) % FILTER_WINDOW_SIZE; if (count FILTER_WINDOW_SIZE) count; for (i 0; i count; i) { sum buffer[i]; } return sum / (float)count; }这里有个细节值得说明窗口没填满时直接除以窗口长度会导致输出偏小。上面的代码用count记录已填充的数据个数不满时除以实际个数避免启动阶段出现奇怪的跳变。3.2 一阶低通滤波的原理与代码实现滑动窗口滤波需要维护数组占用内存多而且对突发毛刺的响应并不理想。一阶低通滤波则用一个简单的递推公式完成同样的平滑任务当前输出等于上一次输出乘以权重加上当前输入乘以另一个权重。它对应模拟RC低通滤波电路的离散化形式这也是为什么很多人把它叫作软件低通滤波。一阶低通滤波只有一个参数alpha取值范围0到1。alpha越大平滑效果越强但响应越慢alpha越小响应越快但噪声抑制能力越差。alpha的物理含义是当前采样值在滤波结果中的权重可以理解为“新鲜度”。实际项目中我把alpha设为0.2到0.4之间既能压住噪声又不会让运动响应明显滞后。// 一阶低通滤波alpha代表当前数据的权重 float FirstOrderLowPassFilter(float newValue, float alpha) { static float filteredValue 0.0f; filteredValue alpha * newValue (1.0f - alpha) * filteredValue; return filteredValue; }这里有一个新手常犯的错误滤波结果初始化的时机。如果静态变量初始化为0系统上电后前几次输出是从0慢慢爬升到真实值的表现为启动时曲线先拉一条长长的斜线再趋于平稳。解决办法是在初始化阶段直接把第一次采样值赋给滤波输出或者允许滤波结果在头几个周期内快速收敛。3.3 互补滤波在姿态解算中的典型应用单独处理加速度计或陀螺仪数据只能得到干净的原始信号还不是姿态角。要得到稳定的角度我推荐互补滤波。它的核心思路是加速度计在低频段准确适合长期稳态但高频段噪声大陀螺仪在高频段响应快适合短时动态但积分会产生漂移。互补滤波用高通和低通组合的方式把两者的优势拼在一起。具体到mpu6050互补滤波的输入有两个由加速度计计算出的俯仰角和横滚角以及由陀螺仪角速度积分得到的角度增量。公式不复杂融合角度等于上一次融合角度加上陀螺仪积分值乘以权重再加上加速度计计算角度与上一次融合角度差值乘以权重。权重比例一般取0.98比0.02也就是98%信任陀螺仪短期积分2%信任加速度计长期校准。// 互补滤波accAngle由加速度计计算gyroRate是陀螺仪角速度dt是采样周期 float ComplementaryFilter(float accAngle, float gyroRate, float dt, float tau) { float alpha tau / (tau dt); static float fusedAngle 0.0f; fusedAngle alpha * (fusedAngle gyroRate * dt) (1.0f - alpha) * accAngle; return fusedAngle; }代码里的tau是时间常数决定两种数据源的切换频率。tau越大越信任陀螺仪角度越平滑但长期漂移明显tau越小越信任加速度计跟踪越快但越容易受振动影响。这个参数必须根据实际采样周期dt来调整不能一拍脑袋写死。3.4 裁剪版卡尔曼滤波的简化实现卡尔曼滤波在姿态解算领域被很多人当成“银弹”但完整实现起来比较复杂还要调好几个协方差矩阵参数。这里给一个裁剪到一维角度的简化版本只估计角度和角速度零偏两个状态对于很多项目已经够用。// 一维角度卡尔曼滤波简化的角度估计实现 typedef struct { float Q_angle; // 角度噪声方差 float Q_bias; // 角速度零偏噪声方差 float R_measure; // 测量噪声方差 float angle; // 角度估计值 float bias; // 角速度零偏估计值 float P[2][2]; // 误差协方差矩阵 } KalmanFilter_t; float Kalman_GetAngle(KalmanFilter_t *kf, float newAngle, float newRate, float dt) { // 预测阶段 kf-angle dt * (newRate - kf-bias); kf-P[0][0] dt * (dt * kf-P[1][1] - kf-P[0][1] - kf-P[1][0] kf-Q_angle); kf-P[0][1] - dt * kf-P[1][1]; kf-P[1][0] - dt * kf-P[1][1]; kf-P[1][1] kf-Q_bias * dt; // 更新阶段 float S kf-P[0][0] kf-R_measure; float K[2]; K[0] kf-P[0][0] / S; K[1] kf-P[1][0] / S; float y newAngle - kf-angle; kf-angle K[0] * y; kf-bias K[1] * y; float P00_temp kf-P[0][0]; float P01_temp kf-P[0][1]; kf-P[0][0] - K[0] * P00_temp; kf-P[0][1] - K[0] * P01_temp; kf-P[1][0] - K[1] * P00_temp; kf-P[1][1] - K[1] * P01_temp; return kf-angle; }三个方差参数需要根据实际数据调整。Q_angle影响对角度变化的信任程度Q_bias影响对零偏变化的响应R_measure则是加速度计测量噪声的方差。我一般先给R_measure设一个较大的初值因为加速度计在动态下噪声确实大Q_angle和Q_bias从小到大试在静态稳定性与动态响应之间找平衡。3.5 四种算法放在一起怎么选上面四种算法不是互斥关系。滑动窗口和一阶低通属于信号预处理层用来清理原始数据。互补滤波和卡尔曼滤波属于姿态融合层负责把两组传感器数据变成角度。完整的处理链路可以是原始加速度计数据经过一阶低通滤波去除振动毛刺原始陀螺仪数据经过滑动窗口滤波减少跳变再把两者送入互补滤波计算角度。这样层次分明每个环节的任务单一排查问题时也容易定位。从资源占用角度看滑动窗口和一阶低通几乎不消耗额外算力stm32f103跑起来毫无压力。互补滤波也只是几次浮点乘加运算。卡尔曼滤波涉及矩阵运算浮点计算量稍大但按照上面的裁剪版实现单次运算也在几十微秒级别完全不影响主循环。4. 实操以stm32f103c8t6为核心的滤波效果对比4.1 测试方案与数据采集方法这一步是筛选滤波参数的实验基础。先把滤波前的原始数据完整保存下来再分别用不同算法处理最后对比波形。我这里用USART每10毫秒发送一帧数据到串口助手数据格式设计为“原始值,滑动窗口值,一阶低通值,互补滤波角度”逗号分隔方便直接导入Excel或者用串口绘图工具看曲线。注意数据的采样节奏要稳定。我配置定时器中断每5毫秒触发一次中断里读取mpu6050并依次执行滤波算法然后把结果写入发送缓冲区。如果在主循环里用延时控制采样延时受其他任务影响会忽长忽短滤波效果对比就不公平了。4.2 静止状态下的噪声抑制效果把模块平放在桌面上先记录一分钟原始数据。加速度计Z轴的原始波动大概在正负0.08g左右陀螺仪X轴的原始波动在正负2度/秒左右。这个噪声水平其实不算差但用来算角度就不够看了。换上滑动窗口滤波后加速度计输出波动缩小到正负0.02g。换上一阶低通滤波alpha取0.3时效果接近波动也是正负0.02g级别但波形比滑动窗口更平滑没有窗口平均的那种“台阶感”。两种算法都能显著压住振动噪声肉眼几乎看不出区别。到了角度层面差异开始显现。直接由加速度计计算得到的倾角静止时波动范围是正负2度。经过互补滤波融合后波动被压到正负0.3度以内波形是一条很干净的直线加上微弱的噪声。卡尔曼滤波的静态效果和互补滤波相当区别在于卡尔曼滤波从零漂移恢复到真实角度值更快。静止场景下两者都是可以接受的。4.3 运动状态下的响应速度对比滤波不能只看静态效果动态响应同样关键。我做了一个简单测试把传感器绕Y轴快速转90度然后用串口绘图工具观察角度曲线从开始变化到稳定所需的时间。不滤波的原始角度响应几乎是瞬时的但噪声特别大。滑动窗口滤波的延迟和窗口长度直接相关10点窗口大约延迟50毫秒。一阶低通滤波alpha取0.3时延迟在80毫秒左右而且曲线有明显的指数爬升过程。互补滤波在这个测试里表现最好因为陀螺仪积分通道保证了快速响应加速度计只负责慢慢校准角度曲线跟手性强几乎没有肉眼可见的滞后。这说明一个原则如果项目对实时性要求高比如平衡车、云台稳定器原始信号用轻量级滤波处理就行重头戏放在姿态融合。如果只是做数据显示比如电子水平仪那滑动窗口或者一阶低通就完全够用不必引入复杂的融合算法。4.4 让滤波效果“肉眼可见”的调试技巧数据调参时最忌讳对着串口助手里的滚动数字凭感觉判断。我习惯把数据同时发到VOFA这类串口绘图工具里实时画出多条曲线。曲线一出来噪声水平、延迟时间、漂移趋势一目了然。一个非常实用的技巧是用一根细线把传感器固定在一个可以摆动的支架上用手拨动支架做周期性摆动。这样既能看到静态噪声又能观察动态响应不需要额外设备就能完成大多数滤波参数的验证。另一个技巧是给传感器安装一个小的配重块增大摆动惯量让运动数据更有规律方便观察算法是否出现严重滞后或过冲。5. 常见问题与排查技巧实录5.1 常见问题速查表现象可能原因解决方向静止时角度缓慢漂移陀螺仪零偏未校准或互补滤波alpha过小上电时采样100次取平均作为零偏或增大tau运动时角度响应滞后滤波程度过重窗口过长或alpha过小减小滑动窗口长度增大低通滤波alpha数据周期性突变I2C读取时序不稳定偶发读取失败检查接线、降低I2C速率、增加读取校验角度在大角度时误差明显加速度计计算姿态使用了简化公式改用四元数姿态解算避免万向锁问题滤波后曲线出现平台期滑动窗口平均导致步进跳变换用一阶低通滤波输出更连续卡尔曼滤波发散Q和R参数设置不合理调大R调小Q让滤波器更信任模型预测5.2 几个容易踩的坑滤波算法本身代码量不大真正坑人的地方全在工程细节里。第一个坑是数据读取频率不固定。我一开始在主循环里加HAL_Delay延时来模拟定时采样结果不同任务执行时间不同实际采样周期在4到8毫秒之间跳动。滤波效果时好时坏定位了很久才发现是采样率不稳导致滤波器参数全部失真。后来改成定时器中断固定5毫秒采样所有滤波参数才稳定下来。第二个坑是陀螺仪零偏没有校准。很多人拿到的传感器零偏可能高达正负10度/秒不做零偏校准直接积分角度几秒钟就飘到飞起。正确的做法是在系统启动后保持传感器静止连续采样200到500个陀螺仪数据求平均把这个平均值作为零偏存储下来后续所有陀螺仪读数都减去这个零偏。第三个坑是量程配置和灵敏度换算不匹配。mpu6050有多个量程挡位每个挡位对应不同的LSB灵敏度。如果在初始化时配置了正负500度每秒但代码里仍然用250度每秒的灵敏度换算角度数据会整体放大两倍滤波参数全部失效。检查这类问题最好的办法是转动传感器90度看输出数据是否与预期物理值一致。第四个坑是定时中断里做浮点运算导致时间开销过长。stm32f103没有硬件浮点单元float运算依赖软件模拟虽然单次滤波计算量不大但多路数据叠加之后中断响应时间会变长。我实际测过每次中断里做两路滑动窗口、一路一阶低通和一路互补滤波总共耗时约500微秒在5毫秒采样周期下还能接受。如果滤波链路更复杂建议先精简代码或者把部分滤波移到主循环里执行。5.3 在定时中断里做滤波的注意事项在stm32的定时器中断服务函数里执行滤波代码需要额外关注几个点。中断服务函数尽量短只做数据读取、滤波计算和缓存结果串口发送放到主循环处理。否则长时间占用中断会导致其他中断任务得不到及时响应。另外一个容易被忽略的问题是滤波器静态变量的线程安全性。滤波器的历史数据存储在static变量中如果在主循环和中断里同时调用滤波函数可能发生数据竞争。我遇到过一次界面按钮触发重新校准主循环里修改了滤波器状态恰好中断里读到了半更新状态的变量角度输出瞬间跳变。解决办法很简单滤波函数只在中断里调用校准操作只影响校准值不影响滤波器内部状态。5.4 滤波参数调节的通用方法论调参阶段建议始终遵循一个原则一次只改一个参数。把参数调节记录成表格每次改动后固定测试流程、记录波形和关键数据最后对比不同组的静态噪声和动态延迟。以互补滤波的tau为例我从0.5开始每次增加0.5记录静止噪声和90度阶跃响应时间。测试结果基本符合理论预期tau越大静止越稳但响应越慢tau越小响应越快但噪声越大。最终根据平衡车项目的实际控制周期选择了tau等于1.0静止噪声正负0.3度响应时间小于150毫秒满足控制需求。如果你的项目追求更极致的滤波效果还可以考虑卡尔曼滤波加滑动窗口的组合先用短窗口滑动平均清理原始信号中的高频毛刺再把清理后的数据送入卡尔曼滤波器进行姿态融合。实测结果表明这种组合比单独使用任何一种算法的静态噪声和动态响应都要均衡代价是代码复杂度和调参工作量都有明显增加。6. 项目代码的整体组织建议滤波代码的模块化程度直接关系后续维护效率。我把滤波函数统一放在filter.c文件里对外只暴露几个简单接口Filter_Init用于初始化所有滤波器参数Filter_Process用于处理一组传感器数据并输出滤波后的结果Filter_Reset用于系统校准时重置内部状态。这样主程序只需要维护一个统一的滤波调用入口新增算法时只需要在filter.c内部增加函数实现不需要改动主程序逻辑。数据管理方面建议在主程序中定义结构体把原始数据、预处理后数据、最终角度数据分开存储。不要把所有中间变量都声明成全局变量否则代码可读性会快速下降。一个可行的组织方式typedef struct { float accel_raw[3]; float gyro_raw[3]; float accel_filtered[3]; float gyro_filtered[3]; float angle_pitch; float angle_roll; } MPU6050_Data_t;这个结构体贯穿整个处理流程。中断里读取寄存器后填充accel_raw和gyro_raw调用滤波模块填充后面几个字段主循环直接读取结构体发送到串口或者送进控制算法。模块边界清晰排查问题也方便。7. 后续可以继续扩展的方向滤波教程走到这一步基础闭环已经建立。如果你还想继续往下深入我建议从这几个方向展开。第一个方向是四元数姿态解算当前互补滤波输出的俯仰角和横滚角在正负90度范围内表现良好但超过这个范围后会遇到万向锁问题四元数可以彻底规避这个限制。第二个方向是动态采样率自适应在运动剧烈时加快采样频率在静止时降低采样频率既保证响应速度又降低功耗。第三个方向是把滤波和PID控制联动起来观察不同滤波参数对闭环性能的影响这一步做完你对滤波的理解会从“数据处理”上升到“系统设计”层面。我个人在实际操作中最深刻的体会是滤波参数没有标准答案只有适合某个具体项目的配置。今天给出的这些参数值都来自我的测试环境你可以作为起点参考但最终一定要结合自己的硬件布局、采样频率和应用场景反复调整。另外调试时别急着上复杂算法先从最简单的滑动窗口和一阶低通起步把数据链路调通、采样节奏调稳再逐步引入互补滤波或卡尔曼滤波这样每一步的结果都可控出了问题也知道去哪里查。最后再分享一个小技巧无论是滤波代码还是调参记录都养成随手写注释、记录测试条件的习惯半个月后回头看你会感谢当时的自己。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门