🏠

Farrow 滤波器设计演进 (Farrow Filter Design Evolution)

概述:为什么需要这个模块?

想象你正在调制解调器的接收端工作。信号以固定的时间间隔到达,但接收机的时钟与发送机并不同步——就像两个人用不同的节拍器演奏同一首曲子。如果直接采样,样本点可能落在"眼图"的中间偏左或偏右位置,导致符号间干扰(ISI),让数据变得难以辨认。

Farrow 滤波器就是解决这个问题的"时间微调器"。它是一种分数延迟滤波器,能够在不重新加载系数的情况下,实时计算任意小数时间偏移处的信号值。与传统的直接型 FIR 分数延迟滤波器(需要为每个延迟值准备一套系数)不同,Farrow 结构使用固定的多项式系数,通过调整延迟参数 \(u\)\(-0.5 < u < 0.5\))来实现连续可变的延迟。

本模块展示了如何在 AMD Versal AI Engine 上实现一个高性能的 Farrow 滤波器,从最初的功能正确版本逐步优化到满足 1 GSPS(每秒十亿样本)吞吐量的最终实现。这是一个典型的算法→优化→并行化的设计演进案例。


核心概念与心智模型

1. Farrow 结构的数学本质

Farrow 滤波器基于多项式插值。对于三阶多项式(本实现采用),输出可以表示为:

\[y(u) = f_0 + u \cdot (f_1 + u \cdot (f_2 + u \cdot f_3))\]

这就是霍纳法则(Horner's Rule),将原本需要多次乘法和加法的多项式求值简化为仅需 3 次乘法和 3 次加法。在硬件实现中,这意味着:

  • 四个子滤波器\(f_0, f_1, f_2, f_3\)):每个都是 8-tap FIR 滤波器
  • 三级 Horner 级联:将子滤波器输出按延迟参数 \(u\) 加权组合

可以把这想象成一个流水线工厂:四个工作站(子滤波器)同时处理原料(输入信号),然后产品沿着装配线(Horner 级联)逐级加工,最终根据控制参数 \(u\) 调整成品规格。

2. AI Engine 的计算模型

AI Engine 是一个 VLIW(超长指令字)处理器阵列,每个 tile 包含:

  • 标量单元:控制流、地址计算
  • 向量单元:每周期执行 16 个 cint16 × int16 MAC 操作
  • 本地内存:32 KB,支持单周期多 bank 访问
  • 流接口:用于 tile 间低延迟数据传输

理解这一点至关重要:AI Engine 的峰值性能来自于向量单元的充分利用,而瓶颈往往出现在寄存器压力、内存带宽或循环流水线上。

3. 数据类型映射

概念 数据类型 说明
信号样本 cint16 复数,16-bit 实部/虚部
延迟参数 int16 分数延迟 \(u\),定点表示
累加器 cacc48 48-bit 复数累加器,防止中间结果溢出
系数 int16 滤波器抽头系数

架构设计与数据流

整体架构

graph TB subgraph "PL 层 (HLS)" DMA_SRC[farrow_dma_src
数据搬入] DMA_SNK[farrow_dma_snk
数据搬出] end subgraph "AIE 层 - 初始/优化版本" direction TB KERNEL_SINGLE[farrow_kernel
单核实现] end subgraph "AIE 层 - 最终版本" direction TB KERNEL1[farrow_kernel1
子滤波器阶段] --> STREAM1[y3/y2/y1/y0
流缓冲] STREAM1 --> KERNEL2[farrow_kernel2
Horner级联阶段] end DMA_SRC -->|sig_i/del_i| KERNEL_SINGLE KERNEL_SINGLE -->|sig_o| DMA_SNK DMA_SRC -.->|sig_i| KERNEL1 DMA_SRC -.->|del_i| KERNEL2 KERNEL2 -.->|sig_o| DMA_SNK style KERNEL_SINGLE fill:#ffcccc style KERNEL1 fill:#ccffcc style KERNEL2 fill:#ccffcc

数据流详解

初始实现 (farrow_initial)

flowchart LR A[输入信号 sig_i
cint16] --> B[FIR f3
antisym] A --> C[FIR f2
sym] A --> D[FIR f1
antisym] A --> E[FIR f0
sym] B --> F[y3] C --> G[y2] D --> H[y1] E --> I[y0] F --> J[Horner: z2 = y3*u + y2] G --> J J --> K[Horner: z1 = z2*u + y1] H --> K K --> L[Horner: out = z1*u + y0] I --> L M[延迟参数 del_i
int32->int16] --> J M --> K M --> L L --> N[输出 sig_o
cint16]

