Developer's Manual - Deep Inside NesCT

Simulation Flow

In order to make NesCT more compatible with TinyOS, NesCT has been going under serious redesign. The most important change that NesCT wants it to have a single simulation context.


What do we mean by context ? TinyOS has two main objects for program executions: tasks and interrupts. Note that, TinyOS is not a real-time operating system. It, however, tries hard to meet deadlines. In order to improve performance and keep operating systems design simple, interrupts are given priority and tasks run while the CPU is in idle state. The race for data between interrupts and tasks are avoided by the usage of atomic keyword. Atomic disallows an interrupt to break the program flow while it runs in a block of code.
In programming language words, the scheduler is like this:
while (1)
{
while (there_is_some_task) consume(task)
sleep();
}
Interrupt handlers are registered with the compiler and therefore, they are automatically called.
 

In the simulation design of NesCT, there is a top module and the rest of the modules are created at the lower level of this module.

With the new design, each time an individual module wants to create a task, it accesses the parentModule by casting it to a local object pointer and enqueues the member function of itself to a global queue in parentModule. Here, we make use of template functions and several libraries. Normally, it is not possible to make a member function of C++ class as a callback function. Our choice is sigc++ library.

NesCT OMNeT++ Scheduler

void cBlink_Blink::activity()
{
        for (;;)
        {
                cMessage * msg= receive();
                if (HandleMsg(msg))
                delete msg;
                execute_tasks();
        }
}

Event Dispatchers for PROVIDED Interfaces


This function dumps the functions of the interfaces that we PROVIDED.

TimerC has an arrayed interface. With the count application, there are multiple timers used by the system: one for genericcomm and the other for the counter application itself.

This tells us that whether an interface is arrayed or not is not handled properly. An arrayed interface is processed as a single interface at DumpFunctionForEvent function if there is only a single connection. However, if there are more than one in connections for an arrayed interface, DumpFunctionForMultipleEventsFromArrayedInterface is called instead.

Event Dispatchers for USED Interfaces


There is this function that dumps the global event dispatcher inside the module DumpFunctionForEventDispatchers. This function dumps the functions of the interfaces that we USE.

For all used interfaces, we collect all function calls in the generic name of the interface (etc., Timer_fired). From here, we make the distribution to Timer_fired and Timer2_fired. On the other hands, functions calls to provided command are made to actual function name. For example, we call Timer2_start if we want to start Timer2. We follow the same design pattern for provided events. Provided events are not collected anywhere; they are just function stubs instead.

These kinds of structures come from this function.

if ((strcmp(modulename,"TimerC")==0) && (strcmp(interfacename,"Timer")==0))
{
if (instance_id == Sense2M_Timer)
return ret =Timer_fired();
}
if ((strcmp(modulename,"TimerC")==0) && (strcmp(interfacename,"Timer")==0))
{
if (instance_id == Sense2M_Timer2)
return ret =Timer2_fired();
}

This function is called for just once among all the interfaces. It is the first appearance of the interface within the module.

There are event dispatchers for all the interfaces that we use.

HandleMsg Design

HandleMsg function is the place where we handle system dependent requests like message reception from UART, radio, timer fire, ADC poll, flash write etc.


Inside HandleMsg, we first check the type of message. This is specified by kind parameter. There are six types of system events that we handle: M_TIMEOUT, M_ADC, M_UART_RECEIVE,M_RADIO_RECEIVE, M_FLASH and M_LOGGER. M_TIMEOUT is a special type of message only used in HPLClock component. We use it to send self messages to the timer providing component. All TinyOS timers are virtual and the actual timer is a periodic that's passed by M_TIMEOUT kind. Similarly, M_ADC is also a special type of message. It is included in components that provide ADC interface. We use it to emulate sensing function. At this current moment we produce an arbitrary random number and return it as a result. The rest of them are self explanatory.

Multiple Interface Provision

There are times when one component wants to export multiple instances of the same interface. The one, that's going to be executed, is specified by the parameter id passed along with other parameters.

