news 2026/8/26 13:10:28

数字滤波器实现指南:从FIR/IIR选型到嵌入式定点优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数字滤波器实现指南:从FIR/IIR选型到嵌入式定点优化

Filter这个词,在不同软件里代表完全不同的东西。你在同花顺里写filter,可能是在做条件选股;你在Fiddler里找filter,大概率是在配抓包过滤规则;但如果你在信号处理团队里说Filter Realizations,那我们聊的就不是“筛选数据”,而是怎么把一个数字滤波器从差分方程变成能在MCU上实时跑的代码。这篇内容围绕滤波器实现,把设计选型、结构选择、代码落地和排查经验完整过一遍,适合正在做音频处理、传感器调理、电机控制或者通信基带的工程师,也适合刚接触DSP、想把课本理论真正跑起来的学生。别急着抄代码,先想清楚一个问题:你要的到底是哪种滤波实现。

1. 滤波器实现:先想清楚选型再动手

1.1 FIR还是IIR:没有万能的滤波器

滤波实现的第一步不是打开代码编辑器,而是选类型。数字滤波器从大方向上分成FIR(有限冲激响应)和IIR(无限冲激响应)两类,这两兄弟的脾气完全不同。

FIR的输出只和当前及过去的输入有关,没有反馈回路。你可以把它理解成“把最近N个采样点做加权平均”,所以结构上天然稳定,还能做成严格线性相位。线性相位意味着不同频率分量通过滤波器后的延迟一致,波形形状不会变形,这在心电信号、音频分频、雷达信号处理这些场景里是硬需求。

IIR就不一样了,输出会反馈回输入端,相当于“今天的输出不仅看今天的输入,还看昨天自己算出来的结果”。因为反馈的存在,相同指标下IIR需要的阶数往往只有FIR的零头,CPU开销小一个数量级。代价是相位非线性,以及高阶时对系数量化极其敏感,稍不留神极点就跑到单位圆外,滤波器直接发散。

我做音频EQ和传感器低通时,默认用IIR双二阶节级联;做心电R波检测和高品质音频分频时,老老实实上FIR线性相位。选型没有绝对对错,关键看你的应用对相位敏不敏感、算力有多少预算。

维度FIRIIR
相位特性可严格线性相位非线性相位,群延迟畸变
稳定性无反馈,天然稳定极点必须在单位圆内
实现效率过渡带越窄阶数越高相同指标阶数通常低很多
存储开销需要N级延迟线状态变量少
典型场景高保真音频、心电、通信语音降噪、均衡器、控制环路

1.2 频率规划:关键参数怎么定

很多人拿到需求就写代码,结果写出来的滤波器和实际需要完全不搭。正确做法是先列指标,再谈实现。

需要定的参数至少有这五个:采样率fs、通带截止频率fp、阻带截止频率fst、通带纹波Rp、阻带衰减As。举个例子,我在一个音频预处理项目里要滤掉5kHz以上的高频噪声,最后定的指标是这样的:

参数指标值
采样率fs48kHz
通带截止fp4kHz
阻带截止fst6kHz
通带纹波0.5dB以内
阻带衰减60dB以上

这里最关键的就是过渡带,也就是fp到fst这中间的2kHz。过渡带越窄,滤波器的阶数就越高,实时计算的开销就越大。很多新手为了让滤波效果“更干净”,把过渡带设得特别窄,结果FIR阶数飙到几百阶,MCU根本跑不动。

还有个容易踩的坑是归一化频率。在scipy这些工具里,如果你用butter(4, 5000/48000)这种写法,这个数是相对Nyquist点归一化的结果,不是绝对的5kHz。后面我会详细讲怎么避免。

1.3 阶数估算:先算账再动手

指标一旦明确,阶数基本就定了,这个账要在动手前算清楚。

