资讯动态

FPGA数字滤波器设计实战:IIR、FIR与自适应滤波器的实现与优化

发布时间:2026/10/21 17:59:24 来源:尧图企业网站定制
FPGA做数字滤波器这个方向算是我个人很偏爱的“硬核玩具”。它不像跑算法那样只讲究数值对不对也不像点个IP核那样调完就完事而是要真正地把差分方程变成电路、把反馈环路变成存储器与运算器的联动每一步都得脚踏实地。做这个“基于FPGA的滤波器设计IIR、FIR与自适应滤波器”项目时我最大的感受是滤波这件事本身不难难的是在硬件资源、时序、精度和实时性之间找到平衡。下面我就把这一整套从原理到实操的经验拆开讲一讲给正在入门或准备啃这块硬骨头的朋友做个参考。1. 三种滤波器的特性分析与选型思路1.1 为什么FPGA适合做实时滤波器先说个经常被问到的问题为什么非要用FPGA做滤波器直接在DSP上跑不行吗我的回答通常是看延迟和吞吐量。DSP或者ARM跑滤波器是“顺序执行”的思路一个节拍算一个乘法累加阶数上去之后延迟会明显增长。FPGA则完全不同它天生是并行的可以把一整条乘加链摊开成硬件电路数据从输入端进去几个时钟周期后从输出端出来整体延迟可以做到微秒级甚至亚微秒级。这个特性在通信、雷达、音频处理和测量仪器这些场景里特别吃香。另一个关键优势是接口的灵活性。FPGA可以挂高速ADC、DAC也可以直接跑在以太网、光纤这类接口后面。滤波器和上下变频、解调、抽值这些模块可以放在同一个时钟域里做流式处理数据一路流水下去不会像DSP那样频繁地进中断、搬数据。所以只要是要求“数据不停流、延迟不能高、接口能定制”的滤波需求FPGA基本是第一选择。当然FPGA也有让人头疼的地方主要在于开发周期长、调试难度高。同样的算法在PC仿真里写几行代码就能跑在FPGA上要考虑位宽、溢出、时序收敛、复位策略、跨时钟域一堆问题。但换个角度看把这些问题全部捋顺之后你对信号处理本身的理解都会提升一个档次。1.2 IIR、FIR与自适应滤波器性能、资源与稳定性对比做滤波设计之前需要先把三种滤波器摆在台面上做一次彻底的对比。它们各自有不可替代的领域选错类型后面全是坑。FIR滤波器走的是非递归结构当前输出只依赖当前和过去的输入不存在反馈回路。这个特性带来两个巨大优势一是稳定二就是可以做严格线性相位。线性相位在实际工程里太重要了比如通信里的码间串扰抑制、音频里的波形保真、测量仪器里的时间延迟一致性这些都要求群延迟恒定。FIR的代价是相同指标下阶数通常比IIR高好几倍资源消耗更大。IIR滤波器依靠反馈回路用较少的阶数就能实现陡峭的过渡带。两个二阶IIR节拍下去可能就能达到十几阶FIR的效果。省资源这点很诱人但IIR的缺点也很明显没有线性相位过渡带和通带的相位变化是比较扭的。而且反馈结构天生对系数量化误差更敏感极点稍微偏移一点就可能让滤波器性能急剧变差甚至不稳定。自适应滤波器则是另一个维度的东西。它的系数不是敲定之后就不管而是随着输入数据的统计特性实时调整。常见的应用有噪声抵消、回声消除、信道均衡、系统辨识。FPGA上最主流的自适应算法是LMS因为它简单、计算量小、容易硬件化。缺点是收敛速度受信号功率和步长影响很大选不好参数容易发散或收敛太慢。为了更直观我把这三种结构的对比整理成了一张表特性FIRIIR自适应LMS结构非递归无反馈递归有反馈递归系数更新回路输出依赖当前及若干历史输入当前及历史输入历史输出输入输出误差反馈线性相位可以严格实现一般无法实现取决于具体配置稳定性无条件稳定极点必须在单位圆内收敛条件受制于步长与信号功率相同指标下的阶数较高较低视场景差别很大资源消耗乘法器多结构规整乘法器少反馈链路复杂运算量和系数存储量都较大典型场景通信前端、音频、抗混叠低延迟滤波、窄带选频噪声抵消、均衡、辨识表格一列出来就很清楚设计人的核心工作其实是“按场景选结构、按指标定阶数、按平台定精度”。有一回我要做一个音频前级抗混叠滤波带宽和延迟要求都很死我当时第一反应是常规IIR因为省资源但一评估群延迟和相位失真就直接否掉了老老实实上了14阶FIR。资源多用了一倍多但至少相位曲线干净后续算法不用额外补偿。2. 设计前的关键功课定点格式、精度与资源规划2.1 定点格式选择Q值、位宽与量化噪声FPGA本身不怎么适合直接吃浮点数。虽然现在一些高端芯片也有浮点运算单元但在大多数中低端器件上浮点运算是靠通用逻辑去“模拟”的资源消耗高得离谱。所以我们要把算法里的浮点系数和变量转换成定点格式这一步就是设计成败的关键。最实用的表示方式是Q格式也就是“整数位宽小数位宽”的固定拆分。举个例子Q1.15表示总位宽16位最高位是符号位接着1位整数位剩下15位小数位。这样表示的范围是-1到0.999969精度大约是0.0000305。滤波器系数绝大多数都小于1用Q1.15绰绰有余。但中间累加结果就未必了几个16位数相乘再累加位宽很快会爆所以内部累加器一般要扩展到32位以上。位宽规划有一个核心原则从头到尾估算每一级运算的动态范围。输入信号是ADC采样值可能是12位有符号数系数是Q1.15格式乘出来的结果可能达到Input_W Coef_W位累加之后再加个log2(N)的增长位。我一般喜欢留个20%的余量宁可多占一两个bit也不要让溢出把整个系统打回原形。有个容易被忽视的点是DAC或者后级处理模块只接收16位定点数。如果你内部用了32位累加器在截断输出时必须做饱和处理而不是简简单单把低16位丢掉。丢掉低位会导致极低电平信号失真直接截高位则会让信号“卷绕”产生可怕的削波噪声。工程上稳妥的做法是“钳位舍入”超上限就饱和到最大值低于下限就饱和到最小值同时把低位的MSB作为舍入进位。2.2 资源规划DSP硬核、查找表与寄存器之间的取舍FPGA做乘法其实就三块资源可以选专用的乘法器硬核、LUT搭出来的分布式乘法器、以及用移位相加的方式实现乘法。这三者各有各的适用场景。现代FPGA基本都内建了不少专用乘法器块我们习惯叫它DSP块。每个DSP块可以在一个时钟周期完成一个乘法还能顺便做乘加。FIR滤波器的本质是“乘累加”正好踩在DSP块的舒适区上。一个16阶FIR把16个系数展开成16路乘法再接一个加法树一两拍就能消耗掉十几个DSP块。如果你用的是小芯片且还需要挂其他算法就得重新扳指头算资源。如果DSP块不够用可以用LUT移位寄存器实现乘法。一个16位乘16位的乘法大概要消耗上百个LUT而且逻辑延迟更长速度上不去。这种做法适合阶数很低或者时钟频率不高的场景。还有一个老派思路是分布式算法把乘法拆成位运算后预先存进查找表再用查表累加实现滤波器。分布式算法非常省乘法器但系数多之后查找表容量会指数增长比较适合固定系数的场合。我自己做规划时习惯先画一个资源估算表滤波器阶数、每路位宽、输入路数、时钟频率四项填进去之后大概能推算出DSP块和寄存器的占用。这里有几条经验如果单通道采样率不高可以把卷积运算分时复用用一套乘加器轮流算多个抽头DSP块用量可以砍到原来的四分之一。多通道滤波器比如同时处理I/Q两路信号可以考虑把两个通道的数据拼接成复信号用复数乘加结构一次性处理资源能省不少。阶数超过32的时候加法树要拆成多级流水不然组合逻辑延迟会让你根本跑不到目标时钟频率。2.3 时序收敛流水线划分是躲不过的坎FPGA工程里最折腾人的一件事不是逻辑错了而是“功能正确但时序收敛不了”。滤波器电路里存在大量乘法和累加运算组合逻辑路径过长后时钟频率就撑不上去。解决思路非常直白在运算链里插入流水线寄存器。比如FIR的完整乘累加链可以拆成“乘法”和“加法”两大阶段。乘法器固件本身内部也有时钟级延迟你只要把它的输出先寄存一拍再进加法树加法树每加一级插一级寄存器时序就能轻松收敛。带来的代价是输出延迟增加几个时钟周期对于大多数滤波器应用来说完全无伤大雅。IIR滤波器的时序收敛更麻烦一点因为它存在反馈路径。反馈路径上“当前输出”经过组合逻辑算出来之后马上又被用来算下一个周期的输出如果这条路径的延迟超过一个时钟周期那就无论如何也收敛不了。处理手段通常是把反馈计算尽可能压缩用高基数的乘加结构减少组合逻辑级数或者干脆降低时钟频率来换取收敛。后面实操部分我会给一个具体的处理方式。3. 动手实操IIR、FIR与自适应滤波器的FPGA实现3.1 FIR滤波器实现抽头延时线加乘加链FIR滤波器最标准的实现就是抽头延时线。简单说把输入数据串进一串寄存器每个寄存器输出和对应系数相乘再把所有乘积加起来。下面的Verilog代码展示了一个参数化的16阶FIR主体结构module fir_top #( parameter DATA_W 16, // 输入数据位宽 parameter COEF_W 16, // 系数位宽 parameter TAP_N 16 // 滤波器阶数 )( input wire clk, input wire rst_n, input wire signed [DATA_W-1:0] din, output reg signed [DATA_WCOEF_W-1:0] dout ); reg signed [DATA_W-1:0] delay_line [0:TAP_N-1]; wire signed [COEF_W-1:0] coeff [0:TAP_N-1]; wire signed [DATA_WCOEF_W-1:0] prod [0:TAP_N-1]; integer i; // 系数表这里先以简单的低通系数占位实际项目用工具生成 assign coeff[0] 16h000F; assign coeff[1] 16h003D; assign coeff[2] 16h00A4; assign coeff[3] 16h0111; assign coeff[4] 16h0140; assign coeff[5] 16h0111; assign coeff[6] 16h00A4; assign coeff[7] 16h003D; assign coeff[8] 16hFFC3; // 负数用补码表示 // 实际16阶应从0到15取值这里仅示意 // 延时线 always (posedge clk or negedge rst_n) begin if (!rst_n) begin for (i 0; i TAP_N; i i 1) delay_line[i] {DATA_W{1b0}}; end else begin delay_line[0] din; for (i 1; i TAP_N; i i 1) delay_line[i] delay_line[i-1]; end end // 乘法结果 genvar g; generate for (g 0; g TAP_N; g g 1) begin : mul_stage assign prod[g] delay_line[g] * coeff[g]; end endgenerate // 两级加法树输出寄存 always (posedge clk or negedge rst_n) begin if (!rst_n) begin dout {DATA_WCOEF_W{1b0}}; end else begin dout prod[0] prod[1] prod[2] prod[3] prod[4] prod[5] prod[6] prod[7] prod[8] prod[9] prod[10] prod[11] prod[12] prod[13] prod[14] prod[15]; end end endmodule这份代码的关键点有三个一是延时线用移位寄存器实现每来一个时钟所有数据整体后移一格二是系数是固定常量综合工具会把它优化到乘法器的常系数输入端三是输出不仅需要寄存位宽也扩展到了输入加系数的总位宽避免累加溢出。实际项目里建议用MATLAB这类工具先把滤波器系数算出来然后量化成整数导成Verilog的coefficient array。别再像我早期那样手填系数又慢又容易抄错。做16阶滤波器时我建议先把加法树做成多级先将相邻两两相加得到一个八组结果再把八组两两相加依次类推。多级加法树虽然会让输出多延迟几拍但逻辑级数低时序收敛快得多。3.2 IIR滤波器实现反馈环路与直接I型结构IIR的设计核心在于把差分方程变成电路。以二阶直接I型为例y[n] b0·x[n] b1·x[n-1] b2·x[n-2] - a1·y[n-1] - a2·y[n-2]对应的Verilog模块可以这样组织module biquad #( parameter W 16 )( input wire clk, input wire rst_n, input wire signed [W-1:0] x, output reg signed [2*W-1:0] y ); reg signed [W-1:0] x_d1; reg signed [W-1:0] x_d2; reg signed [2*W-1:0] y_d1; reg signed [2*W-1:0] y_d2; wire signed [W-1:0] b0 16h0200; wire signed [W-1:0] b1 16h0400; wire signed [W-1:0] b2 16h0200; wire signed [W-1:0] a1 16h0C00; wire signed [W-1:0] a2 16h0E00; wire signed [2*WW-1:0] sum; assign sum x * b0 x_d1 * b1 x_d2 * b2 - y_d1 * a1 - y_d2 * a2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin x_d1 {W{1b0}}; x_d2 {W{1b0}}; y_d1 {2*W{1b0}}; y_d2 {2*W{1b0}}; y {2*W{1b0}}; end else begin x_d1 x; x_d2 x_d1; y_d1 y; y_d2 y_d1; y sum[$bits(y)-1:0]; end end endmoduleIIR最容易出问题的地方就在反馈路径。我上面写的是直接I型虽然直观但反馈路径上要同时完成两次乘法和五次加法组合逻辑延迟不小。如果时钟频率要求高于100MHz我一般会改用直接II型转置结构它能利用流水线寄存器把反馈路径拆开时序收敛容易一些。还有一个特别值得强调的细节反馈运算用到的系数a1、a2通常是负数转补码存的符号处理一旦弄错整个滤波器就会变成自激振荡器。我第一次做IIR的时候把负系数当成无符号数处理结果上板后输出一浪高过一浪最后直流顶死。之后我再也不依赖手工转换一律用脚本或者配合数值工具生成二进制补码既省心又不出错。3.3 自适应滤波器实现LMS算法的硬件化改造自适应滤波在FPGA里最常用的就是LMS算法。它的核心迭代公式可以写成y[n] W[n]·X[n] e[n] d[n] - y[n] W[n1] W[n] μ·e[n]·X[n]这里W是权重向量X是输入向量d是期望信号e是误差μ是步长。硬件实现LMS最核心的矛盾是权重更新这个环节里自带组合逻辑回路如果图省事把计算直接串在一起时序一定爆炸。我下面给一个简化但结构正确的两抽头LMS写法module lms_2tap #( parameter W 16 )( input wire clk, input wire rst_n, input wire signed [W-1:0] x, // 当前输入 input wire signed [W-1:0] d, // 期望信号 output reg signed [W-1:0] y, // 滤波器输出 output reg signed [2*W-1:0] e // 误差 ); reg signed [W-1:0] x_old; reg signed [W-1:0] w0, w1; reg signed [2*W-1:0] y_acc; reg signed [2*W-1:0] e_acc; localparam MU 16h0200; // 步长视实际信号幅度调整 always (posedge clk or negedge rst_n) begin if (!rst_n) begin x_old 0; w0 0; w1 0; y 0; e 0; end else begin x_old x; y_acc w0 * x w1 * x_old; y y_acc[W-1:0]; e_acc d - y; e e_acc; w0 w0 MU * e_acc * x; w1 w1 MU * e_acc * x_old; end end endmodule这段代码示意了将整个LMS流水化成三个时钟周期的过程先算出滤波输出再算误差最后用误差更新权重。每一步都有寄存器隔开时序可收敛。但注意LMS本质上要求在“同一个采样点”完成这三步流水会让权重更新滞后一拍这就是文献里常说的“延迟LMS”。工程上完全能用只要你对滞后一拍带来的性能损失心里有数。步长的选择是另一个大坑。μ太小收敛慢μ太大直接发散。理论上的上限跟输入信号功率有关但FPGA里算信号功率很麻烦我通常的做法是先用PC端工具把目标信号的幅度范围测出来再把μ设成上限的一半左右留足余量。还有一个技巧是在硬件里加一个信号幅度统计模块如果误差连续多拍超过阈值就自动把μ减半这个保护机制能极大提高稳定性。自适应滤波还有一个容易被忽略的资源点权重存储。抽头数少可以像上面那样用寄存器但抽头数一多就得用块内存。内存接口如果做异步读时序会很紧张我一般会把权重放在同步RAM里读权重和更新权重分别用两个时钟周期完成经典的自适应系统在时钟频率和抽头数之间做个取舍这样最干脆。4. 常见问题与排查技巧实录4.1 仿真正常、板卡失真最容易被忽略的三个环节这类问题的出现频率相当高。我总结下来板级效果和仿真对不上的原因集中在以下三处一是位宽截断。仿真用的输入是浮点或者宽位定值板上从ADC送进来的数据却是实实在在的有限位宽比如12位或14位。如果你仿真时用的是16位正弦实际12位信号进来信噪比天然就差一截这不是滤波器的问题。建议仿真时务必用真实位宽的信号源去激励。二是复位状态没处理干紧。滤波器这种有记忆的系统复位之后延时线和反馈寄存器里必须清零。如果复位只清了部分寄存器上电后的前几百拍输出可能带着未知的初始状态表现出来就是“前几个输出莫名其妙地乱跳”。尤其IIR反馈寄存器不清零可能让系统一开始就输出一个巨大的毛刺直接把后面的DAC打保护。三是系数符号位被综合工具优化错了。有些情况下工具看到常数乘法器就自动改成移位相加如果你系数是负数又没声明signed优化出来的结果就可能完全错误。所以Verilog里所有跟系数相乘的信号一律标成signed不要含糊。4.2 反馈环路优化如何把闭环设计做成可收敛的流水IIR和自适应滤波器的时序收敛是硬骨头我的做法是用“重定时”来拆反馈路径。重定时是一种标准的结构变换简单来说在保证传递函数不变的前提下把组合逻辑里的延时重新分配让关键路径上的运算级数变短。以二阶IIR为例可以把反馈项里a1和a2的乘法分别放到两个乘法器硬核里乘法器自带输出寄存器等于把反馈路径切成了两段。不过要注意重定时会改变输出相对输入的延迟也会略微改变环路时序关系设计者需要判断这一点对系统指标的影响是否可接受。实际调试时序时我习惯把综合工具里时序报告里的最差负余量路径展开逐个看是哪一级组合逻辑太长。如果看到关键路径经过了一个很宽的加法器我会把加法器拆成两级流水中间加寄存器。拆完再综合路径延迟通常会降三分之一左右。但拆流水不能乱拆在反馈环路上多插一拍可能让算法本身收敛性尽失所以只能拆那种不参与反馈的路径反馈路径则要靠重定时。4.3 频响验证与调试心得从仿真结果到实测滤波器做完了不能只看时域波形拍个手就完事。我习惯的验证步骤是先把系数和结构在PC端工具里仿真出频响曲线再把这个频响曲线作为基准FPGA实现之后用同样系数的正弦扫频信号灌进去在输出端采集点值然后做FFT看幅度变化。具体操作上我会在模块输入端加一个扫频信号发生器从低频一直扫到Nyquist频率附近。每到一个频点记录输出幅度最后把这些点连起来形成实测频率响应曲线。这条曲线如果和理论曲线基本贴合滤波器就算基本过关否则就要回头查系数量化误差、输出位宽是否不够、累加器是否溢出。还有一个容易被忽略的细节是数据有效标志。FPGA内部数据是连续的但ADC/DAC的采样率未必和FPGA时钟完全一致。如果接口跨时钟域处理得不好输出会偶发一个周期的跳变在频谱上看就像毛刺。所以我在设计滤波器模块时总会附加一条valid信号只有valid拉高时输出才有效后续模块统统按这个标志取数。5. 一些实操体会做了这么多年FPGA滤波器我个人最大的体会是不要在一开始就埋头写RTL花时间把算法仿真、定点和资源规划做扎实后面几乎不会有推倒重来的情况。我见过太多同学先是浮点算法仿真做得爽一上硬件就疯狂抓瞎最后来回改代码项目一拖再拖。反过来如果先把位宽算明白把流水级数定好再把仿真激励做成贴近真实板级的格式整个开发过程会非常顺。最后再分享一个小技巧把滤波器的系数和位宽参数都定义成parameter不要硬编码在逻辑里。这样以后换指标、换采样率、换系数只需要改顶层参数RTL一行不动。配合厂商的综合工具把每个模块的资源报告自动导出你就能非常轻松地对比不同阶数、不同位宽下的资源占用。这套流程我用了很多年任何时候拿出来都很稳妥。

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

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

免费获取报价 →
↑