🏠

DMA Source Ingress Pipeline (DMA 数据源输入管道)

一句话概括

dma_source_ingress_pipeline 是 64K 点 IFFT 系统的"数据闸门"——它负责将存储在 LPDDR 中的原始样本数据,以正确的格式、正确的时序、通过 5 条并行 AXI Stream 通道注入 AI Engine 计算阵列。想象它是一个精密的货物分拣中心:从仓库(DDR)批量取货,重新打包成符合流水线要求的包裹,然后通过多条传送带同步发送给下游的工人(AI Engine tiles)。


问题空间与设计动机

我们面临什么挑战?

在实现一个 64K 点 IFFT @ 2 Gsps 的高性能信号处理系统时,数据输入环节存在三个核心矛盾:

  1. 存储格式 vs 计算格式的不匹配

    • DDR 中的数据按列优先(column-major)顺序存储
    • 但 AI Engine 需要按行接收数据进行 256 点 IFFT 变换
    • 需要一个"翻译层"来完成数据重排
  2. 带宽需求 vs 接口能力的差距

    • 目标吞吐量:2 Gsps(每秒 20 亿样本)
    • 单个 AXI Stream 无法满足,需要 5 条并行流
    • 每条流需要精确同步,不能出现"饥饿"或"堵塞"
  3. 连续数据流 vs 突发传输的矛盾

    • AI Engine 期望稳定的数据流(streaming)
    • DDR 访问是突发的(burst-based),有延迟
    • 需要缓冲机制来平滑这种差异

为什么选择 HLS PL Kernel 而非纯 AI Engine 方案?

方案 优势 劣势
纯 AI Engine 低延迟,易编程 无法直接访问 DDR;资源消耗大
HLS PL Kernel + DMA 高带宽 DDR 访问;灵活的数据重排 需要处理跨时钟域(312.5 MHz PL vs 1 GHz AIE)
PS 直接控制 简单 带宽太低,无法满足 2 Gsps

设计选择了 HLS PL Kernel 方案,利用可编程逻辑(PL)的高带宽内存接口和灵活的流处理能力,作为 DDR 与 AI Engine 之间的桥梁。


核心抽象与心智模型

类比:自动化分拣配送中心

想象 dma_source_ingress_pipeline 是一个大型电商的自动化分拣中心:

┌─────────────────────────────────────────────────────────────────────────┐
│                        自动化分拣配送中心                                │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌──────────────┐     ┌──────────────┐     ┌──────────────────────┐    │
│  │   中央仓库    │────▶│  卸货缓冲区   │────▶│   5条并行分拣线       │    │
│  │   (LPDDR)    │     │  (URAM buff) │     │   (AXI Streams)      │    │
│  └──────────────┘     └──────────────┘     └──────────────────────┘    │
│        │                     │                       │                 │
│        ▼                     ▼                       ▼                 │
│   批量取货(128bit     重新组织货物流            同步发货到              │
│   宽总线突发读取)    (多相分配到5个bank)        5个目的地               │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
  • 中央仓库 (LPDDR):数据以托盘为单位存储(每托盘 = 2 个 cint32 样本 = 128 bit)
  • 卸货缓冲区 (URAM):5 个独立的存储区(banks),每个 bank 存放 52 行 × 260 列的样本
  • 5条并行分拣线:同时向 5 个 AI Engine tile 实例发送数据

关键抽象概念

1. 多相分配 (Polyphase Distribution)

数据不是简单地"分块"给 5 条流,而是采用循环交错的方式分配:

原始矩阵 (260×260,含零填充):
┌─────────────────────────────────────────────────────────┐
│ Row 0 → Stream 0, Row 1 → Stream 1, ..., Row 4 → Stream 4 │
│ Row 5 → Stream 0, Row 6 → Stream 1, ...                   │
│ ...                                                       │
└─────────────────────────────────────────────────────────┘

这种分配方式确保每个 stream 获得均匀的工作负载,且天然支持后续的转置操作。

2. Ping-Pong 缓冲 (隐式于 DATAFLOW)

#pragma HLS DATAFLOW 指令创建了隐式的双缓冲机制:

  • load_buffer() 从 DDR 读取下一帧数据到 URAM
  • transmit() 同时从 URAM 发送当前帧到 AIE
  • 两阶段流水线重叠,隐藏 DDR 访问延迟

