1. 为什么偏偏要聊“位移法”实现一阶低通滤波搞过单片机信号采集的人都有一个共同的痛:传感器出来的数据永远在跳。你明明测的是一个恒定不变的物理量,串口打印出来的数值却像心电图一样上下乱窜,偶尔还蹦出几个离谱的野值。这时候老工程师通常会丢给你一句话——“加个滤波”。于是你打开搜索引擎,搜到的方案五花八门:均值滤波、中值滤波、卡尔曼滤波、一阶低通滤波……代码复制粘贴一通,跑起来好像也能用,但真要问一句“这个系数为什么取0.1而不是0.01”,很多人就答不上来了。这篇内容要聊的,是一阶低通滤波的一种非常实用的实现方式——位移法。它不依赖浮点运算,不需要调用数学库,在8位单片机上也能跑得飞快,特别适合ADC采样后的实时数据处理。核心关键词就三个:一阶低通滤波、位移法、单片机实战。我会从原理推导开始,把系数计算、整数化处理、移位替代除法、定点数精度控制这些细节全部拆开讲清楚,最后给出一套可以直接移植的C代码实现,以及我在实际项目中踩过的坑和排查经验。适合谁看?如果你正在做传感器数据采集、电机控制、电源管理、或者任何需要从噪声中提取有效信号的嵌入式项目,这篇内容应该能帮你省下不少调试时间。哪怕你之前没接触过滤波算法,只要会写基本的C语言,跟着思路走也能理解。我不会堆一堆公式吓人,而是尽量用生活化的类比把原理讲透,再落到代码上。先说一个我自己的经历。早些年做一个温控项目,用的是一款常见的数字温度传感器,分辨率0.0625度,理论上精度够用。但实际读回来的温度值一直在±0.5度范围内跳动,导致继电器频繁启停,用户体验极差。当时第一反应是用均值滤波,攒够10个样本取平均。结果响应变得很慢,温度已经明显变化了,显示值还在慢慢爬。后来换成中值滤波,野值确实被干掉了,但周期性噪声依然存在。最后用一阶低通滤波配合位移法实现,才在响应速度和稳定性之间找到了平衡点。这段经历让我意识到,滤波方案的选择不是“哪个高级用哪个”,而是要看你的信号特征和系统资源。2. 一阶低通滤波的核心原理拆解2.1 从模拟RC电路到数字滤波的思维跨越一阶低通滤波这个概念,最早来自模拟电路里的RC电路。一个电阻和一个电容串联,输入信号经过电阻给电容充电,电容两端的电压就是输出。因为电容的充放电需要时间,高频信号来不及让电容电压跟上变化,就被“滤掉”了;低频信号有足够的时间让电容充放电,所以能顺利通过。这就是低通滤波的物理本质。用数学语言描述这个电路,得到一个一阶微分方程,解出来就是指数衰减的形式。到了数字域,我们没法用电阻电容,但可以用“本次采样值”和“上次滤波输出值”做加权平均来模拟这个充放电过程。公式非常简洁:Y(n) = α × X(n) + (1 - α) × Y(n-1)其中X(n)是本次ADC采样值,Y(n)是本次滤波输出,Y(n-1)是上次滤波输出,α是滤波系数,取值范围0到1。这个公式的含义是:新的输出值由“新采样值”和“旧输出值”共同决定,α越大,越信任新采样值,响应越快但滤波效果越弱;α越小,越信任历史值,输出越平滑但响应越慢。你可以把它想象成一个水池。新采样值是从进水管流进来的水,旧输出值是池子里已有的水。α就是进水管的阀门开度。阀门开得大,池子里的水更新快,但水位波动也大;阀门开得小,水位稳定,但想让它涨上去或者降下来都需要很长时间。这个类比虽然不严谨,但能帮你快速建立直觉。2.2 滤波系数α到底怎么选α的选取是一阶低通滤波最核心的问题,也是最容易拍脑袋决定的地方。很多人直接抄别人的代码,看到α=0.1就跟着用,结果要么响应太慢,要么滤波效果不够。其实α和截止频率之间有明确的数学关系。在数字滤波中,截止频率fc、采样频率fs和系数α满足:α = 1 - exp(-2π × fc / fs)反过来,如果你先确定了α,也可以算出对应的截止频率:fc = -fs × ln(1 - α) / (2π)这个公式告诉你一件重要的事:α不是孤立的,它和你的采样率强相关。同样的α=0.1,在1kHz采样率下截止频率大约是16.7Hz,在100Hz采样率下截止频率只有1.67Hz。所以脱离采样率谈α是没有意义的。实际工程中怎么定?我的经验是分三步走。第一步,明确你需要保留的信号最高频率是多少。比如测温度,温度变化本身很慢,可能1Hz都不到;测振动,可能需要保留几百Hz。第二步,确定你的采样率,通常要满足奈奎斯特采样定理,采样率至少是信号最高频率的2倍以上,工程上一般取5到10倍。第三步,用上面的公式算出α的初始值,然后根据实际效果微调。举个具体例子。假设你做一个电子秤,采样率100Hz,你需要保留的重量变化信号最高频率大约2Hz(已经很快了,正常称重远低于这个)。那么:α = 1 - exp(-2π × 2 / 100) = 1 - exp(-0.1257) ≈ 0.118取α=0.12左右就比较合适。如果你发现响应还是慢,可以适当加大到0.15;如果噪声还是大,可以减小到0.08。这个调整过程需要结合示波器或者串口波形工具来观察。2.3 位移法为什么能替代浮点运算上面那个公式里有乘法,如果α是0.12这种浮点数,在低端单片机上跑起来就很吃力。没有FPU(浮点运算单元)的MCU做一次浮点乘法可能要几十个时钟周期,如果采样率是1kHz,每个采样周期都要算一次,CPU占用率会很难看。位移法的思路很巧妙:把α限制为1/2的整数次幂,这样乘法就可以用右移操作来代替。比如α=1/4,那么:Y(n) = X(n)/4 + 3×Y(n-1)/4除以4就是右移2位,乘以3可以写成(Y(n-1)<<1) + Y(n-1),全是整数移位和加法,速度极快。再比如α=1/8:Y(n) = X(n)/8 + 7×Y(n-1)/8除以8就是右移3位。这种实现方式在8位、16位单片机上特别受欢迎,因为移位指令通常只需要1到2个时钟周期。但这里有一个代价:α的取值被离散化了。你只能选1/2、1/4、1/8、1/16、1/32这些值,对应的α分别是0.5、0.25、0.125、0.0625、0.03125。如果你的目标α是0.12,最接近的是0.125(1/8),误差不大,可以接受。但如果目标α是0.05,最接近的是0.0625(1/16),误差就有点大了。这时候可以考虑用“多次移位组合”来逼近,比如1/16 - 1/64 ≈ 0.0469,但实现复杂度会上升。我的建议是:优先选1/8、1/16、1/32这三个档位,它们覆盖了大多数常见应用场景。1/8适合需要较快响应的场合,1/16是通用档位,1/32适合对稳定性要求极高的慢变信号。3. 位移法的整数化实现细节3.1 定点数表示与精度损失分析位移法虽然避开了浮点运算,但引入了另一个问题:整数除法的截断误差。假设ADC是12位,采样值范围0到4095,滤波输出也在这个范围。如果直接用整数做右移,每次移位都会丢掉低位信息,多次迭代后误差会累积。举个例子。假设α=1/8,当前采样值X=1000,上次输出Y=0。第一次计算:Y = 1000/8 + 7×0/8 = 125 + 0 = 125看起来没问题。但下一次如果X还是1000:Y = 1000/8 + 7×125/8 = 125 + 109 = 234再下一次:Y = 125 + 7×234/8 = 125 + 204 = 329你会发现输出在慢慢逼近1000,但每次都有截断误差。最终稳定时,输出会在1000附近的一个小范围内波动,无法完全等于1000。这个波动幅度取决于移位位数,移位越多,截断误差越大。解决办法是在累加器里保留额外的低位精度。具体做法是:把滤波输出值左移N位存储,计算时用这个放大后的值,最后输出时再右移N位。比如N=4,那么内部变量范围变成0到4095×16=65520,用16位无符号整数刚好能放下。这样截断误差被缩小了16倍,输出波动几乎可以忽略。代码大概长这样:#define FILTER_SHIFT 3 // α = 1/8 #define PRECISION_SHIFT 4 // 额外精度位 static uint16_t filter_acc = 0; // 内部累加器,放大16倍 uint16_t lowpass_filter(uint16_t new_sample) { // 将新采样值也放大16倍 uint32_t x = (uint32_t)new_sample << PRECISION_SHIFT; // Y(n) = X(n)/8 + 7*Y(n-1)/8 filter_acc = (x >> FILTER_SHIFT) + filter_acc - (filter_acc >> FILTER_SHIFT); // 返回时缩小16倍 return (uint16_t)(filter_acc >> PRECISION_SHIFT); }注意这里用了一个小技巧:7*Y/8写成Y - Y/8,避免乘法。filter_acc - (filter_acc >> FILTER_SHIFT)就是Y - Y/8 = 7Y/8。整个计算只有移位、加法和减法,效率极高。3.2 不同移位位数的性能对比为了让你直观感受不同α档位的效果,我用同一组模拟数据做了对比测试。模拟信号是叠加了高斯噪声的阶跃信号,采样率100Hz,阶跃从0跳到1000。测试结果如下:移位位数α值等效截止频率(Hz)达到90%终值时间(s)稳态波动幅度(LSB)10.511.00.04±820.254.60.09±430.1252.10.18±240.06251.00.37±150.031250.50.73±0.5这张表的信息量很大。移位1位时响应极快,40ms就达到90%,但稳态波动有±8个LSB,对于12位ADC来说相当于0.2%的噪声,很多场合不够用。移位3位时响应时间180ms,稳态波动降到±2个LSB,适合大多数工业控制场景。移位5位时响应要0.73秒,但波动只有±0.5个LSB,适合精密测量。选择哪个档位,本质上是在响应速度和稳态精度之间做权衡。我的经验法则是:先确定你能容忍的最大响应延迟,然后选满足这个延迟的前提下最大的移位位数。比如你的控制系统要求200ms内响应到位,那就选移位3位;如果要求50ms内响应,只能选移位1位或2位。3.3 初始值处理与启动收敛问题一阶低通滤波有一个容易被忽视的问题:启动时的收敛过程。如果滤波器的初始输出值Y(0)设为0,而实际信号一开始就是1000,那么输出需要经过一段时间才能爬升到1000附近。这段时间里,滤波输出和真实值偏差很大,如果控制系统直接使用这个输出,可能会导致误动作。解决办法有两个。第一个是用第一次采样值初始化滤波器:static uint8_t filter_initialized = 0; uint16_t lowpass_filter(uint16_t new_sample) { if (!filter_initialized) { filter_acc = (uint32_t)new_sample << PRECISION_SHIFT; filter_initialized = 1; return new_sample; } // ... 正常滤波计算 }这样第一次输出就等于第一次采样值,没有爬升过程。第二个办法是在启动阶段用更快的α,等输出稳定后再切换到正常α。这种方法更复杂,但适合那些启动时信号本身就变化剧烈的场景。我个人的习惯是第一种,简单可靠。但要注意,如果第一次采样恰好是一个野值,那滤波器就被“带偏”了。所以更稳妥的做法是:先连续采几次,取中值作为初始值,然后再开始滤波。多花几毫秒,换来更稳的启动。4. 单片机实战:从ADC采样到滤波输出4.1 硬件平台与ADC配置要点为了让讨论具体化,我以一个典型的STM32F103项目为例,ADC是12位精度,采样率设为1kHz,通过定时器触发ADC转换,转换完成后DMA搬运数据,主循环里做滤波处理。这套配置在工业控制里非常常见。ADC配置有几个关键点需要注意。第一,采样时间要足够长。如果传感器输出阻抗较高,采样时间太短会导致采样电容充不满,读数偏低。一般建议采样时间至少设为7.5个ADC时钟周期以上,高阻抗传感器要设到41.5或239.5个周期。第二,参考电压要干净。ADC的参考电压直接决定转换精度,如果参考电压上有噪声,再好的滤波算法也救不回来。建议在参考电压引脚旁边放一个1uF到10uF的陶瓷电容,再并联一个0.1uF的高频电容。第三,模拟地和数字地要分开走线,最后在一点汇合,避免数字噪声串到模拟部分。DMA搬运的好处是CPU不用干预每次ADC转换,数据自动存到缓冲区。但要注意缓冲区大小和DMA传输完成中断的配合。如果缓冲区设得太小,中断太频繁,CPU开销大;设得太大,数据延迟增加。我的经验是缓冲区设8到16个样本比较合适,中断频率在60到125Hz之间,对大多数应用来说可以接受。4.2 完整代码实现与逐行解析下面是一套可以直接移植的C代码,包含滤波器结构体定义、初始化函数和滤波计算函数。我把它设计成可重入的,每个通道可以独立使用一个滤波器实例。#include <stdint.h> #include <stdbool.h> typedef struct { uint32_t acc; // 内部累加器,放大2^PRECISION_SHIFT倍 uint8_t shift; // 移位位数,对应α=1/(2^shift) uint8_t precision; // 额外精度位数 bool initialized; // 是否已初始化 } lpf_t; // 初始化滤波器 void lpf_init(lpf_t *f, uint8_t shift, uint8_t precision) { f->acc = 0; f->shift = shift; f->precision = precision; f->initialized = false; } // 滤波计算,输入为原始ADC值,返回滤波后的值 uint16_t lpf_update(lpf_t *f, uint16_t sample) { uint32_t x; // 首次调用,用采样值直接初始化累加器 if (!f->initialized) { f->acc = (uint32_t)sample << f->precision; f->initialized = true; return sample; } // 将新采样值放大到与累加器相同的量级 x = (uint32_t)sample << f->precision; // Y(n) = X(n)/2^shift + Y(n-1) - Y(n-1)/2^shift // 等价于 Y(n) = α*X(n) + (1-α)*Y(n-1),其中α=1/2^shift f->acc = (x >> f->shift) + f->acc - (f->acc >> f->shift); // 缩小回原始量级并返回 return (uint16_t)(f->acc >> f->precision); }逐行解析一下关键部分。f->acc是内部累加器,它存储的是放大后的滤波输出值。放大倍数由precision决定,比如precision=4就是放大16倍。为什么要放大?前面讲过,是为了保留低位精度,减少截断误差。x = (uint32_t)sample << f->precision把新采样值也放大到同一量级,这样加减法才有意义。注意这里用了uint32_t强制转换,防止16位整数左移溢出。如果sample是12位,precision是4,那么x最大是4095×16=65520,用16位无符号整数刚好能放下,但为了安全还是用32位。核心计算f->acc = (x >> f->shift) + f->acc - (f->acc >> f->shift)这一行,对应公式Y(n) = X(n)/2^shift + Y(n-1) - Y(n-1)/2^shift。展开来看就是Y(n) = X(n)/2^shift + Y(n-1)*(1 - 1/2^shift),正好是一阶低通滤波的标准形式。整个计算只有移位、加法和减法,没有乘除,在Cortex-M3内核上大约需要6到8个时钟周期,1kHz采样率下CPU占用率不到1%。返回时f->acc >> f->precision把累加器缩小回原始量级。由于累加器里保留了额外精度,这个右移操作相当于做了四舍五入的近似,输出值比直接整数运算平滑得多。4.3 多通道管理与内存优化实际项目里往往不止一路ADC,可能有4路、8路甚至更多。如果每路都用一个独立的lpf_t结构体,内存占用是通道数 × sizeof(lpf_t)。在STM32F103上,lpf_t大约占8个字节,8路就是64字节,对于有20KB RAM的芯片来说完全不是问题。但如果你的芯片只有1KB RAM,就需要考虑优化了。优化思路有几个。第一,共用移位参数。如果所有通道的滤波需求相同,可以把shift和precision提出来做成全局常量,结构体里只保留acc和initialized,这样每个通道只占5个字节。第二,用位域压缩。shift最大31,用5位就够了;precision最大15,用4位;initialized用1位。加起来10位,可以塞进一个16位变量里。第三,如果通道数很多但不需要同时滤波,可以用一个结构体轮流处理,处理完一路把acc存到数组里,下次处理时再取出来。我一般推荐第一种方案,简单且不容易出错。代码改动很小,把shift和precision从结构体里去掉,改成宏定义或者全局变量即可。#define LPF_SHIFT 3 #define LPF_PRECISION 4 typedef struct { uint32_t acc; bool initialized; } lpf_simple_t; uint16_t lpf_simple_update(lpf_simple_t *f, uint16_t sample) { if (!f->initialized) { f->acc = (uint32_t)sample << LPF_PRECISION; f->initialized = true; return sample; } uint32_t x = (uint32_t)sample << LPF_PRECISION; f->acc = (x >> LPF_SHIFT) + f->acc - (f->acc >> LPF_SHIFT); return (uint16_t)(f->acc >> LPF_PRECISION); }这样每个通道只占5个字节(uint32_t加bool,可能有对齐填充),8路也才40字节。5. 常见问题与排查技巧实录5.1 滤波后数据仍然跳动怎么办这是最常见的问题。你按照公式实现了滤波,但输出还是不够稳。排查思路按优先级排列:第一,检查ADC本身是否稳定。把传感器换成固定的参考电压,看ADC读数是否稳定。如果参考电压下读数都在跳,那问题出在硬件上,滤波算法再厉害也没用。常见原因包括参考电压噪声、模拟地干扰、采样时间不足、传感器供电不稳。第二,确认滤波系数是否合适。如果你用的是移位3位(α=0.125),理论上稳态波动应该很小。但如果ADC噪声幅度特别大,比如±20个LSB,那滤波后可能还有±2到±3个LSB的波动。这时候要么加大移位位数,要么在滤波前先做一次中值滤波去掉野值。第三,检查是否有周期性干扰。如果波动呈现明显的周期性,比如50Hz工频干扰,那可能是电源或者空间耦合进来的。这种情况下单纯的低通滤波效果有限,需要从源头解决,比如加屏蔽、改走线、增加电源滤波电容。第四,确认数据类型没有溢出。uint32_t的累加器在precision=4、shift=3时,最大值大约是65520×8=524160,远小于uint32_t的最大值42亿,不会溢出。但如果你把precision设得很大,比如8,那累加器最大值是4095×256=1048320,仍然安全。但如果ADC是16位,precision=8,最大值就是65535×256=16776960,也没问题。真正会溢出的是uint16_t累加器,所以一定要用uint32_t。5.2 响应速度太慢怎么调响应慢和滤波效果好是一对矛盾。如果你发现系统响应跟不上,首先确认是不是移位位数太大了。移位5位(α=0.03125)的响应时间接近0.7秒,对于很多控制系统来说太慢了。可以尝试降到移位4位或3位。如果降移位位数后噪声又变大了,可以考虑变系数滤波:在信号变化剧烈时用大α,信号平稳时用小α。实现方法是监测相邻两次采样值的差值,如果差值超过阈值,临时把shift减小1到2位,否则保持正常shift。这样既保证了快速响应,又保证了稳态精度。uint16_t lpf_adaptive_update(lpf_t *f, uint16_t sample) { static uint16_t last_sample = 0; uint16_t diff = (sample > last_sample) ? (sample - last_sample) : (last_sample - sample); uint8_t shift = f->shift; // 如果变化剧烈,临时提高响应速度 if (diff > 50 && shift > 1) { shift -= 1; } // ... 用shift做滤波计算 last_sample = sample; }阈值50需要根据你的信号幅度和噪声水平来调整。太大起不到作用,太小会导致滤波器频繁切换,反而引入额外波动。5.3 多路ADC之间的串扰问题如果你用多个ADC通道轮流采样,可能会发现某一路的滤波输出受到其他路的影响。这通常是因为ADC采样保持电容在通道切换后没有足够时间稳定。解决办法是在通道切换后丢弃第一次采样,或者增加采样时间。另一个可能的原因是DMA缓冲区管理不当,导致数据错位。比如你配置了4个通道的规则组,DMA缓冲区大小是8,那么前4个数据是通道0到3,后4个又是通道0到3。如果你按顺序取数据但没做通道对齐,就会把通道1的数据当成通道0的。这种问题在调试时表现为“滤波输出偶尔跳一下”,很容易被误认为是噪声。排查方法是:在DMA传输完成中断里打印缓冲区所有数据,对照通道顺序检查。如果发现错位,调整缓冲区大小或者用DMA的循环模式配合通道计数器。5.4 常见问题速查表现象可能原因排查方法解决措施滤波输出仍有较大波动ADC噪声过大短接ADC输入看读数增加硬件滤波、加大移位位数响应明显滞后移位位数过大计算等效截止频率减小移位位数、用变系数滤波输出偶尔跳变野值干扰串口打印原始数据前置中值滤波、检查传感器接触多通道互相影响采样保持电容未稳定切换通道后连续采样两次丢弃第一次、增加采样时间启动时输出爬升慢初始值设为0观察启动波形用首次采样值初始化长时间运行后输出漂移累加器溢出检查数据类型改用uint32_t累加器滤波效果时好时坏采样率不稳定用示波器测ADC触发信号检查定时器配置、优先用硬件触发这张表是我在实际项目中反复验证过的,基本上覆盖了80%以上的常见问题。遇到问题时按表排查,能省不少时间。6. 进阶话题:从一阶到二阶的平滑过渡一阶低通滤波的局限性在于:它只有一个极点,过渡带衰减斜率是-20dB/十倍频。如果你需要更陡的衰减,比如-40dB/十倍频,就需要二阶滤波。二阶滤波可以看成两个一阶滤波串联,但直接串联会引入额外的相位延迟,而且两个滤波器的系数需要仔细匹配。一个实用的二阶实现方式是:先用一阶滤波得到中间值,再对中间值做一次一阶滤波。两次滤波的α可以相同也可以不同。如果相同,等效于一个二阶巴特沃斯滤波器,但截止频率会偏移。如果不同,可以通过调整两个α来优化响应。typedef struct { lpf_t stage1; lpf_t stage2; } lpf2_t; uint16_t lpf2_update(lpf2_t *f, uint16_t sample) { uint16_t mid = lpf_update(&f->stage1, sample); return lpf_update(&f->stage2, mid); }这种实现简单直接,但要注意两次滤波都会引入延迟,总延迟大约是一阶的两倍。如果系统对延迟敏感,需要谨慎使用。另一个进阶方向是自适应滤波,根据信号的信噪比动态调整α。信噪比高时用大α快速跟踪,信噪比低时用小α强力滤波。信噪比可以用信号方差和噪声方差的比值来估计,但计算量会大一些。在资源有限的单片机上,通常用简化方法,比如前面提到的差值阈值法。我在一个电机电流采样项目里用过自适应滤波。电机启动时电流变化剧烈,需要快速响应;稳态时电流平稳,需要高精度。用固定α很难同时满足两个需求,自适应方案完美解决了这个问题。实现代码只比固定α多了十几行,效果提升非常明显。7. 一些掏心窝子的实操心得滤波算法这东西,理论是一回事,实际调参是另一回事。我见过太多人把公式背得滚瓜烂熟,一到现场就抓瞎。分享几条我踩坑换来的经验。第一条:先看原始数据,再谈滤波。很多人一上来就调滤波参数,却从来没仔细看过原始ADC数据的波形。你连噪声长什么样都不知道,怎么知道该用什么滤波?我的习惯是先用串口把原始数据传到电脑上,用波形工具画出来,看清楚噪声的频率、幅度、是周期性还是随机性。这一步花十分钟,能省后面几小时的盲目调试。第二条:滤波不是万能的,硬件问题要硬件解决。如果电源纹波有100mV,你指望用数字滤波把ADC读数滤稳,那是缘木求鱼。先把电源搞干净,把地线走好,把屏蔽做好,然后再上滤波算法。我见过一个项目,工程师花了三天调滤波参数,最后发现是传感器供电引脚虚焊。第三条:移位位数不要超过5。移位5位对应α=0.03125,响应时间已经接近1秒了。再小的话,除非你的信号变化极慢(比如环境温度监测),否则没有实际意义。而且移位位数越大,截断误差累积越严重,即使加了精度位也救不回来。第四条:多通道滤波时,每个通道独立初始化。我见过有人用一个全局变量做累加器,结果通道0的数据污染了通道1的滤波输出。这种bug很隐蔽,因为单通道测试时完全正常,只有多通道同时跑才出问题。所以滤波器结构体一定要每个通道一份,初始化也要逐个做。第五条:保留原始数据接口。调试阶段,一定要能随时切换到“不滤波”模式,直接输出原始ADC值。这样当滤波效果不对时,你可以快速判断是滤波算法的问题还是ADC本身的问题。我的做法是在滤波器结构体里加一个bypass标志,置位时直接返回原始值。第六条:注意采样率和滤波系数的匹配。如果你改了定时器配置,采样率从1kHz变成2kHz,但滤波系数没改,那等效截止频率就翻倍了,滤波效果会明显变差。所以采样率和滤波系数要一起改,改完重新验证。最后说一个我最近才想明白的事。一阶低通滤波的本质是“用历史信息平滑当前观测”,它假设信号是缓慢变化的,噪声是快速变化的。但如果信号本身就有快速变化的需求,比如你要检测一个突发的脉冲,那一阶低通滤波就会把脉冲也滤掉。这时候需要考虑其他方案,比如中值滤波加阈值检测,或者用小波变换做多尺度分析。没有一种滤波算法是万能的,关键是理解你的信号特征,选择匹配的工具。位移法实现一阶低通滤波,在单片机资源受限的场景下是一个非常实用的选择。它用极少的计算资源换来了可接受的滤波效果,特别适合采样率不高、信号变化不快的工业控制和传感器采集应用。把移位位数、精度位数、初始化策略这几个参数调好,基本能覆盖大部分需求。如果哪天你发现一阶不够用了,再考虑二阶或者自适应方案,但那是另一个话题了。
1. 为什么偏偏要聊“位移法”实现一阶低通滤波搞过单片机信号采集的人都有一个共同的痛:传感器出来的数据永远在跳。你明明测的是一个恒定不变的物理量,串口打印出来的数值却像心电图一样上下乱窜,偶尔还蹦出几个离谱的野值。这时候老工程师通常会丢给你一句话——“加个滤波”。于是你打开搜索引擎,搜到的方案五花八门:均值滤波、中值滤波、卡尔曼滤波、一阶低通滤波……代码复制粘贴一通,跑起来好像也能用,但真要问一句“这个系数为什么取0.1而不是0.01”,很多人就答不上来了。这篇内容要聊的,是一阶低通滤波的一种非常实用的实现方式——位移法。它不依赖浮点运算,不需要调用数学库,在8位单片机上也能跑得飞快,特别适合ADC采样后的实时数据处理。核心关键词就三个:一阶低通滤波、位移法、单片机实战。我会从原理推导开始,把系数计算、整数化处理、移位替代除法、定点数精度控制这些细节全部拆开讲清楚,最后给出一套可以直接移植的C代码实现,以及我在实际项目中踩过的坑和排查经验。适合谁看?如果你正在做传感器数据采集、电机控制、电源管理、或者任何需要从噪声中提取有效信号的嵌入式项目,这篇内容应该能帮你省下不少调试时间。哪怕你之前没接触过滤波算法,只要会写基本的C语言,跟着思路走也能理解。我不会堆一堆公式吓人,而是尽量用生活化的类比把原理讲透,再落到代码上。先说一个我自己的经历。早些年做一个温控项目,用的是一款常见的数字温度传感器,分辨率0.0625度,理论上精度够用。但实际读回来的温度值一直在±0.5度范围内跳动,导致继电器频繁启停,用户体验极差。当时第一反应是用均值滤波,攒够10个样本取平均。结果响应变得很慢,温度已经明显变化了,显示值还在慢慢爬。后来换成中值滤波,野值确实被干掉了,但周期性噪声依然存在。最后用一阶低通滤波配合位移法实现,才在响应速度和稳定性之间找到了平衡点。这段经历让我意识到,滤波方案的选择不是“哪个高级用哪个”,而是要看你的信号特征和系统资源。2. 一阶低通滤波的核心原理拆解2.1 从模拟RC电路到数字滤波的思维跨越一阶低通滤波这个概念,最早来自模拟电路里的RC电路。一个电阻和一个电容串联,输入信号经过电阻给电容充电,电容两端的电压就是输出。因为电容的充放电需要时间,高频信号来不及让电容电压跟上变化,就被“滤掉”了;低频信号有足够的时间让电容充放电,所以能顺利通过。这就是低通滤波的物理本质。用数学语言描述这个电路,得到一个一阶微分方程,解出来就是指数衰减的形式。到了数字域,我们没法用电阻电容,但可以用“本次采样值”和“上次滤波输出值”做加权平均来模拟这个充放电过程。公式非常简洁:Y(n) = α × X(n) + (1 - α) × Y(n-1)其中X(n)是本次ADC采样值,Y(n)是本次滤波输出,Y(n-1)是上次滤波输出,α是滤波系数,取值范围0到1。这个公式的含义是:新的输出值由“新采样值”和“旧输出值”共同决定,α越大,越信任新采样值,响应越快但滤波效果越弱;α越小,越信任历史值,输出越平滑但响应越慢。你可以把它想象成一个水池。新采样值是从进水管流进来的水,旧输出值是池子里已有的水。α就是进水管的阀门开度。阀门开得大,池子里的水更新快,但水位波动也大;阀门开得小,水位稳定,但想让它涨上去或者降下来都需要很长时间。这个类比虽然不严谨,但能帮你快速建立直觉。2.2 滤波系数α到底怎么选α的选取是一阶低通滤波最核心的问题,也是最容易拍脑袋决定的地方。很多人直接抄别人的代码,看到α=0.1就跟着用,结果要么响应太慢,要么滤波效果不够。其实α和截止频率之间有明确的数学关系。在数字滤波中,截止频率fc、采样频率fs和系数α满足:α = 1 - exp(-2π × fc / fs)反过来,如果你先确定了α,也可以算出对应的截止频率:fc = -fs × ln(1 - α) / (2π)这个公式告诉你一件重要的事:α不是孤立的,它和你的采样率强相关。同样的α=0.1,在1kHz采样率下截止频率大约是16.7Hz,在100Hz采样率下截止频率只有1.67Hz。所以脱离采样率谈α是没有意义的。实际工程中怎么定?我的经验是分三步走。第一步,明确你需要保留的信号最高频率是多少。比如测温度,温度变化本身很慢,可能1Hz都不到;测振动,可能需要保留几百Hz。第二步,确定你的采样率,通常要满足奈奎斯特采样定理,采样率至少是信号最高频率的2倍以上,工程上一般取5到10倍。第三步,用上面的公式算出α的初始值,然后根据实际效果微调。举个具体例子。假设你做一个电子秤,采样率100Hz,你需要保留的重量变化信号最高频率大约2Hz(已经很快了,正常称重远低于这个)。那么:α = 1 - exp(-2π × 2 / 100) = 1 - exp(-0.1257) ≈ 0.118取α=0.12左右就比较合适。如果你发现响应还是慢,可以适当加大到0.15;如果噪声还是大,可以减小到0.08。这个调整过程需要结合示波器或者串口波形工具来观察。2.3 位移法为什么能替代浮点运算上面那个公式里有乘法,如果α是0.12这种浮点数,在低端单片机上跑起来就很吃力。没有FPU(浮点运算单元)的MCU做一次浮点乘法可能要几十个时钟周期,如果采样率是1kHz,每个采样周期都要算一次,CPU占用率会很难看。位移法的思路很巧妙:把α限制为1/2的整数次幂,这样乘法就可以用右移操作来代替。比如α=1/4,那么:Y(n) = X(n)/4 + 3×Y(n-1)/4除以4就是右移2位,乘以3可以写成(Y(n-1)<<1) + Y(n-1),全是整数移位和加法,速度极快。再比如α=1/8:Y(n) = X(n)/8 + 7×Y(n-1)/8除以8就是右移3位。这种实现方式在8位、16位单片机上特别受欢迎,因为移位指令通常只需要1到2个时钟周期。但这里有一个代价:α的取值被离散化了。你只能选1/2、1/4、1/8、1/16、1/32这些值,对应的α分别是0.5、0.25、0.125、0.0625、0.03125。如果你的目标α是0.12,最接近的是0.125(1/8),误差不大,可以接受。但如果目标α是0.05,最接近的是0.0625(1/16),误差就有点大了。这时候可以考虑用“多次移位组合”来逼近,比如1/16 - 1/64 ≈ 0.0469,但实现复杂度会上升。我的建议是:优先选1/8、1/16、1/32这三个档位,它们覆盖了大多数常见应用场景。1/8适合需要较快响应的场合,1/16是通用档位,1/32适合对稳定性要求极高的慢变信号。3. 位移法的整数化实现细节3.1 定点数表示与精度损失分析位移法虽然避开了浮点运算,但引入了另一个问题:整数除法的截断误差。假设ADC是12位,采样值范围0到4095,滤波输出也在这个范围。如果直接用整数做右移,每次移位都会丢掉低位信息,多次迭代后误差会累积。举个例子。假设α=1/8,当前采样值X=1000,上次输出Y=0。第一次计算:Y = 1000/8 + 7×0/8 = 125 + 0 = 125看起来没问题。但下一次如果X还是1000:Y = 1000/8 + 7×125/8 = 125 + 109 = 234再下一次:Y = 125 + 7×234/8 = 125 + 204 = 329你会发现输出在慢慢逼近1000,但每次都有截断误差。最终稳定时,输出会在1000附近的一个小范围内波动,无法完全等于1000。这个波动幅度取决于移位位数,移位越多,截断误差越大。解决办法是在累加器里保留额外的低位精度。具体做法是:把滤波输出值左移N位存储,计算时用这个放大后的值,最后输出时再右移N位。比如N=4,那么内部变量范围变成0到4095×16=65520,用16位无符号整数刚好能放下。这样截断误差被缩小了16倍,输出波动几乎可以忽略。代码大概长这样:#define FILTER_SHIFT 3 // α = 1/8 #define PRECISION_SHIFT 4 // 额外精度位 static uint16_t filter_acc = 0; // 内部累加器,放大16倍 uint16_t lowpass_filter(uint16_t new_sample) { // 将新采样值也放大16倍 uint32_t x = (uint32_t)new_sample << PRECISION_SHIFT; // Y(n) = X(n)/8 + 7*Y(n-1)/8 filter_acc = (x >> FILTER_SHIFT) + filter_acc - (filter_acc >> FILTER_SHIFT); // 返回时缩小16倍 return (uint16_t)(filter_acc >> PRECISION_SHIFT); }注意这里用了一个小技巧:7*Y/8写成Y - Y/8,避免乘法。filter_acc - (filter_acc >> FILTER_SHIFT)就是Y - Y/8 = 7Y/8。整个计算只有移位、加法和减法,效率极高。3.2 不同移位位数的性能对比为了让你直观感受不同α档位的效果,我用同一组模拟数据做了对比测试。模拟信号是叠加了高斯噪声的阶跃信号,采样率100Hz,阶跃从0跳到1000。测试结果如下:移位位数α值等效截止频率(Hz)达到90%终值时间(s)稳态波动幅度(LSB)10.511.00.04±820.254.60.09±430.1252.10.18±240.06251.00.37±150.031250.50.73±0.5这张表的信息量很大。移位1位时响应极快,40ms就达到90%,但稳态波动有±8个LSB,对于12位ADC来说相当于0.2%的噪声,很多场合不够用。移位3位时响应时间180ms,稳态波动降到±2个LSB,适合大多数工业控制场景。移位5位时响应要0.73秒,但波动只有±0.5个LSB,适合精密测量。选择哪个档位,本质上是在响应速度和稳态精度之间做权衡。我的经验法则是:先确定你能容忍的最大响应延迟,然后选满足这个延迟的前提下最大的移位位数。比如你的控制系统要求200ms内响应到位,那就选移位3位;如果要求50ms内响应,只能选移位1位或2位。3.3 初始值处理与启动收敛问题一阶低通滤波有一个容易被忽视的问题:启动时的收敛过程。如果滤波器的初始输出值Y(0)设为0,而实际信号一开始就是1000,那么输出需要经过一段时间才能爬升到1000附近。这段时间里,滤波输出和真实值偏差很大,如果控制系统直接使用这个输出,可能会导致误动作。解决办法有两个。第一个是用第一次采样值初始化滤波器:static uint8_t filter_initialized = 0; uint16_t lowpass_filter(uint16_t new_sample) { if (!filter_initialized) { filter_acc = (uint32_t)new_sample << PRECISION_SHIFT; filter_initialized = 1; return new_sample; } // ... 正常滤波计算 }这样第一次输出就等于第一次采样值,没有爬升过程。第二个办法是在启动阶段用更快的α,等输出稳定后再切换到正常α。这种方法更复杂,但适合那些启动时信号本身就变化剧烈的场景。我个人的习惯是第一种,简单可靠。但要注意,如果第一次采样恰好是一个野值,那滤波器就被“带偏”了。所以更稳妥的做法是:先连续采几次,取中值作为初始值,然后再开始滤波。多花几毫秒,换来更稳的启动。4. 单片机实战:从ADC采样到滤波输出4.1 硬件平台与ADC配置要点为了让讨论具体化,我以一个典型的STM32F103项目为例,ADC是12位精度,采样率设为1kHz,通过定时器触发ADC转换,转换完成后DMA搬运数据,主循环里做滤波处理。这套配置在工业控制里非常常见。ADC配置有几个关键点需要注意。第一,采样时间要足够长。如果传感器输出阻抗较高,采样时间太短会导致采样电容充不满,读数偏低。一般建议采样时间至少设为7.5个ADC时钟周期以上,高阻抗传感器要设到41.5或239.5个周期。第二,参考电压要干净。ADC的参考电压直接决定转换精度,如果参考电压上有噪声,再好的滤波算法也救不回来。建议在参考电压引脚旁边放一个1uF到10uF的陶瓷电容,再并联一个0.1uF的高频电容。第三,模拟地和数字地要分开走线,最后在一点汇合,避免数字噪声串到模拟部分。DMA搬运的好处是CPU不用干预每次ADC转换,数据自动存到缓冲区。但要注意缓冲区大小和DMA传输完成中断的配合。如果缓冲区设得太小,中断太频繁,CPU开销大;设得太大,数据延迟增加。我的经验是缓冲区设8到16个样本比较合适,中断频率在60到125Hz之间,对大多数应用来说可以接受。4.2 完整代码实现与逐行解析下面是一套可以直接移植的C代码,包含滤波器结构体定义、初始化函数和滤波计算函数。我把它设计成可重入的,每个通道可以独立使用一个滤波器实例。#include <stdint.h> #include <stdbool.h> typedef struct { uint32_t acc; // 内部累加器,放大2^PRECISION_SHIFT倍 uint8_t shift; // 移位位数,对应α=1/(2^shift) uint8_t precision; // 额外精度位数 bool initialized; // 是否已初始化 } lpf_t; // 初始化滤波器 void lpf_init(lpf_t *f, uint8_t shift, uint8_t precision) { f->acc = 0; f->shift = shift; f->precision = precision; f->initialized = false; } // 滤波计算,输入为原始ADC值,返回滤波后的值 uint16_t lpf_update(lpf_t *f, uint16_t sample) { uint32_t x; // 首次调用,用采样值直接初始化累加器 if (!f->initialized) { f->acc = (uint32_t)sample << f->precision; f->initialized = true; return sample; } // 将新采样值放大到与累加器相同的量级 x = (uint32_t)sample << f->precision; // Y(n) = X(n)/2^shift + Y(n-1) - Y(n-1)/2^shift // 等价于 Y(n) = α*X(n) + (1-α)*Y(n-1),其中α=1/2^shift f->acc = (x >> f->shift) + f->acc - (f->acc >> f->shift); // 缩小回原始量级并返回 return (uint16_t)(f->acc >> f->precision); }逐行解析一下关键部分。f->acc是内部累加器,它存储的是放大后的滤波输出值。放大倍数由precision决定,比如precision=4就是放大16倍。为什么要放大?前面讲过,是为了保留低位精度,减少截断误差。x = (uint32_t)sample << f->precision把新采样值也放大到同一量级,这样加减法才有意义。注意这里用了uint32_t强制转换,防止16位整数左移溢出。如果sample是12位,precision是4,那么x最大是4095×16=65520,用16位无符号整数刚好能放下,但为了安全还是用32位。核心计算f->acc = (x >> f->shift) + f->acc - (f->acc >> f->shift)这一行,对应公式Y(n) = X(n)/2^shift + Y(n-1) - Y(n-1)/2^shift。展开来看就是Y(n) = X(n)/2^shift + Y(n-1)*(1 - 1/2^shift),正好是一阶低通滤波的标准形式。整个计算只有移位、加法和减法,没有乘除,在Cortex-M3内核上大约需要6到8个时钟周期,1kHz采样率下CPU占用率不到1%。返回时f->acc >> f->precision把累加器缩小回原始量级。由于累加器里保留了额外精度,这个右移操作相当于做了四舍五入的近似,输出值比直接整数运算平滑得多。4.3 多通道管理与内存优化实际项目里往往不止一路ADC,可能有4路、8路甚至更多。如果每路都用一个独立的lpf_t结构体,内存占用是通道数 × sizeof(lpf_t)。在STM32F103上,lpf_t大约占8个字节,8路就是64字节,对于有20KB RAM的芯片来说完全不是问题。但如果你的芯片只有1KB RAM,就需要考虑优化了。优化思路有几个。第一,共用移位参数。如果所有通道的滤波需求相同,可以把shift和precision提出来做成全局常量,结构体里只保留acc和initialized,这样每个通道只占5个字节。第二,用位域压缩。shift最大31,用5位就够了;precision最大15,用4位;initialized用1位。加起来10位,可以塞进一个16位变量里。第三,如果通道数很多但不需要同时滤波,可以用一个结构体轮流处理,处理完一路把acc存到数组里,下次处理时再取出来。我一般推荐第一种方案,简单且不容易出错。代码改动很小,把shift和precision从结构体里去掉,改成宏定义或者全局变量即可。#define LPF_SHIFT 3 #define LPF_PRECISION 4 typedef struct { uint32_t acc; bool initialized; } lpf_simple_t; uint16_t lpf_simple_update(lpf_simple_t *f, uint16_t sample) { if (!f->initialized) { f->acc = (uint32_t)sample << LPF_PRECISION; f->initialized = true; return sample; } uint32_t x = (uint32_t)sample << LPF_PRECISION; f->acc = (x >> LPF_SHIFT) + f->acc - (f->acc >> LPF_SHIFT); return (uint16_t)(f->acc >> LPF_PRECISION); }这样每个通道只占5个字节(uint32_t加bool,可能有对齐填充),8路也才40字节。5. 常见问题与排查技巧实录5.1 滤波后数据仍然跳动怎么办这是最常见的问题。你按照公式实现了滤波,但输出还是不够稳。排查思路按优先级排列:第一,检查ADC本身是否稳定。把传感器换成固定的参考电压,看ADC读数是否稳定。如果参考电压下读数都在跳,那问题出在硬件上,滤波算法再厉害也没用。常见原因包括参考电压噪声、模拟地干扰、采样时间不足、传感器供电不稳。第二,确认滤波系数是否合适。如果你用的是移位3位(α=0.125),理论上稳态波动应该很小。但如果ADC噪声幅度特别大,比如±20个LSB,那滤波后可能还有±2到±3个LSB的波动。这时候要么加大移位位数,要么在滤波前先做一次中值滤波去掉野值。第三,检查是否有周期性干扰。如果波动呈现明显的周期性,比如50Hz工频干扰,那可能是电源或者空间耦合进来的。这种情况下单纯的低通滤波效果有限,需要从源头解决,比如加屏蔽、改走线、增加电源滤波电容。第四,确认数据类型没有溢出。uint32_t的累加器在precision=4、shift=3时,最大值大约是65520×8=524160,远小于uint32_t的最大值42亿,不会溢出。但如果你把precision设得很大,比如8,那累加器最大值是4095×256=1048320,仍然安全。但如果ADC是16位,precision=8,最大值就是65535×256=16776960,也没问题。真正会溢出的是uint16_t累加器,所以一定要用uint32_t。5.2 响应速度太慢怎么调响应慢和滤波效果好是一对矛盾。如果你发现系统响应跟不上,首先确认是不是移位位数太大了。移位5位(α=0.03125)的响应时间接近0.7秒,对于很多控制系统来说太慢了。可以尝试降到移位4位或3位。如果降移位位数后噪声又变大了,可以考虑变系数滤波:在信号变化剧烈时用大α,信号平稳时用小α。实现方法是监测相邻两次采样值的差值,如果差值超过阈值,临时把shift减小1到2位,否则保持正常shift。这样既保证了快速响应,又保证了稳态精度。uint16_t lpf_adaptive_update(lpf_t *f, uint16_t sample) { static uint16_t last_sample = 0; uint16_t diff = (sample > last_sample) ? (sample - last_sample) : (last_sample - sample); uint8_t shift = f->shift; // 如果变化剧烈,临时提高响应速度 if (diff > 50 && shift > 1) { shift -= 1; } // ... 用shift做滤波计算 last_sample = sample; }阈值50需要根据你的信号幅度和噪声水平来调整。太大起不到作用,太小会导致滤波器频繁切换,反而引入额外波动。5.3 多路ADC之间的串扰问题如果你用多个ADC通道轮流采样,可能会发现某一路的滤波输出受到其他路的影响。这通常是因为ADC采样保持电容在通道切换后没有足够时间稳定。解决办法是在通道切换后丢弃第一次采样,或者增加采样时间。另一个可能的原因是DMA缓冲区管理不当,导致数据错位。比如你配置了4个通道的规则组,DMA缓冲区大小是8,那么前4个数据是通道0到3,后4个又是通道0到3。如果你按顺序取数据但没做通道对齐,就会把通道1的数据当成通道0的。这种问题在调试时表现为“滤波输出偶尔跳一下”,很容易被误认为是噪声。排查方法是:在DMA传输完成中断里打印缓冲区所有数据,对照通道顺序检查。如果发现错位,调整缓冲区大小或者用DMA的循环模式配合通道计数器。5.4 常见问题速查表现象可能原因排查方法解决措施滤波输出仍有较大波动ADC噪声过大短接ADC输入看读数增加硬件滤波、加大移位位数响应明显滞后移位位数过大计算等效截止频率减小移位位数、用变系数滤波输出偶尔跳变野值干扰串口打印原始数据前置中值滤波、检查传感器接触多通道互相影响采样保持电容未稳定切换通道后连续采样两次丢弃第一次、增加采样时间启动时输出爬升慢初始值设为0观察启动波形用首次采样值初始化长时间运行后输出漂移累加器溢出检查数据类型改用uint32_t累加器滤波效果时好时坏采样率不稳定用示波器测ADC触发信号检查定时器配置、优先用硬件触发这张表是我在实际项目中反复验证过的,基本上覆盖了80%以上的常见问题。遇到问题时按表排查,能省不少时间。6. 进阶话题:从一阶到二阶的平滑过渡一阶低通滤波的局限性在于:它只有一个极点,过渡带衰减斜率是-20dB/十倍频。如果你需要更陡的衰减,比如-40dB/十倍频,就需要二阶滤波。二阶滤波可以看成两个一阶滤波串联,但直接串联会引入额外的相位延迟,而且两个滤波器的系数需要仔细匹配。一个实用的二阶实现方式是:先用一阶滤波得到中间值,再对中间值做一次一阶滤波。两次滤波的α可以相同也可以不同。如果相同,等效于一个二阶巴特沃斯滤波器,但截止频率会偏移。如果不同,可以通过调整两个α来优化响应。typedef struct { lpf_t stage1; lpf_t stage2; } lpf2_t; uint16_t lpf2_update(lpf2_t *f, uint16_t sample) { uint16_t mid = lpf_update(&f->stage1, sample); return lpf_update(&f->stage2, mid); }这种实现简单直接,但要注意两次滤波都会引入延迟,总延迟大约是一阶的两倍。如果系统对延迟敏感,需要谨慎使用。另一个进阶方向是自适应滤波,根据信号的信噪比动态调整α。信噪比高时用大α快速跟踪,信噪比低时用小α强力滤波。信噪比可以用信号方差和噪声方差的比值来估计,但计算量会大一些。在资源有限的单片机上,通常用简化方法,比如前面提到的差值阈值法。我在一个电机电流采样项目里用过自适应滤波。电机启动时电流变化剧烈,需要快速响应;稳态时电流平稳,需要高精度。用固定α很难同时满足两个需求,自适应方案完美解决了这个问题。实现代码只比固定α多了十几行,效果提升非常明显。7. 一些掏心窝子的实操心得滤波算法这东西,理论是一回事,实际调参是另一回事。我见过太多人把公式背得滚瓜烂熟,一到现场就抓瞎。分享几条我踩坑换来的经验。第一条:先看原始数据,再谈滤波。很多人一上来就调滤波参数,却从来没仔细看过原始ADC数据的波形。你连噪声长什么样都不知道,怎么知道该用什么滤波?我的习惯是先用串口把原始数据传到电脑上,用波形工具画出来,看清楚噪声的频率、幅度、是周期性还是随机性。这一步花十分钟,能省后面几小时的盲目调试。第二条:滤波不是万能的,硬件问题要硬件解决。如果电源纹波有100mV,你指望用数字滤波把ADC读数滤稳,那是缘木求鱼。先把电源搞干净,把地线走好,把屏蔽做好,然后再上滤波算法。我见过一个项目,工程师花了三天调滤波参数,最后发现是传感器供电引脚虚焊。第三条:移位位数不要超过5。移位5位对应α=0.03125,响应时间已经接近1秒了。再小的话,除非你的信号变化极慢(比如环境温度监测),否则没有实际意义。而且移位位数越大,截断误差累积越严重,即使加了精度位也救不回来。第四条:多通道滤波时,每个通道独立初始化。我见过有人用一个全局变量做累加器,结果通道0的数据污染了通道1的滤波输出。这种bug很隐蔽,因为单通道测试时完全正常,只有多通道同时跑才出问题。所以滤波器结构体一定要每个通道一份,初始化也要逐个做。第五条:保留原始数据接口。调试阶段,一定要能随时切换到“不滤波”模式,直接输出原始ADC值。这样当滤波效果不对时,你可以快速判断是滤波算法的问题还是ADC本身的问题。我的做法是在滤波器结构体里加一个bypass标志,置位时直接返回原始值。第六条:注意采样率和滤波系数的匹配。如果你改了定时器配置,采样率从1kHz变成2kHz,但滤波系数没改,那等效截止频率就翻倍了,滤波效果会明显变差。所以采样率和滤波系数要一起改,改完重新验证。最后说一个我最近才想明白的事。一阶低通滤波的本质是“用历史信息平滑当前观测”,它假设信号是缓慢变化的,噪声是快速变化的。但如果信号本身就有快速变化的需求,比如你要检测一个突发的脉冲,那一阶低通滤波就会把脉冲也滤掉。这时候需要考虑其他方案,比如中值滤波加阈值检测,或者用小波变换做多尺度分析。没有一种滤波算法是万能的,关键是理解你的信号特征,选择匹配的工具。位移法实现一阶低通滤波,在单片机资源受限的场景下是一个非常实用的选择。它用极少的计算资源换来了可接受的滤波效果,特别适合采样率不高、信号变化不快的工业控制和传感器采集应用。把移位位数、精度位数、初始化策略这几个参数调好,基本能覆盖大部分需求。如果哪天你发现一阶不够用了,再考虑二阶或者自适应方案,但那是另一个话题了。