☰
FPGA级联型IIR滤波器Verilog实现:从MATLAB定点化到Vivado上板验证
2026/10/20 4:23:54 网站建设 项目流程

简介:这份资源面向FPGA数字信号处理的学习者与工程实践者,聚焦级联型IIR滤波器的Verilog硬件实现,帮助读者理解如何用Vivado将递归滤波算法落地为可综合的硬件电路。内容围绕多个二阶节(biquad)级联结构展开,涉及乘法器、加法器、系数寄存器、状态变量以及环路稳定性等关键设计点,并配有测试平台用于仿真验证频率响应与输出正确性。资源包共82个文件,约223KB,以Verilog源码、Vivado工程文件、仿真脚本、日志与报告为主,另含MATLAB脚本、测试数据与说明文档,便于对照代码与仿真结果逐步复现设计流程。目前已有2550人学习下载,适合具备一定Verilog与数字信号处理基础、希望掌握FPGA滤波器实现方法的读者参考,可从中获取完整的工程结构、模块划分思路与验证方法。

1. 从一次“玄学”调试说起:为什么级联型IIR滤波器值得单独做一版Verilog

带过FPGA数字信号处理项目的人多半有过这种经历:直接型IIR滤波器在MATLAB里仿真完美,定点化之后上板,输出波形却开始周期性抖动,换一组系数又正常,再换一组又翻车。问题往往不在算法本身,而在高阶直接型结构的系数敏感度和量化误差累积。级联型IIR滤波器把高阶传递函数拆成多个二阶节串联,每一节单独做定点缩放,系数动态范围可控,这种结构在FPGA上做Vivado综合时资源占用和时序收敛都更友好。这套方案面向正在做FPGA数字信号处理课程设计、毕设或需要交付滤波器IP的开发者,用Verilog把级联型IIR从系数生成、定点量化、二阶节实现到Vivado上板验证整条链路走通,交付一份可以直接改参数复用的工程。

2. 需求分析与技术选型:为什么是级联型而不是直接型

2.1 从滤波器指标反推结构选型

假设一个典型需求:采样率48 kHz,通带截止4 kHz,阻带衰减不低于40 dB,过渡带1 kHz。用MATLAB的ellipord或butter算一下,椭圆滤波器大概6到8阶就能满足。8阶直接型IIR意味着差分方程里有8个前馈系数和8个反馈系数,定点化之后分母系数的微小扰动会直接改变极点位置,极点一旦跑到单位圆外,输出就发散。级联型把8阶拆成4个二阶节,每个二阶节的极点只由自己那两个系数决定,量化误差被局部化,不会跨节放大。

选型时我一般按这个顺序判断:阶数低于4阶,直接型够用;4到10阶且对稳定性敏感,优先级联型;超过10阶再考虑并联型。这套方案锁定级联型,是因为它在FPGA上还有一个隐性优势——二阶节结构规整,可以做成参数化模块例化多次,代码复用率高,Vivado综合时也容易推断出DSP48资源。

2.2 定点位宽与系数缩放策略

浮点系数直接截断成定点会引入两类误差:系数量化误差和运算舍入误差。工程上通常用Q格式定点数,比如Q15表示范围[-1, 1),分辨率约3e-5。但二阶节的系数不一定都在[-1,1)内,尤其是增益系数。我的做法是每个二阶节单独做增益缩放,把该节的最大系数归一化到Q15范围内,节与节之间用移位补偿。

具体操作:MATLAB算出级联型的二阶节系数sos矩阵后,对每一节计算max(abs(b))和max(abs(a)),取较大者作为缩放因子,把系数除以该因子,同时记录缩放指数。Verilog里每节输出右移对应位数。这样做的代价是引入额外的移位逻辑,但换来的是系数不溢出、精度可控。

注意:缩放因子不要用2的幂以外的数,否则移位补偿会变成乘法,白白消耗DSP资源。

2.3 Verilog实现与Vivado工具链的配合

代码风格上,二阶节用always @(posedge clk)写时序逻辑,系数用parameter或localparam定义,方便综合前改参数。Vivado综合时把乘法器推断成DSP48E1,需要保证乘法操作数位宽不超过25×18。如果系数位宽是16 bit、数据位宽是16 bit,乘积32 bit,累加器要留够位宽防止溢出。我一般把累加器设成40 bit,留足余量。

仿真阶段用Vivado自带的仿真器跑行为级,测试激励用MATLAB生成正弦扫频信号,导出成.txt,Verilog testbench用$readmemh读入。上板验证时用ILA抓二阶节中间节点,对比MATLAB定点仿真结果,逐节排查。

3. 总体架构设计:四个二阶节加一个控制模块的职责划分

3.1 顶层模块的端口与数据流

顶层模块iir_cascade_top的端口包括:clk、rst_n、data_in(16 bit有符号)、data_valid、data_out(16 bit有符号)、out_valid。数据流是:输入数据依次经过4个biquad二阶节,每个节输出打一拍有效信号,4级流水后out_valid拉高。

