目录
一、定义
RPC(Remote Procedure Call)远程过程调用。假设一个系统中有两个cpu,暂且称为A和B,分别运行着自己的应用程序,当A执行到某功能时需要B配合才能完成,这在程序里就体现为A需要调用B中的方法。RPC就是实现这种CPU间方法调用的协议。
二、应用场景
设想一个应用场景,系统中有两个CPU,一个主处理器A,A的性能强功耗高,负责运行用户界面等需要高算力的应用,一个协处理器B,B的性能弱但功耗低,负责运行传感器数据采集等功能。系统待机时A处于休眠状态,但B持续运行检测到唤醒动作时就唤醒A工作。
当A需要使用传感器数据时常规编程思路是直接调用传感器方法即可,在此场景下就变成了A调用B中的传感器方法。这就是RPC技术的应用。
三、验证
1、实验条件
1、nrf52832作为协处理器,cortex-m4内核,以下称为从机。
2、stm32f429作为主处理器,cotex-m4内核,以下称为主机。
3、通信接口采用常规的串口,各参数两端相同即可。
2、制定协议
结合到使用的处理器,本协议制定没有考虑大小端问题。
1)数据传输协议
数据传输协议比较简单粗暴,直接把函数需要用到的参数组合成字节流即可,然后加上目标函数的id:
函数调用帧格式:
函数id | 函数参数 |
---|---|
1 byte | n 字节 |
如果函数没有返回值,则只返回函数执行结果:
执行结果 |
---|
1 byte |
如果函数有返回值,则返回两次,第一次是函数本身的返回值,第二次是函数的执行结果:
函数返回值 |
---|
n byte |
执行结果 |
---|
1 byte |
2)函数调用协议
出于对RPC协议执行速度上的考虑,舍弃了参数类型解析以及内存访问等功能,在此定义的极简rpc协议本身存在一定限制,即:
1、函数参数个数不能超过8个
2、无返回值函数所有参数不能是指针,并且每个参数的sizeof大小不能超过sizeof(size_t)
3、有返回值的函数第一个参数始终为指针,用来存放返回值,其余要求与第2条相同
另外定义了两种基本函数类型,设置类和获取类:
设置类:函数没有返回值,但返回函数执行结果,典型示例如下
int fun_set(int a,int b);
获取类:函数使用第一个参数的指针来保存函数返回值,典型示例如下
int fun_get(void *p,int a,int b);
3、传输层
传输层封装了底层数据传输细节,保证了数据传输的可靠性。不是本文重点,此段省略。
4、RPC协议解析程序
1)从机端程序
说明:代码摘录于笔者编写的实际项目,存在传输层未定义函数不影响理解代码功能。
编写 rpc.h:
#ifndef cmdAT_ext_h__
#define cmdAT_ext_h__
#include "stdint.h"
// 函数的最大参数个数为8个,此值不可修改
#define CMDAT_EXT_FUNPAR_MAX_NUM 8
// 定义最大注册函数个数,不能超过255
#define CMDAT_EXT_FUN_MAX_NUM 20
// 定义函数类型
#define CMDAT_EXT_FUN_SET 0
#define CMDAT_EXT_FUN_GET 1
typedef struct {
void *fun_addr;
uint8_t par_num;
uint8_t par_size[CMDAT_EXT_FUNPAR_MAX_NUM];
uint8_t fun_type;
const char *help;
}fun_def;
/*----------------------------------------------------------------------------
定义set函数示例
原型:参数par0...par7的类型尺寸sizeof(par)不能超过sizeof(int),最多支持8个参数
返回值0成功,非0失败
int fun_exp_set(par0,par1,par2...par7);
定义get函数示例
原型:参数ptr0始终为指针;参数par1...par7的类型尺寸sizeof(par)不能超过sizeof(int),最多支持8个参数
返回值0成功,非0失败
int fun_exp_get(ptr0,par1,par2...par7);
注册一个set函数示例
fun_def fun;
fun.fun_addr=fun_exp_set;
fun.par_num=1;
fun.par_size[0]=sizeof(int);
fun.help="setfun demo";
cmdAT_setfun_register(1,&fun);
注册一个get函数示例
fun_def fun;
fun.fun_addr=fun_exp_get;
fun.par_num=2;
fun.par_size[0]=sizeof(int);
fun.par_size[1]=sizeof(int);
fun.help="getfun demo";
cmdAT_getfun_register(1,&fun);
-----------------------------------------------------------------------------*/
// 注册函数,index是函数的序号从1开始;fun是函数的定义;返回0成功
int cmdAT_setfun_register(int index,fun_def *fun);
// 注册函数,index是函数的序号从1开始;fun是函数的定义;返回0成功
int cmdAT_getfun_register(int index,fun_def *fun);
// 初始化
int cmdAT_extfun_init(void);
#endif
本文件定义了3个函数,初始化和注册函数。注册函数用于注册可用于rpc调用的函数,注册之后主机端就可以直接调用了。
编写rpc.c:
#include "cmdAT.h"
#include "cmdAT_ext.h"
#define cmd_fun(name) static int name(void *par,int size)
#define ext_set(name) cmd_fun(ext_set_##name)
#define ext_get(name) cmd_fun(ext_get_##name)
#define ext_help(name) static const char ext_help_##name[]
#define ext_extern(NAME,name) cmdAT_extern_cmd_h(NAME,ext_get_##name,ext_set_##name,0,ext_help_##name);
#define ext_extget(NAME,name) cmdAT_extern_cmd_h(NAME,ext_get_##name,0,0,ext_help_##name);
#define ext_extset(NAME,name) cmdAT_extern_cmd_h(NAME,0,ext_set_##name,0,ext_help_##name);
#define ext_ok() {cmdAT_ret(0,0,BYTE(1),1); return 0;}
#define ext_err() {cmdAT_ret(0,0,BYTE(0),1); return -1;}
#define ret_ok() cmdAT_ret(0,0,BYTE(1),1)
#define ret_err() cmdAT_ret(0,0,BYTE(0),1)
#define ext_ret(rc) {if (0==(rc)) ext_ok() else ext_err()}
//跳转至指定地址
uint32_t ext_call_fun (uint32_t a,uint32_t b,uint32_t c,uint32_t d,
uint32_t e,uint32_t f,uint32_t g,uint32_t h,uint32_t addr);
typedef struct __packet{
uint8_t pars[CMDAT_EXT_FUNPAR_MAX_NUM*4];
}par_def;
typedef struct{
fun_def funs[CMDAT_EXT_FUN_MAX_NUM+1];
}fun_table;
static fun_table g_fun_table;
// 注册函数,index是函数的序号从1开始;fun是函数的定义;返回0成功
int cmdAT_setfun_register(int index,fun_def *fun)
{
if((index>CMDAT_EXT_FUN_MAX_NUM)||(index<=0)) return -1;
if(fun==0) return -2;
if(fun->fun_addr==0) return -3;
if(fun->par_num>CMDAT_EXT_FUNPAR_MAX_NUM) return -4;
for(int i=0;i<fun->par_num;i++)
{
if(fun->par_size[i]>sizeof(uint32_t)) return -5;
}
if(fun->help==0) return -6;
fun_def *fun_=&g_fun_table.funs[index];
memcpy(fun_,fun,sizeof(fun_def));
fun_->fun_type=CMDAT_EXT_FUN_SET;
return 0;
}
// 注册函数,index是函数的序号从1开始;max_ret_size返回值的最大尺寸;fun是函数的定义;返回0成功
int cmdAT_getfun_register(int index,fun_def *fun)
{
if((index>CMDAT_EXT_FUN_MAX_NUM)||(index<=0)) return -1;
if(fun==0) return -2;
if(fun->fun_addr==0) return -3;
if(fun->par_num>CMDAT_EXT_FUNPAR_MAX_NUM) return -4;
// get函数参数至少为1
if(fun->par_num<1) return -4;
for(int i=0;i<fun->par_num;i++)
{
if(fun->par_size[i]>sizeof(uint32_t)) return -5;
}
if(fun->help==0) return -6;
fun_def *fun_=&g_fun_table.funs[index];
memcpy(fun_,fun,sizeof(fun_def));
fun_->fun_type=CMDAT_EXT_FUN_GET;
return 0;
}
static int cmdAT_get_help(char *str,int index)
{
*str=0;
if((index>CMDAT_EXT_FUN_MAX_NUM)||(index<0)) return -1;
fun_def *fun=&g_fun_table.funs[index];
memcpy(str,fun->help,strlen(fun->help)+1);
return 0;
}
int cmdAT_extfun_init(void)
{
memset(&g_fun_table,0,sizeof(fun_table));
fun_def *fun=&g_fun_table.funs[0];
fun->fun_addr=cmdAT_get_help;
fun->help="get function help";
fun->par_num=2;
fun->par_size[0]=20;
fun->par_size[1]=sizeof(int);
fun->fun_type=CMDAT_EXT_FUN_GET;
return 0;
}
// 此函数在主机调用无返回值函数时执行
ext_set(ext)
{
uint8_t *data=par;
par_def *fun_par=(par_def *)(data+1);
if(size>sizeof(par_def)) ext_err();
if(data[0]>CMDAT_EXT_FUN_MAX_NUM) ext_err();
fun_def *fun=&g_fun_table.funs[data[0]];
uint32_t u_par[CMDAT_EXT_FUNPAR_MAX_NUM];
uint8_t *ptr=fun_par->pars;
if(fun->fun_type!=CMDAT_EXT_FUN_SET) ext_err();
if(fun->fun_addr==0) ext_err();
for(int i=0;i<fun->par_num;i++)
{
u_par[i]=0;
for(int j=0;j<fun->par_size[i];j++)
{
u_par[i]|=ptr[j]<<(8*j);
}
ptr+=fun->par_size[i];
}
int rc;
rc=ext_call_fun(u_par[0],u_par[1],u_par[2],u_par[3],u_par[4],u_par[5],u_par[6],u_par[7],((uint32_t)fun->fun_addr)|1);
ext_ret(rc);
}
// 此函数在主机条用有返回值函数时执行
ext_get(ext)
{
uint8_t *data=par;
par_def *fun_par=(par_def *)(data+1);
if(size>sizeof(par_def)) ext_err();
if(data[0]>CMDAT_EXT_FUN_MAX_NUM) ext_err();
fun_def *fun=&g_fun_table.funs[data[0]];
uint32_t u_par[CMDAT_EXT_FUNPAR_MAX_NUM];
uint8_t *ptr=fun_par->pars;
if(fun->fun_type!=CMDAT_EXT_FUN_GET) ext_err();
if(fun->fun_addr==0) ext_err();
// get函数第一个参数始终为数据指针
for(int i=1;i<fun->par_num;i++)
{
u_par[i]=0;
for(int j=0;j<fun->par_size[i];j++)
{
u_par[i]|=ptr[j]<<(8*j);
}
ptr+=fun->par_size[i];
}
uint8_t *ret;
ret=malloc(fun->par_size[0]);
u_par[0]=(uint32_t)(ret);
int rc;
rc=ext_call_fun(u_par[0],u_par[1],u_par[2],u_par[3],u_par[4],u_par[5],u_par[6],u_par[7],((uint32_t)fun->fun_addr)|1);
if(rc==0)
cmdAT_ret(BYTE(0xff),1,(void *)ret,fun->par_size[0]);
free(ret);
ext_ret(rc);
}
// 把函数注册到系统回调
ext_help(ext)="user extend function";
ext_extern(ff,ext);
其中函数
uint32_t ext_call_fun (uint32_t a,uint32_t b,uint32_t c,uint32_t d,uint32_t e,uint32_t f,uint32_t g,uint32_t h,uint32_t addr);
使用汇编实现
AREA |.text|, CODE, READONLY, ALIGN=2
THUMB
REQUIRE8
PRESERVE8
EXPORT ext_call_fun
;运行指定地址的函数
ext_call_fun
LDR PC,[SP,#0x10]
ALIGN 4
END
2)主机端程序
主机端没有协议解析部分,所以无需编码。
5、编写RPC函数
1)从机端函数编写
示例中定义了一个有返回值函数和一个无返回值函数
// 有返回值函数定义,返回a+b的值
static int rpc_fun1(int *out,int a,int b)
{
*out=a+b;
return 0;
}
// 无返回值函数定义,根据a的值来控制引脚电平,用于改变LED亮灭
static int rpc_fun2(int a)
{
nrf_gpio_cfg_output(17);
if(a) nrf_gpio_pin_clear(17);
else nrf_gpio_pin_set(17);
return 0;
}
int main(void)
{
// 初始化RPC
cmdAT_extfun_init();
// 注册函数
fun_def fun;
fun.fun_addr=rpc_fun1;
fun.par_num=3;
fun.par_size[0]=sizeof(int);
fun.par_size[1]=sizeof(int);
fun.par_size[2]=sizeof(int);
fun.help="getfun rpc_fun1";
cmdAT_getfun_register(1,&fun);
fun.fun_addr=rpc_fun2;
fun.par_num=1;
fun.par_size[0]=sizeof(int);
fun.help="setfun rpc_fun2";
cmdAT_setfun_register(2,&fun);
}
2)主机端函数编写
说明:代码摘录于笔者编写的实际项目,存在传输层未定义函数不影响理解代码功能。
主机端需要声明相同的函数类型,如下:
编写 rpc_fun.h
#ifndef ble_rpc_h__
#define ble_rpc_h__
// 获取指定index函数的帮助,请保证str的空间大于128
int rpc_get_help(void *obj,char *str,int index);
int rpc_get_fun1(void *obj,int *out,int a,int b);
int rpc_set_fun2(void *obj,int a);
#endif
编写 prc_fun.c
// 获取指定index函数的帮助,请保证str的空间大于128
int rpc_get_help(void *obj,char *str,int index)
{
int rx_len=128;
int rc;
uint8_t tx_table[sizeof(int)+1];
tx_table[0]=0x00;//get_help函数的序号是0
memcpy(&tx_table[1],&index,sizeof(index));
rc=ble_user_cmd_get(obj,tx_table,sizeof(tx_table),(uint8_t *)str,&rx_len);
return rc;
}
int rpc_get_fun1(void *obj,int *out,int a,int b)
{
int rx_len=sizeof(int);
int rc;
uint8_t tx_table[sizeof(int)+sizeof(int)+1];
tx_table[0]=0x01;
memcpy(&tx_table[1],&a,sizeof(int));
memcpy(&tx_table[1+sizeof(int)],&b,sizeof(int));
rc=ble_user_cmd_get(obj,tx_table,sizeof(tx_table),(uint8_t *)out,&rx_len);
return rc;
}
int rpc_set_fun2(void *obj,int a)
{
int rc;
uint8_t tx_table[sizeof(int)+1];
tx_table[0]=0x02;
memcpy(&tx_table[1],&a,sizeof(int));
rc=ble_user_cmd_set(obj,tx_table,sizeof(tx_table));
return rc;
}
6、RPC函数调用示例
编写主机端test程序:
static void test_thread(void *t)
{
ble_struct *ble=malloc(sizeof(ble_struct));
// 初始化传输层
ble_init(ble);
char *rx_table=malloc(128);
// 获取函数帮助
rpc_get_help(ble,rx_table,0);
printf("%s\r\n",rx_table);
rpc_get_help(ble,rx_table,1);
printf("%s\r\n",rx_table);
rpc_get_help(ble,rx_table,2);
printf("%s\r\n",rx_table);
int out;
int a=0,b=0;
int rc;
while(1)
{
// 通过RPC调用获取a+b的值
rc=rpc_get_fun1(ble,&out,a,b);
// 打印结果
printf("rc=%d,a=%d,b=%d,out=%d\r\n",rc,a,b,out);
// 开启LED灯
rpc_set_fun2(ble,1);
delay_ms(500);
// 关闭led灯
rpc_set_fun2(ble,0);
delay_ms(500);
a+=1;b+=2;
}
}
四、现象
https://www.bilibili.com/video/bv1dq4y1J7aW
如视频所示,蓝牙模块上灯光周期闪烁,stm32这边则打印了rpc函数调用的结果,与预期相符。