☰
并行FIR滤波器设计实战:Verilog实现与FPGA优化
2026/10/4 7:41:35 网站建设 项目流程

前阵子调试一块做中频信号处理的FPGA板子,采样率要求50MSPS,通带内纹波不能超过0.5dB,输出延迟还不能太大。第一反应就是上FIR滤波器,但用串行结构一个时钟才完成一次乘加,50M采样率勉强能跑,换成100M采样的项目立马吃紧。于是我把思路转到并行FIR上——用面积换吞吐率,一个时钟周期把整条滤波流水线全部算完。这篇文章就把我做并行FIR滤波器设计的完整过程摊开讲:从指标推导、系数计算、架构选型到Verilog实现、仿真验证、板级调试的坑,一条龙梳理清楚。

标题里的三个关键词——Verilog、FIR滤波器、并行设计——正好对应三条主线:用哪种硬件描述语言实现(Verilog)、要实现的算法是什么(FIR滤波)、采用什么样的电路结构(并行架构)。这套思路特别适合FPGA上做中频/基带信号处理、音频降采样、高速数据采集系统的同学参考,也适合刚入门数字信号处理、想搞明白“并行到底比串行快在哪”的硬件工程师。我尽量把原理讲得通俗,代码也给到能直接跑仿真的程度。

1. 项目概述:并行FIR滤波器解决什么问题

1.1 从数字滤波器说起:为什么选FIR

数字滤波器无非两大类:FIR(有限冲激响应)和IIR(无限冲激响应)。FIR的核心表达式就是卷积:对于N阶滤波器,每个输出采样点是当前和前N-1个输入采样与N个系数分别相乘后累加的结果。这看起来就是一堆乘法和加法,但FIR有两个IIR比不了的优势:一是线性相位,只要系数对称,滤波器对所有频率分量的延迟都一致,信号波形不会失真,这在通信和音频领域几乎是刚需;二是FIR天生稳定,没有反馈回路,不会出现振荡发散的问题。

FIR的短板也显而易见:要达到陡峭的过渡带,阶数N会很大,乘加运算量跟着暴涨。比如过渡带要压到1MHz以内,阶数轻松上百,串行结构一个周期才算一次乘加,采样率稍微上去就扛不住。解决办法就是并行——把N个乘法器全部铺开,一个时钟周期同时算完所有乘积,再用加法树累加。这就是标题里“并行设计”的出发点。

拿生活里的例子类比:串行结构像一个人挨个处理排队订单,一次只能处理一个;并行结构像开N个窗口同时接单,最后汇总到总台。吞吐率直接变成N倍,代价是硬件资源也大概变成N倍。所以并行FIR的核心矛盾就是面积和速度的取舍,设计目标就是在资源可接受的范围内把吞吐率拉满。

1.2 并行、串行与半并行:三种架构怎么选

选择架构之前,先搞清楚各自的路数。串行FIR最省资源,一个乘法器加一个累加器就能转起来,N阶滤波器需要N个时钟周期才输出一个点,适合采样率低、逻辑资源紧张的场景,比如传感器慢速采集、单通道音频处理。半并行是折中方案,用M个乘法器(M < N),M拍内完成全部乘加,速度和资源介于两者之间,适合需要一点性能但不想全量铺乘法器的场景,典型做法是多通道时分复用一套乘加阵列。

并行FIR则把N个乘法器一次性铺开(利用系数对称性可以减到N/2),所有输入样本同时参与运算,每来一个时钟就输出一个新的滤波结果,吞吐率和输入采样率完全一致,不受滤波器阶数影响。代价是使用的DSP Slice和LUT数量明显上升。实际项目中怎么选?我自己的经验是:采样率超过50MSPS、FIR阶数超过32阶、延迟要求严格这三个条件满足任意两条,就直接上并行;资源紧张、采样率只有几百K到几M的,串行性价比更高。

2. 滤波器指标推导与系数计算:动手写代码前必须做的事

2.1 用窗函数法设计16阶低通滤波器

