简介:本资源面向FPGA嵌入式开发工程师与高校相关专业学习者,聚焦XCZU系列MPSoC(含XCZU2CG、XCZU2EG、XCZU4EV)在VITIS平台下的双核AMP驱动实现,解决异构多核任务划分、核间通信与协同调度等工程问题,适合具备一定Zynq UltraScale+基础的开发者进阶实践。压缩包共约2000个文件,整体70.39MB,以C源码与头文件为主体,辅以汇编、链接脚本、Makefile、Tcl脚本、Vivado工程文件及比特流,覆盖驱动开发、系统配置与硬件描述等环节。目前已有108人学习下载。资源完整呈现双核AMP从系统配置、驱动编写、任务调度到性能优化与调试验证的实现脉络,读者可据此理解中断管理与数据共享协议的设计思路,并借助VITIS工具链完成编译、调试与验证,为自动驾驶、图像处理、网络通信等低延迟并行场景提供可复用的参考方案。
1. 从一颗 XCZU2EG 说起:双核 AMP 到底解决什么问题
手里有一块 Zynq UltraScale+ 的板子,芯片型号是 XCZU2EG,属于 MPSoC 家族里比较精简的一档,但该有的东西一样不少:四核 Cortex-A53 的应用处理单元、双核 Cortex-R5 的实时处理单元,外加一颗 FPGA 逻辑。很多人拿到它第一反应是跑个 Linux 就完事,可真到项目里你会发现,纯 Linux 干实时活儿会抖,纯裸机又扛不住网络和文件系统。这时候 AMP(Asymmetric Multiprocessing,非对称多处理)就成了绕不开的方案:让一个核跑 Linux 管人机交互和存储,另一个核跑裸机或 RTOS 管硬实时,两边通过共享内存和中断通信。
这个标题里的关键词——FPGA、MPSoC、XCZU2EG、AMP、VITIS——串起来就是一条完整的落地链路:在 Vitis 里把两个 CPU 核拆开,各自编译各自的镜像,最后打包成 BOOT.BIN 启动。它适合谁?适合已经能把 Zynq MPSoC 跑起来、但被实时性和系统复杂度夹在中间的嵌入式工程师。这篇笔记不讲空泛概念,直接按我实际调通的顺序,把工程怎么建、内存怎么分、中断怎么通、坑在哪,一条条摆出来。
2. 双核 AMP 的硬件底座与软件分工怎么定
2.1 为什么选 A53 加 R5 而不是两个 A53
XCZU2EG 的 APU 里有 4 个 A53,RPU 里有 2 个 R5。做 AMP 时,最常见的分工是 A53 跑 Linux,R5 跑裸机或 FreeRTOS。原因很直接:R5 是双核锁步或分离模式,中断延迟确定,没有 MMU 带来的页表抖动,适合电机控制、协议栈、采样这类硬实时任务;A53 有 MMU、有 Cache 一致性、能跑完整 Linux,适合网络、文件、UI。
如果你非要用两个 A53 做 AMP,也可以,但两个核共享 L2 Cache,内存属性配置稍有不慎就是玄学级的数据不一致。我一般会优先把实时侧放到 R5 上,A53 侧只留一个核给 Linux,另外几个核可以后续用 CPU 热插拔或 remoteproc 再拉起来。这里要提醒一句:XCZU2EG 的 R5 默认是 lockstep 模式,两个 R5 核执行同一份代码做冗余校验,如果你要的是两个独立任务,得在 Vivado 里把 RPU 配成 split 模式,这个配置在 Block Design 里改,改完要重新导出 XSA。
2.2 内存划分:DDR 里切出各自的领地
AMP 最核心的一步是内存划分。Linux 默认会接管它看到的所有 DDR,如果不在设备树里把 R5 要用的那段 reserve 掉,Linux 一启动就可能把数据写进去,R5 侧读到的全是脏数据。我一般按下面的方式切:
| 区域 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Linux 系统内存 | 0x00000000 | 1.5G | A53 Linux 使用 |
| 共享内存 | 0x60000000 | 16M | 双核通信缓冲区 |
| R5 代码与数据 | 0x70000000 | 128M | R5 裸机运行区 |
| R5 保留区 | 0x78000000 | 128M | 中断向量、堆栈 |
地址不是死的,要根据你板子实际 DDR 容量调整。XCZU2EG 常见搭配 2GB 或 4GB DDR,切的时候留足 Linux 侧,否则根文件系统都挂不起来。设备树里用reserved-memory节点声明,内核启动参数里也可以用mem=限制 Linux 可见范围,两种方式选一种即可,别重复限制导致地址对不上。
2.3 Vitis 工程结构:一个平台加两个应用
在 Vitis 里,正确的组织方式是先建 Platform Project,导入 Vivado 导出的 XSA,然后在平台上建两个 Application Project:一个 Linux 应用(其实主要是生成设备树和 BOOT.BIN 的组件),一个 R5 裸机应用。很多人翻车在把两个应用建在同一个 domain 里,结果编译出来的 ELF 互相覆盖。
平台工程里要确认 FSBL 已经生成,FSBL 负责在启动阶段加载 PMU 固件、配置时钟和 DDR,然后把控制权交给 BootROM 指定的镜像。R5 应用的 ELF 会被打包进 BOOT.BIN,由 FSBL 在启动时搬到对应地址执行。Linux 侧则通过设备树里的remoteproc或直接reserved-memory加uio来访问共享内存。Vitis 2022 之后的版本对 MPSoC 的 AMP 支持比较稳,建议用 2022.2 或更新,老版本在生成 BOOT.BIN 时对 R5 split 模式的处理有 bug,会莫名把两个核都拉起来跑同一份代码。
3. 在 Vitis 里把双核 AMP 工程跑起来的最小步骤
3.1 创建平台工程并导入 XSA
打开 Vitis,File → New → Platform Project,输入名字比如zu2eg_amp_platform,在硬件规格里选你 Vivado 导出的 XSA。导入后 Vitis 会自动识别 PS 配置,包括 DDR 型号、RPU 模式、外设使能情况。这一步如果 XSA 里 RPU 是 lockstep,平台里只会看到一个 R5 核;要 split 就得回 Vivado 改。
平台建好后,右键平台工程 → Build,等它生成 BSP。BSP 里包含了 FSBL、PMU 固件和各个外设的驱动。确认zynqmp_fsbl出现在工程列表里,没有的话检查 XSA 是否包含 FSBL 配置。
3.2 建 R5 裸机应用并指定运行核
/* r5_main.c - R5 侧裸机主程序,负责实时任务和共享内存读写 */ #include "xil_printf.h" #include "xil_io.h" #include "xparameters.h" #define SHM_BASE 0x60000000UL /* 共享内存起始地址,与 Linux 侧约定一致 */ #define SHM_SIZE 0x01000000UL /* 16MB 共享区 */ #define MAGIC 0x414D5001UL /* 握手标志,Linux 侧看到这个值才认为 R5 就绪 */ volatile uint32_t *shm = (volatile uint32_t *)SHM_BASE; int main(void) { /* 写入握手标志,告诉 Linux 侧 R5 已经启动 */ shm[0] = MAGIC; /* 初始化一个计数器,模拟实时任务产生的数据 */ shm[1] = 0; xil_printf("R5 AMP core started, shm base = 0x%08X\r\n", SHM_BASE); while (1) { /* 每次循环递增计数器,Linux 侧轮询读取 */ shm[1]++; /* 简单延时,实际项目里这里换成定时器或中断驱动的任务 */ for (volatile int i = 0; i < 100000; i++); } return 0; }这段代码的逻辑很直白:R5 启动后先往共享内存第一个字写一个 magic number,Linux 侧通过轮询这个值判断 R5 是否已经跑起来。第二个字做自增,模拟实时数据。参数上,SHM_BASE必须和 Linux 设备树里 reserved-memory 的地址完全一致,差一个字节都会导致两边看到不同的物理页。MAGIC的值随便定,只要两边约定好即可,但建议用有辨识度的常量,方便调试时在内存窗口里一眼认出来。
建 Application Project 时,在 Target 里选psu_cortexr5_0,OS 选standalone。如果 RPU 是 split 模式,你会看到psu_cortexr5_0和psu_cortexr5_1两个选项,选你要跑的那个。编译后生成 ELF,记下路径,后面打包 BOOT.BIN 要用。
3.3 配置 Linux 侧设备树保留内存
Linux 侧的关键是设备树。在system-user.dtsi或你的板级 dts 里加:
/* 保留共享内存和 R5 运行区,防止 Linux 内存管理误用 */ / { reserved-memory { #address-cells = <2>; #size-cells = <2>; ranges; r5_shm: r5_shm@60000000 { reg = <0x0 0x60000000 0x0 0x01000000>; no-map; }; r5_code: r5_code@70000000 { reg = <0x0 0x70000000 0x0 0x08000000>; no-map; }; }; };no-map表示这段内存不建立页表映射,Linux 完全不会碰它。reg里的地址和大小要和 R5 侧代码里用的完全一致。改完设备树重新编译system.dtb,替换到启动分区。如果你用的是 PetaLinux,把这段加到system-user.dtsi后重新petalinux-build即可。
3.4 打包 BOOT.BIN 并验证双核启动
在 Vitis 里新建一个Create Boot Image配置,按顺序加入:FSBL ELF、PMU 固件、R5 应用 ELF、bitstream(如果 PL 有逻辑)、U-Boot ELF。顺序不能乱,FSBL 必须第一个。R5 应用的加载地址填你链接脚本里指定的地址,通常是 0x70000000。
生成 BOOT.BIN 后放到 SD 卡启动分区,上电。串口会先打印 FSBL 日志,然后看到 R5 的xil_printf输出,最后 Linux 启动。Linux 起来后,用devmem读共享内存验证:
# 读取共享内存第一个字,应该看到 0x414D5001 devmem 0x60000000 32 # 读取第二个字,应该看到不断变化的计数值 devmem 0x60000004 32如果第一个字读出来是 0 或乱码,说明 R5 没跑起来或者地址对不上。先检查 BOOT.BIN 里 R5 ELF 有没有正确加入,再检查 R5 链接脚本的起始地址是否和 BOOT.BIN 配置里一致。这一步是 AMP 调试里最容易卡住的地方,血泪经验是:串口日志比任何调试器都好使,R5 侧一定要留打印。
4. 共享内存与中断通信的避坑清单
4.1 现象:Linux 读到共享内存全是 0xFF
原因:R5 还没启动,或者 R5 的 ELF 没有被 FSBL 正确加载。XCZU2EG 的 FSBL 在加载 R5 镜像时,如果 partition 的load address和 ELF 链接地址不一致,会加载到错误位置,R5 取指失败直接挂死。
解决:在 Vitis 的 Boot Image 配置里,双击 R5 的 partition,确认Destination CPU选的是R5-0或R5-1,Load Address和 R5 工程 linker script 里的ORIGIN一致。R5 裸机工程的链接脚本一般在src/lscript.ld,打开看MEMORY段里的psu_r5_ddr_0起始地址。
4.2 现象:R5 跑着跑着数据就不更新了
原因:R5 侧没有清 Cache,或者共享内存属性配成了 cacheable。R5 的 Cortex-R5 有 Cache,如果共享内存区域被配置为 write-back cacheable,R5 写的数据可能一直在 Cache 里,Linux 侧读 DDR 读到的是旧值。
解决:在 R5 侧把共享内存区域配置为 non-cacheable,或者每次写完后执行 Cache 清理。简单做法是在xparameters.h或链接脚本里把共享内存段标记为NOLOAD且属性设为non-cacheable。更稳妥的是用 Xil_DCacheFlushRange 在写完后刷 Cache:
/* 写完共享内存后刷 Cache,确保数据落到 DDR */ Xil_DCacheFlushRange((INTPTR)shm, SHM_SIZE);Linux 侧读之前也可以执行dc civac类操作,但一般 Linux 侧用devmem读的是物理地址,不经过 Cache,所以重点在 R5 侧。
4.3 现象:中断收不到,两边像聋子
原因:AMP 场景下中断路由没配对。R5 和 A53 之间的软中断通常走 IPI(Inter-Processor Interrupt),但 IPI 的配置在 FSBL 和 PMU 固件里,如果 PMU 固件版本和 Vitis 版本不匹配,IPI 通道可能没使能。
解决:先确认 PMU 固件是从当前 Vitis 版本的平台工程里生成的,不要混用老版本。然后在 R5 侧用XScuGic配置 IPI 中断,A53 侧 Linux 用irqchip驱动触发。如果只是简单通信,我建议先用共享内存加轮询,跑通后再换中断,这样能把问题域缩小。
4.4 现象:Vitis 编译报 collect2.exe 错误
原因:这是 Windows 下 Vitis 常见问题,通常是路径里有中文或空格,或者杀毒软件锁住了临时文件。也有可能是 R5 工程的链接脚本里内存段溢出,链接器找不到足够空间。
解决:把工程路径改成全英文无空格,关闭杀毒软件实时防护,清理工程后重新 Build。如果是内存溢出,检查lscript.ld里psu_r5_ddr_0的LENGTH是否够放你的代码和数据,不够就调大,但别超过 reserved-memory 划定的范围。
4.5 现象:Linux 启动后 R5 被复位
原因:Linux 的 remoteproc 或 watchdog 驱动在启动时可能把 R5 当成待管理的远程核,自动复位。XCZU2EG 的默认设备树里如果使能了zynqmp_r5_remoteproc,它会去控制 R5。
解决:如果不用 remoteproc 管理,就在设备树里把对应节点 status 改成disabled。或者反过来,用 remoteproc 来加载 R5 固件,这样就不需要把 R5 ELF 打包进 BOOT.BIN,而是由 Linux 在启动后加载。两种方式选一种,别混用。
5. 让 AMP 真正可用的几个进阶技巧
5.1 用 IPI 中断替代轮询降低延迟
轮询共享内存简单,但 CPU 占用高、延迟大。跑通之后建议换成 IPI。R5 侧配置XScuGic的 SGI(Software Generated Interrupt),A53 侧 Linux 用irq_sgi触发。具体做法是在 R5 的 GIC 初始化里使能 SGI 中断号,然后在 A53 侧通过devmem写 GIC 的 SGI 寄存器。延迟能从毫秒级降到微秒级。注意 SGI 中断号两边要约定好,别和系统已有中断冲突。
5.2 共享内存加环形缓冲区避免数据覆盖
直接读写同一个地址,生产者和消费者速度不匹配时必然丢数据。我一般会在共享内存里实现一个简单的环形缓冲区:头指针和尾指针各占一个字,R5 写数据前检查尾指针,Linux 读数据后更新头指针。这样即使两边节奏不一致,也不会覆盖未读数据。环形缓冲区的大小根据你的数据速率定,16MB 共享区里切 1MB 做缓冲绰绰有余。
5.3 用 Vitis 的 System Debugger 同时看两个核
Vitis 的 System Debugger 可以同时连接 A53 和 R5,分别下断点。调试 AMP 时,先启动 R5 的调试会话,再启动 Linux 侧的调试(通过 JTAG 连 A53),这样能同时观察两边状态。注意 R5 调试时不要 halt 太久,否则 Linux 侧可能因为等不到握手而超时。我习惯在 R5 侧关键位置加xil_printf,比断点更接近真实运行状态。
5.4 启动顺序与复位策略
XCZU2EG 的启动顺序是 BootROM → PMU → FSBL → R5/Linux。如果 R5 需要在 Linux 之前就绪,就把 R5 ELF 放 BOOT.BIN;如果 R5 依赖 Linux 提供的配置,就用 remoteproc 后加载。复位时,R5 可以独立复位而不影响 A53,但共享内存内容会保留,所以复位后要重新初始化握手标志,否则 Linux 侧会以为 R5 还在跑。
最后说个我自己的习惯:每次改完内存划分或设备树,先用devmem把共享内存前后各读 16 个字,确认没有越界写入,再跑业务逻辑。这个动作花不了一分钟,但能省掉半天找内存踩踏的时间。希望帮到你。
本文还有配套的精品资源,点击获取