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 滤波器基于多项式插值。对于三阶多项式(本实现采用),输出可以表示为:
这就是霍纳法则(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 × int16MAC 操作 - 本地内存:32 KB,支持单周期多 bank 访问
- 流接口:用于 tile 间低延迟数据传输
理解这一点至关重要:AI Engine 的峰值性能来自于向量单元的充分利用,而瓶颈往往出现在寄存器压力、内存带宽或循环流水线上。
3. 数据类型映射
| 概念 | 数据类型 | 说明 |
|---|---|---|
| 信号样本 | cint16 |
复数,16-bit 实部/虚部 |
| 延迟参数 | int16 |
分数延迟 \(u\),定点表示 |
| 累加器 | cacc48 |
48-bit 复数累加器,防止中间结果溢出 |
| 系数 | int16 |
滤波器抽头系数 |
架构设计与数据流
整体架构
数据搬入] 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)
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)
主要改进:
- 合并状态缓冲区:四个独立的状态数组(
f3_state,f2_state,f1_state,f0_state)合并为一个f_state - 合并系数表:四个独立的系数数组合并为一个
f_taps[16],通过偏移量访问 - 简化延迟输入处理:原始实现使用
aie::filter_even()从int32向量中提取偶数位置的int16,改为直接提供已重排的int16数据
这些优化减少了寄存器压力,但仍受限于单核的计算能力。
优化阶段 2 (farrow_optimize2)
关键突破:循环拆分(Loop Fission)
写入内存] 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
为什么拆分循环能提高性能?
- 降低寄存器压力:每个小循环只需要少量向量寄存器,避免"寄存器溢出"到栈内存
- 更好的流水线:编译器可以更激进地调度指令,因为依赖关系更局部化
- 利用 SRS 路径:AI Engine 的向量-累加器乘法需要通过 Shift-Round-Saturate (SRS) 路径转换,拆分为独立循环允许这种转换在循环边界完成
代价是引入了额外的内存流量(中间结果需要写入/读出 tile 内存)。
最终实现 (farrow_final)
当优化阶段 2 的 II(Initiation Interval)总和超过目标(25 > 16)时,必须采用空间并行化:
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_buffer 和 transmit 重叠执行。
性能演进数据
| 版本 | 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. 调试建议
- x86 仿真先行:始终先运行
make x86all验证功能正确性 - vitis_analyzer 分析:查看 Array View 确认内存布局,Trace View 测量实际吞吐量
- II 提取:使用
scripts/get_loop_II.py自动化提取各循环的 Initiation Interval - MATLAB 验证:
check_sim_output.m对比仿真输出与黄金参考
4. 扩展方向
- TDM 实现:当前是 1 GSPS 单通道,可通过时分复用实现多路 lower-rate 通道
- 自适应延迟:当前延迟参数是预设的,可扩展为接收机驱动的自适应更新
- 更高阶多项式:当前是三阶,可增加至五阶以提高插值精度(需更多计算资源)
相关模块
- farrow_baseline_graph:初始功能实现
- farrow_optimization_stage_1_graph:第一次优化迭代
- farrow_optimization_stage_2_graph:第二次优化迭代
- farrow_final_implementation_graph:最终双核实现
- farrow_filter_streaming_io_integration:系统级集成与 DMA 配置
参考文献
- F. M. Gardner, "Interpolation in digital modems. I. Fundamentals," IEEE Trans. Communications, 1993
- C. W. Farrow, "A continuously variable digital delay element," IEEE ISCAS, 1988
- Horner's Method
- AMD Versal Adaptive SoC AI Engine Architecture Manual (AM009)
- AI Engine Kernel and Graph Programming Guide (UG1079)