找回密码
立即注册
搜索
发回帖 发新帖

5418

积分

0

好友

698

主题
发表于 1 小时前 | 查看: 8| 回复: 0

什么是 RPC?

RPC(Remote Procedure Call,远程过程调用) 是一种分布式计算协议。它允许一台机器上的程序像调用本地函数一样,去执行另一台远程机器上的函数,开发者不用关心底层网络通信细节。

RPC 框架的大致思路

RPC 框架通过 IDL 定义接口,自动生成客户端 Stub 和服务端 Skeleton;客户端 Stub 将函数调用和参数序列化,经传输层发给服务端;服务端 Skeleton 反序列化消息,路由到对应的业务函数执行,再把结果序列化返回;框架还提供消息编号、超时、错误处理能力,让远程通道上的调用像本地函数一样使用。

RPC远程过程调用原理时序图,展示客户端Stub与服务端Skeleton的交互流程

RPC 框架重要组成部分:

1. IDL(接口定义语言)

统一描述服务、方法、入参、出参的数据结构,是客户端与服务端的契约。如 protobuf 的 .proto 文件也是一种 IDL 文件。RPC 的 IDL 可能如 .erpc。可通过 IDL 编译器自动生成 Client Stub 和 Server Skeleton 代码。

eRPC IDL代码生成流程:从手写接口定义到自动生成客户端Stub和服务端Skeleton

2. Stub(客户端桩代码)

由 IDL 工具自动生成,放在调用侧。

内部大致实现:

  • 组装请求包(方法 ID、序列号、参数)
  • 调用底层 transport 发送消息
  • 阻塞 / 异步等待响应包
  • 反序列化响应,返回结果 / 错误

3. Skeleton(服务端骨架代码)

IDL 工具生成,放在服务端。负责:

  • 接收 transport 上来的原始消息
  • 反序列化,解析出调用的方法 ID、参数
  • 根据方法 ID,路由到用户注册的业务函数
  • 执行业务函数后,打包返回值 + 错误码
  • 通过 transport 发回响应

Stub/Skeleton 就是 RPC 的胶水层,隔离业务代码和网络 / 总线。

4. 序列化 / 反序列化模块

把内存中的结构体、变量变成连续二进制字节流(序列化);收到字节流还原成内存数据(反序列化)。

可选方案:

  • Protobuf / nanopb(嵌入式最常用,紧凑二进制)
  • JSON(文本,体积大,MCU 一般不用)
  • FlatBuffers(零拷贝,适合高性能嵌入式)

5. Transport 传输层

负责原始字节的收发,RPC 框架把传输抽象成接口,上层 RPC 不用关心是 UART、CAN、RPMSG、TCP、UDP 等。

什么情况下用 RPC?

一些框架 / 工具,总有其适用及不适用的场景。没有最好,只有更好。

嵌入式中,什么场景下用 RPC?

RPC 比较适合需要跨芯片、跨核、跨板双向调用函数,接口多、经常迭代,不想手写大量底层帧解析、命令编码、CRC、应答逻辑的场景。因为 RPC 可以直接定义接口如 read_temp(),调用就像普通函数,序列化自动处理,新增接口只需要更新 IDL,不用手动写报文解析。

不适用微秒级硬实时、极小资源 MCU 场景。因为 RPC 有序列化 / 反序列化开销、内存分配,延迟不确定;完整的 Protobuf 开销太大,序列化会占用栈 / 堆;这种场景一般手写简单帧协议或者极简二进制协议。

下面用一个嵌入式板间控制的例子,直观对比用 RPC、不用 RPC 的方式。

主控 MCU 通过 UART 控制驱动板,如:需要设置电机速度和读取状态等。这个场景是典型的命令-响应、低频控制面交互,很适合 RPC。

1. 方案 1:不用 RPC,手写通信协议

主控和驱动板双方要自己约定帧格式,例如:

帧头 0xAA55 | 命令字 | 长度 | 负载 | CRC16
命令字 0x01 = SetSpeed
命令字 0x02 = GetStatus

主控端要写对应的设置 / 发送函数:

主控端手写帧打包、CRC校验与UART发送代码

驱动板端还要写对应的接收解析函数:

驱动板接收状态机、命令分发与结果序列化代码

两端要单独写对应的协议组包解析、CRC 校验、异常处理等;每新增一条指令,两端都要同步改:命令字、解析代码、回复代码等。比较繁琐也比较容易出错。而且接口没有契约,也不方便理解代码。

我们上一篇文章随笔 | 会调 AI 之后,嵌入式工程师还剩什么?中也提到:AI 时代,我们也要注重一些相关知识的沉淀。这里就是一个例子——如果不了解 RPC,可能在上面这个场景中,给的提示词里没有明确提到 RPC 方案,那 AI 实现出来的可能就是方案一。虽然也能用,但可能不是最优解。

