ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ZYNQ7020双核异构:Linux与FreeRTOS通过OpenAMP RPMsg通信

ZYNQ7020双核异构:Linux与FreeRTOS通过OpenAMP RPMsg通信 最近在做一个基于 ZYNQ7020 的采集板卡项目主处理器跑 Linux 负责网络和存储但传感器那边对实时性要求特别高裸机中断又有点吃紧直接把 Linux 硬扛中断实在不放心。最后就用了 PS 端双核异构的方案CPU0 跑 LinuxCPU1 跑 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 与 remoteprocOpenAMP 不是一个单一库而是三个层面的组合。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 读取 FSBLFSBL 初始化 PS、配置 DDR 和时钟再加载后续分区。做双核启动时BOOT.BIN 里至少要包含 FSBL、bitstream如果用到 PL、U-Boot 或者直接跳 Linux 的可执行文件。一个关键点FSBL 启动时默认会把两个 A9 核都从复位状态释放但如果 CPU1 上的固件还没有准备好它可能会去执行乱码地址然后挂死。所以需要修改 FSBL 代码让它在启动阶段只引导 CPU0CPU1 的释放工作交给 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: rproc3f000000 { no-map; reg 0x3f000000 0x1000000; }; elfloader_ddr: elfloader1f000000 { 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 链接脚本与内存划分两个核怎么瓜分 DDRZYNQ7020 的 DDR 空间是统一编址的两个核都能通过 AXI 总线访问。双核系统里需要把内存分成三块区域Linux 主系统内存、FreeRTOS 程序内存、共享消息内存。我在这块板上的划分方式是这样的Linux 使用 DDR 低地址区域一直到0x3effffffFreeRTOS 固件加载地址放在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_COUNTLinux 侧的设备树里也要同步指定对应的 vring 地址和大小。我用一组测试数据做个对比配置项默认值调大后效果RPMSG_BUFFER_SIZE512 字节2048 字节单次吞吐提升明显但共享内存占用增加VRING_COUNT24并发消息处理能力增强共享内存区域大小16MB32MB可支撑更大数据块但占用了 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 实验吃透再说。
返回列表