很多同学拿到FPGA板子第一件事就是打开Vivado写代码,结果到综合的时候发现系数不对、指标不达标,回头又一版一版改。我个人的习惯是:先算系数,再写RTL。系数是整个滤波器的心脏,架构再好,系数算错全白搭。

这篇文章为了代码演示方便,设计一个16阶(16个抽头)低通FIR,采样率fs=20MHz,通带截止5MHz,阻带起始9MHz,过渡带Δf=4MHz,阻带衰减目标50dB。选择Hamming窗,因为Hamming窗主瓣窄、旁瓣衰减约53dB,工程里最常用。估算阶数的经验公式:

N ≈ 3.3 × (fs / Δf) = 3.3 × (20 / 4) = 16.5

取N=16,正好是偶数阶。归一化截止频率取通带和阻带的中间点,即fc = (5+9)/2 = 7MHz,对应数字角频率:

ω_c = 2π × (7 / 20) = 0.7π rad/sample

理想低通滤波器的无限冲激响应为:

h_d[n] = [sin(ω_c(n - 7.5))] / [π(n - 7.5)]

其中7.5是16阶滤波器的群延迟中心((16-1)/2=7.5)。用Hamming窗w[n] = 0.54 - 0.46cos(2πn/15)截断,得到最终系数h[n] = h_d[n] × w[n]。实际计算我推荐用Python的NumPy或者MATLAB的fir1函数,直接写h = fir1(15, 0.7),一行代码就出来16个系数,省时省力。

算出来的系数具有对称性,h[0]=h[15]、h[1]=h[14]……所以真正需要存储和参与乘法运算的只有8个独立系数。这正好引出后面的对称结构优化。

2.2 系数定点化与量化误差控制

浮点系数不能直接拿来写Verilog,FPGA里做乘法的都是定点数,所以要把浮点系数转成定点。这一步最常见的坑是量化误差太大导致频率响应变形。我自己的方法是:先对所有系数做归一化(除以最大绝对值),让最大系数接近但不超过1,然后乘以2^(Q-1),再四舍五入取整。Q是系数位宽,工程里我一般用16位有符号数,既有足够精度,又不会太浪费DSP资源。

以8个独立系数为例,归一化后数值范围在-0.05到1之间,乘以32767再取整,得到类似下面的一组16位有符号十六进制系数:

系数索引归一化浮点值16位定点表示(h)
h[0]-0.05210xFF2E
h[1]-0.03180xFFE4
h[2]0.07780x02B6
h[3]0.19700x04C9
h[4]0.28210x0662
h[5]0.29620x06A8
h[6]0.21480x052F
h[7]0.10380x01E8

如果发现量化后幅频响应在阻带衰减不够,不要急着改架构,先尝试两个办法:一是加大系数位宽到24位或32位;二是对系数组整体加一个微小的比例缩放因子,让量化误差在频域上的分布更均匀。量化后务必用MATLAB或者Python重新画一遍幅频响应曲线,和浮点理想曲线叠在一起对比,确认带内波动和阻带衰减还在指标范围之内。这一步做扎实了,后面的时序验证和板级测试基本不会出大岔子。

3. 并行FIR架构的实现要点:从结构到细节

3.1 对称系数结构:省一半乘法器的关键

前面提到FIR系数在低通、带通、高通等线性相位应用里是对称的,这个特性在硬件上非常值钱。原始的直接型并行FIR需要16个乘法器,每个输入样本和对应系数相乘,然后全部相加。16个乘法器对FPGA资源不算伤筋动骨,但如果阶数涨到64阶、128阶,乘法器数量会直接压垮DSP Slice资源。

利用对称性后,先把一对对称位置的输入样本相加,再和同一个系数相乘。以16阶为例,h[0]=h[15],那么输出表达式中的两项可以合并:

y = h[0]×x[15] + h[1]×x[14] + ... + h[14]×x[1] + h[15]×x[0] = h[0]×(x[15] + x[0]) + h[1]×(x[14] + x[1]) + ...

