news 2026/10/13 1:26:31

ZCU102多核工作实战:用Vitis实现SMP、AMP与PL多CU并行

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ZCU102多核工作实战:用Vitis实现SMP、AMP与PL多CU并行

刚把手头这个 ZCU102 的 Vitis 多核项目收尾,趁热把过程整理出来分享给后来人。项目名就叫“vitis_zcu102_3_Vitis 实现多核工作”,听起来宽泛,实际落地时你会发现“多核”这个词在 ZCU102 上能拆出好几层意思:A53 四核在 Linux 下的对称多核(SMP)、A53 与 R5 实时核心之间的非对称多核(AMP),以及 FPGA 逻辑里多个计算单元(Compute Unit)的并行流水线。这篇就按这三种“多核”逐一展开,记录我用 Vitis 实现它们时的设计思路、关键步骤和踩过的坑。适合刚上手 Vitis、想在这块板上玩转多核异构开发的同学参考。

1. 先把“多核”这件事拆清楚

1.1 三种常见的“多核”口径

ZCU102 不是普通单片机,它是带完整处理子系统和可编程逻辑的异构平台。芯片内部已经有四核 Cortex-A53、双核 Cortex-R5,还有 GPU 和大量可编程逻辑资源。用 Vitis 干活时,“多核工作”通常对应以下三个方向:

第一,A53 四核的 SMP 并行。A53 四核运行同一个 Linux 系统,由内核调度器把线程分发到不同的 CPU 核心上。应用层用 OpenMP、pthread 或进程多开,都能看到并行效果。这是最简单的多核,目的就是把四个硬核用满。

第二,A53 和 R5 之间的 AMP 异构。A53 主核跑 Linux,负责管理和复杂逻辑;R5 从核跑裸机或 RTOS,负责实时控制、快速响应。两者通过共享内存和消息机制通信。Vitis 里可以分别为主核和从核生成独立工程,再通过 OpenAMP 建立通信通道。

第三,FPGA 逻辑里的多计算单元并行。在 Vitis 里,你写一个 kernel 函数,它可以被综合成硬件加速单元。同一个 kernel 可以例化多个实例,每个实例叫一个 Compute Unit(CU)。多个 CU 可以同时工作,相当于在芯片上“造”出多个软硬件协同的计算核心。

这三层多核可以独立使用,也可以叠加。我的项目“vitis_zcu102_3_Vitis”实际就是按这个顺序逐层推进的:先验证 Linux 四核,再拉通 R5 通信,最后让多个 PL 加速单元并行起来。

1.2 为什么这块板适合做多核验证

ZCU102 的处理系统(PS)和可编程逻辑(PL)之间带宽充足,片内互联协议成熟,尤其适合验证异构多核方案。它提供了两个启动阶段:前期可以在 JTAG 下快速跑裸机测试,后期可以把 Linux 镜像和 R5 固件一起放到 SD 卡启动,整个流程非常接近工业产品的工作方式。

更重要的是,Vitis 工具链把“写 C/C++ 函数 → 综合成 RTL → 映射到 PL”这个流程自动化了,不需要手写 Verilog 就能做出硬件加速单元。同时 Vitis 也支持多核 CPU 工程管理,A53 上的可执行程序、R5 上的裸机程序都能在一个工作空间里维护。对于想同时兼顾软、硬件设计的工程师来说,这套组合很适合做多核架构的原型验证。

1.3 三种多核方案的快速对比

方案参与的计算单元软件形态典型应用通信方式
SMP4× Cortex-A53单个 Linux 系统通用计算、网络服务、批量数据处理共享内存 + 调度器
AMPA53 + R5Linux + 裸机/RTOS实时控制、网关、电机控制RPMSG / 共享内存
PL 多CUA53 + 多个硬件CULinux + OpenCL驱动图像处理、加速计算、流数据处理AXI / 数据搬运引擎

