首页 / 资讯中心 / 文章详情

多通道FIR滤波器设计实战:FPGA IP核配置与仿真全解析

多通道FIR滤波器设计实战:FPGA IP核配置与仿真全解析 ★ FEATURED ARTICLE
写多通道FIR滤波器很多人一开始就被“多通道”三个字吓住了。其实说穿了就是把好几路信号同时丢进同一个滤波器让它们在同一个FPGA工程里各自完成滤波任务。我在刚开始用Xilinx FIR IP核的时候也是抱着数据手册啃了三天最后发现真正难的不是IP核本身而是对通道、时钟、数据排列的理解。这篇文章就按我自己的上手路径来写从需求分析讲到参数配置再到仿真验证和常见坑位排查尽量让一个之前完全没碰过FIR IP核的读者也能把多通道滤波器跑起来。1. 动手之前先把多通道FIR的账算清楚1.1 多通道FIR要解决什么问题FIR滤波器就是有限脉冲响应滤波器它对输入信号做加权求和每个输出点等于最近N个输入点与N个滤波器系数的乘积之和。这个N就是抽头数也叫Taps。FIR因为线性相位特性好、结构稳定在通信、音频、雷达、生物电信号处理里几乎是绕不开的基础模块。那“多通道”是什么场景呢。最典型的是多通道音频比如一个48kHz采样率的音频系统同时处理8路甚至32路声道每路都要过均衡或分频滤波器。还有多通道振动监测几十个加速度传感器同时采数据每个通道要实时滤除工频干扰。雷达和声呐里更常见几十上百个阵元通道每个通道都要做匹配滤波或波束形成的预处理。这些场景下如果你给每个通道都例化一个独立的FIR模块资源很快就爆了。正确做法是复用一个滤波计算单元通过时分复用的方式挨个处理每个通道的数据这正是Xilinx FIR Compiler IP核多通道模式的核心思想。1.2 三种实现路线对比与选型实现多通道FIR大致有三条路纯手写Verilog、用FIR Compiler IP核、用Vitis HLS做高层次综合。纯手写Verilog的优点是可控性最强想怎么优化就怎么优化但缺点也明显FIR涉及到延迟线、系数存储、乘法累加器、多通道状态管理写起来繁琐不说时序优化全靠经验。多通道模式下还要自己设计通道轮询逻辑忙活一个星期写出来的代码性能和稳定性还不一定比得上IP核。用Vitis HLS的优点是开发效率高用C/C描述滤波器算法并且很容易做定点化仿真。但HLS生成的RTL代码在资源利用率和时序收敛上有时不如手工优化的IP核而且多通道高吞吐场景下HLS默认生成的流水线结构也需要反复调pragma才能达到预期性能。FIR Compiler IP核则是Xilinx专门针对FIR滤波优化过的硬核IP它内部使用DSP48硬核资源做乘加运算支持灵活的通道数、多相结构、可配置的系数重载还能自动处理饱和与舍入。对绝大多数量产项目来说这是性价比最高的选择。我自己的项目基本都选IP核只有做ASIC验证的原型RTL才会考虑手写FIR因为那套代码后续要移植到其他工艺库IP核的RTL没法直接用。1.3 核心公式与参数预算多通道FIR设计里最关键的一个公式是数据率预算。假设单通道采样率为Fs通道数为Nch那么IP核输入端的数据率就是Nch × Fs。以48kHz采样、32通道为例输入数据率就是1.536MHz也就是每秒钟要送入153.6万个采样点。如果你的主时钟是100MHz那每个采样点对应大约65个时钟周期这个余量非常充足IP核内部的乘加单元完全忙得过来。但如果你处理的是高速ADC数据比如250MSPS采样率、8通道并行采集那输入数据率就高达2GSPS100MHz主时钟根本扛不住这时必须让多通道数据在更高时钟域下并行输入。FIR Compiler里有一个概念叫硬件过采样率Hardware Oversampling Ratio简称HIL公式是HIL Fclk / (Nch × Fs)它表示每处理一个通道采样点有多少个时钟周期可用。IP核会依据这个数值自动决定内部是单MAC串行结构还是多MAC并行结构。HIL越大需要的DSP48越少HIL小于抽头数时IP核会复制乘法器来保证每个周期能完成足够的乘加运算。所以在配置IP核之前先把这个账算清楚主时钟、采样率、通道数、抽头数四个数一摆资源量级和时钟可行性大概就有数了。这也决定了后面IP核配置界面里几个关键参数该填什么。2. FIR Compiler IP核参数配置详解2.1 滤波器类型与系数加载在Vivado里打开IP Catalog搜索“FIR Compiler”双击进入配置界面。第一步是选择滤波器类型最常见的是Single Rate也就是输入输出速率相同适合大多数普通滤波场景。如果你的系统里需要改变采样率比如先把信号从48kHz插值到96kHz那就选Interpolation如果要从96kHz抽取到48kHz选Decimation。选抽取或插值之后IP核会自动把滤波器按多相结构展开这并不需要你自己手写任何多相分解逻辑。我记得第一次看到这个功能时还挺意外后来仔细看文档才发现多相滤波在硬件上就是原型滤波器系数按相位重新分组FIR Compiler把这个工作完全自动化了。接下来是滤波器系数。配置界面里有两种加载方式一种是直接在Coefficient Vector表格里手动填数值适合调试时随便填几个系数验证通路另一种是加载外部coe文件适合从MATLAB或Python算好系数后导进来。我强烈建议用coe文件因为手填系数很容易填错位而且后期修改滤波器指标时直接改文件重新生成IP核就行。coe文件的格式有严格要求我再三提醒身边同事文件第一行通常是注释用分号开头然后指定radix和coefficient_width最后是coefdata后面跟一串系数系数之间用逗号分隔整个文件以分号结尾。radix支持2、10、16比如radix16就是十六进制radix10就是十进制。很多人第一次写coe文件忘记末尾分号导致IP核加载失败这个错误很低级但真的一搜一大把。2.2 多通道数据接口与时序解析多通道配置的核心参数是Number of Channels和Input Sample Frequency。你填了通道数之后IP核会自动计算输出数据率并显示在Summary里。很多新手看到数据接口只有一组AXI4-Stream输入输出就会疑惑多通道的数据怎么塞进一根总线答案就是时分复用。在相邻两个采样周期之间tvalid拉高的那一段时间里每个时钟周期送一个通道的数据按通道0、通道1、通道2……顺序依次排队进入IP核。假设配置了4通道、每通道数据位宽16bit那么tdata总线位宽就是16bit不是64bit。输入侧需要用计数器控制多路选择器在每个采样周期内把4个通道的数据按顺序送上总线。输出侧同理tvalid有效后会按顺序依次吐出通道0到通道3的滤波结果。有的Vivado版本里Data Interface可以选Independent Bus模式那时tdata位宽会变成Nch×16bit总线上不同位段对应不同通道数据并行送入这个模式适合输入数据率很高、要求并行输入的场合。具体选哪种看你系统架构是并行还是串行采样。时序上最容易出问题的点在于tready。AXI4-Stream协议有握手机制只有当tvalid和tready同时为高时数据才算真正被接受。FIR Compiler在内部处理不过来时会拉低tready进行反压如果你的上游逻辑无视tready硬往里面塞数据那就会丢数据。多通道模式下丢一个数据后果是整个通道序列从此错位滤波结果一塌糊涂而且非常难查。所以写外部逻辑时务必把tready纳入使能条件。2.3 多相滤波与抽取插值实现既然聊到多相滤波就稍微展开一点。FIR滤波器每输出一个点要做N次乘加如果先滤波再抽取M点意味着有M-1个计算结果本来就该丢掉白白浪费了计算量。多相分解的思路是把长度为N的原型滤波器系数重新排列成M个子滤波器每个子滤波器只保留每隔M个取一个的系数。滤波时输入信号按M倍抽取分别进入对应子滤波器每个子滤波器的计算结果直接就是抽取后的输出计算量从N次乘加降为N/M次乘加省了整整数倍。FIR Compiler在处理Decimation和Interpolation配置时内部就是按照多相结构来例化资源。你在配置界面上看到的选项只是抽取倍数或插值倍数背后的系数重排和相位选择全被IP核吞掉了。所以如果你在工程里看到别人说“用Vivado FIR核做多相滤波”他大概率说的就是用这种抽取/插值模式而不是自己去写多相分解代码。有一点要留意抽取模式下输出数据的速率是Fs/M下游模块的时钟和有效信号要按这个速率来设计否则会出现数据断续。2.4 数据位宽、量化与饱和机制位宽是另一个决定成败的细节。FIR IP核有两个输入端位宽要设置输入数据位宽和系数位宽。常见的配置是输入16bit、系数16bit输出位宽则可以自己选。理想的输出位宽应该等于输入位宽加系数位宽再加log2(Nch×Taps)这么一位余量这样才不会有溢出的风险。但工程上输出位宽往往不能无限大后面还有模块要用这个数据。于是IP核提供了多种量化模式Full Precision、Truncate LSBs、Non Convergent Rounding、Convergent Rounding等以及饱和模式Saturate和Wraparound。Full Precision最安全输出位宽很宽不会丢失信息但位宽会大到下游难以接受。Truncate LSBs最简单粗暴直接砍掉低位会引入直流偏置和量化噪声。Convergent Rounding是四舍五入到偶数统计特性比单纯截断好在高精度场景里更推荐。饱和模式则决定数据超出范围时是钳位到最大值还是按二进制自然回绕。对音频信号回绕会产生刺耳的爆音所以一般选Saturate对通信基带信号有时为了算法分析方便反而选Wraparound避免饱和引入非线性。这些参数没有绝对的对错完全取决于后续模块想看到什么样的数据。我的建议是先用Full Precision摸清真实动态范围再根据实测波形决定是否截断和饱和不要一上来就凭感觉设。3. 完整实操从创建工程到仿真波形全绿3.1 用Python生成系数并导出coe文件系数设计推荐用Python的scipy或MATLAB。我用Python比较多就以Python为例。假设需求是48kHz采样、128阶低通、截止频率8kHz、Hamming窗那代码大致长这样from scipy.signal import firwin import numpy as np fs 48000.0 cutoff 8000.0 numtaps 128 b firwin(numtaps, cutoff, fsfs, windowhamming) # 归一化到16bit有符号定点数 Q 15 b_scaled np.round(b * (2**Q - 1)) b_int b_scaled.astype(np.int16) # 导出Xilinx coe文件十六进制补码格式 with open(fir_lpf.coe, w) as f: f.write(; Xilinx FIR Compiler coefficient file\n) f.write(radix16;\n) f.write(coefficient_width16;\n) f.write(coefdata\n) for i, c in enumerate(b_int): if c 0: hex_str format(c 0xFFFF, 04X) else: hex_str format(c, 04X) if i len(b_int) - 1: f.write(hex_str ,\n) else: f.write(hex_str ;\n)注意负数要按补码格式写入也就是c与0xFFFF做按位与。生成完成后可以用一个简单的正弦叠加信号去验证系数设计是否满足指标直接在Python里调用np.convolve对比滤波前后的频谱即可。3.2 Vivado中配置并例化FIR Compiler在Vivado IP Catalog里搜索FIR Compiler打开后按以下步骤配置Filter Options页里把Filter Type选为Single RateNumber of Channels填实际通道数比如4。Sample Rate填每通道的采样频率比如48000Hz。在Coefficient File里加载刚才生成的fir_lpf.coe确认一下抽头数自动识别为128。如果系数文件格式有问题这里会报错或者显示0个系数看到这种情况先回头检查文件末尾分号和小数点格式。Implementation Details页里设置输入数据位宽16bit、有符号数系数位宽16bit量化模式选Convergent Rounding饱和模式选Saturate。输出位宽可以暂时保持自动计算的结果。时钟频率默认按Vivado工程的约束自动推断也可以手动填一个期望值比如100MHz。配置完成后点击OK在IP Sources里选中这个IP核右键选择Generate Output Products等综合生成完成后再打开Instantiation Template就能看到标准的例化模板。多通道模式下例化代码并不复杂我用的最多的是AXI4-Stream接口fir_compiler_0 u_fir ( .aclk (clk), .s_axis_data_tvalid (s_axis_tvalid), .s_axis_data_tready (s_axis_tready), .s_axis_data_tdata (s_axis_tdata), .m_axis_data_tvalid (m_axis_tvalid), .m_axis_data_tdata (m_axis_tdata) );如果你的IP核版本支持独立总线模式的输入接口接口信号会根据配置生成多组数据位段这时候例化模板的位宽定义会差很多所以每次生成完IP核之后务必先看例化模板再连线。3.3 Testbench设计与多通道激励生成Testbench的要点是严格按照AXI4-Stream握手时序产生多通道数据。采样周期用周期信号表示每个采样周期内产生Nch个通道数据。下面是一个4通道16bit数据的Testbench片段timescale 1ns / 1ps module tb_fir; reg clk; reg rst_n; reg [15:0] ch_data [0:3]; reg [3:0] ch_index; reg sample_pulse; wire s_axis_tvalid; wire s_axis_tready; wire [15:0] s_axis_tdata; wire m_axis_tvalid; wire [15:0] m_axis_tdata; // 采样脉冲每100个时钟产生一次 always (posedge clk) begin if (sample_counter 99) begin sample_pulse 1b1; sample_counter 0; end else begin sample_pulse 1b0; sample_counter sample_counter 1; end end // 在每个采样脉冲后的4个时钟依次送4个通道数据 always (posedge clk or negedge rst_n) begin if (!rst_n) begin ch_index 0; end else if (sample_pulse) begin ch_index 0; end else if (s_axis_tvalid s_axis_tready) begin if (ch_index 3) ch_index 0; else ch_index ch_index 1; end end assign s_axis_tvalid (sample_pulse || ch_index ! 0) !ch_done; assign s_axis_tdata ch_data[ch_index]; endmodule这里我把每个通道的激励预存到一个数组里实际工程中可以从ROM读取数据文件也可以直接生成正弦波扫频信号。仿真的第一个目标不是看频谱而是看通路是否打通。首选验证方法是冲激响应法给通道0送一个单位冲激其余通道送0观察输出波形是否和coe文件中的系数完全一致。如果输出序列和系数一一对应说明通路没问题。然后再给每个通道分别送冲激就能确认通道之间没有串扰。3.4 结果对比与延迟校准FIR滤波器是有固定群延迟的。对一个N阶线性相位FIR群延迟是(N-1)/2个采样周期。用128抽头系数时输出相对输入延迟63.5个采样点这就是为什么仿真里拿输入正弦和输出正弦直接对比相位会对不上。多通道模式下这个延迟对每个通道都是一样的所以通道间相对延迟是0。验证滤波结果是否正确的标准做法是把输入激励数据存成txt文件在Python或MATLAB里用相同系数做一次定点仿真然后把Vivado仿真导出的输出数据与软件仿真的结果做逐点对比。允许的误差只有舍入造成的1到2个LSB。如果误差偏大先检查IP核的量化模式是否与软件模型一致再看看输入数据是否无意中经过了符号位扩展。实测中我发现很多对不上的情况都出在Testbench里数据位宽没有扩展到16bit符号位就接到了tdata总线上。4. 高频问题排查与优化实录4.1 多通道FIR常见问题速查表我把实际项目中踩过的坑和同行常问的问题整理成一张表方便直接按图索骥。现象可能原因排查方式处理建议输出一直是0系数文件没有正确加载或tvalid从未拉高检查IP核Summary里的Taps数量仿真里看s_axis_tvalid是否按采样周期脉冲确认coe文件路径和格式改用手动填系数验证通路输出波形放大或缩小系数量化后增益偏移或截断模式引入直流对比coe文件系数和设计系数的增益系数归一化时留足余量输出端做增益补偿通道间数据错位输入数据没有严格按通道顺序排列用通道0单位冲激检查输出对应关系重写输入侧通道轮询逻辑重点检查tready握手高电平饱和削波输出位宽不够或饱和模式选择不当观察输出是否被钳位在最大值加宽输出位宽或改用Wraparound模式数据率上不去时钟频率不够通道数太多计算HIL和抽头数看DSP资源利用率提高主时钟或用独立总线模式并行输入时序收敛失败IP核内部乘法器级数太长查看时序报告定位关键路径在IP核配置里提高多相并行度或增加输出流水级4.2 三个典型案例复盘第一个案例输出全0。那次是同事误把coe文件只写了注释和radix行系数数据全丢了IP核显示Taps数量为0但配置页面不报错。排查了一天才发现是coe文件末尾分号被中文输入法打成了全角符号。从那以后我写了个脚本加载coe文件后立即打印系数个数和第一个系数值确认无误再继续。第二个案例通道错位。现象是4通道输入正弦波通道0的频谱出现在通道2的输出上。查到最后是上游逻辑在tready为低时仍然推进了通道计数器导致后续所有通道的数据整体前移。修复方法很简单只有tvalid和tready同时有效才递增通道计数。但这个bug隐蔽在仿真波形里很难发现因为波形看起来始终有数据只是对应关系不对。第三个案例时序违例。设计是64通道、128抽头、采样率48kHz主时钟100MHz按计算HIL约32.5理论上一个MAC足够。但实际综合后DSP使用量非常大查了IP核配置才发现时钟频率误填成500MHzIP核按照HIL6.5自动复制了大量MAC单元浪费了资源。把时钟频率改成真实的100MHz并重新生成IP核后DSP使用量下降接近5倍。4.3 性能优化与资源权衡多通道FIR的性能优化归根结底是HIL和DSP资源之间的权衡。如果HIL远大于抽头数IP核会串行复用乘加器资源开销小如果HIL小于等于抽头数IP核必须并行展开乘加器DSP开销直线上升。设计阶段我会先用公式粗算一遍再在IP核配置页面里看Resource Utilization预估两者交叉验证。对超多通道场景还有几个进阶优化思路。一是把多通道数据拆成两路并行分别送两个FIR Compiler实例每个实例处理一半通道相当于用面积换吞吐。二是利用多相抽取结构降低有效数据率在滤波前先做CIC抽取把带宽降下来再进FIR精滤这在通信中频信号处理里很常见。三是在FPGA里用多个时钟域ADC采集端用高速率时钟域FIR核心用中速率时钟域中间用异步FIFO桥接这样能把通道数和数据率解耦。另外提醒一句Vivado的IP核在不同版本之间的接口时序和配置界面偶尔有细微差异。网上经常搜到xilinx sdk 2015.4之类的老版本教程很多配置路径在现在的新版本里已经变了。遇到界面对不上的情况直接用当前版本的IP核数据手册和例化模板为准不要硬套老界面截图。老版本工程迁移到新版Vivado时FIR Compiler IP核一般会自动升级但升级后务必重新跑一遍仿真对比输出比特流确认系数加载和时序没有变化。5. 最后再聊一点个人经验多通道FIR设计做到最后真正的难点已经不在FIR本身了。IP核把算法细节封得严严实实你需要担心的反而是数据怎么按时钟编排、握手怎么处理、位宽怎么截断。我个人最大的体会是上手FIR Compiler的第一步不是打开Vivado而是先在纸上把通道数、采样率、主时钟、抽头数这四个数写清楚然后算HIL看资源预估。这一步省下的调试时间远比想象中多。调试时也建议先做最简单的单通道冲激响应验证再逐步扩展到多通道。很多人一上来就填满所有通道跑复杂信号波形一旦不对根本无从定位。把主机逻辑和IP核解耦测试确保每一步通路都对了再接起来这个习惯能帮你少走很多弯路。如果你现在正被某个多通道FIR的问题折磨回过头去检查通道轮询逻辑和tready握手大概率问题就出在那里。
阅读完成 · 觉得有帮助?
咨询建站