DMA Source Kernel (farrow_dma_src) 子模块文档
概述
farrow_dma_src 是 Farrow 滤波器系统的数据入口引擎,负责将存储在 LPDDR4 内存中的信号样本和延迟参数高效地搬运到 AI Engine。它就像是工厂生产线上的自动上料机器人——精确、持续、无间断地将原材料送到加工工位。
核心职责
- DDR → 片上 BRAM 的批量加载:利用 AXI4-MM burst 传输最大化内存带宽
- BRAM → AXI4-Stream 的流式输出:以固定节拍向 AIE 输送数据
- 多轮迭代支持:通过
loop_cnt参数实现测试场景下的重复传输
架构设计
双阶段流水线 (DATAFLOW)
burst read| BRAM[buff[DEPTH]
片上BRAM] end subgraph "Stage 2: Transmit" BRAM -->|hls::stream| AXIS[sig_o
AXI4-Stream] AXIS -->|PLIO| AIE[AIE Kernel] end style DDR fill:#f3e5f5 style BRAM fill:#fff8e1 style AXIS fill:#e8f5e9 style AIE fill:#fff3e0
这种设计的精妙之处在于并行化隐藏延迟:当 Stage 2 正在从 BRAM 读取第 N 帧数据并流式输出时,Stage 1 可以同时在后台从 DDR 预取第 N+1 帧数据。对于外部内存访问来说,这种重叠至关重要——DDR 的随机访问延迟可能高达数十个时钟周期,而 burst 传输可以达到接近理论峰值带宽。
代码结构解析
void farrow_dma_src_wrapper(
TT_DATA mem[farrow_dma_src::DEPTH], // AXI4-MM 接口:连接 DDR
int loop_cnt, // AXI4-Lite 接口:控制参数
TT_STREAM& sig_o // AXI4-Stream 接口:连接 AIE
) {
#pragma HLS interface m_axi port=mem bundle=gmem offset=slave depth=DEPTH
#pragma HLS interface axis port=sig_o
#pragma HLS interface s_axilite port=loop_cnt bundle=control
#pragma HLS interface s_axilite port=mem bundle=control
#pragma HLS interface s_axilite port=return bundle=control
#pragma HLS DATAFLOW
TT_DATA buff[DEPTH]; // 内部乒乓缓冲
load_buffer(mem, buff); // 阶段1
transmit(buff, sig_o, loop_cnt); // 阶段2
}
INTERFACE Pragma 详解
| Pragma | 作用 | 为什么需要 |
|---|---|---|
m_axi port=mem |
声明 DDR 访问接口 | 高带宽数据传输 |
axis port=sig_o |
声明流式输出接口 | 连接 AIE PLIO |
s_axilite port=loop_cnt |
标量控制参数 | XRT 运行时配置 |
s_axilite port=mem |
传递缓冲区基地址 | XRT 设置 DMA 地址 |
s_axilite port=return |
函数返回/中断 | 标准 Vitis 要求 |
关键洞察:mem 同时出现在 m_axi 和 s_axilite 中不是错误,而是 Vitis 的标准模式——m_axi 定义数据通路,s_axilite 定义控制通路(用于传递基地址)。
关键函数分析
load_buffer()
void load_buffer(TT_DATA mem[DEPTH], TT_DATA (&buff)[DEPTH]) {
LOAD_BUFF: for (int mm=0; mm < DEPTH; mm++) {
#pragma HLS PIPELINE II=1
buff[dd] = mem[mm];
dd = (dd == TT_ADDR(DEPTH-1)) ? TT_ADDR(0) : TT_ADDR(dd+1);
}
}
设计要点:
II=1:每个时钟周期读取一个 128-bit 字- 循环索引使用
ap_uint<10>类型:帮助 HLS 工具推断位宽,优化资源 - 顺序访问模式:确保 DRAM burst 效率最大化
吞吐量计算:
- 每周期读取 128 bits
- @ 312.5 MHz = 128 × 312.5M = 40 Gbps = 5 GB/s
- 这远高于 AIE 所需的 1250 Msps × 32 bits = 40 Gbps
transmit()
void transmit(TT_DATA (&buff)[DEPTH], TT_STREAM& sig_o, const int& loop_cnt) {
REPEAT: for (int ll=0; ll < loop_cnt; ll++) {
#pragma HLS LOOP_TRIPCOUNT min=1 max=4
RUN_DEPTH: for (int dd=0; dd < DEPTH; dd++) {
#pragma HLS PIPELINE II=1
TT_DATA val_128b;
TT_SAMPLE val[4];
#pragma HLS array_partition variable=val dim=1
(val[3], val[2], val[1], val[0]) = buff[dd];
val_128b = (val[3],val[2],val[1],val[0]);
sig_o.write(val_128b);
}
}
}
关键特性:
-
LOOP_TRIPCOUNT:给 HLS 编译器提供循环次数提示,帮助其优化资源分配。实际值由运行时
loop_cnt决定。 -
Array Partition:
val[4]被完全分割为独立寄存器,允许并行访问四个 32-bit 子字段。 -
数据打包语义:
(val[3], val[2], val[1], val[0]) = buff[dd];这是 HLS 的位拼接语法,将 128-bit 总线拆分为 4 个 32-bit 样本。
-
循环嵌套结构:外层
loop_cnt控制重复次数,内层DEPTH控制每轮传输的数据量。
数据类型与常量
namespace farrow_dma_src {
static constexpr unsigned DEPTH = 1024; // 缓冲深度
static constexpr unsigned NBITS = 128; // 总线宽度
typedef ap_uint<NBITS> TT_DATA; // 128-bit 数据字
typedef ap_uint<NBITS/4> TT_SAMPLE; // 32-bit 样本
typedef hls::stream<TT_DATA> TT_STREAM; // HLS 流类型
typedef ap_uint<10> TT_ADDR; // 10-bit 地址 (log2(1024))
};
为什么是 128-bit?
这是 PL 侧 (312.5 MHz) 与 AIE 侧 (1250 MHz) 之间的自然匹配点:
- PL 频率 : AIE 频率 = 312.5 : 1250 = 1 : 4
- PL 位宽 : AIE 位宽 = 128 : 32 = 4 : 1
- 两者相乘得到相同的带宽,实现无缝桥接
HLS 配置文件 (hls.cfg)
part=xcvc1902-vsva2197-2MP-e-S
[hls]
flow_target=vitis
clock=3.2ns # ~312.5 MHz
syn.top=farrow_dma_src_wrapper
syn.file=farrow_dma_src.cpp
package.output.file=farrow_dma_src_wrapper.xo
package.output.format=xo
vivado.flow=impl # 执行 OOC 布局布线
关键参数说明:
clock=3.2ns:约等于 312.5 MHz,与 system.cfg 中的freqhz必须一致vivado.flow=impl:启用 Out-of-Context 综合,提前发现时序问题package.output.format=xo:生成 Xilinx Object 文件,供 Vitis 链接使用
测试平台 (tb_wrapper.cpp)
测试平台采用简单的递增序列验证功能正确性:
// 生成测试数据:0, 1, 2, 3, ... 打包成 128-bit 字
for (int mm=0, dd=0; mm < 4*DEPTH; mm+=4, dd++) {
ddr4[dd] = (TT_SAMPLE(mm+3), TT_SAMPLE(mm+2), TT_SAMPLE(mm+1), TT_SAMPLE(mm+0));
}
// 运行 DUT
farrow_dma_src_wrapper(ddr4, loop_cnt, sig_o);
// 验证输出流大小和内容
assert(sig_o.size() == DEPTH);
测试策略:
- 使用确定性递增序列便于调试
- 验证流大小是否正确(应等于 DEPTH × loop_cnt)
- 逐字比较输入输出一致性
常见陷阱与调试技巧
陷阱 1: DATAFLOW 未真正并行
当前实现中 load_buffer 和 transmit 是顺序执行的,因为 buff 是单缓冲而非乒乓缓冲。要实现真正的并行:
// 真正的乒乓缓冲需要两个独立缓冲区
TT_DATA buff_ping[DEPTH];
TT_DATA buff_pong[DEPTH];
// 并在软件层面管理读写指针
不过对于这种演示设计,顺序执行已足够——DDR burst 读取很快,不会成为瓶颈。
陷阱 2: 地址空间溢出
TT_ADDR 定义为 ap_uint<10>,对应 0-1023 的地址范围。如果修改 DEPTH 超过 1024,必须相应增加地址位宽:
// 如果 DEPTH = 2048
typedef ap_uint<11> TT_ADDR; // 需要 11 位
陷阱 3: Loop Count 不匹配
Host 端的 LOOP_CNT_I 必须与 AIE graph 的预期迭代次数匹配。如果 DMA 发送的数据多于 AIE 期望接收的,会导致:
- AXI4-Stream 背压,DMA 停滞
- 或者数据丢失(如果使用了非阻塞写入)
调试建议
- C 仿真 (
make csim):首先验证算法逻辑 - RTL 协同仿真 (
make cosim):验证硬件行为(可选,耗时较长) - 检查综合报告:确认 II=1 是否达成,识别任何依赖导致的延迟
与其他组件的关系
XRT API] CFG[system.cfg
内核实例化] end subgraph "下游消费者" PLIO0[ai_engine_0.PLIO_i_0] PLIO1[ai_engine_0.PLIO_i_1] K1[farrow_kernel1] K2[farrow_kernel2] end HOST -->|配置 loop_cnt| SRC CFG -->|实例化为 dma_src1/dma_src2| SRC SRC -->|sig_o| PLIO0 SRC -->|sig_o| PLIO1 PLIO0 --> K1 PLIO1 --> K2
- 上游:
host.cpp通过 XRT API 配置和启动 DMA;system.cfg定义实例化和连接 - 下游:连接到 AIE Graph 的两个输入 PLIO 端口,分别供给
farrow_kernel1(信号)和farrow_kernel2(延迟参数)
本文档详细说明了 DMA Source Kernel 的设计原理和实现细节。理解这个模块是掌握 Versal AIE-PL 集成的基础。