除此之外,有个小技巧分享给大家:对一些东西不熟悉的时候,也可以先让 AI 出一些方案对比,让多个 AI 出多个方案,最后综合选择比较合理的那个,顺便还能大致学习各个方案的基础原理。

2. 方案 2:用 RPC,定义一次接口,自动生成两端代码

用 EmbedXrpc 这类工具,需要针对应用先写一份 IDL,例如:

EmbedXrpc IDL中结构体与委托定义示例

通过配套工具把 IDL 文件生成对应的 .c 与 .h。

主控端业务代码变成:

RPC主控端像调用本地函数一样调用远程接口

驱动板端只需注册实现:

RPC驱动板端业务函数注册与实现

RPC 的方式底层仍然走 UART,但打包、解包、序列化、命令分发、超时 / 错误处理由 RPC 运行时和生成代码统一完成。

以上手写通信协议的方式,如果封装得足够好,通信运行时能力可以做到比较接近 RPC 的方式,但是接口契约还是 RPC 的方式更好。

成熟的 RPC 框架——EmbedXrpc

GitHub 仓库:https://github.com/EmbeddedRPC/erpc

eRPC GitHub仓库页面,包含star数、贡献者与版本发布信息

eRPC客户端服务端架构层级图,展示从应用到传输层的数据流向

eRPC(Embedded RPC) 是 NXP 开源、面向嵌入式紧密耦合系统的轻量级 RPC 框架,专门用于异构多核 SoC、多芯片板间通信,不面向互联网大规模分布式网络场景。

erpcgen IDL 编译器:

IDL 文件后缀 .erpc,用来定义接口、枚举、数据类型;erpcgen 工具读取 IDL,自动生成客户端 stub(shim 桩代码)、服务端 skeleton 骨架代码。例如:

// 简单 idl 示例
program led_demo;

interface LedService{
    led_on(in uint8 led_id) -> void;
    led_off(in uint8 led_id) -> void;
}

关键特性:

  • 轻量,极小代码占用,适配 Cortex‑M 等资源受限 MCU
  • 传输层完全抽象,可快速切换 UART / RPMSG / TCP
  • 原生 C / C++,可生成 Python 代码,方便上位机和设备联调
  • 支持服务端异步通知(server 主动推送消息给 client)
  • 支持同步阻塞调用、异步回调调用
  • 不需要修改原有 C 函数原型,方便把现有业务函数导出为 RPC 接口

EmbedXrpc 的使用

eRPC demo运行日志,完整记录add与echo调用的帧追踪和消息解析过程

上一节把 eRPC 的能力过了一遍,这一节讲怎么真正用起来。

1. 构建 erpcgen

eRPC 仓库里其实是两样东西:erpc_c 是最终编译进 MCU 的运行时,拷进工程就能用;erpcgen 是跑在 PC 上的 IDL 编译器,得先构建出来。

  • Windows:先跑仓库里的 install_dependencies.ps1,装好本地 MinGW、bison、flex,再用 cmake --preset mingw64 生成工程并构建;也可以直接用 erpcgen/VisualStudio_v14 里的 VS 工程。
  • Linux / macOS:跑 install_dependencies.sh 装依赖,之后 make erpcgen 或走 CMake 目标构建;make install 之后 erpcgen 就是全局命令了。

构建完成后执行 erpcgen -h 能看到选项列表,工具就算就绪。

2. 写 IDL

IDL 里只描述接口,不出现任何通信细节:

// demo_service.erpc
interface DemoService
{
    // 有返回值:客户端阻塞等回复
    add(uint32 a, uint32 b) -> uint32
    echo(uint32 value) -> uint32

    // oneway:只发不收,服务端不回复
    oneway notify(uint32 value)
}

生成 C / C++ 胶水代码:

erpcgen -gc -o ./generated demo_service.erpc

-gc 指定生成 C / C++,-o 指定输出目录。换成 -gpy 就生成 Python 客户端,上位机联调时很省事:同一个 IDL,两侧语言不同也不用手写解析。

生成物有三类:

文件 作用
demo_service.h 对外 C 接口:服务句柄 create_DemoService_service()、客户端绑定 DemoService_client_init()、每个 IDL 方法一个同名函数
demo_service_client.cpp 客户端桩:参数写入缓冲区,performRequest() 发出并等回复,再读回返回值
demo_service_server.cpp 服务端桩:收到帧后按 method ID 反序列化参数、调用业务函数、序列化返回值

demo_service.h:

demo_service.h自动生成的头文件,包含客户端接口声明

