一阶RC低通滤波器:用C语言在单片机上实现信号降噪
空调温控面板上的温度数字一直在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主频16MHzFlash只有32KBRAM只有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没有FPUfloat运算完全靠软件模拟所以效率也不算低因为一次递归只有几次乘加但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位ADC3.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_freq100Hz温度信号截止频率设为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; // 定点化后的alphaQ10格式 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定时器触发ADCTs就是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。fc0.1Hz时输出大概要经过约3秒才走到29℃约8秒才到29.9℃非常平滑但反应很慢fc0.5Hz时约1秒到29℃3秒到29.9℃fc2Hz时输出300ms内就跟上了输入但快速跳变时波形有轻微圆角已经比较接近直通。如果你做空调温控压缩机的启停判断不应该看瞬时温度而应该看滤波后的趋势温度。我最终的温控代码里用了两级判断滤波后的温度低于设定值1℃启动压缩机高于设定值0.5℃停止中间设置了至少3分钟的最小运行时间。这样既保证舒适度又防止压缩机频繁启停。5.3 不同α取值的直观对照表为了方便调参我把常用场景对应的α值整理成表格。表格中的α值都是在采样周期Ts 50ms的前提下计算的如果你改了采样周期需要重新算。应用场景目标截止频率 fc采样周期 Tsα取值滤波效果描述空调温度平滑0.1Hz50ms0.031温度显示极稳定但响应慢一般传感器1Hz50ms0.282消除大部分噪声响应可接受舵机/电机控制5Hz50ms0.792只滤除高频毛刺响应快姿态解算预处理20Hz50ms0.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平台没有FPUfloat运算依赖软件库但一次递归运算量极小几千次每秒毫无压力。唯一要注意的是避免在中断里同时做大量浮点运算否则中断执行时间过长会影响时序精度。第二个坑alpha值计算时用了int类型导致结果恒为0。比如代码写成float alpha ts / (rc ts);如果ts和rc都是int整数除法会先截断得到0再赋给float。解决办法是至少有一个操作数转成float或者直接写成浮点字面量。第三个坑在STM32的ADC中断里调用滤波函数时如果alpha是全局结构体成员要防止主循环同时访问同一结构体导致数据竞争。最稳妥的做法是像我在4.3节的示例那样把滤波后的结果放在volatile变量里主循环只读这个变量绝不反向写入。第四个坑大时间常数场景下float精度不足。比如fc0.01HzTs0.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]); }这样代码简洁也方便后期扩展。你手上的温控、电源、小车项目如果遇到信号抖动的问题不妨先把这篇里的一阶低通滤波器加进去大概率能解决一大半烦恼。