关键观察

  • 所有四个 FIR 滤波器和 Horner 级联都在同一个循环内完成
  • 每轮迭代处理 16 个样本(向量化宽度)
  • 需要大量向量寄存器保存中间结果(y3, y2, y1, y0, z2, z1)

优化阶段 1 (farrow_optimize1)

主要改进:

  1. 合并状态缓冲区:四个独立的状态数组(f3_state, f2_state, f1_state, f0_state)合并为一个 f_state
  2. 合并系数表:四个独立的系数数组合并为一个 f_taps[16],通过偏移量访问
  3. 简化延迟输入处理:原始实现使用 aie::filter_even()int32 向量中提取偶数位置的 int16,改为直接提供已重排的 int16 数据

这些优化减少了寄存器压力,但仍受限于单核的计算能力。

优化阶段 2 (farrow_optimize2)

关键突破:循环拆分(Loop Fission)

flowchart TB subgraph "循环 1: FIR 阶段" A1[输入] --> B1[FIR f3/f2/f1/f0] B1 --> C1[y3/y2/y1/y0
写入内存] end subgraph "循环 2: Horner 阶段 1" D1[读取 y3/y2] --> E1[z2 = y3*u + y2] E1 --> F1[写入 z2] end subgraph "循环 3: Horner 阶段 2" G1[读取 z2/y1] --> H1[z1 = z2*u + y1] H1 --> I1[写入 z1] end subgraph "循环 4: Horner 阶段 3" J1[读取 z1/y0] --> K1[out = z1*u + y0] end C1 --> D1 F1 --> G1 I1 --> J1

为什么拆分循环能提高性能?

  1. 降低寄存器压力:每个小循环只需要少量向量寄存器,避免"寄存器溢出"到栈内存
  2. 更好的流水线:编译器可以更激进地调度指令,因为依赖关系更局部化
  3. 利用 SRS 路径:AI Engine 的向量-累加器乘法需要通过 Shift-Round-Saturate (SRS) 路径转换,拆分为独立循环允许这种转换在循环边界完成

代价是引入了额外的内存流量(中间结果需要写入/读出 tile 内存)。

最终实现 (farrow_final)

当优化阶段 2 的 II(Initiation Interval)总和超过目标(25 > 16)时,必须采用空间并行化

flowchart LR subgraph "Tile 1 (25_0)" K1[farrow_kernel1] end subgraph "Tile 2 (24_1)" K2[farrow_kernel2] end SIG[输入信号] --> K1 K1 -->|y3,y2,y1,y0
ping-pong缓冲| K2 DEL[延迟参数] --> K2 K2 --> OUT[输出]

双核分工

  • Kernel1:负责四个 FIR 子滤波器(计算密集型)
  • Kernel2:负责三级 Horner 级联(数据流型)

两者通过显式的 ping-pong buffer 连接,实现了真正的流水线并行。


设计决策与权衡分析

1. 单核 vs. 多核

方案 优点 缺点 适用场景
单核 低延迟、简单、无通信开销 计算受限、II 难达标 低吞吐量需求
双核 满足高吞吐量、模块化 增加缓冲、延迟增加 1 GSPS 目标

决策理由:当单核优化达到极限(II=25 无法降到 16),必须引入第二个 tile。这是典型的计算并行化策略。

2. 流(Stream)vs. 缓冲(Buffer)

本设计使用 connect<>(隐式缓冲)而非 connect<stream>。原因:

  • 缓冲允许异步执行:Kernel1 可以批量产生数据,Kernel2 批量消费,平滑流量波动
  • 支持 ping-pong 机制:自动的双缓冲防止读写冲突
  • 代价:增加了内存占用和延迟

3. 定点数缩放(DNSHIFT = 14)

所有 FIR 输出和 Horner 中间结果都经过 14-bit 右移(相当于除以 \(2^{14}\))。这是为了:

  • 防止溢出:8-tap FIR 累加 8 个乘积,每个乘积最大约 \(2^{15} \times 2^{15} = 2^{30}\)
  • 保持精度:14-bit 移位保留了足够的有效位,同时避免累加器溢出

风险:如果输入信号幅度过大,仍可能发生饱和。实际部署时需要验证动态范围。

4. 代码优化技术选择

技术 应用场景 效果
chess_prepare_for_pipelining 所有循环 提示编译器尝试软件流水线
chess_loop_range(1,) 外层循环 告知编译器最小循环次数,帮助调度
__restrict 指针声明 消除别名分析顾虑,允许激进优化
alignas(32) 数组声明 确保 256-bit 对齐,满足向量加载要求

关键组件详解

