
两年前我第一次在FPGA上放下一个IIR滤波器时还是被实时性逼的。当时手里有个语音前端降噪的活要在几微秒内完成整段滤波DSP按顺序执行理论时延压不下来功耗和成本也卡得紧只能往FPGA这边挪。后来这块板卡上的滤波器越做越多IIR、FIR、自适应滤波器各占了一小块逻辑资源反而把三类结构在FPGA实现里的脾气摸了个透。这篇文章就是把这几段实战经验拆开讲什么场景该选哪种滤波器上板前系数怎么做定点化自适应滤波器收敛怎么稳住以及在调试台上一眼看出问题的验证方法。适合手里有FPGA开发板、想把数字滤波器真正从Matlab仿真跑到硬件上的人。1. 为什么我最终把滤波器全部搬进了FPGA1.1 被实时性倒逼的选型过程最早做滤波我习惯用DSP加浮点库。公司里的标准流程是先在PC上定滤波器阶数再移植到DSP跑。直到有一次需要同时处理8路ADC信号每路采样率都提了一档DSP的中断负载直接逼近极限加一路通道就得重新评估时序余量。那段时间我做了一个粗暴测试把一段64阶FIR放在FPGA里跑时钟只要给够乘法器和加法器都是并行铺开的延迟是确定的几个时钟周期。而DSP是串行取指、乘加、回写阶数一高延迟会随着系数个数线性增长。对实时系统来说FPGA这种“多干活不加班”的算力供给方式几乎是降噪类前端的标准答案。所以最后定下的方案是凡是固定系数、高吞吐、确定性延迟的滤波路径全部挪到FPGA凡是需要在线调整策略、跑复杂协议的继续留在处理器侧。这个分工到现在都不过时。1.2 FPGA处理滤波的两个核心优势第一个优势是并行性。一个N阶FIR本质就是N次乘法和N-1次加法。虽然是串行递推公式但硬件上每个乘法器可以独立分配乘完的结果通过加法树合并。逻辑综合工具会自动展开流水最终效果是“一拍完成一整个N阶滤波”。这比循环运行N次的DSP方案在吞吐率上高出一个量级。第二个优势是确定性。DSP上有中断、缓存、分支预测最坏执行时间很难拍胸脯保证FPGA里一拍就是一拍时序约束写清楚延迟抖动用示波器都测不出来。对需要多通道同步的应用比如波束成形、通道均衡这个确定性压倒一切。第三个容易被忽略的优势是可重配置。换一组滤波器系数本质是换一组常数或者换一张查找表不需要改板子、不需要换芯片。项目后期现场反馈“低频噪声偏多”我只要重新生成系数文件通过配置接口灌进去就行。2. IIR滤波器用最少的资源换最陡的截止但要提防反馈2.1 结构选择宁可级联不要直接高阶IIR最吸引人的地方就是“省钱”。实现同样的过渡带IIR可能5阶就够了FIR要几十阶。代价是反馈结构和非线性相位。FPGA里做IIR我第一条经验就是绝对不要直接实现一个高阶差分方程。高阶直接型的系数对量化极敏感稍微一截断极点就可能飘出单位圆上板就啸叫。正确的做法是把高阶IIR分解成一串二阶节的级联也就是常说的SOSSecond-Order Sections。每个二阶节只处理两个极点、两个零点稳定性边界清晰中间变量也方便做饱和保护。举个例子一个4阶IIR可以拆成两个二阶节第一级y1[n] b00*x[n] b01*x[n-1] b02*x[n-2] - a01*y1[n-1] - a02*y1[n-2] 第二级y[n] b10*y1[n] b11*y1[n-1] b12*y1[n-2] - a11*y[n-1] - a12*y[n-2]拆分顺序也有讲究。理论上高阶系统任意拆分都能在数学上等价但定点实现不一样。我习惯把极点靠近单位圆的节放前面让动态范围在每一级之间逐步衰减避免第一级饱和后第二级信号过小、白白浪费位宽。这个顺序我之前踩过坑先排了一个零点多的节在前面结果中间信号幅值翻了倍16位整数根本扛不住后来才意识到要按极点模值从大到小排列。2.2 定点化实现的关键Q格式与系数缩放FPGA里没有浮点单元是常态即便现在有些高端芯片带浮点功耗也压不住。所以第一步就是把浮点系数转成定点数。我常用Q15格式1位符号位加15位小数位能表示的最小步长是1/32768约等于3e-5的精度。比如一个系数a2 -0.9816Q15量化就是round(-0.9816 * 32768) round(-32166.75) -32167回代进去实际值是 -32167/32768 -0.981597误差不到1e-5。单个系数这个误差无所谓但要注意IIR反馈路径里的误差会累积。尤其是a1、a2这种反馈系数量化误差稍大在高阶节级联之后可能把极点位置推偏。所以做IIR的定点化我至少保留Q15如果芯片DSP Slice支持18x18或者25x18乘法我会把系数扩到Q17甚至Q24。位宽多给几个比特省下的调试时间远大于逻辑资源。下面是一个二阶节的Verilog骨架可以实例化后级联使用module iir_section #( parameter W 16, parameter Q 15 )( input wire clk, input wire rst_n, input wire signed [W-1:0] x, output reg signed [W-1:0] y ); // 系数示例实际数值由Matlab/浮点设计量化得到 localparam signed [W-1:0] B0 16sd1024; localparam signed [W-1:0] B1 16sd2048; localparam signed [W-1:0] B2 16sd1024; localparam signed [W-1:0] A1 -16sd19856; localparam signed [W-1:0] A2 16sd31950; reg signed [W-1:0] x1, x2; // x[n-1], x[n-2] reg signed [W-1:0] y1, y2; // y[n-1], y[n-2] reg signed [W*2-1:0] acc; wire signed [W*2-1:0] t0 B0 * x; wire signed [W*2-1:0] t1 B1 * x1; wire signed [W*2-1:0] t2 B2 * x2; wire signed [W*2-1:0] t3 A1 * y1; wire signed [W*2-1:0] t4 A2 * y2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin x1 d0; x2 d0; y1 d0; y2 d0; y d0; end else begin acc t0 t1 t2 - t3 - t4; // 取高W位并做饱和截断防止溢出翻转 y sat16(acc[W*2-1:Q]); x1 x; x2 x1; y1 y; y2 y1; end end function automatic signed [W-1:0] sat16; input signed [W*2-1:0] d; begin if (d 2**(W-1)-1) sat16 2**(W-1)-1; else if (d -2**(W-1)) sat16 -2**(W-1); else sat16 d; end endfunction endmodule这段代码里有个关键点乘法器全部输出32位累加在32位里做完只在最后一级截断到16位输出。绝对不要在每一级乘法后立刻截断那样会让量化噪声逐级放大信噪比下降得厉害。我用过这个粗糙做法测出来的THD直接差了10dB。2.3 实测最容易踩的稳定性和溢出坑IIR上板有个经典现象输入停掉输出还在以固定频率小幅度振荡甚至频率很干净。这就是极限环振荡由定点舍入误差在反馈回路里反复循环造成。浮点仿真永远不会出现因为浮点精度的舍入误差淹没在噪声底下定点仿真如果不做长时间零输入测试也容易被忽略。我当时的处理思路分两层第一层内部累加器用满位宽尽量让动态范围大减少舍入频次 第二层输出端做检测当输入为零且输出绝对值连续N个采样低于阈值直接强制清零。这个阈值不能拍脑袋定。我根据实际系统的最低有效位噪声来定输入ADC静默时的底噪大概是±8个LSB所以阈值放到16再往上留一点余量既能消除极限环又不会误杀真实小信号。另一个常见坑是反馈系数的符号和截断位置搞反。很多同学在写差分方程时习惯写成y[n] b0x[n] ... - a1y[n-1]硬件里如果不做减法直接加a1*y[n-1]那极点直接变成镜像滤波变放大上电就爆音。这类问题我排查过不止一次后来在代码里加了断言上电后强制把输入置零检查输出是否稳定在零附近。起振、振荡、漂移都能一眼看出来。碰到IIR不稳定的情况我的一般检查顺序是先把所有中间节点幅值打印出来看哪一级最先溢出或失真把浮点模型在纯C里换成Q15定点复现现象确认是量化问题还是结构问题逐级检查反馈系数的符号跟差分方程一一对应最后加饱和截断再跑零输入测试直到输出完全静默。3. FIR滤波器无反馈的工程友好型选手3.1 从频域指标到系数文件FIR没有反馈结构天生稳定相位还可以做成线性。设计流程相对标准先定采样率、通带、阻带、纹波要求再拿Matlab的firls、firpm或者Python的scipy.signal.remez去优化系数。这里我不纠结具体用哪个库函数重要的是拿到系数之后怎么做定点化。假设采样率48kHz要滤掉8kHz以上的噪声过渡带要求4kHz阻带抑制60dB。打开Matlab生成一个64阶低通得到的64个浮点系数幅度范围大概在[-0.03, 0.03]之间中间有一个主瓣接近0.3。如果直接按Q15量化所有系数放大32768倍后主瓣接近10000旁瓣系数只有几百看起来“能用”但其实量化噪声已经被压缩到了接近0.03的量级。更稳妥的做法是先把系数归一化让最大系数尽量接近满幅但不溢出再按Q15量化。比如最大系数是0.28那就把所有系数除以0.28再乘以32767最后在实现端或运算端统一补一个全局缩放因子。这样能最大限度利用16位整数的动态范围。系数生成完之后我习惯用脚本自动生成Verilog的parameter数组文件或者生成.coe文件灌进ROM。手抄系数这种活绝对不要干64个系数抄错一个频响上就会出现一个莫名其妙的毛刺排查起来非常痛苦。3.2 直接型与转置型时序导向的选择FIR最常见两种实现结构直接型和转置型。直接型的结构是延迟链之后接一条乘加链y[n] h0x[n] h1x[n-1] ... hN*x[n-N]。听起来简单但硬件上这条链是串行累加的N阶就N级加法组合逻辑延迟随着N增大最高时钟频率直线下降。我在200MHz下尝试过128阶直接型FIR时序直接跑不过中间需要插很多流水寄存器。转置型刚好解决这个问题。它的每个乘法器都在输入端并行乘出一个部分积然后逐级累加到一个移位寄存器阵列里。关键路径被压到了“一次乘法一次加法”和阶数几乎无关。这个特性在高速ADC接口场景特别重要。下面是一个简化的转置结构示例module fir_transpose #( parameter N 16, parameter W 16 )( input wire clk, input wire signed [W-1:0] x, output reg signed [W*2-1:0] y ); reg signed [W-1:0] coeff [0:N-1]; reg signed [W*2-1:0] acc [0:N-1]; // coeff 需要通过配置接口或rom加载这里示例只赋初值 integer k; initial begin for (k 0; k N; k k 1) coeff[k] 16sd0; // 实际系数由外部加载 end always (posedge clk) begin acc[0] x * coeff[N-1]; for (k 1; k N; k k 1) acc[k] acc[k-1] x * coeff[N-1-k]; end assign y acc[N-1]; endmodule转置型在时序上友好但寄存器数量比直接型多一些。每个acc都要占一个寄存器128阶就有128个32位寄存器对逻辑资源是真金白银的消耗。所以选型时我的原则是主频要求高、阶数大 → 转置型 资源紧张、主频一般 → 直接型3.3 资源优化对称系数与DA查找表线性相位FIR的系数是对称的h[0]h[N]h[1]h[N-1]。这个对称性可以直接用来砍一半乘法器。比如64阶FIR只有32个独立系数每个乘法器算一次乘积再用两次累加资源能省一半。这个优化我几乎每次都会做尤其是DSP Slice紧张的工程。如果你做的是固定系数的FIR还有一个更激进的优化路径分布式算法DA。DA的核心思想是既然系数是常数那么“系数乘以输入位”的结果就可以提前算好放在查找表里。输入数据每个bit判断0或1查表得到部分积再加起来。16位输入每个抽头就对应16个查找表项。DA对乘法器资源几乎零消耗代价是查找表和加法器网络。不过DA不是银弹。当阶数和位宽同时增大时查找表深度会指数膨胀。我的做法是拆成多个小表比如16阶拆成4个4阶子表每个子表2^416项4张表的结果再合并累加彻底避开大表爆炸的问题。这个方法在需要大量相同系数FIR做多通道滤波时非常划算比如波束成形的8路或16路同时滤波一套DA查找表可以被多通道分时复用资源复用率直接拉满。4. 自适应滤波器LMS上板的第一步是稳住收敛4.1 LMS的核心迭代逻辑与μ值的边界自适应滤波器和前两者的最大区别是系数不再固定而是跟随输入和误差在线调整。最常见的算法是LMS权重更新公式很简洁y(n) W(n)^T * X(n) e(n) d(n) - y(n) W(n1) W(n) 2 * μ * e(n) * X(n)其中d(n)是期望信号X(n)是输入向量。FPGA实现LMS的难点不在公式而在把收敛行为搬到定点数上。先看μ的取值范围。简化判据是0 μ 1 / (N * Px)N是滤波器阶数Px是输入信号的平均功率。假设我的自适应滤波器是32阶输入是幅度0.1V的正弦加噪声信号量化到16位后平均功率估算大约0.005归一化。那μ的上界就是1 / (32 * 0.005) 6.25实际工程里这个上界会偏乐观因为真实信号不是平稳白噪声还有相关性。我一般会再除以3~5的安全系数取μ1.5。要是噪声里有强烈的单频干扰比如工频50Hz输入自相关矩阵的特征值分散μ还要再压低。一个常用的工程技巧是不直接写浮点小数μ而是用右移实现。把μ取成2的负整数次幂比如μ2^-12硬件实现就是每次更新完做12次算术右移。这样既省乘法器又能保证固定点更新量的尺度一致。4.2 FPGA实现里的流水线延迟与delayed LMS芯片上做LMS天然有个矛盾LMS要求每个周期都用最新的误差更新权重但设计乘法器链和加法树需要好几拍。等误差算出来输入信号已经流过好多个延迟寄存器根本来不及“回首”更新刚才那批样本。于是工程上普遍允许延迟LMSdelayed LMS用延迟误差更新对应的延迟输入。理论分析证明只要延迟L拍且μ选得足够小算法依然收敛只是收敛上限略低。我的做法是把滤波输出和误差计算放在第一流水段权重更新在第二流水段两个用寄存器隔开。这样每个时钟沿只能看到前一拍的误差但实测收敛曲线依然稳定稳态失调比理论无延迟版多3-5dB可以接受。下面的Verilog片段展示了这种方式module lms_delayed #( parameter N 32, parameter MU_SHIFT 12 )( input wire clk, input wire rst_n, input wire signed [15:0] x, // 参考输入 input wire signed [15:0] d, // 期望信号 output wire signed [15:0] err_out, output wire signed [15:0] y_out ); reg signed [15:0] delay_line [0:N-1]; reg signed [15:0] weights [0:N-1]; reg signed [15:0] y, err; integer i; always (posedge clk or negedge rst_n) begin if (!rst_n) begin for (i 0; i N; i i 1) begin delay_line[i] 16d0; weights[i] 16d0; end end else begin delay_line[0] x; for (i 1; i N; i i 1) delay_line[i] delay_line[i-1]; end end // 延迟LMS误差由上一个周期的weights和延迟线算出 always (posedge clk) begin y 16d0; for (i 0; i N; i i 1) y y weights[i] * delay_line[N-1-i]; err d - y; // 权重延迟更新 for (i 0; i N; i i 1) weights[i] weights[i] ((err * delay_line[N-1-i]) MU_SHIFT); end assign y_out y; assign err_out err; endmodule注意我在权重更新里用了算术右移运算符这是为了防止负误差和负输入相乘后移位时出现符号位错误。很多新手在这里直接用合成出来的是逻辑右移负数直接变正数权重直接发散这是一类典型的“上板就爆”问题。4.3 一个可复现的自适应陷波例子我在一个振动监测项目里做过自适应陷波传感器信号里有一个很强的转轴基频干扰频率会随着转速漂移固定陷波器根本跟不上。我用的LMS结构是两权重的自适应线性组合器参考信号取sin(ωt)和cos(ωt)输出就是这两个信号的加权和。这个结构非常快只需要两个权重收敛速度远远快于大阶数自适应滤波。迭代式可以写成y(n) w1*sin(ωT n) w2*cos(ωT n) e(n) d(n) - y(n) w1 w1 2μ e(n) sin(ωT n) w2 w2 2μ e(n) cos(ωT n)这个结构的好处是不管基频怎么漂移只要ω的估计值能跟上自适应出来的输出就能追踪到干扰频率并对消掉。我在板上用16位定点跑了整整一夜误差的稳态均值稳定在几个LSB级别收敛时间不到1秒。调μ时我大概试了三档最低档收敛太慢最高档直接发散中间档位才稳定这正好对应了前面说的μ要留安全余量的结论。5. 三种滤波器怎么选一张表和一次实测验证5.1 选型决策对照表手里同时实现过三类滤波器之后我整理了一张内部分析表每次接到新需求先套一遍维度IIRFIR自适应LMS资源消耗最低5-7阶可等效FIR数十阶高阶数通常几十到几百中等除了滤波还有权重更新乘法相位特性非线性适合对相位不敏感的降噪、平坦度要求低的场景线性适合需要保形的信号处理跟随参考信号可用于对消而不只滤波稳定性反馈结构定点化后需要重点验证极限环天生稳定受μ影响步长设计错误会发散实现复杂程度中等需要级联和饱和处理低结构直观voralleled optimization较高流水线延迟和定点精度影响收敛适用场景低资源、高过渡带斜率的需求比如传感器信号预调理多通道均衡、相位敏感链路、高速数据流主动降噪、自适应陷波、通道均衡动态更新这张表现在放在我团队内部的Wiki里每次评审方案直接往上套就行。大部分情况下定点化III加FIR的组合能覆盖八成需求自适应滤波器只在干扰特性随时间变化时才上不盲目追求花哨结构。5.2 上板验证流程和一个小技巧滤波器上板我最常遇到的问题是“明明仿真波形干干净净怎么示波器上看就是有毛刺”。这通常不是滤波算法问题而是信号链数据格式问题。我总结了一套快速验证流程跑完基本能定位90%的毛病。第一步先做“直通测试”。把滤波器系数全部设成只有一个系数为1其余为0再套上完整的量化、饱和逻辑。如果输出和输入一致说明整个端到端的位宽、符号扩展、截断逻辑是通的。这一步能排除掉大量低级但致命的连接错误。第二步做“白噪声频响测试”。用白噪声当作输入把FPGA输出拉回PC做FFT求平均。输入白噪声、输出滤波后的功率谱直接对比就能看到滤波器频响曲线。这个方案比手动扫正弦快很多尤其适合过渡带比较窄的滤波器能一次看全频带响应。第三步针对IIR专门做“零输入测试”。把输入端口短接到静默值观察输出是否归零。如果输出出现小余振就用前面讲的阈值强制清零逻辑处理。这个测试虽然简单但能挡住大半“上电零点几秒内输出乱跳”的投诉。提示测FIR的转置结构时顺便量一下复位后前N拍的数据是否出现异常尖峰。转置结构的寄存器阵列需要N个时钟才能填满复位后的头N拍输出没有意义必须在后端加“数据有效”信号否则后续的FFT处理会多出一堆假频点。最后分享一个小技巧。我在做板级验证时会故意用非整数倍频的信号源比如采样率49.92kHz信号源输出1kHz。这样输出信号会随着时间在ADC采样点之间滑动等效于慢慢扫描相位。配合长时间采集能暴露出滤波器在边界相位下的异常表现比如偶发跳变、微小振荡。这种边角测试在纯仿真里往往触发不出来但硬件一致性测试里经常能抓到。最后说点实际的滤波器设计做到后面一半功夫在算法指标一半功夫在量化、时序和调试手段。FPGA的优点是把算力换成并行逻辑缺点是定点实现的坑比浮点仿真多得多IIR的极限环、FIR的时序收敛、LMS的步长边界每一个都是“看起来懂了、上手才翻车”的典型。我个人的习惯是每上一个板卡项目都先把三类滤波器的测试工程各拉一个跑通。真正把IIR、FIR、自适应滤波器的定点模型都调顺之后看别人的实现也快很多。后面有机会再单独整理一篇动态重配置滤波器系数的方案那个话题牵扯到AXI-Lite接口时序和实时切换的平滑过渡展开讲又是几千字先写到这。