1. 为什么要在 Versal 上折腾 AXI NoC
Versal 这颗芯片跟传统 FPGA 最大的区别,就是它内部塞了一张AXI NoC(Network on Chip)。你可以把它理解成芯片内部的一张"高速公路网":PL 端的逻辑、PS 端的处理器、DDR 控制器、各种硬核 IP,全都挂在这张网上。以前在 Zynq 上做互联,靠的是 AXI Interconnect 一层层搭,带宽和延迟全看你怎么连;到了 Versal,NoC 直接把这件事硬件化了,你只需要配置"从哪个口进、从哪个口出、走哪条路径、给多少带宽",剩下的交给 NoC 自己调度。
这个项目标题里的三个关键词——AXI NoC 配置、仿真、QoS 调优——其实是一条完整的链路。配置解决的是"通不通",仿真解决的是"对不对",QoS 调优解决的是"快不快、稳不稳"。很多人第一次用 Versal,配置完 NoC 就急着上板,结果发现带宽上不去、延迟抖动大,回头再查发现是 QoS 的仲裁策略没配对。所以我把这三步串起来讲,尽量把每一步背后的逻辑说清楚。
这篇文章适合谁看?如果你已经用过 Vivado 做 Zynq 或者纯 FPGA 的 AXI 互联,现在要迁移到 Versal 平台,那这篇基本就是你要的。如果你是完全的新手,建议先把 AXI4 协议的基本握手搞清楚,再来看 NoC,不然会有点懵。我下面会尽量用生活化的类比来解释,但该有的参数和步骤一个都不会少。
先说清楚一个概念:Versal 里的 NoC 不是"一个" NoC,而是分成了NoC Master(NMU)和NoC Slave(NSU)两端,中间通过 NoC 的交换网络连接。NMU 是发起方,比如 PL 里的一个 AXI Master、PS 里的一个端口;NSU 是接收方,比如 DDR 控制器、PL 里的 AXI Slave。你在 Vivado 里配置的,本质上就是"给每个 NMU 到 NSU 的路径分配资源"。
2. 内容整体设计与思路拆解
2.1 先想清楚数据流,再动手配 NoC
我见过太多人一上来就打开 Vivado 的 NoC 配置界面,对着那一堆 NMU/NSU 端口发呆。正确的顺序应该是:先在纸上画出数据流。谁产生数据、谁消费数据、数据量多大、实时性要求多高,这四个问题回答清楚了,NoC 的配置方案基本就出来了。
举个具体的例子。假设你要做一个视频处理链路:PL 里的图像采集 IP 往 DDR 写原始帧,PS 里的处理器读出来做算法处理,处理完再写回 DDR,最后 PL 里的显示 IP 从 DDR 读出来送显示器。这条链路里有四个数据流:
- 采集 IP → DDR(写,大带宽,可以容忍一定延迟)
- PS → DDR(读,中等带宽,延迟敏感)
- PS → DDR(写,中等带宽)
- 显示 IP ← DDR(读,大带宽,延迟极其敏感,晚了就撕裂)
你看,同样是访问 DDR,不同数据流的诉求完全不一样。显示这条如果延迟抖动大,画面就会出问题;采集这条只要平均带宽够,偶尔慢一点无所谓。这就是为什么 QoS 调优必须建立在"想清楚数据流"的基础上——你连谁重要都没搞清楚,怎么分配优先级?
2.2 方案选型:NoC 直连还是走 AXI Interconnect
Versal 里有个容易踩的坑:不是所有互联都必须走 NoC。PL 内部两个 IP 之间如果只是小数据量通信,直接用 AXI Interconnect 或者 AXI SmartConnect 反而更简单、延迟更低。NoC 的价值在于跨 die 区域、跨时钟域、连接硬核资源这些场景。
我的经验判断标准是这样的:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| PL 内部 IP 间小数据量 | AXI SmartConnect | 延迟低,配置简单 |
| PL 访问 DDR | AXI NoC | 必须走 NoC 才能到 DDR 控制器 |
| PS 访问 PL 里的 IP | AXI NoC 或 PL 内部互联 | 看数据量和延迟要求 |
| 多 Master 共享一个 Slave | AXI NoC + QoS | NoC 的仲裁比 Interconnect 更精细 |
| 跨 SLR 的大带宽传输 | AXI NoC | NoC 有专门的跨 SLR 路径优化 |
这个表不是绝对的,但大方向不会错。我个人的习惯是:只要涉及 DDR 或者硬核,一律走 NoC;纯 PL 内部能不走就不走。因为 NoC 虽然强大,但配置复杂度摆在那里,能省则省。
2.3 带宽预算:别等上板了才发现不够
NoC 配置里最核心的参数就是带宽分配。Versal 的 NoC 总带宽是有限的,所有 NMU 到 NSU 的路径共享这个总带宽。如果你给每条路径都标一个很大的带宽值,最后 Vivado 会告诉你资源不够,或者自动帮你压缩,压缩完的结果可能就不是你想要的。
我的做法是先算总账。假设 DDR 控制器的理论带宽是 19.2 GB/s(这个数字取决于你的 DDR 配置,具体看 DDR4 的位宽和频率),那么所有访问 DDR 的路径加起来不能超过这个数,实际还要留 20% 左右的余量给协议开销和刷新操作。然后按优先级分配:
- 显示读:需要 4 GB/s,给 5 GB/s 预算
- 采集写:需要 3 GB/s,给 4 GB/s 预算
- PS 读写:各需要 1 GB/s,各给 1.5 GB/s 预算
加起来 12 GB/s,离 19.2 GB/s 还有余量,这样配置起来就比较从容。如果你算下来发现超了,那就得回头优化数据流,比如降低分辨率、减少拷贝次数,而不是硬着头皮配。
提示:Vivado 的 NoC 配置界面里有个 "Bandwidth" 列,单位是 MB/s,填的时候注意单位换算。我见过有人把 4000 MB/s 填成 4000 GB/s,然后纳闷为什么工具不报错——其实工具会默默帮你截断,但结果肯定不对。
3. 核心细节解析与实操要点
3.1 NoC 配置界面的关键参数逐个拆解
打开 Vivado 的 NoC 配置,你会看到一个类似矩阵的界面,行是 NMU,列是 NSU,交叉点就是一条可能的路径。每条路径上你能配的参数主要有这几个:
Bandwidth(带宽):这条路径期望的带宽,单位 MB/s。这个值不是"保证值",而是"期望值",NoC 会根据所有路径的期望值和实际可用带宽做仲裁。填的时候要基于你的数据流计算,不要拍脑袋。
QoS Class(服务质量等级):这个参数决定了当多条路径竞争同一个 NSU 时,谁优先。Versal 的 NoC 支持多个 QoS 等级,数值越小优先级越高。我一般把实时性要求最高的路径(比如显示)设成最高优先级,把批量传输(比如大块数据搬运)设成低优先级。
Traffic Class(流量类别):这个跟 QoS Class 配合使用,主要影响 NoC 内部的缓冲和调度策略。对于延迟敏感的流量,选低延迟类别;对于带宽敏感但延迟不敏感的,选高吞吐类别。
Read/Write 方向:每条路径可以单独配读和写的带宽。如果你的数据流是单向的(比如采集只写、显示只读),那就只配一个方向,另一个方向填 0,这样能省出带宽给别的路径。
AxPROT / AxCACHE:这些是 AXI 协议本身的属性,NoC 会根据这些属性做一些优化。比如 AxCACHE 标记为 bufferable 的写操作,NoC 可以做写合并,提高效率。这部分一般保持默认就行,除非你有特殊需求。
3.2 时钟域与复位:NoC 最容易翻车的地方
NoC 的时钟域配置是个大坑。Versal 的 NoC 有自己的时钟输入,通常是noc_clk,这个时钟的频率决定了 NoC 的工作频率。同时,每个 NMU 和 NSU 又各自挂在自己的时钟域上,NoC 内部要做时钟域转换。
我踩过的坑是这样的:PL 端的 AXI Master 跑在 250 MHz,NoC 跑在 500 MHz,DDR 控制器跑在 400 MHz。配置的时候没注意,结果上板后发现数据偶尔出错。查了半天才发现是 NoC 的时钟约束没写对,导致时序不满足,偶尔采样错位。
正确的做法是:
- 在 Vivado 的 NoC 配置里,明确指定 NoC 的参考时钟频率。
- 在 XDC 约束文件里,给 NoC 相关的时钟加上正确的
create_clock和set_clock_groups。 - 如果 NoC 时钟和 PL 时钟是异步的,一定要加
set_clock_groups -asynchronous,否则工具会按同步时钟去分析,时序报告会很难看。
复位方面,NoC 的复位信号noc_rst_n必须和 NoC 时钟同步释放。我一般用一个复位同步器,把外部复位同步到 NoC 时钟域再送进去。这个细节看起来小,但不做的话,上板后 NoC 可能起不来,或者起来后行为异常。
3.3 仿真环境搭建:别跳过这一步
很多人觉得 NoC 是硬件自动调度的,仿真没必要。这个想法很危险。NoC 的仿真能帮你验证三件事:地址映射对不对、数据通不通、QoS 策略有没有生效。尤其是 QoS,你在配置界面里设了优先级,但实际仲裁行为是不是符合预期,只有仿真能告诉你。
Versal 的 NoC 仿真需要用到Vivado Simulator或者VCS之类的工具,加上 Versal 的仿真模型。具体步骤我后面会详细讲。这里先说一个关键点:NoC 的仿真模型是加密的,你需要正确的 license 才能跑。如果你用的是 Vivado Lab Edition,可能跑不了完整仿真,得用完整版。
另外,仿真的时候建议把 NoC 的performance monitor打开,这样你能看到每条路径的实际带宽和延迟。这个数据比你在配置界面里填的期望值有价值得多,因为它反映的是真实仲裁后的结果。
4. 实操过程与核心环节实现
4.1 从零搭建一个带 NoC 的 Versal 工程
我以一个具体的例子来走一遍完整流程。假设我们要做一个简单的系统:PL 里有一个 AXI Master(模拟数据源),通过 NoC 往 DDR 写数据;PS 通过 NoC 从 DDR 读数据。目标是验证 NoC 通路和 QoS 配置。
第一步:创建工程并选对器件
在 Vivado 里新建工程,器件选 Versal 系列,具体型号根据你手头的板子来。我用的是一块 VCK190 的评估板,器件是xcvc1902-vsva2197-2MP-e-S。选器件的时候注意,不同型号的 NoC 资源不一样,NMU/NSU 的数量和 DDR 控制器的数量都不同。
第二步:添加 NoC IP 并配置
在 Block Design 里添加AXI NoCIP。Vivado 会自动检测你的器件支持多少个 NMU 和 NSU。然后打开 NoC 的配置界面,开始连线。
这里有个技巧:先用 "Auto Assign" 让工具帮你连一版,然后手动改。工具的自动分配通常能保证连通性,但 QoS 和带宽肯定不是你想要的。在自动分配的基础上改,比从零开始连快得多。
第三步:配置 NMU 和 NSU
对于 PL 端的 AXI Master,你需要把它映射到一个 NMU 上。在 NoC 配置界面里,找到对应的 NMU,设置它的 AXI 属性(数据位宽、ID 位宽等)。对于 DDR,找到对应的 NSU,设置它的地址范围。
地址映射是个容易出错的地方。Versal 的地址空间是统一的,NoC 的 NSU 会占用一段地址。你需要在 Address Editor 里确认每个 Master 看到的地址范围是正确的。我一般会画一张地址映射表,把每个 Master、每个 Slave 的起始地址和大小都列出来,配置的时候对着表填。
第四步:连接时钟和复位
NoC 的时钟输入通常来自一个 MMCM 或者 PLL。我一般用一个单独的 MMCM 给 NoC 供时钟,频率根据你的带宽需求来定。比如你要跑 19.2 GB/s 的 DDR 带宽,NoC 的工作频率至少要能支撑这个数据率。
复位方面,用一个proc_sys_reset或者自己写的复位同步器,把外部复位同步到 NoC 时钟域。
第五步:生成输出产物
配置完成后,Validate Design,然后 Generate Output Products。这一步会生成 NoC 的网表文件和约束文件。注意看有没有 critical warning,尤其是关于时钟和地址的。
4.2 仿真验证:写一个能跑的 testbench
NoC 的仿真 testbench 跟普通 AXI 仿真差不多,但有几个特殊的地方。
首先,你需要例化 NoC 的仿真模型。Vivado 在生成输出产物的时候,会同时生成仿真用的模型文件。你需要在 testbench 里把这个模型例化进去,然后把 AXI 接口连上。
其次,NoC 的仿真需要一些初始化时间。NoC 内部有配置寄存器,上电后需要一段时间来初始化。我一般会在 testbench 里等 1000 个时钟周期再开始发激励,避免在 NoC 还没准备好的时候就发数据。
下面是一个简化的 testbench 结构:
module tb_noc; reg noc_clk; reg noc_rst_n; // 时钟生成 initial begin noc_clk = 0; forever #2.5 noc_clk = ~noc_clk; // 200MHz end // 复位 initial begin noc_rst_n = 0; #1000; noc_rst_n = 1; end // 例化 NoC 模型 noc_wrapper u_noc ( .noc_clk(noc_clk), .noc_rst_n(noc_rst_n), // ... AXI 接口 ); // 激励 initial begin // 等待 NoC 初始化 #5000; // 发起 AXI 写事务 axi_write(32'h0000_0000, 32'hDEADBEEF); // 发起 AXI 读事务 axi_read(32'h0000_0000); #10000; $finish; end endmodule这个结构是最基础的。实际项目中,你可能需要写更复杂的激励,比如多个 Master 同时访问、不同 QoS 等级的流量混合等。
仿真跑起来后,重点看几个东西:
- 波形:看 AXI 握手信号是否正常,有没有卡死。
- NoC performance monitor 的输出:看实际带宽和延迟。
- 有没有协议违例:Vivado 的仿真模型会报 AXI 协议错误,如果有,一定要查。
4.3 QoS 调优:从"能跑"到"跑得好"
QoS 调优是 NoC 配置里最考验经验的部分。我的调优流程一般是这样的:
第一步:建立基线
先按默认配置跑一遍仿真,记录每条路径的带宽和延迟。这个基线数据是你后续调优的参照。
第二步:识别瓶颈
看哪条路径的实际带宽远低于期望值,或者延迟远高于预期。这通常意味着这条路径在跟别的路径竞争时吃亏了。
第三步:调整 QoS 等级
把瓶颈路径的 QoS 等级调高,把不重要的路径调低。然后重新仿真,看瓶颈有没有缓解。
第四步:迭代
QoS 调优是个迭代过程,调一次不一定到位。我一般会调 3 到 5 轮,直到所有路径的指标都满足要求。
这里有个经验:不要把所有路径都设成最高优先级。如果所有路径都是最高优先级,那等于没有优先级,仲裁器会退化成轮询,反而失去了 QoS 的意义。正确的做法是分层:实时性最高的设成最高,次高的设成中等,批量传输设成最低。
另外,Versal 的 NoC 支持"Bandwidth Limiting",你可以给某条路径设一个带宽上限,防止它抢占过多资源。这个功能在调试的时候特别有用,比如你怀疑某条路径在抢带宽,可以给它设个上限,看其他路径的表现有没有改善。
5. 常见问题与排查技巧实录
5.1 NoC 配置报错速查表
| 报错信息 | 可能原因 | 解决方法 |
|---|---|---|
| "NoC bandwidth oversubscribed" | 总带宽超了 | 降低某些路径的带宽期望值 |
| "NMU not connected" | 有 NMU 没连到 NSU | 检查连线,确保每个用到的 NMU 都有路径 |
| "Address overlap" | 地址范围重叠 | 检查 Address Editor,调整地址映射 |
| "Clock not specified" | NoC 时钟没约束 | 在 XDC 里加 create_clock |
| "QoS class conflict" | QoS 等级冲突 | 检查是否有多个路径设了相同的最高优先级 |
| "Simulation model not found" | 仿真模型没生成 | 重新 Generate Output Products,确保选了仿真选项 |
这个表是我自己踩坑总结的,不一定全,但覆盖了大部分常见问题。
5.2 上板后 NoC 不工作的排查思路
仿真过了不代表上板就能跑。上板后如果 NoC 不工作,我一般按这个顺序排查:
先看时钟。用 ILA 或者示波器看 NoC 的时钟有没有起来,频率对不对。NoC 的时钟如果没起来,整个 NoC 就是死的。
再看复位。NoC 的复位释放时机对不对,是不是在时钟稳定之后才释放的。如果复位释放太早,NoC 可能初始化失败。
然后看 AXI 握手。用 ILA 抓 AXI 接口的ARVALID、ARREADY、RVALID、RREADY这些信号,看有没有握手成功。如果ARVALID一直高但ARREADY一直低,说明 NSU 没准备好,可能是地址映射错了。
最后看 NoC 的配置寄存器。Versal 的 NoC 有一些状态寄存器,可以通过 PS 读出来,看 NoC 当前的状态。具体寄存器地址参考 Versal 的 TRM(Technical Reference Manual)。
5.3 几个我踩过的坑
坑一:NoC 的带宽单位是 MB/s,不是 Mb/s。我一开始按 Mb/s 算,结果配出来的带宽只有预期的八分之一。后来才发现单位是字节不是比特。这个坑很隐蔽,因为 Vivado 的界面里没写清楚单位。
坑二:NoC 的 QoS 等级是数值越小优先级越高。我一开始以为数值越大优先级越高,结果把最重要的路径设成了最低优先级,调了半天没效果。后来查文档才发现搞反了。
坑三:NoC 的仿真模型需要 license。我在一台只装了 Vivado Lab Edition 的机器上跑仿真,一直报错说找不到模型。后来换了完整版就好了。如果你也遇到类似问题,先确认 license 是否支持 Versal 仿真。
坑四:NoC 的时钟约束不能少。我一开始没写 NoC 的时钟约束,综合能过,但实现的时候时序报告一塌糊涂。后来补上create_clock和set_clock_groups才正常。
坑五:NoC 的地址映射要跟 PS 的地址空间对齐。如果 PS 要访问 PL 里的 IP,PL 的 IP 地址必须在 PS 的地址空间里可见。这个需要在 Address Editor 里配置,不然 PS 访问会报错。
5.4 性能调优的独家技巧
除了前面说的 QoS 调优,还有几个技巧能帮你把 NoC 的性能榨干:
技巧一:用 AXI ID 做乱序传输。AXI4 支持乱序传输,不同的 ID 可以乱序返回。NoC 内部会根据 ID 做重排序。如果你给不同的数据流分配不同的 ID,NoC 可以并行处理,提高吞吐量。
技巧二:合理设置 Outstanding 事务数。NoC 支持多个 outstanding 事务,但数量不是越多越好。太多会占用 NoC 内部的缓冲资源,反而影响其他路径。我一般根据路径的延迟和带宽来算:outstanding = bandwidth × latency / transaction_size。
技巧三:利用 NoC 的 Performance Monitor。Versal 的 NoC 有内置的性能监控器,可以实时统计每条路径的带宽、延迟、事务数。这个数据在上板调试的时候特别有用,能帮你快速定位瓶颈。
技巧四:写合并。对于写操作,如果地址连续,NoC 可以做写合并,把多个小写合并成一个大写,提高效率。要启用这个功能,需要在 AXI 属性里设置AxCACHE的 bufferable 位。
技巧五:读预取。对于读操作,如果 NoC 检测到连续的读地址,可以预取后面的数据。这个功能默认是开的,但如果你的读模式是随机的,预取反而会浪费带宽,这时候可以关掉。
6. 从配置到调优的完整经验总结
6.1 一个可复用的 NoC 配置模板
经过几个项目的积累,我总结了一个 NoC 配置的模板,基本上新项目直接套用,改改参数就行。
第一步:列出所有 Master 和 Slave。把 PL 和 PS 里所有需要走 NoC 的 AXI 接口都列出来,标注每个接口的数据位宽、时钟频率、ID 位宽。
第二步:画数据流图。用箭头标出每个 Master 到每个 Slave 的数据流,标注方向(读/写)和期望带宽。
第三步:分配 NMU 和 NSU。根据数据流图,把 Master 映射到 NMU,Slave 映射到 NSU。注意有些硬核(比如 DDR)的 NSU 是固定的,不能随便改。
第四步:配置带宽和 QoS。按优先级分配带宽,实时性高的给高 QoS,批量传输给低 QoS。
第五步:约束时钟和复位。给 NoC 加时钟约束,确保复位同步。
第六步:仿真验证。跑仿真,看带宽和延迟是否满足要求。
第七步:上板调试。用 ILA 和 Performance Monitor 看实际表现,必要时微调 QoS。
这个模板看起来简单,但每一步都有细节。我建议你第一次做的时候,把每一步的输出都记录下来,形成文档。下次再做类似项目,直接翻文档就行。
6.2 不同应用场景的 NoC 配置差异
不同的应用场景,NoC 的配置重点不一样。我举几个例子:
视频处理:重点是带宽和延迟抖动。显示路径的 QoS 要设到最高,带宽要留足余量。采集路径可以设中等 QoS,因为偶尔丢一帧影响不大。建议开启写合并和读预取。
神经网络推理:重点是带宽利用率。权重和特征图的数据量很大,NoC 的带宽要尽量给足。QoS 方面,权重加载可以设低优先级,因为是一次性的;特征图的读写要设高优先级,因为影响推理速度。
工业控制:重点是延迟确定性。控制环路的延迟必须稳定,不能有大的抖动。QoS 要设到最高,带宽不用很大但要保证。建议关闭写合并和读预取,因为这些功能会增加延迟的不确定性。
数据采集:重点是持续带宽。采集数据是连续的,NoC 要能持续提供带宽。QoS 设中等即可,但带宽要留足。建议开启写合并,提高写效率。
6.3 工具版本和 license 的注意事项
Versal 的 NoC 功能在不同版本的 Vivado 里有些差异。我用过 2021.2、2022.2 和 2023.1,感觉 2022.2 之后的版本对 NoC 的支持比较成熟。如果你用的是比较老的版本,可能会遇到一些奇怪的 bug。
License 方面,Versal 的 NoC 配置需要Vivado Enterprise或者Vivado System Edition的 license。Lab Edition 只能看,不能配。仿真方面,NoC 的仿真模型需要额外的 license,具体看你的 license 文件里有没有包含。
另外,Versal 的 NoC 配置工具在 Vivado 里叫"Versal ACAP NoC",跟普通的 AXI Interconnect 是分开的。别找错了。
6.4 后续可以扩展的方向
这个项目做完了,其实还有很多可以深挖的地方。比如:
多 NoC 域:Versal 支持多个 NoC 域,不同域之间可以隔离。如果你的系统有安全隔离需求,可以用这个功能。
动态 QoS:NoC 的 QoS 等级可以在运行时通过 PS 修改。如果你的系统有不同的工作模式,可以动态调整 QoS。
NoC 与 AI Engine 的协同:Versal 的 AI Engine 也可以通过 NoC 访问 DDR。如果你在做 AI 推理,可以研究一下 AI Engine 和 PL 如何共享 NoC 带宽。
NoC 的性能建模:如果你要在系统设计早期评估 NoC 的性能,可以用 Vivado 的性能建模工具,不用等到 RTL 写完。
这些方向我后续会陆续展开,有兴趣的可以关注。
最后分享一个我个人的习惯:每次配完 NoC,我都会把配置导出成 Tcl 脚本,存到版本控制里。这样下次做类似项目,直接跑脚本就行,不用从头配。而且出了问题,也能快速回滚到之前的版本。这个习惯帮我省了很多时间,推荐你也试试。