第 2 章:高速公路管理——使用包交换进行组播和路由
在第 1 章中,我们了解了数据如何在 PS、PL 和 AIE 这三个“城市”之间通过“专用道路”流动。但如果我们有 10 个、100 个甚至更多的数据流需要同时传输呢?为每一条数据流都修一条专用道路显然是不现实的——就像现实中我们不可能为每家每户都修一条直达机场的高速公路。
本章我们要学习的包交换(Packet Switching)技术,就是解决这个问题的关键。
2.1 问题的由来:专用连接的局限性
首先,让我们回顾一下第 1 章的架构,并看看它在扩展时会遇到什么问题。
2.1.1 专用连接就像“私家车道”
在第 1 章的例子中,我们使用的是专用流连接(Dedicated Stream Connection):
这就像给每一个 AIE 核心都修了一条私家车道。
专用连接的优势(第 1 章场景适用)
- 简单直接:数据从 A 直接到 B,不需要额外处理
- 延迟最低:没有中间环节,数据一拍即达
- 无额外开销:不需要传输地址、校验等额外信息
专用连接的劣势(扩展时致命)
- 资源浪费:PL 和 AIE 之间的物理连接数量是固定且有限的(通常只有几十到几百条)
- 无法灵活路由:如果 AIE 核心 1 坏了,你不能把 DMA 1 的数据自动重路由到 AIE 核心 2
- 无法实现组播:如果要把同一份数据发给 10 个 AIE 核心,你需要 10 条独立的连接
2.2 解决方案:包交换网络——就像城市快递系统
包交换的核心思想很简单:把数据打包,加上地址标签,然后通过共享的主干道传输。
这就像现代城市的快递系统:
| 现实世界概念 | 技术术语 | 说明 |
|---|---|---|
| 快递包裹 | 数据包(Packet) | 包含“货物”(数据)和“快递单”(包头) |
| 快递单 | 包头(Header) | 包含目的地地址、包大小、类型等信息 |
| 城市主干道 | 共享物理通道 | 所有数据包都走这几条路 |
| 分拣中心 | 包交换器(Packet Switch) | 根据地址标签决定把包发往哪里 |
2.2.1 包交换的基本架构
让我们用一个简单的 4 路输入、1 路共享输出的例子来说明:
温度数据] S2[传感器 2
湿度数据] S3[传感器 3
压力数据] S4[传感器 4
光照数据] end subgraph Packager["打包器(Packet Sender)"] P1[加标签] MUX[时分复用
Time Division Multiplexing] end subgraph Highway["共享主干道(1 条物理链路)"] direction TB PKT1[包 1: 温度] PKT2[包 2: 湿度] PKT3[包 3: 压力] PKT4[包 4: 光照] PKT1 --- PKT2 --- PKT3 --- PKT4 end subgraph Router["解包器(Packet Receiver)"] DEMUX[解复用] CHECK[查标签] end subgraph Outputs["输出(路由到不同处理单元)"] T1[温度处理核] T2[湿度处理核] T3[压力处理核] T4[光照处理核] end S1 --> P1 S2 --> P1 S3 --> P1 S4 --> P1 P1 --> MUX MUX --> Highway Highway --> DEMUX DEMUX --> CHECK CHECK --> T1 CHECK --> T2 CHECK --> T3 CHECK --> T4
关键点:
- 时分复用(TDM):4 路数据在时间上交错排列,共享同一条物理链路
- 带内路由:路由信息(地址标签)就在数据包里面,不需要单独的控制通道
- 统计复用:如果某一路数据暂时没有,其他路可以占用更多带宽(而不是像专用连接那样一直占着路)
2.3 Versal 中的包交换实现:从 PL 到 AIE
现在我们来看 Vitis-Tutorials 中实际的包交换系统是如何工作的。
2.3.1 系统整体架构(来自 packet_switching_and_streaming 模块)
hls_packet_sender] PReceiver[HLS 包接收器
hls_packet_receiver] S2MM1[s2mm_1] S2MM2[s2mm_2] S2MM3[s2mm_3] S2MM4[s2mm_4] end subgraph AIE["AIE 阵列"] AIEIn[输入端口
Datain0] Kernel[AIE 计算内核] AIEOut[输出端口
Dataout0] end %% 连接关系 CPU <--> DDR DDR --> MM2S1 DDR --> MM2S2 DDR --> MM2S3 DDR --> MM2S4 MM2S1 --> PSender MM2S2 --> PSender MM2S3 --> PSender MM2S4 --> PSender PSender -->|共享流| AIEIn AIEIn --> Kernel Kernel --> AIEOut AIEOut -->|共享流| PReceiver PReceiver --> S2MM1 PReceiver --> S2MM2 PReceiver --> S2MM3 PReceiver --> S2MM4 S2MM1 --> DDR S2MM2 --> DDR S2MM3 --> DDR S2MM4 --> DDR
2.3.2 关键组件详解
1. HLS 包发送器(hls_packet_sender)
这是整个系统的“打包员”和“调度员”。
职责:
- 从 4 个 MM2S DMA 接收原始数据流
- 为每个数据块添加包头(Header)
- 目标端口 ID(告诉接收者这个包要发给谁)
- 包长度(有效载荷有多少数据)
- 可选:数据类型(int32、float、cint16 等)
- 通过仲裁逻辑(比如轮询 Round-Robin)把 4 路数据交错放到 1 条共享流上
类比 Express.js: 这就像 Express.js 的中间件,把来自不同路由的请求打包成统一的格式,然后转发给下一个处理环节。
2. HLS 包接收器(hls_packet_receiver)
这是系统的“分拣员”。
职责:
- 从 AIE 接收共享的数据包流
- 解析包头,提取目标端口信息
- 根据目标端口把数据包分发到对应的 S2MM DMA
3. AIE 接口:Buffer-based vs Packet-stream
AIE 支持两种接口模式来接收数据包,就像两种不同的收货方式:
| 模式 | Buffer-based | Packet-stream |
|---|---|---|
| 类比 | 批量送货上门,直接放到仓库 | 快递员一件一件递到你手上 |
| 优点 | 高吞吐量,工具链自动优化 DMA | 灵活性高,可逐包处理路由 |
| 缺点 | 灵活性较低 | 有包头开销,小包效率低 |
| 适用场景 | 图像处理、雷达信号等批量数据 | 通信系统、多通道处理 |
2.4 进阶功能:组播(Multicast)
包交换的另一个超级能力是组播——把同一份数据同时发给多个接收者。
2.4.1 什么是组播?
单播(Unicast):一对一发送(比如私信) 广播(Broadcast):一对所有发送(比如小区广播) 组播(Multicast):一对多发送(比如群聊,只有在群里的人能收到)
2.4.2 组播在 N-Body 模拟中的应用(来自 n_body_packetized_pl_aie_connectivity 模块)
在 N-Body 粒子模拟中,我们需要把每个粒子的位置信息发给所有其他粒子来计算引力。如果不用组播,数据量会是 O(N²),用了组播就可以降到 O(N)!
读取粒子数据] PSender[包发送器
支持组播] end subgraph AIE["AIE 阵列(100 个核心)"] AIE0[AIE 核心 0] AIE1[AIE 核心 1] AIE99[AIE 核心 99] end DMA -->|粒子 i 的数据| PSender PSender -->|组播: 目标 = {0,1,...,99}| AIE0 PSender -->|组播: 目标 = {0,1,...,99}| AIE1 PSender -->|组播: 目标 = {0,1,...,99}| AIE99 style AIE0 fill:#f9f,stroke:#333 style AIE1 fill:#f9f,stroke:#333 style AIE99 fill:#f9f,stroke:#333
2.5 实践指南:如何在你的设计中使用包交换
2.5.1 步骤 1:定义你的包格式
首先,你需要和“寄件人”、“收件人”约定好快递单的格式。
一个典型的包头定义(伪代码):
typedef struct {
ap_uint<8> dest_id; // 目标端口 ID(0-255)
ap_uint<8> src_id; // 源端口 ID(可选,用于调试)
ap_uint<16> pkt_len; // 有效载荷长度(单位:字)
ap_uint<4> data_type; // 数据类型:0=int32, 1=float, 2=cint16...
} packet_header_t;
2.5.2 步骤 2:配置 system.cfg
这是告诉 Vitis 工具链如何连接各个组件的“施工图纸”。
[connectivity]
# 1. 声明我们需要多少个内核实例
nk=mm2s:4:mm2s_1.mm2s_2.mm2s_3.mm2s_4
nk=s2mm:4:s2mm_1.s2mm_2.s2mm_3.s2mm_4
nk=hls_packet_sender:1:hls_packet_sender_1
nk=hls_packet_receiver:1:hls_packet_receiver_1
# 2. 连接 DMA 到包发送器
stream_connect=mm2s_1.s:hls_packet_sender_1.s0
stream_connect=mm2s_2.s:hls_packet_sender_1.s1
stream_connect=mm2s_3.s:hls_packet_sender_1.s2
stream_connect=mm2s_4.s:hls_packet_sender_1.s3
# 3. 连接包发送器到 AIE(共享通道)
stream_connect=hls_packet_sender_1.out:ai_engine_0.Datain0
# 4. 连接 AIE 到包接收器(共享通道)
stream_connect=ai_engine_0.Dataout0:hls_packet_receiver_1.in
# 5. 连接包接收器到 DMA
stream_connect=hls_packet_receiver_1.out0:s2mm_1.s
stream_connect=hls_packet_receiver_1.out1:s2mm_2.s
stream_connect=hls_packet_receiver_1.out2:s2mm_3.s
stream_connect=hls_packet_receiver_1.out3:s2mm_4.s
[clock]
defaultFreqHz=250000000 # 250MHz 系统时钟
2.6 避坑指南:常见问题与解决方案
2.6.1 坑 1:包头格式不一致
症状:数据看起来是乱码,或者路由到了错误的地方。
原因:发送方和接收方对包头的理解不一样(比如一个认为 dest_id 是 8 位,另一个认为是 16 位)。
解决:
- 把包头定义放在一个共享的头文件里
- 使用
static_assert来验证包的大小 - 在仿真阶段先验证端到端的数据完整性
2.6.2 坑 2:FIFO 深度不够导致死锁
症状:系统挂起,没有任何输出。
原因:包发送器内部的 FIFO 太浅,当 4 个 MM2S 同时有数据突发时,FIFO 溢出,导致背压(Back-pressure)阻塞了整个系统。
解决:
- 增加 HLS 中
hls::stream的深度 - 使用
#pragma HLS DATAFLOW来增加流水线缓冲 - 经验公式:
FIFO深度 >= 突发长度 × (源数量 - 1) + 安全余量
2.6.3 坑 3:时序收敛困难
症状:Vivado 综合时报时序违例(Timing Violation)。
原因:包交换器的逻辑太复杂,或者连接太多(比如 x10 设计中的 100 个输出端口),导致关键路径太长。
解决:
- 在 HLS 代码中使用
#pragma HLS PIPELINE II=1来流水线化 - 在
system.cfg中降低时钟频率(比如从 300MHz 降到 200MHz) - 使用物理约束(Pblocks)把 PL 内核放在靠近 AIE 接口的地方
2.7 总结与下一步
在本章中,我们学习了:
- 为什么需要包交换:解决专用连接的资源浪费和不灵活问题
- 包交换的基本原理:打包、加标签、共享传输、解包
- Versal 中的实现:HLS 包发送器/接收器、AIE 的两种接口模式
- 高级功能:组播(Multicast)如何大幅减少数据传输量
- 实践避坑:三个最常见的问题及其解决方案
类比回顾:
- 专用连接 = 私家车道
- 包交换 = 城市快递系统 + 主干道
- 组播 = 群聊消息
下一步预告
在第 3 章中,我们将学习运行时参数重配置(RTP)——这就像在开车的时候不用停车就能调整方向盘和油门,让我们可以在不重启 AIE 的情况下动态改变计算参数!
练习与思考(可选)
- 如果你有 8 个摄像头的数据要传给 4 个 AIE 核心做不同的处理,你会如何设计包格式?
- 在 N-Body 模拟中,如果粒子数量是 1024,AIE 核心是 64 个,组播能减少多少倍的数据传输量?
- 除了本章提到的温度、湿度等传感器,你还能想到哪些应用场景适合用包交换?