选择哪种方案,取决于你的实时性要求和计算形态。实时性要求到微秒级的,R5 是首选;纯吞吐量需求大的,PL 多 CU 压满;常规并行计算,A53 四核 SMP 已经够用。

2. 动手前的环境准备

2.1 硬件连接与启动模式

开发 ZCU102 时,我习惯把 SD 卡作为主要启动介质,JTAG 只用来调试早期裸机代码。板卡上的拨码开关要拨到 SD 启动档位。这个细节不处理好,后续你会发现 Linux 起不来,还以为是镜像坏了。

串口是必须的。ZCU102 板载 USB 转串口,插上数据线后,在主机上可以看到两个设备节点:一个用于调试串口(常为 /dev/ttyUSB1),一个用于 Linux 控制台(常为 /dev/ttyUSB0)。如果识别不到,多半是驱动没装或者线材只支持充电。我一般用 minicom 或 screen 连接,波特率 115200,8N1。

上电前的检查顺序建议是:SD 卡镜像是否插入,拨码开关是否在 SD 模式,电源适配器功率是否足够。别小看供电,PL 大规模综合后的 bit 流下载到板卡后功耗会上一个台阶,供电不足会直接导致 PS 侧复位。

2.2 Vitis 工具链与平台文件

Vitis 是一套集成开发环境,重点在于把嵌入式软件和 FPGA 加速统一管理。安装时我选了包含 Vitis IDE、Vivado 和 Petalinux 的完整套件,避免后续做 Linux 镜像时再补装东西。

工程组织上,要先有一个“平台(Platform)”,平台文件来自 Vivado 导出的 XSA。简单理解,XSA 描述了这个硬件平台有哪些外设、哪些时钟、哪些内存映射关系。Vitis 社区版和标准版在这个地方没有本质差异,但后续做 PL kernel 需要 license,如果你用的 license 只包含嵌入式开发部分,opencl 流程可能跑不通,这点在创建平台前最好确认。

读取平台信息最直接的方式是在 Vitis 环境里打开一个平台工程,查看“Platform → Hardware Specification”。里面会列出地址映射,比如 DDR 基址、GIC 中断控制器、UART 等。后续做 R5 通信时,内存区间的设置就要参考这些地址。

2.3 镜像与 BSP 的准备

如果只想验证 A53 SMP,直接用预编译的 Linux 镜像即可。但要加 R5 固件或 PL kernel,就得自己定制镜像。用 Petalinux 做根文件系统最省心,它能把设备树、Linux 内核和启动脚本整合到一个 boot.bin 里。

我准备的镜像包含:

  • boot.bin:由 FSBL、PMUFW、设备树、ATF 和 u-boot 组成。
  • image.ub:Linux 内核 + 根文件系统。
  • R5 固件文件和 kernel 的 xclbin 文件,放在 SD 卡指定目录。

这些文件都能通过 Petalinux 的打包脚本生成。最开始的几步过于繁琐,但之后每次改动只需重新 make,效率还是很高的。

3. 第一层多核:把 A53 四核跑满

3.1 检查四核是否真正在线

Linux 启动后,第一步不是写代码,而是确认四个核都工作。登录系统执行:

cat /proc/cpuinfo

正常情况下会看到四个 processor 条目,从 0 到 3。再查看在线状态:

cat /sys/devices/system/cpu/online

输出“0-3”代表四个核全部在线。有时候设备树或启动参数有问题,会出现只有 0 这一个核在线,后面第 6 章会详细说排查方式。

如果你想确认中断回调到不同核上,可以观察:

cat /proc/interrupts

每行最后一列会显示每个 CPU 上中断触发的次数。本地定时器、网卡中断会分布到不同核上,这本身就是 SMP 在工作。

3.2 用 OpenMP 写一个并行程序

A53 四核跑 SMP,最常用的加速方式就是 OpenMP。OpenMP 通过编译器指令自动把循环分配到多个线程上,不需要手动 pthread_create。

