DMA Sink Egress Pipeline (DMA 输出流水线)
一句话概括
dma_sink_egress_pipeline 是整个 64K-点 IFFT 系统的"数据出口闸门"——它负责将 AI Engine 处理完成的 5 路并行数据流收集、重组,并以正确的时序写入 LPDDR 内存。想象它是一个高速分拣中心:5 条传送带同时送来包裹(数据),它必须按特定顺序整理后,一次性装入卡车(DDR 突发传输)。
问题空间与设计动机
我们面临什么挑战?
在 Versal ACAP 架构中实现 64K-点 IFFT @ 2 Gsps 时,系统需要解决一个经典的数据流瓶颈问题:
- AI Engine 的高吞吐输出:后端 AI Engine 以 5 路并行 AXI-Stream 输出数据,每路速率约 400 Msps,合计 2 Gsps
- DDR 的突发访问特性:LPDDR4 擅长大块连续读写,对零散小事务效率极低
- 数据重排需求:AI Engine 输出的数据是按列组织(column-major)的,但 DDR 存储需要行优先(row-major)布局以便主机读取
为什么不用简单方案?
方案 A:直接让 AI Engine 写 DDR
- ❌ AI Engine 没有直接的 DDR 控制器接口
- ❌ PL (Programmable Logic) 更适合处理复杂的地址生成和突发调度
方案 B:单路串行收集
- ❌ 无法维持 2 Gsps 的总吞吐量
- ❌ 会引入严重的背压(back-pressure)到 AI Engine
最终选择:多路并行收集 + 片上缓冲 + 突发写入
- ✅ 5 路 AXI-Stream 同时接收,匹配 AI Engine 输出带宽
- ✅ 使用 URAM 作为乒乓缓冲,解耦接收和发送时序
- ✅ 向 DDR 发起大宽度突发传输,最大化内存带宽利用率
核心抽象与心智模型
把 ifft_dma_snk 想象成...
一个智能仓库的分拣系统
- 5 个进货口 (
sig_i[0..4]): 对应 5 路 AI Engine 输出流- 5 个货架区 (
buff[5][...]): 每个进货口有独立的存储区域,避免争抢- 循环扫描器 (
capture_streams): 按固定节奏从各进货口取货,暂存货架- 出货调度员 (
read_buffer): 按客户要求的顺序(行优先)从货架取货,打包发往目的地- 选货开关 (
loop_sel): 允许只提取某一轮循环的数据(用于调试/验证)
关键数据结构映射
┌─────────────────────────────────────────────────────────────┐
│ 260 x 260 数据矩阵 │
│ (256x256 有效数据 + 4 行/列零填充) │
├─────────────────────────────────────────────────────────────┤
│ Stream 0 负责列: 0, 5, 10, ... (模 5 同余的列) │
│ Stream 1 负责列: 1, 6, 11, ... │
│ Stream 2 负责列: 2, 7, 12, ... │
│ Stream 3 负责列: 3, 8, 13, ... │
│ Stream 4 负责列: 4, 9, 14, ... │
└─────────────────────────────────────────────────────────────┘
↓ 按列接收 (Column-major order)
capture_streams()
↓ 存储到 5-bank URAM 缓冲区
buff[NSTREAM][DEPTH*DEPTH/NSTREAM]
↓ 按行读取 (Row-major order)
read_buffer()
↓ 突发写入 DDR
mem[NFFT/2]
架构详解
模块层次结构
dma_sink_egress_pipeline/
├── HLS Kernel: ifft_dma_snk_wrapper
│ ├── capture_streams() # 阶段 1: 流数据捕获
│ └── read_buffer() # 阶段 2: 缓冲区读出
├── HLS Config: hls.cfg # 综合配置与约束
└── System Integration: vitis/system.cfg
└── dma_snk instance # 系统集成实例
数据流图
#pragma HLS DATAFLOW"] BUF[("buff[5][13520]
URAM T2P")] READ["read_buffer()"] end subgraph "Memory Subsystem" DDR["LPDDR
AXI4-Full"] end AIE0 -->|axis| CAP AIE1 -->|axis| CAP AIE2 -->|axis| CAP AIE3 -->|axis| CAP AIE4 -->|axis| CAP CAP --> BUF BUF --> READ READ -->|m_axi
burst write| DDR
时序流水线视图
DMA Sink 采用双阶段流水线设计,capture_streams 和 read_buffer 通过 #pragma HLS DATAFLOW 实现重叠执行:
- 阶段 1 (Capture): 从 5 路 AXI-Stream 接收数据,存入 URAM 缓冲区
- 阶段 2 (Read): 从 URAM 按行优先读取,写入 DDR
当前迭代的读取与下一次迭代的捕获可以重叠执行,最大化吞吐量。
核心组件深度解析
1. capture_streams() —— 流数据捕获
职责: 从 5 路 AXI-Stream 接收数据,按列组织存储到内部缓冲区
函数签名:
void capture_streams(
TT_SAMPLE (&buff)[NSTREAM][DEPTH*DEPTH/NSTREAM],
TT_STREAM sig_i[NSTREAM],
const int& loop_sel,
const int& loop_cnt
);
关键设计决策:
| 决策 | 实现 | 原因 |
|---|---|---|
| 三级嵌套循环 | ll → cc → rr → ss |
外层控制迭代轮次,中层遍历列块,内层处理行和流 |
| II=1 流水线 | #pragma HLS pipeline II=1 |
每周期从每路流读取 2 个样本,维持全速吞吐 |
| 条件存储 | if (ll == loop_sel) |
支持选择性捕获,便于调试和验证 |
| 双样本打包 | (val1, val0) |
128-bit 总线携带两个 cint32 样本,提升带宽效率 |
地址计算逻辑:
// 输入: 按列到达 (Column-major)
// cc: 列块索引 (0 ~ DEPTH/NSTREAM-1)
// rr: 行索引 (0 ~ DEPTH/2-1)
// ss: 流索引 (0 ~ NSTREAM-1)
// addr: 线性缓冲区地址,每次递增 2 (因为一次读 2 个样本)
addr = addr + 2;
注意: 代码中的注释说 "Incoming samples arriving down columns",但实际循环结构是 cc 在外、rr 在内,这意味着数据实际上是按行块优先到达的。这是与 transpose 模块配合的设计约定。
2. read_buffer() —— 缓冲区读出
职责: 从内部缓冲区按行优先顺序读取,打包成 DDR 突发写入格式
函数签名:
void read_buffer(
TT_DATA mem[NFFT/2],
TT_SAMPLE (&buff)[NSTREAM][DEPTH*DEPTH/NSTREAM]
);
关键设计决策:
| 决策 | 实现 | 原因 |
|---|---|---|
| 行优先遍历 | rr 外层,cc 内层 |
匹配主机期望的数据布局 |
| 跨 bank 交织读取 | ss0, ss1 交替 |
实现无冲突的并行 bank 访问 |
| 动态地址计算 | addr0, addr1 更新 |
处理 5-bank 非 2 的幂次方的复杂映射 |
| II=1 流水线 | #pragma HLS PIPELINE II=1 |
每周期输出一个 128-bit 字到 DDR |
地址计算复杂度:
// 由于 NSTREAM=5 不是 2 的幂,地址计算需要模运算
ss1 = (ss0 + 1) % NSTREAM; // 下一个 stream
addr1 = (ss0 == NSTREAM-1) ? addr0 + DEPTH : addr0; // 跨 bank 边界处理
// 每读完一对样本后的更新
if (ss0 == NSTREAM-1 || ss1 == NSTREAM-1) {
addr0 = addr0 + DEPTH; // 跳到下一行的起始
}
ss0 = (ss0 + 2) % NSTREAM; // 步进 2,因为是成对读取
3. ifft_dma_snk_wrapper() —— 顶层封装
接口定义:
void ifft_dma_snk_wrapper(
TT_DATA mem[NFFT/2], // m_axi: DDR 目标地址
int loop_sel, // s_axilite: 选择捕获哪一轮迭代
int loop_cnt, // s_axilite: 总迭代次数
TT_STREAM sig_i[NSTREAM] // axis: 5 路输入流
);
HLS 接口约束:
#pragma HLS interface m_axi port=mem bundle=gmem offset=slave depth=NFFT/2
#pragma HLS interface axis port=sig_i
#pragma HLS interface s_axilite port=loop_sel bundle=control
#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_SAMPLE buff[NSTREAM][DEPTH*DEPTH/NSTREAM];
// = 5 × (260 × 260 / 5) = 5 × 13520 = 67600 个样本
// = 67600 × 64 bit = 5.4 Mbit ≈ 85 URAM blocks (assuming 64Kb each)
#pragma HLS array_partition variable=buff dim=1 // 完全分区第 1 维
#pragma HLS bind_storage variable=buff type=RAM_T2P impl=uram // 真双端口 URAM
为何选择 URAM T2P?
- URAM: 比 BRAM 容量大(64Kb vs 18Kb),适合大缓冲区
- T2P (True Dual Port): 支持同时读写不同地址,DATAFLOW 流水线必需
- array_partition dim=1: 5 个 bank 完全独立,消除访问冲突
关键设计权衡
1. 零填充 (Zero Padding) 的取舍
背景: 256 不能被 5 整除,但为了 5 路并行处理...
| 选项 | 实现 | 利弊 |
|---|---|---|
| A: 严格 256x256 | 不均匀分配 256 行到 5 个流 | ❌ 地址计算极其复杂,容易产生 bank 冲突 |
| B: 零填充到 260x260 | 每流处理 52 行 (260/5=52) | ✅ 均匀分配,简化地址生成;❌ 浪费约 3% 存储和带宽 |
选择 B,因为硬件设计的简洁性远比 3% 的效率损失更有价值。
2. 单缓冲 vs 乒乓缓冲
| 方案 | 资源 | 性能 | 适用场景 |
|---|---|---|---|
| 单缓冲 | 50% URAM | 捕获和读取必须串行 | 低吞吐、资源受限 |
| 乒乓缓冲 | 100% URAM | 完全流水线化 | 高吞吐、实时性要求 |
本设计实际使用单缓冲 + DATAFLOW,依赖 HLS 自动推断的乒乓行为(通过函数级并行实现)。严格来说,这不是硬件乒乓,而是软件流水线的重叠执行。
3. 流控策略
假设: AI Engine 和 PL 之间的流控由 AXI-Stream 的 TREADY/TVALID 握手自动处理
风险: 如果 DDR 写入慢于 AI Engine 输出,背压会传递到 AI Engine
缓解:
- 确保
read_buffer的 throughput ≥capture_streams - 两者都是 II=1,且 DDR 突发写入带宽充足
带宽计算澄清:
- 每帧数据量:
NFFT/2 = 32768个TT_DATA字,每个 128-bit - 总数据量: 32768 × 128 = 4 Mb = 0.5 MB 每帧
- 帧时间 (2 Gsps): 32768 / 2G = 16.384 μs
- 所需 DDR 带宽: 0.5 MB / 16.384 μs ≈ 30.5 MB/s
这个带宽远低于 DDR4 的能力,因此不会成为瓶颈。
4. loop_sel 的设计意图
这个参数看似多余——为什么不总是捕获所有数据?
用途:
- 验证模式: 在仿真中选择特定迭代检查中间结果
- 调试支持: 隔离问题到特定数据帧
- 部分重配置: 理论上支持只更新部分输出(虽然当前设计未充分利用)
与系统其他部分的协作
上游依赖
AI Engine Back-End (ifft256p4)
↓ PLIO_back_o_[0-4]
↓ AXI-Stream @ 312.5 MHz
ifft_dma_snk_wrapper
下游连接
ifft_dma_snk_wrapper
↓ m_axi (gmem bundle)
↓ AXI4-Full burst
LPDDR Controller
↓
Host CPU (PS)
横向关联
| 模块 | 关系 | 说明 |
|---|---|---|
| dma_source_ingress_pipeline | 对称角色 | 负责数据输入,结构与 sink 类似但方向相反 |
| transpose_compute_stage | 数据生产者 | Transpose 的输出是 Sink 的输入 |
| host.cpp | 控制者 | 配置 loop_sel 和 loop_cnt,启动 DMA |
新贡献者必读:陷阱与注意事项
⚠️ 常见错误
-
误解数据到达顺序
- 注释说 "down columns",但看循环结构是行块优先
- 建议: 始终以实际仿真波形为准,不要只看注释
-
忽视
EXTRA的影响DEPTH = 260不是256,缓冲区大小计算要包含零填充- 错误:
256 * 256 / 5 - 正确:
260 * 260 / 5 = 13520
-
地址计算的整数溢出
// 危险: addr 可能超出 int 范围 int addr = rr * DEPTH + cc; // 如果 rr=255, cc=255, DEPTH=260 // 255 * 260 + 255 = 66555 < 2^31 (安全) // 但对于更大的 FFT 尺寸要小心 -
混淆
loop_sel和loop_cntloop_cnt: 总共运行多少次迭代loop_sel: 只保存第几次迭代的数据(0-indexed)- 如果
loop_sel >= loop_cnt,不会保存任何有效数据
🔧 调试技巧
-
使用 C-Simulation 验证地址计算
cd hls/ifft_dma_snk make csim -
检查波形中的握手信号
- 关注
sig_i_*_TVALID和sig_i_*_TREADY - 如果
TREADY长期为低,说明背压传递到 AI Engine
- 关注
-
验证 DDR 写入地址
- 在 hw_emu 中检查
m_axi_gmem_AWADDR是否单调递增 - 突发长度应该是连续的
- 在 hw_emu 中检查
📋 修改检查清单
如果你需要修改这个模块:
- [ ] 常量变更 (
NSTREAM,EXTRA,NFFT_1D) 需要在头文件和 system.cfg 中同步 - [ ] 地址计算逻辑修改后,用 MATLAB 模型验证数据布局
- [ ] 缓冲区大小变化时,检查 URAM 资源是否足够
- [ ] 接口协议变化时,更新 system.cfg 的 connectivity 部分
- [ ] 测试bench 的 golden 数据需要重新生成