YAOTU INSIGHTS

10种常用滤波算法C代码实现:从限幅到卡尔曼的选型指南

10种常用滤波算法C代码实现:从限幅到卡尔曼的选型指南
简介这是一份面向嵌入式开发、单片机应用及信号采集方向的滤波算法实践资料围绕C语言实现展开适合需要处理传感器数据、抑制随机干扰的工程师与学生参考。资源系统整理了限幅、中位值、算术平均、递推平均、中位值平均、限幅平均、一阶滞后、加权递推平均、消抖及限幅消抖共10种常用软件滤波方法并逐一说明其原理、优缺点与适用场景便于按被测参数特性选型。压缩包共3个文件包含1个c源码文件、1个doc说明文档和1个pdf研究资料整体约198KB分别对应可编译的算法实现、方法讲解与理论参考结构紧凑、便于直接移植到实际工程。目前已有3190人学习下载读者可从中获得完整的C代码实现、各算法参数选取思路以及不同干扰类型下的对比依据快速搭建数据采集端的滤波处理框架。1. 滤波算法为什么你的传感器数据总在“跳舞”采样回来的信号从来不是干净的。ADC 读到的电压值里混着工频干扰、热噪声、量化误差电机电流里叠着 PWM 开关尖峰IMU 的加速度计输出在静止时都能看到 ±0.05g 的抖动。很多人第一反应是“换个更贵的传感器”但真正做过产品的人都知道问题往往不在硬件而在信号进入 MCU 之后那几十行代码里。滤波算法就是干这件事的用极小的计算代价把“跳舞”的数据压成一条能用的曲线。10 种常用滤波算法的 C 代码实现本质上是一套从“最粗暴”到“最讲究”的取舍工具箱——限幅、中位值、算术平均、递推平均、中位值平均、限幅平均、一阶滞后、加权递推平均、消抖、卡尔曼。搞懂它们各自的底层机制你才能在资源受限的 MCU 上选对那一把刀而不是把卡尔曼硬塞进 2KB RAM 的 8 位机里翻车。2. 从限幅到中位值最朴素的滤波为什么反而最常用2.1 限幅滤波用“最大偏差”做一道硬闸门限幅滤波的机制极其简单比较本次采样值与上次有效值如果偏差超过预设的A就判定本次为干扰丢弃并用上次值代替否则接受本次值。它的底层假设是——真实信号不会在两次采样之间发生超过A的跳变。#define A 10 // 最大允许偏差根据采样周期和信号最大变化率确定 int limit_filter(int new_val, int last_val) { if (abs(new_val - last_val) A) { return last_val; // 偏差过大视为干扰维持上次值 } return new_val; // 偏差在允许范围内接受新值 }A的取值是这套算法唯一的调参点也是最容易踩坑的地方。A太大脉冲干扰照样通过A太小真实信号的快速变化会被当成干扰砍掉表现为响应迟钝。常见的实现思路是先以最高采样率采集一段真实信号统计相邻两点差值的最大值再乘以 1.21.5 作为A。这个算法无法抑制“缓慢漂移”型干扰因为漂移的每步偏差都小于A它会照单全收。2.2 中位值滤波连续采 N 次取中间那个中位值滤波的做法是连续采样 N 次N 通常取奇数把 N 个值排序后取中间值作为本次有效输出。它的数学本质是排序统计量对脉冲型干扰的抑制能力极强——只要干扰点数不超过 (N-1)/2中位值就不会被带偏。#define N 5 // 采样次数必须为奇数 int median_filter(void) { int buf[N]; for (int i 0; i N; i) { buf[i] read_adc(); } // 冒泡排序N 很小开销可接受 for (int i 0; i N - 1; i) { for (int j 0; j N - 1 - i; j) { if (buf[j] buf[j 1]) { int t buf[j]; buf[j] buf[j 1]; buf[j 1] t; } } } return buf[N / 2]; // 取中位 }代价是 N 次采样加排序。N5 时排序比较次数是 10 次在 72MHz 的 Cortex-M3 上耗时不到 2 微秒但如果你在 1kHz 控制环里每轮都调用累积开销就不能忽略了。更关键的是中位值滤波的输出更新率降为采样率的 1/N对快速变化信号会引入相位滞后。它适合温度、称重这类慢变量不适合电流环。2.3 算术平均与递推平均用“记忆”换平滑算术平均滤波连续采 N 次求平均平滑效果与 N 成正比但同样有 1/N 的更新率问题。递推平均滑动平均改进了这一点维护一个长度为 N 的队列每次新采样入队、最老值出队输出队列均值。#define N 8 int queue[N]; int sum 0; int idx 0; int moving_average(int new_val) { sum - queue[idx]; // 减去最老值 queue[idx] new_val; // 新值入队 sum new_val; // 累加新值 idx (idx 1) % N; // 环形索引 return sum / N; // 输出均值 }递推平均每轮只做一次减法、一次加法、一次取模计算量恒定且输出更新率与采样率一致。它的频域本质是一个 FIR 低通滤波器N 越大截止频率越低但相位滞后也越严重。在电机电流采样中N 通常取 48在电池电压监测中N 可以取到 32 甚至更大。注意sum要用long类型否则 N 较大时累加会溢出。3. 复合滤波与一阶滞后在平滑与响应之间找平衡3.1 中位值平均滤波先剔除再平均中位值平均滤波把中位值和平均值的思路合在一起连续采 N 次去掉一个最大值和一个最小值对剩余 N-2 个值求平均。这样既保留了中位值抗脉冲的能力又利用平均进一步压低随机噪声。#define N 10 int median_avg_filter(void) { int buf[N]; int max -32768, min 32767, sum 0; for (int i 0; i N; i) { buf[i] read_adc(); if (buf[i] max) max buf[i]; if (buf[i] min) min buf[i]; sum buf[i]; } return (sum - max - min) / (N - 2); }这个算法不需要完整排序只需一趟遍历找出最大最小值并累加计算量比中位值滤波还小。代价是它只能剔除一个最大值和一个最小值如果干扰脉冲连续出现两个以上就会有一个漏网进入平均值。N 的取值通常在 612 之间太小则平均效果弱太大则响应慢。3.2 限幅平均滤波先限幅再递推平均限幅平均滤波是限幅和递推平均的串联每次新采样先经过限幅判断通过后再送入滑动平均队列。这样脉冲干扰在第一道闸门就被挡住不会污染平均值。#define A 10 #define N 8 int queue[N] {0}; int sum 0; int idx 0; int last_valid 0; int limit_avg_filter(int new_val) { if (abs(new_val - last_valid) A) { new_val last_valid; // 限幅用上次有效值替代 } last_valid new_val; sum - queue[idx]; queue[idx] new_val; sum new_val; idx (idx 1) % N; return sum / N; }这套组合在工业变送器里非常常见因为它同时应对了脉冲干扰和随机噪声两类问题。调参时先定A根据信号最大变化率再定N根据噪声水平和允许的相位滞后。两个参数相互独立调试起来比单独调一个参数更直观。3.3 一阶滞后滤波用“惯性”吃掉高频噪声一阶滞后滤波一阶低通的公式是Y(n) α·X(n) (1-α)·Y(n-1)其中 α 是滤波系数范围 01。α 越小惯性越大输出越平滑但滞后越严重。float alpha 0.2f; // 滤波系数 float y_last 0.0f; float lag_filter(float new_val) { float y alpha * new_val (1.0f - alpha) * y_last; y_last y; return y; }这个算法的频域本质是一个一阶 IIR 低通截止频率fc α / (2π·T)T 为采样周期。α0.2、T1ms 时fc 约 32Hz。它的优势是计算量极小一次乘加内存占用只有一个历史值非常适合在中断里跑。缺点是相位滞后与平滑度直接绑定无法同时优化。在电机控制中电流环带宽通常要求 1kHz 以上α 不能小于 0.5而在温度控制中α 取 0.05 都很常见。4. 加权递推平均与消抖滤波给不同时刻的数据不同权重4.1 加权递推平均滤波越新的数据越重要递推平均滤波对队列里所有数据一视同仁但实际系统中越新的采样值越能反映当前状态。加权递推平均给队列中每个位置分配不同权重通常新值权重最大旧值权重递减。#define N 4 float weights[N] {0.4f, 0.3f, 0.2f, 0.1f}; // 权重和应为 1.0 float queue[N] {0}; float weighted_avg_filter(float new_val) { // 队列右移新值入队首 for (int i N - 1; i 0; i--) { queue[i] queue[i - 1]; } queue[0] new_val; float sum 0.0f; for (int i 0; i N; i) { sum queue[i] * weights[i]; } return sum; }权重系数的选取没有固定公式常见的做法是线性递减或指数递减。线性递减如 0.4/0.3/0.2/0.1实现简单指数递减如 0.5/0.25/0.125/0.125对最新数据更敏感。这个算法在需要快速响应又要求一定平滑度的场景里很有用比如无人机姿态解算中的角速度预处理。注意权重和必须归一化到 1.0否则输出会有直流偏置。4.2 消抖滤波不是“滤波”是“确认”消抖滤波的机制和前几种完全不同它不改变采样值的幅度而是通过计数来确认一个值是否“稳定”。设定一个计数器如果新采样值与当前有效值相同计数器加一如果不同计数器减一当计数器减到零时才把新值设为有效值。#define DEBOUNCE_CNT 5 int stable_val 0; int counter 0; int debounce_filter(int new_val) { if (new_val stable_val) { if (counter DEBOUNCE_CNT) counter; } else { if (counter 0) { counter--; } else { stable_val new_val; // 连续多次不同确认新值 } } return stable_val; }这个算法本质上是“滞回比较”的数字化实现适合按键、限位开关这类只有两个状态的信号。它不适用于连续变化的模拟量因为它的输出是阶梯状的。DEBOUNCE_CNT决定了确认时间取值过大则响应慢过小则消抖不彻底。在 10ms 采样周期下DEBOUNCE_CNT5意味着 50ms 的确认窗口对按键来说刚好。5. 卡尔曼滤波为什么它被神化又为什么经常被误用5.1 卡尔曼滤波的底层假设线性 高斯卡尔曼滤波不是“更高级的滤波算法”而是一套基于状态空间模型的最优估计器。它的核心假设是系统状态转移是线性的过程噪声和观测噪声都服从高斯分布。在这两个假设成立时卡尔曼滤波给出的是最小均方误差意义下的最优估计。// 一维卡尔曼滤波简化实现 float Q 0.01f; // 过程噪声协方差 float R 0.1f; // 观测噪声协方差 float P 1.0f; // 估计误差协方差 float K 0.0f; // 卡尔曼增益 float x 0.0f; // 状态估计 float kalman_filter(float z) { // 预测 P P Q; // 更新 K P / (P R); x x K * (z - x); P (1 - K) * P; return x; }这段代码里Q和R的比值决定了滤波器的“性格”Q/R越大越信任观测值响应快但噪声大Q/R越小越信任模型预测平滑但滞后。很多人把卡尔曼滤波当成“自动调参的魔法”实际上Q和R的整定需要根据传感器手册和实际噪声统计来定没有免费午餐。5.2 什么时候不该用卡尔曼滤波在 8 位 MCU 上跑浮点卡尔曼滤波每次迭代可能消耗几百微秒如果控制环周期是 100 微秒直接翻车。更常见的问题是很多传感器噪声并不服从高斯分布比如超声波测距的野值卡尔曼滤波的“最优”假设不成立输出反而不如中位值滤波稳定。我一般会这么理解卡尔曼滤波适合“有明确物理模型 噪声统计特性已知 计算资源充足”的场景比如 IMU 姿态融合、电池 SOC 估计。对于单纯的 ADC 去噪一阶滞后或递推平均的性价比高得多。6. 避坑与排查滤波算法落地时的五个血泪教训6.1 现象滤波后数据“阶梯化”分辨率下降原因整数除法截断。递推平均滤波中sum / N是整数除法当 N8 时输出只能是 8 的倍数原始信号的小变化被吃掉。解决把sum和输出改为float或在整数运算中保留余数做补偿。6.2 现象系统上电后输出缓慢爬升很久才稳定原因滤波队列初始值为 0而实际信号在 2.5V 附近递推平均需要 N 轮才能“填满”队列。解决上电时连续采样 N 次直接填充队列或把队列初始值设为第一次采样值。6.3 现象限幅滤波在信号快速变化时“卡死”原因A设得太小真实信号的快速上升沿被反复判定为干扰输出一直维持在旧值。解决用示波器抓取真实信号的最大变化率重新计算A或改用限幅平均让平均值逐步跟上。6.4 现象卡尔曼滤波输出发散或剧烈抖动原因P初始值设为 0导致卡尔曼增益K长期为 0滤波器“不信任”观测值。解决P初始值设为 1.0 或根据初始不确定度设定检查Q和R是否数量级匹配。6.5 现象消抖滤波在按键上“连击”原因DEBOUNCE_CNT太小机械抖动被当成有效变化。解决根据按键手册的抖动时间通常 520ms和采样周期计算DEBOUNCE_CNT并加入“释放确认”逻辑即按下和松开都要消抖。7. 选型取舍没有最好的滤波只有最合适的组合滤波算法的选型本质上是在四个维度上做取舍平滑度、响应速度、计算开销、内存占用。限幅滤波计算量最小但只能抗脉冲中位值滤波抗脉冲最强但有 1/N 的更新率损失递推平均平滑度可调但相位滞后与 N 成正比一阶滞后计算量极小但平滑与滞后绑定卡尔曼滤波理论最优但需要模型和算力。在实际项目中我通常先用示波器抓原始信号判断噪声类型脉冲型、随机型、漂移型再按“先抗脉冲、再压随机、最后补滞后”的顺序组合两到三种算法。比如工业变送器常用“限幅 递推平均”IMU 常用“中位值 一阶滞后 卡尔曼”。这套 10 种算法的 C 代码实现价值不在于让你全部用上而在于给你一套可以随时裁剪、组合的积木。从那以后我每次拿到新的传感器都强制先跑一遍原始数据采集用 Excel 画出噪声分布再决定用哪几种滤波串联——这个习惯帮我省下了至少三次“换传感器”的冤枉钱。希望帮到你。本文还有配套的精品资源点击获取