1.1. Principle introduction

content:

  • Introduction MCU and scripting language

  • The principle analysis of PikaPython

  • Light a lamp with Pikascript

  • use PikaPython to implement an addition function

1.1.1. Introduction MCU and scripting languages

In embedded application scenarios such as IOT and smart terminals, script development is a convenient and fast solution.

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

When it comes to the development of embedded scripting languages, the first thing that comes to mind is micropython. Micropython allows engineers to use the scripting language python for MCU development, which greatly reduces the development threshold.

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

However, there are not many development boards that can be used directly in the development of micropython. It is obviously a huge project and a high threshold to transplant micropython for the MCU without ready-made micropython firmware.

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

Moreover, the operating efficiency of python is low, which is especially obvious in the MCU with limited resources. It is also difficult to make full use of the hardware features such as interrupt and dma of MCU for development with python.

In applications such as high real-time signal processing, data acquisition, and real-time control, it is difficult for python to be truly implemented in the production environment.

For now, in the development of mcu, about 80% of the development is still using the c language, and c++ only accounts for less than 20%.

But there is no doubt that the convenience of scripting languages is very obvious. Server-side developers are often familiar with object-oriented scripting languages such as python and JavaScript.

If the function of MCU can be called directly from the scripting language, the development difficulty will be significantly reduced.

Then, if you use the c language for MCU embedded development, and provide an object-oriented scripting language calling interface to the host computer or server, can you take into account the MCU operating efficiency and development efficiency?

The Pikasciprt library introduced in this article does exactly that.

Pikascrpit library can provide object-oriented scripting language calling interface for mcu project developed in C language. PikaPython has the following features:

  • Support bare metal operation, can run in mcu with more than 4Kb memory, such as stm32f103, esp32.

  • Support cross-platform, can run in linux environment.

  • Code is readable, uses only the C standard library, is structured as clearly as possible (as far as I can), and uses few macros.

1.1.2. The principle analysis of PikaPython

The schematic diagram of the architecture of PikaPython is shown in the following figure. We analyze it layer by layer from top to bottom.

https://user-images.githubusercontent.com/88232613/170965877-31a02323-7fd8-49b8-8a2e-ab9f9181d3cf.png

1.1.2.1. PikaRun script run layer

The PikaRun script running layer is the top-level calling interface of PikaPython, and script running can be realized only by calling obj_run. When calling obj_run, you need to specify an object. When the script runs, it will retrieve the methods of this object and the methods of the sub-objects of this object.

The following figure shows a common object structure in embedded development. sys is the top-level object. The sys object has a reboot() method. The device sub-object and the task sub-object are mounted under the sys object. These two objects The sub-objects are mounted below, and each sub-object has its own method.

https://user-images.githubusercontent.com/88232613/170966488-fb84f65b-6696-4b66-8362-69442dfc1e2c.png

At this time, we only need to pass in the pointer of the top sys object in obj_run, and you can call all methods of all objects with the method shown in the following figure. Among them, the reboot() method directly belongs to the sys object, so it can be called by directly running obj_run(sys, "reboot()"), and the led object is called through obj_run(sys, "device.led. on()") to call.

https://user-images.githubusercontent.com/88232613/170967199-58de200a-d63a-469e-a260-31978eced88f.png

In actual development, we can let the mcu run the data received by the serial port directly as a script. E.g:

obj_run(sys, uartReceiveBuff);

Where uartReceiveBuff is the data received by the serial port.

At this time, send "device.led.on()" to the serial port of the mcu, and the led light can be turned on.

1.1.2.2. PikaObj Object Support Layer

As mentioned in the previous section, we already know how to use PikaPython to execute scripts within an existing object structure. So the next question is, how to construct objects, and how to define properties and methods for objects?

(1) Constructor function

PikaPython constructs objects through a constructor function. A constructor function corresponds to a class in PikaPython. The constructor function for an LED is shown below. In PikaPython, all constructor functions use the same entry and return parameters.

The entry parameter args is a parameter list. The args is internally based on a linked list, and any number and type of parameters can be passed in. Here args is the initialization parameter of the constructor, which will be used when constructing with parameters.

The return value of the constructor function is a PikaObj object.

PikaObj *New_LED(Args *args){
  // inherited from MimiObj base class
  PikaObj *self = New_PikaObj(args);
  // define properties for the object
  obj_setInt(self, "isOn", 0);
  // Bind the on() method to the LED1 object
  obj_defineMethod(self, "on()", onFun);
  return self;
}

