This tutorial explain how to modify the VMCustom module of Terra System to add new functionalities. Having some familiarity with nesC will help you understand this tutorial. Also understanding the event-driven model and the importance of not creating blocking operations is critical to avoiding side effects in the Terra execution. Finally, it is strongly recommended to already know the Terra programming environment, especially the Céu-T features.
The next figure presents the customization and usage flows.
The developer activities related to customization are indicated in
The main developer activities are:
.defs
file or modification on an existing one.At least three files must be edited to add new functionalities to a specific customization.
~/Terra/TerraVM/src/TerraDefs
directory, where XXX is the base customization for target platform.~/Terra/TerraVM/src/platform/XXX
directory.~/Terra/TerraVM/src/platform/XXX
directory.The definitions between these three files must be compatibles. In the case of events, the numeric IDs must be equal to equivalent operations. In the case of data structures, the type and position of the each fields must be equivalent. Same events and functions may have different names between the files, considering that the numeric IDs are the same.
The syntax of each type of operations are described in the sequence. The last section presents some real code examples.
In general, small functionalities are written inside VMCustomP.nc file. If a new functionality requires additional files, a nesC component and/or a library of external functions may be created. In this case the developer may need to modify the VMCustomC.nc file and must modify the Make system to be able to compile the new functionalities. These modifications are out of scope of this tutorial.
This file is used during terrac compilation to validate the event and function names. Any customized data structure used by events or functions are also defined in this file. Additionally, the developer may define constant values to be used in the new functionalities.
A constant definition uses the #define
command as used in the C language.
Some examples are:
#define HIGH 1
#define LOW 0
#define ON 1
#define OFF 0
Registers - Some new functionalities may need exchange values that a single basic type
cannot support.
In these cases we need to create a specific data structure that includes all needed values.
In Céu-T language, a regtype
declaration creates a new register type.
A register can only have fields that are values of basic Terra types or arrays of basic types.
In Céu-T, all variables must start with a lowercase character.
// regtype example
regtype dhtData_t with
var ubyte stat;
var ubyte hum;
var ubyte temp;
end
Packets - In special cases the new functionality may leave some fields to be defined by the user.
This is the case of Radio messages where one part of the message (header) is predefined by the Terra message protocol
and second part (payload) is application dependent.
The pktype
declaration creates a new abstract type with predefined fields and a payload section.
The predefined fields depend on the new functionality needs and the payload section is left to the user application needs.
// pktype example
packet radioMsg with
var ubyte type;
var ushort source;
var ushort target;
var payload[20] data;
end
When using registers in events or functions, Terra system always pass the data address instead of data values. Then, you must use the TerraVM internal support to copy memory data when building your customized function. See below the "VMCustomP.nc file" section to find some examples.
Output events are defined by:
output void EventName ArgumentType NumericID;
Where:
An example of output event definition is:
output void REQ_DHT ubyte 10;
Input events are defined by:
input ReturnType EventName ArgumentType NumericID;
Where:
An example of input event definition is:
input ubyte SEND_DONE void 41;
Functions are defined by:
function ReturnType FunctionName(ListOfArgumentsType) NumericID;
Where:
Examples of function definition are:
function ushort getNodeId() 0;
function ubyte pinWrite(ubyte,ubyte,ubyte) 21;
This file defines the events and function numeric identifiers to be used during the building of TerraVM firmware.
Each numeric ID must be equal to the equivalent one in the .defs
file.
These IDs are defined as a list of enumeration values as:
enum{
OutEvent1 =NumericId1,
OutEvent2 =NumericId2,
InEvent1 =NumericId1,
InEvent2 =NumericId2,
Function1 =NumericId1,
Function2 =NumericId2,
}
The entry point of custom functionalities are written in this file. In the case of functions or output events, you may write all functionality code inside this file or you may call external library functions or nesC component commands. In the case of input events you need to call a internal TerraVM function passing the event ID and event data as argument. In general an input event is triggered by a nesC event, a callback operation, or some kind of system interruption. Data structures must be created when required.
If you have created a custom data structures in the .defs file, then you must define an equivalent structure in nesC code to be used by the custom operations. The definition of this structure can be placed in any file of your project at your convenience. For example, may be in the VMCustom.h, VMCustomP.nc, or another additional file.
The rules to convert the structures are very simple,
(i) each basic data type of Ceú-T has its equivalent in nesC,
(ii) you must use nesC typedef nx_struct
construct to define the structure, and
(iii) you need to follow the same sequence of the fields definition.
The next code shows an example of an equivalent data structure definitions.
// Céu-T Struct // nesC nx struct
regtype dhtData_t with typedef nx_struct dhtData {
var ubyte stat; nx_uint8_t stat;
var ubyte hum; nx_uint8_t hum;
var ubyte temp; nx_uint8_t temp;
end } dhtData_t;
Each Céu-T type must be associated to respective network type nx as defined in the following tables
Céu-T type | C type |
---|---|
ubyte | nx_uint8_t |
ushort | nx_uint16_t |
ulong | nx_uint32_t |
float | nx_float |
Céu-T type | C type |
---|---|
byte | nx_int8_t |
short | nx_int16_t |
long | nx_int32_t |
During the script execution, a Céu-T command like emit EVTx(Arg1);
will call the command
VM.procOutEvt(uint8_t id,uint32_t value)
, where id is the EVTx numeric identifier
and value is a integer value representing either the value of argument or the address of data structure.
Then, the VM.procOutEvt()
function implements a switch/case
block for
all expected output events.
Each case
tests the id to call a specific function like proc_EvtX(id,value);
.
The new custom operation must be implemented in this function paying attention to get corretly the argument value.
If the value is an integer argument, a cast operation is enough to get the correct value like: uint16_t val = (uint16_t)value;
.
If the value is an address, you must execute the function VM.getRealAddr()
casting the result
to the type of expected data structure.
For example: usrMsg_t* usrMsg = (usrMsg_t*)signal VM.getRealAddr(value);
In the case of a new output event, you must create a new case
inside VM.procOutEvt()
and must implement the respective proc_xxxx()
function.
The following code shows a example of customized output event.
// Custom operation
void proc_req_dht_read(uint16_t id, uint32_t value){
call dht.read((uint8_t)value);
}
// Entry point
command void VM.procOutEvt(uint8_t id,uint32_t value){
switch (id){
...
case O_DHT : proc_req_dht_read(id,value); break;
...
}
}
Function entry points are very similar to those for exit events.
The function VM.callFunction(uint8_t id)
is called when a Céu-T function is executed.
The id argument identify the function and, similar to the output event,
a block of switch/case
calls a specific func_xxx(id);
.
The main differences are that a function allows more than one argument and always returns a basic type value.
Both the arguments and return values are passed using the stack,
this allows the use of functions in expressions.
Similar to the output events, the new custom operation must be implemented in func_xxx(id);
function also paying attention to get corretly the values of each argument.
The arguments must be popped out in reverse sequence, from the last to the first,
using the function VM.pop();
and casting the result to corresponding type.
For example, use the comand uint8_t val = (uint8_t)signal VM.pop();
to
pop a ubyte argument.
If the argument is a data structure address, you must first pop it as ushort type
and use the function VM.getRealAddr()
to get the data pointer value.
After getting the arguments and executing the custom function operation,
you must push the result value to the stack.
Use the function VM.push(val);
to push a basic type value to the stack.
Remenber that you can push only the value type that was defined in the .defs
file.
The following code shows a example of customized function.
// Custom function
void func_pinWrite(uint16_t id){
uint8_t stat=0;
uint8_t port, pin, val;
val = (uint8_t)signal VM.pop(); // pop 3⁰ arg from stack
pin = (uint8_t)signal VM.pop(); // pop 2⁰ arg from stack
port = (uint8_t)signal VM.pop(); // pop 1⁰ arg from stack
stat = pinWrite(port,pin,val); // execute local function
signal VM.push(stat); // Returns the stat value
}
// Entry point
command void VM.callFunction(uint8_t id){
switch (id){
...
case F_PIN_WRITE : func_pinWrite(id); break;
...
}
}
In general, a input event is generated outside the core of the Virtual Machine and
must be notified to the VM engine.
The command VM.queueEvt()
is used to enqueue a Terra event and its respective data
allowing the engine to trigger, as necesserary, the script await
commands.
Depending on the custom operation,
the event source may be a nesC event, a callback operation, or some kind of system interruption.
All options will end up running a local customized function that must call VM.queueEvt()
passing the event ID, the sub-event value, and a pointer to the data buffer.
The sub-event value is an uint8_t value used as base to filter a
specific await that must be triggered for same input event.
For example, in the Radio receive event, the sub-event indicates the user message ID value.
In this case is possible to have a Céu-T trail waiting for a RECEIVE(id) event
with a specific message ID.
The sub-event 0 is used as general sub-event ID that triggers events without arguments.
For example, the command msg = await RECEIVE();
will be triggered for any received message,
no matter what the message ID is.
The RECEIVE event is an example where the VM.queueEvt()
is called twice, one
passing the message ID as sub-event and another passing 0 as sub-event.
As the event receiving is not synchronous with VM engine execution, an event must stores its data in a global buffer. Later, when handling internally the event, the VM engine copies, if necessary, the buffer to the target script variable or data structure. Each event must have its own data buffer. This data buffer is a global variable or global data structure that stores, when necessary, the last event data. If a next specific event may happen before the previous one be handled by the VM engine, the custom implementation may allocate more than one buffer avoiding data overlay. For example, a circular buffer may be used to avoid data overlay.
Next code excerpt shows an example of custom input event implementation for the SEND_DONE
event.
In this event the VM.queueEvt()
is called twice for different sub-event IDs.
nx_uint8_t ExtDataSendDoneError;
event void BSRadio.sendDone(uint8_t am_id,message_t* msg,void* dataMsg, error_t error){
if (am_id == AM_USRMSG){
ExtDataSendDoneError = (uint8_t)error;
signal VM.queueEvt(I_SEND_DONE_ID, ((usrMsg_t*)dataMsg)->type, &ExtDataSendDoneError);
signal VM.queueEvt(I_SEND_DONE , 0, &ExtDataSendDoneError);
}
}
TerraXXX.defs
// Interruption used for DHT sensor
#define INT0 0 // PD0
#define INT1 1 // PD1
#define INT2 2 // PD2
#define INT3 3 // PD3
// DHT11 data structure
regtype dhtData_t with
var ubyte stat;
var ubyte hum;
var ubyte temp;
end
// radioMsg - Radio/Queue Data Message Register
packet radioMsg with
var ubyte type;
var ushort source;
var ushort target;
var payload[20] data;
end
output void REQ_DHT ubyte 10 ;
output void SEND radioMsg 40 ;
input dhtData_t DHT void 6 ;
input ubyte SEND_DONE ubyte 40 ;
input ubyte SEND_DONE void 41 ;
function ushort getNodeId() 0 ;
function ubyte pinWrite(ubyte,ubyte,ubyte) 21 ;
VMCustom.h
enum{
O_DHT =10 ,
O_SEND =40 ,
I_DHT = 6 ,
I_SEND_DONE_ID =40 ,
I_SEND_DONE =41 ,
F_GETNODEID = 0 ,
F_PIN_WRITE = 21 ,
}
VMCustomP.nc
void proc_req_dht_read(uint16_t id, uint32_t value){
call dht.read((uint8_t)value);
}
void proc_send(uint16_t id, uint32_t addr){
usrMsg_t* usrMsg;
usrMsg = (usrMsg_t*)signal VM.getRealAddr(addr);
// .....
}
void func_getNodeId(uint16_t id){
uint16_t stat;
stat = TOS_NODE_ID;
signal VM.push(stat);// Returns a value
}
void func_pinWrite(uint16_t id){
uint8_t stat=0;
uint8_t port, pin, val;
val = (uint8_t)signal VM.pop(); // pop 3⁰ arg from stack
pin = (uint8_t)signal VM.pop(); // pop 2⁰ arg from stack
port = (uint8_t)signal VM.pop(); // pop 1⁰ arg from stack
stat = pinWrite(port,pin,val); // execute local function
signal VM.push(stat); // Returns a value
}
dhtData_t ExtDHTData;
event void dht.readDone(dhtData_t* data){
memcpy(&ExtDHTData,data,sizeof(dhtData_t)); // copy data to event buffer
signal VM.queueEvt(I_DHT , 0, &ExtDHTData); // Generates a Terra event
}
nx_uint8_t ExtDataSendDoneError;
event void BSRadio.sendDone(uint8_t am_id,message_t* msg,void* dataMsg, error_t error){
if (am_id == AM_USRMSG){
ExtDataSendDoneError = (uint8_t)error;
signal VM.queueEvt(I_SEND_DONE_ID , ((usrMsg_t*)dataMsg)->type, &ExtDataSendDoneError);
signal VM.queueEvt(I_SEND_DONE , 0, &ExtDataSendDoneError);
}
}
command void VM.procOutEvt(uint8_t id,uint32_t value){
switch (id){
case O_DHT : proc_req_dht_read(id,value); break;
case O_SEND : proc_send(id,value); break;
}
}
command void VM.callFunction(uint8_t id){
switch (id){
case F_GETNODEID : func_getNodeId(id); break;
case F_PIN_WRITE : func_pinWrite(id); break;
}
}