Have a look at TimerM component. This component provides an array of Timer interface while using leds, clock and powermanagemnt interfaces from other components.

The original method name in the nc source code was :

command result_t Timer.start[uint8_t id](char type, uint32_t interval)

and the generated function header is :

result_t Timer_start(char *modname,char *ifacename,uint8_t instance_id,char type,uint32_t interval);


As a compensation of these function call names, all the calls in the code to these parameterized functions are also renamed.

If the original call was:
ret = Timer_stop[instance_id]();

the new call is:
ret=Timer_stop(instance_id);

 

Possible Interface Configurations

Arrayed Interface Multiple Connections

Arrayed Interface Single Connection

Arrayed Interfaces of Multiple Modules Going to Same Arrayed Module

Single Interface Single Connection

Single Interface Multiple Connection

Multiple Interface of Same Kind with Multiple In Connections

 Directions for Shared Methods

 

Task Posts


Due to limitations coming from method pointers, we also had to change the way we post tasks. Every post request in the code is appended with a reference and name of the class.

post(&cTimerM::TimerM_HandleFire);

Interaction with other Components

For every function call that we make, we generate stub functions in the same class. These functions make a call to the actual function and return the result. Due to component structure of nesC, this call sequence may be longer (deeper in stack size) than you may expect.

What the heck about qsort ?

We had to port qsort from libc into the generated code due to translation difficulties. Qsort normally uses one comparator function pointer as an argument. When we translate NesC code to C++, standard function call interface is no longer compatible. We had to build our own.

Unique vs arrayed methods.

A subclass has arrayed methods if it provides or uses a set of interfaces. a subclass has unique call if we have a unique assignment to match one of the arrayed methods.
 

Folders


NesCT uses temp directory for producing preprocessed files. For that reason, it may perform many I/O functions in this directory. It would be wise to not put any important files in this directory.

After the preprocessor completes reduction and generates output in temp directory, next step is to parse the files. NesC is going to parse each file and generate an OMNeT++ compatible C++ file for each component of TinyOS. These files reside in gen directory. All the generated files are included in a generated file named simstart.cc in the main directory. By this way, a user need not add each generated file to his/her makefile.

What if xxx.nc file is not found ?

NesCT will not be able to find a file, if it does not reside in one of $TOSINTDIR,$SYSTEMDIR and components directory. In such case, NesCT expects user to copy these files to components directory.

What if a header file is not found ?

NesCT searches for include files in $TOSINTDIR,$SYSTEMDIR, gen and include directories. If your include file is not in one of these directories, you'll have to copy it to gen directory or include directory yourself.

Debugging simulations.


In order to ease usage, we have borrowed debugging system from TOSSIM. As you do in TOSSIM, you use DBG environment variable to filter debug messages. USR1,USR2 and USR3 are user debugging filters. If you want to see the internals of the simulation set the DBG parameter to more than one filter.

You set the debug environment by "export DBG=usr1" in Linux and "set DBG=usr1" in windows.
 

Logger or Flash Disk Support


We create a file for each logger device (512k memory) in logger directory. File names are given according to node numbers like 1.dat for node number 1 etc. Each read and write operation of EEPROM goes to these files.

How to simulate your application ?


The easiest way is to copy your components to components directory of tictoc. Replace application.nc with your starter component. Run nesct.exe, followed by a make clean and make. This should build tictoc in the directory.

[root@sinan components]# cp Blink.nc ../Application.nc
[root@sinan components]# cd ..
[root@sinan tictoc]# ./nesct.exe -I./include Application.nc
[root@sinan tictoc]# make clean
[root@sinan tictoc]# make
[root@sinan tictoc]# ./tictoc
 

How to write portable code ?


We now define PLATFORM_OMNET in our Makefile. Using this definition you may write OMNeT specific code. Example

#ifdef PLATFORM_OMNETPP
...
#endif
 

How is the component graph ?


Packet Send Path
Surge -----> MultiHopEngineM -----> QueuedSendM
QueuedSendM -> GeneericcommPromiscous -> AmPromiscous -> UartFramed -> Bcast

