简介:围绕基于快速FIR算法(FFA)的高效2n并行FIR滤波器设计,文档整理了从理论推导到高并行度实现架构的完整内容,适合数字信号处理、通信与雷达系统、集成电路设计方向的研究生、工程师及算法实现人员参考。内容从2、4、8并行FIR滤波器的经典形式出发,归纳出通用2n并行FFA表达式,明确预加矩阵、滤波器系数矩阵、后级加法及延时矩阵的构造规律,并给出160并行FFA实现架构与硬件复杂度评估思路,可用于高速滤波、滤波器组等场景的资源与功耗优化。资源包仅含1个docx文档,约185KB,便于下载查阅与二次整理;已有86人学习,在并行FIR算法方向具有一定参考价值。读者可获取2n并行算法的推导脉络、矩阵算子定义、非2n并行结构设计要点及高并行度FIR硬件效率分析框架,适合希望补齐FFA通用化设计思路、对照经典2/4/8并行结构并拓展至更高并行度的读者。
1. 高并行FIR的乘法器账单,为什么不是线性涨
高速光通信接收机里挂一个7阶160并行FIR,按直接展开的老办法铺下去,160条支路每条都要一份完整的8抽头乘法网络,账面上就是1280个乘法器,时序和布线立刻吃紧。可换个角度算,用快速FIR算法(FFA)把并行结构顶上去,同样160并行只需要约3^n·N/2^n量级的乘法器,n=5 时相对传统直接并行能省掉七成以上。这不是近似拟合,而是把一部分乘法运算搬到了预加和后加的加法器上:2抽头2并行原本要4个乘法器,FFA 用3个就干完了,然后把这个思路递归到2^n并行。这份文档做的事情,是把散落在2、4、8并行里的矩阵公式收敛成2^n并行的通用形式,再往下推到160并行这种非2^n的滤波器组架构,最后落到 Xilinx XC7K325T 上的资源账。适合做高速 DSP 链路、FPGA 滤波器组、被乘法器资源卡住的硬件工程师。
2. 从2并行切入:预加矩阵P、系数矩阵H与后加矩阵Q的分工
2.1 三个乘法器替掉四个的推导过程
设滤波器单位冲激响应h(n)长度为 N,按奇偶拆成两个多相分量 H0、H1,输入也拆成 X0、X1。直接写多相形式:
Y0 = H0·X0 + z^-1·H1·X1 Y1 = H1·X0 + H0·X1右侧一共四项子滤波乘法,每个子滤波器 N/2 抽头,合起来就是 2N 个乘法器。把中间项重组一下:
m0 = H0·X0 m1 = H1·X1 m2 = (H0 + H1)·(X0 + X1) Y0 = m0 + z^-1·m1 Y1 = m2 - m0 - m1乘法只剩三个 N/2 抽头子滤波,合计 3N/2 个乘法器,比原来的 2N 省了 25%。代价是多出一次输入预加X0+X1、一次系数预加H0+H1,以及后级那几个加减法。这就是 FFA 的全部秘密:用加法换乘法,然后把这件事递归做下去。
2.2 P2、H2p、Q2 三个矩阵各管什么
把上面的推导写成矩阵形式就是Y2p = Q2·H2p·P2·X2p,三个矩阵的分工很清楚。
| 矩阵 | 维度 | 职责 | 元素含义 |
|---|---|---|---|
| P2 | 3×2 | 输入预加 | 每行对应一个子滤波通道的输入组合 |
| H2p | 3×3 对角 | 子滤波 | 对角元为 P2·h2 的各分量 |
| Q2 | 2×3 | 后加与延时 | 把3路子滤波结果合成2路输出,含 z^-1 |
系数矩阵写成H2p = diag(P2·h2),其中h2 = [H0, H1]^T。展开看,对角线上就是 H0、H1、H0+H1 三个子滤波器,正好对应预加矩阵 P2 作用在系数向量上得到的三行。后加矩阵 Q2 的每一行是一个输出通道的组合规则,z^-1出现在需要跨相位的支路上。这里用抽头延迟 z^-1 的约定,有些文献把 Z 变换的自变量按抽取后的采样率定义,延时项会写成 z^-2,两者在硬件上是同一个移位寄存器组,只是坐标不同。
2.3 用 NumPy 对着直接卷积验证2并行结构
纸面推导容易漏项,跑一遍代码最省事。下面这段把 FFA 2并行和直接卷积放在一起比:
import numpy as np def ffa2(h, x): """2 并行 FFA FIR:3 个 N/2 抽头子滤波器替代 4 个""" h0, h1 = h[0::2], h[1::2] # 偶、奇多相分量 hsum = h0 + h1 # 系数预加,对应第三个子滤波器 x0, x1 = x[0::2], x[1::2] # 偶数路、奇数路输入 xsum = x0 + x1 # 输入预加 m0 = np.convolve(h0, x0) # H0·X0 m1 = np.convolve(h1, x1) # H1·X1 m2 = np.convolve(hsum, xsum) # (H0+H1)(X0+X1) L = len(m0) y0 = m0 + np.r_[0, m1[:-1]] # Y0 = m0 + z^-1·m1 y1 = m2 - m0 - m1 # Y1 = m2 - m0 - m1 y = np.empty(2 * L) y[0::2] = y0 # 偶数输出 y[1::2] = y1 # 奇数输出 return y rng = np.random.default_rng(0) h = rng.normal(size=16) x = rng.normal(size=64) y_ffa = ffa2(h, x) y_ref = np.convolve(h, x) k = min(len(y_ffa), len(y_ref)) print(np.max(np.abs(y_ffa[:k] - y_ref[:k])))h[0::2]和h[1::2]完成系数多相分解,这一步对应 P2 作用在系数向量上;x[0::2]、x[1::2]是对输入按相位抽取;np.r_[0, m1[:-1]]就是给 m1 补一个采样周期实现 z^-1,不能省,省了就变成零相位对齐,误差会立刻出现。跑出来的最大误差在 1e-15 量级,说明结构等价。如果误差是 1e-2 以上,先查延时项有没有加,再查hsum是不是用 h0、h1 相加而不是原序列相加。
2.4 4并行与8并行:张量积把矩阵撑大
4并行的预加矩阵直接由张量积给出P4 = P2 ⊗ P2,8并行是P8 = P2 ⊗ (P2 ⊗ P2)。系数矩阵依然是H4p = diag(P4·h4),只不过h4 = [H0, H2, H1, H3]^T,注意下标是 0、2、1、3 而不是 0、1、2、3。输入输出同理:X4p = [X0, X2, X1, X3]^T,Y4p = [Y0, Y2, Y1, Y3]^T;8并行时X8p = [X0, X4, X2, X6, X1, X5, X3, X7]^T。这个看起来别扭的顺序是张量积的副作用,硬件里就是一次通道重排,不影响功能,但写 RTL 时索引写错会直接让输出错位。
后加矩阵的层数也随之增加。2并行是一层 Q2,4并行是Q4 = Q42·(I3×3 ⊗ Q41)两层,8并行到三层Q8 = Q83·(I3×3 ⊗ Q82)·[I3×3 ⊗ (I3×3 ⊗ Q81)]。每一层负责一次两级合并,延时项的指数也从 z^-2 涨到 z^-8,因为每级合并处理的数据块变大了。
3. 2^n 并行FFA的通用递推:P2n、H2np、Q2n 怎么生成
3.1 输入输出通道的位反转排列规律
把 2、4、8 三种情况的下标列出来:2并行是 0,1;4并行是 0,2,1,3;8并行是 0,4,2,6,1,5,3,7。这不是随手排的,把下标写成 n 位二进制再整体反转就能得到:8并行时 1 写成 001,反转得 100 就是 4;2 写成 010,反转还是 010 就是 2;3 写成 011,反转得 110 就是 6。整组顺序正是位反转序。
def bitrev_order(n): """2^n 并行输入/输出通道的位反转排列下标""" idx = list(range(2 ** n)) return sorted(idx, key=lambda v: int(format(v, f'0{n}b')[::-1], 2)) for n in range(1, 5): print(n, bitrev_order(n))这段代码的输出与前面的X4p、X8p完全一致。硬件上的意义是:预加网络的连线不是顺着排的,而是按位反转把相隔2^{n-1}的通道拉到一起,第一级合并距离最远的两个通道,最后一级合并在相邻通道之间。
3.2 预加矩阵与系数向量的递归构造
预加矩阵的递推就是张量积重复:P2n = P2 ⊗ P2 ⊗ … ⊗ P2,一共 n-1 次张量积。矩阵规模从 2并行的 3×2,到 4并行的 9×4,到 8并行的 27×8,行数是 3^n,列数是 2^n。行数正好等于子滤波器个数,这就是乘法器降下来的根源:传统直接并行需要 2^n 个 N 抽头滤波器,FFA 需要 3^n 个 N/2^n 抽头滤波器。
import numpy as np P2 = np.array([[1, 0], [0, 1], [1, 1]], dtype=int) def build_P(n): """生成 2^n 并行 FFA 的预加矩阵,形状 (3^n, 2^n)""" P = P2 for _ in range(n - 1): P = np.kron(P2, P) # P2 ⊗ P,左乘保持递推顺序 return P for n in range(1, 5): P = build_P(n) print(f"n={n} P.shape={P.shape} 子滤波器数={3**n} 抽头={2**n}")np.kron(P2, P)的顺序不能反,反了行排列会变,虽然后加矩阵能补偿回来,但和后面 Q 的块结构对不上,调试时会绕远路。系数向量h2n按同样的位反转顺序取多相分量,再乘上 P2n 得到H2np = diag(P2n·h2n)的对角线。
3.3 后加矩阵 Q2n 的分层递推与 A_k 矩阵
后加矩阵是三层结构里最绕的部分,但规律是死的。第 k 层Q2n^k是2^k × 2^{k+1}的块矩阵,由I、-I和A_k三种块拼成:上半是单位块、零块、延时块A_k,下半是负单位块、正单位块、负单位块。
A_k本身也在递推:A1 = z^{-2^n},之后每一层把上一层的A_{k-1}嵌进一个2^k × 2^k的块阵里,空出来的位置补零。延时项的指数在每一层都是z^{-2^n},不会随层数变化,变的只是它嵌在块矩阵里的位置。这一点容易搞混,因为 2并行时写的是 z^-2,8并行时写的是 z^-8,让人误以为每层指数会翻倍,实际上是指数由并行度一次性定死,层数只决定块结构。
提示:实现时把
A_k单独写成一个带2^k抽头延迟线的模块,比在每个加法器上挂零散的延时寄存器好维护,综合工具也更容易识别成 SRL。
3.4 结构规模与乘法器数量的闭式表达
把 n 层递推的结果汇总,规模数字很干净:预加后得到 3^n 路中间信号,子滤波模块是 3^n 个 N/2^n 抽头的滤波器,后加模块把 3^n 路合成 2^n 路输出。乘法器总数就是3^n·N/2^n,而传统直接并行是2^n·N,比值(3/4)^n随 n 单调下降。
| 并行度 2^n | 子滤波器数 3^n | 每个子滤波器抽头数 | 乘法器总数 |
|---|---|---|---|
| 2 | 3 | N/2 | 3N/2 |
| 4 | 9 | N/4 | 9N/4 |
| 8 | 27 | N/8 | 27N/8 |
| 16 | 81 | N/16 | 81N/16 |
| 32 | 243 | N/32 | 243N/32 |
这张表和后面第4章的节约率是同一件事的两种看法:乘法器绝对数在涨,但涨的是3^n/2^n而不是2^n,乘上抽头数变小之后总账就下来了。
4. 160并行不是2^n怎么办:滤波器组架构与FPGA资源账
4.1 160 = 20 × 8 的拆分与模块复用
高速光通信要的并行度经常是 160 这种数,它不是 2 的幂,套不进2^n的公式。常见做法是拆成低并行度模块的组合:160 = 20 × 8,用 20 个基于 FFA 的8并行滤波器组成一个160并行滤波器。每个8并行模块内部按第3章的递推搭,模块之间靠输入通道分组和输出抽取关系拼接。输入的160路数据按 8 路一组分成 20 组,系数序列也相应分成 20 段,每段对应一个8并行子模块的 8 个多相分量。
这样拆的好处是所有子模块的 RTL 完全一样,改一个参数就能改并行度,验证只需要验一个模块加一层顶层的连线。另一种拆法是160 = 32 × 5,或者用 4并行和8并行混合,但混合拆分会让子模块种类变多,DFT 和形式验证的工作量成倍涨,实际项目里更倾向于统一拆分粒度。
4.2 两种方案下的乘法器与加法器数量对比
原文给出的统计口径是:L 并行 N 抽头滤波器,传统直接并行需要 L·N 个乘法器,FFA 需要3^n·N/2^n个。
| 并行 FIR | 2 并行 | 4 并行 | 8 并行 | 16 并行 | 32 并行 | 2^n 并行 |
|---|---|---|---|---|---|---|
| 传统并行 | 2N | 4N | 8N | 16N | 32N | 2^n·N |
| FFA | 3N/2 | 9N/4 | 27N/8 | 81N/16 | 243N/32 | 3^n·N/2^n |
| 节约比例 | 25.00% | 43.75% | 57.81% | 68.36% | 76.27% | [1-(3/4)^n]×100% |
节约比例的通式[1-(3/4)^n]×100%说明并行度越高收益越大,但边际收益在递减:从2并行到4并行多省了将近19个点,从16并行到32并行只多省不到8个点。选并行度的时候要拿这条曲线和时序余量一起看,不是越高越好。
4.3 预加溢出的位宽代价
FFA 在前级有预加网络,这是它和传统多相结构最大的差别。16并行时前级最多有16个数相加,如果加法器仍然沿用传统结构的 m 位整数位宽,必然溢出。要把整数位宽拓到 m+4 位,因为 16 个数相加最多让位宽涨 4 位。推广到2^n并行,整数位宽增量就是 n 位。
| 并行度 | 预加最大项数 | 整数位宽增量 |
|---|---|---|
| 2 | 2 | 1 bit |
| 4 | 4 | 2 bit |
| 8 | 8 | 3 bit |
| 16 | 16 | 4 bit |
| 2^n | 2^n | n bit |
位宽一涨,加法器和乘法器的单器件面积都会变大,而且相同并行度下 FFA 的加法器个数本来就多于传统方式。但因为乘法器省下来的量大,总账还是划算的,只是做面积预算的时候不能只数乘法器个数,得把位宽折进去。
注意:预加位宽不足产生的不是随机噪声,而是削顶,在信号幅度大的时候表现为输出端出现平坦段,频谱上是一片谐波。定点仿真必须用满量程附近的激励去逼,小信号测不出来。
4.4 XC7K325T 上的资源折算结果
原文针对高速光纤通信的7阶160并行FIR在 Xilinx XC7K325T 上做了资源评估,乘法器 IPCore 用 Multiplier 11.2 里的常系数乘法器,按查找表 LUT 等效折算。结论是:基于 FFA 的方法比传统方法的乘法器资源缩减 56.3%,总资源缩减 36.2%。注意总资源缩减比例明显小于乘法器缩减比例,原因就在上一节的位宽和加法器增量上——乘法器省下来的那部分被预加和后加网络的加法器吃回去了一截。这 36.2% 是含加法器在内的净收益,比只看乘法器的数字更接近真实。
5. 定点位宽与结构正确性的验证套路
5.1 用随机激励做逐位对齐的定点比对
浮点验证只能证明结构等价,证明不了定点实现没问题。我一般会把第2章那段 FFA 代码改成定点版本,在卷积前对输入和系数做量化,再和定点直接卷积比。
import numpy as np def quant(x, frac, int_w): """1 位符号 + int_w 位整数 + frac 位小数的饱和量化""" scale = 2 ** frac lo, hi = -2 ** (int_w + frac), 2 ** (int_w + frac) - 1 v = np.round(x * scale) return np.clip(v, lo, hi) / scale rng = np.random.default_rng(1) h = quant(rng.normal(size=16), frac=10, int_w=2) x = quant(rng.normal(size=64) * 0.9, frac=10, int_w=2)frac决定小数精度,int_w决定动态范围,两个参数一起决定定点误差的量级。系数用int_w=2是因为滤波器系数归一化后基本落在 ±1 附近,输入乘 0.9 是故意逼到接近满量程,看预加网络有没有溢出。比较两路输出的差值,如果误差随输入幅度增大而跳变,基本就是位宽不够。
5.2 乘法器账先算后写 RTL
写代码之前先拿3^n·N/2^n这个式子把预算做出来。比如7阶160并行,如果按 20 个8并行模块拼,每个8并行模块是 27 个 8 抽头子滤波器,单个模块乘法器 27×8=216 个,20 个模块 4320 个;换成4并行拼需要 81×8=648 个每模块,40 个模块就是 25920 个,差了一个数量级。这个账五分钟能算完,但能省掉一轮推翻架构的时间。算完再去看 XC7K325T 的 DSP48 资源和 LUT 余量,确认放得下再动手写 RTL,比写完综合一遍再返工快得多。
本文还有配套的精品资源,点击获取