基于FPGA的FIR滤波器设计:从MATLAB系数定点化到Vivado板级验证全流程
发布时间:2026/10/28 18:16:15 锦皓数字建站

简介本资源面向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 或 MCU2.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_WIDTHCOEFF_WIDTH3: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_WIDTHCOEFF_WIDTH-1:0] acc; integer i; initial begin // 示例系数低通实际用 MATLAB 生成 coeff[0] 16sd1; coeff[1] 16sd3; coeff[2] 16sd6; coeff[3] 16sd10; coeff[4] 16sd10; coeff[5] 16sd6; coeff[6] 16sd3; coeff[7] 16sd1; end always (posedge clk or negedge rst_n) begin if (!rst_n) begin state 2d0; tap_cnt 4d0; 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) 2d0: 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 2d1; end end 2d1: begin // CALC逐拍乘累加 acc acc shift_reg[tap_cnt] * coeff[tap_cnt]; if (tap_cnt TAP_NUM - 1) begin tap_cnt 0; state 2d2; end else begin tap_cnt tap_cnt 1; end end 2d2: begin // DONE输出结果 y_out acc; y_valid 1; state 2d0; end endcase end end endmodule这段代码的核心是状态机控制的三段式流程IDLE 等新样本CALC 逐拍算乘累加DONE 输出。shift_reg是移位寄存器每来一个新样本就整体右移。acc是累加器位宽要足够大防止溢出。这里取DATA_WIDTHCOEFF_WIDTH3多留 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 : 16sd1000; // 第 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_WIDTHCOEFF_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 RateCoefficient 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 滤波器设计细节决定成败系数文件、位宽、时序约束这三样东西每次都要逐行检查。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。