空调温控面板上的温度数字一直在25℃和26℃之间来回跳,或者舵机在收到PWM信号时末端总在轻微抖动,这两类问题看着不相关,其实都是同一个病根:信号里混入了高频噪声。我以前在做一款小型智能温控器时,被这种跳变折磨了很久,后来发现一个很早的模拟电路知识——一阶RC低通滤波器,用C语言在单片机里重新实现一遍,问题直接消失。
这篇指南我会把一阶RC低通滤波器从数学原理到代码实现完整拆开,分别在Arduino和STM32两套平台上给出可直接运行的C语言代码,并结合空调温控、ADC采样降噪这些实际场景讲清楚参数怎么选、坑在哪里。不管是刚接触单片机的学生,还是做嵌入式项目想快速解决信号抖动问题的工程师,这篇内容都能让你直接抄作业,同时理解背后的原理,不至于只会复制粘贴。
1. 为什么空调温控和信号降噪需要同一个滤波器
1.1 空调温度控制的信号痛点
做空调温控或类似环境监测系统时,最烦的不是传感器精度不够,而是读数不稳定。比如用NTC热敏电阻或DS18B20读取温度,单片机的ADC采集到的原始值总是在目标值附近上下跳动。如果直接用这个跳动值去做继电器开关控制,就会出现压缩机频繁启停、温控面板数字闪烁、风扇转速忽高忽低这些问题。
根源在于传感器信号在传输过程中叠加了各种噪声源:50Hz工频干扰、开关电源的纹波、电机启停造成的电磁干扰,甚至ADC采样本征的量化噪声。这些噪声频率通常远高于温度变化的速率,而温度本身是一个缓变信号,一分钟内变化0.5℃都算剧烈了。所以我们要做的,就是把高频成分削掉,保留低频趋势——这正是低通滤波器的看家本领。
1.2 为什么不用硬件RC电路
传统的做法是在传感器输出端加一颗电阻和一颗电容构成硬件RC低通滤波器。这个方案在模拟电路时代是标准答案,但在单片机项目里其实不太方便。以Arduino Uno和STM32为例,大部分开发板已经有板载滤波电容,你要再加一级RC,就得动电路板、算阻容值、焊接,而且一旦需要调整截止频率,就得换元器件。
用软件实现一阶RC低通滤波器,等于把那颗电阻和电容“写”进了代码里。改一个宏定义或者一个变量的值就能调滤波强度,零成本、免焊接、可复用。温度、光照、湿度、电压采样、电位器信号,只要是缓变信号,都能用同一套代码处理。这也是我最终选择用C语言在MCU里做数字滤波的核心原因:它把硬件问题变成了软件参数问题。
1.3 数字一阶RC滤波的应用范围
在Arduino智能小车项目里,循迹传感器的红外值波动会造成方向误判,用一阶低通做平滑后车子走线明显稳定;在STM32的四开关降压电源项目里,输出电压采样不做滤波,PID控制器会被高频噪声干扰,反馈环路甚至会震荡;即使是简单的按键扫描或编码器计数,加一级低通也能消除毛刺误触发。
所以这篇指南虽然标题里有“空调温控”,但核心方法适用于所有需要平滑ADC数据的场景。学完这个滤波器,你手里就多了一把通用的信号处理瑞士军刀。
2. 一阶RC低通滤波器的数学原理:从电路到差分方程
2.1 模拟RC电路的微分方程
先复习一下硬件RC低通滤波器。一个电阻R和一个电容C串联,输入信号Vin加在串联两端,输出Vout从电容两端取出。电容的阻抗特性决定了它对高频信号呈现低阻抗,对低频信号呈现高阻抗,所以高频成分被旁路掉,低频成分顺利通过,这就是“低通”。
根据基尔霍夫定律和电容的伏安关系,可以得到:
dVout/dt = (Vin - Vout) / (R * C)这里的τ = R * C,叫做时间常数。τ越大,电容充放电越慢,输出跟随输入也越慢,截止频率越低。
2.2 从微分方程到差分方程
单片机是数字系统,只能离散采样,没法真正求解微分方程。我们需要把它转化为差分方程。把微分离散化,用相邻两次采样的差值代替导数:
(Vout[n] - Vout[n-1]) / Δt = (Vin[n] - Vout[n-1]) / τ移项整理后得到:
Vout[n] = Vout[n-1] + (Δt / (τ + Δt)) * (Vin[n] - Vout[n-1])定义滤波系数α = Δt / (τ + Δt),最终形式就是:
Vout[n] = Vout[n-1] + α * (Vin[n] - Vout[n-1])这就是软件一阶RC低通滤波器的核心公式。每次ADC采样得到一个新输入值Vin[n],用上一次的输出值Vout[n-1]做递归运算,得出新的输出Vout[n]。整个过程只需要一次加法、一次减法、一次乘法,计算量小到可以在中断里跑。
2.3 截止频率与α的计算方法
硬件电路里,RC滤波器的截止频率fc = 1/(2πRC),在截止频率处信号幅度衰减到原来的0.707倍(-3dB)。在数字滤波器里,α直接决定了截止频率。
已知采样周期Ts(即每次滤波执行的间隔时间)和目标截止频率fc,可以用下面公式计算:
α = 1 - exp(-2π * fc * Ts)当2π * fc * Ts值很小时,可以用近似公式,即α ≈ 2π * fc * Ts。
举个例子。空调温度采样每500ms采样并滤波一次,即Ts = 0.5s,目标截止频率设为0.1Hz(允许温度信号以周期约10秒以上的变化通过),那么:
α = 1 - exp(-2π * 0.1 * 0.5) = 1 - exp(-0.314) ≈ 0.269如果直接在程序里用这个α,滤波后的温度响应会变得平滑很多。如果发现还是太跳,就把fc调低;嫌响应太迟钝,就把fc调高。
2.4 一个容易踩的公式歧义
网上很多资料直接写α = Ts / (RC + Ts),然后说“RC就是截止频率的倒数”,这个说法是错的。因为在硬件一阶RC中,截止频率的准确关系是fc = 1/(2πRC),所以RC = 1/(2πfc)。代入α = Ts / (RC + Ts),得到:
α = Ts / (1/(2πfc) + Ts) = 2πfcTs / (1 + 2πfcTs)这和指数形式不完全一样,但在α远小于1时非常接近。我在实际项目里更推荐直接使用指数公式α = 1 - exp(-2πfcTs),它在中高截止频率段更准确,而且和模拟原型的幅频特性贴合得更好。
注意:不管用哪个公式,α值必须在0到1之间。α越接近0,滤波越重,输出越平滑,但响应越迟钝;α越接近1,滤波越轻,输出跟随输入越快,几乎等于没滤波。这是调参时唯一的直觉判断依据。
3. 在Arduino上实现一阶RC低通滤波器(C语言)
3.1 Arduino平台下的适用场景
Arduino Uno用的是AVR Atmega328P,主频16MHz,Flash只有32KB,RAM只有2KB。虽然性能不算强,但跑这种递归滤波器绰绰有余。Arduino生态里最常见的使用场景包括:
- 读取光敏电阻模拟值,做智能窗帘/路灯的平滑检测
- 读取DHT11/DS18B20温度数据后做温控判断
- 读取电位器电压,控制舵机角度或LED亮度渐变
- 读取红外传感器模拟值,让智能小车循迹更平稳
因为Arduino的ADC精度是10位的,数值范围0到1023,输入信号本身就有一定的量化噪声,硬件上很难完全消除,软件滤波就成了性价比最高的手段。
3.2 从裸代码到可复用的模块化设计
很多Arduino教程会把滤波代码直接写在loop()里,一个全局变量解决一切。这种做法在演示时没问题,但项目一复杂就不行了。比如你同时要滤波温度、电位器、光照三个信号,每个信号的滤波强度要求还不一样,全局变量sensorValue和filteredValue只有一个,根本不够用。
我的建议是封装一个滤波器结构体,每个需要滤波的信号独立创建实例。
// lowpass_filter.h #ifndef LOWPASS_FILTER_H #define LOWPASS_FILTER_H typedef struct { float alpha; // 滤波系数 0~1 float output; // 上一次滤波输出 uint8_t initialized; // 是否已初始化 } LowPassFilter; float LowPassFilter_Update(LowPassFilter *f, float input); #endif// lowpass_filter.c #include "lowpass_filter.h" float LowPassFilter_Update(LowPassFilter *f, float input) { // 第一次调用时直接把输入赋值给输出,避免从0开始造成的收敛延迟 if (!f->initialized) { f->output = input; f->initialized = 1; return f->output; } f->output = f->output + f->alpha * (input - f->output); return f->output; }这段代码的设计要点有三个:
第一,初始化标志位initialized。如果不加这个判断,滤波器第一次运行时output默认为0,输入如果是500(比如10位ADC的中间值),输出要经过很多次递归才会逼近真实值,这在温控系统启动时会表现为传感器读数迟迟跟不上。
第二,递推公式写成output + alpha * (input - output)而不是(1 - alpha) * output + alpha * input。两者数学上等价,但前一种写法只有一次乘法,计算量更小;后一种在嵌入式环境里如果alpha写成float,可能会有微小的精度损失累积。
第三,把结构体指针传入函数,不依赖任何全局变量,可以同时创建多个互不干扰的滤波器实例。
3.3 Arduino完整示例:空调温度平滑
下面用一个模拟空调温度采集的完整Arduino程序来演示实际用法。假设我们在A0引脚接了一个电位器模拟温度传感器的输出电压(实践中可能接NTC分压电路),每隔500ms采样一次,滤波后映射到20.0℃到30.0℃的温度范围。
#include "lowpass_filter.h" #define TEMP_SENSOR_PIN A0 #define SAMPLE_INTERVAL_MS 500 LowPassFilter tempFilter; unsigned long lastSampleTime = 0; void setup() { Serial.begin(9600); // 采样周期 Ts = 0.5s,截止频率 fc = 0.1Hz tempFilter.alpha = 1.0f - exp(-2.0 * PI * 0.1 * 0.5); tempFilter.initialized = 0; } void loop() { unsigned long now = millis(); if (now - lastSampleTime >= SAMPLE_INTERVAL_MS) { lastSampleTime = now; int raw = analogRead(TEMP_SENSOR_PIN); float voltage = raw * (5.0f / 1023.0f); float temp = 20.0f + (voltage - 1.0f) * (10.0f / 3.0f); // 简单映射,真实项目用查表或公式 float filteredTemp = LowPassFilter_Update(&tempFilter, temp); Serial.print("raw: "); Serial.print(temp, 2); Serial.print(" -> filtered: "); Serial.println(filteredTemp, 2); } }注意要包含math.h头文件才能使用exp()函数,Arduino IDE默认会引入部分标准库,但最好显式加上#include <math.h>,避免某些环境下编译报错。这个程序跑起来后,串口监视器里就能看到原始温度抖动明显,滤波后的温度变化非常平滑。
3.4 在wokwi仿真平台快速验证
如果你手头没有Arduino板子,或者项目还在预研阶段,我强烈建议用wokwi仿真平台。在wokwi.com上创建一个Arduino Uno项目,直接粘贴上面的代码加一个电位器组件,就能在线看到滤波效果。好处是完全免硬件,改一个参数重新跑一次,非常快。
wokwi的Arduino Uno仿真对analogRead()的时序模拟比较真实,电位器可以通过点击和拖动改变阻值,正好用来模拟传感器信号的阶跃变化和缓慢漂移。把滤波前后的数据用串口监视器或者模拟绘图工具展示出来,对比效果直观得惊人。
4. 在STM32上实现一阶RC低通滤波器(C语言)
4.1 STM32平台与Arduino的关键差异
STM32和Arduino虽然都能写C语言,但工程组织方式差别很大。STM32通常使用STM32CubeIDE + HAL库或标准外设库,代码结构分成main.c、中断回调函数、外设初始化等模块。性能上,STM32主频72MHz起步(如STM32F103),带硬件浮点单元的要Cortex-M4F或M7内核。
硬件浮点单元对滤波器代码的影响很大。Arduino Uno的AVR没有FPU,float运算完全靠软件模拟,所以效率也不算低,因为一次递归只有几次乘加;但STM32F4系列有FPU,直接用float做滤波器,一条浮点乘加指令就完成了,性能完全不是瓶颈。
4.2 结构体封装与模块化实现
在STM32工程里,我把滤波器做成了独立的.c/.h模块,和业务逻辑完全解耦。这样既可以在ADC中断里调用,也可以在RTOS任务里调用,甚至可以同时被多个外设共享。
/* lpf.h */ #ifndef LPF_H #define LPF_H #include "stm32f1xx_hal.h" typedef struct { float alpha; float y_prev; uint8_t initialized; } LPF_HandleTypeDef; void LPF_Init(LPF_HandleTypeDef *lpf, float cutoff_freq, float sample_freq); float LPF_Update(LPF_HandleTypeDef *lpf, float input); #endif/* lpf.c */ #include "lpf.h" #include <math.h> void LPF_Init(LPF_HandleTypeDef *lpf, float cutoff_freq, float sample_freq) { lpf->alpha = 1.0f - expf(-2.0f * 3.14159265f * cutoff_freq / sample_freq); lpf->y_prev = 0.0f; lpf->initialized = 0; } float LPF_Update(LPF_HandleTypeDef *lpf, float input) { if (!lpf->initialized) { lpf->y_prev = input; lpf->initialized = 1; return lpf->y_prev; } lpf->y_prev = lpf->y_prev + lpf->alpha * (input - lpf->y_prev); return lpf->y_prev; }注意LPF_Init里用了expf而不是exp,这是C标准库中单精度浮点版的指数函数。在STM32上使用expf可以减少不必要的double类型转换,提升运算速度并减少Flash占用。很多初学者写成exp后编译器悄悄把它转成了double运算,在Cortex-M0这类没有FPU的内核上会触发软件浮点库,效率降低不少。
4.3 ADC采样与定时器中断联动
STM32做温控采样,标准方案是:定时器触发ADC采样,采样完成后进入ADC转换完成中断,在中断里读取ADC数据并喂给滤波器。这样采样周期非常稳定,滤波器的时间基准Ts不会受到主循环调度抖动的影响。
以STM32F103为例,在CubeMX里配置一个基本定时器(比如TIM6),定时周期设为10ms,使能全局中断;ADC配置为软件触发(或者用定时器的TRGO事件触发ADC,但我一般用软件触发省事)。在定时器中断里启动ADC转换,在ADC转换完成中断里读取数据并滤波:
volatile uint16_t adc_raw = 0; volatile float temperature_filtered = 0.0f; LPF_HandleTypeDef temp_lpf; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { adc_raw = HAL_ADC_GetValue(&hadc1); float voltage = adc_raw * (3.3f / 4095.0f); // 12位ADC,3.3V参考电压 temperature_filtered = LPF_Update(&temp_lpf, voltage); } } void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM6) { HAL_ADC_Start_IT(&hadc1); } }在main函数初始化时,调用LPF_Init设置截止频率。假设ADC是10ms采样一次,也就是sample_freq=100Hz,温度信号截止频率设为0.5Hz,那么:
LPF_Init(&temp_lpf, 0.5f, 100.0f);4.4 Cortex-M0平台上的定点滤波版本
STM32F0系列和有些低功耗芯片用的是Cortex-M0内核,没有硬件浮点单元,编译器对float运算会调用软件浮点库,虽然也能跑,但在高采样率场景下会占用大量CPU时间。比如48kHz的音频采样级别,每个采样点都要做一次浮点滤波,CPU负载会高到无法接受。
这时候可以把滤波器改成定点运算。核心思路是用整数代替浮点数,把alpha放大成整数形式,比如alpha_fixed = alpha * 1024,相当于用10位小数精度表示alpha。
typedef struct { int32_t alpha_q; // 定点化后的alpha,Q10格式 int32_t y_prev_q; // 上一次输出,Q10格式 uint8_t initialized; } LPF_Q10_HandleTypeDef; void LPF_Q10_Init(LPF_Q10_HandleTypeDef *lpf, float cutoff_freq, float sample_freq) { float alpha = 1.0f - expf(-2.0f * 3.14159265f * cutoff_freq / sample_freq); lpf->alpha_q = (int32_t)(alpha * 1024.0f); lpf->y_prev_q = 0; lpf->initialized = 0; } int32_t LPF_Q10_Update(LPF_Q10_HandleTypeDef *lpf, int32_t input_q10) { if (!lpf->initialized) { lpf->y_prev_q = input_q10; lpf->initialized = 1; return lpf->y_prev_q; } // y = y + (alpha * (x - y)) >> 10 int32_t diff = input_q10 - lpf->y_prev_q; lpf->y_prev_q += (lpf->alpha_q * diff) >> 10; return lpf->y_prev_q; }调用时把ADC原始值左移10位转换成Q10格式,滤波后再右移10位还原。这样整个滤波器只用了整数加法和乘法,在Cortex-M0上运行效率非常高。代价是精度会损失一点,但对10到12位ADC来说完全够用。
5. 参数怎么整定:从空调温控的实际测试说起
5.1 标定采样周期和截止频率
滤波器参数只有两个:采样周期Ts和截止频率fc。采样周期由你的系统架构决定,比如你每500ms读一次温度,Ts就是0.5s;如果你在STM32上用10ms定时器触发ADC,Ts就是0.01s。采样周期确定后,唯一需要手动调的就是fc。
fc的物理意义是:“信号中频率高于fc的成分会被显著衰减。”对于空调温控,温度本身变化极慢,可能一分钟才变化0.3℃,这时候fc设低一点没关系。但如果是机器人平衡车里的陀螺仪角度数据,信号变化较快,fc设太低会导致信号延迟太严重,车都翻了角度还没跟上。
我给一个实践经验值供参考:在采样周期为50ms时,做普通传感器平滑,fc在0.5Hz到5Hz之间选择;做姿态解算预处理,fc在10Hz到30Hz之间;做音频或振动信号预处理,fc要到几百赫兹以上。
5.2 实际测试案例:温度从25℃跳变到30℃
我用STM32的一个项目测过不同fc下的响应行为。采样周期固定50ms,用信号发生器模拟一个从25℃跳到30℃的阶跃输入,分别设置fc为0.1Hz、0.5Hz、2Hz。
fc=0.1Hz时,输出大概要经过约3秒才走到29℃,约8秒才到29.9℃,非常平滑但反应很慢;fc=0.5Hz时,约1秒到29℃,3秒到29.9℃;fc=2Hz时,输出300ms内就跟上了输入,但快速跳变时波形有轻微圆角,已经比较接近直通。
如果你做空调温控,压缩机的启停判断不应该看瞬时温度,而应该看滤波后的趋势温度。我最终的温控代码里用了两级判断:滤波后的温度低于设定值1℃启动压缩机,高于设定值0.5℃停止,中间设置了至少3分钟的最小运行时间。这样既保证舒适度又防止压缩机频繁启停。
5.3 不同α取值的直观对照表
为了方便调参,我把常用场景对应的α值整理成表格。表格中的α值都是在采样周期Ts = 50ms的前提下计算的,如果你改了采样周期需要重新算。
| 应用场景 | 目标截止频率 fc | 采样周期 Ts | α取值 | 滤波效果描述 |
|---|---|---|---|---|
| 空调温度平滑 | 0.1Hz | 50ms | 0.031 | 温度显示极稳定,但响应慢 |
| 一般传感器 | 1Hz | 50ms | 0.282 | 消除大部分噪声,响应可接受 |
| 舵机/电机控制 | 5Hz | 50ms | 0.792 | 只滤除高频毛刺,响应快 |
| 姿态解算预处理 | 20Hz | 50ms | 0.998 | 几乎直通,只滤极高频噪声 |
注意最后一行α=0.998,这时滤波器和直通几乎没区别。姿态解算里如果采样率不够高,一阶低通滤波器会出现严重的相位延迟,后文会展开讲。
5.4 为什么初始化值这么重要
滤波器第一次调用时,如果直接套用公式,output初始值如果写死为0,那么输入是真实值(比如温度26.5℃),滤波器需要经过1/α个采样周期才能大致逼近。α=0.1时需要约10个采样周期,也就是0.5秒(采样周期50ms时),这段时间温控系统读到的温度一直是错的。
所以我在两个平台的代码里都加了initialized标志,第一次调用直接把输入作为输出。这个细节解决了一个最常见的“滤波器上电后响应慢”问题。如果你写代码时忽略了这一点,测试时就会觉得“滤波后数据怎么一开始偏低很多”。
6. 常见问题与排查技巧实录
6.1 滤波后信号仍然抖动,怎么办
先把α调小一倍试试。如果α已经非常小(比如0.01以下)还在抖,说明噪声幅度太大,单级一阶滤波效果有限。这时候可以用两级串联滤波——把第一个滤波器的输出再喂给第二个滤波器。我试过两级一阶RC的级联,相当于二阶滤波,衰减斜率从-20dB/十倍频提升到-40dB/十倍频,对高频噪声的抑制能力明显增强。
但要注意,级联会进一步增大相位延迟。温控系统可以容忍1秒级别的延迟,但如果是快速响应系统,级联就不合适了。这时可以考虑换用IIR巴特沃斯二阶滤波器,或者做滑动平均滤波,各有优劣。
6.2 滤波后波形出现“滞后感”,甚至控制振荡
一阶低通滤波器本质上是一个惯性环节,所有频率的信号经过它都会产生相位偏移,截止频率附近的信号相位延迟最大可达90度。这在温控系统里问题不大,但如果你用滤波后的信号做PID反馈,相位延迟会降低系统的相位裕度,严重时导致控制器震荡。
我遇到过的一个案例是:给一个直流电机的电流反馈加了低通滤波后,电机在启动瞬间发出啸叫,电流波形开始振荡。排查后发现就是滤波器截止频率设太低(10Hz),而电流环的带宽是几百赫兹,反馈信号被严重滞后。解决方案是把截止频率提到100Hz以上,同时减小滤波级数,问题立刻消失。
经验法则是:滤波截止频率至少要比控制环路的带宽高5倍以上,否则滤波本身可能引入新的不稳定。
6.3 用不用float,以及代码上的四个坑
第一个坑:Arduino的float。AVR平台没有FPU,float运算依赖软件库,但一次递归运算量极小,几千次每秒毫无压力。唯一要注意的是避免在中断里同时做大量浮点运算,否则中断执行时间过长会影响时序精度。
第二个坑:alpha值计算时用了int类型导致结果恒为0。比如代码写成float alpha = ts / (rc + ts);,如果ts和rc都是int,整数除法会先截断,得到0,再赋给float。解决办法是至少有一个操作数转成float,或者直接写成浮点字面量。
第三个坑:在STM32的ADC中断里调用滤波函数时,如果alpha是全局结构体成员,要防止主循环同时访问同一结构体导致数据竞争。最稳妥的做法是像我在4.3节的示例那样,把滤波后的结果放在volatile变量里,主循环只读这个变量,绝不反向写入。
第四个坑:大时间常数场景下float精度不足。比如fc=0.01Hz,Ts=0.5s时,α = 0.0314,还好;但如果fc更低,α小到0.001以下,此时output += alpha * (input - output),其中input - output的差值可能很小,乘以0.001后浮点精度可能丢失。解决方案是用double类型,但单片机内存有限,更实用的方案是把采样值先放大(比如温度乘100保存为整数度),再做定点滤波。
6.4 常见问题速查表
我把实际项目里被问得最多的问题整理成一个速查表,方便排查。
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 滤波后信号低了很多 | 未初始化output导致从0开始收敛 | 首次调用直接赋值 |
| 滤波后仍然抖动 | α太大,截止频率太高 | 减小fc,计算更小的α |
| 信号响应太慢 | α太小,截止频率太低 | 增大fc,或减少滤波级数 |
| 系统控制振荡 | 滤波引入相位延迟过大 | 提高fc,或改用移动平均滤波 |
| 数值突然跳成0或负数 | 变量类型溢出 | 检查int16_t是否溢出,改用float或int32_t |
| 低功耗模式下与掉数据 | 采样周期不稳定导致Ts偏离设计 | 使用定时器触发ADC,不用delay模拟采样周期 |
6.5 一个省事的小工具
最后分享一个调参效率小技巧:用上位机实时画滤波前后的波形。Arduino的Serial Plotter(内置在实际开发环境中)可以直接显示两条曲线,你在串口打印原始值,滤波值即可。STM32可以通过串口DMA发送数据到PC端的VOFA+或匿名上位机,实时观察α调节的响应。每次改参数后看曲线趋势,比看一串数字直观得多。
我在调空调温控参数时,就是一边吹着热风枪给传感器加热,一边盯着串口波形,一点点把fc从1Hz降到0.15Hz,看到原始信号上下乱跳而滤波输出几乎平稳一条线时才收手。整个过程大概花了十分钟,比焊接RC电阻电容再反复拆装快多了。
7. 从空调温控延伸开去:这个滤波器还能用在哪
7.1 智能小车传感器融合预处理
很多人在做Arduino智能小车时,循迹传感器的模拟输出叠加了地面反光不均匀造成的噪声,直接做阈值判断会导致转向忽左忽右。把传感器原始值先过一次低通滤波再和阈值比较,小车的直线行驶稳定性会好很多。我试过在小车巡线项目里加滤波后,过弯时的抖动大幅减少。
7.2 PWM输出平滑与DAC模拟
如果你的单片机没有DAC外设,用PWM加RC滤波输出模拟量是常见做法。现在用软件滤波器,可以在PWM占空比变化路径上做平滑,比如LED呼吸灯从0%到100%渐变时,每步占空比增量用滤波后的值控制,亮度变化曲线就变成平滑的S形,比线性阶梯好看很多。舵机控制同理,目标角度直接给会带着机械冲击,用低通滤波处理角度目标值后,舵机转动更柔和。
7.3 传感器异常毛刺的剔除
一阶低通对随机高频噪声效果显著,但对单个尖峰脉冲(比如静电干扰)的抑制能力比较有限。如果信号里偶尔出现一个远大于正常范围的毛刺,低通滤波后它会以α的比例“残留”在输出里,造成一个可见的小鼓包。这时候可以在滤波前加一个简单的中值判断:如果当前输入偏离上次输出超过一定阈值,判定为异常值,本次直接忽略。这个“限幅+低通”的组合在工业传感器采集里几乎是标配。
7.4 STM32车载以太网项目里的工程约束
在做STM32车载以太网相关的诊断或网关项目时,大量报文数据需要预处理,虽然网络报文本身频率较高,但如果涉及温度、电压、电流这类底层传感器信号的上报,依然要用软件滤波做平滑。车载环境对安全性和稳定性要求极高,滤波代码要求可追溯、可测试,像LQFP这种纯函数式的接口设计就非常利于做单元测试。我把这个结构体加函数的设计沿用到了车载项目里,外部只需要传入原始值和配置参数,不依赖任何全局状态,验证起来非常方便。
8. 写在最后:我的一点实操体会
做嵌入式开发这些年,我越来越觉得,数字滤波是性价比最高的一项技能。它不需要额外硬件、成本为零,却能解决传感器数据抖动、执行机构动作毛糙、控制系统不稳定等一大类问题。一阶RC低通滤波器又是数字滤波里最简单的一个起点,吃透它之后,再学二阶滤波、卡尔曼滤波、滑动平均滤波都会有清晰的对比坐标。
我在实际使用中有一个习惯:每个传感器的滤波参数不会写死在代码里,而是留成可以通过串口命令动态修改的配置项。因为现场的噪声环境往往和实验室不一样,直接改常量要重新编译烧录,非常低效。动态配置让系统在现场调试时只调参数不碰代码,效率提升特别明显。
最后再分享一个小技巧:如果项目里要滤波的信号特别多,比如同时处理八路ADC,我一般会建立一个滤波器数组,用for循环统一更新:
#define FILTER_COUNT 8 LPF_HandleTypeDef filters[FILTER_COUNT]; for (int i = 0; i < FILTER_COUNT; i++) { filtered[i] = LPF_Update(&filters[i], raw[i]); }这样代码简洁,也方便后期扩展。你手上的温控、电源、小车项目如果遇到信号抖动的问题,不妨先把这篇里的一阶低通滤波器加进去,大概率能解决一大半烦恼。