简介:本资源面向FPGA开发初学者与数字信号处理方向的学生、工程师,提供一套完整的FIR滤波器设计实践方案,帮助读者理解从DDS正弦信号产生、DA转换到FIR滤波的完整信号链,掌握滤波器系数导出与硬件仿真验证流程。压缩包共481个文件,约6.3MB,以vhd、v、tdf等HDL源码文件为主,配合dat、mif波形数据文件、cdb、hdb等QuartusII工程与编译中间文件,以及qpf、qsf工程配置文件和仿真脚本,另含系统框图、Matlab导出系数说明与Modelsim仿真文档,结构清晰便于按模块查阅。资源涵盖DDS原理、波形存储、FIR直接型与线性相位型设计方法等核心知识点,并附有完整QuartusII工程与仿真文件,可直接编译运行、观察滤波前后波形对比,适合作为课程设计、实验报告或自学练手的参考范例。目前已有1559人学习下载,具备一定的参考与复用价值。
1. 基于FPGA的FIR滤波器设计:从系数生成到板级验证的完整路径
很多做 FPGA 信号处理的朋友,第一次接触 FIR 滤波器时都会卡在同一个地方:MATLAB 里仿真波形完美,下载到板子上却输出一堆毛刺,或者干脆没有输出。问题往往不在算法本身,而在系数定点化、位宽截断和时序约束这三个环节中的某一个。基于 FPGA 的 FIR 滤波器设计,核心要解决的就是把浮点系数变成硬件能跑的定点数,再用乘累加结构在时钟节拍下算出来,同时保证时序收敛。这套方案适合做数据采集、音频处理、通信基带、传感器信号调理的工程师,也适合正在做 FPGA 项目实战的学生。下面从选型、实现到避坑,把这条路走一遍。
2. 先想清楚:为什么用 FPGA 做 FIR,而不是 DSP 或 MCU
2.1 FIR 在硬件里的天然优势
FIR 滤波器的数学本质是一个卷积:y[n] = Σ h[k]·x[n-k],k 从 0 到 N-1。这个结构有两个特点——没有反馈回路,天然适合流水线;系数固定,可以预先量化成定点数。FPGA 做这件事的优势在于并行度:如果时钟跑 100 MHz,每个周期能完成一次乘累加,那 64 阶滤波器只需要 64 个时钟周期就能出一个结果,等效采样率可以做到很高。相比之下,MCU 做 64 阶 FIR 可能要几百个周期,DSP 虽然快但受限于核数和主频。
常见做法是用 FPGA 内部的 DSP Slice 来做乘法,用寄存器打拍做加法树。Xilinx 7 系列每个 DSP48E1 可以做一个 25×18 的乘法,Altera Cyclone IV 的嵌入式乘法器是 18×18。选型时要看你的阶数和采样率:阶数少、采样率低,用串行乘累加结构省资源;阶数多、采样率高,用全并行或半并行结构。
2.2 三种实现结构的取舍
| 结构类型 | 乘法器用量 | 吞吐率 | 适用场景 |
|---|---|---|---|
| 全并行 | N 个 | 每周期一个输出 | 高阶、高速、资源充足 |
| 半并行 | N/M 个 | 每 M 周期一个输出 | 折中方案,最常用 |
| 串行 | 1 个 | 每 N 周期一个输出 | 低速、省资源 |
我一般推荐半并行结构,比如 64 阶滤波器用 8 个乘法器,每个乘法器分时复用 8 次。这样资源占用可控,时序也容易收敛。全并行虽然快,但 64 个乘法器会把 DSP 资源吃光,布线压力大,新手很容易在这里翻车。
2.3 系数从哪来:MATLAB 生成与定点化
FIR 系数通常用 MATLAB 的fir1或fdatool生成。关键一步是定点化:把浮点系数乘以 2^Q,再取整。Q 的选择直接影响滤波器的幅频响应。Q 太小,阻带衰减不够;Q 太大,位宽爆炸,乘法器资源不够。
% MATLAB 生成 64 阶低通 FIR 系数,截止频率 0.2 N = 64; fc = 0.2; h = fir1(N-1, fc); % 定点化:Q15 格式,系数范围 [-1, 1) Q = 15; h_fixed = round(h * 2^Q); % 检查定点化后的频率响应 [H_f, w_f] = freqz(h, 1, 1024); [H_fixed, w_fixed] = freqz(h_fixed / 2^Q, 1, 1024); plot(w_f/pi, 20*log10(abs(H_f)), 'b', w_fixed/pi, 20*log10(abs(H_fixed)), 'r--'); legend('浮点', 'Q15定点');这段代码先生成浮点系数,再量化到 Q15。round是四舍五入,比直接截断的误差小。画图对比是为了确认定点化后阻带衰减没有明显恶化。如果恶化超过 3 dB,就要考虑提高 Q 值或者增加阶数。
提示:系数定点化后要做归一化,确保通带增益不超过 1,否则输出会溢出。
3. 手把手实现:从 Verilog 乘累加到 Vivado 仿真
3.1 串行乘累加结构的 Verilog 实现
先从一个最基础的串行结构入手,理解 FIR 的硬件映射。下面是一个 8 阶 FIR 的串行实现,每个时钟周期算一个乘累加。
module fir_serial #( parameter DATA_WIDTH = 16, parameter COEFF_WIDTH = 16, parameter TAP_NUM = 8 )( input wire clk, input wire rst_n, input wire signed [DATA_WIDTH-1:0] x_in, input wire x_valid, output reg signed [DATA_WIDTH+COEFF_WIDTH+3:0] y_out, output reg y_valid ); // 移位寄存器,存储最近 8 个输入样本 reg signed [DATA_WIDTH-1:0] shift_reg [0:TAP_NUM-1]; // 系数 ROM,实际项目中用 ROM IP 或参数数组 reg signed [COEFF_WIDTH-1:0] coeff [0:TAP_NUM-1]; // 状态机:IDLE -> CALC -> DONE reg [1:0] state; reg [3:0] tap_cnt; reg signed [DATA_WIDTH+COEFF_WIDTH-1:0] acc; integer i; initial begin // 示例系数:低通,实际用 MATLAB 生成 coeff[0] = 16'sd1; coeff[1] = 16'sd3; coeff[2] = 16'sd6; coeff[3] = 16'sd10; coeff[4] = 16'sd10; coeff[5] = 16'sd6; coeff[6] = 16'sd3; coeff[7] = 16'sd1; end always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= 2'd0; tap_cnt <= 4'd0; acc <= 0; y_out <= 0; y_valid <= 0; for (i = 0; i < TAP_NUM; i = i + 1) shift_reg[i] <= 0; end else begin case (state) 2'd0: begin // IDLE,等待新样本 y_valid <= 0; if (x_valid) begin // 移位 for (i = TAP_NUM-1; i > 0; i = i - 1) shift_reg[i] <= shift_reg[i-1]; shift_reg[0] <= x_in; acc <= 0; tap_cnt <= 0; state <= 2'd1; end end 2'd1: begin // CALC,逐拍乘累加 acc <= acc + shift_reg[tap_cnt] * coeff[tap_cnt]; if (tap_cnt == TAP_NUM - 1) begin tap_cnt <= 0; state <= 2'd2; end else begin tap_cnt <= tap_cnt + 1; end end 2'd2: begin // DONE,输出结果 y_out <= acc; y_valid <= 1; state <= 2'd0; end endcase end end endmodule这段代码的核心是状态机控制的三段式流程:IDLE 等新样本,CALC 逐拍算乘累加,DONE 输出。shift_reg是移位寄存器,每来一个新样本就整体右移。acc是累加器,位宽要足够大,防止溢出。这里取DATA_WIDTH+COEFF_WIDTH+3,多留 3 位余量。
参数说明:DATA_WIDTH是输入数据位宽,通常 12 到 16 位;COEFF_WIDTH是系数位宽,Q15 格式就是 16 位;TAP_NUM是阶数,必须是 2 的幂次方便于地址生成。x_valid是输入有效信号,每 N 个时钟周期来一次。
3.2 用 Vivado 跑仿真:Testbench 怎么写
写完 RTL 必须仿真。下面是一个简单的 testbench,给一个阶跃信号,看输出是否平滑。
`timescale 1ns / 1ps module tb_fir_serial; reg clk, rst_n; reg signed [15:0] x_in; reg x_valid; wire signed [35:0] y_out; wire y_valid; fir_serial #( .DATA_WIDTH(16), .COEFF_WIDTH(16), .TAP_NUM(8) ) uut ( .clk(clk), .rst_n(rst_n), .x_in(x_in), .x_valid(x_valid), .y_out(y_out), .y_valid(y_valid) ); // 100 MHz 时钟 initial clk = 0; always #5 clk = ~clk; integer i; initial begin rst_n = 0; x_in = 0; x_valid = 0; #100 rst_n = 1; // 每 20 个周期给一个样本 for (i = 0; i < 50; i = i + 1) begin @(posedge clk); x_valid <= 1; x_in <= (i < 10) ? 0 : 16'sd1000; // 第 10 个样本后阶跃 @(posedge clk); x_valid <= 0; repeat(19) @(posedge clk); end #1000 $stop; end endmoduleTestbench 的关键是模拟真实采样节奏:每 20 个时钟周期给一个有效样本,其余时间x_valid拉低。阶跃信号从第 10 个样本开始,观察输出是否逐渐上升而不是突变。仿真时重点看y_valid的时序,确保它在acc稳定后才拉高。
3.3 时序约束与综合报告怎么看
仿真过了不代表板子能跑。必须加时序约束,让工具知道时钟频率和输入输出延迟。
# Vivado XDC 约束示例 create_clock -period 10.000 -name sys_clk [get_ports clk] # 输入延迟:假设外部器件在时钟沿后 2ns 给出数据 set_input_delay -clock sys_clk -max 2.000 [get_ports x_in] set_input_delay -clock sys_clk -min 1.000 [get_ports x_in] # 输出延迟 set_output_delay -clock sys_clk -max 3.000 [get_ports y_out] set_output_delay -clock sys_clk -min 1.000 [get_ports y_out]综合后打开 Timing Report,重点看 WNS 和 WHS。如果 WNS 为负,说明建立时间不满足,需要降低时钟频率或者插入流水线寄存器。常见做法是在乘法器后面加一级寄存器,把组合逻辑打断。
注意:串行结构的时序压力主要在乘法器和累加器之间的路径。如果 WNS 差得不多,可以在
acc <= acc + ...这条路径上插一级流水。
4. 避坑指南:FIR 滤波器调试中最容易翻车的 5 个地方
4.1 输出一直是零或者满量程
现象:仿真波形里y_out要么全是 0,要么直接跳到最大值不动。
原因:系数定点化时没有做归一化,或者累加器位宽不够导致溢出。另一个常见原因是shift_reg没有正确复位,初值全是 X,乘出来也是 X。
解决:先检查系数之和是否接近 2^Q。如果系数和远大于 2^Q,输出必然溢出。累加器位宽至少要是DATA_WIDTH + COEFF_WIDTH + log2(TAP_NUM)。复位时用for循环把所有寄存器清零,不要只复位state。
4.2 滤波效果和 MATLAB 对不上
现象:板子输出的波形和 MATLAB 仿真差很多,阻带衰减明显不够。
原因:定点化误差累积。Q 值选小了,系数精度不够;或者输入数据没有做符号扩展,负数被当成正数处理。
解决:把 Q 值提高到 16 或 18,重新生成系数。输入数据在进入乘法器前要做符号扩展到DATA_WIDTH+COEFF_WIDTH位。用$signed()显式转换,不要依赖隐式规则。
4.3 时序不收敛,WNS 负很多
现象:综合实现后 Timing Report 一片红,WNS 负几纳秒。
原因:组合逻辑路径太长。串行结构里acc + shift_reg[tap_cnt] * coeff[tap_cnt]这一条路径包含了乘法、加法、选择器,延迟很大。
解决:在乘法器输出后插一级寄存器,把乘和加分成两个时钟周期。或者改用半并行结构,用多个乘法器并行算,减少每个周期的串行级数。
4.4 采样率上不去,输出断断续续
现象:输入采样率一提高,输出就出现丢点或者错位。
原因:串行结构每个样本需要 N 个时钟周期处理,如果采样率接近时钟频率除以 N,就没有余量。
解决:改用半并行或全并行结构。半并行用 M 个乘法器,每个样本只需要 N/M 个周期。或者提高系统时钟频率,给处理留更多时间。
4.5 换 FPGA 型号后结果不对
现象:在 A 型号上跑得好好的,换到 B 型号输出就变了。
原因:不同型号的 DSP Slice 位宽和符号处理方式不同。比如 Xilinx DSP48 支持预加器,Altera 的乘法器不支持。系数位宽超过 DSP 原生位宽时,工具会自动拆分,引入额外延迟。
解决:换型号后重新跑综合和时序分析,检查 DSP 推断报告。如果系数位宽超过 18 位,考虑用两块 DSP 拼接或者降低 Q 值。
5. 进阶技巧:用 Vivado FIR Compiler IP 核快速迭代
5.1 IP 核配置要点
手写 RTL 适合理解原理,但项目实战里我一般直接用 Vivado 的 FIR Compiler IP。配置时重点看几个参数:Filter Type 选 Single Rate,Coefficient Source 选 COE File,把 MATLAB 生成的定点系数存成.coe文件。Number of Coefficients 填阶数,Input Data Width 和 Coefficient Width 按实际填。
# 用 Tcl 脚本创建 FIR Compiler IP create_ip -name fir_compiler -vendor xilinx.com -library ip -version 7.2 -module_name fir_compiler_0 set_property -dict [list \ CONFIG.Filter_Type {Single_Rate} \ CONFIG.Coefficient_Source {COE_File} \ CONFIG.Coefficient_File {./coeffs.coe} \ CONFIG.Number_Of_Coefficients {64} \ CONFIG.Input_Data_Width {16} \ CONFIG.Coefficient_Width {16} \ CONFIG.Output_Width {32} \ CONFIG.Quantization {Integer_Coefficients} \ ] [get_ips fir_compiler_0]这段 Tcl 脚本直接生成 IP 核,省去 GUI 点来点去。Quantization选Integer_Coefficients表示系数已经是整数,不需要工具再量化。Output_Width设 32 位,留足余量。
5.2 用 ILA 抓实时波形验证
IP 核跑起来后,用 ILA 抓x_in、y_out和y_valid。触发条件设在y_valid上升沿,看输出是否在几个周期后稳定。如果发现输出有毛刺,检查 IP 核的aclk和aresetn是否同步。ILA 的采样深度至少设 1024,才能看到完整的滤波过渡过程。
5.3 资源与功耗的平衡
FIR Compiler 支持多种架构:Systolic Multiply Accumulate、Transposed Multiply Accumulate、Pipelined Multiply Accumulate。Systolic 结构资源少但延迟大,Transposed 结构吞吐率高但乘法器多。我一般先用 Systolic 跑通,再根据时序报告决定要不要换 Transposed。
| 架构 | DSP 用量 | 延迟 | 适用场景 |
|---|---|---|---|
| Systolic MAC | 最少 | 高 | 低速、省资源 |
| Transposed MAC | 中等 | 低 | 高速、时序紧 |
| Pipelined MAC | 最多 | 最低 | 超高速、资源充足 |
最后说一个我踩过的坑:有一次用 IP 核做 128 阶滤波器,系数文件里有一行多了个空格,综合直接报错但提示信息很模糊。后来养成习惯,.coe文件用脚本生成,每行格式固定,再也没出过问题。做 FPGA 滤波器设计,细节决定成败,系数文件、位宽、时序约束这三样东西,每次都要逐行检查。希望帮到你。
本文还有配套的精品资源,点击获取