☰
位移法实现一阶低通滤波:嵌入式定点移位算法与代码落地
2026/10/14 16:36:06 网站建设 项目流程

1. 为什么位移法实现一阶低通滤波值得单独拿出来讲

做嵌入式信号处理的朋友大概率都遇到过这个场景:传感器采回来的数据跳得厉害,直接送进控制算法里,执行机构跟着一顿抖;想加个滤波,翻出教科书上的RC低通公式,发现要算浮点乘法,而手上那颗八位单片机跑浮点慢得让人抓狂。这时候"位移法"就派上用场了——它把浮点乘法换成整数移位,用极小的算力代价换来一条平滑的输出曲线。

一阶低通滤波的本质,是让输出值朝着输入值"缓慢靠近",靠近的速度由滤波系数决定。标准公式是y(n) = α·x(n) + (1-α)·y(n-1),其中 α 是滤波系数,取值在 0 到 1 之间。α 越小,输出越平滑但滞后越大;α 越大,跟随越快但滤波效果越弱。问题就出在这个 α 上:如果 α 取 0.1、0.01 这类值,在定点单片机上做乘法要么损失精度,要么耗时。

位移法的核心思路非常朴素:把 α 设计成 1/2 的整数次幂,也就是 1/2、1/4、1/8、1/16……这样 α·x(n) 就等价于把 x 右移若干位,(1-α)·y(n-1) 也能用移位和减法拼出来。整个滤波过程只剩加减法和移位,一颗最普通的八位机也能在几个机器周期内算完。

这篇内容适合三类人看:一是刚接触嵌入式、被浮点运算卡住的新手;二是在做电机控制、传感器采集、电源管理,需要实时滤波但算力紧张的一线工程师;三是想搞明白"为什么很多开源固件里滤波系数都是 1/16、1/32 这种奇怪数字"的读者。我会从原理推导讲到代码落地,把参数怎么选、坑在哪里、实测效果如何都摊开说清楚。

需要先说明一点:位移法不是万能的,它用"系数只能取 2 的负整数次幂"换来了速度,代价是滤波系数的选择粒度变粗。理解了这一点,后面的取舍就好做了。

2. 从连续域到定点移位:一阶低通的数学底子

2.1 标准一阶低通公式到底在做什么

先把公式摆出来,别急着背:

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

这个式子来自一阶RC低通电路的离散化。连续域的传递函数是H(s) = 1/(1+RC·s),用一阶后向差分离散化之后,就得到上面这个形式,其中 α = T/(RC+T),T 是采样周期。物理意义很直白:新的输出 = 一部分新输入 + 剩下部分的老输出。α 就是"新输入占多大权重"。

举个具体数字感受一下。假设 α = 0.1,当前输入 x = 100,上一次输出 y = 0,那么:

  • y = 0.1×100 + 0.9×0 = 10
  • 下一次输入还是 100,y = 0.1×100 + 0.9×10 = 19
  • 再下一次 y = 0.1×100 + 0.9×19 = 27.1

可以看到输出以指数方式逼近 100,永远追不上但越来越近。这就是低通的"惯性"。α 越小,惯性越大,高频噪声被压得越狠,但真实信号的突变也被拖慢。

2.2 把 α 限制成 2 的负幂次会发生什么

现在关键一步:强制 α = 1/2^k,k 取正整数。代入公式:

y(n) = x(n)/2^k + (1 - 1/2^k)·y(n-1)

在二进制世界里,除以 2^k 就是右移 k 位。而 (1 - 1/2^k)·y(n-1) 可以写成 y(n-1) - y(n-1)/2^k,也就是"老输出减去老输出右移 k 位"。于是整个公式变成:

y(n) = y(n-1) + (x(n) - y(n-1)) / 2^k

这个形式更漂亮:先算输入和老输出的差,右移 k 位,再累加到老输出上。全程只有一次减法、一次移位、一次加法。如果 k 取 4,就是右移 4 位,等价于除以 16,α = 1/16 ≈ 0.0625。

注意:这里的移位是算术右移,对有符号数要保证符号位正确扩展。如果输入是负数(比如电流采样有正负),用逻辑右移会出错,这一点后面代码部分会专门讲。

2.3 系数粒度变粗带来的实际影响

α 只能取 1/2、1/4、1/8、1/16、1/32……这意味着你没法精确地设成 0.05 或 0.08。相邻两档之间差距不小:1/16 = 0.0625,1/32 = 0.03125,直接差一倍。选系数的时候只能在这几档里挑最接近的。

