2. Principle introduction

content:

  1. Introduction MCU and scripting language

Second, the principle analysis of PikaScript

  1. Light a lamp with Pikascript

Fourth, use PikaScript to implement an addition function

2.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? _images/1638491966304-fa5ca1d8-4723-4863-a9c3-8887d8f485e6.webp

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

2.2. The principle analysis of PikaScript

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

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

2.2.1. PikaRun script run layer

The PikaRun script running layer is the top-level calling interface of PikaScript, 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.

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

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.

_images/1638491967502-544467d3-f364-47f0-b09b-b8b1d6531a95.webp In actual development, we can let the mcu run the data received by the serial port directly as a script. E.g:

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

2.2.2. PikaObj Object Support Layer

As mentioned in the previous section, we already know how to use PikaScript 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

PikaScript constructs objects through a constructor function. A constructor function corresponds to a class in PikaScript. The constructor function for an LED is shown below. In PikaScript, 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.

  1. PikaObj *New_LED(Args *args){
  2. // inherited from MimiObj base class
  3. PikaObj *self = New_PikaObj(args);
  4. // define properties for the object
  5. obj_setInt(self, "isOn", 0);
  6. // Bind the on() method to the LED1 object
  7. obj_defineMethod(self, "on()", onFun);
  8. return self;
  9. }

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. _images/1638491967406-0467d35a-d458-470f-b038-cfc2157cf86a.webp

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. _images/1638491967445-1a31b054-7d3f-4c12-9333-e72f09ea9208.webp 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.

  1. 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:

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

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.

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

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

  1. // create a new parameter list
  2. Args *args = New_Args();
  3. // Store an integer parameter a into the parameter list with a value of 1
  4. args_setInt(args, "a", 1);
  5. // Take the parameter a, the value is 1
  6. int a = args_getInt(args, "a");
  7. // modify the value of a to 2
  8. args_setInt(args, "a", 2);
  9. // Take out a again, the value is 2
  10. a = args_getInt(args, "a");
  11. // Create a new native variable in C language
  12. float b = 3.0;
  13. // Bind the variable b as the parameter in args
  14. args_bindFloat(args ,"b", &b);
  15. // Take the parameter b, the value is 3.0
  16. float b2 = args_getFloat(args, "b");
  17. // destroy the parameter list
  18. args_deinit(args);

2.2.4. dataMemory

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

2.3. Light a light with PikaScript

Lu Xun once said: “Lighting is the hello world in the embedded field”. _images/1638491967818-27a5b954-f28d-43b2-b634-7ebe3dc2468e.webp

Then let’s light a light and see how PikaScript 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:

  1. HAL_GPIO_WritePin(GPIOA,GPIO_PIN_8,SET)

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

  1. led1.on()

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

2.3.1. Write an onFun() function.

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

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 PikaScript, all functions bound as methods use these two entry parameters.

2.3.2. Write the constructor for the LED1 class.

  1. PikaObj * New_LED1(Args *args){
  2. // Inherited from PikaObj base class
  3. MimiObj *self = New_PikaObj(args);
  4. // Bind the on() method to the LED1 object
  5. obj_defineMethod(self, "on()", onFun);
  6. return self;
  7. }

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.

2.3.3. Write the constructor for the root object.

  1. PikaObj * New_MYROOT(Args *args){
  2. // inherited from MimiObj base class
  3. MimiObj *self = New_PikaObj(args);
  4. // Import the LED1 class
  5. obj_import(root, "LED1", New_LED1);
  6. // Construct sub-object "led1", the class of "led1" is "LED1"
  7. obj_newObj(root, "led1", "LED1");
  8. return self;
  9. }

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.

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

  1. int uartReceiveOk; //The flag bit that the serial port single-line reception is completed
  2. char uartReceiveBuff[256];//Single-line data received by serial port
  3. int main(){
  4. // Hardware initialization code is omitted
  5. // create root object
  6. PikaObj *myRoot = newRootObj("myRoot", New_MYROOT);
  7. while(1){
  8. // The serial port has received a single line of data
  9. if(uartReceiveOk){
  10. // Execute single-line data input from serial port
  11. obj_run(myRoot, uartReceiveBuff);
  12. // Clear the serial port receive flag
  13. uartReceiveOk = 0;
  14. }
  15. }
  16. }

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

2.4. Implement an addition function in PikaScript.

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.

2.4.1. Write an add() function.

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

  1. void addFun(PikaObj *self, Args *args) {
  2. //get the input parameters
  3. int val1 = args_getInt(args, "val1");
  4. int val2 = args_getInt(args, "val2");
  5. //implement method function
  6. int res = val1 + val2;
  7. // pass the return value back to the parameter list
  8. method_returnInt(args, res);
  9. }

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.

2.4.2. Define the constructor of the test class

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

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.

2.4.3. Write the constructor for the root object.

  1. PikaObj * New_MYROOT(Args *args){
  2. // Inherited from PikaObj base class
  3. PikaObj *self = New_PikaObj(args);
  4. // import the TEST class
  5. obj_import(root, "TEST", New_PikaObj_test);
  6. // Construct sub-object "test", the class of "test" is "TEST"
  7. obj_newObj(root, "test", "TEST");
  8. return self;
  9. }

Mount the test child object in the root object.

2.4.4. Create object and test run script

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

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.