2. 原理简介

目录:

一、引言 MCU与脚本语言

二、PikaScript 的原理解析

三、用 Pikascrip 点一个灯

四、用 PikaScript 实现一个加法函数

2.1. 引言 MCU 与脚本语言

在 IOT、智能终端等嵌入式应用场景中,脚本开发是一个方便快捷的解决方案。

_images/1638491966047-3a50736a-847b-4013-9710-c399a7da1687.webp

说到嵌入式使用脚本语言开发,可能首先想到的就是 micropython , micropython 可以让工程师使用脚本语言 python 进行 mcu 开发,极大地降低了开发门槛。

_images/1638491966123-c1a68063-0d4a-40a1-abfc-129b7ebfef8c.webp

但是使用 micropython 开发能够直接使用的开发板并不多,为没有现成 micropython 固件的 mcu 移植 micropython 显然也是一件工程浩大且门槛很高的工作。

_images/1638492189546-195d7ff9-ef64-49c1-ae0c-ac74988de28d.webp

而且 python 的运行效率较低,在资源紧缺的 mcu 中显得尤为明显,使用 python 开发也难以充分利用 mcu 的中断、 dma 等硬件特性。在高实时性的信号处理、数据采集、实时控制等应用中,python 难以成为真正落地于生产环境。

就目前而言,在 mcu 开发中,占80%左右的开发仍然是使用 c 语言,c++ 也仅占不到20%。

但是无疑脚本语言的便利性是非常明显的。服务器端的开发者往往熟悉 python 和 JavaScript 等支持面向对象的脚本语言,如果能够直接用脚本语言调用 mcu 的功能,将明显降低开发难度。 那么,如果使用 c 语言进行 mcu 嵌入式开发,又向上位机或者服务器提供面向对象的脚本语言调用接口,不就可以兼顾 mcu 运行效率和开发效率了吗? _images/1638491966304-fa5ca1d8-4723-4863-a9c3-8887d8f485e6.webp

本文介绍的 Pikasciprt 库正是可以起到这样的作用。

Pikascrpit 库可以为 c 语言开发的 mcu 工程提供面向对象的脚本语言调用接口。PikaScript 有以下几个特点:

  • 支持裸机运行,可运行于内存 4Kb 以上的 mcu 中,如 stm32f103 ,esp32。

  • 支持跨平台,可运行于 linux 环境。

  • 代码可读性强,仅使用 C 标准库,尽可能的结构清晰(尽我所能),几乎不使用宏。

2.2. PikaScript的原理解析

PikaScript 的架构示意图如下图所示。我们从上往下逐层分析。

_images/1638491966870-6a61e96f-e5d3-4093-ab9e-79c29380bb84.webp

2.2.1. PikaRun 脚本运行层

PikaRun 脚本运行层是 PikaScript 的最上层调用接口,只需要调用 obj_run 就可以实现脚本运行。在调用 obj_run时,需要指定一个对象,脚本在运行时,会检索这个对象的方法和这个对象的子对象的方法。 下图所示的是一个常见的嵌入式开发中的对象结构,sys 是最上层的对象,sys 对象有 reboot() 方法,sys 对象下挂载了 device 子对象和 task 子对象,这两个对象下面又挂载了子对象,每个子对象有自己的方法。

_images/1638491966937-78513a9e-3204-4eb7-b2f3-b0a418cee8f1.webp

这个时候,我们只需要在 obj_run 中传入最上面的 sys 对象的指针,就可以用如下图所示的方法调用所有对象的所有方法。其中,reboot()方法直接属于 sys 对象,因此使用直接运行obj_run(sys, "reboot()")就可以调用,而 led 对象则通 过obj_run(sys, "device.led.on()") 进行调用。

_images/1638491967502-544467d3-f364-47f0-b09b-b8b1d6531a95.webp 在实际开发中,我们可以让 mcu 将串口接收到的数据直接作为脚本运行。 例如:

  1. obj_run(sys, uartReciveBuff);

其中 uartReciveBuff 是串口接收到的数据。 这时向 mcu 的串口中发送"device.led.on()",就可以点亮 led 灯。

2.2.2. PikaObj 对象支持层

如上一节所述,我们已经知道如何使用PikaScript在已有的对象结构中执行脚本。那么下一个问题就是,如何构造对象,又如何为对象定义属性和方法呢?

(1)构造器函数

PikaScript 通过一个构造器函数来构造对象。一个构造器函数对应一个 PikaScript 中的类。如下所示的就是一个 LED 的构造器函数。在 PikaScript 中,所有的构造器函数都使用相同的入口参数和返回参数。 入口参数 args 是一个参数列表,args 内部基于链表,可以传入任意个数、任意类型的参数,此处 args 是构造器的初始化参数,在进行含参构造时将会用到。 构造器函数的返回值是一个 PikaObj 对象。

  1. PikaObj * New_LED(Args *args){
  2. // 继承自MimiObj基本类
  3. PikaObj *self = New_PikaObj(args);
  4. // 为对象定义属性
  5. obj_setInt(self, "isOn", 0);
  6. // 为LED1对象绑定on()方法
  7. obj_defineMethod(self, "on()", onFun);
  8. return self;
  9. }