3. 零填充对齐 (Zero-padding Alignment)

原始 256×256 矩阵被扩展到 260×260:

  • 原因:256 不能被 5 整除,260 可以(260/5 = 52 行 per stream)
  • 额外 4 行/列填充零,不影响 IFFT 结果
  • 这是算法与架构的折中:牺牲少量计算资源换取硬件实现的简化

架构详解与数据流

整体架构图

graph TB subgraph Host_DDR["Host and DDR"] HOST[Host Application host.cpp] DDR[LPDDR Memory Input Data 64K samples] end subgraph DMA_SRC["dma_source_ingress_pipeline PL at 312.5 MHz"] direction TB MAXI[m_axi port 128-bit burst read] LB[load_buffer Column to Row reordering] BUFF[(buff URAM 5-bank partition)] TX[transmit Streaming to AIE] AXIS[axis ports sig_o 0..4] end subgraph AIE_Array["AI Engine Array at 1 GHz"] AIE0[Front-End IFFT-256 Instance 0] AIE1[Front-End IFFT-256 Instance 1] AIE2[Front-End IFFT-256 Instance 2] AIE3[Front-End IFFT-256 Instance 3] AIE4[Front-End IFFT-256 Instance 4] end HOST -->|Configure and Start| MAXI DDR -->|Burst Read| MAXI MAXI --> LB LB -->|Polyphase write| BUFF BUFF -->|Read rows| TX TX --> AXIS AXIS --> AIE0 AXIS --> AIE1 AXIS --> AIE2 AXIS --> AIE3 AXIS --> AIE4

模块层次结构

dma_source_ingress_pipeline/
├── hls.cfg                    # HLS 综合配置
├── ifft_dma_src.h             # 类型定义与常量
├── ifft_dma_src.cpp           # 核心实现
│   ├── load_buffer()          # DDR → URAM 加载
│   ├── transmit()             # URAM → AXI Stream 发送
│   └── ifft_dma_src_wrapper() # 顶层封装
└── tb_wrapper.cpp             # C 仿真测试平台

详细数据流分析

Stage 1: DDR Burst 读取 (m_axi 接口)

#pragma HLS interface m_axi port=mem bundle=gmem offset=slave depth=NFFT/2
  • 接口类型: AXI4-Full,支持突发传输
  • 数据宽度: 128 bit(等于 2 个 cint32 样本)
  • 突发深度: 32768(64K/2,每次读取 2 个样本)
  • 时钟: 312.5 MHz,与 PL 同频

为什么用 m_axi 而不是 axis

  • DDR 是随机访问存储器,AXI4-Full 支持地址寻址和突发传输
  • AXI4-Stream 没有地址概念,不适合 DDR 访问

Stage 2: 数据重排与缓冲 (load_buffer)

void load_buffer( TT_DATA mem[NFFT/2], TT_SAMPLE (&buff)[NSTREAM][NROW][DEPTH] )

核心操作

  1. 列优先读取:从 DDR 按列顺序读取样本
  2. 多相分配到 5 个 bank:使用模运算决定样本进入哪个 bank
  3. 行优先存储:在每个 bank 内按行存储,为后续流式输出做准备

关键代码解析

// 三重嵌套循环展开为一维线性遍历
LOAD_SAMP : for (int samp=0,row=0,rr=0,col=0,ss=0,mm=0; samp < DEPTH*DEPTH; samp+=2) {
#pragma HLS pipeline II=1
    // 从 DDR 读取一对样本
    (val1,val0) = mem[mm++];
    
    // 多相分配到相邻的两个 stream
    if ( ss == NSTREAM-1 ) {
      buff[NSTREAM-1][rr  ][col] = val0;
      buff[0        ][rr+1][col] = val1;  // 跨越 bank 边界
    }
    else {
      buff[ss  ][rr][col] = val0;
      buff[ss+1][rr][col] = val1;
    }
    
    // 更新索引:stream 指针循环,row/col 递增
    rr = ( ss >= NSTREAM-2 ) ? rr + 1 : rr;
    ss = ( ss >= NSTREAM-2 ) ? (ss + 2 - NSTREAM) : (ss+2);
}

