空调温控读数和传感器信号里的毛刺,很多人第一反应是上卡尔曼滤波或者什么高级算法。其实大部分场景根本用不上那么重的手段,一个一阶RC低通滤波器,几行C代码就能解决掉绝大多数噪声问题,而且效果非常直观。
这个标题把空调温控和信号降噪放一起,我觉得挺妙的。这两个场景我恰好都做过实际项目,一个是用Arduino读取DS18B20温度时数据跳得厉害,另一个是STM32采集电流互感器信号时叠加了高频干扰,最后都是靠一阶RC低通滤波搞定的。如果你正在为ADC数据抖动、传感器读数不稳定、PWM解调之后的纹波发愁,这篇内容应该能帮你把问题看清。
我把从原理推导到C语言实现、再到两个平台上调参踩坑的过程完整梳理一遍。内容不涉及复杂的数学推导,重点放在“怎么快速用起来”和“出了问题怎么排查”,适合刚接触嵌入式滤波的开发者,也适合已经写过滤波代码但想搞懂参数含义的朋友。
1. 一阶RC低通滤波器的本质:它为什么能把尖刺磨平
先看一个最简单的物理模型。一个电阻串联一个电容,输入端接在电阻前面,输出端从电容两端取,这就是教科书里的一阶RC低通滤波器。电容对高频信号的阻抗很低,高频成分被旁路到地;低频成分阻抗高,能顺利传到输出端。这个电路的截止频率是1/(2πRC),高于这个频率的信号,幅度会以每倍频程6dB的速度衰减。
数字域里做的低通滤波,本质上是把这个模拟电路的微分方程离散化。用C语言写出来的代码虽然看不见电阻电容,但行为上跟硬件RC电路一模一样。你给滤波器丢进去一个剧烈跳变的信号,它吐出来一个平滑缓变的轨迹,这个过程就是所谓的“磨平”。
为什么空调温控会需要这个?温度传感器读取到的数据,通常包含了环境温度的真实变化,也包含了传感器自身的量化噪声、 ADC采样误差、甚至室内风机的气流扰动。温度本身变化很慢,比如一秒变化0.1度,但噪声可能几百毫秒就跳一次。如果你直接把原始读数拿去控制压缩机,你会发现压缩机频繁启停,继电器咔哒咔哒响个不停。这就是因为噪声被当成了真实的温度变化。
这里有一个关键概念叫时间常数τ,数值上等于RC的乘积。它决定了滤波器对输入变化的响应速度:τ越大,滤波越平滑,但反应也越迟钝;τ越小,输出越贴近原始信号,但滤噪能力弱。数字实现里对应一个叫α的系数,这个系数跟采样周期和τ直接相关。
理解这个本质之后,你就明白了:一阶RC低通滤波器不是把任何频率一刀切干净,而是衰减高频、保留低频。所以用它的前提是,你关心的信号变化速率明显低于你要滤除的噪声频率。如果信号本身就很高频,还用低通滤波,那滤掉的其实是真正的信息。
1.1 空调温控里的真实痛点:为什么原始温度读数不能直接用于控制
用Arduino做恒温控制的典型接法是:DS18B20或者其他温度传感器,每隔几百毫秒读取一次温度,然后比较设定值,决定加热丝或压缩机的开关。这听起来很简单,但实际跑起来你会发现,读取到的温度值在目标温度附近反复横跳,比如设定25度,实际在24.7到25.3之间来回波动。每次波动都触发一次控制逻辑,继电器频繁吸合释放。
问题出在读取链路里。DS18B20是12位分辨率的数字传感器,本身就存在±0.5度的量化误差;再加上传感器封装的热惯性、空气流动、电源波动,最终读到的值会叠加不少随机成分。控制逻辑如果直接吃这个值,相当于在跟噪声做博弈,系统永远不会稳定。
解决办法就是在读取和控制器之间加一层低通滤波。把滤波后的温度作为控制输入,压缩机就不会被瞬时尖刺触发。我实际项目中,把滤波时间常数设在10到20秒,温控器的继电器动作频率明显下降,压缩机启停次数减少了大概一半以上。房间温度的波动范围反而收敛了,因为压缩机不会频繁启停,系统整体热惯性更平稳。
这里有个细节要注意:滤波不是越多越好。时间常数过大,温度传感器测到的升温趋势会被严重滞后,可能导致过冲。比如你设定25度,实际温度已经到25.5度了,滤波后的读数还是24.8度,加热器还在拼命工作。这种滞后在温度控制环路里会引起振荡。所以要找到合适的τ,不能一上来就拍脑袋选个很大的值。
1.2 信号降噪场景:ADC采集里的高频毛刺怎么被压下去
另一个典型场景是用STM32的ADC采集模拟信号。比如你做一个电流监测模块,电流互感器输出经过放大电路之后进入ADC,采样率设在1kSPS。这时候你会发现采集到的数值不仅仅包含50Hz的工频信号,还混入了一堆几百赫兹到几千赫兹的开关噪声,可能是开关电源的纹波,可能是MOSFET开关导致的电磁干扰。
如果直接用这个信号去做阈值判断或者计算有效值,结果会非常不稳定。同一个负载,不同时间读取到的有效值能差出好几个百分点。这时候用一阶RC低通滤波,将截止频率设在几十赫兹,把高频干扰压下去,ADC采样值就会变得非常干净。
我做过一个基于STM32的交流电流监测模块,采样率1kHz,截止频率设计在15.9Hz,α取0.09左右(和滤波器系数对应),原始ADC值从±50的波动大幅降低到±5以内,有效值计算精度明显提升。这个场景下,滤波对实时性的影响微乎其微,因为工频信号本身只有50Hz,15.9Hz截止频率虽然会衰减一点信号幅度,但可以通过校准补偿掉。
2. 一阶RC低通滤波器的C语言实现:从浮点到整数,从简陋到工程化
理论说完了,现在说代码。一阶RC低通滤波器的数字实现,核心就一个公式。假设output_prev是上一次的输出值,input是当前输入,alpha是滤波系数(0到1之间),那么当前输出:
output = output_prev + alpha * (input - output_prev)这就是最经典的低通滤波递推公式,叫指数移动平均(EMA),本质就是一阶RC的离散化。理解这个公式的几何意义,比背公式重要得多:它表示“这一次的输出,在上一次输出的基础上,往输入的方向靠近了一小步,这一小步的比例就是α”。α越大,靠近得越快,滤波越弱;α越小,靠近得越慢,滤波越强。
用C语言写,浮点版本两行就结束,十几K的Flash就能跑。但工程上你可能遇到两个问题:一是目标MCU没有FPU,浮点运算慢;二是你不想在滤波上消耗太多CPU周期。这就引出了整数实现和查表法优化。
2.1 最基本也最容易理解的浮点实现
浮点版本适合带FPU的单片机,比如STM32F4、STM32H7系列,以及Arduino Due这类32位板子。代码长这样:
typedef struct { float output; float alpha; } LowPassFilter_t; float LowPassFilter_Update(LowPassFilter_t *f, float input) { f->output += f->alpha * (input - f->output); return f->output; }这个结构体封装的方式是我个人推荐的做法。为什么不直接用一个全局变量加一个宏?因为一个项目里可能有多个通道需要滤波,每个通道的时间常数还不同。用结构体,每个通道创建自己的实例,代码可读性和可维护性都更好,滤波器的初始化和更新逻辑也清晰。
作为一个只有一句话的滤波器,它最容易被轻视。但恰恰因为它简单,很多人会忽略一个关键点:结构体里的output成员必须要初始化。如果忘记把output设置成第一次输入的值,那么滤波器的初始响应会经历一个从0爬升到输入值的过程,引发很大的启动过冲。这在温控场景里可能造成开机阶段的误动作。
所以滤波器的初始化函数不能留空,至少要有这样的操作:
void LowPassFilter_Init(LowPassFilter_t *f, float alpha, float initial_output) { f->alpha = alpha; f->output = initial_output; }调用的时候,第一次读取传感器数据后,把原始值作为initial_output传进去,滤波器就从“当前状态”开始工作,而不是从0开始爬升。
2.2 整数定点实现:没有FPU的单片机怎么优雅滤波
Arduino Uno用的ATmega328P没有硬件浮点单元,Ultra上的浮点运算全靠软件模拟,速度慢不说,还占用大量Flash。但整数运算就快得多。你能用整型实现一阶低通滤波吗?能,而且只损失一点精度。
思路是把浮点α转换为一个整数比例因子。先看一个常用实现:
typedef struct { uint32_t output; uint16_t alpha_q; // 0~1024, 相当于0.0~1.0 } LowPassFilter_Int_t; uint32_t LowPassFilter_Update_Int(LowPassFilter_Int_t *f, uint32_t input) { f->output = (uint32_t)(((uint64_t)(1024 - f->alpha_q) * f->output + (uint64_t)f->alpha_q * input) / 1024); return f->output; }这里我把α映射到0到1024之间的整数,alpha_q是定点表示的α。换算关系是alpha_q = (uint16_t)(alpha * 1024),比如α=0.1时,alpha_q = 102。计算时用(1024 - alpha_q)替代1 - alpha,即用整数乘法替代浮点乘法,最后除以1024完成归一化。
为什么要用1024而不是1000?因为1024是2的10次方,编译器会把除以1024优化成右移10位,性能极佳。如果你的MCU支持64位整数乘法(Cortex-M0+也有硬件64位乘法指令),这里用uint64_t做中间变量,乘积不会溢出。如果MCU比较弱、不支持64位整数,可以改为32位乘法和移位配合,但需要小心溢出,这是一个值得注意的进阶话题。
整数版本同样支持多个通道,结构体方式一样。实际测试下来,在Arduino Uno上,同样的滤波运算,整数版本耗时大约是浮点版本的十分之一,Flash占用也小了不少。
2.3 提升版实现:加入时间尺度自适应和启动防抖
整数实现解决了一般场景,但工程里有两个高频需求:一是滤波系数α要支持运行时调整(比如用户可调“灵敏度”),二是不希望每次修改α都需要重新推导公式。这时候可以用一个带“采样时间参数”的版本。
定义滤波器时,不再存α,而是存时间常数τ(单位毫秒)和采样周期T(单位毫秒),每次更新时根据这两个值计算动态α:
alpha = 1 - exp(-T / τ)这个公式是一阶RC离散化的精确解。它源自连续时间系统的阶跃响应,在采样周期远小于时间常数时,近似等于T/(τ+T)。实际工程中我建议直接用这个指数公式,因为很多场景下,采样周期不固定(比如Arduino的delay()不精确),用τ和T来描述滤波器行为更直观,调参也更符合直觉。
对应的代码:
typedef struct { float output; float tau; // 时间常数,单位ms float ts; // 采样周期,单位ms } LowPassFilter_Time_t; void LowPassFilter_Init_T(LowPassFilter_Time_t *f, float tau_ms, float ts_ms, float initial_value) { f->tau = tau_ms; f->ts = ts_ms; f->output = initial_value; } float LowPassFilter_Update_T(LowPassFilter_Time_t *f, float input) { float alpha = 1.0f - expf(-f->ts / f->tau); f->output += alpha * (input - f->output); return f->output; }这个版本的优势在于:你可以在运行时修改tau或ts,不需要重新计算α。它也让滤波器描述变得非常可读——“时间常数10秒,采样周期200毫秒”,任何人都能理解。代价是需要expf()函数,如果MCU的数学库不完善,可能要多占用一些Flash,但通常可以接受。
启动防抖同样重要。理想状态下,滤波器输出应该是一个平滑的曲线。但如果输入信号突然有一个大跳变,滤波器的输出需要很长时间才能跟上。这时候你可能需要额外的“快速启动逻辑”:在滤波器初始化或者检测到输入跳变超过阈值时,直接把输出值拉近输入,缩短暂态时间。这个技巧在很多实时控制系统里非常实用。
我把这个整理成一个小功能:
void LowPassFilter_Update_WithBypass(LowPassFilter_t *f, float input, float threshold) { if (fabsf(input - f->output) > threshold) { f->output = input; // 大跳变时直接跟踪 } else { f->output += f->alpha * (input - f->output); } }这个逻辑我常用于温控场景里处理“开门瞬间的冷热冲击”——如果温度突然下降超过5度,立即把输出坐标跳到当前值,让加热器快速响应;否则就正常滤波,防止噪声引发误动作。
3. Arduino上的实战调参记录:从抖动到稳定,我踩过哪些坑
Arduino平台是很多人入门滤波的第一站,操作简单,但正因为简单,反而容易忽略很多细节。我在Arduino Uno和NodeMCU(ESP8266)上都跑过一阶RC低通滤波,这里把调参过程和坑记录下来。
3.1 温度传感器滤波案例:确定采样周期,再确定滤波系数
用DS18B20测温的典型代码里,读取频率固定在每250ms一次。如果你要滤波,首先要定采样周期T,因为它直接决定了截止频率和α的对应关系。我测试时先把温度读取频率固定下来,然后根据需要的响应速度去选时间常数τ。
比如希望滤波后的温度读数在5秒内跟上真实温度的阶跃变化(升温过程),那么τ设2秒左右比较合适。α = 1 - exp(-0.25/2) ≈ 0.1175。滤波效果:原始读数波动约±0.5度,滤波后波动约±0.1度,而且对变化的响应大概在5到8秒内完成,完全满足恒温控制需求。
我写这段程序时用到了之前封装的结构体和带时间常数的实现,在Arduino的loop()里调用:
#include <math.h> LowPassFilter_Time_t tempFilter; float filteredTemp; void setup() { Serial.begin(115200); // 初始化:时间常数2000ms,采样周期250ms,初始温度25.0 LowPassFilter_Init_T(&tempFilter, 2000.0f, 250.0f, 25.0f); } void loop() { float rawTemp = readTemperature(); // 假设这个函数返回DS18B20读数 filteredTemp = LowPassFilter_Update_T(&tempFilter, rawTemp); // 用filteredTemp做控制逻辑,而不是rawTemp controlHeater(filteredTemp); delay(250); // 和采样周期保持一致 }这里一个容易犯的错误是:在loop()里用delay()实现定时间隔,但delay()并不是精确的定时器。如果你的readTemperature()耗时不固定,或者中断服务函数占用了时间,实际的采样周期会有抖动。采样周期抖动在滤波领域会影响截止频率的稳定性,可能导致滤波器行为不如预期。更稳妥的做法是用定时器中断或者millis()来实现精确的固定采样周期。
3.2 滤波系数的调参节奏:先定时间常数,再定采样周期,最后看波形
很多新手拿到滤波器之后,直接拍脑袋设一个α=0.2,运行之后发现要么太迟钝、要么没效果,然后开始怀疑算法本身。实际上α、T、截止频率三者是一体的,忽略任何一个变量都会造成错乱。
我的调参套路分三步:第一步确定采样周期T,根据你对信号实时性的需求来定,温控场景250ms到1秒,电机电流采集1ms到10ms。第二步根据滤除噪声的最低频率要求,定截止频率fc,然后用公式fc = 1/(2πτ)算出τ,再算α。第三步运行起来看波形,如果滤波后滞后过重,调大α(减小τ);如果还有毛刺,调小α(增大τ)。
这个三步法听起来朴素,但实测非常有效。它让你的调参有据可依,而不是靠感觉。例如,你要滤除100Hz以上的噪声,T=1ms,那么至少需要截止频率远低于100Hz,比如20Hz。那么τ = 1/(2π×20) ≈ 8ms。α = 1 - exp(-1/8) ≈ 0.1175。有了这个起点,再面对实际波形微调,就快很多。
我之前用串口绘图仪直接观察滤波前后的数据,是调参过程中最直观的手段。在Arduino上配合Serial Plotter,把原始温度、滤波后温度、开关状态同时发出去,很快就能发现滤波器有没有起作用、滞后是不是过重,比断点调试高效太多。
3.3 Arduino里容易忽略的坑:浮点精度、全局变量和中断上下文
Arduino Uno的float只有32位,精度约7位有效数字,对滤波结果来说完全够用。但如果你在中断服务函数里做滤波,要小心两点:一是float运算在中断里处理时间较长,会破坏时序;二是如果滤波器输出变量在中断里被修改,在主循环里读取它,虽然不会像多字节变量那样容易撕裂,但为了稳妥,还是建议加个“原子访问”的标记,或者在读取时短暂关中断。
另一个坑是:Arduino的String类和delay()混用很容易造成内存碎片,但跟滤波器本身关系不大,就不展开了。如果滤波使用在ADC采样中断里,推荐把滤波器的结构体声明为volatile,并采用整数定点版本,把中断处理时间压到极致。整数运算在中断里的耗时远小于浮点,这对高采样率的场景非常有意义。
我在实际做Arduino智能小车时也用过这个滤波器。小车的红外传感器或者超声波测距数据在运动时波动特别大,用低通滤波后,转向控制平滑了很多。值得注意的是,小车场景的采样周期很短(20ms到50ms),时间常数如果太大,障碍物检测的实时性会变差,所以τ设500ms以内比较合适。
4. STM32上的滤波实现细节:从寄存器到工程化,这些处理比算法更重要
STM32上跑一阶RC低通滤波,代码本身和Arduino差异不大,但工程化的讲究更多。毕竟STM32项目通常更复杂,滤波往往只是信号链里的一环,一旦滤波出了问题,排查起来更麻烦。
STM32的ADC采样触发方式、DMA传输、定时器中断、以及FreeRTOS的任务调度,每一项都可能影响滤波器的表现。滤波算法本身写不好,顶多结果不理想;但滤波器和它的调用环境配合不好,结果就可能出现“输出有时会跳变一下”这种难以复现的诡异问题。
4.1 采样率和中断优先级对滤波器的影响:时序稳定是第一位
我做一个STM32F103的电流采集项目时,ADC采样由定时器触发,每次采样完成触发DMA传输,DMA传输完成中断里做一次滤波。采样率是1kHz(周期1ms),滤波器的时间常数τ=20ms,α大约等于0.05。
这个设计里最关键的是时间稳定性:中断里的滤波运算必须保证在下一个采样周期到来之前完成,否则就会“丢拍”,滤波器结果会出现抖动。在F103上,这个滤波运算本身耗时不到1微秒,但中断函数入口出口的开销,加上DMA搬运时间,必须降级评估。更关键的是,如果更高的优先级中断(比如通信中断)抢夺CPU,滤波中断可能会被延迟,导致采样周期偶尔变成2ms、3ms。这时候滤波器的时间常数不变,但α对应的采样周期变了,滤波特性就会飘。
用之前的带时间常数版本(LowPassFilter_Time_t)能部分缓解这个问题,因为它每次更新时会根据实际的采样时间计算α。但要真正做到精确,需要在中断里记录时间戳,在滤波函数里传入真实的采样间隔。代码可以这样扩展:
typedef struct { float output; float tau; } LowPassFilter_Time_Tick_t; float LowPassFilter_Update_Tick(LowPassFilter_Time_Tick_t *f, float input, float dt_ms) { float alpha = 1.0f - expf(-dt_ms / f->tau); f->output += alpha * (input - f->output); return f->output; }然后在定时器中断里,先读取当前时间戳,算出与上次滤波的间隔dt,再调用函数。这么做多花几行代码,换来的是滤波器行为对中断延迟不敏感,非常值得。
4.2 Multi-channel滤波管理:结构体数组和通道状态机
STM32项目里滤波通道往往不止一个。比如一个三相电流检测模块,至少要同时滤波三路电流信号,也许还要加一路母线电压。如果每个通道复制粘贴一套代码,会非常糟糕。
用结构体数组加上通道索引,可以轻松管理:
#define FILTER_CH_NUM 4 LowPassFilter_Time_t filters[FILTER_CH_NUM]; void FILTER_InitAll(float tau) { for (int i = 0; i < FILTER_CH_NUM; i++) { LowPassFilter_Init_T(&filters[i], tau, 1.0f, 0.0f); } } void FILTER_ProcessADC(uint16_t *adcValues, float *filteredValues) { for (int i = 0; i < FILTER_CH_NUM; i++) { filteredValues[i] = LowPassFilter_Update_T(&filters[i], (float)adcValues[i]); } }在ADC的DMA传输完成中断里,直接调用FILTER_ProcessADC()即可。这样即使通道数增加,代码结构也不用大改。
如果你用FreeRTOS,滤波可以在ADC采集任务里完成。任务延时函数用精确的vTaskDelayUntil(),采样周期可以保持稳定。要注意的是,滤波计算不要放在中断里,留给任务去做,降低中断负荷。DMA传输的中断只负责置一个“数据就绪”的标记位,任务检测到这个标记后取数据、滤波、输出。
4.3 STM32滤波器性能考量:从Cortex-M3到M7,有多少优化空间
不同STM32系列的CPU性能差异巨大,滤波代码性能评估也不同。Cortex-M3(F103)没硬浮点,浮点滤波虽然也能跑,但最好用整数定点;Cortex-M4F(F405/407)有FPU,浮点滤波性能和整数滤波性能相当,直接用浮点是最优选;Cortex-M7(H743)不仅浮点快,还带DSP指令,但滤波本身太简单,DSP指令帮助不大,反而是一些优化宏和编译器选项更重要。
对滤波代码来说,最大的性能优化其实是避免不必要的大循环和函数调用开销。如果滤波调用频率很高(比如10kHz采样率),内联函数比单次函数调用有优势:
static inline float LowPassFilter_Update_Inline(LowPassFilter_t *f, float input) { f->output += f->alpha * (input - f->output); return f->output; }GCC的-O2优化级别下,编译器会自动把短小且被高频调用的函数内联。如果不放心,可以显式加上static inline。但要注意:如果滤波函数很大、调用次数少,内联反而浪费Flash,所以这个优化只在采样率极高、调用很频繁时才有意义。
在F407上带着FPU跑浮点滤波,我测过调用周期大约60ns,几乎可以忽略;在F103上跑整数滤波,大约200ns,也完全可以接受。所以大多数情况下,滤波算法的CPU占用不是瓶颈,真正要花时间优化的是采样链路。信号链设计好,滤波只是顺手的事。
5. 从空调温控到SPWM信号解调:滤波应用之外的几个追加技巧
写到这里,你已经看到一阶RC低通滤波器在温度控制、ADC信号采集上如何发挥价值。但滤波器本身还可以做一些超出“低通滤波”本身的应用容错,我分享几个我实际用到的追加技巧。
5.1 滤波结果作为控制反馈:PID控制器里的“微分先行”思路
在温控系统里,直接拿原始温度做PID控制,微分项(D)对噪声极其敏感。因为微分是采集信号的斜率,噪声一抖,微分计算出来的值可能瞬间放大几倍,造成控制器输出抖动。把低通滤波放在PID之前,可以显著改善这种情况。
更高级的做法是“微分先行”:只对反馈量的微分做滤波,而不是对所有反馈量滤波。这样既能抑制噪声,又不至于让PID的比例和积分项变得迟钝。这个思路在很多工业仪表里都有应用,你可以根据自己的控制需求决定。
如果你只是想做简单恒温,可以在控制逻辑之前加一个一阶低通滤波,然后仅用比例和积分控制,不启用微分。这样系统既平稳又简单,非常可靠。
5.2 PWM转模拟信号:低通滤波在输出端的妙用
低通滤波不只可以处理输入信号,还可以用于输出端。STM32的DAC通道有限,很多板子只有一路DAC,这时候你可以用一个PWM引脚加一个RC硬件电路来输出模拟电压。做法是:用定时器输出固定频率的PWM,PWM的占空比对应目标电压,再经过一个RC低通滤波电路得到平滑的模拟信号。
这个场景下需要计算硬件RC电路的时间常数。比如PWM频率20kHz,想要残余纹波小于1%,RC截止频率要低于PWM频率的1/20左右,也就是低于1kHz。取1/(2πRC) ≈ 500Hz,R=1kΩ时C≈0.318μF,取标准值0.33μF即可。
有人会问,这个场景和软件滤波有什么关系?关系在于:你可以通过软件补偿RC电路的滞后效应。比如你目标电压3.3V,但RC电路实际输出需要时间上升,你可以先用软件对目标值做一阶高通滤波补偿,让实际输出更快到达目标值。这种做法叫做“软件预加重”,在信号发生器里有广泛应用。
5.3 多级级联滤波:当一阶不够用时,怎么办
一阶RC低通滤波的衰减斜率是20dB/decade,也就是频率每增加10倍,增益下降20dB。如果你需要更陡峭的衰减(比如对50Hz以上的噪声有更强抑制),有两种思路:一是级联多个一阶滤波,比如两个一阶滤波串联,得到二阶低通,衰减率变为40dB/decade;二是换用Butterworth或者Bessel滤波器,这些滤波器有更好的通带平坦度和相位特性。
用软件实现级联很容易,就是上一个滤波器的输出作为下一个滤波器的输入:
typedef struct { LowPassFilter_Time_t stage1; LowPassFilter_Time_t stage2; } LowPassFilter_2nd_t; float LowPassFilter_2nd_Update(LowPassFilter_2nd_t *f, float input) { float tmp = LowPassFilter_Update_T(&f->stage1, input); return LowPassFilter_Update_T(&f->stage2, tmp); }需要注意的是,二阶滤波不要简单地使用两个相同时间常数的一阶级联,因为那样响应曲线会有比较大的过冲和相位滞后。更合理的方式是让两级的截止频率有一定分离,第一级偏高、第二级偏低,或者根据标准的二阶滤波器设计公式来分配系数。这个讨论已经超出本文范围,但知道有这种扩展方向就够了。
6. 常见问题排查实录:滤波器失效的七大原因
滤波器代码本身只有一行,但实际项目中“滤波没效果”或者“滤波后波形异常”的情况很常见。我梳理了七个高频故障点,整理成速查表,方便你直接对照排查。
6.1 常见故障速查表
现象:滤波后波形仍然有很多毛刺
原因大概率是α设置太大,截止频率偏高,噪声没有被有效滤除。或者采样周期T和α不匹配,实际α对应的截止频率跟预期不符。解决方式是按照fc=1/(2πτ)重新计算α,并往小调。
现象:滤波后输出滞后严重,反应迟钝
原因是τ太大,滤波器截止频率太低。如果用在控制环路里,可能引发系统振荡。解决方法是逐步减小τ,观察波形直到满意的响应速度。
现象:滤波输出启动时从0爬升,持续很长时间才达到真实值
原因是初始化时没有把output设置为第一次输入值。需要在初始化时传入初始输入值,或者第一个采样点直接旁路滤波。
现象:滤波结果偶尔跳变
优先检查采样周期是否稳定,是否有中断延迟导致“丢拍”。另外检查输入数据本身是否偶尔出现极大异常值(比如传感器偶发读数为0),需要在滤波前做简单的限幅处理。
现象:整数定点滤波结果有偏差或溢出
需要检查alpha_q和1024-alpha_q的乘积是否溢出32位。如果是32位乘法,建议把中间变量提升到64位,或者采用分段移位限幅的方法。
现象:实时性要求高的场景用浮点滤波导致卡顿
如果是AVR系列或Cortex-M0,不建议用浮点。换整数定点,或者在带FPU的平台上启用编译器的FPU优化选项。
现象:滤波器在不同环境下表现不一样
大概率是采样周期不稳定导致α对应的实际效果飘忽。建议改用带时间戳的滤波版本,输入真实的采样间隔,让α动态修正。
6.2 实战定位思路:用数据说话,别拍脑袋
排查滤波器问题,不能只盯着滤波代码看,要从采样源头开始沿着信号链路一步一步查。我的做法是:先把原始信号和滤波后信号同时打印出来,画在串口绘图仪上,对比前100个采样点,观察滤波器的暂态响应是否正确。如果暂态响应正确,稳态还有波动,就是α和截止频率的问题;如果暂态就很怪,那就是初始化、时序或数据异常。
再用一个固定的正弦波或者阶跃信号注入到滤波器的输入(直接在代码里写固定值测试),看滤波器输出是否符合理论响应。这个方法可以帮你快速把“滤波器本身的问题”和“信号源的问题”分开。我自己排查问题时,经常用这个办法节省大量时间。
最后提一个容易被忽略的细节:滤波后的数据如果要传输到上位机或者存到SD卡,最好保留足够的有效位数。比如用整数滤波时,输出不要直接丢弃小数部分,而是以“放大的定点值”进行后续计算,最后再统一缩放。很多精度问题和这个细节有关。
写在最后
滤波器是一行代码,但背后对应的是信号与系统的基础知识。我在实际项目里用过它解决空调温控抖动、电流信号毛刺、PWM解调纹波,几乎每一次都很快见效。如果项目对实时性要求不高,优先考虑一阶RC低通滤波;如果不够,再考虑提升采样率、用二阶滤波或者卡尔曼滤波。工具链从简单到复杂,别一上来就上重型方案。
有一点经验想特别分享:滤波器的参数不要一次性调到完美,先按公式算出理论值,跑起来,再根据波形微调。放在不同的硬件环境下,实际效果会有差异,保持“先看数据,再改参数”的习惯,能少走不少弯路。
后面我会继续分享二阶滤波、卡尔曼滤波在嵌入式里的实际应用,以及在温控PID里如何组合使用这些滤波策略。有具体场景疑问的朋友,欢迎在评论区带上波形图一起讨论。