1.1. Principle introduction
content:
Introduction MCU and scripting language
The principle analysis of PikaScript
Light a lamp with Pikascript
use PikaScript 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.
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.
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.
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. 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.
1.1.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.
1.1.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.
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.
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 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.
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
.
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.
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");
// Create a new native variable in C language
float b = 3.0;
// Bind the variable b as the parameter in args
args_bindFloat(args ,"b", &b);
// Take the parameter b, the value is 3.0
float b2 = args_getFloat(args, "b");
// 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 PikaScript
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:
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 PikaScript 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 PikaScript, 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 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.
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.