OpenAMP核间通信中的RPMsg协议工作机制详解
从一个常见的多核困境说起
你有没有遇到过这样的场景?在一款基于Cortex-A + Cortex-M的异构处理器上开发系统,主核跑 Linux 要处理网络和 UI,从核跑裸机负责实时控制电机。两者需要频繁交换数据——比如 A 核下发指令,M 核上报传感器状态。
最开始你可能用共享内存加轮询:A 核不断读某个地址看 M 核有没有写新数据。结果发现 CPU 占用飙升,延迟还不可控;后来改用中断通知,但很快又陷入“谁该初始化内存”“怎么避免冲突”“如何动态建通道”的泥潭。
这正是OpenAMP(Open Asymmetric Multi-Processing)想要解决的问题。它不是操作系统,也不是完整的中间件,而是一套开放标准与参考实现,目标是让不同类型的处理器——无论是运行 Linux、Zephyr 还是裸机代码——能像搭积木一样协同工作。
而在整个 OpenAMP 架构中,真正承担“传话员”角色的,就是RPMsg(Remote Processor Messaging)协议。它不显眼,却至关重要。今天我们就来彻底拆解这个嵌入式系统里的“隐形桥梁”。
RPMsg 到底是什么?别被名字吓到
先说人话:RPMsg 就是一种轻量级的“跨核聊天协议”。
想象两个住在同一栋楼里的程序员,一个在 10 楼(Host),一个在 3 楼(Remote)。他们不能直接见面,但可以往公共信箱里放纸条。信箱有编号,每人有自己的收发区,还有个铃铛用来提醒对方“有新消息了!”。
RPMsg 做的就是这件事:
- “信箱” = 共享内存
- “纸条格式” = 固定的消息结构
- “铃铛” = IPC 中断
- “收发区管理规则” = vring 队列机制
它的设计哲学非常明确:最小依赖、最大兼容。不需要 TCP/IP 协议栈,不需要文件系统,甚至连操作系统都不是必须的——只要能访问一段共享内存并触发中断,就能通信。
📌 它最早由 TI 提出,后被 Linaro 推广为 OpenAMP 标准的一部分,现已被 Linux 内核原生支持(
virtio_rpmsg_bus),也成为 Zephyr、FreeRTOS 等 RTOS 的标配组件。
RPMsg 是怎么工作的?一步步拆开看
我们不讲抽象模型,直接还原真实启动过程:
第一步:共享内存初始化 —— 先划好地盘
系统上电后,通常由主核(如 Cortex-A)先启动,在 DDR 或片内 RAM 中预留一块区域作为共享内存(Shared Memory),例如:
// 示例:保留 64KB 内存用于 OpenAMP #define SHARED_MEMORY_BASE 0x3ED00000 #define SHARED_MEMORY_SIZE (64 * 1024)这块内存会被映射到两个核心的地址空间中。注意:物理地址相同,但逻辑地址可能不同(尤其是带 MMU 的 A 核)。
接着,主核在这块内存里放置一个关键结构体:VirtIO 设备描述符。它包含:
- 设备类型(这里是VIRTIO_ID_RPMSG = 7)
- 支持的特性位
- 队列数量(通常是 2:发送和接收)
- 各队列的位置与大小
这就像是在信箱门口贴了一张说明书:“我是谁,我能干啥,我的收发区在哪。”
第二步:从核上线 —— 握手建立连接
此时,Cortex-M 核心启动,执行自己的固件代码。它知道自己应该去哪找共享内存(通过链接脚本或配置参数指定基址),然后开始解析 VirtIO 结构。
一旦识别出这是一个 RPMsg 设备,它就会做几件事:
1. 分配本地资源
2. 映射 vring 缓冲区指针
3. 注册中断处理函数
4. 设置设备状态为“驱动已加载”
这个过程完成后,主核会收到一个“设备就绪”事件。双方现在有了共同的认知基础——就像两台对讲机调到了同一个频道。
第三步:创建通信端点 —— 怎么找到对方?
这时候你想发消息了,但问题来了:你怎么知道对方有没有准备好接收?
RPMsg 引入了一个巧妙的设计:名称服务广播机制(Name Service Announcement)
当你在从核上调用:
rpmsg_create_ept(rdev, cb_func, 0x10, 0x20, "my-service");RPMsg 不仅创建了一个本地端点(endpoint),还会自动发送一条特殊消息给主核:“嘿,我这里开了个叫my-service的服务,地址是 0x10,欢迎连接!”
主核上的virtio_rpmsg_bus驱动监听着所有这类公告。一旦发现匹配的服务名,就会自动创建对应的接收端点,并通知用户空间或内核模块。
💡 这意味着你可以完全不用硬编码地址!只要两边约定好服务名,连接就能自动建立——大大降低了配置复杂度。
第四步:消息传递 —— 数据是怎么飞过去的?
假设你现在要从 M 核向 A 核发送一串字符串"Hello Linux!"。
发送流程如下:
- RPMsg 查找可用的 vring 描述符(desc entry)
- 将数据拷贝到共享内存中的缓冲区(或者使用零拷贝方式直接引用)
- 填写 desc 表项:包括地址、长度、是否链式等
- 更新
availring,标记该 buffer 可用 - 执行
kick操作 → 触发 IPI 中断 → A 核中断触发
接收流程如下:
- A 核进入 IPC 中断服务程序(ISR)
- 检查 vring 的
usedring,发现有新的已用 buffer - 根据 desc 找到数据位置,调用注册的回调函数
- 处理完后释放 buffer,更新
usedring - 发送响应(如有)
整个过程耗时极短,典型延迟在几十微秒级别,远优于 socket 或串口通信。
底层支撑:VirtIO 和 vring 到底强在哪?
很多人以为 RPMsg 是独立协议,其实它是构建在VirtIO之上的高层抽象。理解这一点,才能真正掌握其高效本质。
VirtIO:虚拟设备的标准接口
VirtIO 最初用于虚拟机与宿主机通信,但它抽象得很好:把任何远程实体都当作一个“虚拟设备”。在 OpenAMP 中,每个远程处理器就是一个 VirtIO 设备。
Linux 内核看到它时,不会关心它是 QEMU 虚拟机还是隔壁的 Cortex-M,只要符合 VirtIO 规范,就能用统一驱动加载。
vring:性能的核心秘密
vring 是 VirtIO 的核心数据结构,全称virtual ring buffer,本质上是一个无锁循环队列,由三部分组成:
| 组件 | 作用 | 操作方 |
|---|---|---|
desc[] | 描述符数组,记录 buffer 地址/长度/标志 | 双方可读,写需同步 |
avail | 可用环,生产者填写,告知哪些 desc 已放入数据 | 发送方写,接收方读 |
used | 已用环,消费者填写,告知哪些 desc 已处理完毕 | 接收方写,发送方读 |
这种设计的好处在于:
-避免竞争:双方操作不同的 ring 区域
-支持批量操作:一次 interrupt 可处理多个消息
-可扩展性强:可通过 feature bits 启用 packed ring、indirect descriptor 等高级特性
而且由于所有结构都在共享内存中,加上合理的内存屏障(memory barrier)控制,完全可以做到lock-free通信。
实战代码剖析:从裸机侧发起通信
下面这段代码是在 Cortex-M 上使用RPMsg-Lite(NXP 提供的轻量实现)的经典范例:
#include "rpmsg_lite.h" #include "metal/metal.h" struct rpmsg_lite_instance *rl_inst; struct rpmsg_lite_endpoint *ept; char *msg = "Ping from CM4"; static int app_cb(struct rpmsg_lite_endpoint *ept, void *data, size_t len, uint32_t src, void *priv) { PRINTF("Received response: %s\n", (char *)data); return RL_RELEASE; } void start_rpu_communication(void) { /* 1. 初始化 RPMsg 实例,绑定共享内存 */ rl_inst = rpmsg_lite_remote_init((void *)SHARED_MEMORY_BASE, RL_PLATFORM_IMX8MQ_M4, RL_NO_FLAGS); /* 2. 创建端点,指定服务名 */ ept = rpmsg_lite_create_ept(rl_inst, 0x10, 0x20, "my_ping_channel", app_cb, NULL); if (!ept) { PRINTF("Endpoint creation failed!\n"); return; } /* 3. 等待通道 ready */ while (!rpmsg_lite_is_link_up(rl_inst)) { __WFE(); // 低功耗等待 } /* 4. 发送消息 */ rpmsg_lite_send(rl_inst, ept, 0x20, msg, strlen(msg)); /* 5. 启动轮询或中断处理主循环 */ while (1) { rpmsg_poll(rl_inst); // 处理 incoming messages } }🔍 关键点解读:
rpmsg_lite_remote_init():表示当前是 Remote 端,主动连接 Host 初始化好的环境create_ept()使用静态地址0x10和0x20,也可设为RL_ADDR_ANY动态分配is_link_up()必须等待,否则早期发送会导致丢包rpmsg_poll()在裸机环境下替代中断下半部,定期检查是否有新消息
⚠️ 常见坑点:如果没正确设置 cache 一致性(如 A 核开了 D-cache),可能出现“明明写了数据,对方却读不到最新值”的诡异现象。务必调用
SCB_InvalidateDCache_by_Addr()或metal_cache_invalidate()。
工程实践中的五大黄金法则
要在产品级项目中稳定使用 RPMsg,光会调 API 远远不够。以下是多年实战总结的经验:
✅ 1. 共享内存规划要有前瞻性
不要只留 1KB!建议至少预留:
- 16KB 用于控制结构 + 两个 vring(默认各 16 项)
- 额外缓冲池用于大数据传输(如音频帧、图像块)
示例布局:
+-----------------------+ <- SHARED_MEMORY_BASE | VirtIO Device Config | 512 B +-----------------------+ | TX vring (desc/avail/used) | ~4KB +-----------------------+ | RX vring | ~4KB +-----------------------+ | Buffer Pool | 剩余空间,按需划分 +-----------------------+✅ 2. 中断优先级要合理设置
IPC 中断建议设为高优先级但非最高,避免阻塞更紧急的实时任务(如 PWM 故障保护)。推荐采用“中断唤醒任务”模式:
void ipc_isr(void) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; vTaskNotifyGiveFromISR(rx_task_handle, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }这样既保证及时响应,又不影响实时性。
✅ 3. 内存一致性不能忽视
在 SMP 或带 cache 的系统中,必须确保以下操作:
- 发送前:flush cache(确保数据写入 RAM)
- 接收前:invalidate cache(确保读取最新数据)
常用方法:
metal_cache_flush(buffer, len); // 发送前 metal_cache_invalidate(buf, len); // 接收前否则会出现“对方明明发了数据,我这边读出来是旧值或乱码”的问题。
✅ 4. 服务命名要有规范
建议格式:<功能>-<方向>-<序号>
例如:
-sensor-data-uplink-0
-control-cmd-downlink
-audio-stream-ch1
避免使用通用名如"channel",防止冲突。
✅ 5. 加入心跳与重连机制提升鲁棒性
RPMsg 本身没有内置保活机制。建议应用层实现简单心跳:
// 每 5 秒发送一次 void heartbeat_task(void *p) { while (1) { if (rpmsg_endpoint_is_valid(ept)) { rpmsg_send(ept, "PING", 4); } else { attempt_reconnect(); // 尝试重建通道 } vTaskDelay(pdMS_TO_TICKS(5000)); } }这对长期运行的工业设备尤其重要。
它解决了哪些传统痛点?
回顾一下,在 RPMsg 出现之前,工程师们是怎么搞核间通信的?
| 方式 | 缺陷 |
|---|---|
| 自定义共享内存 + 轮询 | CPU 占用高,延迟大 |
| 共享内存 + 中断 + 手动管理 | 易出错,移植困难 |
| 串口模拟 | 带宽低,协议开销大 |
| 私有 IPC 框架 | 锁定厂商,无法跨平台 |
而 RPMsg + OpenAMP 的组合带来了根本性改变:
✅标准化:一套 API 可用于 i.MX、Zynq、AM6x、RISC-V 多核等平台
✅自动化:服务发现、地址分配、连接建立全部自动完成
✅高性能:基于中断 + vring,延迟低至 μs 级,吞吐可达 MB/s
✅低耦合:Linux 与裸机互不干扰,各自独立升级
更重要的是,它让开发者可以专注于业务逻辑,而不是陷入底层同步细节的泥潭。
写在最后:为什么你应该关注 RPMsg?
随着 AIoT、边缘计算、车载计算的发展,异构多核已成为主流架构趋势:
- NPU + CPU
- DSP + MCU
- RISC-V Cluster + Application Core
- FPGA Soft-core + Hard-core
这些组合背后都需要可靠的核间通信机制。而 RPMsg 正是以其简洁、高效、开放的特点,成为这一领域的事实标准。
掌握 RPMsg,不只是学会一个通信协议,更是建立起一种跨域协同的设计思维——如何让不同类型、不同调度策略、甚至不同开发团队维护的软件模块安全、高效地协作。
如果你正在做以下方向的工作,强烈建议深入研究 RPMsg:
- 工业控制器(PLC、HMI)
- 智能摄像头(ISP + CPU + NPU)
- 新能源车 BMS / MCU 控制
- 嵌入式 AI 推理设备
- 开源硬件平台开发
未来属于并行,也属于协同。而 RPMsg,正是那根看不见却至关重要的纽带。
如果你在实际项目中遇到 RPMsg 初始化失败、消息丢失、cache 不一致等问题,欢迎留言交流,我们可以一起排查典型 case。