IIR数字滤波器C语言实现:从系数设计到MCU落地完整指南
2026/9/6 17:31:34 网站建设 项目流程

简介:围绕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陷波至少要一百多阶,计算量完全不是一个量级。

下面这张表是我选型时常用的对照思路:

对比项IIRFIR
所需阶数低,四到八阶常见高,几十上百阶常见
每样本计算量
线性相位一般做不到可以严格线性相位
稳定性极点位置不当会发散始终稳定
适用场景单片机、实时控制、对相位不敏感音频均衡、通信、需要波形保真

如果你的场景是“传感器数据去噪”“电力信号滤波”“控制环路里的低通”,那IIR是性价比之王。但如果是对波形相位敏感的多通道音频处理,宁可多花算力用FIR。工程选择从来不是哪个先进,而是哪个够用且成本低。

1.2 巴特沃斯、切比雪夫、椭圆,工程上到底选哪个

IIR按逼近方式分为巴特沃斯、切比雪夫I型、切比雪夫II型、椭圆等。它们本质区别是“把误差放在哪”:

  • 巴特沃斯:通带和阻带都单调,没有纹波,代价是过渡带相对最宽。
  • 切比雪夫I型:允许通带等纹波,阻带单调,过渡带比巴特沃斯陡。
  • 切比雪夫II型:通带单调,阻带等纹波,适合对阻带纹波有要求的场景。
  • 椭圆:通带和阻带都有纹波,但同样阶数下过渡带最陡。

我个人的工程习惯是:没有特殊要求一律先上巴特沃斯。原因只有一个——好设计、好排查。巴特沃斯的极点分布在以原点为圆心的圆上,公式规整,手算和用工具核对都很方便。传感器信号、电机电流、音频辅助通道,这些场景巴特沃斯低通基本都能拿下。只有当设计指标要求“阶数必须压到某个数以下”,才考虑切比雪夫或椭圆。

2. 设计参数怎么算:一个二阶低通从指标到系数的完整推导

2.1 设计一个IIR滤波器的完整步骤

常规流程可以拆成五步:

  1. 确定指标:采样率fs、截止频率fc、通带纹波、阻带衰减、过渡带宽度。
  2. 选原型滤波器类型:默认巴特沃斯。
  3. 确定阶数N:主要看阻带衰减和过渡带宽,查表或用公式。
  4. 模拟域到数字域变换:最常用双线性变换,先把模拟截止频率做预畸变。
  5. 把数字传递函数转成二阶节级联,得到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直接返回输入。这样在系统联调时能随时对比滤波前后信号,快速判断是硬件噪声还是软件滤波的问题。这个小功能只要一行代码的成本,但能省下大量定位问题的时间。

本文还有配套的精品资源,点击获取

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

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

立即咨询