1.1. 原理简介

目录:

一、引言 MCU与脚本语言

二、PikaPython 的原理解析

三、用 PikaPython 点一个灯

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

1.1.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

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

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

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

  • 支持跨平台,可运行于 linux, windows 环境,方便仿真开发。

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

1.1.2. PikaPython 的原理解析

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

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

1.1.2.1. PikaRun 脚本运行层

PikaRun 脚本运行层是 PikaPython 的最上层调用接口,只需要调用 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 将串口接收到的数据直接作为脚本运行。

例如:

obj_run(sys, uartReciveBuff);

其中 uartReciveBuff 是串口接收到的数据。

这时向 mcu 的串口中发送 "device.led.on()" ,就可以点亮 led 灯。

1.1.2.2. PikaObj 对象支持层

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

(1)构造器函数

PikaPython 通过一个构造器函数来构造对象。一个构造器函数对应一个 PikaPython 中的类。如下所示的就是一个 LED 的构造器函数。在 PikaPython 中,所有的构造器函数都使用相同的入口参数和返回参数。

入口参数 args 是一个参数列表,args 内部基于链表,可以传入任意个数、任意类型的参数,此处 args 是构造器的初始化参数,在进行含参构造时将会用到。 构造器函数的返回值是一个 PikaObj 对象。

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

构造器的第一行代码用于类继承,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 的返回值是根对象的指针。

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

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

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

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

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

1.1.2.3. dataArgs 动态参数列表

dataArgs 是基于链表的动态参数列表,其结构体是 ArgsdataArgs 在运行时动态地申请和释放内存,所以可以在运行时增删改查参数,Pikaobj 的属性和方法信息的存取均基于 dataArgs 参数列表实现。

dataArgs 支持整形、浮点型、字符串、指针类型的参数,也支持将原生的c语言变量绑定为 dataArgs 中的参数。

下面的例子是 Args 的基本使用方法。dataArgs 的实现原理会在后续的文章中介绍,在此文中不作重点。

// 新建一个参数列表
Args *args = New_Args();
// 向参数列表中存入一个整形参数a,值为1
args_setInt(args, "a", 1);
// 取出参数a,值为1
int a = args_getInt(args, "a");
// 修改a的值为2
args_setInt(args, "a", 2);
// 再取出a,值为2
a = args_getInt(args, "a");
// 销毁参数列表
args_deinit(args);

1.1.2.4. dataMemory

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

1.1.3. 用PikaPython点一个灯

鲁迅曾说过:“点灯是嵌入式领域的hello world”。

_images/1638491967818-27a5b954-f28d-43b2-b634-7ebe3dc2468e.webp

那我们就点一点灯,看一看实际工程中 PikaPython 如何为 mcu 提供面向对象的脚本支持。

我们以 STM32 的 HAL 库为例,假设在管脚 PA8 上连接了一个 LED 灯,我们称之为 led1,PA8 拉高时灯亮,拉低时灯灭。

那么点亮灯 led1 需要使用下面的 c 语言代码:

HAL_GPIO_WritePin(GPIOA,GPIO_PIN_8,SET)

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

led1.on()

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

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

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

这个函数将会被作为一个方法注册到脚本对象里面,注册之后就不会再由开发者在c语言开发中调用了,只会在脚本运行时被脚本解释器调用。

onFun()函数的入口参数有selfargs,其中self是对象的指针,args是传入和传出的参数列表(在第四章会用到)。

在 PikaPython 中,所有被绑定为方法的函数都使用这两个入口参数。

1.1.3.2. 编写LED1类的构造器。

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

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

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

PikaObj * New_MYROOT(Args *args){
    // 继承自MimiObj基本类
    MimiObj *self = New_PikaObj(args);
    // 导入LED1类
    obj_import(root, "LED1", New_LED1);
    // 构造子对象"led1","led1"的类是"LED1"
    obj_newObj(root, "led1", "LED1");
    return self;
}

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

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

int uartReciveOk; //串口单行接收完成的标志位
char uartReciveBuff[256];// 串口接收到的单行数据
int main(){
    // 硬件的初始化代码略

    // 创建根对象
    PikaObj *myRoot = newRootObj("myRoot", New_MYROOT);
    while(1){
        // 串口已经接收到单行数据
        if(uartReciveOk){
            // 执行串口输入的单行数据
            obj_run(myRoot, uartReciveBuff);
            // 清除串口接收标志位
            uartReciveOk = 0;
        }
    }
}

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

1.1.4. 用 PikaPython 实现一个加法函数。

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

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

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

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

args_getInt 用来从参数列表中取出整型参数,此处从参数列表中取出输入参数 val1val2。参数列表还支持 float 类型、字符串类型和指针类型。

method_returnInt 用来传递方法的返回值,同样可以返回 float 类型、字符串类型和指针类型。

1.1.4.2. 定义测试类的构造器

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

这次用 obj_defineMethod 绑定一个带有输入输出参数的方法。

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

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

PikaObj * New_MYROOT(Args *args){
    // 继承自PikaObj基本类
    PikaObj *self = New_PikaObj(args);
    // 导入TEST类
    obj_import(self, "TEST", New_PikaObj_test);
    // 构造子对象"test","test"的类是"TEST"
    obj_newObj(self, "test", "TEST");
    return self;
}

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

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

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

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

本例中root对象挂载了test对象,因此在销毁root对象前,test对象会被自动销毁。

1.1.5. 更方便的构造 class 和对象

通过编写构造器函数的方式实现一个 class 在还是略显麻烦,在实际开发中,PikaPython 提供了自动生成构造器函数的工具:PikaPython 预编译器

只要用 Python 的语法声明一个 class 就能自动连接到 C 的函数,详情请看 C 模块-> 将 C 库制作为 Python 库