构造器的第一行代码用于类继承,LED 类继承自 Pikaobj 基类,Pikaobj 基类是所有类的源头。 obj_setIntLED 类定义一个属性,属性名为"isOn",初始值为0_images/1638491967406-0467d35a-d458-470f-b038-cfc2157cf86a.webp

obj_defineMethodLED类绑定一个方法,绑定的方法为on()方法。onFunon()方法所绑定的c原生函数的函数指针。onFun函数具体的编写方式在第三章和第四章中介绍。

(2)构造对象 构造对象有两种方法,一种是构造obj_run传入的对象,称为根对象,如下图中的sys对象,其他对象则是一般对象,挂载在根对象之下。 一般一个项目中只构造一个根对象。 _images/1638491967445-1a31b054-7d3f-4c12-9333-e72f09ea9208.webp newRootObj函数用于构造根对象。构造根对象需要传入对象名"led"和构造器函数指针,newRootObj的返回值是根对象的指针。

  1. PikaObj *led = newRootObj("led", New_LED);

一般对象的构造在父对象的构造器中完成,如果要在sys对象下挂载led子对象,就可以这样编写SYS类的构造器函数:

  1. PikaObj * New_SYS(Args *args){
  2. // 继承自MimiObj基本类
  3. PikaObj *self = New_PikaObj(args);
  4. // 通过LED类的构造器导入LED类
  5. obj_import(self, "LED", New_LED);
  6. // 使用LED类新建led对象,led对象作为sys对象的子对象
  7. obj_newObj(self, "led", "LED");
  8. return self;
  9. }

obj_import通过构造函数的函数指针导入一个类,上面代码中导入的类名为LEDobj_newObj通过导入的类新建对象,新建对象作为子对象挂载在当前类下。 这时,通过调用下面的函数,就可以得到一个挂载了led对象的sys根对象了。

  1. PikaObj *sys = newRootObj("sys", New_SYS);

2.2.3. dataArgs 动态参数列表

dataArgs是基于链表的动态参数列表,其结构体是ArgsdataArgs在运行时动态地申请和释放内存,所以可以在运行时增删改查参数,Pikaobj的属性和方法信息的存取均基于dataArgs参数列表实现。 dataArgs支持整形、浮点型、字符串、指针类型的参数,也支持将原生的c语言变量绑定为dataArgs中的参数。 下面的例子是Args的基本使用方法。dataArgs的实现原理会在后续的文章中介绍,在此文中不作重点。

  1. // 新建一个参数列表
  2. Args *args = New_Args();
  3. // 向参数列表中存入一个整形参数a,值为1
  4. args_setInt(args, "a", 1);
  5. // 取出参数a,值为1
  6. int a = args_getInt(args, "a");
  7. // 修改a的值为2
  8. args_setInt(args, "a", 2);
  9. // 再取出a,值为2
  10. a = args_getInt(args, "a");
  11. // 新建一个c语言的原生变量
  12. float b = 3.0;
  13. // 将变量b绑定为args中的参数
  14. args_bindFloat(args ,"b", &b);
  15. // 取出参数b,值为3.0
  16. float b2 = args_getFloat(args, "b");
  17. // 销毁参数列表
  18. args_deinit(args);

2.2.4. dataMemory

dataMemorydataArgs提供动态内存申请和释放,在此文中不做重点。

2.3. 用PikaScript点一个灯

鲁迅曾说过:“点灯是嵌入式领域的hello world”。 _images/1638491967818-27a5b954-f28d-43b2-b634-7ebe3dc2468e.webp

那我们就点一点灯,看一看实际工程中 PikaScript 如何为 mcu 提供面向对象的脚本支持。 我们以 STM32 的 HAL 库为例,假设在管脚 PA8 上连接了一个 LED 灯,我们称之为 led1,PA8 拉高时灯亮,拉低时灯灭。 那么点亮灯 led1 需要使用下面的 c 语言代码:

  1. HAL_GPIO_WritePin(GPIOA,GPIO_PIN_8,SET)

我们希望使用如下的面向对象脚本更优雅地点灯~

  1. led1.on()

下面我们看看如何使用 PikaScript 实现这个需求。

2.3.1. 编写一个 onFun() 函数。

  1. void onFun(MimiObj *self, Args *args){
  2. HAL_GPIO_WritePin(GPIOA,GPIO_PIN_8,SET);
  3. }

这个函数将会被作为一个方法注册到脚本对象里面,注册之后就不会再由开发者在c语言开发中调用了,只会在脚本运行时被脚本解释器调用。 onFun()函数的入口参数有selfargs,其中self是对象的指针,args是传入和传出的参数列表(在第四章会用到)。 在 PikaScript 中,所有被绑定为方法的函数都使用这两个入口参数。

