🏠

Farrow 滤波器流式 IO 集成模块

概述

Farrow 滤波器流式 IO 集成模块是 Versal ACAP 平台上 AI Engine (AIE) 与可编程逻辑 (PL) 之间的高速数据搬运层。想象一下,它就像是连接两个高速运转城市的专用货运铁路系统——AI Engine 是计算密集型工厂,而 PL 则是负责原材料输入和成品输出的物流枢纽。

该模块的核心使命是解决一个关键问题:如何在保持 1+ Gsps 采样率的同时,将大量数据从外部 DDR 内存高效地输送到 AIE 进行实时分数延迟滤波处理,并将结果回写。这不是简单的"搬运工"角色,而是一个精心编排的流水线系统,需要在时钟域转换、数据宽度匹配和吞吐量保证之间取得精妙平衡。

flowchart LR subgraph PL["Programmable Logic (PL) @ 312.5 MHz"] SRC1[farrow_dma_src
数据源1] -->|128-bit AXI-Stream| PLIO0[PLIO_i_0] SRC2[farrow_dma_src
数据源2] -->|128-bit AXI-Stream| PLIO1[PLIO_i_1] SNK[farrow_dma_snk
数据汇聚] <-->|128-bit AXI-Stream| PLIO2[PLIO_o_0] end subgraph AIE["AI Engine Array @ 1250 MHz"] K1[farrow_kernel1
FIR滤波阶段] -->|y3,y2,y1,y0| K2[farrow_kernel2
Horner规则阶段] K2 --> sig_o[输出信号] end DDR[(LPDDR4)] -.->|AXI4-MM| SRC1 DDR -.->|AXI4-MM| SRC2 SNK -.->|AXI4-MM| DDR PLIO0 --> K1 PLIO1 --> K2 PLIO2 --> SNK style PL fill:#e1f5fe style AIE fill:#fff3e0 style DDR fill:#f3e5f5

架构设计:三层数据流模型

第一层:DDR ↔ PL 的数据搬运(DMA 引擎)

系统的起点和终点都是 LPDDR4 内存。为了维持高吞吐量,我们使用两个独立的 HLS DMA 源核 (dma_src1, dma_src2) 和一个 DMA 汇聚核 (dma_snk)。这种分离不是随意的——它反映了 Farrow 滤波器的双输入特性:一路是信号样本 (sig_i),另一路是延迟参数 (del_i)。

关键设计决策:为什么选择 128-bit 数据宽度?

  • AIE 运行在 1250 MHz,每个 PLIO 端口每周期传输 32-bit
  • PL 运行在 312.5 MHz(即 1250/4)
  • 通过 4:1 的时钟比,128-bit @ 312.5 MHz = 32-bit @ 1250 MHz
  • 这样实现了时钟域的自然对齐,无需复杂的异步 FIFO

第二层:PL ↔ AIE 的流式桥接(PLIO 接口)

PL 和 AIE 之间的通信通过 AXI4-Stream 协议完成。这里的关键抽象是 PLIO (Processor Logic I/O) —— 它是 Xilinx 定义的跨域边界点。

flowchart TB subgraph "DMA Source Kernel" MEM[mem: TT_DATA*]<-->BRAM[内部BRAM缓冲] BRAM -->|hls::stream| AXIS_OUT[sig_o: AXI-Stream] end subgraph "System Connectivity" AXIS_OUT -->|sc| PLIO[ai_engine_0.PLIO_i_*] end subgraph "AIE Graph" PLIO -->|input_buffer| KERNEL[farrow_kernel*] end style MEM fill:#ffebee style BRAM fill:#fff8e1 style AXIS_OUT fill:#e8f5e9

第三层:AIE 内部的计算流水线

最终的 AIE 实现采用了双核协作架构,这是性能优化的关键成果:

  • farrow_kernel1: 执行四个并行的 8-tap FIR 滤波(对称/反对称),生成中间结果 y3, y2, y1, y0
  • farrow_kernel2: 执行 Horner 规则的嵌套乘法累加,将中间结果合成为最终输出

这种拆分解决了单核无法同时满足 II=16 周期约束的问题——就像一条生产线如果工序太多,拆成两段并行反而更快。

核心组件详解

1. farrow_dma_src_wrapper - 数据源头引擎

位于 hls/farrow_dma_src/farrow_dma_src.cpp,这是一个经典的双阶段数据流模式

#pragma HLS DATAFLOW

// 阶段1: 从 DDR 加载到片上 BRAM
load_buffer(mem, buff);

// 阶段2: 从 BRAM 流式输出到 AXI-Stream
transmit(buff, sig_o, loop_cnt);

为什么需要这个两阶段设计?

想象你在搬家:load_buffer 相当于先把散落的物品装进箱子(DDR → BRAM),transmit 则是把箱子整齐地搬上货车(BRAM → Stream)。直接零散搬运效率极低,而批量装箱可以充分利用 DRAM burst 传输的带宽优势。