demo_service_client.cpp:

demo_service_client.cpp客户端桩代码,展示消息序列化与请求流程

demo_service_server.cpp:

demo_service_server.cpp服务端骨架代码,按方法ID路由到业务实现

契约就落在 .erpc 文件里了。要加一个方法,改 IDL 重新生成一遍,两端同时更新,不用再手工对帧格式和命令字。

3. 适配例子

3.1 常用配置

erpc_config.h 常用配置:

#define ERPC_THREADS (ERPC_THREADS_FREERTOS)                    // 锁/信号量走 FreeRTOS
#define ERPC_ALLOCATION_POLICY (ERPC_ALLOCATION_POLICY_STATIC)  // 静态分配,运行期不 malloc
#define ERPC_DEFAULT_BUFFER_SIZE (256U)                         // 单帧上限
#define ERPC_DEFAULT_BUFFERS_COUNT (4U)                         // 静态消息池 4 块

eRPC 内部靠 __has_include() 去探测 FreeRTOSConfig.h,不显式指定的话,配置会静默退化成无线程 + 动态分配。

FreeRTOS 侧则要打开 configUSE_RECURSIVE_MUTEXES(FramedTransport 的收发锁)和 configUSE_COUNTING_SEMAPHORES(静态消息池的信号量)。

3.2 传输层:继承 FramedTransport,实现两个函数

eRPC 把传输抽象成纯虚接口。帧头、CRC 校验、重发确认都由基类 FramedTransport 完成:

class UartTransport : public erpc::FramedTransport
{
protected:
    // 把 data 里的 size 个字节送到串口
    erpc_status_t underlyingSend(const uint8_t *data, uint32_t size) override
    {
        return (HAL_UART_Transmit(&huart1, (uint8_t *)data, size, 100) == HAL_OK)
                       ? kErpcStatus_Success
                       : kErpcStatus_SendFailed;
    }

    // 阻塞收满 size 个字节:中断里塞环形缓冲,这里等信号量
    erpc_status_t underlyingReceive(uint8_t *data, uint32_t size) override
    {
        /* 实现略:从环形缓冲取字节,取空就等信号量 */
    }
};

换总线时只动这两个函数。UART、SPI、CAN、RPMSG 都是同一个套路,上层完全无感。

3.3 注册服务,起一个轮询任务

初始化顺序是固定的:

/* 1) 静态消息缓冲池:4 块 × 256 字节 */
erpc_mbf_t mbf = erpc_mbf_static_init();

/* 2) 服务端:transport + codec + 消息池 */
erpc_server_t server = erpc_server_init(transport, mbf);

/* 3) 注册服务,service ID 由生成代码带进去 */
erpc_add_service_to_server(server, create_DemoService_service());

/* 4) 同一块板子上做双向调用时才需要客户端 */
erpc_client_t client = erpc_client_init(clientTransport, mbf);   // 另一端的 transport
DemoService_client_init(client);

服务端任务体只有一个循环,收帧、解帧、分发、回包都在里面:

while (1)
{
    erpc_server_poll(server);
}

业务代码写在单独的文件里,和 RPC 完全解耦:

uint32_t demo_service_add(uint32_t a, uint32_t b)
{
    return a + b;   // 只关心业务,不关心帧、CRC、序列号
}

调用侧就一行:

uint32_t sum = DemoService_add(100, 23);   // 返回 123

写法和本地函数调用没有区别。

整个流程走完,我们真正自己写的代码只有两个收发函数和一个业务文件。序列化、CRC、命令分发、超时和错误码都不需要我们处理。

新增接口也是同样的动作:改 IDL、重新生成、补业务实现。两端不会因为漏改解析代码而对不上。

总结

RPC 把两端约定帧格式换成两端约定接口。帧头、命令字、CRC、序列号这些细节无需再在业务代码里维护,交给工具统一维护。

要不要用 RPC,关键看接口的规模和变更频率:

  • 跨芯片、跨核、跨板,接口多、还要经常加和改,且属于命令-响应式的低频控制交互:适合用
  • 微秒级硬实时、资源极小的 MCU:不适合。序列化 / 反序列化存在不可控的开销和延迟

更多嵌入式 RPC、跨核通信以及板间协议的设计经验,欢迎在云栈社区和开发者一起交流讨论。




上一篇:用 VirtualBox 在 Windows 11 上安装 Windows XP 虚拟机完整教程
下一篇:eRPC嵌入式RPC框架解析:NXP开源轻量级多核通信方案
您需要登录后才可以回帖 登录 | 立即注册

关闭

站长推荐上一条 /1 下一条

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-28 04:26 , Processed in 0.080381 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表