FIR阶数可以用一个经验公式粗估:N约等于(As-8)/(2.285×Δω)。其中As是阻带衰减,单位dB,Δω是过渡带归一化宽度,单位是rad/sample。拿上面的例子算一下,Δω=2π×(6000-4000)/48000=π/12≈0.2618,代入公式N≈(60-8)/(2.285×0.2618)≈87。所以FIR大概要取到89阶到91阶,才能同时满足阻带衰减60dB和过渡带2kHz的要求。

IIR就不需要这么夸张,4阶Butterworth通常就能达到阻带斜率要求,8阶椭圆甚至能压到更窄的过渡带。这也是为什么IIR在嵌入式里那么受欢迎——省算力、省内存。

所以说,Filter Realizations的第一步不是“怎么写”,而是“写什么”。先算清楚阶数和资源开销,后面才不会被实时性问题卡住。

2. 从差分方程到可运行代码:三种经典实现结构

2.1 直接I型、直接II型到级联二阶节

滤波器的数学本质是差分方程,但同样的方程换成不同实现结构,数值行为和效率差别巨大。

IIR的差分方程长这样:y[n]=b0x[n]+b1x[n-1]+...+bMx[n-M]-a1y[n-1]-...-aNy[n-N]。最直观的写法是直接I型,把所有的b系数和a系数一次性全部算掉。这种结构很简单,但有个致命弱点:对系数量化非常敏感。如果滤波器阶数一高,系数稍微量化一下,极点位置就飘了,最终导致输出发散。

所以高阶IIR在实践中几乎不用一次性的大滤波器,而是拆成一串二阶节(biquad)串联,每个二阶节只处理两个极点。这样每个节点的系数范围小,量化误差影响也被限制在局部。这也是为什么scipy的butter函数默认返回SOS格式,而不是传统的b,a数组。

二阶节内部还分直接I型、直接II型和转置直接II型。直接II型通过引入中间状态变量,可以减少延迟单元的个数,但最常用的是转置直接II型。它的好处是每个采样点处理时,状态变量更新和乘加运算顺序很顺,对缓存和流水线更友好,在Cortex-M这类处理器上能省不少周期。

2.2 浮点还是定点:MCU上的两个世界

在PC上用double跑滤波器,你基本不需要关心数值细节。但MCU环境不一样,很多芯片没有FPU,float运算全靠软件模拟,慢到心碎;有些即使有FPU,为了省电省内存,也倾向用定点。

定点实现的核心是Q格式。比如Q15,用16位有符号数表示[-1,1)范围内的数,定标就是S=2^15。两个Q15数相乘,乘积是Q30,需要左移15位回到Q15。累加的时候要用32位累加器,否则中途溢出就直接炸了。

下面是我在STM32上写过的一个16阶FIR定点实现:

#define N 16 static int16_t coeff[N] = { /* Q15格式系数 */ }; static int16_t delay_line[N]; int16_t fir_q15(int16_t input) { int32_t acc = 0; for (int i = N - 1; i > 0; i--) { delay_line[i] = delay_line[i - 1]; } delay_line[0] = input; for (int i = 0; i < N; i++) { acc += (int32_t)coeff[i] * delay_line[i]; } acc = acc >> 15; if (acc > 32767) acc = 32767; if (acc < -32768) acc = -32768; return (int16_t)acc; }

第15行和第16行是饱和处理,不是可选项。如果直接把32位结果截断成16位,信号一旦稍微大点就会产生爆音,而且这种失真非常难听。另外一个经验:移位时最好做四舍五入而不是直接截断,简单点就是(acc + 0x4000) >> 15,这样量化噪声不会积累。

2.3 实时逐样本处理与块处理

实时滤波器的数据流方式,很多人一开始没搞明白。在MCU上,ADC每采一个样本就触发一次中断,你在中断里调用一次滤波函数,这是逐样本处理。它延迟最低,但要求中断处理函数足够短,不然会拖垮整个系统。