module iir_cascade_top #( parameter DATA_WIDTH = 16, parameter COEFF_WIDTH = 16, parameter ACC_WIDTH = 40 )( input wire clk, input wire rst_n, input wire signed [DATA_WIDTH-1:0] data_in, input wire data_valid, output wire signed [DATA_WIDTH-1:0] data_out, output wire out_valid ); // 4级二阶节级联,每级输出位宽保持DATA_WIDTH wire signed [DATA_WIDTH-1:0] stage_out [0:3]; wire stage_valid [0:3]; // 第一节 biquad #(.DATA_WIDTH(DATA_WIDTH), .COEFF_WIDTH(COEFF_WIDTH), .ACC_WIDTH(ACC_WIDTH)) u_biquad0 ( .clk(clk), .rst_n(rst_n), .data_in(data_in), .data_valid(data_valid), .data_out(stage_out[0]), .out_valid(stage_valid[0]) ); // 后续节依次连接,此处省略例化细节 endmodule

这段代码的关键点是级联顺序不能乱,MATLAB生成的sos矩阵顺序就是Verilog例化顺序。每个二阶节的系数通过parameter传入,改滤波器只需重新生成系数,不动结构代码。

3.2 二阶节模块的内部结构

单个biquad模块实现传递函数:

H(z) = (b0 + b1z^-1 + b2z^-2) / (1 + a1z^-1 + a2z^-2)

直接II型转置结构最省寄存器,也最容易做流水线。内部维护x1, x2, y1, y2四个延迟寄存器,每个时钟周期算一次输出。

module biquad #( parameter DATA_WIDTH = 16, parameter COEFF_WIDTH = 16, parameter ACC_WIDTH = 40, parameter signed [COEFF_WIDTH-1:0] B0 = 16'sd32767, parameter signed [COEFF_WIDTH-1:0] B1 = 16'sd0, parameter signed [COEFF_WIDTH-1:0] B2 = 16'sd0, parameter signed [COEFF_WIDTH-1:0] A1 = 16'sd0, parameter signed [COEFF_WIDTH-1:0] A2 = 16'sd0 )( input wire clk, input wire rst_n, input wire signed [DATA_WIDTH-1:0] data_in, input wire data_valid, output reg signed [DATA_WIDTH-1:0] data_out, output reg out_valid ); reg signed [DATA_WIDTH-1:0] x1, x2, y1, y2; reg signed [ACC_WIDTH-1:0] acc; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin x1 <= 0; x2 <= 0; y1 <= 0; y2 <= 0; data_out <= 0; out_valid <= 0; end else if (data_valid) begin // 前馈部分 + 反馈部分,注意反馈用负号 acc = B0 * data_in + B1 * x1 + B2 * x2 - A1 * y1 - A2 * y2; // 截位回DATA_WIDTH,实际工程中需做饱和处理 data_out <= acc[ACC_WIDTH-1 - (ACC_WIDTH-DATA_WIDTH) : 0]; x2 <= x1; x1 <= data_in; y2 <= y1; y1 <= data_out; out_valid <= 1'b1; end else begin out_valid <= 1'b0; end end endmodule

参数说明:B0/B1/B2/A1/A2是Q15格式系数,ACC_WIDTH要保证B0*data_in不溢出,16 bit乘16 bit得32 bit,加上4个乘积项累加,40 bit足够。截位时直接取高位会引入直流偏置,更稳妥的做法是加舍入再截位,或者做饱和判断。

3.3 系数生成与定点化脚本

MATLAB脚本负责从滤波器指标生成sos矩阵,再做定点缩放,最后输出Verilog可用的参数文件。

% 设计8阶椭圆低通,采样48kHz,截止4kHz [order, Wn] = ellipord(4000/(48000/2), 5000/(48000/2), 0.5, 40); [z, p, k] = ellip(order, 0.5, 40, Wn); [sos, g] = zp2sos(z, p, k); % 逐节定点缩放 scale = zeros(size(sos,1),1); sos_fixed = sos; for i = 1:size(sos,1) max_coeff = max(abs([sos(i,1:3), sos(i,5:6)])); scale(i) = 2^floor(log2(1/max_coeff)); sos_fixed(i,1:3) = sos(i,1:3) * scale(i); sos_fixed(i,5:6) = sos(i,5:6) * scale(i); end % 转Q15 sos_q15 = round(sos_fixed * 32768); % 输出到文件,格式按Verilog参数顺序 fid = fopen('coeff.txt', 'w'); for i = 1:size(sos_q15,1) fprintf(fid, '%d %d %d %d %d\n', sos_q15(i,1), sos_q15(i,2), ... sos_q15(i,3), sos_q15(i,5), sos_q15(i,6)); end fclose(fid);

这段脚本的核心是scale的计算,用2^floor(log2(1/max_coeff))保证缩放后最大系数不超过1。实际工程中还要检查缩放后系数是否超出Q15范围,超出就再降一档。

4. 关键模块实现:二阶节流水线与系数加载

4.1 流水线设计对时序收敛的影响