这样乘法器的数量从16个直接减到8个,加法器增加8个。FPGA里加法器用的是LUT,乘法器用的是DSP Slice,而DSP Slice是稀缺资源,用LUT加法换DSP乘法非常划算。到了并行结构里尤其如此:串行结构只有一个乘法器,多出来的这些倒不敏感,但并行结构乘法器数量是线性的,省一半就是省几十个DSP Slices。

需要注意的是,对称求和这一步要把位宽扩展1位,因为两个16位有符号数相加,结果范围扩大一倍,需要17位才能无溢出表示。这个细节很多初学者容易忽略,直接拿16位去接乘法器,结果高位被截掉,输出波形全是毛刺。

3.2 流水线插入策略:让时序收敛更容易

并行FIR把16个乘加堆在一个时钟周期里完成,组合逻辑路径会非常长:数据进来,先走移位寄存器,再走对称加法器,再走乘法器,最后走累加加法树。以Artix-7这样的主流中端FPGA为例,系统时钟超过100MHz后,这条路径的时序大概率收敛不了,要么出现setup违例,要么就得把时钟压到很低的频率。

解决思路是插入流水线寄存器,把长组合路径切分成多级短路径。我的做法是标准三级流水:

  • 第一级:对称加法寄存器。输入样本先做对称位置两两相加,结果打一拍,进入寄存器。
  • 第二级:乘法结果寄存器。17位对称和乘以16位系数,结果(33位)打一拍。
  • 第三级:累加结果寄存器。8个乘法结果用加法树汇总,结果打一拍后输出。

这样每一级组合逻辑只有“一个加法”或者“一个乘法+一个加法”的深度,时序裕量非常充足。代价是输出会延迟3个时钟周期,但FIR本来就有群延迟,额外增加3个周期的流水线延迟在很多场景里完全可以接受。如果做高精度音频或者雷达测距这类对延迟极敏感的系统,可以把流水线减到两级,或者把加法树做成全并行形式,用更多LUT换更短的延迟。

流水线插入的位置不是随便定的,核心原则是寄存器两侧的组合逻辑深度尽量均衡。如果把乘法器和对称加法放一级,加法树放一级,前者路径深度明显大于后者,时序瓶颈还是卡在前级。所以调试时序的时候,看时序报告里最差的路径在哪个模块,往往就能反过来推断哪一级流水切得不合理。

4. Verilog可综合实现:完整代码与仿真验证

4.1 顶层模块代码:移位寄存器、对称加法、流水线MAC

下面给出一个可以直接拿去综合和仿真的并行FIR滤波器Verilog实现。模块参数化了数据位宽、系数位宽、抽头数和累加器位宽,这样换一组系数、改一个阶数不需要动代码结构,改参数就行。

