Zynq UltraScale MPSoC 这款芯片在实际项目里用了好几年了从最初在裸机上调外设到后来在 APU 上跑 Linux再到需要在 RPU 上并行跑裸机实时任务一步步踩过来的。最近刚好把一套 OpenAMP 点对点通信方案从零搭完Vitis IDE 里 RPU 裸机程序这边从平台创建到跑通 echo 测试过程其实比想象中要绕网上资料也比较碎所以我把整套操作流程、配置要点和踩坑记录整理出来希望能帮你少走些弯路。这套方案能解决什么问题简单说就是在 Zynq UltraScale MPSoC 里让跑着 Linux 的 Cortex-A53APU和跑着裸机程序的 Cortex-R5FRPU之间通过共享内存和 OpenAMP 框架实现高效的消息通信。对于需要做电机控制、工业网络协议栈、实时信号采集同时又需要 Linux 承担人机交互、网络通信的场合这个软件架构非常实用。我写这篇的适用读者是手里有 ZynqMP 开发板想在 RPU 上跑裸机实时程序又需要和 APU 侧互通数据的嵌入式工程师无论你是刚接触 Vitis 还是已经能跑通 hello world这条链路都能用。1. 项目概述与整体架构拆解1.1 先理清 Zynq UltraScale MPSoC 的异构架构Zynq UltraScale MPSoC 的 PS 端集成了三套关键处理单元四核 Cortex-A53APU64位主频可达 1.5GHz、双核 Cortex-R5FRPU32位主频可达 600MHz以及 Mali-400 GPU。对嵌入式开发来说最核心的就是 APU 和 RPU 的协同工作——A53 适合跑 Linux、跑复杂协议栈R5F 则非常适合跑裸机实时程序中断延迟低、行为可预测而且还专门配备了 TCM紧耦合内存非常适合做时序敏感的控制任务。RPU 双核有两种运行模式Split 模式和 LockStep 模式。LockStep 模式下两个 R5 核心像一个核心一样同步执行适合对安全性要求极高的场景Split 模式下两个核各自独立运行各自访问各自的外设和中断适合把一个核留给裸机实时控制另一个核跑别的任务。我们这套方案用的是 Split 模式默认让 R5_0 作为 OpenAMP 的远端处理器实例R5_1 暂不启用。1.2 OpenAMP 框架与 RPU 裸机的角色分工OpenAMPOpen Asymmetric Multi-Processing是个开源异构多核通信框架它提供了远程处理器生命周期管理、消息传递、多核同步等能力。在 ZynqMP 里OpenAMP 主要做了以下几件事管理 RPU 的资源表resource table比如共享内存地址、vring 地址、通知机制等通过 RPMsgRemote Processor Messaging协议在 APU 和 RPU 之间建立通道化的消息通信依靠 libmetal 硬件抽象层屏蔽不同处理器、不同操作系统的差异。在这套方案里APU 侧负责主导通常是主处理器它负责加载 RPU 的固件ELF 文件、启动和停止 R5 核心RPU 侧是远端处理器运行一个裸机程序不做启动引导的活只在被通知后执行通信和应用逻辑。这个角色分工非常像“师傅带徒弟”——APU 把固件放到共享内存告诉 R5 从哪里开始执行之后两者通过 RPMsg 通道你一言我一语地交换数据。1.3 为什么选择 Vitis IDE 而不是传统 SDKVitis IDE 是 Xilinx现在叫 AMD官方推出的统一开发环境它最大的优势在于把硬件平台配置文件XSA、BSP板级支持包、多核异构工程管理、调试下载都整合在一起了。相比老一代 SDKVitis 对多域、多处理器的工程管理做得更好你可以基于同一个平台工程分别为 R5 建 standalone 裸机工程、为 A53 建 Linux 工程或裸机工程并且可以在同一窗口里调试不同核。这里有个经验值得说如果你只是做 RPU 裸机开发不涉及 Linux那么 Vitis 的轻量级创建方式只建 standalone 域就够了。但如果要同时调试 Linux 侧的 remoteproc 驱动最好把 Linux 域和 standalone 域放在同一个平台工程里这样后续交叉调试时路径、宏定义、依赖关系都不容易乱。2. 开发环境准备与工程创建2.1 硬件平台与软件工具链清单我个人实测用的是 ZCU104 开发板但这套流程对 ZCU102、Ultra96 等同样适用因为核心原理和软件栈是统一的。硬件上你需要Zynq UltraScale MPSoC 开发板一块USB-UART 线用于 APU 和 RPU 的串口输出JTAG 调试器板载 JTAG 或外部 USB-JTAGZCU104 板载 JTAG 即可。软件方面我使用的是 Vitis IDE 2022.1 版本Vivado 硬件工程配套导出的 XSA 文件作为平台输入。注意不同版本的 Vitis 内置的 OpenAMP 和 libmetal 版本会有差异代码接口细节略有不同但整体流程一致。如果你用的是 2023.x 或 2023.2API 差异也不大遇到编译报错时优先查看头文件定义即可。在开始之前建议先在 Vivado 里完成 PS 配置DDR、UART、SD 等外设按需使能并导出 XSA 文件。这里强调一个关键点在 Vivado 的 PS-PL 配置界面中PSU DDR 的地址段决定了 R5 和 A53 能看到的内存窗口建议给共享内存预留一个固定且不与 Linux 内核重叠的高端地址区域比如 0x3ED00000后续 OpenAMP 配置会用到这个值。2.2 创建 Vitis 平台工程这一步决定后续程序结构打开 Vitis IDE先设置工作空间目录然后通过File - New - Platform Project创建平台工程。在向导中选择前面 Vivado 导出的 XSA 文件操作系统这里重点来了——要同时勾选standalone和linux两个域如果不想做 Linux 侧可以只选 standalone但以后想加 Linux 调试就得重新建平台不如一次到位。处理器选择psu_cortexr5_0作为 standalone 域的处理器Linux 域则自动关联psu_cortexa53。点击完成并等 Vitis 自动编译平台会在工程里生成 standalone BSP里面的xparameters.h包含所有硬件基地址和中断号等关键宏定义。这个文件后面写裸机程序时会频繁用到尤其要留意XPAR_XUARTPS_0_BASEADDR、XPAR_PSU_DDR_0_S_AXI_BASEADDR等宏的值。平台工程创建完最好立即检查一下 BSP 里支持的库列表右键平台工程中的psu_cortexr5_0选择Board Support Package Settings在 Libraries 选项卡里确认openamp和libmetal有对应的条目。如果在 Vitis 2022.1 里没有看到 openamp 库选项那是因为部分版本默认不显示你需要通过手动添加库路径的方式解决具体方法我在第 3 节展开讲。2.3 创建 RPU 裸机应用工程优先用官方模板在平台工程上右键选择New - Application Project应用名称可以叫r5_openamp_demo处理器选psu_cortexr5_0语言选 C然后在模板页面选择OpenAMP echo-test。这个模板包含了 RPU 侧的通信测试代码、资源表、链接脚本等是官方提供的“最小可跑通”工程特别适合用来验证硬件环境和 OpenAMP 框架是否工作。如果你没找到OpenAMP echo-test模板也可以先选择Empty Application(C)然后手动添加 OpenAMP 相关的源文件和配置。不过我还是建议先用官方模板跑通一次再自行修改到自己的业务代码逻辑这样排查问题会简单很多。官方模板生成的代码结构比“从零写”要清晰尤其资源表那部分直接手写很容易出错。创建完成后Vitis 会自动生成echo_test.c、rsc_table.c、rsc_table.h、lscript.ld等关键文件。先不要急着改代码先编译一遍确保基础工程能通过再往下进行配置调整。3. 核心配置让 OpenAMP 在 RPU 上跑起来3.1 启用 BSP 中的 OpenAMP 与 libmetal 库在 Vitis 2022.1 里如果平台工程采用 standalone 域BSP 设置中默认并不会自动把 OpenAMP 库编译进来。你需要右键psu_cortexr5_0-Board Support Package Settings左侧选中libraries勾选libmetal和openamp。如果这里没有这些库选项说明这个版本的 Vitis 没有内置匹配的编译依赖一个可行的办法是修改platform.spr文件里的 BSP 配置或者在应用工程的 Makefile 里主动链接库路径但我更推荐直接在应用工程源码目录下手动加入 OpenAMP 头文件和源码在官方模板里已经附带了一部分以模板为准。还有一个容易忽略的细节BSP 里stdin和stdout设备要设置为可用的串口设备。在 Vitis 默认设置中standalone BSP 会优先寻找psu_uart_0如果 Vivado 里使能的是psu_uart_1输出就会莫名其妙地消失这不是程序逻辑问题纯粹是 BSP 配置问题。排查时一看到 R5 程序跑起来但串口毫无反应先检查这里。3.2 链接脚本与内存区域规划RPU 裸机程序的链接脚本lscript.ld决定了固件在内存中的布局。对于 OpenAMP 方案至少需要两个内存区域程序运行区域代码、数据、堆栈放在 DDR 或 TCM 中。TCM 访问延迟低又没有缓存一致性问题但只有 128KB I-TCM、128KB D-TCM如果你的程序不大放 TCM 很合适。缺点是 TCM 空间有限且要注意 TCM 的访问权限配置。OpenAMP 共享内存区域用于 vring 和 RPMsg 消息缓冲必须同时能被 APU 和 RPU 访问所以一般都制定在 DDR 的固定地址上比如 0x3ED00000 这个地址附近。以官方模板为例它生成的lscript.ld默认把程序起始放在psu_ddr_0_S_AXI_BASEADDR 0x10000000也就是 DDR 的某个偏移共享内存则放在OPENAMP_MEM段地址可以从资源表里的 vdev 定义看出来。实际使用中你需要确认这个地址是否和 Linux 设备树中预留的内存区间不冲突否则 APU 上的 Linux 可能会写入同一地址导致数据互相踩踏。这里我建议的分配方式比如 DDR 基地址是 0x00000000给 RPU 程序 1MB 用于代码和堆栈例如 0x3EB00000 ~ 0x3EC00000给 OpenAMP 共享内存 1MB例如 0x3ED00000 ~ 0x3EE00000。这样 A53 Linux 内存从 0x0 一直到 0x3EAFFFFF完全不冲突。具体地址可以在 Vivado 里查 DDR 地址映射也可以在实际运行时通过 resource table 回调打印验证。3.3 资源表Resource Table到底在配置什么资源表是 OpenAMP 通信的核心数据结构它告诉主处理器APU远端处理器RPU如何把共享内存区域转换成通信队列。资源表里最关键的是firmware resource unit其中最核心的条目是fw_rsc_vdev里面定义了 virtio 设备的两个 vring 信息vring 的地址、对齐方式、描述符数量、通知 ID。看模板中rsc_table.c的示例代码resources.offset[0] 0x120这个偏移量对应到resources.rsc_tbl[0]中定义的那个fw_rsc_vdev结构体。结构体里vring[0].da指向 0x3ED40000vring[1].da指向 0x3ED41000每个 vring 有 64 个描述符对齐是 4096 字节。这些值设计得很讲究vring 需要 4096 字节对齐两个 vring 之间预留了足够的空间避免描述符表互相覆盖。实操里经常出现的问题就是 APU 侧和 RPU 侧的资源表不一致比如 vring 地址不同、num 不同、notifyid 不同导致握手失败或者报错。所以当你修改了 RPU 侧的共享内存地址务必同步修改 APU 侧 remoteproc 设备树里的资源表定义。4. 通信程序实现与验证4.1 RPU 侧 echo 测试程序核心逻辑梳理官方模板的 RPU 侧核心代码主要做三件事初始化 OpenAMP 运行时、注册 RPMsg 回调函数、轮询等待消息并原样回发。关键的流程是初始化 libmetal 底层metal_init初始化底层设备抽象如内存映射、设备树节点解析初始化远程处理器rproc_init传入传输层比如基于共享内存的虚拟传输并启动该处理器初始化 RPMsgrpmsg_init创建 RPMsg 虚拟设备并注册rpmsg_channel_created、rpmsg_channel_destroyed、rpmsg_read_cb三个回调函数进入主循环程序不断轮询并调用rpmsg_poll或metal_poll来处理接收到的消息收到消息后读取内容再原样调用rpmsg_send回发到源地址。static int rpmsg_read_cb(struct rpmsg_channel *rp_chnl, void *data, int len, void *priv, unsigned long src) { xil_printf(Received: %.*s, echoing...\r\n, len, (char *)data); return rpmsg_send(rp_chnl, data, len); }回调函数里要注意data缓冲区是共享内存区域中的副本还是直接指向共享内存不同版本的 OpenAMP 行为不一样。Vitis 自带的实现中数据读入的是本地缓冲所以可以直接在data上操作但在某些 openamp 版本里data是共享内存的虚拟地址别名如果你在这里长时间持有该地址并再次写入需要留意缓存一致性问题。R5 在默认配置下是带缓存的而共享内存区域通常配置为普通内存此时操作数据前后没有显式 flush/invalidate 的话很容易出现数据“半新半旧”的现象。4.2 APU 侧Linux如何配合 RPU 通信如果 APU 侧跑的是 Linux你需要先在 Linux 内核菜单中确认以下配置已开启CONFIG_REMOTEPROCy CONFIG_RPMSGy CONFIG_RPMG_CHARy CONFIG_ZYNQMP_R5_REMOTEPROCy如果内核里没有CONFIG_ZYNQMP_R5_REMOTEPROC说明内核版本里的 remoteproc 驱动没有包含 ZynqMP R5 支持这在高版本 Linux如 5.10 之后通常是有的。编译内核时还可以开启CONFIG_RPMG_CHAR以便在用户空间通过/dev/rpmsg_ctrl0直接收发消息。设备树里需要为 R5 预留内存并添加 remoteproc 节点。以 5.15 内核为例可以在设备树中添加reserved-memory { #address-cells 2; #size-cells 2; ranges; rproc_0_reserved: rproc3ed00000 { no-map; reg 0x0 0x3ed00000 0x0 0x100000; }; }; zynqmp-r5-remoteproc0 { compatible xlnx,zynqmp-r5-remoteproc; reg 0x0 0x3ed00000 0x0 0x10000; core_conf split; r5-tcm-a psu_r5_tcm_a; r5-tcm-b psu_r5_tcm_b; memory-region rproc_0_reserved; };之后在 Linux 用户空间可以通过 configfs 或 sysfs 把编译好的 R5 ELF 固件加载到 remoteproc再触发启动。例如mkdir /sys/kernel/config/remoteproc/remoteproc0 echo -n /path/to/r5_openamp_demo.elf /sys/kernel/config/remoteproc/remoteproc0/firmware echo start /sys/kernel/config/remoteproc/remoteproc0/state启动成功后可以通过rpmsg_char或直接写测试程序进行消息收发。一个最简单的 Linux 侧 RPMsg 测试程序通过/dev/rpmsg_ctrl0创建通道后往/dev/rpmsg0写入数据就能看到 R5 原样回发的数据。4.3 编译下载与运行验证的完整流程在 Vitis 中先编译平台工程再编译 RPU 裸机应用工程。编译无误后你可以直接用 Vitis 的Run Configuration将程序下载到 R5 并运行前提是 JTAG 连接正常。选择 R5 核添加 ELF 文件路径然后启动调试会话。比较关键的一点是如果你打算最终用 Linux 来加载 R5 固件那么 Vitis 调试阶段对 R5 的内存写入和 Linux 后续的 remoteproc 加载会互相影响。我通常的做法是先在 Vitis 里单独用 JTAG 跑通 R5 侧通信确认没有硬件问题后再联合 Linux 做整体验证。这样做的好处是万一通信不成功你能快速判断是 R5 程序本身的问题还是 APU 侧配置的问题。串口输出是主要验证手段。R5 侧的 UART 打印建议用xil_printf而不是重定向到标准库的printf前者的实现更轻量而且针对裸机环境做了适配。Linux 侧的用户程序则直接printf到终端方便对比两端日志。实际验证成功时的输出效果大致是Linux 侧往 RPMsg 通道写入一条 hello r5 字符串R5 串口打印出收到该消息并同时通过 RPMsg 把消息原样回发Linux 侧读取到回包打印出回复内容。这个 echo 来回如果稳定 1000 次不丢包说明通信链路是可靠可用的。5. 常见问题与排查技巧实录5.1 高频问题盘点按症状给解决方案这几个问题我在这类项目里碰到最多次整理成表方便你速查现象可能原因解决办法R5 程序一启动就死机不打印任何信息BSP 里 stdout 串口配置错误TCM 或 DDR 地址访问权限不对核对 BSP 的 stdin/stdout 设备在 Vivado PS 配置中确认 R5 的 DDR slave 访问权限OpenAMP 模板编译报错找不到openamp/open_amp.hBSP 未启用 openamp 库xparameters.h 中宏定义缺失检查 BSP 的 Libraries 勾选确认平台工程重新编译过echo 测试进行中消息收发偶尔失败vring 地址不是 4096 对齐共享内存与 Linux 内存冲突检查资源表 vring 地址核对设备树预留内存范围R5 收到消息内容乱码共享内存缓存一致性问题在 R5 侧读取前做 cache invalidate发送前 flush或者把共享内存区域配置为 non-cacheable使用 Linux remoteproc 加载 R5 固件失败ELF 格式或加载地址不匹配设备树 remoteproc 节点地址错误检查 ELF 加载地址和设备树 reg、memory-region 是否一致确认core_conf正确RPU 双核配置时另一个核不执行Split 模式未正确配置两个核的入口点相同检查 Vivado 中 RPU 工作模式在 Vitis 中分别为 R5_0、R5_1 配置不同程序5.2 一些调试经验和特别提醒第一个经验是关于“先确认裸机基础再上 OpenAMP”。很多新手一上来就想把 echo 跑通结果 RPU 连最基本的串口打印都没搞定。我自己的习惯是先创建一个最简单的空应用只调用xil_printf打印一行字符用 JTAG 下载运行确认 R5 能正常启动、串口输出正常再去启动 OpenAMP 工程。这一步能省下至少一晚上的排查时间。第二个经验是关于共享内存地址的规划。这个地址牵涉三处R5 链接脚本、资源表、Linux 设备树。三处必须一致否则两边的数据根本不在同一块物理内存上。我在实际项目中用过 0x3ED00000 这个地址当时是参考了某方案的内存布局但你自己做项目时要根据 DDR 容量、Linux 保留区域、R5 代码大小综合确定。建议将地址规划直接写到设计文档里并在代码中加宏定义避免改一处漏两处。第三个经验比较特别当通信频率高、数据量大时单靠 RPMsg 通道效率可能不够用。RPMsg 是复制语义每次发送都要把数据从用户缓冲区拷贝到共享内存再从共享内存拷贝到接收缓冲区开销不小。如果对实时性要求高一个折中方案是用 RPMsg 传递小消息而大数据走共享内存加硬件同步机制。这个方案我们在多个项目里验证过既保持了 OpenAMP 的标准通信又能满足带宽需求。最后再分享一个小技巧在 Vitis 的调试视图里可以同时连接 APU 和 RPU 两个目标进行源码级联调。启动两个调试配置一个是 A53 的 Linux 内核调试或裸机一个是 R5 的裸机调试然后分别在两个核上打断点。这样就能非常直观地看到消息从 Linux 侧发出后有没有真正进入 R5 的回调函数。单看串口日志只能确定最终结果源码联调可以看到中间每一层的数据流转排查问题快很多。