二阶节内部如果在一个时钟周期内完成5次乘加,组合逻辑路径会很长,Vivado综合后时序可能跑不到100 MHz。我的做法是在乘加链中间插一级流水寄存器,把5次乘加拆成两级:第一级算前馈三项,第二级算反馈两项并求和。代价是输出延迟增加一个时钟周期,但时序余量明显改善。

// 第一级流水:前馈乘加 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin ff_sum <= 0; end else if (data_valid) begin ff_sum <= B0 * data_in + B1 * x1 + B2 * x2; end end // 第二级流水:反馈乘加并求和 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin acc <= 0; end else if (data_valid) begin acc <= ff_sum - A1 * y1 - A2 * y2; end end

这样改完之后,Vivado时序报告里WNS从负值变成正值,代价是out_valid要多打一拍,级联时注意对齐。

4.2 系数在线加载的取舍

有些场景需要运行时切换滤波器参数,比如自适应滤波。如果系数用parameter写死,综合后无法改。要支持在线加载,得把系数存到寄存器里,通过AXI-Lite或简单寄存器接口写入。但这样会消耗额外寄存器资源,而且系数路径变成寄存器输出,时序可能变差。

我的建议:课程设计和毕设阶段,系数固定用parameter,简单可靠;如果确实需要动态切换,用case语句做多组系数切换,综合工具会推断成多路选择器,比寄存器堆省资源。

4.3 测试激励的生成与对比方法

Testbench里用$readmemh读MATLAB生成的扫频信号,每个时钟周期送一个样点。输出数据写到文件,再用MATLAB读回来和定点仿真结果对比。

initial begin $readmemh("stimulus.txt", stim_mem); // 复位、逐点送入 for (i = 0; i < STIM_LEN; i = i + 1) begin @(posedge clk); data_in = stim_mem[i]; data_valid = 1'b1; end data_valid = 1'b0; $writememh("output.txt", out_mem); end

对比时看两个指标:最大绝对误差和信噪比。如果误差超过1个LSB,检查截位方式;如果信噪比低于理论值,检查系数缩放是否正确。

5. 避坑与常见问题排查:从仿真到上板的五个翻车点

5.1 输出周期性抖动

现象:上板后输出波形有规律的小幅抖动,周期和某个二阶节的极点频率一致。原因通常是截位时直接取高位,低位被丢弃导致极限环振荡。解决:在截位前加舍入,或者把累加器位宽再放宽,输出时做饱和处理而不是简单截断。

5.2 综合后时序不收敛

现象:Vivado综合报告WNS为负,频率跑不到目标值。原因:二阶节组合逻辑太长,5次乘加串在一起。解决:按4.1节插流水线,或者把乘法器输出打一拍再累加。如果DSP48不够用,检查是否被推断成了逻辑乘法而不是DSP。

5.3 系数溢出导致输出饱和

现象:输入信号幅度稍大,输出就削顶。原因:某个二阶节的增益系数缩放后仍然大于1,或者级联后总增益超过预期。解决:重新检查MATLAB脚本里的scale计算,确保每节最大系数不超过Q15范围;级联时在节间加移位补偿。

5.4 仿真结果和MATLAB对不上

现象:Verilog仿真输出和MATLAB定点仿真逐点对比,误差越来越大。原因:MATLAB用的是浮点,Verilog用的是定点,两者本身就有差异。解决:MATLAB侧也做定点仿真,用fi对象或者手动量化,确保对比基准一致。另外检查Verilog的截位方式和MATLAB是否一致。

5.5 上板后ILA抓不到有效数据

现象:ILA触发后看不到out_valid拉高。原因:data_valid的时序不对,或者级联时有效信号没有逐级传递。解决:检查每个二阶节的out_valid是否在data_valid后一拍拉高,级联时把上一级的out_valid接到下一级的data_valid。

6. 验收与答辩:怎么证明这套滤波器真的能用

验收阶段我一般分三步走。第一步,MATLAB浮点仿真和定点仿真对比,确认定点化引入的误差在可接受范围内,通常信噪比不低于60 dB。第二步,Verilog行为级仿真和MATLAB定点仿真逐点对比,最大绝对误差不超过2个LSB。第三步,上板实测,用信号发生器送正弦扫频,ILA抓输出,看幅频响应是否和理论一致。

答辩时评委常问的几个问题:为什么选级联型不选直接型?系数怎么定点化的?资源占用多少?时序余量多少?这些问题在方案里都有答案。我习惯准备一张对比表,把直接型和级联型的资源、时序、稳定性列在一起,一目了然。

对比项直接型级联型
DSP48用量8个乘法器每节5个,共20个
时序余量紧张宽松
系数敏感度高低
代码复用性低高

资源占用方面,8阶级联型在主流FPGA上大约用20个DSP48、500个LUT、600个寄存器,具体数字看器件和综合选项。时序余量在100 MHz时钟下WNS不低于0.5 ns。

从那以后我每次做IIR滤波器,都强制先跑一遍MATLAB定点仿真,再写Verilog,最后上板对比。这个顺序看起来慢,实际上省掉了大量来回调试的时间。希望帮到你。

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

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

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

立即咨询