那截止频率怎么算?一阶低通的截止频率 f_c 和 α 的关系是:

f_c = α / (2π·T)

T 是采样周期。假设采样率 1kHz(T = 1ms),α = 1/16 = 0.0625,那么 f_c ≈ 0.0625 / (6.2832 × 0.001) ≈ 9.95Hz。也就是说,高于约 10Hz 的噪声会被明显衰减。如果换成 α = 1/32,截止频率降到约 5Hz,滤波更狠但响应更慢。

实际选型时,先根据信号带宽定一个目标截止频率,反推 α,再在 2 的负幂次里找最接近的。下面这张表可以当速查用(采样率按 1kHz 算):

k 值α = 1/2^k近似截止频率适用场景
20.25约 40Hz信号变化快,轻度滤波
30.125约 20Hz一般传感器平滑
40.0625约 10Hz温度、压力等慢变量
50.03125约 5Hz强噪声环境
60.015625约 2.5Hz极慢变量,如电池电压监测

采样率变了,这张表要按比例缩放。比如采样率 100Hz,同样的 α 对应截止频率只有原来的十分之一。

3. 定点实现里的三个关键决策

3.1 用 Q 格式还是纯整数累加

位移法落地时第一个要决定的是数据表示。常见两种做法:

做法一:纯整数,输出就是整数。直接把 ADC 原始值当输入,移位后累加。优点是简单,缺点是当 k 较大时,(x - y) >> k可能因为差值太小而被移成 0,导致输出"卡死"不再变化。比如 x - y = 15,k = 4,15 >> 4 = 0,输出就停在原地了。这在信号接近稳态时很常见,表现为滤波后有一个固定的稳态误差。

做法二:Q 格式定点,保留小数位。把内部状态放大 2^m 倍存储,比如用 Q8 格式(放大 256 倍)。这样移位后的小数部分不会丢失,稳态精度高。代价是状态变量位宽要够,否则会溢出。

我的经验是:如果输入是 12 位 ADC(0~4095),k 不超过 4,纯整数够用;如果 k 到 5、6,或者对稳态精度有要求,老老实实上 Q 格式。下面给一个 Q8 的实现思路:

// 输入 x 为原始整数,内部状态 y_q8 为 Q8 格式(放大 256 倍) // k = 4,即 α = 1/16 int32_t y_q8 = 0; // 内部状态 int32_t lpf_q8(int32_t x, int k) { int32_t x_q8 = x << 8; // 输入也放大到 Q8 int32_t diff = x_q8 - y_q8; // 差值 y_q8 += (diff >> k); // 累加移位后的差值 return y_q8 >> 8; // 输出还原为整数 }

注意diff >> k对负数在 C 语言里是实现定义行为,大多数编译器做算术右移,但为了可移植性,最好显式处理符号,或者确认目标平台的行为。

3.2 移位方向与舍入误差的累积

纯右移是"向下取整",对正数相当于 floor,对负数相当于向负无穷取整。这会导致一个系统性的偏差:如果差值总是正的小数,每次都被截掉,输出会永远差那么一点点。单次误差很小,但滤波器是迭代的,误差会累积成稳态误差。

解决办法有两个。一是加"四舍五入":在移位前加上 2^(k-1)。比如 k = 4,就加 8 再右移:

y_q8 += (diff + (1 << (k-1))) >> k;

二是用 Q 格式保留小数,让误差留在低位状态里,不丢。前者简单,后者彻底。我一般在对精度敏感时用 Q 格式,对速度极度敏感时用加偏置的整数法。

提示:加偏置法在 diff 为负数时要注意,(diff + 8) >> 4对 -8 会得到 0,对 -9 得到 -1,整体是向零舍入,比纯 floor 更合理。但边界情况要测。

3.3 状态变量位宽怎么定才不溢出

这是新手最容易翻车的地方。假设输入是 12 位 ADC,最大 4095,用 Q8 格式,x_q8 最大就是 4095 × 256 ≈ 104.8 万,需要 21 位。如果状态变量声明成 int16_t,直接溢出,输出乱跳。

位宽估算公式:位宽 ≥ 输入位宽 + Q 格式小数位 + 1位符号 + 余量。12 位输入 + 8 位小数 + 1 位符号 = 21 位,取 32 位 int32_t 稳妥。如果输入是 16 位,Q8 就要 25 位,还是 32 位够。但如果 Q 格式到 Q16,16 位输入就需要 33 位,得用 int64_t 或者降低 Q 精度。

