十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

IIR数字滤波器C语言实现:从系数设计到MCU落地完整指南

IIR数字滤波器C语言实现:从系数设计到MCU落地完整指南 简介围绕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滤波器的完整步骤常规流程可以拆成五步确定指标采样率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)/(z1)整理后得到数字传递函数H(z) (z1)²·Ω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代码里抄结果符号没处理好滤波器输出直接飞了。校验方法也很简单看直流增益。把z1代入传递函数等于所有b系数之和除以所有a系数之和a01算出来应该是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, btypelow, outputsos)直接输出二阶节矩阵。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/(1s)二阶是H(s) s²/(s²√2s1)把s替换成Ωc·(z-1)/(z1)后整理就能得到系数。带通滤波器可以把低通原型通过频率变换映射过来也可以直接用工具生成。我在做设备故障诊断时就靠一个通用IIR处理框架挂了三条滤波器链低通管振动主频、带通管特定谐波、陷波管电源工频。三条链共用同一套iir_process函数只是初始化系数不同。5.2 级联更高阶和嵌入式低算力方案当单级二阶满足不了指标时直接增加二阶节数量比如四阶低通由两个二阶节串联六阶由三个二阶节串联。注意高阶滤波器的各个二阶节要按“先Q值高后Q值低”或“先极点在低频”的顺序排列可以优化数值范围内的小信号精度这一点工具生成系数时通常已经排好了手动调整时要格外小心。如果未来想进一步降低计算延迟或者实现并行多通道滤波可以考虑FPGA方案。用Verilog实现FIR或IIR时核心思想也是把系数乘加运算拆成并行流水线很多做FPGA滤波器的朋友都会提到分布式算法和查找表结构但这些通用经验基本都是C语言原型验证之后的事情。先把C语言版本的算法逻辑吃透换到任何硬件平台上都只是计算资源的重新映射。我个人在实际项目里最常用的流程是先用Python算好系数、仿真频响再把这些系数原封不动填进C语言结构体最后用正弦波扫频做板级验证。这套流程看着简单但能挡住九成以上的低级错误。最后再分享一个调试小技巧给滤波器加一个“旁路模式”就是在结构体里加一个bool bypass标志为true时iir_process直接返回输入。这样在系统联调时能随时对比滤波前后信号快速判断是硬件噪声还是软件滤波的问题。这个小功能只要一行代码的成本但能省下大量定位问题的时间。本文还有配套的精品资源点击获取
返回列表