资讯动态

单片机一阶低通滤波位移法实现:原理、代码与实战优化

发布时间:2026/10/20 19:20:37 来源:尧图企业网站定制
1. 为什么偏偏选位移法来实现一阶低通滤波搞过单片机信号采集的人都有一个共同的痛传感器出来的数据永远在跳。你盯着串口助手里那一串上下翻飞的数字明明被测对象稳如泰山采样值却能给你演出心电图的效果。这时候老工程师通常会拍拍你肩膀说加个滤波吧。于是你翻开教材一阶低通滤波的公式赫然在目Y(n) α × X(n) (1 - α) × Y(n-1)公式简单到令人发指但真正落到单片机上问题就来了。α 这个系数通常是个小于 1 的小数比如 0.1、0.05 甚至 0.01。如果你用的是没有硬件浮点单元的 8 位或低端 32 位芯片每一次滤波都要做一次浮点乘法和一次浮点加法中断频率一高CPU 的算力就被这个小小的滤波吃掉了相当一部分。更别提有些场景下你压根不想引入浮点运算库代码体积和运行效率都要精打细算。位移法就是在这种背景下被大量一线工程师采用的一种实现技巧。它的核心思路非常朴素把 α 取成 1/2 的整数次幂然后用右移操作代替除法或浮点乘法。比如 α 1/16那么 α × X(n) 就等价于 X(n) 4。移位指令在几乎所有单片机上都是单周期或极少周期的操作速度快到可以忽略不计。这个内容适合谁看如果你正在做传感器数据采集、电机控制、电源管理、音频预处理这类需要实时滤波的嵌入式项目并且对代码执行效率有要求那这套方法你大概率用得上。哪怕你是刚接触单片机的初学者只要理解二进制和移位的基本概念也能跟着把整套逻辑跑通。我会从原理推导讲起把参数怎么选、代码怎么写、定点化怎么处理、实际调试中会遇到什么坑全部掰开揉碎说清楚。2. 一阶低通滤波的核心原理拆解2.1 从模拟RC电路到数字差分方程一阶低通滤波这个概念最早来自模拟电路里的 RC 低通网络。一个电阻串一个电容输入信号的高频成分被电容旁路到地输出就变得平滑。这个电路的传递函数是H(s) 1 / (1 RC·s)其中 RC 是时间常数 τ。到了数字域我们用离散的采样点来近似这个连续过程就得到了那个经典的差分方程。它的物理意义很直观新的输出值 一部分新的输入 剩下部分旧的输出。α 越大越信任新数据响应快但滤波效果弱α 越小越信任历史数据输出平滑但滞后严重。这里有个容易被忽略的点α 和截止频率的关系。在采样周期为 T 的系统中截止频率 fc 近似满足α ≈ 2π·fc·T / (1 2π·fc·T)反过来如果你先确定了想要的截止频率可以据此反推 α。比如采样率 1kHzT 1ms想要 10Hz 的截止频率那么 2π×10×0.001 ≈ 0.0628α ≈ 0.059取个近似值 1/16 0.0625 就非常合适。这也是为什么位移法里 α 常取 1/8、1/16、1/32 这些值——它们既接近常用的截止频率需求又能用移位干净利落地实现。2.2 位移法为什么能替代浮点运算关键就在于 α 的取值。当 α 1/2^n 时α × X(n) 就是 X(n) 右移 n 位。而 (1 - α) (2^n - 1)/2^n这个系数乘以 Y(n-1) 稍微麻烦一点但可以变形处理。把原公式展开Y(n) X(n)/2^n Y(n-1) - Y(n-1)/2^nY(n) Y(n-1) (X(n) - Y(n-1)) / 2^n这个变形后的公式是位移法实现的精髓。它把两次乘法变成了一次减法加一次移位。具体来说计算当前输入与上次输出的差值diff X(n) - Y(n-1)把差值右移 n 位diff n累加到上次输出上Y(n) Y(n-1) (diff n)整个过程只有减法、移位、加法三种操作全部是整数运算。在 8 位单片机上如果数据类型选得合适这几条指令加起来也就十几个机器周期比浮点运算快一到两个数量级。注意右移操作对负数的处理在不同编译器上行为可能不同。diff 完全可能是负数算术右移和逻辑右移的结果天差地别。这一点后面会专门讲。2.3 滤波效果与相位的取舍关系任何低通滤波都不是免费的午餐。α 越小输出越平滑但相位滞后越大。在控制系统中这个滞后可能直接导致环路不稳定。我见过不少人在电机电流环里把滤波加得太狠结果响应迟钝、震荡加剧回头还找不到原因。从经验上看对于一般的传感器去噪α 取 1/16 到 1/64 是比较舒服的区间。对于需要快速响应的场合比如过流保护α 不要小于 1/8。如果你实在拿不准可以先在 MATLAB 或者 Python 里把阶跃响应仿真一下看看上升时间和超调量能不能接受再决定移位位数。3. 位移法实现的完整技术细节3.1 数据类型选择与溢出防范位移法虽然简单但数据类型选错了照样翻车。假设你用 8 位无符号数存 ADC 采样值范围 0 到 255。如果 Y(n-1) 是 200X(n) 是 50那么 diff 50 - 200 -150。在 8 位无符号运算里这个 -150 会变成一个巨大的正数比如 106右移之后加回去输出直接飞掉。所以第一条铁律中间变量必须用有符号类型。推荐用 int16_t 或 int32_t具体取决于你的 ADC 位数和精度要求。对于 12 位 ADC0 到 4095int16_t 完全够用diff 的范围在 -4095 到 4095 之间不会溢出。对于 16 位 ADC就得用 int32_t 了。另外Y(n) 的存储类型也要和中间变量一致。很多人习惯把滤波结果存在 uint16_t 里然后在计算时临时转成 int这样容易在类型转换的边界上出问题。统一用有符号类型省心。3.2 移位位数的选择与截止频率对照移位位数 n 直接决定了 α 1/2^n也就决定了滤波强度。下面这张表是我在实际项目中总结的常用对照关系采样率假设为 1kHz移位位数 nα 值近似截止频率适用场景30.125约 20Hz快速响应轻度滤波40.0625约 10Hz一般传感器去噪50.03125约 5Hz温度、压力等慢变量60.015625约 2.5Hz高精度静态测量80.00390625约 0.6Hz极慢变化信号这张表不是死规矩但能给你一个快速定位的参考。实际选的时候先想清楚你的信号带宽是多少噪声主要分布在哪个频段然后反推需要的截止频率再对照选 n。3.3 定点化处理的精度损失分析位移法本质上是定点运算精度损失主要来自两个方面。一是右移操作直接丢弃低位相当于截断而非四舍五入。二是当 diff 很小时diff n 可能直接变成 0导致输出停止更新出现所谓的“死区”。举个例子n 8diff 100100 8 0。也就是说当输入和输出的差值小于 256 时滤波器就不再响应了。对于 12 位 ADC 来说256 相当于满量程的 6.25%这个死区相当可观。解决办法有两个。一是减小 n让死区变小。二是采用带舍入的移位比如Y(n) Y(n-1) ((diff (1 (n-1))) n)这样相当于加了半个步长的偏移能有效减小死区代价是多一次加法和一次移位。在算力允许的情况下我强烈建议加上这个舍入。4. 单片机实战从零搭建一个可用的滤波模块4.1 硬件平台与开发环境说明为了把内容讲得具体我这里以一个常见的 32 位 Cortex-M0 内核单片机为例主频 48MHz没有硬件浮点单元ADC 为 12 位、采样率 1kHz。开发环境用常见的嵌入式 C 工具链。这套配置在工业传感器、小家电控制板里非常普遍参考价值比较高。代码本身不依赖任何特定厂商的库纯标准 C移植到 8 位平台上只需要改一下数据类型即可。4.2 核心滤波函数的三种写法对比先看最直观的浮点写法作为对照float alpha 0.0625f; float y_prev 0.0f; float lowpass_float(float x) { float y alpha * x (1.0f - alpha) * y_prev; y_prev y; return y; }这段代码清晰但每次调用至少两次浮点乘法加一次浮点加法。在没有 FPU 的芯片上一次浮点乘法可能就要几十个周期。再看直接移位写法int32_t y_prev 0; #define SHIFT_N 4 int32_t lowpass_shift(int32_t x) { int32_t diff x - y_prev; y_prev y_prev (diff SHIFT_N); return y_prev; }简洁到极致但存在前面说的截断死区问题。最后是带舍入的改进版int32_t y_prev 0; #define SHIFT_N 4 #define ROUND_BIAS (1 (SHIFT_N - 1)) int32_t lowpass_shift_round(int32_t x) { int32_t diff x - y_prev; y_prev y_prev ((diff ROUND_BIAS) SHIFT_N); return y_prev; }这个版本多了一次加法和一个常量但死区明显减小输出更接近浮点版本的效果。我在实际项目里基本都用这个版本。4.3 初始化与运行时参数调整滤波器的初始值 y_prev 怎么设如果设成 0而实际信号在 2000 左右那么开机后输出会从 0 慢慢爬升到 2000这个爬升过程可能持续好几秒。对于某些应用来说这几秒的过渡是不可接受的。常见的做法是用第一次采样值直接初始化void lowpass_init(int32_t first_sample) { y_prev first_sample; }这样开机瞬间输出就等于输入之后才开始滤波。如果你希望更平滑一点也可以用前几次采样的平均值来初始化。运行时调整移位位数也是常见需求。比如系统在快速模式和慢速模式之间切换滤波强度也要跟着变。这时候可以把 SHIFT_N 改成变量但要注意移位位数是变量的情况下编译器可能无法优化成单条移位指令性能会打折扣。如果对性能敏感可以用 switch-case 把几种常用位数写死。4.4 实测数据与效果验证我在一个温度采集项目里实测过这套滤波。传感器是常见的数字温度芯片分辨率 0.0625 度采样率 1Hz。原始数据在 25.0 到 25.5 度之间跳动峰峰值约 0.5 度。用 n 4 的位移法滤波后输出稳定在 25.2 到 25.3 度之间峰峰值压缩到 0.1 度以内而且响应时间在可接受范围内。用示波器观察阶跃响应从 25 度跳到 30 度时滤波输出大约经过 15 个采样周期达到最终值的 90%。这个结果和理论计算基本吻合说明位移法在慢变量场景下完全够用。5. 常见问题与排查技巧实录5.1 输出震荡或飞掉的排查思路这是位移法最容易踩的坑根源几乎都出在数据类型上。排查顺序建议如下检查 diff 的计算是否用了有符号类型。如果 x 和 y_prev 都是无符号减法结果会回绕。检查右移操作符作用在什么类型上。对无符号数右移是逻辑移位对负数会补 0 而不是补 1结果完全错误。检查 y_prev 的更新是否在中断和主循环之间共享。如果中断里在写、主循环里在读没有保护的话可能读到半更新的值。我遇到过一次典型情况某开发者在 8 位机上用 uint8_t 存 y_prevdiff 也是 uint8_t结果输入从 100 降到 50 时diff 算出来是 206右移后加回去输出直接冲到 250 以上然后系统就失控了。改成 int16_t 之后问题立刻消失。5.2 滤波滞后过大的参数调整如果发现输出跟不上输入变化先确认是不是 n 选大了。把 n 减小 1 或 2滞后会明显改善。但减小 n 会削弱滤波效果需要在两者之间找平衡。另一个容易被忽视的因素是采样率。如果采样率远高于信号带宽那么即使 α 不小等效截止频率也会很低造成不必要的滞后。这时候可以考虑降低采样率或者改用滑动平均等其他滤波方式。还有一种情况是信号本身含有较大的直流偏置而噪声是高频的。这时候可以先减去偏置再做滤波或者用高通加低通的组合效果比单纯加大滤波强度好得多。5.3 多通道滤波的资源优化实际项目里往往不止一路信号要滤波。如果每路都独立写一套变量和函数代码会显得很臃肿。更好的做法是把状态封装成结构体typedef struct { int32_t y_prev; uint8_t shift_n; } lpf_t; int32_t lpf_update(lpf_t *f, int32_t x) { int32_t diff x - f-y_prev; int32_t bias 1 (f-shift_n - 1); f-y_prev (diff bias) f-shift_n; return f-y_prev; }这样每路信号只需要一个 lpf_t 实例代码复用率高也方便统一管理。代价是 shift_n 变成变量后移位不再是编译期常量性能略有下降。如果通道数不多且算力充裕这点损失可以接受。5.4 常见问题速查表现象可能原因解决方法输出飞掉或震荡数据类型无符号导致回绕改用 int16_t 或 int32_t输出长期停在某个值不动移位死区过大加舍入偏置或减小 n滤波效果不明显n 太小或采样率过高增大 n 或降低采样率响应太慢n 太大减小 n或改用变系数方案多通道互相干扰变量未隔离或中断冲突用结构体封装加临界区保护开机输出爬升慢初始值设为 0用首次采样值初始化提示在中断里调用滤波函数时尽量保证滤波变量只在该中断中被访问。如果主循环也要读滤波结果建议读完后立即拷贝到另一个变量避免数据竞争。6. 位移法的扩展玩法与进阶思路6.1 变步长滤波快慢自适应的实现固定 n 的位移法有个天然矛盾快和稳不可兼得。一个实用的改进是根据信号变化率动态调整 n。当 diff 很大时说明信号在快速变化此时用较小的 n 快速跟踪当 diff 很小时说明信号稳定用较大的 n 强力滤波。实现上可以设两个阈值int32_t lpf_adaptive(int32_t x) { int32_t diff x - y_prev; int32_t abs_diff (diff 0) ? diff : -diff; uint8_t n; if (abs_diff 500) { n 2; } else if (abs_diff 100) { n 4; } else { n 6; } int32_t bias 1 (n - 1); y_prev (diff bias) n; return y_prev; }这段代码在信号突变时响应快稳态时又足够平滑。代价是多了一次绝对值计算和几次比较但相比浮点运算还是轻量得多。我在一个手持设备项目里用过这个思路效果比固定系数好不少。6.2 级联位移滤波实现更陡的滚降一阶滤波的滚降是 -20dB/十倍频对某些应用来说不够。把两个一阶位移滤波器级联起来就能得到接近二阶的效果滚降变成 -40dB/十倍频。实现上就是连续调用两次滤波函数每级有自己的状态变量。需要注意的是级联后相位滞后会累加总滞后大约是单级的两倍。所以级联适合对相位不敏感的场景比如静态测量、显示平滑等。控制环路里要慎用。6.3 与滑动平均的对比与组合使用滑动平均是另一种常见的轻量滤波它把最近 N 个采样值求平均。相比位移法滑动平均对周期性噪声的抑制更好但需要存储 N 个历史值内存开销大而且每次都要做 N 次加法。两者可以组合先用滑动平均去掉明显的周期性干扰再用位移法做最后的平滑。或者反过来先用位移法快速压制高频噪声再用滑动平均进一步稳定输出。具体怎么组合取决于你的噪声特性和资源预算。我个人在大多数场景下更倾向位移法因为它内存占用极小代码简单调试直观。只有在噪声频谱比较特殊、位移法效果不理想时才会考虑引入滑动平均。6.4 在中断服务函数中的部署要点滤波函数经常放在 ADC 中断里调用。这时候有几个细节要注意。第一滤波函数本身要尽量短避免在中断里做复杂判断。第二如果用了自适应变步长阈值比较和分支会增加执行时间要评估中断频率和 CPU 负载。第三滤波变量如果也要在主循环里用记得加 volatile 或者用临界区保护。我一般的做法是中断里只做滤波和存结果主循环里读结果做后续处理。滤波变量用结构体封装中断和主循环各操作各的副本通过一个标志位或者队列来传递数据。这样既保证了实时性又避免了竞争。7. 一些实操心得与踩坑记录位移法看起来简单但真正在项目里用好还是有一些只可意会的东西。我把自己这些年踩过的坑和总结的经验列几条供你参考。第一条移位位数不要超过数据有效位数的一半。比如 12 位 ADC有效数据 12 位n 最好不要超过 6。超过之后死区会大到影响正常测量滤波输出会显得“迟钝”甚至“卡死”。这个界限不是绝对的但可以作为快速判断的依据。第二条调试时先把滤波关掉确认原始数据正常再开滤波。我见过有人花半天调滤波参数最后发现是传感器接线松动导致数据本身就在乱跳。滤波只能处理噪声不能修复错误的数据源。第三条用串口或者调试器把滤波前后的数据同时输出画成曲线对比。光看数字很难判断滤波效果画成波形一目了然。我通常会用简单的上位机工具把两组数据实时绘制出来调参效率高很多。第四条不同信号不要共用一套滤波参数。温度、压力、电流的变化速率差异很大用同一个 n 必然有一路效果不好。多花几行代码给每路独立配置后期维护会轻松很多。第五条记得处理滤波器的复位。系统重启、传感器重新初始化、量程切换时滤波器的历史状态应该清零或者用新数据重新初始化否则会有一段莫名其妙的过渡过程。最后再分享一个小技巧如果你不确定 n 取多少合适可以先用浮点版本在 PC 上仿真把不同 α 对应的阶跃响应和噪声抑制效果都跑一遍找到满意的 α 之后再换算成最接近的 1/2^n。这样比在单片机上反复烧录试参数快得多。换算的时候记住α 1/2^nn 取整数所以实际 α 和仿真值会有偏差选最接近的那个就行。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