
做Zynq的人迟早要面对一个问题PS端的双核Cortex-A9到底怎么拆开用。如果老老实实跑Linux SMP两个核心对你来说只是“多了个CPU”除开多线程性能没什么特别感觉。但当你手里有一块ZYNQ7020既想让Linux处理网络协议栈、文件系统、UI又想让另一个核跑FreeRTOS去处理微秒级的中断或运动控制时SMP就完全不对劲了。这时候真正需要的是AMP非对称多核而OpenAMP就是Xilinx和开源社区在这个场景下给出的标准答案。这篇文章我不打算念PPT就把我自己调通ZYNQ7020下LinuxFreeRTOS双核通信的完整过程拆开讲。你会看到为什么选OpenAMP、内存和资源表怎么规划、Linux侧和FreeRTOS侧分别要做什么、编译烧录之后怎么验证以及我在实际调试中踩过的最深的几个坑。如果你正准备在Zynq上做异构多核或者已经被remote proc启动问题折磨到怀疑人生这篇文章应该能帮你少走不少弯路。1. 为什么是OpenAMP而不是自己写标志位加共享内存1.1 先搞清楚ZYNQ的双核能怎么玩ZYNQ7020的PS端是两个Cortex-A9这不是普通MCU的双核是可以单独配置运行模式的应用处理器。常见玩法有三种。第一是SMP对称多核两个核统一跑Linux内核会把任务均衡到两个核上你用起来就是一个8核不存在的普通Linux简单省事但实时性做不上去线程调度延迟是不可控的。第二是纯AMP两个核各跑各的裸机或RTOS见面全靠共享内存加几个标志位自己定协议自己维护同步能用但每次换人都得把上一任的协议文档翻出来看三天。第三就是OpenAMP方案它在AMP之上做了一层标准化的核间通信框架把远程固件加载、共享内存、消息传递、生命周期管理都封装好了两边跑什么系统不重要重要的是通信接口是统一的。很多第一次接触的人会问既然Linux跑得很好FreeRTOS也跑得很好那我直接用两个处理器不就行了问题是ZYNQ7020只有一个PS双核物理上共用一块DDR、一组外设、一组中断控制器。你想让两个系统在同一块内存上协同工作就必须解决四个问题内存地址怎么划分、怎么互相通知、数据怎么同步、哪个核先启动会把另一个核的程序加载到内存。这四个问题如果自己从头搞每一个都能让人折腾两周。OpenAMP正好把这四件事都给你做了。1.2 OpenAMP到底帮你做了什么OpenAMP的全称是Open Asymmetric Multi-Processing它实际包含三个层面的东西Linux内核侧的remoteproc/rpmsg框架、libmetal硬件访问抽象层、以及跑在远程核上的OpenAMP固件库。remoteproc负责“启动远程处理器”这件事。Linux起来之后通过remoteproc节点把FreeRTOS编译出来的elf或bin加载到预先约定好的内存地址然后释放远程核的复位信号让它开始执行。这解决了“谁先启动、怎么启动”的问题。rpmsg负责“核间消息传递”。它在底层用shared memory加virtqueue虚拟队列实现多个逻辑通道上的数据传输上层只需要open一个channel然后读write就能收数据。这解决了“数据怎么收发、怎么封装”的问题。libmetal则统一封装了物理内存映射、内存分配、设备访问、中断等操作让Linux用户态程序和远端固件都用同一套API去操作共享内存。这解决了“两个系统怎么访问同一块物理内存”的问题。我见过不少项目组一开始为了图省事自己写一个固定地址的共享内存结构体再用一个GPIO或者软件中断来同步。初期数据量小确实能跑但一旦需要加协议、加通道、做动态消息长度自己维护的成本会上来得很快而且调试工具基本没有。OpenAMP的另一个好处是调试手段成熟Linux侧的remoteproc sysfs节点可以直接看状态rpmsg的通道信息也会暴露在/dev/rpmsg*下面比起两眼一抹黑的裸共享内存方式排查问题友好太多。2. 开发环境与初始资源规划2.1 硬件与工具链清单先说硬件。我这次用的是最常见的ZYNQ7020开发板PS侧DDR3总容量1GBPL端没做额外设计或者说只做了一个最小系统把PS启动必须的MIO、DDR、UART、SD卡接口引出来就够。你手上的板子只要是7020基本逻辑一样。软件栈方面我用的版本组合是Vivado/Vitis 2021.2老版本其实也行只是库的位置和命名稍有差异。Linux内核来自Xilinx官方linux-xlnx分支用5.4或5.10都可以关键是打开remoteproc和rpmsg相关配置。交叉编译工具链arm-linux-gnueabihf-gcc版本8.x以上。FreeRTOS内核可以直接用Vitis里自带的版本或者从FreeRTOS官网拉一个LTS版本都行版本不用太新稳定就好。OpenAMP和libmetal我建议直接用Xilinx维护的版本因为跟Zynq平台适配得最好。有一个细节值得多说一句Vitis的example仓库里其实自带了一个完整的OpenAMP echo_test工程Linux端和FreeRTOS端都有现成代码。第一次做这个功能我强烈建议先把官方echo_test调通再改成自己的业务逻辑。直接上来就改通信协议出了问题很难判断是框架问题还是自己代码的问题。2.2 动手前先给DDR分好“地盘”OpenAMP启动之前最重要的一件事是确定DDR空间怎么分。Linux占一块FreeRTOS占一块核间通信再占一块。ZYNQ7020的DDR物理基地址一般是0x00100000如果板载1GB内存地址范围就是0x00100000到0x3FFFFFFF。Linux内核通过设备树里的reg属性知道自己能用多少内存而FreeRTOS固件会被加载到Linux不管理的那段区域。我常用的划分方式是把高地址区域留下来区域起止地址大小用途Linux系统0x00100000 ~ 0x2FFFFFFF约768MBLinux内核、根文件系统、应用程序远程固件加载区0x30000000 ~ 0x3EEFFFFF约239MB存放FreeRTOS镜像和堆栈共享内存区0x3F000000 ~ 0x3F0FFFFF1MBresource table、vring、数据缓冲预留0x3F100000 ~ 0x3FFFFFFF15MB备用或日志缓冲这个划分不是绝对的但有一个原则共享内存区尽量放在DDR的高地址并且对齐到1MB边界方便设备树和MMU页表做映射。Linux侧需要把共享内存区域从默认的Linux内存管理中排除掉否则内核可能会把这块内存分配出去然后远程核往里面写数据就直接把系统写崩。在设备树里我是这样预留区域的reserved-memory { #address-cells 1; #size-cells 1; ranges; rproc_0_reserved: rproc3F000000 { compatible shared-dma-pool; reg 0x3F000000 0x00100000; no-map; }; };这里的no-map很关键意思是这块区域不映射到内核页表谁都不能乱动。实际调试时如果发现Linux起来莫名其妙卡死先检查是不是这块区域没预留好。3. 资源表、共享内存与IPI中断三个要素怎么联动3.1 它们分别起什么作用很多人把OpenAMP想得很玄其实拆开看核心就是三样东西资源表、共享内存、核间中断。资源表resource table是远程核固件里的一张“配置清单”里面说明了这个固件需要哪些virtqueue、需要多大的共享内存、固件版本是多少。Linux侧的remoteproc在加载固件时会先解析这张表然后根据表里的信息去初始化通信结构。共享内存就是双方真正交换数据的地方。它被组织成一个或者多个virtqueue每个virtqueue内部分成若干bufferLinux侧和FreeRTOS侧各自维护自己的读写指针不需要每次通信都靠中断打断对方只在必要时发IPI通知。IPIInter-Processor Interrupt是“敲门”的动作。当A核往共享内存写完数据后通过GIC发一个核间中断给B核B核收到中断后用libmetal从共享内存中把数据读走。实时系统中IPI一般只有一个信号量级别的开销延迟可控。用生活化的比喻来说资源表是两家公司签的合同共享内存是双方共用的仓库IPI就是仓库门口的门铃。你要开张做生意先得把合同签好仓库备好门铃接好少一样都没法干活。3.2 资源表怎么定义FreeRTOS侧的固件里必须定义一张资源表。Xilinx官方例程用的是预定义在linker script里的结构体我改造后的版本大概长这样#define RSC_TABLE_ADDR 0x3F000000 #define VRING0_ADDR 0x3F002000 #define VRING1_ADDR 0x3F004000 #define SHARED_BUF_ADDR 0x3F010000 #define SHARED_BUF_SIZE 0x00080000 static struct fw_rsc_table my_resource_table { .header { .ver 1, .num 1, .reserved {0, 0} }, .rpmsg_vring { { .da VRING0_ADDR, .align 4, .num 8, .notifyid 0, .reserved {0, 0} }, { .da VRING1_ADDR, .align 4, .num 8, .notifyid 1, .reserved {0, 0} } } };需要注意这里的地址da字段是物理地址而且必须是实际DDR物理地址不是虚拟地址。很多新手在这里犯迷糊直接在链接脚本里写了一个虚拟地址段结果Linux侧解析出来的地址和自己预想的内存位置对不上数据自然传不通。3.3 IPI中断配置要点ZYNQ7020的IPI中断可以使用软件生成的中断SGI也可以使用PL逻辑做出来的中断控制器。OpenAMP官方例程里通常用的是SGI中断两个核共享一个中断ID。配置时需要分别在Linux侧和FreeRTOS侧注册同一个中断处理函数。Linux侧通常是在设备树remoteproc节点里绑定一个interrupt属性系统启动时会自动申请对应的中断。FreeRTOS侧则需要在初始化时通过libmetal把中断回调挂上。我在代码里是这样写的metal_softirq_init(); metal_softirq_register(IPI_IRQ_ID, remoteproc_ipi_handler);这里的IPI_IRQ_ID要根据你的BSP实际情况来。有的版本用SGI 6有的用SGI 7如果你改过GIC配置这个值就得跟着变。调这块的时候有一个笨办法就是在中断处理函数第一行打一个调试状态把全局变量置1然后在主循环里轮询这个变量只要能置位就说明中断路径通了。4. Linux侧工程搭建4.1 内核要打开哪些开关Linux侧的第一步不是写代码而是确认内核配置项都打开了。跟OpenAMP相关的配置主要是remoteproc和rpmsg。CONFIG_REMOTEPROCy CONFIG_ZYNQ_REMOTEPROCy CONFIG_RPMSGy CONFIG_RPMSG_VIRTIOy CONFIG_RPMSG_CHARy其中CONFIG_RPMSG_CHAR会生成/dev/rpmsg0这样的字符设备节点方便应用层直接读写。如果你看到/dev下面没有rpmsg节点先回来查这个配置。设备树里则要添加remoteproc节点remoteproc0 { compatible xlnx,zynq-remoteproc; reg 0x30000000 0x01000000; core 0; vring0 0x3F002000; vring1 0x3F004000; reserved-mem rproc_0_reserved; interrupts 0 29 4; interrupt-parent intc; };有些旧版本的内核用的是interrupts属性指定IPI中断号新版本则走mbox或者直接靠smp genpd这个要看内核版本。核心是让内核知道远程核的固件加载到哪、vring在哪、共享内存区域在哪、中断用哪个。4.2 Linux侧的echo_test应用Xilinx官方在open-amp仓库里提供了echo_test.c它主要是通过libmetal映射共享内存然后创建一个rpmsg endpoint向FreeRTOS侧发送字符串远端收到后再返回应用侧做比对。完整的代码确实有些长度我直接把核心骨架写一下struct _msg { uint32_t len; char data[]; } __attribute__((packed)); int main(void) { struct metal_init_params metal_param METAL_INIT_DEFAULTS; struct rpmsg_endpoint ept; char msg[64]; int ret; metal_init(metal_param); rpmsg_init(rproc, rpdev, NULL); rpmsg_create_ept(ept, rpdev, echo, 0, 0, NULL, NULL); while (1) { sprintf(msg, hello from linux, count%d, count); ret rpmsg_send(ept, msg, strlen(msg) 1); if (ret 0) { printf(send failed: %d\n, ret); break; } ret rpmsg_recv(ept, rbuf, sizeof(rbuf), len, 0); if (ret 0) ... } }这段代码里面有几个关键点。rpmsg_create_ept里的通道名“echo”必须和FreeRTOS侧注册的通道名一致否则两边无法配对。rpmsg_send是异步的数据会先进入virtqueue然后由逻辑层通过IPI通知远程核所以别指望send一返回对方就已经执行了。实际项目里如果接收逻辑复杂应该把rpmsg_recv放到一个独立的线程里不要在主循环里阻塞否则主循环一旦跑在UI刷新或其他的事情上消息处理就卡住了。4.3 直接用rpmsg字符设备更省事如果你不想在Linux侧引入libmetal的复杂依赖还可以用内核提供的rpmsg字符设备接口。前提是内核打开了CONFIG_RPMSG_CHAR。系统起来之后如果通道建立成功/dev下会出现rpmsg0节点你用普通的open/read/write就能和远程核通信。int fd open(/dev/rpmsg0, O_RDWR); write(fd, hello, 6); read(fd, buf, sizeof(buf));这种方式的优点是代码简单缺点是灵活性低只能做无连接的消息读写没法精细化控制endpoint而且通道的建立时机要依赖驱动加载顺序。对于原型验证和测试工具来说字符设备接口完全够用。我自己在业务落地时用的是libmetal方式但调试期经常直接用/dev/rpmsg0做连通性验证两个方案不冲突。5. FreeRTOS侧工程搭建5.1 在Vitis里创建FreeRTOS工程FreeRTOS侧的搭建我用的是Vitis自带的软件平台方式。先通过Vivado导出XSA文件然后在Vitis里创建一个standalone Board Support PackageOS Selection选FreeRTOS。接着把openamp和libmetal的库添加进BSP设置里注意在Library列表里勾选libmetal和open_amp这样BSP会自动把相关头文件和源码打进工程。如果你用的Vitis版本比较老找不到open_amp库选项可以手动下载open-amp和libmetal源码放到工程src目录下直接编译。Xilinx官方在GitHub上的libmetal和open-amp虽然支持多平台但Zynq相关的初始化代码通常位于libmetal/lib/system/generic/zynq_a9目录下别放错位置。FreeRTOS的堆大小建议调大一点我一般把configTOTAL_HEAP_SIZE设成64KB或者更大。OpenAMP的virtqueue和内存映射在初始化时会动态分配一些内存堆太小直接启动失败而且这种失败往往没有任何日志排查起来相当费劲。5.2 FreeRTOS侧main与hardware_initFreeRTOS侧的程序入口和普通裸机不一样它要先完成硬件初始化再启动调度器。整体顺序我建议这样做int main(void) { // 1. 初始化BSP init_platform(); // 2. 配置共享内存和MMU属性 metal_phys_to_virt(SHARED_BUF_ADDR); // 3. 初始化OpenAMP metal_init(); remoteproc_init(rproc, rproc_ops, NULL); remoteproc_load(rproc, (const void *)RSC_TABLE_ADDR, 0, NULL); remoteproc_start(rproc); // 4. 创建通信端点注册回调 rpmsg_init(rpdev, NULL, NULL); rpmsg_create_ept(ept, rpdev, echo, 0, 0, rpmsg_callback, NULL); // 5. 启动FreeRTOS调度器 vTaskStartScheduler(); while (1); }这里有两个容易出错的地方。第一init_platform里的DDR初始化不是可选项。很多人直接把裸机工程的init_platform复制过来里面的DDR初始化其实必须跑一遍尤其当你从SD卡持载裸机程序而不是由FSBL引导时如果不手动初始化DDR控制器后续访问共享内存地址全是总线错误。第二共享内存区域的缓存属性配置。Zynq的Cortex-A9默认是写回缓存但核间共享内存必须保证两个CPU看到的数据是一致的。最稳妥的做法是在FreeRTOS侧把共享内存区域设置为不可缓存或者至少设置成写通缓存。我在hardware_init里用Xil_SetTlbAttributes把共享内存段设为非缓存#define SHARED_MEM_UNCACHED 0x00000000 // DEVICE/STRONGLY ORDERED Xil_SetTlbAttributes(SHARED_BUF_ADDR, SHARED_MEM_UNCACHED);有些例程还会把整个1MB共享内存全段都改成uncached牺牲一点性能换稳定性实测下来值得。5.3 链接脚本里的门道FreeRTOS固件在链接脚本里要把共享内存和资源表放到约定地址。我用的lscript.ld里内存段是这样划分的MEMORY { DDR_0 : ORIGIN 0x30000000, LENGTH 0x01000000 SHARED_MEM : ORIGIN 0x3F000000, LENGTH 0x00100000 } SECTIONS { .resource_table : { KEEP (*(.resource_table)) } SHARED_MEM .data : { *(.data) } DDR_0 }资源表一定要放到共享内存段的起始位置因为Linux侧解析固件时默认资源表在镜像头或者固定地址。如果你把resource_table放到了DDR_0段Linux侧会拿着0x3F000000的地址去读读到的要么是全0要么是乱码然后报resource table invalid。链接脚本里另一个常见坑是堆栈大小。FreeRTOS调度器启动以后任务栈是自己在FreeRTOS的堆里分配不在乎链接脚本的栈大小但OpenAMP初始化那部分代码运行在调度器之前用的还是链接脚本里的栈。如果栈太小metal_init和remoteproc_start时直接溢出表现就是莫名其妙的跑飞。我是把栈大小设到了0x10000够用且不会有明显浪费。6. 编译部署与启动验证6.1 固件编译和系统部署FreeRTOS的固件在Vitis里直接编译生成elf或bin文件。编译好之后把elf文件放到SD卡或者通过TFTP放到Linux可访问的位置。Linux侧remoteproc在start时会自动解析elf的段信息并把各段加载到对应物理地址所以用elf格式比bin更省事不建议用bin。Linux系统和根文件系统我用的是PetaLinux构建配置好上面说的设备树和内核选项然后打包BOOT.BIN和image.ub烧到SD卡。如果你想用自己手搓的Linux根文件系统也行只要保证内核模块里有remoteproc驱动的二进制并且insmod能成功即可。启动顺序有个关键点先启动Linux等Linux完全起来之后再启动FreeRTOS。也就是说FreeRTOS的固件不是放在FSBL阶段启动的而是Linux起来后由remoteproc动态加载的。如果你在FSBL里就把FreeRTOS启动了Linux启动后再去load remoteproc两边就会对共享内存的初始状态有冲突轻则握手失败重则直接死机。我自己最开始就是这样踩了一晚上。6.2 启动后的验证步骤系统起来之后按下面顺序检查# 1. 查看remoteproc设备状态 cat /sys/class/remoteproc/remoteproc0/state # 2. 加载远程固件 echo start /sys/class/remoteproc/remoteproc0/state # 3. 再次查看状态应该是running cat /sys/class/remoteproc/remoteproc0/state # 4. 查看rpmsg通道 ls /dev/rpmsg*如果状态停在offline或者启动后马上变成recovery说明固件加载或启动过程出错了。接着去内核日志看具体的报错dmesg | grep -i rproc dmesg | grep -i rpmsg正常的话FreeRTOS侧启动后会在串口打印自己的日志然后Linux侧的echo_test应用开始周期性发送数据。如果两边都正常你会看到类似这样的输出echo test: sent: hello from linux, count0 echo test: received: hello from linux, count0 echo test: sent: hello from linux, count1 echo test: received: hello from linux, count1收到的字符串和发送的字符串完全一致说明整条链路已经通了。这时候就算大功告成可以把自己协议层的东西往上叠了。7. 常见问题与排查技巧实录7.1 缓存一致性AMP最容易翻车的点我可以负责任地说OpenAMP联调里一半以上的诡异问题都出在缓存一致性上。Cortex-A9的L1是写回策略两个核各自有私有L1共享内存如果被一个核写入数据可能还留在L1 cache里另一个核去读DDR物理地址时读到的就是旧数据。表现出来的症状非常迷惑有时候数据是通的概率性地出现乱码或者Linux侧刚发完字符串FreeRTOS侧SGI中断也触发了但读到的buffer全是0。解决办法就是确保共享内存区域不被cache或者在做通信的关键路径上主动flush/invalidate。我在Linux侧用的是设备树里no-mapFreeRTOS侧用Xil_SetTlbAttributes做uncached相当于两边都从MMU层面把共享内存做成不缓存这样最干净。如果你出于性能要求必须保留cache那就要在发送数据前执行干净缓存在接收后执行失效缓存操作具体用到Xil_DCacheFlushRange和Xil_DCacheInvalidateRange注意方位别搞反发送前flush自己的写入接收前invalidate自己的读取cache。7.2 起不来、没反应的问题排查对照表现象可能原因解决办法dmesg报resource table invalid资源表地址不对或者资源表内容被破坏确认资源表链接到0x3F000000elf加载后检查内存内容FreeRTOS侧跑飞在remoteproc_start栈溢出或MMU配置错误加大链接脚本栈检查libmetal初始化Linux侧echo_test send失败rpmsg endpoint通道名不匹配确认两边名称都是“echo”或者你自己定义的名称一致数据偶发乱码缓存一致性问题共享内存段全部设为uncachedFreeRTOS的串口没有输出固件没被加载到正确地址用dmesg查看rproc加载的elf段地址确认与链接脚本一致scrub启动后卡死DDR控制器配置被Linux重新初始化覆盖确认FreeRTOS侧init_platform正确执行并且不要重复初始化DDR控制器7.3 我在调试中印象最深的三次翻车第一次翻车是低估了设备树reserved-memory的作用。我把共享内存区域留在了0x3F000000但在设备树里没写no-mapLinux启动后这块内存被伙伴系统分配出去跑了个高负载测试数据就被写乱了远程核一踩就导致内核panic。后来我在reserved-memory里加上了no-map世界瞬间清净。第二次翻车是资源表的字符设备vs no-map搞混。Vitis里生成的固件默认把resource table放在镜像开头但我的链接脚本放在了共享内存的0x3F000000两个地址不一致导致Linux解析出来的结果全是乱码。排查了很久才发现是.text段里也包含了一份资源表的拷贝而真正要mapping到共享内存的那份却穿错了section。第三次翻车比较隐蔽是GIC中断初始化顺序的问题。FreeRTOS侧如果在remoteproc启动之后才初始化GIC那么Linux侧发过来的IPI会在中断控制器未启用时直接丢失导致FreeRTOS永远等不到数据。后来我把GIC和IPI的初始化全部挪到remoteproc启动之前问题就消失了。顺序这个事OpenAMP文档里没强调但实际面向硬件的工程里它比代码本身还重要。这些坑我后来总结成一个检查单内存布局对不对、缓存属性对不对、中断顺序对不对、elf加载段对不对。每次联调出问题我第一件事不是改代码而是先跑一遍这个清单百分之七八十的问题都出在这几个点上面。ZYNQ7020的双核通信只要过了这一关后面的业务层开发就会顺利很多这也是我写下这篇文章的初衷与其让新手在同样的坑里反复打转不如一次性把这些关键路径讲透。