ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于FPGA的FIR数字滤波器设计与Verilog实现全流程

基于FPGA的FIR数字滤波器设计与Verilog实现全流程 简介这份毕业设计文档围绕基于FPGA的FIR数字滤波器设计展开面向电子信息工程、通信工程等专业的本科毕业生及数字信号处理初学者可用于毕业设计选题参考、论文框架搭建与滤波器实现方案的梳理。内容从FIR滤波器基本结构入手讨论延迟单元、加权求和等核心环节梳理Matlab窗函数法设计、VHDL硬件描述语言实现以及在Quartus II中完成综合与仿真验证的完整思路并对高速度、低功耗、高灵活性等优势做了归纳。压缩包内共1个doc文件约1.64MB为学士论文正文含中英文摘要、关键词、模块化与层次化设计说明及Matlab对比仿真分析结构接近正式论文格式便于读者借用章节组织与论证逻辑快速理解FPGA实现FIR滤波器的关键步骤。目前已有126人浏览学习适合需要现成参考范例、希望缩短前期调研与资料整理时间的读者。1. 从一片 ADC 采样到一路干净波形基于 FPGA 的 FIR 数字滤波器到底在做什么实验室里最常见的一幕信号源给出 10 kHz 正弦波叠加了 50 Hz 工频和几十千赫兹的开关噪声ADC 以 200 kSPS 采样后送进 FPGA你希望输出只剩那条干净的正弦。如果直接做 FFT 再逆变换资源、延迟和时序都不好控制用 IIR 又怕相位非线性答辩时被问群延迟不好解释。FIR 数字滤波器就是为这种场景准备的它用一组固定系数对采样序列做加权滑动平均既能拉出陡峭的过渡带又能保证严格线性相位而且结构规整、易于流水线化——这恰好撞上 FPGA 最擅长的并行与流水线实现。毕业设计选这个题目核心要交付的不是一份说明文档而是一条能跑通的链路系数算得对、定点量化不溢出、Verilog 综合出时序、板上实测频响符合预期。读这篇的人多半正在纠结窗函数怎么选、阶数给多少、乘法器要不要省以及为什么仿真过了板子上却是一片噪声。2. FIR 滤波器的系数从哪来窗函数法、等波纹法与定点量化2.1 为什么毕业设计里优先用窗函数法算系数FIR 设计本质是找一组系数 h[0..N-1]使频响逼近目标。常见三条路窗函数法、频率采样法、等波纹最优法Parks-McClellan。窗函数法从理想低通冲激响应 sinc 截断出发乘窗抑制吉布斯现象优点是公式直白、系数天然对称、线性相位有保证缺点是过渡带宽度和阻带衰减互相牵制要更陡就得加阶数。频率采样法适合任意形状频响但过渡带采样点不好设容易在时域产生振铃。等波纹法能用较低阶数达到给定指标但依赖 remez 迭代毕业设计里如果只是做低通或带通窗函数法足够交差也更容易在答辩时讲清楚每一步。我一般会先定指标采样率 fs、通带截止 fp、阻带截止 fst、通带纹波 δp、阻带衰减 δs。然后按窗函数经验公式估阶数。比如汉明窗过渡带宽度约 3.3/N归一化频率要 20 kHz 到 60 kHz 的过渡带在 200 kSPS 下归一化宽度 0.2N 至少 17实际取 32 或 64 更稳。阶数取偶数还是奇数也有讲究奇数长度阶数 N-1 为偶数能保证群延迟为整数线性相位更干净偶数长度在 Nyquist 处响应固定为 0做低通没问题做高通就要避开。下面这张表把三种方法放在毕业设计常见指标下对比方便选型时直接引用。设计方法阶数需求线性相位过渡带控制适合场景窗函数法中严格靠阶数和窗型低通、带通快速出系数频率采样法中可保证过渡带需插值任意频响窄带等波纹法低严格等波纹最优指标苛刻资源紧张提示毕业设计说明书里不要只写「用 MATLAB 的 fir1」要把窗型、阶数、过渡带带宽写清楚否则答辩老师很容易追问系数为什么是这一组。2.2 用 MATLAB 脚本在 5 分钟内生成一套可综合的 FIR 系数系数生成不用手算一段 MATLAB 脚本就能把浮点系数、量化系数、频响对比一次跑完。下面这段脚本以 200 kSPS 采样率、63 阶汉明窗低通为例通带 20 kHz阻带从 60 kHz 开始。脚本最后把 Q15 定点系数写成 Verilog 能读的十进制列表直接粘进参数数组即可。% fir_coef_gen.m fs 200e3; % 采样率 N 63; % 滤波器长度抽头数 fc 20e3; % 截止频率 win hamming(N); % 窗函数 b fir1(N-1, fc/(fs/2), win); % 浮点系数线性相位 % 归一化并量化到 Q15 Q 15; b_q round(b * 2^Q); b_q(b_q 2^(Q-1)-1) 2^(Q-1)-1; % 饱和 b_q(b_q -2^(Q-1)) -2^(Q-1); b_q b_q / 2^Q; % 回浮点看误差 % 频响对比 [H_f, w] freqz(b, 1, 1024, fs); [H_q, ~] freqz(b_q, 1, 1024, fs); plot(w/1e3, 20*log10(abs(H_f)), w/1e3, 20*log10(abs(H_q))); grid on; xlabel(频率/kHz); ylabel(幅度/dB); legend(浮点,Q15 定点); % 导出十进制系数供 Verilog 参数使用 fid fopen(coef_q15.txt,w); fprintf(fid, %d,\n, round(b_q * 2^Q)); fclose(fid);代码逻辑不复杂fir1返回浮点系数round(b*2^Q)完成定点化饱和处理防止溢出freqz对比量化前后频响。参数说明N越大过渡带越窄但乘法器资源和群延迟同步增加fc是 -6 dB 截止点不是阻带起点Q选 15 意味着系数范围约 ±1适合 16 位有符号数。如果阻带衰减不够先把hamming换成blackman或kaiser再考虑加阶数。2.3 系数量化Q 格式、位宽与通带纹波的取舍FPGA 里没有免费的浮点乘法器系数必须定点。Q 格式表示小数点在二进制中的位置Q15 表示 1 位符号 15 位小数范围 [-1, 1)。系数位宽直接决定频响能保持到什么程度。位宽太窄通带纹波变大、阻带衰减变差位宽太宽乘法器位宽和累加器位宽跟着涨DSP48 可能一个抽头都放不下。下面这张表是 63 阶低通在相同阶数下不同系数位宽的实测对比测试信号为 10 kHz 正弦加白噪声。系数格式系数位宽通带纹波阻带最小衰减单抽头乘法器资源Q1112约 0.8 dB约 45 dB12×12Q1516约 0.1 dB约 70 dB16×12Q1920约 0.02 dB约 85 dB20×12从表里能看出Q15 是性价比拐点再往上加位宽阻带改善有限但 DSP48 的 25×18 乘法器还能塞下 16 位系数和 12 位数据。如果开发板 DSP 资源紧张可以先把数据位宽从 12 降到 10系数保持 Q15通带纹波变化通常比截系数位宽更小。注意系数和数据的位宽要一起规划不能只看系数。累加器位宽也要提前算。N 抽头、数据 DATA_W 位、系数 COEF_W 位最坏情况下累加器需要DATA_W COEF_W ceil(log2(N))位。63 阶、12 位数据、16 位系数累加器至少 1216634 位。实际写 Verilog 时给 36 位留余量最后再截位输出。截位不是简单砍低位要先判断是否溢出再饱和到输出位宽。2.4 量化后必须回验把定点系数丢回浮点模型看频响定点系数生成后最容易犯的错是直接写进 Verilog 综合上板结果发现通带边缘爬不起来。正确做法是把量化后的系数单独拿出来和原始浮点系数做频响对比。上面脚本已经画了一张图但更严格的做法是做定点仿真用浮点输入乘定点系数按 FPGA 的累加和截位规则算一遍再和 MATLAB 的filter结果逐点比。% 定点回验模拟 FPGA 的乘累加和截位 x sin(2*pi*10e3/fs*(0:4095)) 0.1*randn(1,4096); y_float filter(b, 1, x); y_fixed zeros(1, length(x)); acc_w 36; % 累加器位宽 for n N:length(x) acc 0; for k 1:N acc acc round(x(n-k1)*2^11) * round(b(k)*2^15); end y_fixed(n) acc / 2^(1115); % 还原比例 end err y_fixed - y_float; fprintf(最大误差 %.6f均方误差 %.8f\n, max(abs(err)), mean(err.^2));这段代码把数据也量化到 Q11模拟了乘法器和累加器行为。acc_w在这里只用于说明实际 Verilog 里用signed位宽控制。如果最大误差超过一个最低有效位优先检查系数是否饱和、累加器是否溢出而不是急着改阶数。回验通过后再导出系数能省掉大量上板调试时间。3. 在 FPGA 里把 FIR 跑起来直接型、转置型与分布式算法选型3.1 直接型结构一条乘累加流水线怎么排直接型 FIR 的公式就是卷积y[n] h[0]x[n] h[1]x[n-1] … h[N-1]x[n-N1]。在 FPGA 里落地时先用一组移位寄存器把输入序列延时每个抽头对应一个寄存器输出然后 N 个乘法器并行算乘积最后加法树求和。这个结构直观和公式一一对应写 Verilog 也容易。缺点是关键路径长如果加法树不插流水线N 个乘积的加法延迟会限制最高时钟频率。常见做法是在加法树每一级插入寄存器做成流水线代价是延迟增加几个时钟周期但吞吐率不变。直接型还有一个变体利用系数对称性。线性相位 FIR 的系数满足 h[k] h[N-1-k]因此可以把 x[n-k] 和 x[n-N1k] 先相加再乘同一个系数。这样乘法器数量几乎减半加法器数量增加一点。63 阶滤波器从 63 个乘法器降到 32 个对资源紧张的 FPGA 很划算。对称加法要在移位寄存器之后、乘法之前做注意位宽要加 1 位防止溢出。3.2 转置型结构为什么它更适合高采样率转置型是把直接型流图转置输入同时送到所有乘法器乘积进入一条加法器链每个加法器后面接寄存器。它的关键路径是「一个乘法器 一个加法器」和阶数无关因此更容易跑到高时钟频率。对于 200 kSPS 采样率直接型流水线可能也够但如果采样率上百兆或者你要做多相滤波、抽取转置型的时序优势就非常明显。转置型的 Verilog 写法和直接型不同它不是先移位再乘而是每个时钟把当前输入乘上所有系数然后逐级累加。代码里通常用一个 for 循环生成抽头或者用 generate 展开。注意转置型的输出需要额外一级寄存器对齐且初始几拍输出无效要用 valid 信号标记。3.3 分布式算法DA用查表替乘法器的老思路分布式算法是 FPGA 实现 FIR 的经典方法核心思想是把输入数据的每一位单独看待用查找表预先存好所有输入位组合对应的系数和。比如 4 位输入、8 抽头查找表深度 16每个时钟处理一位累加后移位。它用 ROM 代替乘法器在早期 FPGA 乘法器稀缺时很有吸引力。现在 DSP48 资源丰富DA 的查表深度随抽头数指数增长的问题反而更突出所以常见做法只在低阶、低速、资源极度受限时用。毕业设计里如果老师问「为什么不用 DA」可以从资源增长和时序两个角度解释。不过 DA 有个适合毕业设计的点它容易讲清楚「用面积换速度」或「用查表换乘法器」的取舍。答辩时展示一个 8 抽头 DA 的 ROM 初始化表比单纯说「调 IP 核」更有内容。3.4 Verilog 实现一个 16 阶对称 FIR 的完整模块下面给一个参数化的 16 阶对称 FIR 模块数据 12 位系数 16 位采用直接型 对称加法 三级流水线加法树。代码省略了系数具体数值用COEF参数传入方便从 MATLAB 导出后替换。module fir_sym #( parameter DATA_W 12, parameter COEF_W 16, parameter TAP_NUM 16, // 抽头数取偶数 parameter ACC_W DATA_W COEF_W 5 // 累加器位宽 )( input wire clk, input wire rst_n, input wire signed [DATA_W-1:0] din, input wire din_vld, output reg signed [ACC_W-1:0] dout, output reg dout_vld ); localparam HALF TAP_NUM / 2; reg signed [DATA_W-1:0] sr [0:TAP_NUM-1]; wire signed [DATA_W:0] pair_sum [0:HALF-1]; // 对称相加多 1 位 reg signed [DATA_W:0] pair_sum_r [0:HALF-1]; wire signed [DATA_WCOEF_W:0] prod [0:HALF-1]; reg signed [DATA_WCOEF_W:0] prod_r [0:HALF-1]; // 输入移位寄存器 integer i; always (posedge clk or negedge rst_n) begin if (!rst_n) begin for (i 0; i TAP_NUM; i i 1) sr[i] {DATA_W{1b0}}; end else if (din_vld) begin sr[0] din; for (i 1; i TAP_NUM; i i 1) sr[i] sr[i-1]; end end // 对称相加h[k] h[TAP_NUM-1-k] genvar g; generate for (g 0; g HALF; g g 1) begin : gen_pair assign pair_sum[g] sr[g] sr[TAP_NUM-1-g]; end endgenerate // 第一级流水对称和与系数乘 always (posedge clk or negedge rst_n) begin if (!rst_n) begin for (i 0; i HALF; i i 1) begin pair_sum_r[i] 0; prod_r[i] 0; end end else if (din_vld) begin for (i 0; i HALF; i i 1) begin pair_sum_r[i] pair_sum[i]; prod_r[i] pair_sum_r[i] * COEF[i]; // COEF 从参数传入 end end end // 第二级流水加法树求和 reg signed [ACC_W-1:0] sum_tree [0:HALF-1]; always (posedge clk or negedge rst_n) begin if (!rst_n) begin for (i 0; i HALF; i i 1) sum_tree[i] 0; end else if (din_vld) begin for (i 0; i HALF; i i 1) sum_tree[i] prod_r[i]; end end // 第三级流水两两相加最终输出 reg signed [ACC_W-1:0] acc; integer j; always (posedge clk or negedge rst_n) begin if (!rst_n) begin acc 0; dout 0; dout_vld 0; end else if (din_vld) begin acc sum_tree[0]; for (j 1; j HALF; j j 1) acc acc sum_tree[j]; dout acc; dout_vld 1b1; end else begin dout_vld 1b0; end end endmodule这段代码的关键点有三个一是sr移位寄存器把历史采样对齐到抽头二是pair_sum利用对称性先加再乘乘法器从 16 个降到 8 个三是prod_r和sum_tree插入寄存器把乘法与加法树隔开缓解关键路径。参数说明DATA_W要和 ADC 输出位宽一致COEF_W是系数位宽Q15 用 16ACC_W要足够大建议DATA_W COEF_W ceil(log2(TAP_NUM))这里取 5 是留了余量。COEF需要另外用参数数组定义或者用include文件导入。注意pair_sum_r[i] prod_r[i]这一级在实际综合中可能被优化掉如果时序紧张可以改成显式的两级加法树每级都打拍。不要依赖综合器猜你的流水线意图。3.5 资源与时序DSP48、加法器树和流水线级数选结构之前先看资源。Xilinx 的 DSP48 和 Altera/Intel 的 DSP 块都能做 18×18 或 25×18 乘法16 位系数乘 12 位数据绰绰有余。但如果滤波器阶数到 128 以上乘法器数量可能超过 DSP 数量这时要么利用对称性减半要么用时分复用一个乘法器多拍算多个抽头。时分复用会降低吞吐率适合采样率不高、资源紧张的场合。结构乘法器数16 阶加法器数关键路径适合采样率资源占用直接型1615乘法加法树中中对称直接型8158乘法加法树中低转置型1616乘法加法高中高对称转置型8168乘法加法高中DA8 抽头01 累加查表累加低ROM 随抽头指数增长流水线级数不是越多越好。每加一级流水线延迟增加一拍控制逻辑和 valid 信号要同步跟进。对于 200 kSPS 的音频或传感器信号三级流水线已经能把时序跑到 100 MHz 以上完全够用。综合后先看时序报告里的 WNS最差负裕量如果为负优先在加法树中间插寄存器而不是降采样率。4. 仿真、板级验证与联调从 ModelSim 到示波器看波形4.1 用 MATLAB 生成测试向量在 ModelSim 里做逐点比对仿真不是看波形差不多就行要逐点比对。先用 MATLAB 生成一段包含通带、过渡带、阻带频率的测试信号写成十六进制文本再在 ModelSim 的 testbench 里读入把 FIR 输出和 MATLAB 浮点结果对比。下面这段 MATLAB 代码生成 10 kHz 正弦 80 kHz 噪声的混合信号量化到 12 位并写文件。% gen_test_vector.m fs 200e3; n 0:4095; x 0.6*sin(2*pi*10e3/fs*n) 0.3*sin(2*pi*80e3/fs*n); x_q round(x * 2^11); x_q(x_q 2047) 2047; x_q(x_q -2048) -2048; fid fopen(stim.txt,w); for i 1:length(x_q) fprintf(fid, %04x\n, typecast(int16(x_q(i)), uint16)); end fclose(fid);这段代码把浮点信号量化到 Q11typecast把有符号整数转成无符号十六进制方便 Verilog 的$readmemh直接读。参数2^11对应 12 位数据如果 ADC 是 14 位就改成2^13。写出的stim.txt在 testbench 里用$readmemh(stim.txt, mem)加载然后每个时钟送一个样点。ModelSim 侧的 testbench 不需要太复杂关键是产生din_vld和时钟把输出写到文件再用 MATLAB 读取比对。// tb_fir.v initial begin $readmemh(stim.txt, stim_mem); clk 0; rst_n 0; #100 rst_n 1; for (i 0; i 4096; i i 1) begin (posedge clk); din stim_mem[i]; din_vld 1; end din_vld 0; #1000 $finish; end always (posedge clk) begin if (dout_vld) $fwrite(fout, %d\n, dout); endtestbench 的逻辑就是循环送激励、等 valid、记录输出。$fwrite写出的十进制结果和 MATLAB 的filter结果对齐时要注意延迟FIR 模块内部有几级流水线输出会晚若干拍比对时要把 MATLAB 结果整体偏移相同的样点数。如果误差在几个最低有效位内说明定点实现正确如果偏差很大先查系数是否加载正确、符号位是否扩展。4.2 板级接线ADC、DAC、信号发生器和数码管显示板级验证的链路一般是信号发生器产生正弦或方波ADC 采样后送入 FPGAFIR 滤波后送 DAC 输出示波器看输入输出波形。ADC 常见并口或 SPI 接口DAC 常用并行或 I2S。如果开发板自带音频编解码器可以直接用音频接口做低通滤波演示人耳听降噪效果更直观。数码管动态显示可以用来显示当前滤波器系数编号或输出幅度方便答辩时观察。数码管扫描频率一般 1 kHz 左右每位数码管点亮 1 ms人眼就不闪。接线时注意电平匹配。ADC 输出可能是 3.3 V CMOSDAC 输入也可能是 3.3 V直连即可如果 ADC 是差分输出要先经过单端转换或直接送 FPGA 的 LVDS 接收对。电源去耦不能省模拟部分和数字部分尽量分开供电否则噪声会从电源串进 ADC滤波效果再好也看不出来。4.3 常见故障复位亚稳态、时序违例、定点溢出与直流偏置板上调试最常见的问题不是算法错而是复位、时序和定点处理。下面这张表列了典型现象和排查顺序。现象可能原因排查方法输出一直是 0 或最大值复位没释放、valid 没拉高用 SignalTap 看 rst_n 和 din_vld输出周期性毛刺时钟域交叉、亚稳态异步复位同步释放跨时钟加两级寄存器输出波形削顶累加器溢出或 DAC 满幅检查累加器位宽输出前做饱和截位通带衰减比仿真大系数位宽不够、系数量化错回看 Q 格式用 MATLAB 定点模型复现输出有直流偏置输入偏置未去除、截位舍入输入减均值截位用四舍五入而非直接截断复位亚稳态是 FPGA 设计的老问题。异步复位如果不经过同步释放复位撤除时可能落在时钟建立保持窗口内导致寄存器输出未知。常见做法是异步复位、同步释放复位信号直接进寄存器的异步复位端同时在时钟域内用两级触发器同步一个复位释放信号。代码片段如下reg [1:0] rst_sync; always (posedge clk or negedge rst_n_async) begin if (!rst_n_async) rst_sync 2b00; else rst_sync {rst_sync[0], 1b1}; end wire rst_n rst_sync[1]; // 同步后的复位释放这段代码让复位撤除与时钟同步避免亚稳态传播到 FIR 的移位寄存器和累加器。参数上两级同步寄存器足够如果时钟频率很高或环境噪声大可以加到三级。4.4 用 SignalTap 或 ILA 抓内部节点仿真通过不代表板上通过在线调试工具能抓真实节点。Quartus 里用 SignalTapVivado 里用 ILA设置触发条件为din_vld上升沿或某个抽头值超过阈值采样深度至少 1024采样时钟用 FIR 的主时钟。抓到的数据可以导出成 CSV再和 MATLAB 仿真结果对比。注意插入调试逻辑会占用 BRAM 和逻辑资源可能轻微影响时序验证完成后要记得移除或设为不使能。提示ILA 的采样深度和触发条件要提前规划抓 4096 点会消耗大量 BRAM。通常抓 512 到 1024 点就能看出通带波形和阻带衰减是否正常。5. 让毕业设计经得起答辩参数重配、性能对比与可复现记录5.1 用参数化脚本一键重配滤波器答辩时老师常问「如果采样率变成 500 kSPS你的滤波器还能用吗」如果系数是硬编码在 Verilog 里的改一个数就要重新综合现场演示容易翻车。更好的做法是写一个参数化脚本MATLAB 改fs和fc导出新的系数文件Verilog 顶层用parameter或include读取重新综合一次即可。把fs、N、fc、Q四个变量放在脚本头部改完跑一遍频响图和系数列表一起生成。% reconfigure_fir.m cfg.fs 500e3; % 新采样率 cfg.fc 50e3; % 新截止频率 cfg.N 127; % 新阶数 cfg.Q 15; b fir1(cfg.N-1, cfg.fc/(cfg.fs/2), hamming(cfg.N)); b_q round(b * 2^cfg.Q); b_q max(min(b_q, 2^(cfg.Q-1)-1), -2^(cfg.Q-1)); fid fopen(coef_q15.txt,w); fprintf(fid, %d,\n, b_q); fclose(fid); [H, w] freqz(b_q/2^cfg.Q, 1, 2048, cfg.fs); plot(w/1e3, 20*log10(abs(H))); grid on; xlabel(频率/kHz); ylabel(幅度/dB);这段脚本把配置集中到cfg结构体改三个数就能重新生成一套系数和频响图。fir1的第一个参数是阶数注意N-1max/min完成饱和比手动 if 更简洁。导出文件用逗号分隔Verilog 侧可以用$readmemh或直接粘贴到参数数组。5.2 答辩现场能拿出的三张对比图一张好的对比图比十页公式管用。第一张是浮点系数和 Q15 定点系数的频响对比证明量化误差可控第二张是 MATLAB 定点模型和 ModelSim 仿真输出的时域波形对比证明 RTL 实现正确第三张是板级实测的输入输出频谱用示波器的 FFT 功能或上位机采集后画图证明阻带衰减达到指标。三张图分别对应「系数对不对」「仿真对不对」「板子对不对」逻辑链完整。对比图数据来源看什么合格标准频响对比MATLAB freqz通带纹波、阻带衰减与设计指标差 3 dB 以内时域对比MATLAB vs ModelSim逐点误差误差小于 2 个 LSB板级频谱示波器/上位机阻带抑制阻带衰减大于 40 dB5.3 可复现记录清单毕业设计最怕换一台电脑就跑不起来。把 MATLAB 脚本、系数文件、Verilog 源码、testbench、约束文件、综合报告和测试向量放在同一个目录下用相对路径引用。约束文件里写清楚时钟频率和引脚分配综合报告里保留资源占用和时序裕量。测试向量不要只留生成后的 txt把生成脚本也留下别人才能复现。答辩前用另一台机器跑一遍完整流程MATLAB 生成系数、ModelSim 仿真、综合、上板确认每一步都有输出文件。如果时间允许把 FIR 的阶数从 16 改到 64 再跑一遍验证参数化是否真的有效。这样即使老师临时要求改指标也能在十分钟内重新生成一套可演示的配置。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进