// ============================================================= // 并行 FIR 低通滤波器 // 特征:16阶、对称系数、3级流水线、全并行MAC // 输入:16bit有符号,输出:36bit有符号(全精度) // ============================================================= module fir_parallel #( parameter DATA_WIDTH = 16, // 输入数据位宽 parameter COEF_WIDTH = 16, // 系数位宽 parameter NUM_TAPS = 16, // 滤波阶数 parameter ACC_WIDTH = 36 // 累加器位宽 )( input wire clk, input wire rst_n, input wire signed [DATA_WIDTH-1:0] data_in, output wire signed [ACC_WIDTH-1:0] data_out ); // 8个独立对称系数(h[0]~h[7]) localparam signed [COEF_WIDTH-1:0] COEF [0:7] = { 16'hFF2E, // h[0] 16'hFFE4, // h[1] 16'h02B6, // h[2] 16'h04C9, // h[3] 16'h0662, // h[4] 16'h06A8, // h[5] 16'h052F, // h[6] 16'h01E8 // h[7] }; // 输入移位寄存器 reg signed [DATA_WIDTH-1:0] shift_reg [0:NUM_TAPS-1]; integer i; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin for (i = 0; i < NUM_TAPS; i = i + 1) shift_reg[i] <= {DATA_WIDTH{1'b0}}; end else begin shift_reg[0] <= data_in; for (i = 1; i < NUM_TAPS; i = i + 1) shift_reg[i] <= shift_reg[i-1]; end end // 抽头连线 wire signed [DATA_WIDTH-1:0] tap [0:NUM_TAPS-1]; genvar g; generate for (g = 0; g < NUM_TAPS; g = g + 1) begin : tap_assign assign tap[g] = shift_reg[g]; end endgenerate // 第一级:对称加法(16bit + 16bit -> 17bit) wire signed [DATA_WIDTH:0] sym_sum [0:7]; generate for (g = 0; g < 8; g = g + 1) begin : sym_sum_gen assign sym_sum[g] = tap[g] + tap[NUM_TAPS-1-g]; end endgenerate // 第二级:乘法结果寄存器(17bit * 16bit -> 33bit) reg signed [COEF_WIDTH+DATA_WIDTH:0] mul_reg [0:7]; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin for (i = 0; i < 8; i = i + 1) mul_reg[i] <= { (COEF_WIDTH+DATA_WIDTH+1){1'b0} }; end else begin for (i = 0; i < 8; i = i + 1) mul_reg[i] <= sym_sum[i] * COEF[i]; end end // 第三级:加法树累加(33bit x 8 -> 36bit) reg signed [ACC_WIDTH-1:0] acc_reg; always @(posedge clk or negedge rst_n) begin if (!rst_n) acc_reg <= {ACC_WIDTH{1'b0}}; else acc_reg <= mul_reg[0] + mul_reg[1] + mul_reg[2] + mul_reg[3] + mul_reg[4] + mul_reg[5] + mul_reg[6] + mul_reg[7]; end assign data_out = acc_reg; endmodule

几个地方需要特别说明。累加器位宽我设成36位是有讲究的:8个33位乘积相加,理论上最大值是33位加上log2(8)=3位扩展,也就是36位。如果截成32位,数据一大就会溢出,输出波形顶部被削平,看起来以为是系数算错了,实际上是位宽不够。设计里先用36位全精度输出,后面怎么截位、饱和处理,看外部模块需要多少位再做。

综合工具对generate循环的处理非常成熟,不用担心for循环展开的问题。Vivado和Quartus都能自动把上面的对称求和和乘法展开成并行硬件。

4.2 Testbench设计:如何验证滤波结果

写Testbench的目标很简单:喂一个已知频谱的信号进去,看输出是不是符合预期。我习惯在仿真里混两个频率的正弦波:一个2MHz(通带内,应该正常通过),一个8MHz(阻带内,应该被抑制)。采样率20MHz,2MHz和8MHz都能被采样定理覆盖,不会混叠。

