RPC(远程过程调用)基于单片机的极简实现

一、定义

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 byten 字节

如果函数没有返回值,则只返回函数执行结果:

执行结果
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函数调用的结果,与预期相符。

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值