|
|
Lesson 5: Adding Functions
Last modified: Wed Jul 14 11:24:01 PDT 2004
|
|
Introduction
|
Lessons 1-3 show you how to install a Maté
and write TinyScript programs. Lesson 4 shows you
how to build a new Maté environment from
existing functions and handlers. This lesson
describes how to write new functions and how to
incorporate them into your VMs. Doing so allows you
to extend VMs to meet the computational requirements
of particular applications.
|
Maté's dbg Statements
|
The existing Maté code uses TOSSIM's
dbg functionality heavily; we strongly
recommend that you test any new function or event in
TOSSIM before running it on a mote. Maté uses
three DBG settings: USR1, USR2, and USR3. Each has
its own partilcuar use.
USR1 pertains to execution; almost every
instruction has a USR1 statement to say that it's
executing, so a tester can see what happens as a
program executes. Also, events that trigger contexts
to run or halt (as you'll see in Lesson 6) fall
under USR1.
USR2 pertains to synchronization. USR2 statements
instrumetn the Maté scheduler, its wait and
ready queues, and system locks. Unless you are
delving into Maté much more deeply than these
tutorials do, you shouldn't need to add any USR2
statements.
USR3 pertains to code propagation and
installation: the capsule propagation component,
MVirus, uses USR3 for information on its
timers and state. You shouldn't need to add any USR3
statements.
|
Adding a Simple Function
|
A Maté function is a TinyOS component that
provides the MateBytecode interface, which
has two commands:
interface MateBytecode {
command result_t execute(uint8_t instr, MateContext* context);
command uint8_t byteLength();
}
byteLength() returns the length of an
instruction in bytes. Function's byteLength
must always return 1. execute is the actual
logic of the function. execute takes two
parameters. The first, instr is the opcode
of the instruction that led to the function being
called; this can be ignored. The second,
context, is the execution context that is
calling the function. context can be
used to access execution state, such as parameters
passed to the function.
In addition to a TinyOS component, a function
must have an ODF file that describes it. The
Maté toolchain uses the description to check
that a program passes the right number of
parameters, to figure out which component implements
the function, and provide information to a user. An
ODF file contains a FUNCTION element with several
tags. For example, this is the ODF for the
bfull function:
The following table describes the set of tags
that a FUNCTION element must have, and their
meanings. There are also several optional tags,
which you'll cover later.
name |
The name of the function as a user writing a
script will use it. |
opcode |
The name of the function as the toolchain
and VM use it. While the name tag should be
human readable, the opcode tag need not be. When
VMBuilder generates the files for a VM, it
generates a constant based on the opcode tag to
identify the binary instruction. |
numParams |
The number of parameters that the function
takes. For example, bfull takes a
single parameter. The TinyScript compiler checks
that the number of parameters passed to a
function match this value. |
param1...N |
For each parameter (specified by numParams),
there must be a tag of the form paramN,
where N is the parameter number, starting
at 1. A function with zero parameters has no
param tags For example, a function that
takes two parameters must have a param1
tag and a param2 tag. Each tag should
say what type the corresponding parameter
is. These types are currently unchecked by the
TinyScript compiler; they serve only as
documentation. For example, bfull's
parameter is a buffer. |
returnval |
Either true or false;
denotes whether the function has a return
value. bfull has a return value (an
integer, denoting whether the buffer is full). |
desc |
Text, describing the function. The Scripter
and VMBuilder GUIs display this text. |
To understand how writing a function works,
you'll start by writing increment, a very
simple new function. increment takes a
single integer as a parameter and returns it
incremented by one. The first step is to write the
function description file. Make a new directory in
tos/lib/VM, named extensions:
mkdir extensions
In the extensions directory, open a file named
"OPincrement.odf". This will contain the
FUNCTION element describing your function. The
function takes a single parameter, an integer, and
has a return value. Write the FUNCTION element
describing increment. It should look something like
this:
<FUNCTION name=increment opcode=incr numparams=1 param1=integer returnval=TRUE desc="Takes an integer as a parameter; returns that integer incremented by one.">
By default Maté toolchain looks for a
function in a TinyOS component generated from the
opcode tag. If the tag's value is
incr, Maté looks for a component named
OPincr (the nesC rules require this component
to be in the file OPincr.nc). The component should
provide a single instance of the
MateBytecode interface, and nothing else:
it will almost always be a configuration. For
example, this is OPbfull.nc:
includes Mate;
configuration OPbfull {
provides interface MateBytecode;
}
implementation {
components OPbfullM, MStacksProxy, MTypesProxy;
MateBytecode = OPbfullM;
OPbfullM.Stacks -> MStacksProxy;
OPbfullM.Types -> MTypesProxy;
}
OPbfull takes the actual implementation
of the function -- in OPbfullM -- and wires
it to the needed components. The configuration then
exports the function as a provided
interface. This is OPbfullM.nc:
module OPbfullM {
provides interface MateBytecode;
uses interface MateStacks as Stacks;
uses interface MateTypes as Types;
}
implementation {
command result_t MateBytecode.execute(uint8_t instr,
MateContext* context) {
MateStackVariable* arg = call Stacks.popOperand(context);
dbg(DBG_USR1, "VM (%i): Checking if buffer full.\n", (int)context->which);
if (!call Types.checkTypes(context, arg, MATE_TYPE_BUFFER)) {return FAIL;}
call Stacks.pushValue(context, (arg->buffer.var->size == MATE_BUF_LEN)? 1: 0);
return SUCCESS;
}
command uint8_t MateBytecode.byteLength() {return 1;}
The implementation of the function,
execute, has five lines. We'll step through
them, one by one, but touch on the interfaces it
uses first.
bfull uses two interfaces, MateStacks
and MateTypes. Looking back at the
configuration, the component MateStacksProxy
provides the first, while MateTypesProxy provides
the latter. MateStacks has a
set of commands for manipulating the operand stack
of a Maté context. Before performing an
operation, the VM pushes operands onto the stack;
the operation then pops the operands, using them as
parameters. For example, to add two integers, a
program pushes the two integers onto the operand
stack, then executes the add
instruction. The instruction's implementation pops
the two integers off the stack, adds them, and
pushes the result back onto the stack. TinyScript
passes function parameters on the operand stack; the
buffer a program is testing to see if it's full is
on top of the stack.
The operands that MateStacks returns
from popOperand commands must be handled
carefully. Specifically, they can be corrupted or
invalidated by any command that pushes something
onto the operand stack. The pointer that
popOperand returns is a pointer onto the
actual stack. If something is pushed, the region of
the stack where the return value of the pop points
to may be overwritten. Therefore, when writing a
function, you should process all operands before
pushing anything back onto the stack.
MateTypes has commands for examining the
types of operands. There are two types of commands,
checks and queries. Checks begin with
check, while queries begin with
is. For example, MateTypes has a
checkInteger and an
isInteger. Both kinds of commands return
TRUE if the operand matches the type. In addition to
returning the result, check commands will trigger an
error condition if the test fails. For example, if
you call isInteger on a buffer, it will
return FALSE; if you call checkInteger on a
buffer, it will return FALSE and trigger a type check
error in the core VM, halting execution.
The first line of execute pops the top
of the operand stack, followed by a dbg
statement for when using TOSSIM. Functions should
always print a dbg message of type USR1
when they execute; this makes debugging in TOSSIM
much easier.
MateStackVariable* arg = call Stacks.popOperand(context);
dbg(DBG_USR1, "VM (%i): Checking if buffer full.\n", (int)context->which);
If the programmer called bfull properly,
then this argument should be a buffer; the third
line checks that this is the case:
if (!call Types.checkTypes(context, arg, MATE_TYPE_BUFFER)) {return FAIL;}
If the operand is not a buffer, then
checkTypes will trigger an error in the VM;
the instruction exits immediately, and the VM halts
execution.
Since the operand is a buffer, the function does
the actual work, checking if the buffer is full:
call Stacks.pushValue(context, (arg->buffer.var->size == MATE_BUF_LEN)? 1: 0);
This line pushes an integer onto the operand
stack: if the buffer is full, it pushes 1, if false
it pushes 0. Operands (MateStackVariabless)
are a union of their possible types: arg->buffer
accesses the variable as a buffer, while arg->value
accesses it as a value. In TinyScript, the actual
structure is:
typedef struct {
int16_t var;
} MateValueVariable;
typedef struct {
uint8_t type;
uint8_t size;
int16_t entries[MATE_BUF_LEN];
} MateDataBuffer;
typedef struct {
MateDataBuffer* var;
} MateBufferVariable;
typedef struct {
uint8_t type;
union {
MateValueVariable value;
MateBufferVariable buffer;
};
} MateStackVariable;
These and other Maté types can be found in
tos/lib/VM/types/Mate.h.
The function has consumed its parameter (it
popped the buffer off the operand stack), and
produced a return value (it pushed the result onto
the operand stack).
With increment, we want to do something
similar to bfull: it takes a single
parameter, and returns a value. In the
extensions directory, open a file named
OPincrementM.nc and type:
module OPincrementM {
provides interface MateBytecode;
uses interface MateStacks as Stacks;
uses interface MateTypes as Types;
}
implementation {
command result_t MateBytecode.execute(uint8_t instr,
MateContext* context) {
MateStackVariable* arg = call Stacks.popOperand(context);
dbg(DBG_USR1, "VM (%i): Incrementing an integer.\n", (int)context->which);
if (!call Types.checkInteger(context, arg)) {return FAIL;}
call Stacks.pushValue(context, (arg->value.var + 1));
return SUCCESS;
}
command uint8_t MateBytecode.byteLength() {return 1;}
}
It also needs a configuration. Open
OPincrement.nc and type:
includes Mate;
configuration OPincrement {
provides interface MateBytecode;
}
implementation {
components OPincrementM, MStacksProxy, MTypesProxy;
MateBytecode = OPincrementM;
OPincrementM.Stacks -> MStacksProxy;
OPincrementM.Types -> MTypesProxy;
}
Now that you've written all three files (the ODF,
the configuration, and the implementation),
increment is ready to use. Go back to your
specification file for SimpleVM,
simple.vmsf, and add the following two lines:
<SEARCH PATH="../extensions">
<FUNCTION NAME="increment">
The first line tells VMBuilder to look in your
new extensions directory, while the second tells it
to include your new function. Rebuild the VM and
install it on a mote. You can now write CntToLeds
using the function instead of addition:
private counter;
counter = increment(counter);
led(counter % 8);
|
Maté Abstractions and Split-Phase Operations
|
When writing the increment function, the
configuration wired OPincrementM to a
component named MateStacksProxy, which
provided interfaces for manipulating the operand
stack and type checking. You can find the full set
of data abstraction components in
tos/lib/VM/components. They include:
- MateEngine: The core VM scheduler. It
signals when the VM has rebooted (e.g., when new
code arrives), which contexts and some functions
need to know, to clear state properly.
- MBufferProxy: Commands for accessing
data buffers.
- MContextSynchProxy: Commands and events
for controlling context execution: the section on
writing a new handler component goes into how to
use this component's interface.
- MErrorProxy: Commands for triggering an
error condition in the VM.
- MHandlerStoreProxy: Commands and events
for monitoring and controlling code propagation:
the section on writing a new handler component
goes into how to use this component's interface.
- MQueueProxy: Commands for manipulating
context queues. Functions that encapsulate
split-phase operations usually need to maintain a
queue of pending requests, if more than one
context wants to execute it at once.
- MStacksProxy: Commands for manipulating
the operand stack.
- MTypesProxy: Commands for type checking.
- MTypeManager:: Commands for transforming
data types to and from network representations.
There are a few additional components, such as
MateLocksProxy, but they are only relevant
if you're modifying VM internals, as opposed to
adding new functions and contexts. Using "Proxy"
components allows components to refer to VM
abstractions in terms of interfaces, instead of
implementation. For example, MHandlerStore is a a
component that provides an implementation of code
storage; wiring to MHandlerStore specifies an
implementation. If components wired to
MHandlerStore, then changing the implementation used
would either require changing all of the components
that wire to it (to wire to something else),
changing MHandlerStore itself, or trying to use
search path tricks to use a different component also
named MHandlerStore.
In contrast, MHandlerStoreProxy is a
configuration wrapper around an implementation; by
having components wire to MHandlerStoreProxy, one
can change the implementation the entire VM uses by
modifying only this component.
Earlier in the lesson, you wrote a simple
function that incremented an integer. Functions can
also encapsulate underlying TinyOS operation. For
example, the rand() Maté function
calls the standard Tiny)S
RandomLFSR.Random.random() command to
generate a random number:
command result_t MateBytecode.execute(uint8_t instr,
MateContext* context) {
uint16_t rval = call Random.rand();
dbg(DBG_USR1, "VM (%i): Pushing random number: %hu.\n", (int)context->which,
rval);
call Stacks.pushValue(context, rval);
return SUCCESS;
}
Functions can also encapsule split-phase
operations, such as sending a packet or sampling a
sensor. This is a bit more complex than calling a
TinyOS command, and requires using several of the
abstractions described above. A function that
encapsulates a split-phase operation has to interact
with the Maté scheduler and synchronization
subsystem, in order to suspend and resume the
executing context properly. Some split-phase
operations require the function component to
allocate some state; for example, the send
function component (OPsendM) allocates a
TOS_Msg for sending. The implementing module has to
ensure atomic access to this state, to make sure
that it doesn't corrupt it if multiple contexts
execute the function concurrently.
Finally, the function must keep track of when the
VM reboots; if new code arrives while it's in the
middle of its split-phase operation, then it must
keep the lock on any state, but can release the
executing context. This is the basic pseudocode for
a function that encapsulates a split-phase
operation. There are three entry points:
MateBytecode.execute(), the event
signalling completion of the split-phase event, and
the reboot event signalling that the VM has
rebooted.
MateQueue queue; // A wait queue
MateContext* execing; // The currently executing context
bool busy; // Whether the state is in use
State state; // State that requires atomic access
// Only execute if busy == FALSE, try starting the split-phase op
// If you can't start it, put the context on the wait queue
try_exec(context) {
// access state, etc
if (split_phase_command(state) == SUCCESS) {
execing = context;
mark context as executing operation
busy = TRUE;
call Synch.yieldContext(context); // We've started the operation,
// Let the scheduler know
}
else { // For some reason we can't start the op, put on wait queue
mark context as waiting
put context on queue
}
}
// MateBytecode.execute: a context is trying to execute the function
execute(context) {
// Somebody is using the shared state: put the context on a wait queue
if (busy) {
context->state = WAITING;
enqueue(queue, context);
}
// Noboody is using the shared state: try starting the op
else {
try_exec(context);
}
}
// Split phase op complete: if it's for us, resume who was executing,
// try letting another context execute the operation
split_phase_event(State s) {
if (s != state) {return;} // Not for us
if (execing != NULL) {
call Synch.resumeContext(execing);
execing = NULL;
}
busy = FALSE;
if (!empty(queue)) {
pull next context off queue
try_exec(context);
}
}
// Rebooting means that nobody is execing the op any longer,
// and nobody is waiting for it; if a completion event comes in,
// execing must be null or it may try to resume somebody who isn't waiting.
reboot() {
execing = NULL;
empty queue
}
There's a good deal of pseudocode, because the
function has to deal with many corner cases. The
function needs to keep track of whether anyone is
accessing the shared state (the busy flag),
whether there is a context waiting to resume when
the operation completes (the execing
pointer), and whether any contexts are waiting for
the shared state to be available or the underling
resource (the wait queue).
A function which has a wait queue must handle
reboot events from the VM. Otherwise, system state
can become inconsistent if the VM reboots (due to
new code arriving) in the middle of the
operation. Rebooting requires all of the executing
contexts to halt and reset, waiting for their
triggering event. However, when the split-phase op
completes, then the function component will try to
resume the context that was executing it. This could
cause a halted context to start execution when it
shouldn't. When the VM reboots, the function
component must clear its wait queue and executing
context. The components that implement individual
contexts are responsible for clearing their state;
all the function needs to do is clear its queue.
However, as the operation is still executing, it
should not clear the busy flag on the shared
state. If, after reboot, a context tries to execute
the function before the underlying operation
completes, then the component should put the context
on a wait queue. When the split-phase operation
completes, releasing the shared state, the component
can let a context on the wait queue execute. If the
function component wants a context to stop executing
Maté instructions(e.g., wait on a queue), it
must change the context's state variable
from MATE_STATE_RUN; otherwise, the
scheduler will continue to have the context issue
instructions.
Here is the full code of the uart
function, which sends a packet to the UART. Its
shared state is the message buffer it passes to
SendMsg.send():
includes Mate;
module OPuartM {
provides {
interface MateBytecode;
interface StdControl;
event result_t sendDone();
}
uses {
interface MateQueue as Queue;
interface MateContextSynch as Synch;
interface MateError as Error;
interface MateTypes as TypeCheck;
interface MateType as Type[uint8_t typeID];
interface MateStacks as Stacks;
interface MateEngineStatus as EngineStatus;
interface SendMsg as SendPacket;
}
}
implementation {
MateQueue sendWaitQueue;
MateContext* sendingContext;
bool busy;
TOS_Msg msg;
command result_t StdControl.init() {
call Queue.init(&sendWaitQueue);
sendingContext = NULL;
busy = FALSE;
return SUCCESS;
}
command result_t StdControl.start() {
return SUCCESS;
}
command result_t StdControl.stop() {
return SUCCESS;
}
result_t trySend(MateContext* context) {
MateStackVariable* arg = call Stacks.popOperand(context);
if (!call Type.supported[arg->type]()) {
call Error.error(context, MATE_ERROR_TYPE_CHECK);
dbg(DBG_USR1|DBG_ERROR,
"VM (%i): UART tried to send data type %i, \
doesn't have a network representation.\n",
(int)context->which, (int)arg->type);
return FAIL;
}
else {
uint16_t maxLen = TOSH_DATA_LENGTH;
uint8_t len = call Type.length[arg->type]((void*)arg->buffer.var);
MateStructMsg* destMsg = (MateStructMsg*)msg.data;
if (len >= maxLen) {
call Error.error(context, MATE_ERROR_BUFFER_OVERFLOW);
return FAIL;
}
destMsg->type = arg->type;
call Type.encode[arg->type](&(destMsg->data), arg->buffer.var);
if (call SendPacket.send(TOS_UART_ADDR, TOSH_DATA_LENGTH, &msg) == SUCCESS) {
dbg(DBG_USR1,
"VM (%i): OPuartM sending data of length %i to uart.\n",
(int)context->which, (int)len);
busy = TRUE;
sendingContext = context;
context->state = MATE_STATE_BLOCKED;
call Synch.yieldContext(context);
}
else {
call Stacks.pushOperand(context, arg);
context->state = MATE_STATE_WAITING;
call Queue.enqueue(context, &sendWaitQueue, context);
}
}
return SUCCESS;
}
command result_t MateBytecode.execute(uint8_t instr,
MateContext* context) {
if (busy) {
dbg(DBG_USR1,
"VM (%i): Executing OPuart, but UART busy, put on queue.\n",
(int)context->which);
context->state = MATE_STATE_SEND_WAITING;
call Queue.enqueue(context, &sendWaitQueue, context);
return SUCCESS;
}
else {
dbg(DBG_USR1, "VM (%i): Executing OPuart.\n", (int)context->which);
return trySend(context);
}
}
command uint8_t MateBytecode.byteLength() {return 1;}
event result_t SendPacket.sendDone(TOS_MsgPtr mesg, result_t success) {
if (mesg != &msg) {return SUCCESS;}
busy = FALSE;
if (sendingContext != NULL) {
call Synch.resumeContext(sendingContext, sendingContext);
sendingContext = NULL;
}
if (!call Queue.empty(&sendWaitQueue)) {
MateContext* context = call Queue.dequeue(NULL, &sendWaitQueue);
trySend(context);
}
dbg(DBG_USR1, "VM: UART send completed with code. %i\n", (int)success);
return SUCCESS;
}
// It may be that we couldn't send a packet because another component was.
// If someone is waiting and we're not sending, try sending.
event result_t sendDone() {
if (!busy &&
sendingContext == NULL &&
!call Queue.empty(&sendWaitQueue)) {
MateContext* context = call Queue.dequeue(NULL, &sendWaitQueue);
trySend(context);
}
return SUCCESS;
}
event void EngineStatus.rebooted() {
sendingContext = NULL;
call Queue.init(&sendWaitQueue);
}
}
OPuartM introduces another Maté framework
service, MateType. This interface has commands
for components to transform an on-mote data structure
into a network-friendly representation. This is important
for functions to be language independent. To take
a program type and convert it to a network representation,
a component calls the encode command. To
convert a network type back into an on-mote representation,
the component calls the decode command. A
component, MTypeManagerProxy, provides a parameterized
interface whose parameter is a type ID of the type.
To include an additional type into a VM,
a component must wire to MTypeManager. This is how,
for example, TinyScript includes the buffer type.
The MBuffer component provides the MateType interface,
and wires to MTypeManager so that calls which go
to MTypeManagerProxy for encoding/decoding buffers
pass through to MBuffer. If any component (such as the one
which implements the opcode for putting an element in
a buffer) wires to the
MBuffer component, that is, uses buffers, then
full support for the buffer type will be included.
Function components can interact with the
Maté synchronization component
(MContextSynchProxy) to improve
parallelism. When a context executes, it has
exclusive access to any shared resources (such as
shared variables or buffers) that it uses. This
prevents race conditions between concurrent
contexts. A context can release shared resources it
holds as it executes; this allows other contexts
that need them to execute. The most useful time to
release resources is during split-phase
operations, as the context executing the operation
cannot use the resources and is not running (it's
waiting for the operation to complete).
As a context executes, it may mark some resources
as safe to release. A function encapsulating a
split-phase operation should have the context
actually release the resources it has marked by
calling MateContextSynch.yield(). If the
function calls this, then it must call
MateContextSynch.resume() when the context
resumes (the operation completes). yield()
makes the context yield any resources it has marked,
and resume contexts that were waiting for those
resources. resume() tells the
synchronization component that the context is ready
to run again. If there are any resources the context
needs to reacquire but are held by another context,
then resume() will cause the context to
wait until those resources are available (either
through another yield, or the holder
halting).
In the above uart function, the buffer
passed on the operand stack is a shared
variable. The function cannot yield a context until
its state has been copied into the TOS_Msg to send
(otherwise, it's possible someone else may modify
it).
|
A Complex Function
|
Now that you've seen a complex Maté
function, you'll write your own. Instead of
encapsulating a split-phase operation, you'll take
increment and have it perform its
computation in a separate task: posting the task is
equivalent to starting a split-phase operation,
while the task running is equivalent to a completion
event. However, by using a task, it will be easier
to debug and test what's going on. We strongly
recommend that you first run and test your function
in TOSSIM, before trying a mote.
Return to OPincrementM.nc. Because it
will increment an integer in a task, the component
needs to store the value in its frame. It also needs
a wait queue, a context pointer, and a busy
flag. Add the following variables to
OPincrementM's frame:
MateContext* executingContext;
MateQueue waitQueue;
bool busy;
int16_t value;
Some of these variables need initialization;
OPincrementM must provide
StdControl. Also, the component must
monitor when the VM reboots, so it must use
MateEngineStatus. Finally, it manages a
queue, so needs to use MateQueue. Change
the module declaration to:
module OPincrementM {
provides {
interface StdControl;
interface MateBytecode;
}
uses {
interface MateStacks as Stacks;
interface MateTypes as Types;
interface MateQueue as Queue;
interface MateEngineStatus as EngineStatus;
}
}
The next step is to initialize the variables in
StdControl.init(). Add the following code
to the module implementation:
command result_t StdControl.init() {
call Queue.init(&waitQueue);
executingContext = NULL;
busy = FALSE;
return SUCCESS;
}
command result_t StdControl.start() {return SUCCESS;}
command result_t StdControl.stop() {return SUCCESS;}
Next, add the handler for when the VM reboots:
event void EngineStatus.rebooted() {
executingContext = NULL;
call Queue.init(&waitQueue);
}
All that's left to write are the execute
and completion code. Most complex functions in the
standard Maté release have a "try to execute"
function in the component, as both execute
and the completion event need to get contexts to
execute the operation. Putting it in a separate C
function allows both of them to share the same code
and reduces the chances of bugs. The component only
calls try_execute if the local state is
free. Change MateBytecode.execute to:
command result_t MateBytecode.execute(uint8_t instr, MateContext* context) {
if (busy) {
dbg(DBG_USR1,
"VM (%i): Executing increment, but busy, put on queue.\n",
(int)context->which);
context->state = MATE_STATE_WAIT;
call Queue.enqueue(context, &waitQueue, context);
return SUCCESS;
}
else {
dbg(DBG_USR1, "VM (%i): Executing increment.\n", (int)context->which);
return try_execute(context);
}
}
Next, add the code for actually trying to execute
the function: this involves type-checking the
parameter, saving the local state and posting the
task:
result_t try_execute(MateContext* context) {
MateStackVariable* arg = call Stacks.popOperand(context);
if (!call Types.checkInteger(context, arg)) {return FAIL;}
else {
executingContext = context;
busy = TRUE;
value = arg->value.var;
post incrementTask();
context->state = MATE_STATE_BLOCKED;
call Synch.yieldContext(context);
return SUCCESS;
}
}
Finally, there's the task itself, which acts as
the completion event:
task void incrementTask() {
busy = FALSE;
if (executingContext != NULL) {
call Stacks.pushInteger(executingContext, value + 1);
call Synch.resumeContext(executingContext, executingContext);
executingContext = NULL;
}
if (!call Queue.empty(&waitQueue)) {
MateContext* context = call Queue.dequeue(NULL, &waitQueue);
try_execute(context);
}
dbg(DBG_USR1, "VM: Increment completed.\n");
}
Many Maté data abstractions -- queues, for
example -- require a context as their first
argument. This parameter represents the context
performing the action (if there is one); this is so
that, if an error occurs, the VM can report which
context caused the error. Passing NULL means there
isn't really a context responsible for the call (as
is the case, for example, in dequeueing a context
off of the wait queue in incrementTask): if
an error occurs, Maté will report that it
does not know who is responsible. Commands that
actually modify or access the context, however, such
as operand stack manipulation, cannot take NULL as a
parameter. Calling Queue.dequeue with NULL
as the first parameter is safe, calling
Stacks.pushInteger with NULL as the first
parameter will most probably cause the node to
crash.
Because your SimpleVM already includes
increment as a function, all you need to do
is recompile it. Run the VM in TOSSIM and inject a
handler that calls increment. If you've set DBG to
include usr1, you'll see messages showing
you that increment has executed as a split-phase
operation (the separate execute and task
messages).
Each function must have a configuration which
wires to the appropriate interface on a module; this
means that a particular module can implement more
than one function. This is useful for functions that
share state. For example, if you wanted to write a
set of functions for accessing non-volatile storage,
you could implement them in a single module,
allowing you to easily keep track of state relevant
to all of them (such as a seek pointer).
|
Optional Tags
|
You've implemented increment as a
split-phase operation, which now interacts with the
Maté scheduler. For the TinyScript compiler
to be aware that the function blocks, you have to
add an additional tag to the ODF file. Open
OPincrement.odf and add the following tag
to the FUNCTION element:
schedpoint=true
If you forget to include this tag, nothing bad
will happen: the compiler will just think it is a
regular instruction, and not try to have a program
release shared resources which are safe to
release.
As described in Lesson 4, a FUNCTION element in a
VM specification file causes VMBuilder to look for a
ODF. For example, <FUNCTION
name=increment> causes VMBuilder to look for
a file named OPincrement.odf. When
VMBuilder loads this file, it uses the
opcode tag of the FUNCTION element to
determine what component implements the
function. For example, in your ODF for
increment, opcode=increment leads
VMBuilder to wire to the nesC component
OPincrement, contained in
OPincrement.nc, for the function
implementation. You can tell VMBuilder to wire to a
different component. This is useful when the opcode
name is not a good name for a component, as
functions are case insensitive in TinyScript. For
example, if you add the tag
component=IncrementInteger
to the FUNCTION element, then VMBuilder will wire
the increment function to a component named
OPIncrementInteger, which is implemented in
OPIncrementInteger.nc.
|
Conclusion
|
You implemented a new function,
increment, and incorporated it into your
SimpleVM. You then re-implemented it as a
split-phase operation, and learned how to let the
TinyScript compiler know that it is a blocking
call. The next and final lesson describes how to add
new execution events to a VM.
< Previous Lesson |
Next Lesson > |
Top
|
|