另一种方式是块处理。ADC通过DMA把一段数据搬进内存,攒够一帧后触发中断,你在中断里处理一整帧。音频场景基本都是这么干的,I2S+DMA传128个样本,处理完再DMA送出去。块处理的好处是吞吐高,缺点是会引入一帧的延迟。48kHz采样率下,128个样本就是2.67ms,人耳对延迟的感知阈值大概在10ms以上,所以128或256样本的帧长完全可以用。

逐样本处理的代码大概是这个逻辑:

void ADC_IRQHandler(void) { int16_t cur = (int16_t)ADC1->DR; int16_t out = biquad_process(&lp_biquad, cur); DAC1->DHR12R1 = out; }

这里有个关键提醒:中断处理函数里千万别放打印日志、显示刷新这类操作,一次串口输出可能比滤波本身慢几百倍,采样间隔一旦抖动,滤波器特性就会劣化。

3. 实操:用Python设计一个低通滤波器,再移植到C语言

3.1 Python设计系数:用SOS而不是b,a

我在PC上做滤波器原型,基本都用SciPy。这里设计一个48kHz采样率下截止频率5kHz的4阶Butterworth低通:

import numpy as np from scipy.signal import butter, sosfilt, firwin fs = 48000 fc = 5000 order = 4 sos = butter(order, fc, btype="low", fs=fs, output="sos") print(sos) # 如果换成FIR,89阶Hamming窗 fir_coeff = firwin(89, fc, fs=fs, window="hamming")

注意output="sos"这个参数。很多人习惯写b, a = butter(...),然后拿b,a去设计。这在低阶时没问题,但一高阶就容易出数值问题。SOS格式是二阶节级联的矩阵,在浮点和定点实现里都更稳健。

滤波验证也很简单:

t = np.arange(0, 0.1, 1 / fs) sig = np.sin(2 * np.pi * 1000 * t) + 0.5 * np.sin(2 * np.pi * 9000 * t) filtered = sosfilt(sos, sig)

这里用的是sosfilt,对应因果实时滤波。离线数据分析时,如果允许非因果,可以用sosfiltfilt做零相位滤波,效果更好。但实时系统里只能用因果滤波,很多人把离线脚本的filtfilt直接搬上板子,结果产生预振铃和额外延迟,这就是对因果性和实时性没分清。

3.2 验证频响:不止看一条曲线

设计完不能直接相信系数,必须验证频响。我通常用sosfreqz画幅度响应,注意要对数频率轴,这样低频段看得更清楚:

from scipy.signal import sosfreqz import matplotlib.pyplot as plt w, h = sosfreqz(sos, worN=2048, fs=fs) plt.semilogx(w, 20 * np.log10(abs(h))) plt.axvline(5000, color="r", linestyle="--", label="5kHz") plt.axhline(-60, color="gray", linestyle=":", label="-60dB") plt.legend() plt.show()

确认3dB点在5kHz附近,6kHz达到-60dB。除此之外,我强烈建议用合成信号做时域验证:把1kHz和9kHz的正弦波叠在一起,滤波后做FFT,看9kHz分量是否被抑制。肉眼观察波形加FFT频谱,比只看幅频理论曲线可靠得多。因为我曾经遇到过理论频响正常,但实际信号经过后底噪异常的情况,就是靠时域测试抓出来的。

3.3 把系数搬进C:双二阶级联实现

Python里算出的SOS系数,在C里最常用转置直接II型双二阶节实现。结构体就三块:系数、状态变量、处理函数。