为什么 II=1 是关键?

  • Initiation Interval = 1 意味着每个时钟周期处理 2 个样本
  • 在 312.5 MHz 下,理论带宽 = 312.5M × 2 = 625M samples/sec
  • 5 条并行流合计 = 3.125 Gsps > 2 Gsps 目标,满足要求

Stage 3: 流式传输 (transmit)

void transmit( TT_SAMPLE (&buff)[NSTREAM][NROW][DEPTH], 
               TT_STREAM sig_o[NSTREAM], 
               const int& loop_cnt )

核心操作

  1. 外层循环 (REPEAT): 支持多次迭代(loop_cnt 控制)
  2. 内层循环 (RUN_DEPTH): 遍历所有行和深度
  3. 并行写入 5 条流: 每个周期向所有 stream 写入 2 个样本

关键设计决策:最后一次迭代发送零

TT_SAMPLE val0 = (ll != loop_cnt) ? buff[ss][rr][dd  ] : TT_SAMPLE(0);
TT_SAMPLE val1 = (ll != loop_cnt) ? buff[ss][rr][dd+1] : TT_SAMPLE(0);

为什么需要额外的零发送迭代?

  • 注释说明:transpose kernel 有一个单周期转置操作的延迟
  • 这个额外的零迭代用于"冲洗"流水线,确保所有有效数据都被下游处理
  • 这是流水线平衡的典型技巧

内存模型与资源利用

缓冲区分区策略

TT_SAMPLE buff[NSTREAM][NROW][DEPTH];  // [5][52][260]
#pragma HLS array_partition variable=buff dim=1
#pragma HLS bind_storage variable=buff type=RAM_T2P impl=uram
#pragma HLS dependence variable=buff type=intra false
属性 说明
总容量 5 × 52 × 260 × 64 bit = 540,800 bit ≈ 66 KB 存储一帧完整数据
分区维度 dim=1 (NSTREAM) 5 个 bank 完全独立访问
存储类型 URAM (UltraRAM) 比 BRAM 更大、更密集
端口配置 T2P (True Dual-Port) 支持同时读写,无冲突

为什么用 URAM 而不是 BRAM?

  • 66 KB 容量超出单个 BRAM(36 Kb)的范围
  • URAM 更适合大容量、高带宽的缓冲应用
  • Versal 器件有丰富的 URAM 资源

伪依赖消除

#pragma HLS dependence variable=buff type=intra false

这条指令告诉 HLS 工具:同一数组的不同元素之间没有数据依赖

  • 默认情况下,HLS 保守地假设对同一数组的任何访问都可能依赖
  • 这会导致不必要的串行化,降低流水线效率
  • 由于我们明确分区了 bank,不同 stream 的访问确实独立

设计权衡与决策分析

权衡 1: 零填充 vs 复杂控制逻辑

选择: 将 256×256 扩展到 260×260,填充 4 行/列零

方面 评估
代价 额外 (260² - 256²) / 256² = 3.1% 的计算浪费
收益 简化为 5-bank 内存架构,避免复杂的非均匀分配逻辑
替代方案 保持 256×256,但需要处理余数(256 % 5 = 1),控制逻辑复杂

结论: 3% 的计算开销换取显著的硬件简化,是合理的工程折中。

权衡 2: 单核 DATAFLOW vs 多核并行

选择: 单个 HLS kernel 内部使用 DATAFLOW,而非实例化多个 kernel

方面 评估
资源效率 共享控制逻辑,减少面积开销
时序收敛 单核更容易满足 312.5 MHz
灵活性 不如多核方案,但本场景需求固定

权衡 3: 显式 Ping-Pong vs DATAFLOW 隐式缓冲

选择: 依赖 #pragma HLS DATAFLOW 自动插入 ping-pong buffer

#pragma HLS DATAFLOW
load_buffer( mem, buff );   // Stage 1
transmit( buff, sig_o, loop_cnt );  // Stage 2

DATAFLOW 指令自动:

  • 识别 producer-consumer 关系
  • 在函数间插入适当深度的 FIFO/buffer
  • 允许两阶段并行执行

