
简介面向单片机与嵌入式开发者这套C语言十大滤波算法代码围绕STM32 ADC采样场景覆盖移动平均、滑动平均、中值、指数平均、巴特沃斯、卡尔曼、自适应、维纳、窗函数及傅里叶变换滤波用于抑制传感器与模拟量采集中常见的随机噪声、脉冲干扰和动态波动帮助提高信号质量与系统稳定性。压缩包共82个文件以35个C源文件与37个头文件为主另有启动文件、Keil工程配置、hex固件、txt说明等整体仅206KB结构清晰便于直接查看算法实现或参考工程集成。资源基于STM32F10x标准库构建包含ADC、USART、LCD等硬件驱动模块与README说明可结合示例观察不同滤波算法在实际采样数据上的处理效果也方便按需裁剪到自己的工程。已有1984人学习下载适合正在做STM32数据采集、ADC底层开发或需要快速对比滤波效果的工程师参考。1. 为什么ADC采样数据要过一遍C语言滤波算法ADC采集到的12位数据常态下就在几个LSB之间跳动这不是故障而是参考电压纹波、采样电容电荷注入、电源噪声和走线耦合共同作用的结果。模拟前端加RC低通或EMI滤波电路能压掉一部分高频干扰但压不掉工频纹波和传感器本身的白噪声而且每一路都加硬件成本。C语言十大滤波算法就是这个分工里软件端的那一半在MCU内部用一段可预测的代码把“有噪声的ADC采样值”还原成“更接近物理量的估计值”。它适合STC、STM32、51这类资源受限的MCU工程特别适合ADC采样率不高、又要做实时控制的场景。下面给出一套能直接落地的STM32滤波方案十个算法全部用纯C实现重点讲清参数为什么这么设、不同场景怎么选避免把滤波做成抄一段代码就完事。2. 十大滤波算法按干扰模型分组与统一C语言接口2.1 十大滤波算法按干扰模型分三组没有全能的滤波算法。十大滤波算法流传广是因为它们覆盖了ADC数据里最常见的三类干扰脉冲毛刺、白噪声和缓慢漂移。按干扰模型分十个算法可以归为三组去毛刺、平滑噪声、缓变跟随。分组算法处理思路典型代价去毛刺限幅滤波、中值滤波、中位值平均滤波、限幅平均滤波、限幅消抖滤波用变化率或排序剔除异常点窗口排序滞后或阈值误判平滑噪声算术平均滤波、递推平均滤波、加权递推平均滤波多采样点取均值压低噪声方差平滑越强响应越慢缓变跟随一阶滞后滤波、消抖滤波用历史输出修正当前值相位滞后或稳态死区十个算法之间没有谁比谁强的线性关系。做项目时最常见的坑是抱着一个递推平均走天下遇到脉冲干扰被毛刺带偏遇到阶跃信号横竖追不上。工程上的标准做法是组合使用限幅先掐掉粗大误差递推平均再平滑细粒度噪声。2.2 为滤波算法设计统一C语言接口十种算法写起来都不难难在切换。上电想对比两种算法效果时如果每个算法暴露的调用参数不一样就得反复改调用点。常见做法是给滤波器做一个统一C接口每个算法只暴露一个入口函数和一份状态结构体/* flt_common.h */ #include stdint.h typedef struct flt_obj flt_obj_t; /* 前向声明 */ typedef struct { int16_t (*proc)(flt_obj_t *obj, int16_t raw); /* 处理一个采样点 */ } flt_ops_t; struct flt_obj { flt_ops_t ops; /* 算法入口 */ void *state; /* 各算法私有状态 */ void *cfg; /* 各算法私有参数 */ }; static inline int16_t flt_run(flt_obj_t *o, int16_t raw) { return o-ops.proc(o, raw); }接口约定是每次传入一个ADC原始值返回一个滤波值调用方不关心内部是排序还是环形缓冲。这样采样回调里写v flt_run(flt, adc_val);切换算法时只改初始化函数业务代码一行不动。参数类型统一用int16_t12位ADC最大4095能覆盖如果换成24位外部ADC把类型改成int32_t即可排序和平均逻辑完全不变。这个设计的隐藏收益是所有算法都可被静态分配不在堆上申请内存适合裸机工程。2.3 ADC采样周期与滤波窗口的关系动手写代码前先定ADC采样周期。滤波窗口N指的是N个采样点不是N秒一阶滞后滤波的时间常数τ与系数α的关系为 τ ≈ T·(1−α)/αT是采样周期递推平均窗口的等效时间滞后约为(N1)/2个采样周期。提高ADC采样率并不能绕过滤波滞后只会让滞后在时间维度上变短。例如ADC从1kHz提高到2kHz同样的窗口N8时滞后从4ms降为2ms代价是DMA搬运和CPU平均处理次数翻倍。提示在STM32上配置ADC采样周期时把采样时间和窗口长度放在同一张表里标注避免换采样率后忘了同步改滤波参数。3. 五个基础ADC滤波算法的C语言实现与参数标定下面的实现顺序按推荐给ADC数据使用的优先级排列同时也按从简单到复杂排列。每个算法给最小可用实现、参数标定方法和适用边界。3.1 限幅滤波阈值取相邻最大差值的1/3/* limit.c */ typedef struct { int16_t last; /* 上一点输出 */ int16_t max_delta; /* 相邻两点最大允许偏差 */ } limit_state_t; int16_t flt_limit_proc(flt_obj_t *o, int16_t in) { limit_state_t *s (limit_state_t *)o-state; int16_t d in - s-last; if (d s-max_delta || d -s-max_delta) { return s-last; /* 超限丢点 */ } s-last in; return in; }实现上就是一次差值和一次比较没有任何数组几条指令完成。第一次调用前把last初始化为第一次采样值。max_delta怎么定是关键如果信号是温度、液位这类变化缓慢的量可以把10到20个采样周期内能发生的最大物理变化换算成ADC码值如果是电机电流这类快速变化量阈值取太小会导致阶跃时输出长时间停在旧值上。工程经验是先把信号源快速推到目标状态记下相邻两次ADC采样的最大差值取该差值的三分之一作为阈值。3.2 中值滤波窗口N取奇数MCU上N5最划算/* median.c */ #define MED_WIN 5 typedef struct { int16_t buf[MED_WIN]; uint8_t idx; } median_state_t; int16_t flt_median_proc(flt_obj_t *o, int16_t in) { median_state_t *s (median_state_t *)o-state; int16_t t[MED_WIN]; int i, j; s-buf[s-idx] in; s-idx (s-idx 1) % MED_WIN; memcpy(t, s-buf, sizeof(t)); for (i 0; i MED_WIN - 1; i) { /* 冒泡排一次 */ for (j 0; j MED_WIN - 1 - i; j) { if (t[j] t[j 1]) { int16_t tmp t[j]; t[j] t[j 1]; t[j 1] tmp; } } } return t[MED_WIN / 2]; /* 返回中间值 */ }中值滤波的窗口必须为奇数否则中间位置存在二义性。窗口越大抗脉冲能力越强但响应滞后同步加大输出滞后约为(N−1)/2个采样点N5时滞后2拍。对STM32这类MCUN5是常用选择冒泡排序5个元素最坏情况下只需比较10次几微秒结束不会影响1kHz到10kHz的ADC中断节奏。如果信号里周期性毛刺非常多再上调到N7。3.3 算术平均滤波攒齐一个窗口再输出#define AVG_WIN 8 typedef struct { int16_t buf[AVG_WIN]; int32_t acc; uint8_t cnt; int16_t last_out; } avg_state_t; int16_t flt_avg_proc(flt_obj_t *o, int16_t in) { avg_state_t *s (avg_state_t *)o-state; s-acc in; if (s-cnt AVG_WIN) { s-last_out (int16_t)(s-acc / AVG_WIN); s-acc 0; s-cnt 0; } return s-last_out; /* 窗口攒满之前维持上一次输出 */ }算术平均的本质是每N个采样周期只输出一次输出率被降为采样率的1/N。如果业务侧要求每个ADC采样周期都出数据算术平均就不合适这正是递推平均存在的理由。累加器用int32_t8个4095累加是32760不会溢出但窗口到64时最好换成int64_t或分段求和。对噪声以白噪声为主的场景窗口N每翻倍噪声标准差降为原来的 1/√2。初始化时同样需要预热前N个周期输出0会误导控制逻辑通常让last_out直接等于第一个样本。3.4 递推平均滤波滑动窗口滤波每个采样点都输出#define SLIDE_WIN 8 typedef struct { int16_t buf[SLIDE_WIN]; uint8_t pos; int32_t sum; } slide_state_t; int16_t flt_slide_proc(flt_obj_t *o, int16_t in) { slide_state_t *s (slide_state_t *)o-state; s-sum - s-buf[s-pos]; /* 减掉最旧样本 */ s-buf[s-pos] in; /* 写入最新样本 */ s-sum in; s-pos (s-pos 1) % SLIDE_WIN; return (int16_t)(s-sum / SLIDE_WIN); }递推平均也叫滑动窗口滤波、滑动平均。它维护一个环形缓冲每来一个样本就踢掉最旧的一个输出与输入同步率为1:1实时性明显好于算术平均。初次启动时buf全为0会导致头N个输出偏低所以初始化阶段要把第一个样本灌满整个窗口或者直接丢弃前N个输出。窗口宽度是平滑度和滞后的折中STM32上做1kHz ADC采样窗口取8或16上位机要求波形平滑度更高时可以取32但阶跃响应会更钝。3.5 中位值平均滤波先掐尖再平均#define REM_WIN 5 /* 窗口至少为3的倍数2 */ typedef struct { int16_t buf[REM_WIN]; uint8_t idx; } rem_state_t; int16_t flt_rem_proc(flt_obj_t *o, int16_t in) { rem_state_t *s (rem_state_t *)o-state; int16_t t[REM_WIN]; int32_t sum 0; int i, j; s-buf[s-idx] in; s-idx (s-idx 1) % REM_WIN; memcpy(t, s-buf, sizeof(t)); for (i 0; i REM_WIN - 1; i) for (j 0; j REM_WIN - 1 - i; j) if (t[j] t[j 1]) { int16_t x t[j]; t[j] t[j 1]; t[j 1] x; } for (i 1; i REM_WIN - 1; i) /* 跳过t[0]最小和t[WIN-1]最大 */ sum t[i]; return (int16_t)(sum / (REM_WIN - 2)); }中位值平均滤波也叫防脉冲干扰平均滤波思路是先对窗口内排序删掉一个最大和一个最小再对中间样本做算术平均。它能同时对付脉冲毛刺和随机噪声毛刺落在最值位置被删掉随机噪声被平均压低。N越大能抵抗的毛刺数量越多但排序耗时变长且最大/最小两个位置最多挡住两处离群点。窗口N5时平均用3个点N7时平均5个点建议从5起步。3.6 五个基础算法的选型对照上面五个基础算法分别解决限幅处理异常跳变、中值处理脉冲、算术平均压低白噪声、递推平均实时平滑、中位值平均兼顾脉冲加噪声。实际项目里大多数ADC通道用限幅加递推平均组合就够了剩下的是给特定信号准备的。场景推荐算法说明温度/NTC热敏电阻一阶滞后 或 递推平均N16信号缓慢滞后影响小电机电流/力传感器限幅 递推平均N8快速变化中压毛刺按键/开关量消抖滤波对连续模拟量不适用4. 五个进阶滤波算法的C语言写法与参数取舍后五个算法大多是前五位的组合或变种代码量小但参数意义深越是老工程师越会在α系数和加权数组上较真。4.1 限幅平均滤波两级组合的代码结构限幅平均就是先限幅、再进递推平均。它的价值在于递推平均会把毛刺平均进结果里一个巨大的毛刺被N−1个正常样本稀释后会留下一个小凸起。先限幅把这个凸起拦在门外递推平均再去处理小扰动#define LA_LIMIT 120 #define LA_WIN 8 static int16_t la_last; static int16_t la_buf[LA_WIN]; static uint8_t la_pos; static int32_t la_sum; int16_t flt_lim_avg_proc(int16_t in) { int16_t d, v; d in - la_last; if (d LA_LIMIT || d -LA_LIMIT) { /* 第一级限幅 */ v la_last; } else { la_last in; v in; } la_sum - la_buf[la_pos]; /* 第二级滑动窗口平均 */ la_buf[la_pos] v; la_sum v; la_pos (la_pos 1) % LA_WIN; return (int16_t)(la_sum / LA_WIN); }两个参数的标定顺序是先标限幅阈值用上一章说的1/3经验法再标窗口N。如果限幅阈值偏小阶跃响应会出现阶梯状爬升反之毛刺拦不住。出现阶梯时先加大阈值而不是缩小窗口。这个组合非常适合STM32做电机电流采样既有快速变化又有斩波毛刺。4.2 一阶滞后滤波一阶低通滤波定点化α系数与截止频率typedef struct { int16_t y; /* 上一拍输出 */ uint8_t alpha; /* 0~255表示alpha/256 */ } lag_state_t; int16_t flt_lag_proc(flt_obj_t *o, int16_t in) { lag_state_t *s (lag_state_t *)o-state; int32_t v ((int32_t)s-alpha * in (256 - s-alpha) * s-y) 8; s-y (int16_t)v; return s-y; }这里把浮点系数α映射成0到255的定点数避免在MCU上跑浮点。α的物理意义是本次采样值的信任度α1时直通不过滤α→0时输出几乎不变化。它与采样周期T和滤波时间常数τ的关系为 τ ≈ T·(1−α)/α反解出 α ≈ T/(Tτ)。例如采样周期1ms想要τ100ms则 α≈1/101≈0.01映射到定点就是3左右。注意α过小时输出非常钝PID环路上慎用容易把相位裕度吃光。如果被滤对象有明显可预测的运动模型一阶滞后不够用卡尔曼滤波是更完整的替代方案本质是把预测值和新息按方差动态分配权重。4.3 加权递推平均滤波旧样本按距离打折递推平均的弱点是窗口内老样本和新样本权重相同信号连续上升时输出滞后明显。加权递推平均给越新的样本分配越大权重改善滞后#define WWIN 8 /* 窗口长度和权重数组一一对应 */ static const uint8_t wtab[WWIN] {1, 2, 3, 4, 5, 6, 7, 8}; static const uint16_t wsum 36; /* 12...8 */ static int16_t wbuf[WWIN]; static uint8_t wpos; int16_t flt_wslide_proc(int16_t in) { int32_t acc 0; int i, idx; wbuf[wpos] in; wpos (wpos 1) % WWIN; for (i 0; i WWIN; i) { idx (wpos i) % WWIN; /* 从最旧到最新 */ acc (int32_t)wbuf[idx] * wtab[i]; } return (int16_t)(acc / wsum); }权重数组按越旧越小排列加权后的输出对新数据倾斜。代价是每次输出都要循环N次做乘法8个点的乘法在STM32上开销可忽略。权重分配没有统一标准线性递增数组是最常用的想更钝就把尾部权重加大想更跟手就把权重集中到最新2到3个点。总权重不能超过int16范围8点线性数组最大值为36×4095落在安全区间。同样的环形缓冲初始为0需要预热窗口。4.4 消抖滤波连续N次一致才认可#define DEBOUNCE_N 8 typedef struct { uint8_t first; uint8_t cnt; int16_t last; } debounce_state_t; int16_t flt_debounce_proc(flt_obj_t *o, int16_t in) { debounce_state_t *s (debounce_state_t *)o-state; if (s-first) { s-last in; /* 首拍直接用真实值兜底 */ s-first 0; return in; } if (in s-last) { if (s-cnt DEBOUNCE_N) s-cnt; } else { s-cnt 0; } if (s-cnt DEBOUNCE_N) s-last in; return s-last; }消抖滤波的思路是连续N次采样值完全相同才承认稳定用来处理按键电平、光电开关、限位信号这类稳态加跳变的输入非常合适N8对应8个采样周期约8到16ms的确认时间。但对连续模拟量它几乎没有平滑能力ADC噪声导致相邻值总在变cnt永远清零输出就是原始值。把它用到模拟量上是常见误用这组代码里特意加了first标志避免首采值是满量程时消抖逻辑卡死。4.5 限幅消抖滤波阈值加计数的双判据最后一个是限幅消抖本质是限幅和消抖叠加typedef struct { int16_t last; int16_t max_delta; uint8_t cnt; uint8_t first; } ld_state_t; int16_t flt_lim_debounce_proc(flt_obj_t *o, int16_t in) { ld_state_t *s (ld_state_t *)o-state; int16_t d; if (s-first) { s-last in; s-first 0; return in; } d in - s-last; if (d s-max_delta || d -s-max_delta) { s-cnt 0; /* 跳变太大不承认 */ return s-last; } if (in s-last) { if (s-cnt DEBOUNCE_N) s-cnt; } else { s-cnt 0; } if (s-cnt DEBOUNCE_N) s-last in; return s-last; }与消抖滤波不同的是多了一道限幅门超过max_delta的瞬间变化直接丢弃不进消抖计数因此能对付单个大幅毛刺加缓慢漂移的叠加信号。代价是参数有两个阈值和计数N。标定时先固定N8用信号源注入幅值为阈值1.2倍的阶跃观察输出是否出现半个周期的伪响应。到这里十种算法的实现与参数讨论结束真实项目不会全上多数是选2到3种做组合。5. STM32里接ADC滤波DMA配置、调用时机与选型十大滤波算法本身和芯片厂商无关51、STM32、K210都能用但要做成稳定的ADC采样链路需要处理采样触发、数据搬运和滤波调用时机三个问题。5.1 定时器触发ADCDMA的STM32最小配置ADC连续采样在STM32里最省CPU的接法是定时器TRGO事件触发ADC启动ADC完成转换后自动DMA搬运到内存CPU只关心DMA传输完成标志。CubeMX里三处配置TIM3在update事件上输出TRGO周期设为采样周期1kHz就是1msADC1开启Regular conversion触发源选Timer 3 Trigger OutDMA设为Circular循环模式数据宽度Half Word内存地址指向uint16_t数组初始化完成后CPU主循环不参与单点采样只有DMA回调和滤波函数在跑#define ADC_BUF_LEN 64 static uint16_t adc_buf[ADC_BUF_LEN]; static volatile uint8_t frame_flag; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { frame_flag 1; /* 通知主循环一帧数据已就绪 */ } }5.2 滤波调用时机中断做轻量、主循环做重活DMA循环模式会自动填满数组无需每条数据都触发CPU中断。可以在中断里做一次限幅防止毛刺污染缓冲区到主循环里再做平均不要在DMA回调里做排序排序耗时不确定放在主循环反而安全int main(void) { while (1) { if (frame_flag) { frame_flag 0; for (int i 0; i ADC_BUF_LEN; i) { adc_buf[i] flt_limit(adc_buf[i]); /* 第一级掐毛刺 */ } int32_t acc 0; for (int i 0; i ADC_BUF_LEN; i) { acc adc_buf[i]; /* 第二级整帧平均 */ } control_value (int16_t)(acc / ADC_BUF_LEN); } } }用frame_flag做握手保证DMA写数组时主循环不会读同一位置。如果DMA长度超过一帧可以考虑双缓冲Ping-Pong配合DMA半满中断哪半边写完就处理哪半边。这个结构在稳定性和实时性上远好于在定时器中断里逐点滤波是ADC加滤波链路的常用框架。5.3 十大滤波算法在STM32上的选型对照表下表给出一套可直接抄的选型思路按信号类型到算法组合到参数组织。参考采样周期在1ms量级时可以照抄采样周期差异大时按2.3节的公式换算。信号类型典型应用算法组合参数温度/湿度NTC采集、温室控制、鱼缸恒温一阶滞后 或 递推平均N16α≈0.01N16压力/称重电子秤、液位传感器中位值平均N5 递推平均N8先中值再去抖动电机电流变频驱动、堵转检测限幅 递推平均N8阈值取满量程2%按键/开关键盘扫描、限位开关消抖或限幅消抖N8电池电压充电监控、低功耗设备限幅 加权递推N8阈值0.5V对应ADC码5.4 外部ADC和内部ADC的滤波差异用外部ADC时滤波策略要调整。外部ADC单次读取往往走I2C或SPI串行完成采样周期远长于内部ADC可能到几十毫秒量级此时滑动窗口N8会让阶跃响应滞后接近0.4秒。这种情况下优先用限幅加一阶滞后小幅牺牲平滑度保住响应速度。另外外部ADC的增益误差和偏移误差要用三点校准去修正滤波只管噪声管不了基准误差这点和内部ADC的校准流程是一样的。6. 阶跃响应验证与滤波参数自适应技巧滤波参数标得对不对不能靠肉眼观察串口波形。最有效的验证方法是阶跃响应加脉冲注入双测试用电阻分压器产生可瞬间切换的电压从0V直接跳到满量程半程同时用串口或调试器记录滤波前后的值。打印出的序列里看三件事第一阶跃到来后输出经过多少个采样周期进入2%误差带这个周期数应当约等于窗口N或一阶滞后时的3τ第二输出是否出现超调或阶梯爬升出现阶梯说明限幅阈值偏小出现超调说明加权系数过于集中第三在稳态段手动碰一下信号线看尖峰是否被压到满量程1%以内。参数自适应可以做得更细。对ADC输入做一阶差分当差分绝对值连续3次超过限幅阈值时认为发生的是真实阶跃而不是噪声自动把一阶滞后系数从强平滑临时切到快速跟随输出跟上后再切回强平滑uint8_t warm; /* 连续超阈值次数 */ int16_t last_raw; uint8_t alpha_now; int16_t y; static int16_t adaptive_lag(int16_t in) { int16_t diff in - last_raw; int16_t absd (diff 0) ? diff : (int16_t)-diff; last_raw in; if (absd LIMIT_THR) { if (warm 3) alpha_now 77; /* 0.3*256加速跟随 */ } else { warm 0; alpha_now 3; /* 0.01*256强平滑 */ } y ((int32_t)alpha_now * in (256 - alpha_now) * y) 8; return (int16_t)y; }三个参数要一起调LIMIT_THR决定什么算真实阶跃warm决定需要几拍确认alpha_now的切换值决定跟随多快。切换瞬间输出不会跳变因为y的递推式是连续的α变化只改变后面每一拍的收敛速度。这个方法用在PID启动阶段和电池电压跳变场景里能同时拿到稳态低噪声和阶跃快响应。最后留一个自查点当滤波序列在静置时均值仍然缓慢漂移而外部示波器确认电压稳定问题不在算法去查参考电压和PCB布局。滤波算法只能处理采样值里的噪声处理不了采样基准本身的漂移电源噪声和时钟抖动引起的基准偏移交给模拟域的布局和去耦电容解决。本文还有配套的精品资源点击获取