2.3.2. 编写LED1类的构造器。

  1. PikaObj * New_LED1(Args *args){
  2. // 继承自PikaObj基本类
  3. MimiObj *self = New_PikaObj(args);
  4. // 为LED1对象绑定on()方法
  5. obj_defineMethod(self, "on()", onFun);
  6. return self;
  7. }

obj_defineMethod用于将编写的 c 语言函数绑定为脚本对象的方法。这里将 c 语言的原生函数onFun()的函数指针作为参数注册进对象中,"on()"字符串声明了脚本调用时的方法名和参数,此处"on()"方法没有参数,含参数的方法绑定在第四章介绍。

2.3.3. 编写根对象的构造器。

  1. PikaObj * New_MYROOT(Args *args){
  2. // 继承自MimiObj基本类
  3. MimiObj *self = New_PikaObj(args);
  4. // 导入LED1类
  5. obj_import(root, "LED1", New_LED1);
  6. // 构造子对象"led1","led1"的类是"LED1"
  7. obj_newObj(root, "led1", "LED1");
  8. return self;
  9. }

obj_import通过构造函数的函数指针导入LED1类。 obj_newObj通过导入的LED1类新建led1对象,led1对象作为子对象挂载在MYROOT类下。

2.3.4. 创建根对象并监听串口的输入数据。当获得整行数据后直接当作脚本执行。

  1. int uartReciveOk; //串口单行接收完成的标志位
  2. char uartReciveBuff[256];// 串口接收到的单行数据
  3. int main(){
  4. // 硬件的初始化代码略
  5. // 创建根对象
  6. PikaObj *myRoot = newRootObj("myRoot", New_MYROOT);
  7. while(1){
  8. // 串口已经接收到单行数据
  9. if(uartReciveOk){
  10. // 执行串口输入的单行数据
  11. obj_run(myRoot, uartReciveBuff);
  12. // 清除串口接收标志位
  13. uartReciveOk = 0;
  14. }
  15. }
  16. }

这时只要向 mcu 的串口发送​led1.on(),灯就亮了(神奇不~)

2.4. 用 PikaScript 实现一个加法函数。

上面的例子中的方法没有输入输出,下面的例子中,我们会定义一个TEST类,并为TEST类添加一个add方法,实现加法功能,看一看如何绑定一个带有输入输出的方法。

2.4.1. 编写一个add()函数。

和上次onFun函数一样,这次被绑定的函数是addFun函数。

  1. void addFun(PikaObj *self, Args *args) {
  2. //取出输入参数
  3. int val1 = args_getInt(args, "val1");
  4. int val2 = args_getInt(args, "val2");
  5. //实现方法功能
  6. int res = val1 + val2;
  7. //将返回值传回参数列表
  8. method_returnInt(args, res);
  9. }

args_getInt用来从参数列表中取出整型参数,此处从参数列表中取出输入参数val1val2。参数列表还支持float类型、字符串类型和指针类型。 method_returnInt用来传递方法的返回值,同样可以返回float类型、字符串类型和指针类型。

2.4.2. 定义测试类的构造器

  1. PikaObj *New_PikaObj_test(Args *args){
  2. //继承MimiObj基本类
  3. MimiObj *self = New_PikaObj(args);
  4. //绑定方法
  5. obj_defineMethod(self, "add(val1:int, val2:int)->int", addFun);
  6. return self;
  7. }

这次用obj_defineMethod绑定一个带有输入输出参数的方法。 "add(val1:int,val2:int)->int"是python的带类型函数声明语法,表示add方法有int类型的val1val2两个输入参数,输出参数也是int类型。同样,传入addFun函数的函数指针。

2.4.3. 编写根对象的构造器。

  1. PikaObj * New_MYROOT(Args *args){
  2. // 继承自PikaObj基本类
  3. PikaObj *self = New_PikaObj(args);
  4. // 导入TEST类
  5. obj_import(root, "TEST", New_PikaObj_test);
  6. // 构造子对象"test","test"的类是"TEST"
  7. obj_newObj(root, "test", "TEST");
  8. return self;
  9. }

在根对象中挂载test子对象。

2.4.4. 创建对象并测试运行脚本

  1. void main(){
  2. //新建根对象
  3. PikaObj *root = newRootObj("root", New_MYROOT);
  4. //运行脚本(也支持 "res = test.add(val1 = 1, val2= 2)"的调用方式)
  5. obj_run(root , "res = test.add(1, 2)");
  6. //从root 对象中取出属性值res
  7. int res = obj_getInt(root, "res");
  8. //销毁根对象
  9. obj_deinit(root);
  10. /* 打印返回值 res = 3*/
  11. printf("%d\r\n", res);
  12. }

obj_run运行脚本后会动态新建res属性,该属性属于root对象。obj_deinit用于销毁对象,所有挂载在root对象下的子对象都会被自动销毁。本例中root对象挂载了test对象,因此在销毁root对象前,test对象会被自动销毁。