The first line of the constructor is for class inheritance. The LED class inherits from the Pikaobj base class, which is the source of all classes.

obj_setInt defines a property for the LED class, the property name is "isOn", and the initial value is 0.

https://user-images.githubusercontent.com/88232613/170967485-7bcde335-2f71-4460-bc20-273b5b18ae62.png

obj_defineMethod binds a method to the LED class, and the bound method is the on() method. onFun is a function pointer to the c native function to which the on() method is bound. The specific way of writing the onFun function is introduced in Chapters 3 and 4.

(2) Construct the object

There are two ways to construct objects. One is to construct the object passed in by obj_run, which is called the root object, such as the sys object in the following figure, and the other objects are general objects, which are mounted under the root object.

Generally, only one root object is constructed in a project.

https://user-images.githubusercontent.com/88232613/170967692-2addc236-cb79-47c3-814b-4ce54f3d7499.png

The newRootObj function is used to construct the root object. To construct a root object, you need to pass in the object name "led" and the constructor function pointer. The return value of newRootObj is the pointer of the root object.

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

The construction of general objects is done in the constructor of the parent object. If you want to mount the led child object under the sys object, you can write the constructor function of the SYS class like this:

PikaObj * New_SYS(Args *args){
    // inherited from MimiObj base class
    PikaObj *self = New_PikaObj(args);
    // Import the LED class through the constructor of the LED class
    obj_import(self, "LED", New_LED);
    // Use the LED class to create a new led object, and the led object is used as a sub-object of the sys object
    obj_newObj(self, "led", "LED");
    return self;
}

obj_import imports a class through the function pointer of the constructor. The imported class in the above code is named LED. obj_newObj creates a new object through the imported class, and the new object is mounted as a sub-object under the current class.

At this time, by calling the following function, you can get a sys root object that mounts the led object.

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

1.1.2.3. dataArgs dynamic parameter list

dataArgs is a dynamic parameter list based on a linked list. Its structure is Args. dataArgs dynamically applies for and releases memory at runtime, so you can add, delete, modify, check parameters at runtime, and attribute and method information of Pikaobj The access is based on the dataArgs parameter list.

dataArgs supports integer, floating point, string, pointer type parameters, and also supports binding native C language variables as parameters in dataArgs.

The following example is the basic usage of Args. The implementation principle of dataArgs will be introduced in subsequent articles, and will not be emphasized in this article.

// create a new parameter list
Args *args = New_Args();
// Store an integer parameter a into the parameter list with a value of 1
args_setInt(args, "a", 1);
// Take the parameter a, the value is 1
int a = args_getInt(args, "a");
// modify the value of a to 2
args_setInt(args, "a", 2);
// Take out a again, the value is 2
a = args_getInt(args, "a");
// destroy the parameter list
args_deinit(args);

1.1.2.4. dataMemory

dataMemory provides dynamic memory allocation and release for dataArgs, which is not the focus of this article.

1.1.3. Light a light with PikaPython

Then let’s light a light and see how PikaPython provides object-oriented scripting support for mcu in actual projects.

Let’s take the HAL library of STM32 as an example. Suppose an LED light is connected to pin PA8, which we call led1. When PA8 is pulled high, the light is on, and when it is pulled low, the light is off.

Then to turn on the light led1, you need to use the following c language code:

HAL_GPIO_WritePin(GPIOA,GPIO_PIN_8,SET)

We hope to use the following object-oriented script to turn on the lights more elegantly~

led1.on()

Let’s see how to use PikaPython to achieve this requirement.

1.1.3.1. Write an onFun() function.

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

This function will be registered in the script object as a method. After registration, it will no longer be called by the developer in C language development, but will only be called by the script interpreter when the script is running.

The entry parameters of the onFun() function are self and args, where self is the objectpointer, args is a list of incoming and outgoing arguments (used in Chapter 4).

In PikaPython, all functions bound as methods use these two entry parameters.

1.1.3.2. Write the constructor for the LED1 class.

PikaObj * New_LED1(Args *args){
    // Inherited from PikaObj base class
    MimiObj *self = New_PikaObj(args);
    // Bind the on() method to the LED1 object
    obj_defineMethod(self, "on()", onFun);
    return self;
}

obj_defineMethod is used to bind the written C language function as the method of the script object.