另一个溢出点是累加过程。y_q8 += (diff >> k)里,y_q8 本身不会超过 x_q8 的最大值(因为输出总是介于历史输出和输入之间),所以只要 y_q8 的位宽够表示 x_q8,就不会因为累加溢出。真正要小心的是x << 8这一步,如果 x 是 int16_t 且值较大,左移会溢出,必须先转成 int32_t 再移。

4. 从公式到可运行代码的完整落地

4.1 一个可直接用的整数版滤波函数

先给一个最朴素、最容易移植的版本,适合八位机或者资源极紧的场景:

#include <stdint.h> // 一阶低通滤波,位移法实现 // x: 当前输入采样值 // y: 指向滤波状态的指针(首次调用前置为第一次采样值) // k: 移位位数,α = 1/2^k,建议 2~6 // 返回:滤波后的输出 int16_t lpf_shift(int16_t x, int16_t *y, uint8_t k) { int16_t diff = x - *y; // 加偏置实现四舍五入,避免稳态误差 if (diff >= 0) { *y += (diff + (1 << (k - 1))) >> k; } else { *y += -(((-diff) + (1 << (k - 1))) >> k); } return *y; }

这个版本有几个细节值得说。第一,初始化时*y要设成第一次的采样值,而不是 0,否则滤波器会从 0 慢慢爬上来,前几十个采样点都是废的。第二,正负分开处理是为了避免负数右移的实现定义问题,同时保证舍入方向一致。第三,k 最小取 1,取 0 就变成直通没有滤波意义了。

调用方式:

int16_t filtered; int16_t state = first_sample; // 用第一次采样初始化 void loop(void) { int16_t raw = adc_read(); filtered = lpf_shift(raw, &state, 4); // α = 1/16 // filtered 送后续处理 }

4.2 Q 格式高精度版本及其适用边界

如果稳态精度要求高,或者 k 要取到 5 以上,用 Q8 版本:

#include <stdint.h> typedef struct { int32_t y_q8; // Q8 格式内部状态 uint8_t k; // 移位位数 uint8_t inited; // 初始化标志 } lpf_t; void lpf_init(lpf_t *f, int32_t first_x, uint8_t k) { f->y_q8 = first_x << 8; f->k = k; f->inited = 1; } int32_t lpf_update(lpf_t *f, int32_t x) { int32_t x_q8 = x << 8; int32_t diff = x_q8 - f->y_q8; // 四舍五入右移 if (diff >= 0) { f->y_q8 += (diff + (1 << (f->k - 1))) >> f->k; } else { f->y_q8 += -(((-diff) + (1 << (f->k - 1))) >> f->k); } return f->y_q8 >> 8; }

这个结构体封装的好处是状态和参数绑在一起,多路滤波互不干扰。比如同时滤三路 ADC,声明三个 lpf_t 实例即可。

适用边界要说清楚:Q8 意味着内部精度是 1/256,如果输入信号本身动态范围很小(比如只有 0~100),Q8 有点浪费;如果输入是 16 位满量程,Q8 刚好够用。Q16 精度更高但位宽压力大,一般没必要。

4.3 初始化与首采样处理的坑

前面提了一句初始化,这里展开讲。滤波器的状态 y 如果初始化为 0,而实际信号在 2000 左右,那么输出会从 0 开始,经过约 5 倍时间常数才爬到 2000 附近。时间常数 τ = 2^k 个采样周期,k = 4 就是 16 个周期,5τ = 80 个周期。采样率 1kHz 的话,前 80ms 输出都是不可信的。

正确做法是用第一次有效采样直接初始化状态。如果第一次采样本身就有噪声,可以连续采几次取平均再初始化。有些场景下传感器上电需要稳定时间,那就等稳定后再初始化滤波器。

还有一个坑:如果滤波器中途被复位(比如模式切换),状态要重新用当前采样初始化,不能清 0,否则又会出现爬升过程。

5. 参数整定与实测中的那些意外

5.1 怎么根据信号特征反推 k 值

参数整定没有万能公式,但有一套可操作的流程。第一步,确定你关心的信号带宽。比如测温度,变化周期是秒级甚至分钟级,带宽不到 1Hz;测电机电流,PWM 频率 20kHz,关心的基波可能到几百 Hz。第二步,确定采样率,通常取信号最高频率的 5~10 倍。第三步,用f_c = α/(2πT)反推 α,再找最接近的 1/2^k。

举个例子:测一个变化较慢的压力信号,有效带宽约 5Hz,采样率取 100Hz(T = 10ms)。希望截止频率设在 5Hz 附近,则 α = 2π × 5 × 0.01 ≈ 0.314。最接近的 2 的负幂次是 1/4 = 0.25(k=2)或 1/8 = 0.125(k=3)。选 1/4 截止频率约 4Hz,选 1/8 约 2Hz。如果噪声大就选 1/8,如果怕滞后就选 1/4。

