☰
嵌入式三阶RC低通滤波器:移位运算替代除法的高效实现
2026/10/20 1:24:01 网站建设 项目流程

1. 为什么要在嵌入式中折腾三阶RC低通滤波器

做嵌入式信号采集的朋友多半遇到过这种场景:ADC采回来的波形毛刺满天飞,原始数据跳得比心电图还刺激,直接拿去做控制或者显示根本没法用。这时候第一反应就是上滤波,而RC低通滤波器因为结构简单、参数直观,成了大多数人的首选。但问题来了,一阶RC滤波的滚降斜率只有-20dB/十倍频,遇到稍微强一点的干扰根本压不住,于是自然就想到多阶级联。二阶、三阶能把斜率拉到-40dB、-60dB每十倍频,效果立竿见影。

可真正动手写代码的时候,麻烦才刚开始。数字滤波器的标准实现里躲不开除法运算,而很多低端MCU——尤其是那些跑在8位或16位架构上的芯片——硬件除法器要么根本没有,要么慢得让人想砸键盘。一次除法动辄几十个时钟周期,放在高频采样中断里,CPU大半时间都在做除法,主循环都快饿死了。这时候“用移位运算替代除法”这个思路就变得非常有吸引力,因为移位在绝大多数MCU上都是单周期指令,速度快到可以忽略不计。

这篇内容就是围绕这个核心矛盾展开的:怎么把一个三阶RC低通滤波器用移位运算高效地实现出来,既保证滤波效果,又不把CPU榨干。适合那些正在做传感器信号调理、电源纹波采集、音频前端处理的嵌入式开发者,尤其是资源受限平台上的朋友。我会把参数怎么定、系数怎么凑、代码怎么写、坑怎么避,全部拆开讲清楚,你照着抄作业就能用。

2. 三阶RC低通滤波器的核心原理与离散化思路

2.1 从模拟RC到数字滤波的映射逻辑

模拟三阶RC低通滤波器就是三个一阶RC环节级联,每个环节的传递函数是1/(1+sRC),三个乘起来就是1/(1+sRC)³。截止频率fc=1/(2πRC),这个公式大家都熟。但到了数字域,不能直接把s换成jω就完事,得做离散化。最常用的方法是双线性变换或者后向差分,前者频率映射更准确但计算复杂,后者简单粗暴但在低频段够用。

对于嵌入式场景,我一般推荐后向差分,因为它推导出来的差分方程形式最简洁,容易改写成移位运算。后向差分的映射关系是s≈(1-z⁻¹)/T,代入一阶RC的传递函数,整理后得到:

y[n] = y[n-1] + α·(x[n] - y[n-1])

其中α = T/(RC+T),T是采样周期。这个α就是滤波系数,取值范围在0到1之间。三阶级联就是把上一级的输出作为下一级的输入,重复三次这个运算。

2.2 为什么移位运算能替代除法

关键就在α这个系数上。如果α刚好是1/2、1/4、1/8这种2的负整数次幂,那么α·(x[n]-y[n-1])就可以写成(x[n]-y[n-1])>>k,其中k是对应的移位位数。比如α=1/8,就是右移3位。右移在C语言里就是>>操作符,编译后基本就是一条机器指令,比除法快一个数量级不止。

那怎么让α落在这些“好算”的值上?这就需要在设计阶段反推RC和T的取值。给定目标截止频率fc,先选定一个合适的α值(比如1/8),然后根据α=T/(RC+T)反解出RC,再结合采样率T确定具体的R和C。实际电路中R和C都有标准值系列,不可能完全精确,但数字滤波器的系数是我们自己定的,只要在代码里用那个凑好的α就行,模拟前端的RC只负责抗混叠,精度要求没那么苛刻。

2.3 三阶级联的相位延迟与稳定性考量

三阶级联有个绕不开的问题:相位延迟累加。每一阶都会引入滞后,三阶加起来在截止频率附近的相位偏移能达到135度以上。如果你的应用是闭环控制,这个延迟可能直接导致振荡。我踩过这个坑,当时做一个温度控制环路,用了三阶滤波后系统开始周期性波动,查了半天才发现是滤波器相位裕度不够。