我这里用一个蒙特卡洛计算 π 的例子来验证加速比:

#include <stdio.h> #include <stdlib.h> #include <omp.h> int main(void) { long long N = 1 << 24; long long count = 0; unsigned int seed = 12345; #pragma omp parallel for reduction(+:count) for (long long i = 0; i < N; i++) { double x = (double)rand_r(&seed) / RAND_MAX; double y = (double)rand_r(&seed) / RAND_MAX; if (x * x + y * y <= 1.0) count++; } printf("pi = %f\n", 4.0 * count / N); return 0; }

注意用rand_r而不是rand,后者在多线程下结果不确定且锁竞争严重。编译时加-fopenmp:

gcc -O2 -fopenmp pi.c -o pi

设置线程数:

export OMP_NUM_THREADS=4 ./pi

也可以强制把线程绑定到指定核:

export GOMP_CPU_AFFINITY="0-3"

绑定后线程迁移变少,cache 命中率更高。

3.3 观察加速比和瓶颈

在我的环境下,1 线程大约 0.9 秒,4 线程大约 0.25 秒,加速比约 3.6 倍。没有到 4 倍,原因是随机数生成、内存访问和数据规约在竞争同一个缓存行时仍有一定开销。如果换成计算密度更高的浮点运算,接近 4 倍并不难。

这里想特别说一句:加速比不达标,不一定是“多核没生效”,很可能瓶颈在内存带宽。A53 的 L2 cache 是共享的,内存访问竞争会让并行度收益下降。遇到这种问题,先用perf stat看 cache miss 率,如果很高,就优化数据局部性,而不是盲目加核。

4. 第二层多核:A53 主核与 R5 从核的 AMP 通信

4.1 为什么需要 R5 这样的独立核心

Linux 擅长管理复杂资源,但它不是实时操作系统。任务切换、中断响应都可能造成不确定延迟。R5 是低延迟实时核,适合做电机控制、协议栈解析、传感器采样等硬实时任务。让 A53 跑上层业务流程,R5 跑底层实时循环,这就是典型的 AMP 架构。

AMP 的关键在于两个核各自独立运行,但通过硬件机制共享数据。最常用的通道是 OpenAMP + RPMSG:主核 Linux 侧加载一个 remoteproc 驱动,R5 侧跑一个裸机或 FreeRTOS 程序,两个核通过共享内存和 M 信箱传递消息。

4.2 创建 R5 裸机应用

在 Vitis IDE 里,可以为同一个 XSA 创建一个 R5 处理器上的裸机工程。注意 R5 有核心 0 和核心 1,你可以单独使用 R5_0,也可以让两个 R5 都工作。我项目里只用了一个 R5 核心做通信测试,另一个留给后续其他外设。

创建步骤大致是:

  1. 在 Vitis 中新建“Platform Project”,选择 XSA 并勾选 R5 处理器。
  2. 为 R5 创建“Application Project”,语言选 C,system template 选“OpenAMP OpenAMP Echo Test”或“Empty Application”。
  3. 编译生成 r5.elf 固件。

有一点值得注意:R5 的默认启动地址、TCROM 配置都取决于 Vivado 硬件设计。如果你的 R5 固件需要访问 DDR 里的共享缓冲区,必须在 Vivado 里配置好内存地址映射,并在 R5 链接脚本中把对应 DDR 地址段设置为可读写。Vitis 自动生成的链接脚本通常只把 TCM 作为 RAM,需要在 lscript.ld 中添加 DDR 区域。

#include <metal/sys.h> #include <metal/device.h> #include <metal/io.h> #include "baremetal.h" volatile int shared_var; int main(void) { struct metal_init_params init_params = METAL_INIT_DEFAULTS; metal_init(&init_params); // 这里初始化通信能力 while (1) { // 处理收到的消息,并回发确认 } }

R5 程序里最核心的是接收主核发来的命令,然后通过共享内存把结果写回。实际代码可以更简单一些,直接用裸机环境下的 OpenAMP 库。

