🏠

第 1 章:跨越边界——数据如何在 PS、PL 和 AIE 之间流动

欢迎来到 Versal ACAP 异构计算世界的第一站!

想象一下,你正在经营一家 现代化的三合一工厂

  • 一间是办公室(PS):经理在这里接订单、安排生产、核对账目
  • 一间是传送带车间(PL):专门负责快速搬运原材料和成品
  • 一间是精密加工中心(AIE):拥有最先进的机床,负责核心的加工工序

这三个车间各自效率极高,但如果它们之间的货物运输出了问题,整个工厂就会瘫痪。

本章的目标,就是教会你如何在这三个核心区域之间铺设高效的“数据传送带”。


1.1 首先,认识我们的三个“车间”

在开始搭建管道之前,我们先彻底搞清楚 Versal 芯片里的这三个核心计算域(Domains)。

你可以把 Versal 想象成一架 大型民航客机

graph TD subgraph Cockpit["驾驶舱 (PS - Processing System)"] Pilot["机组人员
(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 的无缝连接,我们需要三个核心角色。让我们用 “外卖点单” 的比喻来理解它们:

  1. 你 (Host/PS):在手机 app 上下单,把钱(数据)准备好。
  2. 取餐员 (MM2S):去商家的仓库(DDR)把整份餐取出来,放到外卖箱里,然后骑着车(AXI4-Stream)一路送过去。
  3. 餐厅后厨 (AIE Graph):把菜做好。
  4. 送餐员 (S2MM):把做好的菜从后厨取出来,送回你家(DDR)。
  5. 你 (Host/PS):开门收餐,品尝(验证)结果。

现在,我们把这个流程固化成技术架构图:

flowchart LR subgraph User["用户端 (Host/PS)"] App[host.cpp
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 一次完整的数据旅行(带时间线)

让我们跟着一组数据,看看它从输入到输出的完整一生。

sequenceDiagram participant Host as PS (host.cpp) participant DDR as DDR 内存 participant MM2S as PL (MM2S 核) participant AIE as AIE 处理阵列 participant S2MM as PL (S2MM 核) Note over Host,DDR: 阶段 1: 准备原料 Host->>Host: 1. 在内存中生成数据 Host->>DDR: 2. sync(XCL_BO_SYNC_BO_TO_DEVICE)
数据搬运到设备 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 个关键细节(新手必记):

  1. XRT 是什么? 图中 Host 发出的所有命令,都是通过 XRT (Xilinx Runtime) 库发送的。你可以把它想象成 Versal 芯片的“驱动程序”,它提供了 xrt::devicexrt::kernel 等工具,让你在 C++ 代码里控制硬件。

  2. 启动顺序很重要! 注意我们是先启动 AIE Graph,再启动 MM2S。如果搞反了,数据已经流过来了,但 AIE 还没准备好,数据就会丢失,系统就会报错(Deadlock)。这就像你必须先开水龙头,再把杯子放过去接水。

  3. 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 本章总结与下章预告

你在本章掌握了什么?

  1. Versal 三兄弟:PS(大脑)、PL(骨架/血管)、AIE(肌肉)。
  2. 翻译官:MM2S(内存拆成流)和 S2MM(流拼成内存)。
  3. 基本流程:Host 准备数据 -> 同步到 DDR -> 启动 AIE -> 启动 PL 搬运 -> 等待完成 -> 取回数据。

类比复习卡

技术术语 生活比喻
DDR 内存 工厂仓库/冰箱
MM2S 取餐员/拆包员
S2MM 送餐员/打包员
AIE Stream 传送带
AIE Window 工人的工作台
XRT 工厂的对讲机系统

下一章,我们将面对一个新的挑战: 如果传送带的数量不够,但是我们要同时运很多不同种类的货物怎么办? 我们将学习如何在同一条“传送带”上通过 Packet Switching(数据包交换) 技术运送多股数据流,实现真正的“高速公路管理”!

On this page