这里有个经验:截止频率不要设得比信号带宽还低太多,否则真实信号的快速变化会被当成噪声滤掉,表现为响应迟钝。一般截止频率取信号带宽的 1~2 倍比较稳妥。

5.2 滤波滞后对闭环控制的影响

开环显示滤波,滞后一点无所谓。但如果是闭环控制,滤波器在反馈回路里,滞后会直接吃掉相位裕度,严重时导致振荡。这是很多人忽略的问题。

一阶低通在截止频率处有约 45 度相位滞后,在截止频率的 0.1 倍处也有约 5.7 度。如果控制环的带宽和滤波截止频率接近,相位裕度会被明显压缩。我的做法是:滤波截止频率至少取控制环带宽的 5~10 倍,这样相位滞后控制在几度以内,对稳定性影响小。如果噪声实在太大必须强滤,那就考虑在控制算法里做相位补偿,或者改用二阶滤波但把相位提前设计进去。

注意:位移法实现的是一阶低通,相位滞后是固有的,不是实现方式带来的。换任何一阶滤波都一样。要减小滞后只能提高截止频率或降低滤波强度。

5.3 实测中输出"卡死"和"抖动"的排查

现象一:输出长时间不变,像卡死了。最常见原因是 k 太大加上纯整数实现,差值被移成 0。排查方法:打印x - y的值,如果经常在 ±(2^k - 1) 以内,就是这个问题。解决:改用 Q 格式,或者减小 k,或者加四舍五入偏置。

现象二:输出还是有明显抖动。可能原因有三个:一是 k 太小,滤波强度不够;二是采样率太低,噪声混叠进来了,这时候要先加抗混叠滤波(硬件RC或前置数字滤波);三是输入信号本身有周期性干扰,一阶滤波对特定频率的抑制有限,需要考虑陷波或其他手段。

现象三:输出有固定偏差。除了前面说的舍入误差,还要检查输入是否有偏置。有些 ADC 有零点偏移,滤波不会消除它,只会平滑它。这种情况要在滤波前做校准。

我踩过最典型的一个坑:用八位机做 Q8 滤波,状态变量声明成 int16_t,结果输入稍大就溢出,输出在正负之间乱跳。查了半天才反应过来是位宽不够。后来统一改成 int32_t,问题消失。这个教训是:位宽估算宁大勿小,32 位在绝大多数单片机上代价可以接受。

6. 位移法之外:什么时候该换方案

位移法虽好,但有明确的适用边界。如果出现下面几种情况,就该考虑其他方案了。

需要精确的截止频率。位移法的 α 只能取离散值,如果你需要精确的 10Hz 截止而 1/16 对应 9.95Hz、1/8 对应 19.9Hz,中间没有档位,那就只能用浮点或定点乘法实现任意 α。现在很多 Cortex-M 系列带 FPU,浮点乘法一两个周期就完成,位移法的速度优势没那么明显了。

需要高阶滤波。一阶滤波的滚降是 -20dB/十倍频,对强噪声抑制不够。二阶、四阶滤波效果更好,但位移法很难直接推广到高阶,通常还是用标准差分方程加乘法。

需要自适应滤波。如果滤波系数要根据信号动态调整,位移法的离散系数就成了障碍。自适应滤波一般需要连续可调的系数。

输入动态范围极大。比如从微伏级到伏级都要处理,定点 Q 格式的位宽会非常紧张,浮点的动态范围优势就体现出来了。

反过来说,如果你的场景是:算力紧张(八位机、低端 Cortex-M0)、采样率高(几十 kHz 以上)、对截止频率精度要求不苛刻、信号动态范围适中,那位移法几乎是性价比最高的选择。我在好几个电机电流采样和电池电压监测的项目里都用它,稳定跑了几年没出过问题。

最后分享一个实用技巧:如果觉得 2 的负幂次档位太粗,可以用"两次移位组合"来逼近中间值。比如想要 α ≈ 0.05,可以用 1/16 和 1/32 加权组合,或者用(diff >> 4) - (diff >> 6)这种形式,等效于乘以 (1/16 - 1/64) = 3/64 ≈ 0.047。这样既保持了移位运算的速度,又细化了系数粒度。代价是多一两次移位和加减,但比浮点还是快得多。这个技巧在需要微调滤波强度时特别好用,值得记下来。

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

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

立即咨询