4.3 Linux 端加载 R5 固件

在 Linux 侧,设备树中要添加 remoteproc 节点,告知系统 R5 固件路径、使用的内存区域以及中断信息。Petalinux 默认带 OpenAMP 相关内核配置,但设备树需要手动确认。

一个典型节点描述类似:

&r5f0 { firmware-name = "r5.elf"; memory-region = <&rproc_shared_mem>; vdev0 = <&vdev0>; }; vdev0: vdev@10000000 { compatible = "openamp,virtio-ipc"; reg = <0x10000000 0x1000>; interrupts = <GIC_SPI 29 IRQ_TYPE_LEVEL_HIGH>; };

加载固件的命令:

mkdir -p /lib/firmware cp /run/media/mmcblk0p1/r5.elf /lib/firmware/ echo r5f0 > /sys/bus/platform/drivers/remoteproc-platform/probe

或者采用更常见的方法,让 remoteproc 在平台驱动初始化时自动加载。

4.4 验证消息收发

通信测试程序里,A53 侧创建两个通信端点,一个发消息,一个收 R5 的回复。消息内容可以是一个简单字符串,也可以是一包带 CRC 的传感器数据。跑通后串口上会周期性看到主核和 R5 各自打印“send/recv”。

常见坑在于共享内存的地址对齐。R5 的访问如果不经过 cache,或者 A53 侧对同一地址有 cache 缓存,就会看到数据不一致。解决办法是在设备树里把共享内存区域标记为no-map,并让两端程序对该区域使用metal_io的 uncached 访问。我在第一次测试时就遇到主核发出去的数据 R5 读不到,最后在 R5 侧把共享缓冲区映射属性改为 Device 类型才解决。

5. 第三层多核:PL 里的多计算单元并行

5.1 Kernel 与 Compute Unit 的区别

Vitis 加速流程里,你写的 C 函数称为 kernel。一个 kernel 可以实例化多个硬件单元,每个硬件单元就是一个 CU。Vitis 编译时通过nk参数控制实例数量。比如:

v++ --compile --kernel myaccel --num-compute-unit 2 ...

这样会生成两个物理上完全独立的 myaccel CU。两个 CU 可以同时接收不同数据,计算完成后分别写回 DDR。这种多核是真正意义上的“空间并行”,没有时钟中断和上下文切换,非常适合高吞吐任务。

5.2 在主机代码中调度多个 CU

从 A53 侧看,每个 CU 都是一个 OpenCL 计算设备。主机程序获取两个 kernel 句柄,然后把数据分成两半,分发给两个 CU 执行。

代码逻辑示意:

cl::Kernel kernel1(program, "myaccel"); cl::Kernel kernel2(program, "myaccel"); cl::CommandQueue q1(context, device, CL_QUEUE_PROFILING_ENABLE); cl::CommandQueue q2(context, device, CL_QUEUE_PROFILING_ENABLE); // 将输入 buffer 分块 err = q1.enqueueTask(kernel1); err = q2.enqueueTask(kernel2); q1.finish(); q2.finish();

每个 CU 独享一部分数据,避免了锁和临界区。这里要注意的是,Vitis 的 OpenCL 实现中,同一个 kernel 多次 enqueueTask 不一定并行,需要靠多个 queue 或者cl::NDRange全局并发。稳妥的做法是创建两个 kernel 句柄分别对应两个 CU。

5.3 多 CU 设计中的数据切分

数据切分是最有价值的优化点。比如一个图像二值化 kernel,输入 1920×1080 图像,两个 CU 各自处理上半帧和下半帧,就能让两个运算单元都满负荷运行。

切分时要注意对齐。DDR 带宽最优访问通常是 64 字节对齐,因此切分宽度最好按行划分,而不是按像素块划分。如果按块划分,CU 之间会产生行边界处理,得不偿失。

