首页 / 资讯中心 / 文章详情

嵌入式信号处理:10种C语言滤波算法实现与工程选型指南

嵌入式信号处理:10种C语言滤波算法实现与工程选型指南 ★ FEATURED ARTICLE
简介这是一份面向嵌入式开发、单片机应用与信号采集方向的滤波算法代码合集围绕工程中常见的采样噪声与脉冲干扰问题整理了十种经典软件滤波方法的实现思路与C语言代码。内容涵盖限幅滤波、中位值滤波、算术平均、递推平均、中位值平均、限幅平均、一阶滞后、加权递推平均、消抖以及限幅消抖等算法并逐项说明各自的方法、优缺点与适用参数场景便于读者按被测信号特性选型。资源包共3个文件包含1个C源码文件、1个doc说明文档和1个pdf研究资料分别用于代码实现、方法讲解与理论参考压缩包约198KB体积轻便易于查阅。目前已有3190人学习下载适合需要快速对比滤波效果、为温度、压力、流量等采集通道挑选合适算法的开发者参考也可作为课程设计与项目调试的实用素材。1. 嵌入式信号处理里为什么我建议你手写这10种滤波算法做单片机采集的朋友大概率遇到过这种场景ADC 读回来的数据跳得厉害屏幕上曲线像心电图客户看一眼就说“你这东西是不是坏了”。第一反应是加个平均滤波结果发现响应慢半拍按键按下去要等两秒才有反应。换中值滤波遇到连续脉冲干扰又压不住。折腾一圈才明白滤波算法不是越复杂越好而是要看噪声类型和信号特征来选。这份 C 代码资源整理了 10 种工程里最常用的滤波算法从最基础的限幅、中值、算术平均到一阶滞后、加权递推、卡尔曼这些进阶方案全部用纯 C 实现不依赖任何第三方库直接丢进 STM32、GD32、ESP32 或者 51 单片机的工程里就能编译。适合正在做传感器数据采集、电机控制、电源监测的嵌入式开发者也适合刚接触信号处理、想搞明白“滤波到底在滤什么”的新手。代码风格偏底层没有花哨的封装每个函数独立可用方便你按需裁剪。2. 算法选型与工程结构10种滤波器的适用边界2.1 从噪声类型反推算法选择拿到一份滤波代码最忌讳的就是“哪个名字顺眼用哪个”。我一般会先问三个问题噪声是高频毛刺还是低频漂移信号本身变化快不快单片机 RAM 和算力还剩多少高频毛刺比如电机换向时 ADC 上的尖峰中值滤波和限幅滤波是首选它们对孤立脉冲的抑制效果立竿见影。低频漂移比如温漂导致的缓慢偏移一阶滞后滤波和卡尔曼滤波更合适因为它们能跟踪信号趋势。如果信号本身变化很快比如音频或振动信号就不能用太重的滤波否则相位延迟会把有用信息吃掉。这份资源里的 10 种算法按计算量和效果大致可以分成三档档位算法RAM占用适用场景轻量限幅、中值、算术平均极小低速ADC、按键消抖中等加权递推、一阶滞后、防脉冲干扰平均小温度、压力、电池电压较重卡尔曼、粒子滤波简化版中等姿态解算、电机闭环注意卡尔曼滤波虽然效果好但需要调 Q 和 R 两个参数调不好反而比一阶滞后还差。新手建议先从一阶滞后入手。2.2 目录结构与文件组织这份代码没有复杂的工程结构就是一个典型的嵌入式 C 模块组织方式。解压后你会看到类似这样的目录filter_algorithms/ ├── inc/ │ ├── filter.h // 所有滤波函数的声明 │ └── filter_config.h // 参数配置宏 ├── src/ │ ├── filter_limit.c // 限幅滤波 │ ├── filter_median.c // 中值滤波 │ ├── filter_average.c // 算术平均 │ ├── filter_weighted.c // 加权递推 │ ├── filter_lag.c // 一阶滞后 │ ├── filter_kalman.c // 卡尔曼 │ └── ... └── example/ └── main.c // 使用示例filter.h里统一用typedef定义了数据类型方便你在不同平台间移植。比如// filter.h 中的类型定义 typedef float filter_data_t; // 默认用 float资源紧张可改 int32_t typedef struct { filter_data_t last_value; filter_data_t threshold; } limit_filter_t;这里filter_data_t默认是float如果你用的是没有 FPU 的 51 单片机可以改成int32_t或者int16_t但要注意定点运算的溢出问题。limit_filter_t结构体里存了上一次的有效值和限幅阈值调用时把结构体指针传进去就行不需要全局变量。2.3 编译与集成步骤把代码集成到你的工程里步骤不复杂但有几个地方容易翻车。我一般按这个顺序来第一步把inc和src两个目录拷贝到你的工程目录下在 IDE 里把inc加入头文件搜索路径把src下的.c文件加入编译列表。如果你用的是 Keil 或 IAR直接在工程设置里添加即可。第二步根据你的单片机型号修改filter_config.h里的宏定义。比如// filter_config.h 中的可配置项 #define FILTER_USE_FLOAT 1 // 1: 使用float0: 使用定点数 #define FILTER_MEDIAN_WINDOW 5 // 中值滤波窗口大小必须是奇数 #define FILTER_KALMAN_Q 0.01f // 卡尔曼过程噪声协方差 #define FILTER_KALMAN_R 0.1f // 卡尔曼测量噪声协方差FILTER_MEDIAN_WINDOW必须是奇数因为中值滤波要取中间那个值。如果你设成偶数代码里会有一个#error提示你改回来。FILTER_KALMAN_Q和R的初始值可以先不动后面调参时再改。第三步在main.c里包含filter.h然后按需调用。比如你要用限幅滤波先定义一个结构体变量初始化阈值然后在 ADC 中断或主循环里调用#include filter.h limit_filter_t limit; filter_data_t adc_value; int main(void) { limit_init(limit, 5.0f); // 阈值设为5.0 while (1) { adc_value read_adc(); adc_value limit_filter(limit, adc_value); // 使用滤波后的值 } }limit_init把last_value清零threshold设为你传入的值。limit_filter每次比较新值和上一次有效值的差如果超过阈值就认为这次是干扰直接返回上一次的值否则更新last_value并返回新值。这个逻辑对抑制突发尖峰特别有效但阈值设太小会把正常信号也限掉设太大又没效果一般取信号最大变化率的 1.5 到 2 倍。3. 核心滤波算法逐行拆解从限幅到卡尔曼3.1 限幅滤波与中值滤波的配合使用限幅滤波的代码很短但里面有个细节容易写错。先看实现// filter_limit.c filter_data_t limit_filter(limit_filter_t *f, filter_data_t new_value) { filter_data_t diff new_value - f-last_value; if (diff 0) diff -diff; // 取绝对值 if (diff f-threshold) { return f-last_value; // 超过阈值丢弃新值 } f-last_value new_value; // 更新有效值 return new_value; }diff取绝对值那一步有些实现会用fabs()但在嵌入式里能不用浮点库就不用手动判断正负更省周期。threshold是唯一需要调的参数我一般先用示波器看原始信号的最大正常跳变幅度然后乘以 1.5 作为初始值。限幅滤波单独用有个问题如果干扰持续存在比如连续几个采样点都是尖峰限幅会一直返回旧值导致输出“卡死”。这时候就要配合中值滤波。中值滤波的实现稍微长一点// filter_median.c filter_data_t median_filter(filter_data_t *buf, uint8_t size) { filter_data_t temp[FILTER_MEDIAN_WINDOW]; uint8_t i, j; // 拷贝到临时数组避免破坏原缓冲区 for (i 0; i size; i) { temp[i] buf[i]; } // 冒泡排序窗口小的时候够用 for (i 0; i size - 1; i) { for (j 0; j size - 1 - i; j) { if (temp[j] temp[j 1]) { filter_data_t t temp[j]; temp[j] temp[j 1]; temp[j 1] t; } } } return temp[size / 2]; // 返回中间值 }buf是外部维护的采样缓冲区每次新数据进来时用memmove或循环移位把旧数据往前挪把新数据放到末尾。size就是窗口大小必须是奇数。冒泡排序在窗口小于 10 的时候完全够用没必要上快排。返回temp[size/2]就是中值。把限幅和中值串起来用效果比单独用好很多先限幅去掉离谱的尖峰再中值平滑剩下的毛刺。我一般会写一个组合函数filter_data_t limit_median_filter(limit_filter_t *l, filter_data_t *buf, uint8_t size, filter_data_t new_value) { filter_data_t limited limit_filter(l, new_value); // 把限幅后的值推入中值窗口 for (uint8_t i 0; i size - 1; i) { buf[i] buf[i 1]; } buf[size - 1] limited; return median_filter(buf, size); }这个组合在电机电流采样里特别管用换向尖峰被限幅吃掉剩下的随机噪声被中值抹平。3.2 一阶滞后滤波的参数整定一阶滞后滤波也叫指数加权平均是工程里性价比最高的平滑算法。它的公式很简单// filter_lag.c filter_data_t lag_filter(lag_filter_t *f, filter_data_t new_value) { // alpha 越大跟随越快平滑越弱 f-last_value f-alpha * new_value (1.0f - f-alpha) * f-last_value; return f-last_value; }alpha是唯一参数范围 0 到 1。alpha越接近 1输出越跟随输入平滑效果越弱越接近 0输出越平滑但延迟越大。我一般用这个经验公式来选初始值alpha 采样周期 / (采样周期 时间常数)比如你每 10ms 采样一次希望时间常数是 100ms那alpha 0.01 / (0.01 0.1) ≈ 0.09。这个值可以先烧进去看波形如果还是抖就减小alpha如果响应太慢就增大alpha。有个坑要注意alpha用浮点乘法在低端单片机上很费时间。如果主频低于 24MHz建议改成定点实现// 定点版一阶滞后alpha 用 Q8 格式表示 int32_t lag_filter_fixed(lag_filter_t *f, int32_t new_value) { // alpha_q8 范围 0~256对应 0~1.0 f-last_value (f-alpha_q8 * new_value (256 - f-alpha_q8) * f-last_value) 8; return f-last_value; }alpha_q8是alpha乘以 256 后的整数比如alpha0.1就填 26。右移 8 位代替除法速度比浮点快很多。3.3 卡尔曼滤波的简化实现与调参卡尔曼滤波在这份代码里是一个简化版只处理一维数据适合单片机跑。完整实现大概三十行// filter_kalman.c filter_data_t kalman_filter(kalman_filter_t *f, filter_data_t new_value) { // 预测 f-p f-p f-q; // 更新 f-k f-p / (f-p f-r); f-x f-x f-k * (new_value - f-x); f-p (1.0f - f-k) * f-p; return f-x; }q是过程噪声协方差r是测量噪声协方差。q越大越相信测量值响应越快但噪声越大r越大越相信模型预测输出越平滑但延迟越大。我一般先把q设成 0.001r设成 0.1然后看波形微调。调参有个血泪经验不要同时调q和r固定一个调另一个。比如先固定r0.1把q从 0.0001 慢慢往上加直到输出既能跟上信号变化又不会明显抖动。如果怎么调都不满意大概率是模型本身不适合你的信号换一阶滞后反而更省事。卡尔曼的初始化也很关键。x应该设成第一次采样值p设成 1.0k不用管第一次迭代会自动算。如果x初始为 0 而实际信号在 100 左右前几个输出会慢慢爬上去看起来像“启动延迟”其实不是算法问题是初始值没给对。4. 避坑与排查滤波代码移植时最容易翻车的5个地方4.1 现象滤波后数据完全没变化原因结构体变量没有初始化last_value是随机值导致每次比较都超阈值一直返回旧值。或者alpha设成了 0输出永远是初始值。解决所有滤波结构体在使用前必须调用对应的init函数。如果代码里没有init就手动把last_value设成第一次采样值。alpha不要设 0 或 1取中间值。4.2 现象中值滤波输出偶尔跳变原因窗口缓冲区没有正确移位新数据覆盖了旧数据但顺序乱了。或者窗口大小设成了偶数size/2取到了偏大的那个中间值。解决检查memmove的方向确保旧数据往前挪。窗口大小强制用奇数可以在filter_config.h里加编译期检查#if (FILTER_MEDIAN_WINDOW % 2 0) #error FILTER_MEDIAN_WINDOW must be odd #endif4.3 现象卡尔曼滤波输出发散原因p没有在每次迭代后更新或者q和r设成了负数。浮点数溢出也会导致发散。解决检查p (1 - k) * p这一行有没有漏掉。q和r必须大于 0。如果用的是float确保编译器开启了浮点支持如果是定点检查移位有没有溢出。4.4 现象多个滤波通道互相干扰原因用了全局变量存last_value两个通道共用了同一个结构体。解决每个通道独立定义一个结构体变量不要共用。如果 RAM 紧张可以把结构体放在数组里用索引区分通道。4.5 现象滤波后信号相位延迟太大原因一阶滞后的alpha太小或者卡尔曼的r太大导致输出跟不上输入变化。解决先确认延迟是不是算法本身造成的。用信号发生器输入一个方波看输出上升沿滞后多少。如果滞后超过应用允许的范围就增大alpha或减小r。实在不行就换限幅中值组合这两种算法几乎没有相位延迟。5. 二次开发与参数整定把这套代码改成你自己的滤波库这份代码最大的价值不是直接拿来用而是作为一个起点让你按自己的硬件和信号特征去改。我一般会做三件事统一接口、加运行时调参、写自动化测试。统一接口的意思是把所有滤波函数都包装成同样的签名typedef filter_data_t (*filter_func_t)(void *ctx, filter_data_t new_value); typedef struct { filter_func_t func; void *ctx; } filter_t; filter_data_t apply_filter(filter_t *f, filter_data_t new_value) { return f-func(f-ctx, new_value); }这样上层代码不用关心具体用的是哪种滤波换算法只需要换func和ctx。ctx指向对应的结构体比如limit_filter_t或kalman_filter_t。加运行时调参是为了在不重新烧录的情况下改参数。我一般会留一个串口命令解析器收到SET ALPHA 0.05就修改对应结构体的alpha。调试阶段特别有用不用反复编译下载。自动化测试是很多人忽略的一步。我会在 PC 上写一个简单的测试程序把滤波函数编译进去喂入模拟的噪声数据看输出是否符合预期。比如// test_filter.c void test_limit_filter(void) { limit_filter_t f; limit_init(f, 5.0f); assert(limit_filter(f, 10.0f) 10.0f); // 第一次直接通过 assert(limit_filter(f, 20.0f) 10.0f); // 跳变10超过阈值5丢弃 assert(limit_filter(f, 12.0f) 12.0f); // 跳变2通过 }这个测试在 PC 上跑一遍只要几毫秒但能提前发现逻辑错误比在单片机上用串口打印调试快得多。最后说一个我踩过的坑不要迷信“高级算法”。有一次我做电池电压采样用了卡尔曼滤波调了两天参数效果还不如一阶滞后加中值。后来想明白了电池电压本身变化就慢卡尔曼的预测模型根本用不上反而引入了不必要的计算量。从那以后我每次选滤波算法都强制先问自己信号带宽是多少噪声是什么类型单片机还剩多少算力这三个问题答不上来就不动手写代码。希望这份代码和上面的经验能帮到你少走一些我当年走过的弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站