解决办法有两个:一是把截止频率设得比控制带宽高至少5倍,让滤波器在工作频段内的相位偏移小到可以忽略;二是改用二阶甚至一阶,牺牲滤波效果换稳定性。具体怎么选,得看你的信号带宽和干扰频率分布。如果干扰频率远高于信号带宽,三阶没问题;如果两者挨得近,就得慎重。

另外,级联时每一级的输出范围要留意。定点运算下,如果中间结果溢出,整个滤波器就废了。我通常会在每级之后加一个饱和处理,虽然多几条指令,但能避免莫名其妙的数据跳变。

3. 系数凑整与移位替代除法的实操细节

3.1 如何把滤波系数凑成2的负整数次幂

这一步是整个实现的核心。假设你的采样率是1kHz,目标截止频率是10Hz,那么时间常数RC≈1/(2π×10)≈0.0159秒。采样周期T=0.001秒。α=T/(RC+T)=0.001/(0.0159+0.001)≈0.0592。这个值离1/16=0.0625很近,离1/8=0.125就太远了。所以选α=1/16,对应右移4位。

但α=1/16意味着实际截止频率会偏移。反算一下:α=1/16时,RC=T(1-α)/α=0.001×15=0.015秒,fc=1/(2π×0.015)≈10.6Hz。跟目标10Hz差了6%,对于大多数传感器应用完全可以接受。如果你对截止频率精度要求极高,那就别用移位法,老老实实上浮点或者定点乘法。

我一般会做一个表格,把常用的α值和对应的移位位数、截止频率偏差列出来,设计时直接查表。比如:

α值移位操作等效除法截止频率偏差范围
1/2>>1÷2较大,仅用于高频
1/4>>2÷4中等
1/8>>3÷8较小
1/16>>4÷16很小
1/32>>5÷32极小
1/64>>6÷64极小但响应慢

α越小,滤波越平滑,但响应速度越慢,相位延迟越大。这是个权衡,没有免费午餐。

3.2 定点运算中的Q格式选择与溢出防范

嵌入式里做小数运算,绕不开Q格式。简单说就是用整数表示小数,比如Q15就是把1.0表示为32768。但移位法其实不需要严格的Q格式,因为α本身就是2的负幂,移位后自然就是小数结果。真正要小心的是中间变量的位宽。

假设ADC是12位,输入范围0到4095。第一级滤波后,输出范围还是0到4095,不会溢出。但如果你把输入左移放大后再滤波,就得算清楚最大可能值。我习惯用32位中间变量,最后再截断回16位或12位。这样即使三级累加,也不会溢出。

还有一个坑:右移操作对负数是有符号扩展的,如果你用的是有符号整数,右移会保持符号位,结果正确。但如果用无符号整数,右移就是逻辑移位,负数会变成大正数。所以滤波器变量一律用有符号类型,别图省事用uint。

3.3 三阶级联的代码结构设计

代码结构上,我见过两种写法:一种是写一个函数,内部循环三次;另一种是展开成三段独立的代码。前者代码简洁但循环有开销,后者代码长但执行快。在资源紧张的MCU上,我倾向于展开,因为三次循环的计数器操作和跳转指令加起来可能比滤波本身还耗时。

展开后的核心代码大概长这样:

int32_t filter3(int32_t input) { static int32_t y1 = 0, y2 = 0, y3 = 0; int32_t d; d = input - y1; y1 += d >> 4; // α=1/16 d = y1 - y2; y2 += d >> 4; d = y2 - y3; y3 += d >> 4; return y3; }

这段代码里,每次滤波只用了减法、加法和移位,没有乘法也没有除法。在大多数MCU上,这四条指令加起来不到10个时钟周期。对比一下用除法或者浮点的版本,动辄上百周期,差距是数量级的。

但注意,这里的y1、y2、y3是静态变量,意味着这个滤波器不是可重入的。如果你有多个通道需要滤波,要么给每个通道单独写一套变量,要么把状态打包成结构体传参。我一般用结构体,清晰且不容易出错。

4. 完整实现流程与参数计算实例

4.1 从需求到参数的完整推导链条

假设你要做一个电池电压监测,采样率1kHz,希望滤除50Hz以上的干扰,同时保留电池电压的缓慢变化。电池电压变化通常不到1Hz,所以截止频率设在10Hz左右很合适。

第一步,确定α。根据前面的计算,α=1/16对应fc≈10.6Hz,满足需求。移位位数k=4。

第二步,确定模拟前端RC。虽然数字滤波已经搞定了主要滤波任务,但ADC前面还是需要一个RC做抗混叠,防止高频信号折叠到低频。抗混叠滤波器的截止频率应该设在采样率的一半以下,这里1kHz采样,奈奎斯特频率是500Hz,RC截止频率设在200Hz左右比较稳妥。取R=1kΩ,C=0.82μF,fc=1/(2π×1000×0.82e-6)≈194Hz,接近200Hz,可以。

第三步,确定ADC分辨率和参考电压。假设12位ADC,参考电压3.3V,那么LSB对应3.3/4096≈0.8mV。电池电压经过分压后接入ADC,分压比要保证满量程时不超过3.3V。

第四步,写代码。初始化时把y1、y2、y3清零,然后每次ADC采样中断里调用滤波函数。输出可以进一步做标度变换,转换成实际电压值。

4.2 采样率与截止频率的匹配计算

采样率的选择有个经验法则:至少是截止频率的10倍,最好是20倍以上。1kHz采样对10Hz截止频率,比值是100,非常充裕。但如果你的截止频率是100Hz,采样率至少得1kHz,最好2kHz。比值太低会导致离散化误差增大,滤波器的实际频响和理论值偏差明显。

我做过一个对比实验,截止频率100Hz,采样率分别用500Hz、1kHz、2kHz,用同样的α=1/16。结果500Hz时实际截止频率偏移到120Hz以上,1kHz时偏移到105Hz左右,2kHz时基本就是100Hz。所以采样率越高,离散化越接近连续域,但代价是CPU负载增加。这个平衡点得根据你的MCU性能来定。

还有一个隐藏问题:如果采样率不是固定的,比如用定时器触发但偶尔有抖动,那么α对应的实际截止频率也会跟着抖。对于大多数应用影响不大,但如果你的系统对滤波特性要求严格,就得保证采样时钟稳定。

4.3 代码实现与逐行解析

把上面的思路整理成完整的C代码,大概是这样:

#include <stdint.h> typedef struct { int32_t y1; int32_t y2; int32_t y3; } RC3Filter; void RC3_Init(RC3Filter *f) { f->y1 = 0; f->y2 = 0; f->y3 = 0; } int32_t RC3_Process(RC3Filter *f, int32_t input) { int32_t d; d = input - f->y1; f->y1 += d >> 4; d = f->y1 - f->y2; f->y2 += d >> 4; d = f->y2 - f->y3; f->y3 += d >> 4; return f->y3; }

逐行看:d = input - y1计算当前输入和上一级输出的差值;y1 += d >> 4就是y1 = y1 + d/16,完成一阶滤波。后面两级同理。返回值是第三级的输出,也就是最终滤波结果。

这里有个细节:d >> 4对于正数就是除以16,对于负数,算术右移也是除以16并向下取整,结果和整数除法一致。所以不用担心负数的问题。

如果你需要更高的滤波阶数,比如四阶,就再加一级,多一个状态变量。但阶数越高,相位延迟越大,一般三阶已经够用了。

4.4 实测效果与性能对比

我在一块主频24MHz的Cortex-M0上做过测试,ADC采样率1kHz,在中断里调用滤波函数。用移位版本,每次滤波耗时约1.2微秒,CPU占用率不到0.2%。换成浮点版本(软件浮点),每次耗时约15微秒,CPU占用率接近2%。如果换成整数除法版本,每次约8微秒,占用率约1%。差距非常明显。

滤波效果方面,输入一个叠加了50Hz干扰的直流信号,幅度约100mV峰峰值。经过三阶滤波后,50Hz成分被衰减到不到1mV,直流成分几乎无衰减。用示波器看输出波形,平滑得像用尺子画的一样。

但要注意,这是稳态响应。如果输入信号本身有快速跳变,比如电池突然接入,滤波器的输出会缓慢爬升,时间常数大约是3×RC≈45毫秒。对于电池监测这种慢变量,完全没问题;但如果你要检测按键或者脉冲,这个延迟就太大了。

5. 常见问题与排查技巧实录

5.1 滤波后数据不动或跳变异常

这是最常见的问题,通常有几个原因。第一,移位位数搞错了。比如你想右移4位,结果写成了<<4,那数据直接飞了。第二,中间变量溢出。如果你用的是16位变量,三级滤波后可能超出范围,结果被截断成乱七八糟的值。第三,静态变量没初始化。C语言里静态变量默认是0,但如果你用的是局部变量或者动态分配的内存,里面可能是垃圾值,导致滤波器从错误的状态开始。

排查方法很简单:在滤波函数里加一个调试输出,把每一级的中间结果都打印出来。如果第一级就不对,那问题在输入或者第一级的计算;如果第一级对但第二级不对,那就是级联的问题。我一般用串口把数据传到电脑上,用Excel画个图,一眼就能看出哪里不对。

5.2 截止频率与理论值偏差过大

前面说过,α凑整会导致截止频率偏移。如果你发现实际截止频率和设计值差太多,先检查α是不是选得太极端。比如α=1/2,截止频率会高得离谱,滤波效果很弱。α=1/64,截止频率很低,但响应慢得让人着急。

另一个原因是采样率不稳定。如果你的ADC是用定时器触发的,但定时器配置有问题,实际采样率可能和设定值差很多。用示波器量一下ADC的转换触发信号,确认周期是否准确。

还有一个容易忽略的点:模拟前端的RC也会影响整体频响。如果模拟RC的截止频率和数字滤波的截止频率接近,两者会叠加,导致实际截止频率比设计值低。解决办法是把模拟RC的截止频率设得远高于数字滤波的截止频率,让它只负责抗混叠,不参与主要滤波。

5.3 多通道滤波时的资源冲突

如果你有多个ADC通道需要滤波,每个通道都需要独立的状态变量。我见过有人图省事,所有通道共用一个滤波器结构体,结果通道之间互相干扰,数据完全乱套。正确做法是给每个通道分配一个独立的RC3Filter实例,初始化时分别清零。

如果通道数很多,比如8个或者16个,可以考虑用数组来管理状态变量,然后用循环处理。但前面说过,循环有开销,在高速采样时可能吃不消。折中方案是写一个宏或者内联函数,编译时展开,既避免了循环开销,又不用手动写十几遍重复代码。

5.4 定点运算中的舍入误差累积

移位运算本质上是截断,不是四舍五入。每次滤波都会引入一点误差,三级下来误差会累积。对于大多数应用,这个误差小到可以忽略,因为ADC本身的噪声都比它大。但如果你的输入信号非常小,比如只有几个LSB,那误差就可能影响结果。

解决办法是在移位前加一个偏移量,实现四舍五入。比如d>>4改成(d+8)>>4,这样当d的绝对值大于8时,结果会向上取整。代价是多一条加法指令,但精度提升明显。我一般在低幅度信号处理时会加上这个偏移,高幅度时就不加,省一条指令。

5.5 常见问题速查表

现象可能原因排查方法解决措施
输出恒定不变状态变量未初始化或输入未更新检查初始化代码和ADC读取确保初始化清零,确认ADC在跑
输出跳变剧烈移位方向写反或溢出打印中间变量检查>>和<<,加宽变量位宽
截止频率偏高α值过大反算实际α减小α,增加移位位数
截止频率偏低α值过小或采样率不足检查采样定时器增大α或提高采样率
多通道串扰共用状态变量检查结构体实例每通道独立实例
响应速度太慢阶数过高或α过小观察阶跃响应降低阶数或增大α

6. 进阶优化与扩展思路

6.1 变采样率下的自适应滤波

有些应用里采样率不是固定的,比如用事件触发采样,或者MCU负载变化导致采样间隔抖动。这时候固定的α就不太合适了,因为截止频率会跟着采样率变。一个简单的解决办法是准备几组不同的α值,根据当前采样率切换。比如采样率减半时,α也减半,保持RC乘积不变。

但α减半意味着移位位数加1,这又回到了凑整的问题。如果采样率变化范围不大,比如±10%,那固定α带来的截止频率偏差也就±10%,大多数应用能接受。如果变化范围很大,那就得考虑用乘法代替移位了,或者上更复杂的自适应算法。

6.2 从三阶扩展到任意阶的通用框架

三阶够用不代表所有场景都够用。如果你需要更陡的滚降,比如-80dB/十倍频,那就得上四阶甚至更高。代码结构其实是一样的,就是多几级级联。但每增加一级,相位延迟和计算量都线性增加。我的建议是先用三阶,如果效果不够再往上加,别一上来就搞个八阶,纯属浪费CPU。

通用框架可以这样设计:用一个数组存状态变量,用一个参数指定阶数,然后循环处理。虽然循环有开销,但阶数高的时候,循环的开销相对于滤波本身已经可以忽略了。而且代码更简洁,容易维护。

6.3 与其他滤波算法的混合使用

RC低通滤波器适合滤除高频噪声,但对低频干扰比如工频50Hz,效果一般。如果你既要滤高频又要滤工频,可以考虑RC低通加陷波器的组合。陷波器专门针对某个频率,用二阶IIR实现,计算量也不大。两者级联,各司其职。

还有一种情况是信号里有脉冲干扰,比如静电放电导致的尖峰。RC低通对尖峰的抑制有限,因为尖峰能量集中在高频,但幅度太大,滤波后还是有残留。这时候可以在RC滤波前加一个中值滤波,先把尖峰剔掉,再进RC平滑。中值滤波计算量稍大,但只对异常值处理,正常采样时可以跳过,整体开销可控。

6.4 资源极度受限时的极致优化

如果你用的MCU连移位都觉得慢,比如一些超低功耗的8位机,主频只有几MHz,那还可以进一步优化。第一,把滤波函数用汇编重写,手动分配寄存器,减少内存访问。第二,把三级展开成流水线,每一级用不同的寄存器,避免读写冲突。第三,如果采样率很低,比如10Hz,那滤波本身耗时多少已经无所谓了,因为大部分时间CPU都在休眠。

我做过一个极端案例,用一颗主频1MHz的8位机做温度采集,采样率10Hz,滤波用移位法,整个中断服务程序不到20微秒,CPU占用率0.02%。剩下的时间全用来休眠,电池寿命从几个月延长到几年。所以优化无止境,关键看你的瓶颈在哪里。

6.5 调试与验证的实用技巧

最后分享几个调试技巧。第一,用信号发生器输入一个已知频率和幅度的正弦波,测量输出幅度,画出幅频响应曲线,和理论值对比。第二,用阶跃信号测试响应速度,看上升时间是否符合预期。第三,长时间运行,观察输出是否有漂移或者周期性波动,排查数值稳定性问题。

我习惯在代码里加一个编译开关,打开时把滤波前后的数据都通过串口输出,用Python脚本实时画图。这样调参的时候非常直观,改一个α值,立刻能看到波形变化。虽然占点资源,但调试阶段非常值得。

这个三阶RC低通滤波器的移位实现,我从第一次踩坑到现在用了快五年,中间改过无数版本,最后沉淀下来的就是上面这些代码和参数。它不完美,截止频率有偏差,相位有延迟,但在资源受限的嵌入式场景里,它是我能找到的最优解。如果你也在为滤波和CPU负载纠结,不妨试试这个方案,大概率能帮你省下不少折腾的时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询