RadioCRCPacket ---> MultiHopRouter
MultiHopLEPSM
 

Packet Receive Path


TossimPacketM-> RadioCRCPacket -> AMPromiscuous -> GenericCommPromiscous ----> MultiHopEngineM --> MultiHopLEPSM -->

 

What is a shared method ?


Besides from connecting interfaces of different modules to each other, it is also possible to make connection between regular functions. If there is such a method it is going to be stored in shared_methods structure of the module.
 

What about errors about uniquecount ?

NesCT does not support uniquecount function. uniqueCount function is mostly used to allocate data structures for arrayed interfaces. uniqueCount returns the number of connections made to this interface. we do not have the capability to make this number a constant.
 

Here is an example from Matchbox.h:

original:
enum {
 FS_NUM_RFDS = uniqueCount("FileRead")
 FS_NUM_WFDS = uniqueCount("FileWrite")
}

updated:

#define SOME_MAX_NUMBER 10
#define FS_NUM_RFDS SOME_MAX_NUMBER
#define FS_NUM_WFDS SOME_MAX_NUMBER
 

TOS Example directory comes with two modifications to uniqueCount. First one is in Matchbox.h and another is in PageEEPROMShare.nc. If defined maximums are not enough, redefine it. One more ByteEEPROMAllocate.nc.
 

What about unique call errors ?


NescT does not like unique function. Do not expect nesct to find you a value from an array interface. Replace your unique call with a constant.

IFS.h has a unique(...) call and it's been replaced with 50

C++ does not allow you to use offsetof with dynamic parameters. offsetof function is strictly typed in C++ and it will not allow you to compile this expression. Here is an example from Remote.nc

replyMsgLen = offsetof(struct ReplyMsg, data[len]);

Replace this with
replyMsgLen = offsetof(struct ReplyMsg,data);
replyMsg += len;

Interface directions ?

Interface directions are named as directionIN,directionOUT and directionFORWARD. Direction assignment depend on the interface type.

Example: TimerC provides interface Timer and Surge uses Timer from TimerC.

Surge ----OUT --------------------------------IN---TimerC

According to NescT Surge has one interface with a direction of OUT and TimerC has one IN direction coming from Surge.

It is also possible to forward interfaces between components with nesc. If you have a look at TimerC, you'll see that TimerC forwards Timer interface to the actual timer implementation named TimerM. Direction name depends on whether the component uses or provides that interface. If it provides it as it's the case for TimerC, it looks like this in this case.

TimerC -- FORWARD -----------------------------IN---TimerM

Example: MultiHopRouter uses ReceiveMsg interface from MultiHopEngineM

MultiHopRouter --- IN ------------ OUT--- MultiHopEngineM

Packet transmission and reception logs

A directory named log is created relative to the simulation run directory. This directory contains the binary representations of all messages received over the uart and radio. File indices are given according to the node number. You may use it for debugging purposes.


How to Collect Statistics?

I recommend you to read OMNeT++ tutorial before starting to collect statistics. You need to understand what plove and scalar are doing. If you want to collect statistics from NesC source code, you can include your classes to the source code by enclosing it inside PLATFORM_OMNETPP definition.

#ifdef PLATFORM_OMNETPP
long numNack=0;
cLongHistogram NackStats;
#endif


When the simulation completes, you'll want to record the results to a file. This file is scalars file and you can use recordScalar function for this purpose. You can record the minimum, maximum, average, median or other value of the data collected. If you want a function of yours to be called at the end of the simulation create function in your nc file with the name omnetpp_finish. Finish method of generated class will call this function.


#ifdef PLATFORM_OMNETPP
void omnetpp_finish()
{
// This function is called by OMNeT++ at the end of the simulation.
recordScalar("#Pump", PumpStats.mean());
recordScalar("#Repair",RepairStats.mean());
recordScalar("#Nack", NackStats.mean());
}
#endif

 

 

This project has been supported by Featherlight project at University of Twente, the Netherlands and European Embedded WiseNt project at Yeditepe University.