如果任务本身有顺序依赖,多 CU 就不好直接并行。这时可以考虑流水线模式:CU0 做完第一步,CU1 做第二步,Vitis 的#pragma HLS dataflow可以在 kernel 内部实现流水,但跨 kernel 的流水需要主机代码配合多队列异步操作。我建议先做独立任务并行,等摸熟性能模型后再上流水。

5.4 与 A53 多核的协同方式

PL 多 CU 再快,也需要有人把数据搬进搬出。这时 A53 四核就有用武之地了——它可以在用户态用多个线程同时调用不同的 OpenCL queue,把搬运任务分散到四个核上。

我的做法是开 2 个主机线程,每个线程负责一个 CU 的 buffer 搬运和 enqueueTask。CPU 侧用pthread_setaffinity_np把线程绑定到 A53 核 0 和核 1,核 2 和核 3 留给操作系统和网络。通过xrt.ini开启 profiling 后,可以看到每个 CU 的执行时间以及 PCIe/AXI 的搬运带宽,这样能判断 CPU 侧是否成为瓶颈。

如果实测出来 CU 计算时间远小于搬运时间,那就别再把时间花在优化 CU 上,而是想怎么让 A53 侧的数据搬运更快,比如用多路cl::Buffer预分配并做双缓冲。

6. 常见问题与排查实录

6.1 Linux 启动后只有单核在线

症状是/proc/cpuinfo只有一个 processor。常见原因有四个:

第一,启动参数中带了maxcpus=1。检查 bootargs,去掉这个参数即可。

第二,设备树中缺少 cpu-release-addr。A53 启动时,从核需要在上电后读取一个特定地址,等待主核写入释放地址。Petalinux 默认生成的设备树会包含 cpu-release-addr,但如果你是手工维护设备树,容易漏掉。

第三,FSBL 或 ATF 版本与 Linux 内核不匹配。老版本 ATF 可能只引导了主核,需要升级 ATF。

第四,你可能用了 git 下支持的 defconfig,但缺少 SMP 选项。检查内核配置:

grep CONFIG_SMP /proc/config.gz

如果不是y,重新编译内核。

6.2 R5 固件加载失败

典型错误是 Linux 日志中出现Failed to load firmware。原因通常如下:

  • SD 卡里没有把固件拷贝到/lib/firmware,或者文件名不一致。
  • R5 的内存区域与 Linux 使用的内存重叠。R5 固件必须运行在 Linux 未管理的内存里,设备树的 memory-region 需要标注no-map。
  • R5 端程序访问了未初始化 clock。在 PL 或 PS 时钟配置有问题时,R5 会卡在启动阶段。

排查时先确认 R5 工程用 JTAG 模式能不能单独跑通。如果 JTAG 下正常,SD 启动异常,那问题基本是设备树或启动时序。

6.3 Vitis 多 CU 没跑出预期性能

性能没提升,先看 profiling 报告。Vitis 生成 profile summary 之后,重点看这几个字段:

指标正常值异常表现
CU Utilization接近 100%CU 等待数据,利用率低
DDR Bandwidth不超过上限带宽打满,计算空闲
Kernel Execution Time各 CU 均衡某个 CU 时间特别长
Data Transfer与计算重叠搬运和计算串行

如果两个 CU 串行执行,大概率是 enqueue 顺序和依赖问题。检查同一个 queue 内是否连续调用了两个 kernel,改成两个 queue 各调一个。还有如果只有单一cl::Buffer被两个 CU 读,数据仲裁会让它们失去并行意义。

6.4 CPU 频繁迁移导致性能抖动

SMP 下操作系统会主动迁移线程,但迁移开销不可忽略。当多核程序里有共享状态时,A53 的 cache 在核间迁移后会大量失效,导致性能忽高忽低。

可以采用:

taskset -c 0,1 ./multi_thread_program

或 OpenMP 亲和绑定:

export OMP_PROC_BIND=true export OMP_PLACES=cores

