ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Zynq UltraScale+ MPSoC异构双核通信:OpenAMP实践指南

2026/9/28 17:58:57 拓冰建站 浏览量
Zynq UltraScale+ MPSoC异构双核通信:OpenAMP实践指南 做异构SoC的工程师应该都有这种体会手头明明是一颗ZYNQ UltraScale MPSoC芯片上四个Cortex-A53和两个Cortex-R5全都整整齐齐摆着可真到项目里A53跑着Linux处理网络和多媒体R5却要么闲置要么被我当成普通MCU单独点灯。两边数据要靠PL侧FIFO片上可编程逻辑的先进先出队列中转延时高逻辑还复杂。这个痛点我纠结了很久直到在Xilinx 2018.3工具链里把OpenAMP开放式非对称多处理框架跑通才真正体会到异构双核协同的正确姿势A53上的Linux通过remoteproc直接加载和控制R5固件rpmsg共享内存通道负责数据收发整条链路稳定且高效。这篇文章就是我当时从零搭建这套方案的全过程记录包括2018.3版本的软硬件工程配置、设备树编写、R5裸机侧OpenAMP库集成以及调试中踩过的各种坑给正在用ZU系列做异构开发的同行一个可以直接上手的参考。1. 为什么非要用OpenAMPA53和R5协作的正确打开方式1.1 双核各干各的数据却要汇合MPSoC这类芯片的优势很直白A53适合跑复杂的Linux系统做协议栈、图形界面、文件系统都很舒服R5则胜在实时性好中断响应快适合电机控制、工业总线这类对时序敏感的活儿。但实际产品里这两个处理器不是互相独立就完事了控制逻辑和业务逻辑之间总有数据要交换。比如一个运动控制设备R5负责读编码器、算PID比例积分微分控制A53负责做人机界面和远程通信那位置、速度、状态这些数据总得在两边来回传吧。最笨的办法是直连GPIO模拟握手协议或者用PL里的BRAM块随机存储器做一个共享缓冲区再配合自定义中断。这两种方案我自己都试过开发周期长不说到了调通信时序的阶段基本就是在跟逻辑分析仪较劲每改一次帧格式都得动RTL寄存器传输级代码和两边的驱动。如果项目只是验证一下还好真要进量产这种方案的维护成本高得吓人。1.2 OpenAMP 2018.3解决了核心的“从哪来、到哪去”问题OpenAMP是一个开源的非对称多处理框架它把高端处理器和实时处理器之间的协同通信标准化了。在2018.3版本的Xilinx工具链里OpenAMP的集成度已经相当高Linux侧是内核自带的remoteproc和rpmsg驱动栈R5裸机侧是官方SDK里可以直接引用的libmetal和open-amp库。两边不需要自己维护“私有协议”只要按照框架规范申请共享内存、注册回调函数数据就能在双核之间自动打通。对工程师来说这套方案最大的价值在于把“处理器间通信”从应用层问题变成了一次性的系统配置工作。我只需要在硬件环境里把R5的启动方式指定好在设备树里把共享内存和IPI核间中断描述清楚剩下的事情框架都处理好了。相比自己用寄存器写收发逻辑开发量至少省一半以上而且框架自带了echo_test这类现成测试demo可以快速验证链路是否通。2. 先把通信模型吃透共享内存和中断是怎么配合的2.1 共享内存一个快递柜的比喻OpenAMP的底层数据交换靠的就是一块双核都能访问的物理内存区域在ZynqMP上一般是DDR里专门划出来的一段地址。我习惯把它理解成小区里的快递柜A53往柜子里放包裹R5从柜子里取包裹两边不需要同时出现在同一个门口碰头只要约定好用哪个柜子就行。但这套机制有个前提两个核都必须清楚柜子的地址范围和大小否则A53放在0x3ED00000的数据R5却去0x3EC00000找那就鸡同鸭讲了。所以在设备树和R5固件的链接脚本里共享内存区域的地址和大小必须完全一致我后面会专门讲这块踩过的坑。快递柜还有一个规则两边拿完快递要有个回执机制不然一方一直在放另一方溢出都察觉不到。这个回执在OpenAMP里就靠vring虚拟环形缓冲和virtio机制来实现。2.2 IPI快递到了喊你来取共享内存解决了“数据放哪”的问题但A53把数据放进共享内存后R5正在跑它的实时控制循环怎么知道有新数据到了这时候就必须有中断机制。在ZynqMP上APU和RPU之间通过一组专用的Inter-Processor Interrupt寄存器互相触发中断这就是IPI处理器间中断。Xilinx 2018.3里已经把这些IPI通道的地址和中断号在设备树里定义好了我只需要在PetaLinux配置时确认用哪个IPI然后让R5侧的BSP对应上就行。实际调试中中断号写错是最常见的根因之一因为GIC通用中断控制器的SPI编号从32开始设备树里填的是逻辑编号而很多人想当然地往上加偏移结果中断永远触发不了。2.3 一条数据从A53到R5的完整旅程以echo_test为例假设Linux侧发了一个字符串“hello”给R5它走的路径是这样的A53上的应用先通过rpmsg设备节点/dev/rpmsg0写入数据内核驱动把数据放进共享内存的发送vring中然后写IPI触发寄存器R5收到IPI中断后进入OpenAMP的中断处理函数从vring中取出数据然后调用用户注册的收发回调函数echo_test里就是直接把数据原封不动放回接收队列紧接着R5触发反向的IPI通知A53数据已经回写A53侧的内核驱动收到通知后应用从设备节点读到的就是R5返回的数据。这条路径里最容易被忽视的是缓存一致性问题。A53有L1和L2缓存R5也有自己的缓存如果两边直接读同一个物理地址很可能读到的都是自己缓存里的旧值。OpenAMP的机制里通过内存栅栏和缓存操作保证了同步但前提是共享内存区域必须被标记为non-cached或者使用预留的DMA属性这个在设备树里没有配好的话问题表现就是“偶尔能收到偶尔收不到”。3. 环境准备2018.3工具链和Vivado硬件工程的关键配置3.1 版本选型为什么2018.3是稳定的一代我最初其实是从2019.1版本开始接触OpenAMP的那时候的坑比2018.3更多尤其是PetaLinux的RPU启动配置界面经常变化。后来退回到2018.3这个版本对应的是Xilinx相对成熟的Linux内核分支4.14OpenAMP的生态也基本稳定社区里能搜到的案例最多出了问题也好找人问。建议直接用Ubuntu 16.04作为开发机系统Vivado、PetaLinux、SDK全部安装2018.3版本。这里有一个版本匹配的细节Vivado和PetaLinux的版本必须完全一致我用2018.3的Vivado配合2018.3的PetaLinuxSDK则是Vivado安装包自带的。另外SDK里新建R5裸机工程时standalone BSP版本也要跟SDK一致否则库函数链接会报各种奇奇怪怪的错。3.2 Vivado硬件工程里的三个必改项创建Vivado工程、例化ZYNQ UltraScale MPSoC IP这一步大部分人都熟但有几个地方新手特别容易漏。第一PS-PL配置里的FCLK_CLK0频率要确认好它会影响PL侧逻辑的时序也常被R5外设的时钟参考误用。我通常设为100MHz对外设比较友好。第二在ZYNQ UltraScale MPSoC IP配置的“PS-PL Configuration”页面里一定要启用UART1并且把它的MIO管脚分配正确否则后面串口日志完全看不到。做双核调试时A53和R5最好分别映射到两个串口UART0给R5打印UART1给A53的Linux用这样两边日志不会混在一起。第三是对DDR的配置。MPSoC的DDR控制器参数最好直接沿用评估板的默认配置不要自己去改型号和时序除非你很清楚自己的板子用了哪颗DDR颗粒。DDR配错会直接导致系统起不来而且报错信息并不直观。硬件导出后在SDK里除了生成FSBL之外R5侧的DDR地址空间也必须确认好。比如在ZCU102上DDR的基地址是0x00000000但OpenAMP的共享内存区域我习惯放在0x3ED00000之后这样就能避开Linux内核镜像和文件系统的常用区域后面章节会详细讲地址划分。3.3 PetaLinux里RPU启动方式的配置很多人第一步就错了PetaLinux工程创建不需要多说关键是拿到Vivado导出的硬件描述文件后执行petalinux-config时一定要进入“Subsystem AUTO Hardware Settings”找到“PS-PL Configuration → RPU”相关的菜单把RPU Operating Mode选为SplitRPU Boot Mode选为Direct。这一步漏掉的后果是R5处于Lockstep模式Linux侧remoteproc只能操作R5_0但实际运行的R5核心可能还是两个绑在一起的状态行为会非常诡异。我在第一次配置时就没找到这个菜单默认把工程编出来结果remoteproc启动R5时log一直报“Failed to set RPU operating mode”排查了半天才发现是这里没设置。建议在PetaLinux配置阶段就把RPU配置界面完整截图保留后面出问题回来对照会省很多事。4. APU侧实现设备树、共享内存和remoteproc节点的完整配置4.1 在设备树里预留内存mem参数之外的关键一步要让A53侧的Linux不碰R5用的内存最直接的办法是在设备树的reserved-memory节点中把这块区域标记成no-map。Vivado导出的默认设备树里通常没有这个节点需要在系统用户设备树文件system-user.dtsi里添加。我常用的配置是给R5预留16MB空间放在DDR末尾区域比如0x3ED00000开始、大小0x1000000这样对Linux常规内存的影响最小。设备树片段长这样reserved-memory { #address-cells 2; #size-cells 2; ranges; rproc_0_reserved: buffer3ed00000 { no-map; reg 0x0 0x3ed00000 0x0 0x1000000; }; };这里有一个非常关键的细节reserved-memory里写的是“设备树视角”的物理地址而U-Boot的启动参数里如果用mem限制Linux可用内存两者的地址必须对得上。比如我预留了DDR末尾的16MB如果总内存是2GBU-Boot启动参数里就要写mem2032M或者mem0x7ED00000确保Linux内核不会把这16MB占走。在实际开发中我推荐先看U-Boot的bdinfo输出确认DDR实际大小再做地址规划不然预留区域超出了真实内存范围R5固件加载后可能访问到不存在的物理地址系统直接进入异常状态。4.2 remoteproc和mboxes节点告诉内核去哪里加载R5固件reserved-memory只是把内存划出来了内核的remoteproc驱动还需要知道去哪一片内存加载R5的elf固件以及用哪组IPI发中断。这些信息通过remoteproc节点描述。在2018.3版本的内核里ZynqMP R5的remoteproc驱动节点是这么写的remoteproc0: remoteproc0 { compatible xlnx,zynqmp-r5-remoteproc-1.0; reg 0x0 0xff000000 0x0 0x1000; core_conf split0; #address-cells 2; #size-cells 2; ranges; memory-region rproc_0_reserved; mboxes ipi_mailbox_rpu0, ipi_mailbox_rpu0; mbox-names tx, rx; };mboxes是这套机制里的又一个关键角色。实现r5和APU核间通信时Linux侧实际上是通过mailbox框架封装IPI操作这里引用了两个相同的mailbox通道分别作为收发方向。mboxes后面的中断号和物理地址必须跟硬件实际配置严格一致不然即使固件加载成功rpmsg的收发也完全不通。mailbox节点本身通常是这样的ipi_mailbox_rpu0: mailboxff990400 { compatible xlnx,zynqmp-ipi-mailbox; reg 0x0 0xff990400 0x0 0x100; interrupt-parent gic; interrupts 0 35 4; #mbox-cells 1; xlnx,ipi-id 8; };这里最容易错的是interrupts属性。35这个数字是从GIC角度看到的实际spi中断号但很多人从手册里查到的是“中断编号0~31表示PPI处理器私有外设中断”这类信息就会习惯性地把SPI中断号再加32结果中断线整体偏移数据永远发不出去。这个问题我排查了差不多两天最后是用devicetree的interrupts文档对照GIC手册才定下正确序号。4.3 U-Boot和kernel配置别让启动参数把内存坑了设备树配好之后还需要检查U-Boot的环境变量。PetaLinux默认的启动脚本会读取bootargs如果里面没有限制Linux可用内存的mem参数Linux内核可能会在初始化时扫描到全部DDR把预留区域也纳入页表管理这样R5固件一访问共享内存A53侧就报一堆异常。在实际项目里我直接在petalinux-config的U-Boot配置里把bootargs强制加上mem参数。以2GB内存为例预留末尾16MB后Linux可用内存就是2032M即mem2032M。同时PetaLinux内核需要把OpenAMP相关模块编进去在kernel配置里确认CONFIG_OPENAMP、CONFIG_REMOTEPROC、CONFIG_RPMSG_VIRTIO这几个选项为y或m这样Linux启动后才会出现remoteproc设备。5. RPU侧实现在SDK里搭出R5的OpenAMP裸机环境5.1 创建R5 standalone BSP并集成libmetal和open-ampR5侧我在Xilinx SDK里单独创建了一个Application Project处理器选择psu_cortexr5_0操作系统选择standaloneBSP版本保持2018.3。Xilinx在这版SDK里已经把libmetal和open-amp做成了可选的库创建BSP之后可以在它的board support settings里把这两个库勾选上然后重新生成BSP。如果你是从旧版本工程迁移过来的注意SDK生成libmetal和open-amp时会自动带有一套针对ZynqMP的platform实现里面有一个metal-config.h这个头文件里定义了软硬件平台相关的配置项比如缓存使能方式、中断控制器类型等。编译时如果报找不到metal_config那一定是库没有正确生成或者工程引用路径没加进来。5.2 链接脚本的调整R5的代码和共享内存必须落在预留区R5裸机工程最耽误时间的其实是链接脚本。SDK自动生成的lscript.ld默认会把所有代码段放在DDR基地址0x00000000如果Linux已经从0x00080000开始跑起来了R5的固件直接加载进去就等于踩掉了内核代码系统必崩。我在lscript.ld里做了这样的处理把R5的可执行代码放到DDR起始的0x3EC00000位置大小8MB把openamp需要的共享内存区域定义到0x3ED00000之后。这样一个是代码区一个是共享区都紧挨着放在Linux保留的16MB区域内。注意R5的入口地址在split模式下默认是TCM紧耦合内存0xFFE00000也可以改为DDR地址工程里的boot和链接脚本要同时改动。实操中我建议先在SDK里把R5的hello world跑通用Xilinx的XSDK调试器或者JTAG直接下载确认R5的DDR访问没问题再集成OpenAMP库。如果连最基础的DDR读写都有问题那就先把硬件地址和内存控制器检查一遍别急着上通信。5.3 echo_test示例R5侧回环代码的核心逻辑SDK里OpenAMP的示例工程通常会自带echo_test它是验证双核通信链路的经典demo。R5侧的核心代码逻辑并不复杂初始化libmetal和open-amp框架注册一个用户回调函数然后进入循环等待数据。关键代码如下所示#define SHARED_MEM_PA 0x3ED00000 #define SHARED_MEM_SIZE 0x100000 static void rpmsg_echo_callback(struct rpmsg_endpoint *ept, void *data, size_t len) { rpmsg_send(ept, data, len); } int main(void) { struct metal_init_params metal_params METAL_INIT_DEFAULTS; metal_init(metal_params); openamp_init(METAL_IO_REGION_PHYS, SHARED_MEM_PA, SHARED_MEM_SIZE); rpmsg_create_ept(rproc, echo_ept, echo, 0, rpmsg_echo_callback); while (1) { /* 等待中断触发 */ } return 0; }具体工程里流程会多一些比如用platform_create_proc函数创建远程处理器实例用rpmsg_init_vdev初始化virtio设备再用rpmsg_create_ept创建端点。需要特别留意的是endpoint名字必须和Linux侧请求的端口名一致echo_test里通常约定为“echo”如果两边名字对不上Linux侧会报找不到endpoint。5.4 R5固件的启动方式remoteproc从Linux侧加载R5固件编译好之后会有两个产物一个是elf文件一个是bin文件。Linux侧remoteproc加载一般是直接用elf文件因为elf里带有段地址信息驱动可以正确把代码放到链接脚本指定的物理地址。我用的是这样一条命令echo stop /sys/class/remoteproc/remoteproc0/state echo /lib/firmware/r5_fw.elf /sys/class/remoteproc/remoteproc0/firmware echo start /sys/class/remoteproc/remoteproc0/state将编译出的elf文件拷贝到根文件系统的/lib/firmware目录下文件命名为r5_fw.elf然后执行上面的命令序列。如果设备树和共享内存配置都正确start之后remoteproc会复位R5并加载代码加载完成后R5开始运行OpenAMP框架启动然后在Linux侧会出现/dev/rpmsg0设备节点。6. 双核通信验证从Linux侧发数据到R5再收回来6.1 验证整条链路的经典操作等到/dev/rpmsg0节点出现就可以跑通信测试了。我用的是官方示例的echo_test工具它会在Linux侧创建一个rpmsg端点并发送一串测试数据R5收到后原样回传echo_test再比较数据是否一致。执行命令echo_test -d rpmsg0如果一切正常终端会打印类似“Message received: hello from RPMsg”的日志并且统计收发字节数。我第一次跑通的时候看到日志刷出来心里那股踏实感是说不出的毕竟这个“hello”已经在A53和R5之间走了两个来回经过了共享内存、IPI、virtio和缓存一致性处理等一系列机制。这个测试非常重要如果它不通过后面的任何业务开发都是基于一个不可靠的通信底座。我自己就遇到过echo_test偶尔失败的情况后来发现在R5回调函数里不能耗时太长否则A53侧的超时机制会认为失败而R5这边其实只是把数据回得晚了。6.2 吞吐量和实时性实测的初步数据双核通信的吞吐量取决于共享内存带宽、vring数量和中断频率。我在ZCU102上粗略测试过不分包、纯echo回环的场景下小数据包64字节的单次往返延迟在微秒级大批量传输时通过调整vring深度和buff数量能跑到数百Mbps级别具体数值取决于是否开启DMA和缓存优化。如果实际项目里对实时性要求高建议把vring数量调整为1对把每个buffer的大小调大减少中断触发频率如果对吞吐量要求高则适当增加vring深度但要注意共享内存的占用会同步增加别超出预留范围。6.3 其它两种启动R5的方式各有用武之地除了Linux侧remoteproc启动之外还有两个方案可以备选。一个是在U-Boot里把R5的elf直接加载到指定地址再启动适合开机就要让R5先跑控制的场景另一个是通过Xilinx SDK的XSDK调试器连JTAG把固件download到R5后手动运行适合调试阶段断点查看R5内部状态。实际开发中我推荐先把JTAG方式作为主力调试手段固件逻辑稳定后再切换到remoteproc方式由Linux自动加载。这样既利用了调试器的便利性又保证了最终部署的自动化。7. 避坑指南我从零到跑通遇到的典型问题实录7.1 问题速查表我把调试中最常撞上的问题整理成一张表方便后来者对照排查。问题现象最常见根因解决方向remoteproc启动R5报“Failed to set RPU operating mode”PetaLinux里RPU Operating Mode没设置进入petalinux-config的RPU菜单选择Split模式后重新编译Linux侧没有生成/dev/rpmsg0节点设备树mboxes或interrupts配置错误检查remoteproc节点的mboxes、mailbox节点的interrupts和地址echo_test发送超时R5固件没正确加载或R5侧回调函数异常确认R5链接脚本地址和reserved-memory一致单独用JTAG跑R5验证Linux启动后异常卡死预留内存区域与Linux可用内存重叠检查U-Boot的bootargs用mem参数隔离预留区域rpmsg数据收发随机失败缓存一致性问题检查共享内存区域是否声明为no-map并确认libmetal初始化时缓存配置正确R5加载后代码跑飞lscript.ld里的代码段地址不在保留区内把R5代码段固定在预留内存范围内并检查入口地址7.2 三个最容易让人崩溃的深坑第一个深坑是IPI中断号。我前面提到过interrupts 0 35 4这个35代表的是硬件SPI中断号但很多人从Xilinx手册的“Interrupt IDs”表格里看到的是“IPI_6 87”这种编号然后直接填到设备树里结果GIC无论如何都不会触发。正确做法是查GIC的SPI编号换算规则设备树里填的是“0表示SPI 实际SPI编号”千万不要自己脑补加偏移。第二个深坑是共享内存区域的重复映射。PetaLinux默认的设备树里如果有两个节点引用同一段物理地址范围Linux内核在解析reserved-memory时可能只保留第一个第二个直接被覆盖。我遇到过R5固件放在0x3EC00000而某个PL驱动的设备树节点也引用到这一区域导致R5的代码被PL侧的DMA操作冲掉系统不定时死机。排查方法是启动后检查生成的实际设备树二进制用dtc工具反编译出来对照。第三个深坑是缓存一致性问题。即便设备树配了no-mapR5侧如果使用默认的WB写回缓存策略A53写入共享内存的数据可能会停留在L2缓存里R5读到的还是旧数据。我建议在R5侧的把共享内存映射为强序Strongly Ordered或设备类型内存同时开启Linux侧的DMA API来保证同步。如果发现数据偶尔错乱优先怀疑缓存而不是去抓中断和地址。7.3 我的调试顺序建议最后分享一个调试顺序经验这个顺序能帮你把问题限定在单一层面而不是所有功能同时失灵时无从下手。第一步先确认R5裸机的基础环境不加载OpenAMP只跑一个点灯或串口打印程序用JTAG下载到R5确保R5能正常访问DDR和串口。第二步在Linux侧把R5固件通过remoteproc方式加载成功查看R5打印的启动日志确认固件确实在运行。第三步再启用OpenAMP通信先跑echo_test不要直接上业务数据。等echo_test稳定通过后再逐步加入实际业务协议。这套顺序能让你避免“R5没起来、数据发不出去、中断没触发”这些问题混在一起时完全无处下手的尴尬局面。在我自己跑通OpenAMP这套流程之后最大的感受是它把异构双核通信从“偏门手艺”变成了“标准配置”。只要环境搭建和设备树配置这关过了后续业务开发基本就像在单片机上写逻辑一样简单。尤其是调试阶段先用JTAG和echo_test把底层链路焊牢再做业务整个效率会高很多。如果你们项目里也正好遇到A53和R5之间数据交换的问题别急着写自定义协议把OpenAMP这套方案评估一遍值得。