关键参数

  • DEPTH = 1024: 缓冲深度,对应 4096 个 32-bit 样本(因为每次传输 4 个打包样本)
  • loop_cnt: 重复传输次数,用于测试多轮迭代场景
  • NBITS = 128: 总线宽度,打包了 4 个 cint16int32 样本

2. farrow_dma_snk_wrapper - 数据汇聚引擎

位于 hls/farrow_dma_snk/farrow_dma_snk.cpp,结构与源端对称但增加了选择性捕获功能:

capture_streams(buff, sig_i, loop_sel, loop_cnt);
read_buffer(mem, buff);

loop_sel 参数的巧妙之处:在多轮迭代测试中,我们可能只想保存某一轮的输出进行验证。loop_sel 让硬件在运行时决定哪一轮数据值得写入 DDR,避免了不必要的数据传输。

3. system.cfg - 系统集成蓝图

这是 Vitis 编译器的配置脚本,定义了完整的系统拓扑:

# 时钟统一配置:所有 DMA 核共享 312.5 MHz
freqhz=312500000:dma_src1.ap_clk,dma_src2.ap_clk,dma_snk.ap_clk

# 内核实例化:声明我们需要 2 个源核 + 1 个汇聚核
nk = farrow_dma_src_wrapper:2:dma_src1,dma_src2
nk = farrow_dma_snk_wrapper:1:dma_snk

# 内存映射:连接到 LPDDR
sp=dma_src1.mem:LPDDR
sp=dma_src2.mem:LPDDR
sp=dma_snk.mem:LPDDR

# 流式连接:PL ↔ AIE
sc = dma_src1.sig_o:ai_engine_0.PLIO_i_0
sc = dma_src2.sig_o:ai_engine_0.PLIO_i_1
sc = ai_engine_0.PLIO_o_0:dma_snk.sig_i

数据流全景追踪

让我们跟随一个样本的完整旅程:

sequenceDiagram participant Host as Host (PS) participant DDR as LPDDR4 participant SRC as dma_src1/dma_src2 participant PLIO as PLIO Interface participant K1 as farrow_kernel1 participant K2 as farrow_kernel2 participant SNK as dma_snk Host->>DDR: 写入 sig_i.txt / del_i_optimized.txt Host->>SRC: xrt::run.start() loop 每 4 个样本 (128-bit) SRC->>DDR: m_axi 读取 (burst) SRC->>SRC: 存入 BRAM 缓冲 end loop DEPTH 次 SRC->>PLIO: AXI-Stream 传输 PLIO->>K1/K2: input_buffer 接收 end K1->>K1: FIR 滤波 (sliding_mul_sym) K1->>K2: y3,y2,y1,y0 (乒乓缓冲) K2->>K2: Horner 规则 (mac) K2->>PLIO: output_buffer 发送 PLIO->>SNK: AXI-Stream 接收 SNK->>SNK: 存入 BRAM (loop_sel 筛选) loop 每 4 个样本 SNK->>DDR: m_axi 写入 (burst) end SNK-->>Host: wait() 返回 Host->>DDR: 读取结果验证

设计权衡与工程决策

权衡 1: 单宽总线 vs. 多窄总线

选择: 使用 128-bit 总线 @ 312.5 MHz,而非 32-bit @ 1250 MHz

理由:

  • PL 侧更容易达到较低频率的时序收敛
  • 减少跨时钟域 (CDC) 逻辑的复杂度
  • 128-bit 对齐天然适配 4 个 cint16 样本的向量化处理

代价: 需要额外的打包/解包逻辑,但在 HLS 中由编译器自动处理

权衡 2: 乒乓缓冲 vs. 单缓冲

选择: 内部使用双缓冲 (ping-pong),对外呈现单缓冲语义

理由:

  • load_buffertransmit 可以并行执行(DATAFLOW)
  • 隐藏 DDR 访问延迟:当一帧数据被流式输出时,下一帧正在从 DDR 加载

注意: 当前实现使用的是顺序执行模式(先 load 后 transmit),若要真正启用 ping-pong 并行,需要更复杂的索引管理

权衡 3: 双核拆分 vs. 单核优化

选择: 将 Farrow 滤波拆分到两个 AIE 核

背景: 初始单核实现的 II=123,远未达到 II=16 的目标。瓶颈在于:

  • 向量寄存器溢出(register spilling)
  • SRS (Shift-Round-Saturate) 路径的限制
  • 过多的 MAC 操作无法在单核内流水化

收益:

  • kernel1 专注 FIR 滤波(II=16)
  • kernel2 专注 Horner 求值(三个循环各 II=3)
  • 总吞吐量达到 1115 Msps,超过 1 Gsps 目标

代价: 增加了核间通信开销(通过乒乓缓冲),但这是 AIE 架构擅长处理的

新贡献者必读:陷阱与注意事项

1. 数据格式对齐陷阱