为什么不手动实现 ping-pong?

  • HLS 的自动优化通常优于手工编码
  • 代码更简洁,更易维护
  • 工具可以根据目标频率自动调整 buffer 深度

接口契约与系统集成

AXI 接口规范

#pragma HLS interface m_axi      port=mem         bundle=gmem    offset=slave   depth=NFFT/2
#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
接口 协议 用途
mem AXI4-Full (gmem) DDR 数据访问
sig_o[5] AXI4-Stream 到 AI Engine 的数据流
loop_cnt AXI4-Lite 主机配置的迭代次数

系统连接 (system.cfg)

nk = ifft_dma_src_wrapper:1:dma_src
sp=dma_src.mem:LPDDR
sc = dma_src.sig_o_0:ai_engine_0.PLIO_front_i_0
sc = dma_src.sig_o_1:ai_engine_0.PLIO_front_i_1
sc = dma_src.sig_o_2:ai_engine_0.PLIO_front_i_2
sc = dma_src.sig_o_3:ai_engine_0.PLIO_front_i_3
sc = dma_src.sig_o_4:ai_engine_0.PLIO_front_i_4

连接拓扑

  • DMA Source → 5 条 PLIO → 5 个 Front-End AIE 实例
  • 每条 PLIO 承载一条 AXI Stream
  • 时钟域跨越:PL @ 312.5 MHz → AIE @ 1 GHz(由 PLIO 自动处理)

新贡献者须知:陷阱与注意事项

⚠️ 常见陷阱

1. 索引计算的隐式假设

// 这段代码有微妙的边界条件
if ( row >= NFFT_1D || col >= NFFT_1D ) {
    (val1, val0) = TT_DATA(0);  // 零填充区域
}

注意: 零填充只在矩阵边缘生效。如果修改 NFFT_1DEXTRA,必须重新验证索引逻辑。

2. Stream 顺序的严格性

AI Engine 期望数据按特定顺序到达:

  • 先 row 0 的所有列
  • 再 row 1 的所有列
  • ...

任何顺序错误都会导致错误的 IFFT 结果。不要随意修改 transmit() 中的遍历顺序。

3. Loop Tripcount 的影响

#pragma HLS LOOP_TRIPCOUNT min=1 max=8

这只是综合时的提示,不影响实际功能。但如果实际 loop_cnt 超出范围,性能估计会不准确。

4. DATAFLOW 的死锁风险

如果 load_buffertransmit 的速率不匹配,可能导致死锁:

  • load_buffer 写满 buff 后等待 transmit 读取
  • transmit 尝试读取时 load_buffer 可能还未完成写入

当前的 II=1 设计和适当的 buffer 大小避免了这个问题,但修改时要小心。

🔧 调试建议

  1. C 仿真验证: 先用 tb_wrapper.cpp 验证功能正确性
  2. 检查波形: 硬件仿真时关注 sig_o 各通道的 valid/ready 握手
  3. 带宽验证: 确认每个 stream 的数据率是否均衡

📊 性能调优方向

如果需要进一步优化:

方向 方法 预期效果
提高频率 优化时序约束,尝试 > 312.5 MHz 线性提升带宽
增加并行度 从 5 streams 扩展到更多 需要重构整个系统
减少延迟 优化 DATAFLOW buffer 深度 降低首包延迟

与其他模块的关系

上游依赖

  • Host Application: 配置 loop_cnt,启动 kernel 执行
  • LPDDR: 存储输入数据,通过 m_axi 访问

下游消费

兄弟模块


总结

dma_source_ingress_pipeline 是 64K IFFT 系统的数据入口网关。它的核心价值在于:

  1. 桥接异构存储层次: DDR ↔ PL URAM ↔ AIE Stream
  2. 实现数据格式转换: Column-major → Row-major,多相分配到 5 条流
  3. 保证吞吐量: 通过精心设计的流水线和并行度, sustain 2 Gsps 数据率
  4. 零填充的工程智慧: 以 3% 的计算开销换取硬件实现的极大简化

理解这个模块的关键是把握数据如何在时间和空间两个维度上流动——从 DDR 的批量突发,到 URAM 的多相缓冲,再到 AXI Stream 的同步分发。这是一个典型的"数据搬运工"设计,看似简单,却蕴含着对硬件架构深刻理解的工程实践。

On this page