资讯动态

FPGA实战:IIR数字滤波器设计全流程解析

发布时间:2026/10/3 4:21:34 来源:尧图企业网站定制
基于FPGA做IIR数字滤波器这事儿我前前后后折腾了小两个月才算是真正跑通。以前用C语言在单片机上调滤波器总觉得不过瘾数据是一帧一帧进来的CPU一忙就丢采样点算出来的结果心里没底。后来转到FPGA上把滤波逻辑全做成硬件流水线每个时钟周期都能进一个新样本那种“工业级稳定”的体验确实很难回头。这篇文章我把整个设计流程完整拆开讲从IIR最基本的差分方程到系数量化、Verilog实现、ModelSim仿真再到最后上板实测的波形对比每一步都用我踩过的坑来标注。不管你是刚学FPGA的学生还是在做信号处理相关的工程师照着这套思路走一遍基本都能把IIR滤波器从零到一落地实现。1. 设计思路拆解为什么IIR适合上FPGA以及整体架构怎么定IIR数字滤波器和FIR最大的区别就是有反馈。FIR是纯粹的前馈结构输入进来乘累加完就出结果IIR除了当前输入还要把之前输出结果乘上系数再反馈回来。这个反馈就带来了两个核心问题一个是稳定性极点必须在单位圆内出一点岔子输出就发散另一个是实现结构上的依赖当前输出依赖上一个输出天然是串行的。很多人一看到“串行依赖”就头大觉得FPGA的并行优势用不上。但其实IIR拐个弯用Direct Form II结构做反馈支路和前馈支路可以看成两个独立的乘累加网络只要把时序安排好照样能把吞吐率做到一拍一个样本。这也是我在项目里最终选择Direct Form II Transposed结构的主要原因。整体架构我分成了四个模块时钟管理模块给主逻辑提供稳定的工作时钟关键是把输入采样时钟和FPGA主时钟处理好。我用的是Xilinx的MMCM/PLL IP核输入50MHz内部统一跑100MHz。系数存储模块IIR阶数不高系数也就几组我直接放在ROM或者寄存器数组里用参数化方式写死在里面方便综合时优化。滤波运算模块核心的乘累加逻辑包括输入信号寄存、乘系数、累加、截位处理是整个设计里最需要抠时序的地方。输出截位与饱和模块滤波器内部为了保精度位宽肯定要比输入大不少。输出之前要重新映射到需要的位宽这里处理不好就会听到明显的爆音。这么拆的另外一个考虑是测试方便。每个模块都能单独写testbench验证比一上来就整个顶层一起调要省心得多。2. 系数设计与量化MATLAB里做浮点到了FPGA就得向定点低头滤波器设计的第一步永远是定指标。我这次要实现的是一个低通滤波器采样率48kHz通带截止频率5kHz阻带起始频率8kHz通带波纹小于1dB阻带衰减大于40dB。用MATLAB的FDATool选的椭圆滤波器最后得到的是6阶IIR。6阶拆成3个二阶节SOS级联这是工程里最扎实的做法。为什么不用一个6阶直接实现因为高阶IIR对系数误差极其敏感稍微量化偏一点极点就可能跑出单位圆滤波器直接不稳定。级联二阶节相当于把风险分摊到三段每段只负责自己的两个极点系数变化对极点位置的影响小得多。拿到浮点系数之后就到量化环节了这里是最容易踩坑的地方。IIR的反馈系数对量化误差特别敏感我一开始图省事直接把系数四舍五入保留4位小数结果仿真时波形直接发散了。后来改成标准做法先用MATLAB的realize还是dfilt对象查看系数动态范围再统一用定点格式表示。我选的是Q1.15格式也就是符号1位整数1位小数14位用十进制数乘以32768再四舍五入。拿第一组二阶节的系数举例量化后a1约为-1.3587量化值-44581a2约为0.6856量化值22468。这些数值要用十六进制写进Verilog的parameter里。有一点很重要浮点仿真完全正常不代表定点没问题。我在MATLAB里用quantize把滤波器转成定点模型再仿真了一遍对比频响曲线确认阻带衰减从42dB降到了39dB左右还在要求范围之内才放心去写RTL。这一步多花十分钟能省下后面调板子的好几个晚上。2.1 通带增益归一化不解决这个问题输出幅度会漂还有一个细节是增益归一化。IIR滤波器在通带内往往有直流增益不是1的问题。6阶椭圆滤波器级联起来直流增益可能是几个二阶节增益的乘积数值能到2倍甚至更高。我处理的方式是在MATLAB里算出每个SOS在DC处的增益把分子系数整体除以这个增益这样一来每个级联节的直流增益就归一化为1了。归零之后再做的定点量化。如果不做这步滤波后的信号幅度会明显偏大后续的截位饱和策略都得跟着改非常麻烦。3. Verilog实现细节位宽、流水线、饱和处理一个都不能少确定好了结构和系数进入写代码阶段。我的输入是12位有符号数滤波器内部乘累加用的位宽是16位系数乘12位输入乘积是28位。累加器按28位走级联节之间通过截位保持16位数据宽度避免位宽在每一级都膨胀。3.1 Direct Form II Transposed结构的乘累加逻辑Transposed结构的好处是把延迟单元放在乘法器后面这样每个计算周期内数据路径更短时序更容易收敛。每个二阶节的运算逻辑是下面这样的// 以第一个二阶节为例系数已在参数中量化 // b0 0.1143, b1 0.0121, b2 0.1143 // a1 -1.3587, a2 0.6856 (使用正数表示符号在运算中处理) // 采用Q1.15格式量化乘以32768后四舍五入 localparam signed [15:0] b0 16sd3746; // 0.1143 * 32768 localparam signed [15:0] b1 16sd397; // 0.0121 * 32768 localparam signed [15:0] b2 16sd3746; // 0.1143 * 32768 localparam signed [15:0] a1 16sd44581; // 1.3587 * 32768 localparam signed [15:0] a2 16sd22468; // 0.6856 * 32768 wire signed [27:0] mult_b0, mult_b1, mult_b2; wire signed [27:0] mult_a1, mult_a2; assign mult_b0 x_in * b0; assign mult_b1 delay_1 * b1; assign mult_b2 delay_2 * b2; assign mult_a1 delay_1 * a1; assign mult_a2 delay_2 * a2; wire signed [27:0] acc_1 mult_b0 mult_b1 mult_b2 mult_a1 mult_a2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin delay_1 16sd0; delay_2 16sd0; y_reg 16sd0; end else begin delay_1 y_reg; delay_2 delay_1; y_reg saturate_16bit(acc_1 14); end end注意最后那行acc_1是28位包含了Q1.15系数和12位定点输入相乘后的小数位移。统一右移14位舍去乘法引入的小数部分然后截位到16位。这一点在级联结构里尤其关键因为下一级的输入必须是同一位宽的有符号数。3.2 截位策略直接截断不可取饱和处理要跟上截位处理我吃了不少亏。第一次实现时直接取高16位没有做饱和。问题出在信号瞬时输入过大时28位累加结果的高16位会溢出回绕正数变成负数输出波形会出现严重的削波失真听感上就是那种啪啪啪的爆破声。后来加上了饱和模块判断高12位高于16位的扩展位不是全0也不是全1时说明数据已经溢出直接输出最大值或最小值function automatic signed [15:0] saturate_16bit(input signed [27:0] din); if (din[27] 1b0 din[26:16] ! 11b0) saturate_16bit 16sh7FFF; else if (din[27] 1b1 din[26:16] ! 11h7FF) saturate_16bit 16sh8000; else saturate_16bit din[15:0]; endfunction有了饱和保护就算输入端突发一个满幅度的干扰输出最多是限幅不会出现符号翻转这种更恶劣的异常。3.3 关键路径与时序收敛为什么Transposed结构更好优化之前做FIR时我习惯用MAC结构一个乘法器复用累加器在回路上时钟频率跑不高。IIR的反馈回路让这种情况更糟一个二阶节的输出反馈到输入算完才能算下一个环路延时直接限制了最高工作频率。我用Transposed结构之后数据路径上只剩一个乘法器加几个加法器中间我再用打拍的方式把乘法和加法分到两个时钟周期关键路径短了不少。在Artix-7上跑100MHz时序报告是clean的没有负slack。实测把采样率推到10MHz以上也能稳定跑不过我的ADC采样率只有48kHz理论余量非常大。4. 仿真与上板验证测试向量设计对了问题才能暴露出来写RTL只是开始验证才是真正花时间的地方。4.1 ModelSim仿真正弦波激励和阶跃响应怎么配合用我先写了testbench生成一个频率1kHz和9kHz叠加的正弦波模拟“低频有用信号高频噪声”的场景。1kHz在通带内应该无损通过9kHz在阻带内应该被衰减到很小。仿真波形出来之后用ModelSim里自带的波形导出功能把输出数据转成文本文件再拉到MATLAB里画频域图。输入信号的1kHz分量幅度保持住了9kHz分量衰减了约38dB和定点的预期值基本对上。阶跃响应也要测这一步能很快看出稳定性问题。给输入一个阶跃IIR输出应该稳定在一个固定值。如果输出振荡不止还发散说明系数出了问题或者结构错误。我刚开始仿真阶跃响应时发现第一个二阶节的阶跃输出定在了1.28而不是1.0排查了一下是增益归一化没完全处理好修正后输出稳定在1.0附近。4.2 上板实测DA输出的一堆毛刺是哪里来的仿真过了上板实测才是真实考验。我用FPGA开发板自带的ADC采集外部信号发生器的输出经过IIR滤波再把结果送到DAC用示波器看波形。第一次实测效果很差滤波后的波形上叠了一层高频毛刺整个信号看起来脏兮兮的。排查了一下午最后发现问题根本不在滤波器本身而在数据同步。ADC输出的12位数据是随采样时钟变化的而FPGA内部逻辑用的是100MHz主时钟。直接用主时钟去采ADC数据采样边沿正好落在数据翻转窗口内时采到的值就是亚稳态的可能是上下两个值的混合状态。处理办法是接一个两级同步寄存器把异步输入先同步到主时钟域reg [11:0] adc_sync1, adc_sync2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin adc_sync1 12d0; adc_sync2 12d0; end else begin adc_sync1 adc_data_in; adc_sync2 adc_sync1; end end接了同步器之后毛刺基本消失了波形干净很多。另一个让我印象很深的小问题是DAC输出端。开始没做任何后级处理示波器上能看到台阶状波形这是DAC保持特性决定的正常现象。我用一个简单的RC低通在板级把DAC的采样保持台阶平滑掉当然这个RC的截止频率选得比信号带宽高不少不会影响目标频带。4.3 用示波器验证滤波器效果扫频率不能偷懒正弦波单点测完还不够。我把信号发生器设成扫频模式从100Hz一路扫到20kHz同时在示波器上观察输出幅度变化。能明显看到5kHz以下基本平坦过了5kHz开始下滑8kHz附近已经掉了一截10kHz以上输出很小。这和MATLAB仿真的幅频响应曲线趋势吻合心里的一块石头才算落地。如果示波器有声卡分析功能也可以把输入输出分别录一段用FFT功能看频谱。我用的是Tektronix的入门款直接看Vpp变化也能说明问题。5. 常见问题与排查技巧实录5.1 问题速查表现象原因解决方案输出波形发散、幅度越来越大系数量化误差导致极点跑出单位圆改用Q1.15更高精度量化或减小系数误差在MATLAB定点模型中验证极点位置输出信号幅度偏大且变形没有做增益归一化或者截位策略不对计算每个SOS的DC增益并归一化分子系数输出前做饱和截位波形上有大量高频毛刺跨时钟域没有做同步处理ADC输入加两级同步寄存器有信号的时候正常无信号时输出有低频杂波可能是累加器位宽不够出现溢出回绕把累加结果符号位扩展位做饱和判断再截位输出滤波性能比浮点模型差很多系数位宽不够或矩阵结构错误检查定点化后的幅频响应必要时增加系数位宽到18位或24位时钟频率上不去时序报告有负slack直接型结构数据路径太长改用Transposed结构或插入流水线寄存器5.2 一个容易被忽略的点复位信号也要做异步复位同步释放FPGA上电时全局复位信号如果直接接在逻辑上很容易因为复位撤销时刻不一致导致模块时序混乱。我在IIR滤波模块里用了异步复位、同步释放的方式reg rst_n_sync1, rst_n_sync2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin rst_n_sync1 1b0; rst_n_sync2 1b0; end else begin rst_n_sync1 1b1; rst_n_sync2 rst_n_sync1; end end assign rst_n_sync rst_n_sync2;所有的always块里都用rst_n_sync而不是外部直接进来的rst_n。这个习惯我在后续所有模块都保持了亚稳态问题明显少很多。5.3 调试期的大杀器ILA在线逻辑分析仪很多老工程师喜欢直接出波形但我觉得Vivado里的ILA集成逻辑分析仪在FPGA调试里的价值被低估了。把滤波器的输入、输出、中间级delay_1和delay_2信号全部挂到ILA上触发条件设置为输入信号的上升沿跑一段时间抓数据。ILA抓到的是FPGA内部的真实运行数据精度到时钟周期比示波器看引脚数据更细腻。有一次我怀疑某个二阶节内部数据溢出用ILA抓了中间节点的数值发现确实在某个瞬间超过了16位能表示的范围饱和模块触发了限幅。如果没有ILA这个问题可能要到很后面才暴露。5.4 资源占用和优化思路写完之后我看了下资源报告在Artix-7 XC7A35T上IIR滤波模块只用了不到1%的LUT和FF乘法器用了3个DSP48。这个资源占用非常小做多通道完全没问题。如果一个DSP48都不想用还可以退回到纯LUT搭建的乘法器代价是LUT占用会上升到2%左右对于滤波需求来说仍然可以忽略。如果要做更高阶的滤波器比如设计一个64阶IIR级联32个二阶节建议把每个二阶节的运算时钟倍频上去多个节之间共享DSP资源或者用流水线把二阶节串起来。这种情况下位宽和系数的定点精度要重新评估尤其是高Q值的节系数量化误差会直接影响滤波器响应。我在实际调试中还发现把RTL中所有的中间信号尽量用(* keep true *)标注防止综合器优化掉不利于调试的节点然后配合ILA查看速度能提升很多。6. 设计扩展与升级建议IIR滤波器这个项目做完之后很多地方其实都可以继续深挖。比如把滤波器系数设计成可配置的通过UART或SPI接口在运行时改变系数寄存器就能做一个实时参数可调的滤波平台。这样在音频信号处理、ECG心电信号预处理、电力系统中的谐波检测等多个场景都能直接复用。另一个方向是把单通道扩展成多通道。前面说过运算模块资源占用极小如果是8路信号同时滤波要么并行放8个滤波模块要么用时分复用加倍的时钟频率在一套运算单元里分时处理多个通道的数据。工程上更推荐后者省资源还能保证多通道之间的一致性。如果你想彻底吃透IIR建议把不同结构Direct I、Direct II、Transposed、State-Variable都用Verilog写一遍比较它们在时序收敛、资源占用、数值稳定性的差异。我就是在这次实践中发现Transposed结构在FPGA上的优势这个结论是查多少资料都换不来的。最后再分享一个我自己的经验每次改完系数或者位宽一定要把MATLAB定点模型和RTL仿真对拍用同一段测试输入数据把两端输出拉到一个坐标系里比较。两边波形重合了再去上板。这样做的调试成本是最低的也是最容易定位问题的。

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

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

免费获取报价 →
↑