🏠

DMA Source Kernel (farrow_dma_src) 子模块文档

概述

farrow_dma_src 是 Farrow 滤波器系统的数据入口引擎,负责将存储在 LPDDR4 内存中的信号样本和延迟参数高效地搬运到 AI Engine。它就像是工厂生产线上的自动上料机器人——精确、持续、无间断地将原材料送到加工工位。

核心职责

  1. DDR → 片上 BRAM 的批量加载:利用 AXI4-MM burst 传输最大化内存带宽
  2. BRAM → AXI4-Stream 的流式输出:以固定节拍向 AIE 输送数据
  3. 多轮迭代支持:通过 loop_cnt 参数实现测试场景下的重复传输

架构设计

双阶段流水线 (DATAFLOW)

flowchart LR subgraph "Stage 1: Load" DDR[(LPDDR4)] -->|m_axi
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_axis_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);
    }
}
}

关键特性

  1. LOOP_TRIPCOUNT:给 HLS 编译器提供循环次数提示,帮助其优化资源分配。实际值由运行时 loop_cnt 决定。

  2. Array Partitionval[4] 被完全分割为独立寄存器,允许并行访问四个 32-bit 子字段。

  3. 数据打包语义

    (val[3], val[2], val[1], val[0]) = buff[dd];
    

    这是 HLS 的位拼接语法,将 128-bit 总线拆分为 4 个 32-bit 样本。

  4. 循环嵌套结构:外层 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_buffertransmit 是顺序执行的,因为 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 停滞
  • 或者数据丢失(如果使用了非阻塞写入)

调试建议

  1. C 仿真 (make csim):首先验证算法逻辑
  2. RTL 协同仿真 (make cosim):验证硬件行为(可选,耗时较长)
  3. 检查综合报告:确认 II=1 是否达成,识别任何依赖导致的延迟

与其他组件的关系

flowchart TB subgraph "本模块内部" SRC[farrow_dma_src_wrapper] end subgraph "上层调用者" HOST[host.cpp
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 集成的基础。

On this page