资讯动态

Verilog并行FIR滤波器设计:从架构选型到时序收敛的完整实践

发布时间:2026/10/4 1:22:58 来源:尧图企业网站定制
Verilog 并行 FIR 滤波器设计从原理到时序收敛的完整实践做 FPGA 数字信号处理两年我接手的第一个正经项目就是 16 阶并行 FIR 滤波器那时候一边对着 Xilinx FIR IP 核的手册发愁一边又被领导要求“必须用纯 Verilog 实现不能偷懒调 IP”。等自己真把并行 FIR 从头到尾撸了一遍才发现这个模块就像一个数字信号处理领域的“练功房”乘法器布局、加法树结构、流水线时序、系数定点量化一个不落全都涉及。到现在我带新人也还是让他们先把并行 FIR 吃透再碰其他复杂模块。这篇博客不打算讲百度百科上能查到的定义而是聊聊当你要用 Verilog 实现一个并行 FIR 滤波器时从架构选型、RTL 编写到仿真验证、时序收敛需要经历哪些完整链路。内容基于我实际跑过的项目涉及 48kHz 音频采样、16 阶低通滤波器、16bit 数据位宽。如果你想用 FPGA 做信号采集预处理、音频均衡、通信基带成型滤波这篇文章可以直接作为参考。1. 并行 FIR 的设计思路与架构选型1.1 为什么是并行而不是串行FIR 的核心运算就一个公式y[n] sum(h[k] * x[n-k])k 从 0 到 N-1。说白了就是一个滑动窗口的乘累加。问题是这个累加怎么算直接决定了硬件架构长什么样。串行 FIR 的思路是分时复用一个乘法器、一个加法器每个时钟周期算一个抽头N 阶滤波器就 N 个周期出一个点。好处是资源省坏处是吞吐率低。如果采样率只有几千赫兹串行架构绰绰有余但一旦采样率跑到几十兆赫兹甚至上百兆赫兹串行就顶不住了——因为输出数据率等于输入数据率你必须在两个相邻采样点之间算完所有抽头的乘加时钟频率就得是采样率的 N 倍以上。并行 FIR 的思路则不同N 个乘法器同时工作一个时钟周期完成所有抽头乘法再用加法树把 N 个乘积加起来一拍输出一个完整的 y[n]。时钟频率不用做乘法直接等于采样率就能跑。这就是为什么高速信号处理场景下并行 FIR 是默认选择。我实测过一个 16 阶、16bit 输入、16bit 系数的 FIR 滤波器串行架构工作在 48kHz 采样率时完全没问题但当我把它搬到 5MHz 采样率的超声回波信号处理时串行方案需要至少 80MHz 的时钟布线后时序很难收敛。改成并行架构后系统时钟直接降回 5MHz时序余量非常充裕。1.2 直接型结构和转置型结构怎么选并行 FIR 有两种经典实现形式直接型Direct Form和转置型Transposed Direct Form。不弄清楚两者的差异就开写代码后面会走不少弯路。直接型结构就是公式最直观的翻译先把输入 x[n] 送进一串移位寄存器Delay Line形成 x[n]、x[n-1]、x[n-2]……然后用 N 个乘法器分别乘以系数 h[0] 到 h[N-1]最后用一个加法树把所有乘积收敛成一个输出。转置型结构则把数据流方向反过来输入信号 x[n] 直接广播给所有乘法器每个乘法器自己的乘积在寄存器里累加并逐级传递。也就是说每个抽头旁边都有一个寄存器寄存器的值等于“本抽头新乘积 前一级寄存器旧值”。两者功能完全等价但工程特性差别很大对比项直接型结构转置型结构关键路径加法树深度级联路径长每级只有一个乘法器加一个加法器路径短寄存器数量输入延迟链 N 个寄存器每个抽头一个累加寄存器共 N 个时序收敛难度高阶数时困难容易适合高采样率代码结构直观好理解稍绕需要想清楚数据流我个人的建议是只要不是做研究性质的学习工程实现一律用转置型。原因很简单FPGA 时序收敛是第一位的大山转置型结构天然把关键路径压到“乘法器输出 一级加法器 寄存器”这么短不需要额外插入流水线寄存器就能在高主频下工作。我最初用直接型写了一个 32 阶滤波器在 100MHz 时钟下时序报告里 WNS 是负的后来改成转置型同一套约束直接通过了。1.3 并行度不完全等于阶数看到“并行”两个字有些新手第一反应是阶数多少就上多少个乘法器。其实“并行”还有一层含义是指并行度和阶数之间的 trade-off。你可以做全并行Full Parallel也就是 N 阶滤波器用 N 个乘法器一拍出一个点也可以做半并行Semi-Parallel比如 N 阶滤波器用 N/2 个乘法器两拍出一个点。后者在吞吐率上仍有优势相比串行但乘法器资源砍半。具体选哪种要看资源预算和速度要求。举例来说Xilinx Artix-7 系 FPGA 上做 16 阶全并行 FIR16 个乘法器大概消耗 2 个 DSP48E1 Slice每个 Slice 里有多个 DSP资源其实不紧张但在低端 CPLD 或者资源极其紧张的 SoC 里半并行也许是更现实的选择。我在一个用 Cyclone IV 的项目里因为要同时跑 8 个通道的滤波乘法器实在不够就把全并行改成了 2 倍半并行代价是每个通道输出频率降低为原来的一半但那套系统的采样率本来就低于系统时钟的一半所以完全够用。2. 滤波器系数设计与定点量化2.1 系数从哪来写 Verilog 之前得先把系数算出来。这里我不推荐手算太容易出错也没必要。常用的方法有两种第一种直接用 MATLAB 的 fdatool现在叫 Filter Designer图形化设计设定滤波器类型低通/高通/带通、阶数、截止频率、窗函数类型软件自动生成浮点系数。第二种用 Python 的 scipy.signal 库调用firwin或者remez函数。比如设计一个 16 阶、归一化截止频率 0.25对应 fs/4的低通 FIRimport numpy as np from scipy.signal import firwin, freqz taps firwin(16, 0.25, windowhamming) np.set_printoptions(precision8) for i, coef in enumerate(taps): print(fh[{i}] {coef:.8f})跑出来就是 16 个浮点系数。这些系数直接用是肯定不行的FPGA 内部是定点数得做量化。2.2 定点量化把浮点系数变成硬件能用的数系数量化的核心问题是用多少位来表示小数用多少位来表示整数。这里要引入 Q 格式的概念。Qm.n 表示 m 位整数含符号位、n 位小数。比如 Q1.15就是 1 位符号位、0 位整数、15 位小数能表示从 -1 到 1 - 2^(-15) 的范围——FIR 低通滤波器的浮点系数一般都在 ±1 之间所以 Q1.15 是常用选择。量化的操作就是把浮点系数乘以 2^n四舍五入取整然后转换成补码。以 16bit 系数位宽Q1.15为例h_fixed[k] (short)(h_float[k] * 32768 0.5)为什么加 0.5这是为了四舍五入而不是简单截断简单截断会引入直流偏移。有个新手容易忽略的点系数量化本身会带来滤波器频率响应的微小偏差。尤其当系数很小比如小于 2^(-15)时量化完直接变成 0带外抑制性能会下降。如果设计出来的滤波器要求阻带衰减特别高比如 80dB 以上建议把系数位宽提到 18bit 或 24bit。我做过一个 32 阶带通滤波器16bit 系数量化后阻带衰减比浮点模型差了差不多 6dB改成 24bit 就基本一致了。2.3 输入和输出位宽怎么定输入数据的位宽取决于你的 ADC 或者上游模块比如 16bit 是常见配置。内部乘法结果的位宽 输入位宽 系数位宽。16bit 乘 16bit 得到 32bit 结果。加法树的每一步累加都会增加位宽所以理论上并行 FIR 内部数据位宽会越来越大。但工程上不能让位宽无限涨必须做截位。截位策略有两个极端保守方案所有中间累加都保留完整位宽直到最后输出才截一次。好处是精度最高坏处是内部资源浪费大。激进方案每一步都截位。资源省但误差会逐级积累。我的做法是折中在加法树每一级截掉一些低有效位但要保证截位误差远小于最终量化噪声。具体做法下面实操章节细说。3. 并行 FIR 的 Verilog 实现3.1 转置型结构的 RTL 设计理论讲完直接上代码。这个模块的核心是一个参数化的转置型全并行 FIR我把关键参数都做成了 parameter方便复用。// ----------------------------------------------------------------------------- // Transposed Direct Form Parallel FIR Filter // Parameters: // DATA_WIDTH : input/output data width // COEF_WIDTH : coefficient width // TAP_NUM : number of filter taps (must match COEF array size) // The coefficient array is initialized in hex format (Q1.15 or arbitrary fixed) // ----------------------------------------------------------------------------- module fir_parallel #( parameter DATA_WIDTH 16, parameter COEF_WIDTH 16, parameter TAP_NUM 16 )( input wire clk, input wire rst_n, input wire en, input wire [DATA_WIDTH-1:0] din, output reg [DATA_WIDTH-1:0] dout ); // coefficient ROM / parameter // Example coefficients for a low-pass filter (Q1.15) localparam [COEF_WIDTH-1:0] COEF [0:TAP_NUM-1] { 16h00A3, 16h0114, 16h020F, 16h0350, 16h0473, 16h0513, 16h0513, 16h0473, 16h0350, 16h020F, 16h0114, 16h00A3, 16h0000, 16h0000, 16h0000, 16h0000 }; // product registers: p[i] coef[i] * din reg [DATA_WIDTHCOEF_WIDTH-1:0] prod [0:TAP_NUM-1]; // accumulator chain registers: acc[i] prod[i] acc[i1] (last tap: prod) reg [DATA_WIDTHCOEF_WIDTH4:0] acc [0:TAP_NUM-1]; integer i; always (posedge clk or negedge rst_n) begin if (!rst_n) begin for (i 0; i TAP_NUM; i i 1) begin prod[i] d0; acc[i] d0; end dout d0; end else if (en) begin // Stage 1: multiply all taps in parallel for (i 0; i TAP_NUM; i i 1) begin prod[i] din * COEF[i]; end // Stage 2: accumulation chain for (i 0; i TAP_NUM; i i 1) begin if (i TAP_NUM-1) acc[i] prod[i]; else acc[i] prod[i] acc[i1]; end // Output truncation to DATA_WIDTH dout acc[0][DATA_WIDTHCOEF_WIDTH4 -: DATA_WIDTH]; end end endmodule这段代码的核心思想分两个阶段第一阶段把 16 个乘法器同时启动每个乘法器把din和对应的系数相乘结果存入prod寄存器第二阶段从最后一个抽头开始往前一级一级累加acc[14] prod[14] acc[15]、acc[13] prod[13] acc[14]……直到acc[0]得到完整滤波结果。因为转置型结构的特性acc链上的每个寄存器在每一个采样周期都会更新一次所以不需要额外的输入延迟链。需要特别注意的是acc的位宽扩展。16bit 乘 16bit 得到 32bit而 16 个数相加最大会多出 log2(16) 4 个 bit所以我给acc定义了DATA_WIDTHCOEF_WIDTH4位宽防止累加溢出。3.2 利用系数对称性节省一半乘法器线性相位 FIR 滤波器有个很重要的性质系数关于中心点对称也就是 h[k] h[N-1-k]。这是绝大多数低通、高通、带通设计的结果只要用了线性相位设计方法比如 firwin 默认就是线性相位系数就一定对称。既然系数对称那么相同系数的两个乘法可以合并先算 x[n-k] x[k]再乘以系数 h[k]乘法器数量直接从 N 变成 N/2。这个优化在硬件上非常可观。还是 16 阶的例子全并行本来要 16 个乘法器利用对称性之后只要 8 个。改动后的核心代码片段localparam HALF_TAP TAP_NUM / 2; reg [DATA_WIDTH:0] sum_pair [0:HALF_TAP-1]; always (*) begin for (i 0; i HALF_TAP; i i 1) begin sum_pair[i] {1b0, din_delay[i]} {1b0, din_delay[TAP_NUM-1-i]}; end end注意sum_pair的位宽是DATA_WIDTH1因为两个满幅度的数据相加可能溢出 1 位。这一步做的是“先加后乘”把两个对称抽头的输入先加起来再用一个乘法器处理。代价是多了一个加法器和额外一位位宽但省下的乘法器资源是实打实的。我在实际项目中用这个技巧把 32 阶滤波器的 DSP 资源占用从 32 降到了 16节省非常明显。3.3 数据延迟链和对称结构结合把对称结构和转置型结构结合时有一个细节必须处理清楚直接型结构里输入样本天然经过延迟链所以你随时可以取到 x[n] 和 x[n-N1] 做相加但转置型结构里输入是广播的根本没有延迟链对称加法怎么实现解决方案是在转置型结构外围额外加一条输入延迟链专门用来构造对称对。这条延迟链的寄存器就是直接型的输入延迟链但它不参与乘加运算只负责给后续的对称加法提供两端的样本。这样可以在保留转置型短关键路径的同时利用系数对称性省乘法器。我的最终架构因此变成了“混合形式”输入延迟链构造对称对 转置型累加链乘加计算。代码结构如下// Input delay line for symmetric pairs reg [DATA_WIDTH-1:0] delay_line [0:TAP_NUM-1]; always (posedge clk or negedge rst_n) begin if (!rst_n) begin for (i 0; i TAP_NUM; i i 1) delay_line[i] d0; end else if (en) begin delay_line[0] din; for (i 1; i TAP_NUM; i i 1) delay_line[i] delay_line[i-1]; end end // Then use sum_pair based on delay_line[i] delay_line[TAP_NUM-1-i] // and proceed with transposed accumulation这里有一个容易踩的坑转置型结构的输出在时间上如何处理标准的转置型 FIR输入din直接参与乘法不需要延迟链但对称型混合架构因为要先配对相加会让输出比标准转置型多出若干拍延迟。这个延迟在单通道滤波场景没什么影响但在多通道同步、数据链路需要严格对齐的场景必须把这几个周期的 latency 考虑进去否则后端对齐模块会错位。3.4 参数化设计方便快速复用实际工程项目里不可能只滤波一次采样率、阶数、位宽在不同场景下都可能变化。我把 FIR 模块的参数全部做成 parameter包括数据位宽、系数位宽、抽头数、是否启用对称优化方便直接生成不同配置的滤波器。一个常用的技巧是配合 Verilog 的generate语句来做条件生成。比如用同一个模块SYMM_MODE置 1 时生成对称结构置 0 时生成标准转置型结构这样一套代码可以适配多种需求不需要维护两份几乎一样的 RTL。parameter SYMM_MODE 1; generate if (SYMM_MODE 1) begin : symm_gen // symmetric pair-based implementation end else begin : transposed_gen // standard transposed direct form end endgenerate还有一点系数数组在 Verilog-2001 里可以用parameter数组传入但在 Vivado 和 Quartus 里我建议用localparam放在模块内部或者用defparam在实例化时重新赋值。不过要注意有些综合器对参数数组的支持不太友好遇到问题时报错信息也不直观。稳妥的做法是给常用的滤波器配置各写一个封装模块内部硬编码系数对外只暴露输入输出端口然后在上层用generate选择不同的封装。4. 仿真验证与 MATLAB 联合分析4.1 Testbench 搭建让数据说话写完了 RTL下一步是仿真验证。很多新手在这里直接对着波形一顿乱看看到有信号在动就觉得“应该没问题”。我的建议是一定要做量化对比把 RTL 的输出和 MATLAB 浮点模型的参考输出放在一起比对量化误差在预设范围内才算通过。下面是一个简单但有效的 testbench 框架timescale 1ns/1ps module tb_fir_parallel; parameter DATA_WIDTH 16; parameter COEF_WIDTH 16; parameter TAP_NUM 16; parameter CLK_PERIOD 20; // 50MHz reg clk, rst_n, en; reg [DATA_WIDTH-1:0] din; wire [DATA_WIDTH-1:0] dout; // stimulus data from file integer file_in, file_out; integer scan_result; fir_parallel #( .DATA_WIDTH(DATA_WIDTH), .COEF_WIDTH(COEF_WIDTH), .TAP_NUM(TAP_NUM) ) u_fir ( .clk(clk), .rst_n(rst_n), .en(en), .din(din), .dout(dout) ); initial begin clk 0; forever #(CLK_PERIOD/2) clk ~clk; end initial begin rst_n 0; en 0; din d0; repeat(10) (posedge clk); rst_n 1; en 1; end initial begin file_in $fopen(input_samples.txt, r); file_out $fopen(output_samples.txt, w); if (file_in 0) begin $display(ERROR: cannot open input file); $finish; end while ($fscanf(file_in, %d, scan_result) 1) begin din scan_result[DATA_WIDTH-1:0]; (posedge clk); $fwrite(file_out, %d\n, $signed(dout)); end $fclose(file_in); $fclose(file_out); $finish; end endmodule测试数据从哪里来我一般先用 MATLAB 生成一个含多频分量的正弦叠加信号量化成 16bit 整数后写入input_samples.txt。然后同时用 MATLAB 的filter函数和同一个量化系数计算参考输出写入ref_output.txt。最后把 RTL 仿真输出的output_samples.txt和ref_output.txt做逐点比较计算最大绝对误差和均方误差。4.2 MATLAB 参考模型与误差分析MATLAB 端参考模型的关键代码如下% Load coefficients (must match Verilog localparam) coefs [0.0050, 0.0100, 0.0200, 0.0310, 0.0430, 0.0500, ... % Quantize coefficients to Q1.15 coef_q round(coefs * 32768) / 32768; % Generate test signal: multi-tone fs 48000; t (0:9999) / fs; sig 0.5*sin(2*pi*1000*t) 0.3*sin(2*pi*8000*t) 0.2*sin(2*pi*15000*t); % Quantize input to 16-bit sig_q round(sig * 32767); ref filter(coef_q, 1, sig_q);误差判定标准我一般定三条前 TAP_NUM 个周期属于流水线延迟时段不参与误差比较。稳定之后的最大绝对误差不能超过输出量化步长1 个 LSB的 2~3 倍。因为 RTL 输出截位本身就有 0.5 LSB 的误差加上累加过程可能多一两个 LSB 的舍入误差这是合理范围。时域对齐RTL 输出的群延迟要符合理论值。线性相位 FIR 的群延迟是 (N-1)/2 个采样周期再加上内部流水线 latency加起来的总延迟要和仿真波形吻合。我第一次做这个验证时发现 RTL 输出比 MATLAB 参考结果整体大了一倍找了一晚上没头绪后来才发现是 MATLAB 里滤波器系数没有归一化而 Verilog 里系数转成定点数时正好把量程满打满算。这样的小错误如果不建立量化对比机制光靠看波形几乎不可能发现。4.3 用 Vivado/Quartus 做等价性验证仿真是功能级的跑通了只能说明逻辑行为正确不能代表综合后的网表行为也正确。严谨的做法是综合后还要跑一遍 gate-level 仿真后仿真确认时序约束下的输出仍然满足要求。不过对于 FIR 这种纯组合逻辑加寄存器的模块前仿真和后仿的差异主要集中在一个地方转置型累加链里如果 MAR最大时钟频率不够某些寄存器的建立时间可能不满足导致后仿真出现随机跳变。这个时候在前仿真里是永远查不出来的。所以我的流程是前仿真过功能。加时序约束跑综合和布局布线。看时序报告WNS 必须为正。输出一个小的测试向量跑后仿真确认输出和 MATLAB 参考仍然一致。如果后仿真出现错误优先检查的是acc链上的寄存器是否因为组合路径太长导致时序问题那就需要拆分累加链插入中间寄存器。5. 时序优化与资源调优实战5.1 关键路径分析与加法树拆分转置型结构虽然关键路径短但阶数很高比如 64 阶以上的时候累加链本身也可能变成瓶颈。原因是acc[i1]到acc[i]的路径上有一个加法器链式结构会让最前端的acc[0]等很久。解决方式是把链式累加改成加法树。转置型结构用链式累加是因为它天然适合数据流传递但如果从时序角度考虑可以把整个累加过程拆成多级每级两两相加相当于把 log2(N) 级深度变成 log2(N) 的加法树。具体到 Verilog 实现我会这样调整第一级所有prod寄存器两两相加得到 N/2 个和第二级再两两相加得到 N/4 个和以此类推直到得到最终结果。每一级之间插入寄存器打拍。这样看起来多用了几个寄存器但把每一级组合逻辑的路径长度都控制在一到两个加法器以内。// Stage 1: pair-wise add reg [WIDTH1:0] sum_stage1 [0:TAP_NUM/2-1]; always (posedge clk or negedge rst_n) begin if (!rst_n) begin for (i 0; i TAP_NUM/2; i i 1) sum_stage1[i] d0; end else begin for (i 0; i TAP_NUM/2; i i 1) sum_stage1[i] prod[2*i] prod[2*i1]; end end // Stage 2: pair-wise add again reg [WIDTH2:0] sum_stage2 [0:TAP_NUM/4-1]; // ... similar pattern代码会比链式累加长一些但换来的是时序收敛的确定性。在 Xilinx FPGA 上如果乘法器用的是 DSP48E1那么加法器天然可以打散到 DSP 的 pre-adder 或者后级流水线里配合时序约束效果更好。5.2 DSP Slice 映射与乘法器资源控制FPGA 里的乘法器并不是真的一堆 LUT 拼出来的而是查用专门的 DSP Slice。以 Xilinx 7 系列为例一个 DSP48E1 可以完成一个 25x18 的乘法也可以完成乘加运算MACC。这意味着转置型 FIR 的“乘法累加”结构天然适合映射到 DSP48E1 上因为每个抽头可以打包成一个 DSP Slice 做乘加。这里有个经验值16 阶 16bit 全并行 FIR只用 8 个 DSP Slice对称优化后在 Artix-7 上资源占比可以忽略不计。但如果阶数涨到 256 阶16bit 输入带宽 50MHzDSP 用量就要 128 个以上这时候必须关注 DSP 是否够用。资源不够时通常的优先级策略是第一优先利用系数对称性免费优化不牺牲性能。第二优先改用半并行结构乘法器数量降一半代价是吞吐率降一半。第三优先降低系数位宽或输入位宽。16bit 降到 12bit乘法器资源能省很多但信噪比会下降。这三个策略可以叠加使用实际工程里我见过 256 阶 FIR 最后只用 32 个 DSP Slice 做出来的就是“对称化 4 倍半并行 12bit 位宽”的组合拳。5.3 Vivado 时序约束注意事项在 Vivado 里做时序约束时FIR 模块本身不需要太复杂的 XDC只要好好约束时钟就行。但有几个细节容易忽略第一输入输出延迟约束要给足。如果din来自 ADC 引脚那set_input_delay必须参考 ADC 的数据手册设置否则 Vivado 会把输入端的路径约束得过松或过紧。过松可能导致实际上板抓到的数据不稳定过紧则可能导致布线不收敛。第二多周期路径约束不要乱加。FIR 的每个输入都被延迟链使用严格来说是每个周期都要采一次数不存在多周期路径。有的工程师为了让时序通过盲目加set_multicycle_path结果是把错误藏起来了。我见过一个案例有人给 FIR 输入路径加了 2 周期约束仿真看不出问题上板后数据偶尔错一拍排查了很久才发现是约束出了问题。第三时钟抖动和不确定度设置。如果有 MMCM/PLL 生成时钟Vivado 会自动计算抖动但如果是外部时钟直接驱动最好手动设置set_clock_uncertainty给时序余量留一点缓冲。6. 常见问题与调试实录6.1 输出全是 0复位和使能信号的坑这个问题在调试中最常见。如果仿真输出一直是 0先别急着查算法先确认模块有没有走出复位状态。rst_n低有效复位很多新手在 testbench 里给rst_n一个固定高电平模块完全没有复位内部寄存器初始值是 X 或不定态加法结果最终变成 0 也不奇怪。还有一个坑是en信号。我在一个多通道设计里把en接成了某个模块的握手信号而非固定高电平结果 FIR 只有握手有效的那几个周期在算其他时间输出保持旧值。这种问题表现形式是“输出时有时无”而且往往只在特定数据流下出现。调试方法很简单先用一个计数器确认en拉高的频率和数据流是否一致。6.2 定点量化后噪声过大如果 MATLAB 参考模型输出很平滑但 RTL 输出噪声明显多半是截位策略的问题。我最早做的版本是每一级加法都直接截位输出噪声在那个项目里还不明显后来换了个高动态范围的信号输出底噪立刻涨了。后来我改成中间累加保留全部位宽只在最后输出时做一次截位。这样做虽然内部数据位宽比较大但 DSP 资源其实不敏感因为 FPGA 的 DSP 乘法器输出本身就有 48bit 宽内部 32bit 累加完全放得下。另外截位时不要直接把低 bit 砍掉。正确做法是加上当前位的半值rounding再截位。Verilog 里可以这样写dout (acc[0] (1 (SHIFT_BIT-1))) SHIFT_BIT;这里的SHIFT_BIT是你最终输出要缩位的位数。加上 1 左移SHIFT_BIT-1相当于加上二进制的 0.5 LSB实现四舍五入而不是截断。6.3 时序收敛不了的第一反应看综合报告里用了几层逻辑遇到时序违例我的排查顺序是看综合后的 schematic 或者 LUT 层级报告确认关键路径上是一长串 LUT 还是 DSP 直连。如果是一长串 LUT大概率是乘法器没有被正确推断成 DSP而是被综合成了 LUT 乘法器。这种问题通常是因为写法不标准比如用了*号的变量位宽不一致综合器没有识别出来。解决办法是在综合属性里加(* use_dsp yes *)。如果是 DSP 到 DSP 的连接出了问题检查是不是累加链太长考虑换成加法树结构。我在一个 64 阶滤波器上就遇到这种情况Vivado 综合出来关键路径 7ns 多时钟周期要求 5ns根本收不了。后来发现乘法器全被推断成 LUT 乘法器了加了一行(* use_dsp yes *)之后关键路径一下降到 3ns 以内。6.4 多通道 FIR 的资源复用技巧最后分享一个实战技巧如果你要处理 8 个通道的低速数据完全可以只例化一个 FIR 模块通过时分复用实现多通道滤波。思路是把 8 个通道的采样点排队用 8 倍于单通道采样率的时钟送入同一个 FIR输出后按通道号拆分。这样做的资源开销是单个 FIR比例化 8 个 FIR 省得多。但要注意输入数据切片和输出重组需要额外的 buffer 和状态机如果通道数不多代码复杂度提升还在可控范围内。还有一个变体是并行多通道 FIR即用 N 个 FIR 核分别处理 N 个通道但共享系数 ROM这种适合通道间需要严格独立延时的场景。我的实操体会最后说点代码之外的感受。FIR 滤波器大概是数字信号处理里最“小而美”的模块功能不复杂但它逼着你去想清楚数据流、时序和资源三个维度的问题。我见过很多同学RTL 仿真通过就急着往下走结果到上板调试阶段被时序或者截位误差折磨到崩溃。这个模块最大的价值不是那个滤波效果本身而是它训练了你从“行为级”到“电路级”思考的能力——你写的每一行 Verilog最终都要落在 LUT、FF、DSP 这些物理资源上而并行 FIR 恰好是这几个资源配合最典型、最直观的场景。如果你正在接触这个模块我建议你动手之前先花半天时间把架构问题想透是直接型还是转置型是全并行还是半并行系数怎么量化输出怎么截位。这些决策会直接影响你后面所有的代码调试和上板性能。想清楚了再写代码大概率一次流片就能过。

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

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

免费获取报价 →
↑