最近在做一个基于 ZYNQ7020 的采集板卡项目,主处理器跑 Linux 负责网络和存储,但传感器那边对实时性要求特别高,裸机中断又有点吃紧,直接把 Linux 硬扛中断实在不放心。最后就用了 PS 端双核异构的方案:CPU0 跑 Linux,CPU1 跑 FreeRTOS,中间数据通道走 OpenAMP 的 RPMsg。这套组合在 ZYNQ7020 上很经典,但网上能查到的资料大多是 Xilinx 官方 PDF 里的示例工程,照着复制粘贴容易,想弄清楚每一步为什么这么做,反而要翻不少文档。这篇文章就把我完整跑通 Linux 与 FreeRTOS 数据交互的过程拆开讲,包括 OpenAMP 的底层逻辑、工程搭建、代码怎么编、坑怎么踩,适合手里有 ZYNQ7020 开发板,准备上双核但还没完全捋清思路的工程师。
1. 为什么要折腾双核:ZYNQ7020 异构架构与任务分配的现实需求
1.1 PS 与 PL 之外,还有一个容易被忽略的第二个 A9 核
ZYNQ7020 实际上是两颗 ARM Cortex-A9 硬核加一片 Artix-7 可编程逻辑的组合,很多人把它当"带 FPGA 的 ARM"用,FPGA 部分做接口扩展和算法加速,A9 就随便跑个 Linux,整块芯片的资源利用率其实并不高。双核 AMP 的思路是把另一个 A9 核也拉出来干活,两个核各自运行独立操作系统,通过核间通信机制协作。
这颗芯片的两个 A9 核共享 DDR 和外设地址空间,但每个核都有自己的 GIC 中断控制器接口和私有定时器。AMP 模式下,CPU0 和 CPU1 互不干扰地跑各自系统,通信完全靠共享内存加中断。对于实时性要求高的应用,这套方案比在 Linux 里打实时补丁要简单直接得多,也比单独用一片 MCU 省掉一颗芯片的成本和布线空间。
1.2 双核分工的典型场景:Linux 管逻辑,FreeRTOS 管实时
我最开始接触双核需求,是客户要求设备既有以太网远程管理、文件读写、参数配置界面,又要对外部 ADC 数据做毫秒级响应。单纯 Linux 靠 PREEMPT_RT 补丁能改善延迟,但中断处理和内核线程调度的不确定性依然存在,万一网络流量大了导致调度抖动,数据采集就会丢点。
用双核分工之后,CPU0 上 Linux 负责网络协议栈、Web 配置页面、日志存储这些"重逻辑",CPU1 上 FreeRTOS 专门做 ADC 采样任务、电机控制、实时状态上报。两个核之间只传递精简的控制指令和数组数据,比如 Linux 下发采样率配置,FreeRTOS 回传采样数据块,数据通路就是 OpenAMP 提供的 RPMsg 通道。
这种架构还有一个隐藏好处:FreeRTOS 侧代码量小,实时任务可以做到极简设计,万一 Linux 崩了或者重启了,FreeRTOS 这侧还能独立维持安全保护逻辑,这在工业现场非常实用。
1.3 为什么不用裸机共享内存,非要引入 OpenAMP
有些人会说,双核通信无非就是两个核访问同一块内存地址,加个标志位轮询不就行了?确实,最原始的双核通信就是共享内存加标志位,但实际工程里这种方案坑很多。多核并发访问同一块内存时,需要考虑缓存一致性、内存屏障、标志位竞争、对端状态未知等问题。你往共享地址写了一个结构体,CPU1 的 D-Cache 没有失效,读出来的数据可能还是旧的,这类问题排查起来极其折磨人。
OpenAMP 把这些问题都封装好了。它基于 RPMsg 协议,实现了共享内存上的环形缓冲区管理和核间中断通知,发送方写入消息后自动触发对端中断,接收方从环形缓冲区取出消息,整个流程是标准化的消息传递模型,不需要自己去写缓存同步和并发控制代码。同时 OpenAMP 还包含 remoteproc 组件,能直接完成 CPU1 固件的加载启动,Linux 侧这一套基本是现成的驱动框架。
所以从工程效率角度,引入 OpenAMP 不是增加复杂度,恰恰是把最复杂的多核同步问题交给了成熟框架,我们只需要关注业务数据格式。
2. OpenAMP 和 RPMsg 到底是怎么把两个核"连起来"的
2.1 OpenAMP 的组成:libmetal、open_amp 与 remoteproc
OpenAMP 不是一个单一库,而是三个层面的组合。libmetal 负责底层硬件抽象,提供物理内存映射、缓存操作、原子操作、设备访问这些接口,屏蔽掉不同处理器架构的差异。open_amp 是核心通信库,实现了 RPMsg 协议、虚拟队列管理、端点管理等功能。remoteproc 则是处理器生命周期管理,负责加载远程固件、启动或停止远程处理器。
我打个比方,libmetal 相当于砖头水泥,open_amp 是管道系统,remoteproc 是装修队。装修队把房间(CPU1)布置好之后,两个房间之间的管道(RPMsg)就能正常通水了。
Linux 内核侧的 open_amp 功能已经以内核驱动形式存在,比如rpmsg、remoteproc驱动模块,FreeRTOS 侧则是以静态库形式提供,Xilinx SDK 里直接有现成的open_amp和libmetal库可以编译链接。
2.2 RPMsg 的工作流程:一个请求从 Linux 到 FreeRTOS 经过的路径
RPMsg 消息发送是一个多步骤的流水线。先用rpmsg_send()接口传入数据指针和长度,open_amp 会把消息拷贝到一块预先分配好的共享内存环形缓冲区中,然后根据目标端点的地址信息构造一个 rpmsg 头部消息体,最后触发目标核的中断。目标核的中断服务程序收到通知后,从环形缓冲区释放消息,交给注册在对应端点上的回调函数处理。
接收侧处理完数据后如果想回消息,流程完全对称,只是方向和端点地址变了。整个过程对应用层来说像两台主机之间的 socket 通信,只是物理层从网线变成了共享内存加核间中断。
2.3 双核通信中的角色划分:远程核、主核与端点到端点
在 OpenAMP 的语境里,运行 Linux 的核通常被称为主核,运行 FreeRTOS 的核被称为远程核。Linux 侧的 remoteproc 驱动是主动发起固件加载的一方,FreeRTOS 侧的 open_amp 库在启动时完成自身资源初始化后,进入等待主核控制的状态。
端点是 RPMsg 通信的基本寻址单位,类似于网络协议里的端口号。每个端点有一个名字字符串,比如测试通信时我习惯命名为echo,主核发送消息时指定对端端点名,远程核内核会根据端点名匹配对应回调函数。这里需要注意,端点的"一次发送、一次接收"是异步的,回调函数返回前应该尽量快速处理完数据,避免阻塞中断上下文太久。
3. 动手前的准备:硬件平台、工具链与源码版本选型
3.1 硬件平台怎么选:官方开发板还是自己画的核心板
做双核通信验证,我建议优先用 Xilinx 官方或者第三方厂商的 ZYNQ7020 开发套件,比如米尔、黑金、正点原子这些成熟方案的板子。原因不是自己画不了板,而是第一轮跑 OpenAMP 实验时,你需要一个已知良好的硬件参考平台,出问题能排除硬件因素。官方或大厂的开发板配套资料里一般都会给好硬件工程模板,省掉自己从头建 Block Design 的时间。
自己画核心板当然可以,但要注意两点:DDR 布线质量和 PL-PS 之间的 MIO 分配一定要对照参考设计检查。OpenAMP 实验对 DDR 时序很敏感,如果内存控制器参数配置不对,CPU1 加载后运行 FreeRTOS 可能出现随机崩溃,这个锅很容易甩给软件,最后查下来全是硬件问题。
3.2 软件栈版本选择:Vivado/SDK 版本与 Linux 内核版本的匹配
软件版本选择是很多人第一步就翻车的地方。Xilinx 官方工具链版本和 Linux 内核版本是配套发布的,比如 Vivado 2020.2 对应 linux-xlnx 5.4 分支,Vivado 2021.1 对应 5.10 分支,SDK 里自带的 FreeRTOS BSP 和 open_amp 库也是和工具链版本绑定的。
我的建议是不要追求最新版本,而是选一套你熟悉的稳定组合。我自己一直在用 Vivado 2020.2 加 linux-xlnx 5.4,这套组合在 ZYNQ7020 上跑 OpenAMP 非常成熟,网络上有大量问题记录可以参考。如果你想用更新的 Vitis 版本,也可以,只要保证三个部分版本匹配:硬件工程导出到 SDK 的 hdf 文件、内核源码分支、FSBL 源码。
这里有个实操经验:把 Xilinx 官方提供的uart16550、gem这些外设驱动和主内核源码统一从 xlnx-rebase 分支编译,不要混用主线内核的驱动,否则经常会出现外设驱动和设备树节点不匹配的问题。
3.3 我踩过的一个坑:工具链版本不匹配导致 OpenAMP 库编译失败
这个坑我印象特别深。最开始我电脑上装了 Vivado 2019.2,但内核源码用的是 linux-xlnx 5.4,结果 SDK 生成的 open_amp 库编译倒是过了,但 FreeRTOS 工程跑起来后在rpmsg_init阶段直接硬死机。后来查了半天,发现是 libmetal 库的编译工具链版本和硬件工程导出的 xparameters.h 文件版本不一致,两边的结构体定义和宏定义对不上造成的。
所以强烈建议:在首次创建 FreeRTOS 工程时,就用工程里自带的 BSP 选项里 "Generate OpenAMP and libmetal libraries" 自动生成这两个库,不要手动去 github 拉最新源码替换。等你能跑通一个最小 echo 实验之后,再去折腾替换更高版本的库。
另外一个容易忽略的点是交叉编译器的 sysroot 路径。SDK 自带的 gcc-arm-none-eabi 工具链和内核交叉编译用的 aarch64-linux-gnu 或 arm-linux-gnueabihf 不是同一套,FreeRTOS 侧必须用前者,Linux 侧用后者。如果你在 Makefile 里配错路径,轻则编译报错,重则链接出来一个根本运行不了的 ELF。
4. 第一步:在 CPU0 上跑起 Linux 并把 CPU1 隔离出来
4.1 生成 BOOT.BIN 时需要注意的信任链与启动顺序
ZYNQ7020 的启动流程是 BootROM 读取 FSBL,FSBL 初始化 PS、配置 DDR 和时钟,再加载后续分区。做双核启动时,BOOT.BIN 里至少要包含 FSBL、bitstream(如果用到 PL)、U-Boot 或者直接跳 Linux 的可执行文件。
一个关键点:FSBL 启动时默认会把两个 A9 核都从复位状态释放,但如果 CPU1 上的固件还没有准备好,它可能会去执行乱码地址然后挂死。所以需要修改 FSBL 代码,让它在启动阶段只引导 CPU0,CPU1 的释放工作交给 Linux 侧的 remoteproc 驱动。
修改办法是在 FSBL 的main.c里找到对应的 CPU1 启动处理函数,按官方文档注释掉或者条件编译掉 CPU1 的启动动作。在你用 SDK 生成 FSBL 工程时,里面的qspi、sd相关的启动文件里其实已经有双核处理的框架,需要仔细看一下FsblHookBeforeHandoff这类回调函数,在里面显式阻止 CPU1 启动即可。
4.2 设备树里如何声明 remoteproc 节点
等到 Linux 跑起来了,下一步要让内核知道 CPU1 存在,以及预留哪段内存给共享通信。这些信息通过设备树传递。我在设备树里加了这样一段:
reserved-memory { #address-cells = <1>; #size-cells = <1>; ranges; rproc_0_reserved: rproc@3f000000 { no-map; reg = <0x3f000000 0x1000000>; }; elfloader_ddr: elfloader@1f000000 { no-map; reg = <0x1f000000 0x1000000>; }; }; remoteproc0 { compatible = "xlnx,zynq-remoteproc-1.0"; reg = <0x0 0x10000000>; vring0 = <0x3f000000>; vring1 = <0x3f008000>; memory-region = <&elfloader_ddr>, <&rproc_0_reserved>; interrupt-parent = <&intc>; interrupts = <0 29 1>; };这里核心是reserved-memory节点。no-map属性表示这段内存不归 Linux 页表管理,避免 Linux 把它分配出去后两个系统互相踩踏。remoteproc0节点里的vring0和vring1指定了 RPMsg 虚拟环形缓冲区在共享内存区域中的位置,memory-region则告诉 remoteproc 驱动的固件加载地址和共享内存范围。
设备树地址的具体数值要跟 SDK 里的 FreeRTOS 链接脚本保持一致,这是最容易出错的地方,我在下一步也会强调。
4.3 内核配置中必须打开的开关
内核编译时,有几个配置项是 OpenAMP 运行的必要条件:CONFIG_REMOTEPROC、CONFIG_RPMSG、CONFIG_RPMSG_VIRTIO、CONFIG_VIRTIO、CONFIG_VIRTIO_MMIO。如果使用 Xilinx 官方内核,还需要确认CONFIG_ZYNQ_REMOTEPROC是否被自动选上。
我建议直接用make xilinx_zynq_defconfig作为基础配置,再通过make menuconfig检查上述配置是否开启。不要从空配置开始手选,Xilinx 的 defconfig 里已经包含了大量板级支持,手动选很容易漏掉文件系统、网络驱动等关键模块。
配置完成后,重新编译内核和设备树,替换到启动卡上,然后确认系统启动后能出现/sys/class/remoteproc/remoteproc0这个目录。如果没出现,说明 remoteproc 驱动没绑定成功,优先检查设备树 compatible 字段和驱动名称是否匹配。
5. 第二步:在 CPU1 上移植 FreeRTOS 并加载 OpenAMP 库
5.1 直接用 Xilinx SDK 生成 FreeRTOS 工程还是自己移植
FreeRTOS 侧的代码可以用两种方式组织:第一种是在 SDK 里新建空应用工程,然后在 BSP 设置里选择freertos10_xilinx版本,再添加open_amp和libmetal库支持,这种方式最简单,适合快速验证;第二种是从 FreeRTOS 官方源码手动移植,这种方式更灵活,能通配非标准硬件环境,但工作量大很多,涉及定时器、中断控制器、串口驱动适配。
我的建议是第一次跑通实验,直接走 SDK 自动生成路线。SDK 创建 BSP 时,在 "Board Support Package" 配置面板中找到 "open_amp" 选项并勾选,SDK 会自动把对应的 open_amp 库源文件添加到工程里,并且会自动链接 libmetal。这样 FreeRTOS 侧的初始化代码基本只需要关注业务回调函数。
等你对整个流程完全理解后,再考虑把代码迁出手动构建,这样万一以后要换平台,也有经验可循。
5.2 链接脚本与内存划分:两个核怎么瓜分 DDR
ZYNQ7020 的 DDR 空间是统一编址的,两个核都能通过 AXI 总线访问。双核系统里需要把内存分成三块区域:Linux 主系统内存、FreeRTOS 程序内存、共享消息内存。
我在这块板上的划分方式是这样的:Linux 使用 DDR 低地址区域一直到0x3effffff;FreeRTOS 固件加载地址放在0x1f000000,大小 16MB,用于存放整个 RTOS 镜像的代码和数据;共享内存放在0x3f000000,大小 16MB,其中前 64KB 给 RPMsg 环形缓冲区用,剩下的作为实际业务消息池。
对 FreeRTOS 侧来说,链接脚本里_vector_table、_stack_start、_heap_start这些符号的地址必须落在固件加载区域内。SDK 生成的 lscript.ld 里默认地址很可能不符合你的划分方案,需要手动改。我直接把原文件里的DDR_BASE_ADDRESS改成0x1f000000,把DDR_SIZE改成0x1000000,然后再编译,保证生成的 elf 文件各个段都落在预留区域内。
这一步是整个双核通信里最容易出问题的环节,建议编译完成后用arm-none-eabi-readelf -l查看 ELF 的段加载地址,确认没有段超出范围。
5.3 编译 FreeRTOS 侧 OpenAMP 库的步骤
如果使用 SDK 自动生成,编译 open_amp 库基本是点几下按钮的事。在 BSP 配置里勾选 open_amp 后,SDK 会生成libopen_amp.a,同时xil库、xilffs、xilrsa等基础库也会一起编译。你需要做的只是把库添加到应用工程的链接选项里。
如果是手动编译,需要注意 open_amp 的编译选项里要开启-DOPENAMP_HOWTO_RSC_TABLE之类的宏定义吗?没必要,直接用 Xilinx 提供的 Makefile 即可。手动场景我踩过的坑主要是没有指定METAL_INCLUDE_DIR或者对libmetal编译时用了错误的 CPU 架构选项,导致编译出的库在运行时发生 HardFault。
编译成功后会生成echo_test或者你自己的应用 ELF,这个 ELF 需要拷贝到 Linux 的文件系统中,默认放在/lib/firmware目录下,文件名要跟设备树里 remoteproc 驱动期望的固件名一致,一般是rproc-remoteproc0-fw。
6. 第三步:编写双核通信代码,从 echo 实验开始
6.1 FreeRTOS 侧:初始化 RPMsg 并注册回调函数
FreeRTOS 侧的代码核心是:系统启动后初始化 open_amp,然后创建一个名为echo的 RPMsg 端点,注册好回调函数,最后进入一个循环保持任务存活。下面是一段能工作的最小化 FreeRTOS 侧代码结构:
#include <openamp/open_amp.h> #include <metal/alloc.h> #include "xil_cache.h" static struct rpmsg_endpoint echo_ept; static struct rpmsg_device *rpmsg_dev; static void echo_rpmsg_cb(struct rpmsg_device *rdev, void *data, int len, void *priv, unsigned long src) { struct rpmsg_endpoint *ept = priv; if (ept == &echo_ept) { /* 原样回传,用于测试 */ rpmsg_send(ept, data, len); } } static void rpmsg_service_unbind(struct rpmsg_endpoint *ept) { /* 反注册时资源清理 */ }主任务里初始化并注册端点:
int app_main(void) { struct metal_init_params metal_params = METAL_INIT_DEFAULTS; unsigned int tmp; int ret; metal_init(&metal_params); ret = rpmsg_initialize(NULL, NULL, &tmp); if (ret) { return ret; } rpmsg_endpoint_init(&echo_ept, "echo", echo_rpmsg_cb, rpmsg_service_unbind); rpmsg_create_ept(&echo_ept, rpmsg_dev, "echo", RPMSG_ADDR_ANY, RPMSG_ADDR_ANY, echo_rpmsg_cb, rpmsg_service_unbind); while (1) { /* 任务调度由 FreeRTOS 管理,这里保持主循环存在 */ vTaskDelay(pdMS_TO_TICKS(100)); } }注意rpmsg_create_ept调用时的回调函数和端点名,必须跟在 Linux 用户空间打开的设备端点匹配,如果名字不一致,或者地址绑定错误,消息就找不到回调函数,直接丢包。在 FreeRTOS 端的调试串口,我建议在echo_rpmsg_cb里加一个简单的xil_printf打印收到的数据,方便第一时间判断消息是否到达。
还有一个细节:FreeRTOS 侧的rpmsg_initialize在 Xilinx SDK 的例程中经常有额外的资源表参数,如果传入 NULL,驱动会去默认地址找资源表。这个默认地址要在platform.h或链接脚本里和rsc_table定义保持一致。
6.2 Linux 侧:用标准文件操作读写 /dev/rpmsg0
在内核 rpmsg_char 驱动加载成功后,系统会创造/dev/rpmsg_ctrl0和/dev/rpmsg0这样的设备节点。/dev/rpmsg_ctrl0用于创建新的端点,/dev/rpmsg0是默认端点。最简单的方式是直接用 open/read/write 操作访问:
#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #include <errno.h> int main(void) { int fd; char buf[256]; int ret, len; fd = open("/dev/rpmsg0", O_RDWR | O_NONBLOCK); if (fd < 0) { perror("open"); return -1; } strcpy(buf, "hello from linux"); len = strlen(buf) + 1; ret = write(fd, buf, len); printf("write ret: %d\n", ret); memset(buf, 0, sizeof(buf)); ret = read(fd, buf, sizeof(buf) - 1); if (ret > 0) { printf("recv: %s\n", buf); } else { printf("no data yet, errno=%d\n", errno); } close(fd); return 0; }这里面有个关键点:打开文件时用了O_NONBLOCK。RPMsg 的 read 是异步的,如果对端没有回包,开启阻塞模式后进程会一直挂起等待,不利于测试脚本写。用非阻塞模式后,没数据时read会立即返回-1并设置EAGAIN,你可以多次尝试读取。
在 Linux 侧触发 CPU1 固件加载最简单的方式是:
echo start > /sys/class/remoteproc/remoteproc0/state执行这个命令后,remoteproc 驱动会从/lib/firmware目录加载固件、解析 ELF、搬运到内存地址,然后释放 CPU1。执行完后,检查一下:
cat /sys/class/remoteproc/remoteproc0/state显示running就说明固件已经跑起来了。
6.3 测试结果与验证方法
一个完整的 echo 测试流程是:先启动 FreeRTOS 固件,让echo端点处于监听状态;然后在 Linux 端写一个字符串过去;如果数据通道正常,FreeRTOS 回调函数会把同样的字符串回传回来,Linux 侧 read 就能读到。
我第一次测试时,FreeRTOS 串口打印出收到的字符串内容,Linux 侧也成功打印了回包,那一刻说明整个链路已经全部打通。到了这个阶段,双核通信的最小框架就已经成立,后面再往共享内存里塞具体的业务数据结构,就只是内存布局和协议设计的问题了。
为了验证稳定性,我用一个脚本连续发送 10000 条不同长度的消息,然后统计回包的正确率和总耗时。这里建议用一个简单的序列号加 CRC 校验的协议,能快速发现丢包和数据错位问题。
7. 常见排错与调试追踪:双核通信的问题排查链路
7.1 现象一:Linux 启动时 remoteproc 加载固件失败
如果你执行echo start > /sys/class/remoteproc/remoteproc0/state时,报类似remoteproc: Failed to load firmware的错误,第一步先确认固件 elf 文件的路径和名字是否正确。Linux 的 remoteproc 框架默认会找固件名,你可以通过以下命令查看:
ls /lib/firmware/固件缺失的问题大概率是设备树里firmware-name属性没配对。有的设备树版本需要显式配置:
firmware-name = "rproc-remoteproc0-fw";另外还要检查 elf 文件的架构是否为 ARM 32 位。如果你误编了一个 aarch64 的镜像,remoteproc 在解析 ELF 头时就会失败。用file命令看一眼:
file /lib/firmware/rproc-remoteproc0-fw输出里应该有ELF 32-bit LSB executable, ARM字样,如果是ARM aarch64,就说明你在 FreeRTOS 侧用了错误的交叉编译器。
7.2 现象二:FreeRTOS 端收不到消息,但 Linux 端显示发送成功
这个是双核通信里最让人头疼的问题之一。Linux 端的 write 返回了成功,但是 FreeRTOS 串口没有任何打印。我遇到这种情况,第一步会查中断有没有触发。在 FreeRTOS 端的中断服务函数里加一个标志位打印,如果中断没进来说明核间中断配置有问题,或者 GIC 中断号不对。
第二步查共享内存地址是否一致。FreeRTOS 侧代码里资源表中声明的共享内存地址和 Linux 设备树reserved-memory区域必须一模一样。我出过一个问题:Linux 设备树里共享内存写在0x3f000000,但 FreeRTOS 链接脚本里的共享地址是0x3f200000,差了 2MB,结果每次写入都进了一个没有映射的区域。
第三步看数据是否被 D-Cache 吃掉了。ARM Cortex-A9 的 D-Cache 默认是开着的,两个核共享内存通信时必须对相应区域执行 flush 和 invalidate 操作。在 FreeRTOS 侧,回调函数收到消息前最好调用:
Xil_DCacheInvalidateRange((INTPTR)buffer, len);发送数据前调用:
Xil_DCacheFlushRange((INTPTR)buffer, len);Xilinx 的 open_amp 库在内部通常会处理,但如果你改了自己的数据拷贝路径,这步必须做好。我就是因为自己写了一个 memcpy 把数据从 RPMsg 缓冲区拷到了业务结构体,结果忘记 flush,缓存里的数据一直没写回 DDR,另一端怎么都读不到。
7.3 现象三:偶尔一次通信失败,怀疑是内存访问冲突
双核通信非常怕一件事:两个核同时访问共享内存,导致数据踩踏。RPMsg 机制本身有环形缓冲区的读写指针管理,但如果两端对端点的访问频率过高,或者缓冲区过满,就可能出现消息丢失。这类问题的排查思路是用压力测试,逐步增加消息长度和频率,找到丢包拐点。
在我这边,加大消息频率后遇到的丢包问题是共享内存区域大小和 vring 数量不匹配导致的。默认vr0、vr1两个虚拟队列共享一个 64KB 的区域,如果单条消息超过缓冲区容量,写入就会失败。解决办法是增大 vring 区域,或者把消息拆成更小的数据包。
另外建议不要用两个核同时通过 AXI 总线访问同一个非原子变量做标志位。RPMsg 已经帮你管理好了环形缓冲区,你对业务数据做保护时尽量用 open_amp 提供的端点级锁机制,而不是自己裸写一个全局变量锁。
7.4 调试工具的选择:XSDB、串口日志与逻辑分析仪
双核调试和单核调试的差异很大,因为两个核同时在跑,你用 GDB 连上 CPU0 去打断点,CPU1 还在独立运行,两者之间的交互情况很难直观观察。我常用的组合是:XSDB 脚本用来启动、停止单个核,查看寄存器和内存;串口日志分别接两路,Linux 输出一路,FreeRTOS 输出一路;逻辑分析仪用来抓核间中断 GPIO 信号,确认中断事件的时间戳。
XSDB 是一个命令行工具,通过 JTAG 连接到 ZYNQ7020。调试时我可以分别用targets命令查看两个核的状态,用mrd命令直接读共享内存地址。如果怀疑 FreeRTOS 侧的数据缓冲区内容有问题,直接在 XSDB 里读那块地址,和 Linux 侧打印的内容比对,就能快速定位是不是缓存一致性问题。
我一直保留着一个习惯:双核调试时不要只靠单点的 log,尽量在两侧同时打印关键事件和计数器。比如发送侧每次发送后递增一个本地计数器,接收侧每次收到后也递增一个计数器,然后通过共享内存定期交换计数值,这种双向印证比单纯看一侧日志高效得多。
8. 性能调优与后续扩展想法
8.1 影响吞吐量的关键参数:buffer size 与 ring buffer 数量
数据吞吐量主要由三个参数决定:单个消息缓冲区大小、RPMsg 环形缓冲区总数、以及核间中断频率。open_amp 的默认配置一般能满足中小数据量的交互,但如果你的业务是高清图像或者大数据块传输,就需要调大参数。
调参不是只改一个宏的事。FreeRTOS 侧open_amp库里的rpmsg_config.h中定义了RPMSG_BUFFER_SIZE和VRING_COUNT,Linux 侧的设备树里也要同步指定对应的 vring 地址和大小。我用一组测试数据做个对比:
| 配置项 | 默认值 | 调大后 | 效果 |
|---|---|---|---|
| RPMSG_BUFFER_SIZE | 512 字节 | 2048 字节 | 单次吞吐提升明显,但共享内存占用增加 |
| VRING_COUNT | 2 | 4 | 并发消息处理能力增强 |
| 共享内存区域大小 | 16MB | 32MB | 可支撑更大数据块,但占用了 Linux 可用内存 |
调参后建议重新做一遍压力测试,因为缓冲区变大后,CPU1 的内存占用会增大,如果超出了链接脚本分配的堆空间,运行时照样会崩。
8.2 从轮询到中断的实时性优化
RPMsg 本身就支持核间中断通知,应用层用rpmsg_send发送时底层会自动触发对方中断,不需要应用层额外处理。但有一种情况需要优化:FreeRTOS 侧任务通过轮询方式读取共享内存中的业务数据,这在高频率场景下会很浪费 CPU。
我的做法是把共享内存数据到达事件直接映射到 FreeRTOS 的二值信号量,在 RPMsg 回调函数中只做xSemaphoreGiveFromISR操作,把耗时的数据处理工作放到高优先级任务里。这样中断服务函数保持极短,数据处理的实时响应也能得到保证。
另外,Linux 侧如果读到的数据是高频的,可以考虑使用poll或epoll机制监控 rpmsg 设备文件的可读事件,而不是忙等 read。这样 Linux 侧的 CPU 占用也能降下来。
8.3 后续可以尝试的方向:OpenAMP 上的 TTY 服务和自定义协议
OpenAMP 的 RPMsg 不止能传自定义二进制数据,还可以封装成更上层的服务。Xilinx 的 demo 里有个rpmsg_tty服务,可以在远程核上实现一个类似串口终端的功能,Linux 侧挂载一个虚拟串口,这样调试输出就能直接通过 /dev/ttyRPMSG 访问,非常方便。
我在实际项目里就是在 RPMsg 之上封装了一层简单的请求响应协议,包括消息类型、长度、版本号、校验字段。这样一来,Linux 侧写一个结构体发过去,FreeRTOS 侧解析字段后执行对应动作,再回传结果。这套协议比直接传裸字符串要可靠得多,也方便以后加命令类型时做兼容。
如果想更进一步,可以在 FreeRTOS 侧跑一个最小化的 TCP/IP 栈,把网络数据通过 RPMsg 透传到 Linux 侧处理,实现"双核网卡"的方案。不过这个对内存规划要求比较高,建议先把基础的 echo 实验吃透再说。