第 1 章:跨越边界——数据如何在 PS、PL 和 AIE 之间流动
欢迎来到 Versal ACAP 异构计算世界的第一站!
想象一下,你正在经营一家 现代化的三合一工厂:
- 一间是办公室(PS):经理在这里接订单、安排生产、核对账目
- 一间是传送带车间(PL):专门负责快速搬运原材料和成品
- 一间是精密加工中心(AIE):拥有最先进的机床,负责核心的加工工序
这三个车间各自效率极高,但如果它们之间的货物运输出了问题,整个工厂就会瘫痪。
本章的目标,就是教会你如何在这三个核心区域之间铺设高效的“数据传送带”。
1.1 首先,认识我们的三个“车间”
在开始搭建管道之前,我们先彻底搞清楚 Versal 芯片里的这三个核心计算域(Domains)。
你可以把 Versal 想象成一架 大型民航客机:
(ARM Cortex-A72/A53)"] Controls["操控系统
(Linux/裸机应用)"] end subgraph CargoHold["货舱与传送带系统 (PL - Programmable Logic)"] ConveyorIn["入货传送带
(MM2S 核)"] ConveyorOut["出货传送带
(S2MM 核)"] end subgraph Engines["引擎组 (AIE - AI Engine Array)"] Engine1["引擎 1
(AIE Tile)"] Engine2["引擎 2
(AIE Tile)"] Engine3["引擎 3
(AIE Tile)"] end FuelTank[(油箱/货舱
DDR 内存)] Pilot -->|发布指令| Controls Controls -->|管理| FuelTank FuelTank <-->|装卸货物| ConveyorIn FuelTank <-->|装卸货物| ConveyorOut ConveyorIn -->|输送原料| Engine1 Engine1 -->|半成品| Engine2 Engine2 -->|半成品| Engine3 Engine3 -->|成品| ConveyorOut
PS (Processing System):驾驶舱
- 它是什么:ARM 处理器集群,就像飞机的驾驶舱。
- 它的工作:运行 Linux 操作系统、执行你的主程序(host app)、给其他部分发号施令、管理 DDR 内存。
- 它的性格:灵活、通用,但不适合做极其重复的数学计算。
PL (Programmable Logic):传送带系统
- 它是什么:你可以重新配置的硬件电路,就像工厂里的传送带和分拣机。
- 它的工作:本章的重点——数据搬运工。它负责在 PS 的“内存仓库”和 AIE 的“加工中心”之间搬运数据。
- 它的性格:带宽极高、完全并行、不懂拐弯抹角(电路是什么样就只能做什么事)。
AIE (AI Engine Array):精密加工中心
- 它是什么:由数百个(甚至更多)计算核心组成的阵列,专门用于数字信号处理(DSP)和机器学习(ML)。
- 它的工作:进行高强度的数学运算,比如 FFT、滤波、矩阵乘法。
- 它的性格:极度高效、低功耗,但需要数据源源不断地“喂”给它。
1.2 核心矛盾:语言不通怎么办?
这三个车间虽然在同一块芯片上,但它们说的语言完全不一样:
| 区域 | "母语" (接口协议) | 数据偏好 |
|---|---|---|
| PS / DDR | AXI4-MM (Memory Mapped) | 喜欢大块大块的连续数据块(Buffer),就像整箱的货物。 |
| PL | AXI4-Stream | 喜欢连续不断的数据流,没有地址,只有一个个顺次排列的数据,就像传送带上的单个包裹。 |
| AIE | Window / Stream | 内部有两种方言: 1. Window (窗口):一小块局部内存,适合需要“回头看”历史数据的算法(如 FIR 滤波)。 2. Stream (流):纯单向数据,来一个处理一个。 |
这就麻烦了!办公室的人把货物装在箱子里,精密机床只接收单独的零件。
解决方案是什么?
我们需要一个翻译官,或者说货物打包/拆包员。
在 Versal 里,这个角色由 Data Movers(数据搬运核) 担任。
1.3 数据搬运的“三剑客”
为了实现 PS ↔ PL ↔ AIE 的无缝连接,我们需要三个核心角色。让我们用 “外卖点单” 的比喻来理解它们:
- 你 (Host/PS):在手机 app 上下单,把钱(数据)准备好。
- 取餐员 (MM2S):去商家的仓库(DDR)把整份餐取出来,放到外卖箱里,然后骑着车(AXI4-Stream)一路送过去。
- 餐厅后厨 (AIE Graph):把菜做好。
- 送餐员 (S2MM):把做好的菜从后厨取出来,送回你家(DDR)。
- 你 (Host/PS):开门收餐,品尝(验证)结果。
现在,我们把这个流程固化成技术架构图:
XRT 应用程序] end subgraph KitchenBackend["厨房后端 (PL)"] MM2S[取餐员
MM2S HLS 核
Memory to Stream] S2MM[送餐员
S2MM HLS 核
Stream to Memory] end subgraph Chefs["主厨团队 (AIE)"] Interp[切菜工
Interpolator] Clip[调味工
Polar Clip] Class[装盘工
Classifier] end Fridge[(冰箱
DDR 内存)] App -->|1. 把原料放入冰箱| Fridge App -->|2. 开始工作| MM2S MM2S -->|3. 取原料| Fridge MM2S -->|4. 流水式送菜| Interp Interp -->|切好| Clip Clip -->|调好| Class Class -->|5. 成品流出| S2MM S2MM -->|6. 存入冰箱| Fridge Fridge -->|7. 取出成品| App
角色详解
1. MM2S (Memory Map to Stream)
- 功能:把数据从 DDR(内存映射地址空间)读取出来,转换成 AXI4-Stream 流协议发送出去。
- 类比:拆包员。把整箱的东西一件一件放到传送带上。
- 代码印象(后面会细看):一个简单的
for循环,从数组里读一个数,写到流接口里。
2. AIE Graph (AI Engine 图)
- 功能:实际处理数据的流水线。
- 类比:车间流水线。
- 接口选择:
- 如果是
FIR滤波器这种需要记住前面数据的,我们用 Window 接口(给它一小块工作台)。 - 如果是简单的
y = x * 2这种逐样本处理,我们用 Stream 接口(直接从传送带上拿)。
- 如果是
3. S2MM (Stream to Memory Map)
- 功能:接收 AXI4-Stream 流数据,把它们写回 DDR 内存。
- 类比:打包员。把传送带上的东西一件件捡起来,装箱放回仓库。
1.4 一次完整的数据旅行(带时间线)
让我们跟着一组数据,看看它从输入到输出的完整一生。
数据搬运到设备 DDR Note over Host,AIE: 阶段 2: 启动生产线 Host->>AIE: 3. graph.run(1)
AIE 各就各位 Host->>MM2S: 4. mm2s.run()
取餐员出发 Host->>S2MM: 5. s2mm.run()
送餐员出发 Note over MM2S,S2MM: 阶段 3: 数据流动 (关键!) MM2S->>DDR: 6. 读取大段数据 (Burst) loop 每个时钟周期 MM2S->>AIE: 7. 流式发送 (Stream) AIE->>AIE: 8. 计算处理 AIE->>S2MM: 9. 流式输出 (Stream) S2MM->>DDR: 10. 写回数据 end Note over Host,DDR: 阶段 4: 收获结果 S2MM-->>Host: 11. s2mm.wait() 完成 DDR->>Host: 12. sync(XCL_BO_SYNC_BO_FROM_DEVICE) Host->>Host: 13. 验证结果是否正确
这张图里的 3 个关键细节(新手必记):
-
XRT 是什么? 图中 Host 发出的所有命令,都是通过 XRT (Xilinx Runtime) 库发送的。你可以把它想象成 Versal 芯片的“驱动程序”,它提供了
xrt::device、xrt::kernel等工具,让你在 C++ 代码里控制硬件。 -
启动顺序很重要! 注意我们是先启动 AIE Graph,再启动 MM2S。如果搞反了,数据已经流过来了,但 AIE 还没准备好,数据就会丢失,系统就会报错(Deadlock)。这就像你必须先开水龙头,再把杯子放过去接水。
-
Buffer (缓冲区) 对齐 当 Host 分配内存给 DDR 时,必须使用 4KB 对齐。这就好比停车场的车位必须画得整整齐齐,停车机器人(DMA)才能准确无误地把车停进去。如果不对齐,传输速度会变得极慢,甚至直接报错。
1.5 眼见为实:看一段极简的代码
光说不练假把式。我们不用看几干行的复杂代码,只看最核心的三行伪代码,你就能理解整个系统的骨架。
1. PL 侧的 MM2S (拆包员)
文件位置:pl_kernels/mm2s.cpp
这是一个 HLS 代码(High-Level Synthesis,把 C++ 变成硬件电路)。
// 极简版 MM2S
void mm2s(int* memory, hls::stream<int>& stream_out, int size) {
for (int i = 0; i < size; i++) {
#pragma HLS PIPELINE II=1 // 关键:每个时钟周期处理一个数据
int data = memory[i]; // 从 DDR 内存拿一个
stream_out.write(data); // 写到流里去
}
}
2. AIE 侧的 Graph (连接图)
文件位置:aie/graph.h
这是用来描述 AIE 核之间是怎么连接的“接线图”。
// 极简版 AIE Graph
using namespace adf;
class MyGraph : public graph {
private:
// 声明三个加工工人(核)
interpolator my_interp;
polar_clip my_clip;
classifier my_class;
public:
// 声明外部的输入输出端口
input_plio in;
output_plio out;
MyGraph() {
// 像拼乐高一样把它们连起来!
// PL 的输入 -> 第一个工人
connect(in.out[0], my_interp.in[0]);
// 工人之间手拉手
connect(my_interp.out[0], my_clip.in[0]);
connect(my_clip.out[0], my_class.in[0]);
// 最后一个工人 -> PL 的输出
connect(my_class.out[0], out.in[0]);
}
};
3. PS 侧的 Host (指挥官)
文件位置:host/host.cpp
// 极简版 Host 代码
int main() {
// 1. 找到设备,加载 xclbin 文件(也就是把硬件电路“烧”进去)
auto device = xrt::device(0);
auto xclbin = xrt::xclbin("design.xclbin");
device.register_xclbin(xclbin);
// 2. 准备数据 (注意:这里要 4KB 对齐!)
std::vector<int, aligned_allocator<int>> input_data(SIZE);
std::vector<int, aligned_allocator<int>> output_data(SIZE);
// ... fill input_data ...
// 3. 把数据交给 DDR
auto bo_input = xrt::bo(device, input_data.data(), SIZE*4, 0);
bo_input.sync(XCL_BO_SYNC_BO_TO_DEVICE);
// 4. 启动所有部件
auto mm2s_kernel = xrt::kernel(device, xclbin, "mm2s");
auto s2mm_kernel = xrt::kernel(device, xclbin, "s2mm");
auto my_graph = xrt::graph(device, xclbin, "mygraph");
my_graph.run(1); // 先开流水线
auto run_mm2s = mm2s_kernel(bo_input, ..., SIZE);
auto run_s2mm = s2mm_kernel(bo_output, ..., SIZE);
// 5. 等待完成,收结果
run_s2mm.wait();
bo_output.sync(XCL_BO_SYNC_BO_FROM_DEVICE);
// ... verify output_data ...
}
1.6 配置文件:告诉编译器怎么“接线”
除了代码,我们还需要一个“施工蓝图”来告诉 Vitis 工具如何把上面这三块拼在一起。
这个文件通常叫 system.cfg。
[connectivity]
# 实例化 1 个 mm2s 核,名字叫 mm2s_1
nk=mm2s:1:mm2s_1
# 实例化 1 个 s2mm 核,名字叫 s2mm_1
nk=s2mm:1:s2mm_1
# 关键连线!
# 把 mm2s_1 的 s 端口 (流输出) 连到 AIE 引擎的 DataIn1 端口
sc=mm2s_1.s:ai_engine_0.DataIn1
# 把 AIE 引擎的 DataOut1 端口连到 s2mm_1 的 s 端口 (流输入)
sc=ai_engine_0.DataOut1:s2mm_1.s
1.7 本章总结与下章预告
你在本章掌握了什么?
- Versal 三兄弟:PS(大脑)、PL(骨架/血管)、AIE(肌肉)。
- 翻译官:MM2S(内存拆成流)和 S2MM(流拼成内存)。
- 基本流程:Host 准备数据 -> 同步到 DDR -> 启动 AIE -> 启动 PL 搬运 -> 等待完成 -> 取回数据。
类比复习卡
| 技术术语 | 生活比喻 |
|---|---|
| DDR 内存 | 工厂仓库/冰箱 |
| MM2S | 取餐员/拆包员 |
| S2MM | 送餐员/打包员 |
| AIE Stream | 传送带 |
| AIE Window | 工人的工作台 |
| XRT | 工厂的对讲机系统 |
下一章,我们将面对一个新的挑战: 如果传送带的数量不够,但是我们要同时运很多不同种类的货物怎么办? 我们将学习如何在同一条“传送带”上通过 Packet Switching(数据包交换) 技术运送多股数据流,实现真正的“高速公路管理”!