
做信号处理的时候总有一道绕不过去的坎——噪声。我最近在弄一个传感器采集的工程采样率50kHz信号有效带宽5kHz模拟端用RC滤波怎么调都压不住纹波想过换高阶有源滤波但PCB已经定了元件参数一变整板都要动。后来索性在FPGA内部用IIR数字滤波器直接处理采样数据进芯片之后先滤波再送上层逻辑效果立竿见影。这也是我今天想聊这件事的起因基于FPGA的IIR数字滤波器设计到底难不难关键点在哪怎么从一张差分方程一路折腾到实测波形干净、时序收敛。这篇文章适合三类人看一是FPGA入门后想做点信号处理项目的二是用DSP或ARM做滤波但觉得延迟和资源调配不顺手、想换FPGA试试的三是已经在FPGA上写过FIR滤波器、想进一步掌握IIR结构的。我会把从滤波器原理、结构选型、系数量化到Verilog实现、仿真验证、板上实测的完整链路都拆开讲并且把我在实际项目里踩过的坑一并交代清楚。1. 先想明白一个问题FPGA做IIR到底图什么1.1 模拟滤波器、DSP处理器和FPGA各自的边界很多人一提到数字滤波第一反应是FIR毕竟FIR稳定、线性相位、教材上讲得也多。但在工程里IIR从来没有退出过舞台。做实时信号处理的时候系统延迟和硬件资源是两个硬指标IIR在相同过渡带和阻带衰减指标下需要的阶数通常只有FIR的1/5到1/10对应的乘法器和寄存器都省很多。如果产品用的是几百K逻辑单元级别的小芯片资源预算非常紧IIR几乎是必然选择。当然先得把方案选型的问题聊透不然很容易出现做完发现根本不合适的情况。模拟滤波器的硬伤是灵活性差。电阻电容的电感值定了截止频率就定了产品改版想换频点就得换物料更不用说高温环境下无源器件参数漂移带来的频率偏移。高端一点的模拟有源滤波器可以调但也得通过模拟开关切换电阻网络而且PCB面积占用真不小。DSP或ARM上跑IIR也不是不行代码写起来还更快。但实际搞过实时系统的人都清楚处理器方案的问题是延迟和确定性。哪怕中断频率固定分支预测、cache miss、中断抢占这些不可控因素都会让每次处理时间出现抖动。对音频、电机控制这类对相位一致性和周期抖动敏感的场景这个抖动就是麻烦本身。而且处理器做滤波是串行的多个通道轮流算通道数一多每个通道实际能分配到的算力就下降。FPGA的优势在于并行和确定性时延。每个通道的乘法器是独立的所有通道同时算延迟就是固定的几个时钟周期。这一点在伺服控制、电力电子、医疗设备前端这些场景里是硬需求。再就是功耗同样的滤波任务用FPGA硬逻辑做比在DSP里连续跑乘加指令省得多对那些需要被动散热的产品来说很重要。1.2 IIR和FIR在FPGA实现上的本质差异从实现角度看FIR是纯前馈结构y[n]只依赖输入样本的历史值不管怎么写都不可能发散顶多是系数量化导致频率响应偏差。IIR是递归结构当前输出依赖之前已经算出的输出值相当于系统自带反馈环路反馈系数一旦处理不好硬件上就可能出现输出饱和甚至发散。这也是不少人在FPGA上做IIR之前的最大顾虑。其实IIR在FPGA上真正难的不是RTL本身有多复杂而是三个工程问题一是系数量化导致的极点偏移二是循环依赖结构带来的时序收敛压力三是有限字长效应导致的输出噪声和极限环。这三个问题不解决仿真再漂亮一上板就露馅。我在后续章节会逐个展开并且给出我实际验证过的对策。2. 从差分方程到RTLIIR硬件结构的取舍逻辑2.1 三种直接结构的对比与转置直接II型的优势IIR数字滤波器的基础是一个线性常系数差分方程y[n] b0·x[n] b1·x[n-1] b2·x[n-2] - a1·y[n-1] - a2·y[n-2]这里a和b是滤波器系数容易发现输出由两部分组成输入前馈项的加和以及输出反馈项的加和。教科书上常见三种直接结构直接I型、直接II型、转置直接II型它们在数学上完全等价换成浮点理想精度运算输出一模一样。但在FPGA里三种结构的寄存器数量、关键路径长度和量化特性都有差别选型直接影响资源占用和时序收敛。直接I型最直观输入先经过两个延迟单位组成的前馈链再经过反馈链前馈和反馈各需要N个寄存器N为阶数结构简单但要2N个寄存器。直接II型把前馈和反馈的延迟链合并成一条寄存器数量减半到N个看起来更省资源。但它有一个工程隐患中间的求和节点数值范围可能比输入大很多需要额外留足够的位宽否则容易在中间节点溢出。转置直接II型在信号流图上是把直接II型的支路方向反转输入和输出交换位置它的特点在于每一级的加法到寄存器之间的路径非常短天然适合流水线插入对FPGA的高时钟频率要求更友好。实际工程里我个人优先用转置直接II型。2.2 级联二阶节高阶滤波器的工程标准做法直接实现一个6阶或8阶IIR理论上可行工程上没有人这么干。原因是直接型的系数灵敏度太高每一个系数微小的量化误差都可能让极点位置大幅偏移尤其高阶滤波器极点密集量化偏差很容易让原本稳定的一对共轭极点跑到单位圆外。你问为什么2阶不够、非要用高阶因为工程中的滤波器指标往往不是能滤就行而是要求过渡带窄、阻带衰减够。拿Butterworth来说2阶低通的阻带衰减斜率只有12dB/oct4阶才能到24dB/oct要压住更近的干扰源就得提高阶数。处理方式是把高阶传递函数分解成多个二阶节的级联这就是SOSSecond-Order Sections结构。每个二阶节有自己的一对共轭极点整体级联后总阶数等于各节阶数之和。这样做的好处是每个节的极点位置相对独立系数量化只影响本节的极点位置不会让所有极点整体挤在一起互相放大误差。实际使用中一般每个二阶节设计成单位直流增益节与节之间通过24位或32位定点数无缝衔接极大降低内部溢出风险。级联顺序也有讲究。通常把Q值最高的节放在前级也就是谐振峰最尖锐的那一节。这么做能提升信噪比因为高Q节在前面可以较早抑制带外噪声避免噪声在后续节被放大。我在MATLAB里用tf2sos函数分解时会显式指定order参数做频率排序实际效果比默认顺序更好。2.3 系数定点和内部位宽最容易失控的环节IIR的反馈特性和有限字长效应是写RTL之前必须搞清楚的事情。先看系数量化一个标准设计的浮点系数比如0.6180339887转成16位有符号定点数后只能近似表示量化误差直接映射到极点的极径和幅角上反映到频率响应就是截止频率偏移、通带纹波变大极端情况下极点越过单位圆导致自激振荡。我的处理流程是先在浮点域设计好滤波器得到浮点系数然后量化成定点再在MATLAB里用相同输入信号分别跑浮点模型和定点模型对比输出波形和频谱。定点格式选Q1.14或Q1.15比较常见也就是1位符号位15位小数的定点数。若系数动态范围大比如某些带通滤波器的分子系数很小建议用Q2.13或Q1.14先做仿真再拍板别想当然。内部累加器的位宽更关键。两个16位数相乘得到32位结果若只保留16位每一步都截位输出噪声和极限环问题就会冒出来。我的经验是乘法器输出用32位全精度保留每级内部累加用36位或40位只有最后输出到下一级或端口时才做一次截位或饱和。这样可以有效把量化误差压缩到最低。3. 从系数设计到仿真通过一套可以照抄的实操流程3.1 MATLAB/Octave里设计一个4阶低通滤波器我以一个实际的例子来走一遍全流程。系统时钟50MHz采样率50kHz也就是每1000个时钟周期采一次样。要设计一个4阶Butterworth低通通带截止频率5kHz阻带衰减大于40dB。在MATLAB里用下面这段代码设计并分解成二阶节fs 50000; % 采样率 50kHz fpass 5000; % 通带截止频率 5kHz % 设计4阶巴特沃斯低通归一化截止频率 0.2*pi [z, p, k] butter(4, 2*fpass/fs); % 转成SOS形式二阶节级联 [sos, g] zp2sos(z, p, k); % 查看系数 disp(sos);这里得到两个二阶节每个节的系数格式是[b0 b1 b2 1 a1 a2]注意a0已经归一化成1RTL里用不到也不能用。在FPGA实现时每个二阶节按照差分方程独立实现y1[n] b0·x[n] b1·x[n-1] b2·x[n-2] - a1·y1[n-1] - a2·y1[n-2]第二节把y1[n]当输入x2[n]输出就是最终滤波结果。用zpk而不是直接传递函数形式一个主要原因是数值稳定性更好尤其滤波器阶数高的时候直接展开多项式系数会出现严重的条件数问题。3.2 系数定点化的验证方法拿到浮点系数后我习惯先在MATLAB里做一次定点仿真确认系数量化对频率响应的影响可接受。下面是一段16位定点量化并对比频率响应的脚本思路% 量化系数到 Q1.15 q_coeff round(sos * 2^15) / 2^15; % 用量化后系数计算频率响应 [Hq, f] freqz(q_coeff(1,1:3), q_coeff(1,4:6), 4096, fs); [Horig, ~] freqz(sos(1,1:3), sos(1,4:6), 4096, fs); % 对比两组响应之差 semilogy(f, abs(Horig), b-, f, abs(Hq), r--);重点关注三个地方第一是截止频率有没有明显偏移偏移超过10%就要考虑提升系数位宽16位不够就用24位FPGA的DSP48乘法器原生支持25x18用起来也不浪费。第二是阻带衰减有没有退化尤其是40dB的指标量化后掉到35dB以内就预示系数位宽不足。第三是极点是否仍在单位圆内这个可以直接查看量化后a1、a2对应的极点半径。3.3 Verilog实现以转置直接II型二阶节为例下面给出一个4阶低通IIR滤波器的核心Verilog代码框架第一节和第二节结构相同只是例化两次。模块接口包含时钟、复位、输入有效标志、输入数据输出有效标志和输出数据。module iir_sos ( input wire clk, input wire rst_n, input wire din_valid, input wire signed [15:0] din, output reg dout_valid, output reg signed [15:0] dout ); // 第一节系数Q1.15格式具体值按设计结果填 localparam signed [15:0] B0_1 16sdXXXX; localparam signed [15:0] B1_1 16sdXXXX; localparam signed [15:0] B2_1 16sdXXXX; localparam signed [15:0] A1_1 16sdXXXX; // 实际存 -a1 localparam signed [15:0] A2_1 16sdXXXX; // 实际存 -a2 // 中间信号定义 reg signed [15:0] x_reg1, x_reg2; reg signed [15:0] y1, y1_reg1, y1_reg2; wire signed [31:0] mul_b0, mul_b1, mul_b2; wire signed [31:0] mul_a1, mul_a2; wire signed [35:0] acc; // 输入延迟链 always (posedge clk or negedge rst_n) begin if (!rst_n) begin x_reg1 16sd0; x_reg2 16sd0; end else if (din_valid) begin x_reg1 din; x_reg2 x_reg1; end end // 乘法器由综合工具映射到DSP48 assign mul_b0 din * B0_1; assign mul_b1 x_reg1 * B1_1; assign mul_b2 x_reg2 * B2_1; assign mul_a1 y1_reg1 * A1_1; assign mul_a2 y1_reg2 * A2_1; // 累加采用36位宽防止中间溢出 assign acc mul_b0 mul_b1 mul_b2 mul_a1 mul_a2; // 输出截位并更新 always (posedge clk or negedge rst_n) begin if (!rst_n) begin y1 16sd0; y1_reg1 16sd0; y1_reg2 16sd0; dout_valid 1b0; dout 16sd0; end else if (din_valid) begin y1 acc[32:17]; // 高16位截位等价于右移17位后舍入 y1_reg1 y1; y1_reg2 y1_reg1; dout_valid 1b1; dout y1; end end endmodule几个细节得说明白。A1_1和A2_1在Verilog里存的是负反馈系数的相反数也就是把原差分方程的减号变成加负系数这样RTL里统一用加法器处理省一堆符号判断逻辑。截位的地方我用的是直接取高16位实际工程更推荐用带舍入的截位方式是在右移前先加上0x4000相当于加半个LSB再截位能显著降低直流偏置和量化噪声。两个二阶节级联时把第一节的y1接到第二节的din第二节的输出作为最终结果。为了稳妥我在两节之间加了一级寄存器可以打断组合路径时序收敛更容易。3.4 仿真验证RTL输出和MATLAB参考对齐光有代码不能算完验证这一步决定上板之后是不是要返工。我的做法是用MATLAB生成一段混合信号包含5kHz的期望正弦波和20kHz的干扰信号量化成16位有符号整数写入文本文件作为Testbench激励。Testbench的核心逻辑是读出每个采样点给上din_valid脉冲等dout_valid拉高把每个dout存入文件跑完全部数据后退出。仿真结束后把RTL输出文件导入MATLAB做FFT看频谱里20kHz分量是否被压下去、5kHz分量是否保留。理想情况下20kHz衰减应大于30dB同时5kHz通带增益接近0dB。如果仿真结果对不上别急着改RTL先回到定点模型跑同一份激励数据看定点模型输出和RTL输出是否一致。误差在几个LSB以内说明RTL实现正确差异大的话优先检查截位位置和乘法器符号位处理是否一致。在Xilinx Vivado里仿真建议直接用行为仿真跑一次几万个点也就几秒钟效率很高。4. 实测中遇到的三个问题与完整排查链路4.1 输出发散先区分是板级问题还是算法问题我第一次上板调试时把激励信号从信号发生器通过ADC送进FPGA示波器上看滤波输出直接变成一条接近满幅的振荡波形第一反应是RTL写错了。后来冷静下来按下面思路一步一步排除。先把ADC输入断开改用FPGA内部的常数序列或正弦查找表作为激励喂给滤波器模块再看输出是否发散。这样避免ADC时序或模拟前端噪声干扰判断。用内部激励测试后发现依然发散问题锁定在滤波器算法本身。接着检查系数符号。IIR差分方程里反馈项是减号但在RTL实现中我用加负系数的做法如果系数在MATLAB里忘了取负反馈就从负反馈变成正反馈输出必发散。这是最容易踩的坑。最后检查极点位置。把量化后的系数导入MATLABpzmap看一下极点是否在单位圆内。我在Q1.15下测试第二个二阶节时发现极点半径是0.998离单位圆很近通带增益裕量很小稍有一点运算误差就可能超过1。这说明16位系数精度对这个滤波器不太够我随后将内部乘法和系数位宽提升到24位问题立刻解决。4.2 输出噪声偏大和直流偏移发散问题解决后示波器上波形形状对了但总感觉底噪偏大而且输出信号有一个明显的直流偏置。这个问题的排查链路不太一样也更隐蔽。直流偏置最直接的来源是截位策略。直接截掉低位的做法相当于向负无穷方向取整如果信号统计特性不对称大量截位会累积成一个稳定的直流分量。换成带舍入的截位后直流偏置明显减小。还有一个来源是ADC输入本身如果ADC偏移没有校准输入信号带有直流分量即使是理想滤波器也会在输出留下同样的直流分量。底噪偏大则需要检查内部位宽。用16位乘法器做反馈路径时每一轮迭代都会引入截位噪声经过反馈环路累积最终体现在输出底噪。我的处理方法是反馈路径的乘法结果保留全精度32位累加器用36位或40位只在最后一级输出截位。提升内部位宽后实测底噪下降了接近10dB效果非常明显。4.3 时序收敛压力与综合警告处理4阶IIR只含两个二阶节理论上逻辑规模不大但在50MHz甚至更高时钟下组合路径从输入直接穿到累加器再到输出长度可能超标。时序收敛的解决方法很直接在每一级乘法和加法之间插入流水线寄存器把大组合逻辑切成小段代价是增加几个周期延迟对滤波本身的群延迟影响很小。IIR的反馈结构决定了流水线不能随意插在环路内部插入不当会改变算法行为。安全的做法是对转置直接II型结构在反馈环路的加法器输出处打一拍前提是数学上验证过这种改动不改变输入输出关系。综合时如果出现Multipliers inferred警告先确认乘法器是否被映射到DSP48而不是LUT。Xilinx默认综合策略下16x16乘法器一般会用DSP48但如果位宽不是标准大小或使用了非乘法运算符就可能被推断为LUT乘法器白白消耗大量逻辑资源。用width24位的有符号乘法DSP48利用率最合适。也可以直接在RTL里例化DSP48宏单元做法更可控但代码可移植性稍差。5. 再往前走一步多通道复用与更多实战细节5.1 多通道时分复用IIR很多系统不止处理一路信号。ADC有8个通道轮流采样每个通道信号特点相似按传统思路例化8个滤波器模块资源开销直接乘8。工程上更优的做法是时分复用用一个时钟下按周期轮询处理所有通道也就是把滤波器的状态寄存器做成8组每组由一个通道号索引每来一个新样本就切换到当前通道对应的那组状态。实现上有一个问题需要解决IIR的反馈计算依赖上一时刻输出不同通道切换时不能把状态搞混。解决办法是用BRAM存储每组状态计数器作为通道号同时作为BRAM地址在同一个时钟周期内完成读出、计算、写回。时序上要求每通道的样本间隔大于完成一轮递归计算需要的周期数不然资源复用做不成。比如计算一个二阶节需要5个时钟周期采样率50kHz对应20us时钟50MHz下每样本间隔1000周期复用8通道完全够用。这样整个多通道IIR的资源开销几乎和一个单通道滤波器相当。5.2 数据有效信号与上下游联调滤波器模块和上下游模块交互时valid信号的设计直接影响系统的鲁棒性。一个经验法则是每个模块的输入valid和输出valid必须严格对应模块内部不得悄悄吞掉或重复数据。级联两个二阶节时中间的数据valid要跟着数据流同步打拍不能直接把输入valid一路送到末端。我在第一版代码里省了中间valid结果第二级滤波偶尔会漏采一个点输出波形出现周期性毛刺排查了很久才定位到是valid同步问题。还有一个小细节IIR滤波器的初始状态要干净。复位时不仅要把数据寄存器清零也要把内部累加器、输出延迟链全部清零。如果只清了输入链而忘了输出反馈链上电后会有一段持续数毫秒的建立过程表现为输出起始阶段有一个衰减振荡。对某些严格要求上电即稳定的系统这个瞬态是不能接受的。5.3 库函数能力和排错工具链做这类设计时我习惯用Vivado的IP Integrator配合AXI4-Stream接口把滤波器封装成一个可复用的IP。一方面AXI-Stream的tvalid/tready握手天然具备背压能力另一方面上下游用标准接口对接后续加FIFO、加DMA都很顺畅。直接在顶层模块里手写握手逻辑当然也行但跨工程复用时需要复制修改代码容易引入新问题。仿真排查时别看只在最终的dout上抓波形。把内部节点的波形也导出对照MATLAB定点模型逐节点比对。比如第一节输出和MATLAB定点模型第一节输出逐点比对差异在几个LSB内才能确认第二级的问题如果直接拿最终输出对比一旦不一致很难定位是哪一级出错。这个逐级对比的做法几乎能解决90%的算法实现类bug。最后分享一点个人体会IIR滤波器在FPGA上做出来后我最大的感受是这个模块的原理和代码在全网能找到很多版本但真正拉开差距的往往是那些没写进PDF里的经验——系数怎么量化、位宽怎么留、截位怎么做、valid怎么同步、时序怎么收敛。纸上谈兵的时候觉得IIR不如FIR好弄实际做完才发现只要把级联二阶节和定点策略想清楚IIR在FPGA上非常稳资源省、延迟低、效果好尤其适合那种资源紧、实时性要求高的项目。如果看完这篇文章你也想动手试我建议从4阶Butterworth低通起步按我上面的流程走一遍MATLAB定点仿真和RTL实现再上板用信号发生器灌两路混频信号看频谱。整个流程走通一遍之后你对IIR的理解会比翻十篇论文都深。后面再想加自适应滤波、可变截止频率也只是在这套底子上加控制逻辑而已。