typedef struct { float b0, b1, b2; float a1, a2; // 注意:这里存的是差分方程中移项后的“负系数” float z1, z2; } biquad_t; float biquad_process(biquad_t *f, float x) { float y = f->b0 * x + f->z1; f->z1 = f->b1 * x - f->a1 * y + f->z2; f->z2 = f->b2 * x - f->a2 * y; return y; }

系数怎么填?拿Python的SOS输出逐行转。每行是[b0,b1,b2,a0,a1,a2],C结构体里要归一化除以a0,并且注意a1和a2字段要取负号。手动转换很烦,我通常直接写个小脚本:

for section in sos: b0, b1, b2, a0, a1, a2 = section print(f"{{ {b0/a0:.10f}f, {b1/a0:.10f}f, {b2/a0:.10f}f, " f"{-a1/a0:.10f}f, {-a2/a0:.10f}f }},")

4阶Butterworth会有两个二阶节,4个系数对,放在一个数组里,每个输入样本依次过两节。注意每个二阶节都有自己的状态变量,初始化必须清零,否则输出会有很大的瞬态。

如果你用的是ST、NXP这些带CMSIS-DSP库的平台,不建议自己手写这个循环。直接调arm_biquad_cascade_df1_f32,底层已经针对ARM内核做了优化,指令周期比普通写法少很多,而且经过大量验证,比自己写的函数稳。手写版适合学习、无库平台,或者你需要完全控制实现细节的时候。

4. 工程落地中我遇到的坑与排查实录

4.1 滤波后信号相位不对:群延迟和预振铃

有次做心电信号预处理,滤波后R波的峰值位置总是往后偏,导致心率计算有偏差。一开始我还以为是算法问题,查到最后发现是滤波器群延迟在作怪。

IIR滤波器在截止频率附近的群延迟会突然增大,不同频率分量经过滤波器的延迟不一样,波形自然就变形了。R波含有一定的高频分量,被延迟得比低频基线更多,位置就看歪了。

排查办法很简单:画群延迟曲线。如果应用对波形形状、事件时刻敏感,IIR的相位特性会害死人。我当时直接换成线性相位FIR,问题立刻消失,代价是阶数高了几倍,但MCU算力尚可接受。如果你必须用IIR,至少要做延迟补偿,或者在系统级把这一部分延迟算进总预算里。

我后来在电机电流环里也栽过一次。为了抑制PWM噪声,我加了一阶低通滤波,截止频率设在500Hz。结果低速运行正常,高速工况下系统开始啸叫。原因就是滤波器的相位滞后吃掉了控制环路的相位裕度。把截止频率往上调到2kHz,噪声抑制效果虽然弱了一点,但系统稳定性完全恢复。这提醒我:滤波器不只是“信号调理工具”,在闭环系统里它就是一个会引入延迟的环节。

4.2 定点实现出现爆音和底噪

定点滤波器跑起来后,最典型的问题就是输出偶尔会冒出一个大尖峰,或者安静状态下底噪明显偏大。这两个问题我都遇到过。

大尖峰的原因基本是累加器溢出。Q15格式下,输入信号接近满幅,系数接近满幅,16阶累加器在32位范围内不会溢出,但如果你用的是16位累加器,或者把累加结果直接截断成16位,一旦信号幅度稍大就会削波。配合饱和处理能明显缓解,但最好的办法还是保证全程32位累加。

底噪大的原因通常是量化噪声积累。定点乘法有舍入误差,如果不做四舍五入直接截断,误差会累积起来。我在C代码里常用的处理是:acc = (acc + 0x4000) >> 15;,相当于在右移前加半个LSB,做四舍五入。另外,你可以在Python里把系数量化成Q15,再把乘法和累加过程模拟成整数运算,对比浮点输出。这个操作十几行代码就能写完,但能帮你在上板之前发现80%的定点问题。

4.3 切换滤波器系数瞬间发生爆音

设备运行中动态调整滤波参数,比如EQ切换、带宽切换,切换瞬间往往“啪”的一声。这个爆音不是滤波算法算错了,而是滤波器内部状态变量在新参数下不匹配。

假设滤波器工作在旧系数下,延迟线和状态变量已经稳定在某个值。这时候你把系数换成新值,但状态变量还停留在旧状态,输出就会产生一个很大的阶跃。解决办法很简单:切换前把每个双二阶节的z1z2全部清零,再做平滑过渡;或者更稳一点,用双缓存系数,先算好所有新系数,再把运行指针一次性切换过去。

如果你做的是音量渐变或EQ渐变,还可以让新旧系数按比例交叉淡入淡出,每处理一个采样点都重新计算一次当前系数,这样完全听不到切换噪声明。代价是CPU开销高一些,但对音频产品来说非常值。

4.4 截止频率和设计值对不上

有朋友拿着设计好的滤波器来找我,说明明设的是5kHz低通,实测4.2kHz就开始衰减。我第一个问题就是:你的实际采样率确定是48kHz吗?

很多板子上ADC的时钟配置有分频误差,实际采样率可能只有44.1kHz,甚至采样率在抖动。滤波器性能对采样率极其敏感,采样率偏了,截止频率自然就偏了。排查方法是用示波器或逻辑分析仪测AD转换完成中断的实际频率,不要只看寄存器配置。

另一个隐藏很深的坑是归一化频率用错。比如butter(4, 5000/48000, output='sos'),这里的5000/48000不是5kHz,而是相对Nyquist频率归一化的结果。当fs=48000时,Nyquist是24000Hz,这个参数对应实际频率是5000/48000×24000=2500Hz。正确写法是butter(4, 5000, fs=48000),或者写成butter(4, 5000/(fs/2))。每次设计时我都直接用带fs=参数的写法,避免自己换算出错。

4.5 滤波实时性不够:算力瓶颈

滤波器算不过来,直接表现就是中断处理时间超标,采样间隔开始抖动,输出噪声变大,更严重时系统直接卡死。排查的时候要区分:是滤波器本身太重,还是中断里塞了太多其他任务。

滤波器本体的开销可以用一个简单公式估算:单样本处理周期数≈阶数×每阶乘加周期数。Cortex-M4不带FPU时,一个浮点双二阶节大概要几百个周期,但如果你用的是CMSIS-DSP库,这个数字能压到几十个周期。如果还是不够,把逐样本处理改成块处理,用DMA搬数据,就能把中断频率降下来。

压测方法我常用的是:在高优先级中断里翻转一个GPIO,用示波器测高电平时间,就能看到滤波函数实际占用了多少周期。别靠猜,实测最靠谱。

现象可能原因排查方法
输出相位偏、波形变形IIR相位非线性看群延迟曲线,换FIR或做延迟补偿
定点输出大尖峰累加器溢出用32位累加器,加饱和处理
低噪明显偏大量化误差累积右移前做四舍五入,Python模拟定点
切换参数瞬间爆音状态变量未清零切换前清零状态或做系数渐变
截止频率不对实际采样率偏差或归一化错误测真实采样率,用fs=写法
中断超时/卡顿CPU算力不足换库函数、改块处理、降低阶数

5. 工具链与调试验证:能少踩一半坑

5.1 常用工具链:从SciPy到CMSIS-DSP

做Filter Realizations,我常用的工具链组合是Python+SciPy做原型设计,CMSIS-DSP做端侧实现,必要时再用MATLAB对照验证。

SciPy免费、脚本化、可复现,非常适合回放数据和批量实验。MATLAB的Filter Designer是图形化界面,适合对滤波器指标做交互式调整,看零点极点图、群延迟曲线都方便,但商业授权很贵。Octave是免费的MATLAB替代品,基本功能齐全。CMSIS-DSP是ARM官方DSP库,里面有FIR、IIR、Biquad的各种定点和浮点实现,我只要在STM32/NXP平台上做音频和信号处理,基本都基于它来写。

还有个调试利器是Audacity这类音频编辑软件。你可以生成扫频信号丢进板子,录音回PC,直接看频响曲线。实测频响和设计频响一对比,问题立刻暴露。

5.2 离线到在线的正确验证姿势

很多问题其实可以在上板前就发现,关键是建立一套“离线验证→在线确认”的流程。

第一步,在Python里用sosfilt处理一段真实采集的原始数据,确认滤波效果符合预期。第二步,把同一段原始数据经过你写的C代码滤波,把输出导出回PC,和Python输出做比对。浮点实现误差应该在1e-5量级,定点实现误差也应控制在1%以内。第三步,再上板跑,用DAC输出或用串口导出滤波后数据回PC分析。

我每次做新滤波器都会走这套流程,虽然前期多花一点时间,但能避免直接在板子上盲调,效率反而高得多。尤其是定点实现,Python模拟到位了,板子上基本不会出大问题。

我自己在实际操作中的体会是:滤波器实现翻车的地方,很少是“数学公式看不懂”,而是设计、实现、验证三个环节脱节。设计时没算清楚阶数和延迟预算,实现时没注意结构选型和数值细节,验证时只看理论曲线不看真实信号,一环扣一环,问题就来了。每做一版滤波器,我都会先写清指标、算好开销、把定点量化在Python里模拟一遍再部署。这套习惯帮我在电机控制、音频前端、传感器调理上省了大量调试时间。最后一个建议是:尽量用现成库,比如CMSIS-DSP,除非你很清楚为什么要自己写。滤波实现这件事,稳定、可控、可复现,比什么都重要。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/26 13:09:09

从零构建支持中断恢复的AI Agent:状态机与记忆系统设计实践

1. 项目概述&#xff1a;为什么我们需要一个能“被打断”的AI助手&#xff1f;最近在折腾一个挺有意思的项目&#xff0c;核心目标就写在标题里了&#xff1a;从零开始搭建一个能理解“人机协作”与“中断恢复”的AI Agent。这听起来可能有点抽象&#xff0c;但如果你用过市面上…

作者头像 李华
网站建设 2026/8/26 13:08:45

拆解IDM与鼓打贝斯:从Breakbeat切片到Reese低音的制作全流程

如果你在播放器里看到《Sadesper Record - Externalization 1 (1999)》这个标题&#xff0c;大概率会直接划过去。它没有流行旋律&#xff0c;没有大厂封面&#xff0c;也没有一首能让人立刻哼出来的副歌。但把视角从“乐评”切到“技术分析”之后&#xff0c;它反而变成了一份…

作者头像 李华
网站建设 2026/8/26 13:08:03

删除不等于清除:用PrivaZer彻底清理C盘与隐私痕迹

很多人清理电脑时都有一种错觉&#xff1a;文件放进回收站再清空&#xff0c;就等于彻底删掉了。实际上&#xff0c;Windows 的“删除”只是把文件在文件系统里的条目标记为“可覆盖”&#xff0c;数据本身仍然留在硬盘上。只要没有被新数据覆盖&#xff0c;任何数据恢复工具都…

作者头像 李华
网站建设 2026/8/26 13:04:56

数学建模竞赛实战:基于四阶段法与MATLAB的交通可达率优化

1. 从“未来新城”到“可达率”&#xff1a;一次数学建模竞赛的实战复盘 五一数学建模竞赛的B题&#xff0c;题目一出来就让我眼前一亮——“未来新城背景下的交通需求规划与可达率问题”。这题目&#xff0c;它不只是在考你数学&#xff0c;更像是在考你如何用数学的骨架&…

作者头像 李华
网站建设 2026/8/26 13:03:39

MATLAB统计学预测:四类基础方法实战指南

1. 项目概述&#xff1a;用MATLAB做统计学预测&#xff0c;到底在解决什么问题&#xff1f; “MATLAB简单统计学预测方法分析”这个标题看起来平实&#xff0c;但背后藏着大量新手和工程人员的真实痛点。我带过十几届本科生课程设计、帮过二十多个工业客户做数据预研&#xff0…

作者头像 李华