危险: del_i_optimized.txtdel_i.txt 格式不同!

host.cpp 中可以看到:

std::ifstream del_i;
del_i.open("del_i_optimized.txt", std::ifstream::in);  // 注意是 _optimized 版本

优化后的格式将 16-bit 延迟值连续放置,低位补零,这使得 AIE 可以直接用 vector_cast 提取,避免了昂贵的 filter_even 操作。如果使用错误的输入文件,延迟值会被错误解析!

2. Loop Count 的双重含义

host.cpp 中:

static constexpr int32_t LOOP_CNT_I = 4;  // DMA 源端循环次数
static constexpr int32_t LOOP_CNT_O = 4;  // DMA 汇聚端期望的循环次数

而在 AIE graph 中:

my_graph.run(NUM_ITER);  // NUM_ITER = -1 表示无限运行

理解: DMA 核负责控制数据流动的"批次",而 AIE graph 一旦启动就持续运行。终止条件实际上由 dma_snkloop_cnt 参数控制——当它接收到指定数量的循环后就停止,从而间接停止了数据消费,整个系统自然停顿。

3. HLS INTERFACE pragma 的微妙之处

#pragma HLS interface m_axi port=mem bundle=gmem offset=slave depth=DEPTH
#pragma HLS interface s_axilite port=mem bundle=control

注意到 mem 同时出现在 m_axis_axilite 中?这是 Vitis 的标准做法:

  • m_axi: 定义数据传输接口(实际的数据搬运通道)
  • s_axilite: 定义控制接口(用于传递基地址等标量参数)

新手常犯错误: 遗漏 s_axilite 会导致 XRT 无法正确配置 DMA 地址。

4. 时钟频率的隐性契约

system.cfg 中设置的 312.5 MHz 必须与以下保持一致:

  • HLS 核的 clock=3.2ns 约束(约 312.5 MHz)
  • AIE 的 1250 MHz 时钟(4 倍频关系)

如果不一致,PLIO 接口的宽度-频率换算就会出错,导致数据损坏或时序违例。

5. 验证容差说明

host.cpp 的结果验证部分:

flag |= ( err_re > 5 ) || ( err_im > 5 ); // Matlab is not bit accurate

重要: MATLAB 参考模型使用浮点运算,而 AIE 实现使用定点运算(cint16 × int16)。允许最大 5 LSB 的误差是正常的,不代表实现错误。如果你看到误差在这个范围内,系统是正常工作的。

子模块文档

本模块包含以下核心子模块,每个都有专门的文档页面详细说明其实现细节:

DMA Source Kernel

职责:从 LPDDR4 读取数据并通过 AXI4-Stream 发送到 AIE

关键文件

  • hls/farrow_dma_src/farrow_dma_src.cpp
  • hls/farrow_dma_src/farrow_dma_src.h
  • hls/farrow_dma_src/hls.cfg

核心组件farrow_dma_src_wrapper —— HLS 顶层函数,实现 DDR-to-Stream 的数据搬运

DMA Sink Kernel

职责:从 AIE 接收 AXI4-Stream 数据并写回 LPDDR4

关键文件

  • hls/farrow_dma_snk/farrow_dma_snk.cpp
  • hls/farrow_dma_snk/farrow_dma_snk.h
  • hls/farrow_dma_snk/hls.cfg

核心组件farrow_dma_snk_wrapper —— HLS 顶层函数,实现 Stream-to-DDR 的数据搬运,支持选择性捕获功能

System Configuration

职责:定义系统级的连接关系、时钟配置和内核实例化

关键文件

  • vitis/system.cfg

核心组件

  • dma_src1, dma_src2 —— 两个 Source 实例
  • dma_snk —— Sink 实例

与相关模块的关系

本模块是 farrow_filter_design_variants 教程的系统集成层。设计演进路径如下:

  1. farrow_baseline_graph: 纯 AIE 功能验证
  2. farrow_opt_1/opt_2: 逐步优化 II
  3. farrow_final: 双核最终实现
  4. 本模块 (farrow_filter_streaming_io_integration): 添加 PL DMA 层,形成完整系统

上游依赖:

下游使用者:

  • 完整的 VCK190 硬件部署流程
  • 作为其他需要高吞吐量流式输入的 AIE 设计的模板

性能指标总结

指标 数值 备注
目标采样率 1000 Msps 设计规格
实测稳定吞吐 ~1115 Msps 最终优化版本
PL 时钟频率 312.5 MHz 便于时序收敛
AIE 时钟频率 1250 MHz 标准 AIE 频率
数据总线宽度 128-bit (PL), 32-bit (AIE) 4:1 比例匹配
内部缓冲深度 1024 × 128-bit 支持 4 轮迭代
端到端延迟 ~14.4 μs (4096 样本) 含启动开销

本文档基于 Vitis-Tutorials 15-farrow_filter 设计编写,适用于 Vitis 2024.2 及兼容版本。

On this page