Systolic FIR Filtering(Vitis-HLS-Introductory-Examples)算法深度解析
1. 问题陈述(Problem Statement)
给定长度为 $N$ 的 FIR(Finite Impulse Response)滤波器系数向量 $\mathbf{h}=[h_0,\dots,h_{N-1}]$、输入离散时间序列 $x[n]$ 与偏置项 $b$,目标是在高频 FPGA 平台上实现高吞吐、可流水化的滤波计算,输出:
$$ y[n] = b + \sum_{k=0}^{N-1} h_k,x[n-k]. $$
在 Vitis HLS 的 systolic_fir.cpp 中,问题被进一步约束为:
- 通过 DSP intrinsic 的
cascade结构构建 tap 链; - 使用
#pragma HLS unroll将 tap 级联静态展开; - 以最小控制开销实现每采样周期(理想)一个输出的流式计算。
2. 直觉(Intuition)
朴素 FIR 实现通常采用“移位寄存器 + for 循环累加”,在软件上是自然的,但在高频 HLS 中会遇到两类瓶颈:
- 关键路径过长:串行 MAC 链难以满足高时钟;
- 调度/访存冲突:若延迟线与累加共享资源,II(initiation interval)难降到 1。
关键洞见是:将 FIR 映射为systolic(脉动)级联结构,让每个 tap 对应一个独立 PE(Processing Element, 常由 DSP48 承载),数据与部分和沿级联通道传播。这样:
- 计算拓扑固定;
- 局部连线短;
- 易于插入寄存器并实现深流水。
这与经典的 Kung-Leiserson 脉动阵列思想一致:以规则数据流换取高并行与可时序收敛性。
3. 形式化定义(Formal Definition)
设第 $j$ 个 PE 的输入为 $(b_{j-1}, p_{j-1})$,输出为 $(b_j, p_j)$。定义递推:
$$ \begin{aligned} b_0 &= x[n], \ p_0 &= h_0 b_0 + b, \ b_j &= b_{j-1}, \quad j=1,\dots,N-1,\ p_j &= h_j b_{j-1} + p_{j-1}, \quad j=1,\dots,N-1,\ y[n] &= p_{N-1}. \end{aligned} $$
若将内部寄存器(REG_A1/REG_B1/REG_B2/REG_M/REG_P)建模为固定延迟算子 $\mathcal{D}$,则每个 PE 形式为:
$$ (b_j, p_j) = \mathcal{D}j\big(h_j, b{j-1}, p_{j-1}\big), $$
整体形成线性 pipeline,吞吐率由最慢级和握手协议决定。
4. 算法(Algorithm)
4.1 伪代码
Input: coeff[0..N-1], bias, input_sample
State: dsp0, dsp_full[0..N-2] // each PE has internal pipeline registers
function FIR_STEP(input_sample):
out = dsp0.mul_add(coeff[0], input_sample, bias)
for j from 1 to N-1 do in parallel-unrolled form:
out = dsp_full[j-1].mul_add(coeff[j], out.bcout, out.pcout)
return out.pcout
4.2 实际实现(节选)
class FIR{
private:
const long* coeff_;
long bias_;
cascade<REG_A1|REG_B1| REG_M|REG_P > dsp0;
cascade<REG_A1|REG_B1|REG_B2|REG_M|REG_P > dsp_full[size - 1];
public:
FIR(const long* coeff, long bias) : coeff_(coeff), bias_(bias) {
#pragma HLS ARRAY_PARTITION variable=dsp_full complete dim=1
};
long fir(B_t input){
auto out = dsp0.mul_add(coeff_[0], input, bias_);
for(int j=1; j < size ; j++){
#pragma HLS unroll
out = dsp_full[j - 1].mul_add(coeff_[j], out.bcout, out.pcout);
}
return out.pcout;
};
};
4.3 执行流程(flowchart)
4.4 状态转移(stateDiagram-v2)
4.5 数据结构关系(graph)
5. 复杂度分析(Complexity Analysis)
令 tap 数为 $N$。
软件语义(按一次 fir() 调用计)
- 时间复杂度:
$$ T(N)=\Theta(N) $$ - 空间复杂度(显式存储,不含系数表):
$$ S(N)=\Theta(N) $$ (dsp_full为长度 $N-1$ 的级联对象数组)
硬件映射语义(HLS 展开后)
- 乘法器数量(DSP 数)约为 $N$;
- 组合路径由局部 PE + 级间寄存器决定,非线性累积被寄存器切断;
- 在足够流水下,可达: $$ \text{II} \approx 1 $$
- 首输出延迟(latency)近似随级数线性增长: $$ L(N)\approx L_0 + \sum_{j=1}^{N-1} L_j = \Theta(N) $$ 但吞吐率可保持常数级(每周期一采样)。
Best/Worst/Average:该结构为确定性数据通路,无分支数据相关性,三者在渐近复杂度上基本一致。
6. 实现说明(Implementation Notes)
-
理论与代码的差异
理论 FIR 常显式写出 $x[n-k]$ 延迟线;此实现不直接维护软件移位数组,而通过cascade对象内部寄存与bcout/pcout传播实现时序对齐。 -
#pragma HLS unroll的作用
循环被完全展开,形成静态 PE 链,而非单 MAC 复用。换言之,以面积换吞吐/频率。 -
ARRAY_PARTITION complete的作用
保证dsp_full每个元素独立可并发访问,避免数组端口限制阻碍展开。 -
寄存器配置不对称
dsp0与dsp_full的REG_B2配置不同,通常用于级间延迟配平与时序收敛(工程化折中,而非纯数学需求)。
7. 对比分析(Comparison)
| 方案 | 结构 | 吞吐潜力 | 资源开销 | 高频可达性 |
|---|---|---|---|---|
| 朴素串行 FIR | 单 MAC 循环复用 | 低(常需 $N$ 周期/样本) | 低 | 中 |
| 对称系数 FIR(线性相位) | 预加 + MAC | 中-高 | 中 | 中-高 |
| 本例 Systolic FIR | 全展开 DSP 级联 | 高(II≈1) | 高(约 N DSP) | 高 |
| FFT 分块卷积 | 频域乘法 | 大 $N$ 时高 | 高(控制/缓存复杂) | 取决于系统 |
与经典 Direct Form 相比,本例更接近硬件友好的 Transposed/Systolic 映射:牺牲面积,换取规则数据流与高时钟下稳定吞吐。这正是 FPGA DSP 密集内核在工程中最常见的优化路线。