farrow_kernel(初始/优化版本)

位于 aie/farrow_initial/aie/farrow_optimize1/aie/farrow_optimize2/

职责:完整的 Farrow 滤波功能,从输入信号和延迟参数产生滤波输出。

关键 API 使用

// 对称/反对称 FIR 计算
aie::sliding_mul_sym_xy_ops<8,8,1,1,int16,cint16>::mul_antisym(coeffs, offset, buffer, start);
aie::sliding_mul_sym_xy_ops<8,8,1,1,int16,cint16>::mul_sym(coeffs, offset, buffer, start);

// Horner 级联乘法累加
aie::mac(acc, vec1, vec2);  // acc += vec1 * vec2

类型定义

  • TT_SIG = cint16:信号样本类型
  • TT_DEL = int32:延迟参数输入类型(实际使用 int16)
  • TT_ACC = cacc48:复数累加器类型

farrow_kernel1 / farrow_kernel2(最终版本)

位于 aie/farrow_final/

farrow_kernel1

  • 输入sig_i(信号)
  • 输出y3, y2, y1, y0(四个子滤波器结果)
  • 计算:四个 8-tap FIR,使用 sliding_mul_sym_xy_ops

farrow_kernel2

  • 输入del_i(延迟参数)、y3, y2, y1, y0(来自 kernel1)
  • 输出sig_o(最终结果)
  • 计算:三级 Horner 级联

内存优化技巧z 数组被复用于存储 z2 和 z1(顺序访问,不重叠),节省 50% 的中间存储。

PL 层 DMA 内核(HLS)

位于 hls/farrow_dma_src/hls/farrow_dma_snk/

farrow_dma_src

  • 从 DDR 加载数据到 PL BRAM
  • 以流方式发送到 AIE
  • 支持循环重复(loop_cnt

farrow_dma_snk

  • 从 AIE 接收流数据
  • 选择性捕获特定循环(loop_sel
  • 写回 DDR

关键技术#pragma HLS DATAFLOW 启用任务级并行,load_buffertransmit 重叠执行。


性能演进数据

版本 II(周期) 原始吞吐量 备注
initial 123 ~205 MSPS 功能正确,性能未优化
optimize1 82 ~301 MSPS 寄存器优化
optimize2 25(总和) ~768 MSPS 循环拆分,单核极限
final 16 + 9 ~1150 MSPS 双核并行,满足 1 GSPS 目标

注:final 版本的 II=16(kernel1)+ 3+3+3(kernel2 的三个循环)= 25,但通过双核并行,有效吞吐量达到目标。


新贡献者注意事项

1. 隐式契约与约束

  • 缓冲区大小BUFFER_SIZE = 1024 是硬编码的,修改需要同步更新 MATLAB 测试向量生成脚本
  • 数据对齐:所有数组必须使用 alignas(32),否则向量加载会崩溃或性能骤降
  • 延迟参数格式:optimize1 及之后版本期望 del_i 是重排后的格式(见 gen_vectors.m

2. 常见陷阱

问题 症状 解决方案
忘记设置 rounding/saturation 数值误差大 构造函数中调用 aie::set_rounding/set_saturation
向量寄存器溢出 II 异常高 减少循环内活跃变量,或拆分循环
内存 bank 冲突 性能不达标 使用 alignas(32)_restrict 关键字
仿真通过但硬件失败 时序/资源问题 检查 runtime<ratio> 设置,确保 ≤ 0.9

3. 调试建议

  1. x86 仿真先行:始终先运行 make x86all 验证功能正确性
  2. vitis_analyzer 分析:查看 Array View 确认内存布局,Trace View 测量实际吞吐量
  3. II 提取:使用 scripts/get_loop_II.py 自动化提取各循环的 Initiation Interval
  4. MATLAB 验证check_sim_output.m 对比仿真输出与黄金参考

4. 扩展方向

  • TDM 实现:当前是 1 GSPS 单通道,可通过时分复用实现多路 lower-rate 通道
  • 自适应延迟:当前延迟参数是预设的,可扩展为接收机驱动的自适应更新
  • 更高阶多项式:当前是三阶,可增加至五阶以提高插值精度(需更多计算资源)

相关模块


参考文献

  1. F. M. Gardner, "Interpolation in digital modems. I. Fundamentals," IEEE Trans. Communications, 1993
  2. C. W. Farrow, "A continuously variable digital delay element," IEEE ISCAS, 1988
  3. Horner's Method
  4. AMD Versal Adaptive SoC AI Engine Architecture Manual (AM009)
  5. AI Engine Kernel and Graph Programming Guide (UG1079)
On this page