简介:围绕IIR数字滤波器C语言实现整理的配套文档,面向数字信号处理学习者、嵌入式开发人员及课程设计学生,系统讲解采用间接法设计IIR滤波器的完整流程。内容以巴特沃斯低通滤波器为原型,先推导滤波器次数计算公式,再根据振幅特性分母多项式求解传递函数极点,并通过复数结构体在C语言中完成稳定极点筛选、乘法展开与系数计算;后续还介绍了双线性z变换原理及对应的C实现,并给出设计指标与程序执行结果,便于验证滤波器性能。文档通过数学公式与分步代码对照,帮助读者掌握从模拟原型到数字滤波器的转换方法,可直接应用于音频降噪、信号预处理、生物电信号分析等实际项目。资源为单个Word文档,共一个文件,压缩包约478KB,阅读和传递十分方便。目前已有663人学习下载,适合需要补强滤波器原理并动手实现的人群。文档不仅给出完整可运行的C语言代码片段,还特别说明稳定极点选取、复数乘法展开、次数向上取整等关键细节,避免初学者在推导中迷失方向,是数字信号处理课程设计或工程实现中一份实用的参考资料。 从嵌入式信号处理到音频效果器,IIR数字滤波器配合C语言实现,属于那种“看着不起眼,但工程里躲不开”的基础技能。我之前在单片机项目里用ADC采集传感器信号,一开始图省事用滑动平均,后来噪声频段和有用信号离得近,滑动平均压不住,换IIR之后阶数不高、计算量小,效果直接上了一个档次。这篇就把我从滤波器选型、系数设计到C语言落地的完整过程走一遍,适合正准备用C语言在MCU或PC上实现数字滤波的朋友,也适合已经把代码跑通但没想明白原理的同学。
1. IIR数字滤波器,为什么值得用C语言写一遍
1.1 IIR和FIR,一张表看清怎么选
数字滤波器按冲激响应分两大类:FIR和IIR。FIR没有反馈,结构天然稳定,还能做到严格线性相位;IIR有反馈,冲激响应无限长,但代价换来的是“同样滤波效果下阶数大大降低”。
举个例子,要压制一个带外噪声,过渡带宽度大致相等时,FIR可能要几十甚至上百阶,IIR往往四到八阶就够。阶数少意味着每次采样需要的乘加次数少,这在单片机、DSP这些算力有限的环境里非常关键。我最早做心电信号预处理时,50Hz工频干扰用IIR陷波器只要两个二阶节,换成FIR陷波至少要一百多阶,计算量完全不是一个量级。
下面这张表是我选型时常用的对照思路:
| 对比项 | IIR | FIR |
|---|---|---|
| 所需阶数 | 低,四到八阶常见 | 高,几十上百阶常见 |
| 每样本计算量 | 小 | 大 |
| 线性相位 | 一般做不到 | 可以严格线性相位 |
| 稳定性 | 极点位置不当会发散 | 始终稳定 |
| 适用场景 | 单片机、实时控制、对相位不敏感 | 音频均衡、通信、需要波形保真 |
如果你的场景是“传感器数据去噪”“电力信号滤波”“控制环路里的低通”,那IIR是性价比之王。但如果是对波形相位敏感的多通道音频处理,宁可多花算力用FIR。工程选择从来不是哪个先进,而是哪个够用且成本低。
1.2 巴特沃斯、切比雪夫、椭圆,工程上到底选哪个
IIR按逼近方式分为巴特沃斯、切比雪夫I型、切比雪夫II型、椭圆等。它们本质区别是“把误差放在哪”:
- 巴特沃斯:通带和阻带都单调,没有纹波,代价是过渡带相对最宽。
- 切比雪夫I型:允许通带等纹波,阻带单调,过渡带比巴特沃斯陡。
- 切比雪夫II型:通带单调,阻带等纹波,适合对阻带纹波有要求的场景。
- 椭圆:通带和阻带都有纹波,但同样阶数下过渡带最陡。
我个人的工程习惯是:没有特殊要求一律先上巴特沃斯。原因只有一个——好设计、好排查。巴特沃斯的极点分布在以原点为圆心的圆上,公式规整,手算和用工具核对都很方便。传感器信号、电机电流、音频辅助通道,这些场景巴特沃斯低通基本都能拿下。只有当设计指标要求“阶数必须压到某个数以下”,才考虑切比雪夫或椭圆。
2. 设计参数怎么算:一个二阶低通从指标到系数的完整推导
2.1 设计一个IIR滤波器的完整步骤
常规流程可以拆成五步:
- 确定指标:采样率fs、截止频率fc、通带纹波、阻带衰减、过渡带宽度。
- 选原型滤波器类型:默认巴特沃斯。
- 确定阶数N:主要看阻带衰减和过渡带宽,查表或用公式。
- 模拟域到数字域变换:最常用双线性变换,先把模拟截止频率做预畸变。
- 把数字传递函数转成二阶节级联,得到C代码里要用的b0、b1、b2、a1、a2系数。
这里我不推荐直接在数字域凑系数,那样做出来的滤波器频响可能完全不对。双线性变换虽然公式看起来麻烦,但它保证了模拟原型和数字滤波器之间频率响应的代数映射关系,是工程上最稳的路径。
2.2 手算示例:8kHz采样,1kHz截止
以采样率8000Hz、截止频率1000Hz的二阶巴特沃斯低通为例,整个计算过程可以完整走一遍。
双线性变换的关键是预畸变公式:
Ωc = tan(π × fc / fs)代入数值:
Ωc = tan(π × 1000 / 8000) = tan(π/8) ≈ 0.41421356二阶巴特沃斯的归一化模拟原型是:
H(s) = 1 / (s² + √2·s + 1)把模拟频率缩放代入,令s = Ωc·(z-1)/(z+1),整理后得到数字传递函数:
H(z) = (z+1)²·Ωc² / [ (1+√2Ωc+Ωc²)z² + (2Ωc²-2)z + (1-√2Ωc+Ωc²) ]把Ωc≈0.4142代进去,分子分母同时除以z²的系数,得到最终系数:
b0 = 0.0976 b1 = 0.1952 b2 = 0.0976 a1 = -0.9428 a2 = 0.3333注意这里a1、a2是分母多项式1 + a1·z^-1 + a2·z^-2的系数,其中a1是负数。很多人在这一步栽跟头,拿到Matlab或Python算出来的a向量就往C代码里抄,结果符号没处理好,滤波器输出直接飞了。
校验方法也很简单:看直流增益。把z=1代入传递函数,等于所有b系数之和除以所有a系数之和(a0=1),算出来应该是1。上面这组系数:
(0.0976 + 0.1952 + 0.0976) / (1 - 0.9428 + 0.3333) ≈ 0.3904 / 0.3905 ≈ 1对得上,说明系数没问题。
2.3 用现成工具快速出系数
手算能帮你建立直觉,但日常开发没必要每次都推一遍公式。我常用的几个手段:
- Python的scipy.signal库:
butter(N, Wn, btype='low', output='sos'),直接输出二阶节矩阵。 - Octave/Matlab:
[b,a] = butter(N, Wn, 'low')。 - 在线滤波器计算器:填截止频率和阶数,直接给系数。
用工具出系数时,务必把采样率、截止频率的单位换算清楚。Matlab里Wn是相对奈奎斯特频率的归一化值,Wn = fc / (fs/2),比如上面的例子就是butter(2, 0.25, 'low')。Python的Wn同样是归一化到奈奎斯特频率的。
3. C语言实现:从结构体设计到可运行代码
3.1 数据结构和状态变量怎么安排
IIR滤波器在C语言里最常见的表示是二阶节,也叫Biquad。单个二阶节的差分方程为:
y[n] = b0·x[n] + b1·x[n-1] + b2·x[n-2] - a1·y[n-1] - a2·y[n-2]如果用直接I型实现,需要保存两个历史输入和两个历史输出。实际操作中我更推荐直接II型转置结构,它的优点是每个二阶节只需要两个状态变量,而且对定点化的数值稳定性更好。
数据结构可以这样定义:
typedef struct { double b0, b1, b2; // 前馈系数 double a1, a2; // 反馈系数,即分母多项式 1 + a1*z^-1 + a2*z^-2 中的 a1、a2 double s1, s2; // 两个状态变量 } Biquad; typedef struct { int num_sections; // 二阶节数量 Biquad *sections; // 指向二阶节数组的指针 } IirFilter;用结构体把系数和状态封装在一起,初始化一个滤波器就是初始化一个结构体数组。处理多路信号时,每一路独立复制一份状态变量就行,互不干扰。这地方顺便把C语言结构体、指针、数组几个基本功全练到了。
3.2 核心滤波函数,直接能抄的那段
中心处理函数是这个滤波器的灵魂,逻辑非常紧凑:
double biquad_process(Biquad *f, double x) { double y = f->b0 * x + f->s1; f->s1 = f->b1 * x - f->a1 * y + f->s2; f->s2 = f->b2 * x - f->a2 * y; return y; } double iir_process(IirFilter *filter, double x) { double y = x; for (int i = 0; i < filter->num_sections; i++) { y = biquad_process(&filter->sections[i], y); } return y; }状态变量的更新顺序非常关键。必须先保存新的y值,再更新状态变量;如果顺序反了,滤波器就变成另一个传递函数了。这段代码里状态变量更新用的是“当前输入x”和“当前输出y”,一步算完,不需要额外保存x[n-1]、x[n-2]这些历史值,效率很高。
用第2节算出的二阶低通系数初始化:
Biquad bq = { .b0 = 0.0976, .b1 = 0.1952, .b2 = 0.0976, .a1 = -0.9428, // 注意这里是负值 .a2 = 0.3333, .s1 = 0.0, .s2 = 0.0 }; IirFilter filter = { .num_sections = 1, .sections = &bq };处理每来一个采样点,调用一次iir_process,返回滤波后的值即可。
3.3 进阶:定点化改造与精度控制
浮点运算在大多数MCU上没问题,但如果你用的是没有硬件浮点单元的单片机,或者要跑到极高采样率,就得考虑定点化。常见做法是Q15或Q31格式,把小数系数放大到整数范围,再通过移位还原。
以Q15为例,系数乘以32768后取整,中间累加用32位甚至64位变量防止溢出,最终结果再右移15位:
typedef struct { int32_t b0, b1, b2; int32_t a1, a2; // 乘以32768后的系数 int32_t s1, s2; // 定点状态变量 } BiquadQ15; int32_t biquad_process_q15(BiquadQ15 *f, int32_t x) { int64_t y = (int64_t)f->b0 * x + ((int64_t)f->s1 << 15); y >>= 15; int64_t s1 = (int64_t)f->b1 * x - (int64_t)f->a1 * y + ((int64_t)f->s2 << 15); f->s1 = (int32_t)(s1 >> 15); int64_t s2 = (int64_t)f->b2 * x - (int64_t)f->a2 * y; f->s2 = (int32_t)(s2 >> 15); return (int32_t)y; }定点化有两个容易踩的坑。第一是系数量化误差,原本a2是0.3333,量化成整数后会有误差,二阶节还好,高阶直接型会被放大到极点跑出单位圆。第二是中间结果溢出,所以乘法尽量提升到int64_t再算,能省很多排查时间。如果MCU是Cortex-M系列,编译器会生成单周期的乘法累加指令,计算开销比想象中小很多。
4. 实测调试:那些容易翻车的细节
4.1 符号反了输出就飞起:a系数的坑
关于a系数的符号约定,我必须单独拎出来说。Matlab和Python返回的a系数本身就带了符号,例如上面例子中a向量是[1, -0.9428, 0.3333]。写差分方程时,反馈项是“减去a1乘以前一个输出”,这个“减”和a1的负号叠加,实际代码里变成了加0.9428。
很多人用- a1 * y写代码时,以为a1应该存0.9428,结果差分方程直接变成减两个正数,滤波器变成一个低Q值的谐振器,输出一路震荡。
我的建议:结构体里存分母多项式原本的系数,代码里统一写成- f->a1 * y1 - f->a2 * y2,让符号显式出现在公式中,而不是在初始化时手动“修正”符号。每写一次滤波器,先用一个已知信号做直流测试,输入常数1,输出应该稳定收敛到1左右,这是最快速的冒烟测试。
4.2 高阶滤波器为什么必须拆成二阶节级联
如果你设计的滤波器阶数高于2阶,千万别把高阶系数直接塞进一个差分方程。原因是双线性变换后的高阶多项式系数数值范围非常悬殊,用浮点勉强能用,用定点数或有限精度计算时,极点位置会严重偏移,甚至移出单位圆导致系统发散。
正确的做法是把高阶传递函数分解成多个二阶节串联,每个二阶节单独处理自己的两个极点。这样每个节的系数误差只会影响局部极点,不会产生级联放大的灾难。我通常把四阶和六阶滤波器拆成两个或三个二阶节,中间用变量传递信号,iir_process里那个循环就是为了这个准备的。
4.3 初始瞬态和增益校验
刚上电时所有状态变量都是0,输入一个阶跃信号,滤波器输出会从0逐渐逼近目标值,这个过程叫初始瞬态。对应到工程场景,就是开机后头几十个采样点可能不准。我在传感器采集项目里一般让系统先跑一小段预热数据,或者开机后连续采集几十个点丢弃,再开始用滤波数据做控制,能省很多麻烦。
另一个必做检查是给滤波器输入一个已知频率的正弦波,比如1kHz采样率8kHz的场合,输入1kHz正弦,理论上幅度应该衰减到0.707倍左右。用这个办法可以确认系数设计和代码实现都没问题。如果幅值不对,优先查增益归一化;如果相位没问题但幅值整个放大了,多半是b系数缩放错了。
4.4 采样率、截止频率与稳定性的关系
双线性变换在接近奈奎斯特频率时,频率映射会发生明显压缩。如果截止频率设计得离采样率一半太近,比如fc大于0.4倍的fs,滤波器的实际频响会和预期有较大出入,而且系数中可能出现很大的b值和很小的分母值,数值敏感度剧增。
我自己的经验是把fc控制在fs的20%以内,这是双线性变换最舒服的区域。如果应用确实需要截止频率靠得很近,建议调整采样策略,先提高采样率再做滤波,或者考虑改用其他设计方法。
5. 还能怎么扩展:高通、带通、滤波器组
5.1 只改系数不换代码
IIR滤波器的C语言处理框架是通用的,低通、高通、带通、带阻的区别只在于系数不同。代码层面完全不用动,只需要替换结构体里的b0、b1、b2、a1、a2。
高通设计方法和低通类似,还是双线性变换,差别在于原型传递函数不同。巴特沃斯一阶高通原型是H(s) = s/(1+s),二阶是H(s) = s²/(s²+√2s+1),把s替换成Ωc·(z-1)/(z+1)后整理就能得到系数。带通滤波器可以把低通原型通过频率变换映射过来,也可以直接用工具生成。
我在做设备故障诊断时,就靠一个通用IIR处理框架,挂了三条滤波器链:低通管振动主频、带通管特定谐波、陷波管电源工频。三条链共用同一套iir_process函数,只是初始化系数不同。
5.2 级联更高阶和嵌入式低算力方案
当单级二阶满足不了指标时,直接增加二阶节数量,比如四阶低通由两个二阶节串联,六阶由三个二阶节串联。注意高阶滤波器的各个二阶节要按“先Q值高后Q值低”或“先极点在低频”的顺序排列,可以优化数值范围内的小信号精度,这一点工具生成系数时通常已经排好了,手动调整时要格外小心。
如果未来想进一步降低计算延迟或者实现并行多通道滤波,可以考虑FPGA方案。用Verilog实现FIR或IIR时,核心思想也是把系数乘加运算拆成并行流水线,很多做FPGA滤波器的朋友都会提到分布式算法和查找表结构,但这些通用经验基本都是C语言原型验证之后的事情。先把C语言版本的算法逻辑吃透,换到任何硬件平台上都只是计算资源的重新映射。
我个人在实际项目里最常用的流程是:先用Python算好系数、仿真频响,再把这些系数原封不动填进C语言结构体,最后用正弦波扫频做板级验证。这套流程看着简单,但能挡住九成以上的低级错误。
最后再分享一个调试小技巧:给滤波器加一个“旁路模式”,就是在结构体里加一个bool bypass标志,为true时iir_process直接返回输入。这样在系统联调时能随时对比滤波前后信号,快速判断是硬件噪声还是软件滤波的问题。这个小功能只要一行代码的成本,但能省下大量定位问题的时间。
本文还有配套的精品资源,点击获取