`timescale 1ns / 1ps module tb_fir_parallel; reg clk; reg rst_n; reg signed [15:0] data_in; wire signed [35:0] data_out; fir_parallel u_dut ( .clk (clk), .rst_n (rst_n), .data_in (data_in), .data_out (data_out) ); // 20MHz 采样时钟 initial clk = 0; always #25 clk = ~clk; // 产生测试激励 real t; integer out_file; initial begin rst_n = 0; data_in = 16'sd0; #100; rst_n = 1; out_file = $fopen("fir_out.txt", "w"); // 仿真20000个采样点,足够观察稳态波形 for (t = 0; t < 20000; t = t + 1) begin @(posedge clk); // 输入 = 1000*sin(2MHz) + 1000*sin(8MHz) data_in = $rtoi( 1000 * $sin(2 * 3.1415926 * 2.0 * t / 20.0) + 1000 * $sin(2 * 3.1415926 * 8.0 * t / 20.0) ); #1; $fwrite(out_file, "%0d %0d\n", t, $signed(data_out)); end $fclose(out_file); $stop; end endmodule

仿真跑完,把fir_out.txt拖进MATLAB或者Python里画一下时域波形,能看到两个明显现象:一是8MHz分量幅度相比2MHz分量被压下去很多,阻带抑制生效;二是整个输出相对输入有固定延迟,这是FIR群延迟和流水线延迟叠加的结果。如果想看频域特性,对输出数据做FFT,观察两个频点的幅度差,和系数设计时的幅频响应曲线对照。

我建议在Testbench里加一个简单的理想参考模型,比如直接在TB里用浮点实现FIR卷积,然后和RTL输出做差值。差值应该非常小,主要来自系数量化误差。如果差值很大,说明RTL代码里有位宽截断或者符号处理错误,这比肉眼盯波形高效得多。

4.3 综合结果与资源占用对照

以Xilinx Artix-7 xc7a35t为例,用Vivado默认策略综合上述16阶并行FIR,不开任何DSP优化选项,资源占用大致如下:

资源类型16阶并行FIR(本文)16阶串行FIR(对比)
LUT约260约180
FF约410约230
DSP48E181
最高时钟频率约180MHz约200MHz

很明显,并行结构用8个DSP换来了每个时钟周期输出一个点的吞吐率。串行结构一个DSP就能转,但输出一个点需要16个时钟周期,同样跑180MHz主频,等效采样率只有11.25MSPS,远低于并行结构的180MSPS。如果实际采样率就是20MSPS,串行结构在180MHz主频下确实够用,但主频一旦受布局布线影响降到150MHz,串行结构就会突破吞吐率上限,并行结构依然从容。

综合时还可以开启“流水线乘法器”选项,工具会自动在乘法器内部再插入一级寄存器,进一步提高时序。代价是输出延迟再多一拍。要不要开,取决于你的时序预算,我一般会先不开,时序违例了再考虑。

5. 常见问题与排查技巧:仿真、时序、数据精度

5.1 输出波形不对,先查溢出和符号位

仿真发现输出波形不对,我见过最多的情况不是滤波器写错了,而是位宽和符号处理出了问题。16位有符号数的范围是-32768到32767,两个16位有符号数相加,范围是-65536到65534,必须用17位表示。犯这个错的人不在少数,直接敲代码累加,最后符号位被截掉,输出出现大量正负交替的尖峰。

排查方法很简单:把data_out设成全精度36位观察,如果波形正常,说明滤波器本身没问题,问题出在输出截位。截位时不要直接丢弃低16位,那样输出会带有直流偏置,因为负数在二进制补码下直接截断不等于除以65536。正确的做法是先加一个舍入偏置(比如0x8000)再右移16位,或者用饱和截位逻辑,把超出目标位宽的数值钳位到最大值或最小值。

5.2 时序不收敛,别只盯代码

并行FIR的时序问题多数出在加法树上。8个乘法结果一次性相加,综合器会把它展开成一个深度为3的多级加法树,这本身不算深,但如果你把对称加法、乘法、最终累加全放在一个always块里,组合逻辑路径就会从移位寄存器输出一直延伸到最终输出,深度超过5级,100MHz以上基本跑不过。

解决办法就是我前面说的三级流水。如果做完三级流水还违例,可以进一步检查:乘法器输出后加的寄存器是否真的被综合器保持住了(别被优化掉);时钟约束是否写对了;有没有在跨时钟域路径上漏掉异步FIFO。很多时候时序不收敛根本不在FIR本身,而是旁边的异步接口没处理好,把时钟质量拖垮了。

5.3 仿真和实测差很多的几个坑

仿真结果正常,上板实测却不理想,这个问题我自己碰到过好几次。最常见的原因是输入数据没有做符号位处理。比如ADC输出的是偏移二进制码(offset binary),直接接到RTL里当有符号数用,一半的波形全是乱的。解决方法是把偏移二进制码转换成二进制补码,转换逻辑很简单:最高位取反即可。

另一个坑是时钟。FPGA开发板上的时钟源可能带抖动,ADC采样时钟如果是PLL生成的,相位噪声会直接影响采样精度。滤波器本身设计得再好,前级采样信号质量差,输出照样一堆杂散。这时候别急着改滤波器,先用频谱仪或者ILA抓一下输入信号的频谱,确认输入干净了再查RTL。

还有一类问题是在仿真环境里忽略复位时序。有些开发板的复位芯片是异步复位、释放不同步,导致FPGA内部寄存器进入亚稳态。我习惯在RTL里加一个同步复位释放逻辑,把外部复位打两拍再驱动全局复位,成本极低,收益却很大。

5.4 仿真License报错这类环境问题的快速定位

仿真环境偶尔会出一些和代码无关的报错,比如有朋友在使用17.1版本软件时遇到“failure to obtain a verilog simulation license”的报错,代码和工程本身没有问题,纯粹是仿真License没有获取到。快速排查顺序是:先确认环境变量LM_LICENSE_FILE有没有指向正确的License文件,再确认License服务是否启动、是否过期,最后看看是否和其他后台程序占用了License端口。这类问题多数重开服务或者环境源一下就好了,跟FPGA设计本身没有关系,不用浪费太多时间在代码上反复找原因。

6. 工程实践心得:我在项目中积累的经验

6.1 参数化设计,一步到位

开头提到的那块中频信号处理板子,我一开始写的FIR模块是硬编码16阶、16位数据、16位系数,当时觉得项目指标就那样,够用就行。结果第二版需求改成32阶、采样率升到100MSPS,我不得不整个模块重写一遍,浪费时间不说,还引入了一个新的位宽bug。从那以后,我所有的DSP模块都用parameter做参数化设计,数据位宽、系数位宽、滤波器阶数、流水线级数全部做成可配置。换场景的时候只改参数和系数表,RTL主体一个字符都不用动。

参数化还有一个好处是方便做设计空间探索。同一套代码,在资源紧张的芯片上编译成一版精简参数,在资源富裕的芯片上编译成一版全并行参数,对比综合报告就能快速找到性能和面积的最佳平衡点。这种灵活性在实际项目中非常值钱。

6.2 别急着上并行,先算清吞吐率账

最后说一个很多人容易忽略的点:并行FIR并不是所有场景的最优解。我做并行设计前一定会算一笔账——当前系统时钟是多少,滤波器阶数是多少,需要的输出采样率是多少。比如一个256阶FIR,系统时钟200MHz,输出采样率只要1MSPS,那么串行结构绰绰有余,还能省下255个DSP做其他事;但如果输出采样率和系统时钟接近,或者延迟要求苛刻,这时候并行结构才真正发挥价值。

我见过一些项目,明明串行结构一个DSP就能搞定,工程师却写成全并行结构,资源爆炸还不自知。FPGA设计里“杀鸡用牛刀”不是什么值得炫耀的事,资源占用、功耗、成本都是实打实的代价。所以我在每个项目开始之前,都会先花半天时间做架构分析,把吞吐率、延迟、资源、功耗四个维度全部列出来再动手。这个习惯帮我规避了至少两个后期返工的大坑。

6.3 后续可以扩展的方向

并行FIR这套架构本身还有很多扩展空间。一个是半并行结构,用4个乘法器分时完成8组对称乘法,速度是纯并行的一半,DSP只消耗一半,特别适合中等性能需求的系统。另一个是多通道滤波器组,如果多个信号源需要相同的滤波特性,可以时分复用同一套并行FIR阵列,每个通道分配一个时隙,硬件成本几乎不增加。还有就是把FIR和CIC(积分梳状滤波器)级联,第一级CIC做抽取降速,第二级FIR做精细化滤波,高采样率下资源效率更高。

如果你做的项目涉及动态系数切换,比如数字下变频里的匹配滤波器,那还需要在并行FIR基础上加入系数寄存器组,通过总线接口在运行时更新系数。这个功能对软件无线电、自适应滤波系统几乎是刚需。扩展的方向还有很多,核心思想是一致的:把滤波器做成一枚可配置的“积木”,在需要的地方灵活拼接。

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

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

立即咨询