Here, the function pointer of the native function onFun() of the C language is registered into the object as a parameter, and the "on()" string declares the method name and parameters when the script is called, here "on()" Methods have no parameters, and method binding with parameters is introduced in Chapter 4.

1.1.3.3. Write the constructor for the root object.

PikaObj * New_MYROOT(Args *args){
    // inherited from MimiObj base class
    MimiObj *self = New_PikaObj(args);
    // Import the LED1 class
    obj_import(root, "LED1", New_LED1);
    // Construct sub-object "led1", the class of "led1" is "LED1"
    obj_newObj(root, "led1", "LED1");
    return self;
}

obj_import imports the LED1 class through the function pointer of the constructor.

obj_newObj creates a new led1 object through the imported LED1 class, and the led1 object is mounted as a sub-object under the MYROOT class.

1.1.3.4. Create a root object and listen for incoming data from the serial port. When the entire row of data is obtained, it is directly executed as a script.

int uartReceiveOk; //The flag bit that the serial port single-line reception is completed
char uartReceiveBuff[256];//Single-line data received by serial port
int main(){
    // Hardware initialization code is omitted

    // create root object
    PikaObj *myRoot = newRootObj("myRoot", New_MYROOT);
    while(1){
        // The serial port has received a single line of data
        if(uartReceiveOk){
            // Execute single-line data input from serial port
            obj_run(myRoot, uartReceiveBuff);
            // Clear the serial port receive flag
            uartReceiveOk = 0;
        }
    }
}

At this time, just send led1.on() to the serial port of mcu, the light will be on (magic no~)

1.1.4. Implement an addition function in PikaPython.

The method in the above example has no input and output. In the following example, we will define a TEST class and add an add method to the TEST class to implement the addition function. method of input and output.

1.1.4.1. Write an add() function.

Like the last onFun function, the function to be bound this time is the addFun function.

void addFun(PikaObj *self, Args *args) {
    //get the input parameters
    int val1 = args_getInt(args, "val1");
    int val2 = args_getInt(args, "val2");
    //implement method function
    int res = val1 + val2;
    // pass the return value back to the parameter list
    method_returnInt(args, res);
}

args_getInt is used to get integer parameters from the parameter list, here the input parameters val1 and val2 are taken from the parameter list. The parameter list also supports float type, string type and pointer type.

method_returnInt is used to pass the return value of the method, and it can also return float type, string type and pointer type.

1.1.4.2. Define the constructor of the test class

PikaObj *New_PikaObj_test(Args *args){
    //Inherit MimiObj base class
    MimiObj *self = New_PikaObj(args);
    // bind method
    obj_defineMethod(self, "add(val1:int, val2:int)->int", addFun);
    return self;
}

This time use obj_defineMethod to bind a method with input and output parameters.

"add(val1:int,val2:int)->int" is python’s typed function declaration syntax, indicating that the add method has two input parameters, val1 and val2 of type int, and the output The parameter is also of type int. Likewise, pass a function pointer to the addFun function.

1.1.4.3. Write the constructor for the root object.

PikaObj * New_MYROOT(Args *args){
    // Inherited from PikaObj base class
    PikaObj *self = New_PikaObj(args);
    // import the TEST class
    obj_import(self, "TEST", New_PikaObj_test);
    // Construct sub-object "test", the class of "test" is "TEST"
    obj_newObj(self, "test", "TEST");
    return self;
}

Mount the test child object in the root object.

1.1.4.4. Create object and test run script

void main(){
    // create a new root object
    PikaObj *root = newRootObj("root", New_MYROOT);
    //Run the script (also supports the calling method of "res = test.add(val1 = 1, val2= 2)")
    obj_run(root , "res = test.add(1, 2)");
    // Get the attribute value res from the root object
    int res = obj_getInt(root, "res");
    //destroy the root object
    obj_deinit(root);
    /* print return value res = 3*/
    printf("%d\r\n", res);
}

After obj_run runs the script, it will dynamically create a res property, which belongs to the root object.

obj_deinit is used to destroy the object, all child objects mounted under the root object will be automatically destroyed.

In this example, the root object mounts the test object, so the test object will be automatically destroyed before the root object is destroyed.

1.1.5. Constructing classes and objects more easily

Implementing a class by writing a constructor function is still a bit cumbersome. In practice, PikaPython provides a tool to automatically generate constructor functions: PikScript precompiler.

Just declare a class in Python syntax and it will automatically link to C functions, see C Module -> Making C Libraries into Python Libraries