这样线程固定在物理核心上,抖动会明显好转。

7. 多核优化的核心经验

7.1 区分“并行”和“核数”

多核不是越多一定越快。只有任务本身可以被切分、且切分后没有太多共享依赖,性能才会线性增长。我在项目里做图像处理时,四个 A53 核分别处理四个区域,效果接近线性,因为区域之间没有依赖。但做数据统计时,因为共享同一个累加变量,无论核数加到多少,性能都被频繁锁竞争拖垮。

所以先分析任务的“可并行度”,再决定用哪一层多核。判断标准很简单:把数据拆成 4 份独立处理,最后有没有一个必须共享的全局状态?如果有,就必须用局部归约来规避。

7.2 让计算和搬运重叠

多核系统里,最耽误时间的往往是等待数据。在 A53 和 PL CU 之间,DDR 搬运经常占用整个执行时间的一半以上。我后来把搬运改成双缓冲:线程 A 在搬运下一批数据的同时,CU 正在计算当前这一批。这样隐藏了搬运延迟,整体吞吐量几乎翻倍。

OpenCL 里可以用事件回调或者enqueueMigrateMemObjects+ completion handler 实现。

7.3 别忽略中断和优先级

R5 与 A53 通信时,R5 的中断优先级和 A53 的中断优先级需要统筹布局。如果 A53 侧频繁打扰 R5,会直接破坏实时性。实践中可以把 A53 到 R5 的消息做成队列写入共享内存,通过 M 信箱发一个简单的“有新消息”中断,而 R5 根据自己的节拍处理队列,不依赖于中断频率。

7.4 从最简单的点数开始

整个多核系统很复杂,但拆开看就是一个个小部件:CPU 核、R5 核、CU、通信通道。我强烈建议先做最小验证,再逐步叠加。先让 A53 四核跑起来,再把 R5 通信跑通,最后才上 PL 多 CU。每一步都留下可返回的 checkpoint,不然出了问题,你会发现根因不在新代码,而在三天前的一个地址映射错误上。

个人体会是,多核开发很容易被“一切正常”的假象遮蔽。每个核都很正常,但它们并行后是否真的有收益,必须用真实数据和 profiling 报告说话。工具提供的报告虽然繁琐,却是唯一能定位瓶颈的入口。搞完这一轮,我最深的感受还是:在 ZCU102 这种异构平台上,设计一开始就要考虑“数据怎么流动”“谁在等待谁”,而不是单纯追求核数量。先把一条链路打通,再把核并行起来,最终的效果会比同时铺开四个方向稳妥得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 1:26:10

PerfDog实战:移动端游戏CPU性能分析与优化指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:24:58

D-LinkNet:面向高分辨率卫星影像的轻量级语义分割方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:24:35

Spring AI Alibaba停更不用慌:Java开发者迁移路线与隔离架构指南

一条消息在开发者群里传开&#xff1a;Spring AI Alibaba已停更了。说实话&#xff0c;我在看到仓库停更公告的时候也愣了一下&#xff0c;毕竟身边确实有几个项目把模型调用都挂在它上面。但等我冷静下来&#xff0c;把依赖关系、问题边界、替代方案重新捋过一遍之后&#xff…

作者头像 李华
网站建设 2026/10/13 1:24:31

Java企业级Agent Harness平台设计与落地实践

讲真&#xff0c;这两年聊 Agent 的人特别多&#xff0c;但真正能在企业里落地的很少。我在团队里做了个纯 Java 的 Agent Harness 平台&#xff0c;代号 BizBuddy。所谓 Harness&#xff0c;不是说它又一个聊天机器人框&#xff0c;也不是把某个 Python 编排框架搬到 Java 上来…

作者头像 李华
网站建设 2026/10/13 1:24:24

Spring Cloud Gateway 按 URL 差异化超时配置实战与踩坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 1:23:45

云解析DNS核心概念与生产配置排查实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华