ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

并行FIR滤波器设计实战:Verilog实现与FPGA优化

并行FIR滤波器设计实战:Verilog实现与FPGA优化 前阵子调试一块做中频信号处理的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 NM拍内完成全部乘加速度和资源介于两者之间适合需要一点性能但不想全量铺乘法器的场景典型做法是多通道时分复用一套乘加阵列。并行FIR则把N个乘法器一次性铺开利用系数对称性可以减到N/2所有输入样本同时参与运算每来一个时钟就输出一个新的滤波结果吞吐率和输入采样率完全一致不受滤波器阶数影响。代价是使用的DSP Slice和LUT数量明显上升。实际项目中怎么选我自己的经验是采样率超过50MSPS、FIR阶数超过32阶、延迟要求严格这三个条件满足任意两条就直接上并行资源紧张、采样率只有几百K到几M的串行性价比更高。2. 滤波器指标推导与系数计算动手写代码前必须做的事2.1 用窗函数法设计16阶低通滤波器很多同学拿到FPGA板子第一件事就是打开Vivado写代码结果到综合的时候发现系数不对、指标不达标回头又一版一版改。我个人的习惯是先算系数再写RTL。系数是整个滤波器的心脏架构再好系数算错全白搭。这篇文章为了代码演示方便设计一个16阶16个抽头低通FIR采样率fs20MHz通带截止5MHz阻带起始9MHz过渡带Δf4MHz阻带衰减目标50dB。选择Hamming窗因为Hamming窗主瓣窄、旁瓣衰减约53dB工程里最常用。估算阶数的经验公式N ≈ 3.3 × (fs / Δf) 3.3 × (20 / 4) 16.5取N16正好是偶数阶。归一化截止频率取通带和阻带的中间点即fc (59)/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)/27.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 系数定点化与量化误差控制浮点系数不能直接拿来写VerilogFPGA里做乘法的都是定点数所以要把浮点系数转成定点。这一步最常见的坑是量化误差太大导致频率响应变形。我自己的方法是先对所有系数做归一化除以最大绝对值让最大系数接近但不超过1然后乘以2^(Q-1)再四舍五入取整。Q是系数位宽工程里我一般用16位有符号数既有足够精度又不会太浪费DSP资源。以8个独立系数为例归一化后数值范围在-0.05到1之间乘以32767再取整得到类似下面的一组16位有符号十六进制系数系数索引归一化浮点值16位定点表示(h)h[0]-0.05210xFF2Eh[1]-0.03180xFFE4h[2]0.07780x02B6h[3]0.19700x04C9h[4]0.28210x0662h[5]0.29620x06A8h[6]0.21480x052Fh[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] { 16hFF2E, // h[0] 16hFFE4, // h[1] 16h02B6, // h[2] 16h04C9, // h[3] 16h0662, // h[4] 16h06A8, // h[5] 16h052F, // h[6] 16h01E8 // 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{1b0}}; 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_WIDTHDATA_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_WIDTHDATA_WIDTH1){1b0} }; 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{1b0}}; 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阻带内应该被抑制。采样率20MHz2MHz和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 16sd0; #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约180FF约410约230DSP48E181最高时钟频率约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基础上加入系数寄存器组通过总线接口在运行时更新系数。这个功能对软件无线电、自适应滤波系统几乎是刚需。扩展的方向还有很多核心思想是一致的把滤波器做成一枚可配置的“积木”在需要的地方灵活拼接。
返回列表