TOSThreads Tutorial (TOS 2.1.1)
This lesson discusses the TOSThreads library with the following objectives:
- Give readers an high-level overview of the library.
- Give a summary of the currently available services supported by the TOSThreads library.
- Demonstrate how to use the nesC API to create and manipulate both static and dynamic threads.
- Demonstrate how to use the C API to create and manipulate threads.
- Demonstrate how to add TOSThreads support to new system services.
Note: TOSThreads is part of TinyOS since release 2.1.
Contents
TOSThreads
TOSThreads combines the ease of a threaded programming model with the efficiency of a fully event-based OS. In fact, TOSThreads adds an additional execution class to TinyOS in the following sense. The existing TinyOS concurrency model has two classes of execution: synchronous (tasks) and asynchronous (interrupts). They follow a hierarchy: asynchronous code can preempt synchronous code but synchronous code is run-to-completion. TOSThreads extends this concurrency model with user-level application threads. Application threads cannot preempt either synchronous or asynchronous code, but can preempt one another. Application threads synchronize using standard primitives such as mutexes, semaphores, barriers, and condition variables.
Compared to earlier threads packages designed for TinyOS, TOSThreads offers the following benefits:
- It supports fully-preemptive application-level threads.
- It does not need explicit continuation management, such as state variables between corresponding commands and events.
- It does not violate TinyOS's concurrency model.
- It requires minimal changes to the existing TinyOS code base. In addition, adding TOSThreads support to a new platform is a fairly easy process.
- It offers both nesC and C APIs.
Architecture
In TOSThreads, TinyOS runs inside a single high-priority kernel thread, while the application logic is implemented in user-level threads. Since the TinyOS kernel thread has a higher priority, user-level threads execute whenever the TinyOS kernel thread becomes idle. This approach is a natural extension to the existing TinyOS concurrency model: adding support for long-running computations while preserving the timing-sensitive nature of TinyOS itself.
In this model, application threads access underlying TinyOS services using a kernel API of blocking system calls. The kernel API defines the set of TinyOS services provided to applications, such as radio, collection (TEP119), and so on. Each system call in the API is comprised of a thin blocking wrapper built on top of one of these services. The blocking wrapper is responsible for maintaining states across the non-blocking split-phase operations. This transfer of control between the TinyOS kernel thread and application threads resembles message passing. This approach ensures that application threads do not touch the kernel code directly, and makes it easier to build a thread-safe system.
TOSThreads provides both nesC and C flavor of kernel APIs. This means that users can choose to code TOSThreads applications in either nesC or C. Later sections of this tutorial discusses how to write TOSThreads applications, and how to implement kernel APIs for new TinyOS services.
TOSThreads Code Organization
At the time of writing, TOSThreads supports the following platforms: telosb, micaZ, and mica2. You can find the TOSThreads code in tos/lib/tosthreads/ directory as described below.
TOSThreads system files are located in several subdirectories under tos/lib/tosthreads/:
- chips: Some chip-specific files that shadow tinyos-2.x/tos/chips to add code such as the interrupt postamble.
- csystem: Contain C API system files and the header file for different system services.
- interfaces: Contain nesC API interfaces.
- lib: Shadow some files in tinyos-2.x/tos/lib, and contain the blocking wrapper for CTP.
- platforms: Shadow some files in tinyos-2.x/tos/platforms.
- sensorboards: Contain blocking wrappers for telosb's onboard SHT11 sensors, and an universal sensor that generates a sine wave.
- system: Contain nesC API system files and the blocking wrappers for different system services.
- types: Define the structs used by TOSThreads system files.
The TOSThreads library currently supports many TinyOS services. For convenience, we list these TinyOS services below:
- Radio AM communication: This includes turning ON/OFF the radio, and send/receive messages. nesC APIs are provided by BlockingAMReceiverC.nc, BlockingAMSenderC.nc, and BlockingActiveMessageC.nc in tos/lib/tosthreads/system/ directory. For C APIs, the header file is tosthread_amradio.h in tos/lib/tosthreads/csystem/ directory.
- Serial AM communication: This includes turning ON/OFF the serial port, and send/receive messages. nesC APIs are provided by BlockingSerialAMReceiverC.nc, BlockingSerialAMSenderC.nc, and BlockingSerialActiveMessageC.nc in tos/lib/tosthreads/system/ directory. For C APIs, the header file is tosthread_amserial.h in tos/lib/tosthreads/csystem/ directory.
- Permanent Storage abstractions: Block, log, and config storage abstractions are all supported. nesC APIs are provided by BlockingBlockStorageC.nc, BlockingLogStorageC.nc, and BlockingConfigStorageC.nc in tos/lib/tosthreads/system/ directory. For C APIs, the header files are tosthread_blockstorage.h, tosthread_logstorage.h, tosthread_configstorage.h in tos/lib/tosthreads/csystem/ directory.
- Network protocols: CTP is supported. nesC APIs are provided by BlockingCollectionSenderC.nc in tos/lib/tosthreads/lib/net/ctp/ directory, and BlockingCollectionReceiverC.nc and BlockingCollectionControlC.nc in tos/lib/tosthreads/lib/net/ directory. For C APIs, the header file is tosthread_collection.h in tos/lib/tosthreads/lib/net/ directory.
- printf debugging tool. nesC APIs are provided by PrintfC.nc in tos/lib/tosthreads/lib/printf/ directory. For C APIs, the header file is printf.h in tos/lib/tosthreads/lib/printf/ directory.
- Sensors: This include the Telosb onboard temperature, humidity and light-intensity sensors. A universal sine-function sensors. And, the basicsb sensor board.
Adding TOSThreads support to a TinyOS service involves writing a blocking wrapper around the service, and exposing the service through kernel API. Later sections of the tutorial discuss how to do this.
You can find example TOSThreads applications in apps/tosthreads/ directory to help you get started.
nesC API
As mentioned above, one of the two ways that TOSThreads exposes system services is through the nesC APIs. Not surprisingly, with the nesC APIs, you write the thread code in nesC. With nesC API, you can choose to create either static threads or dynamic threads. The primary difference between the two is that statically allocated threads have their TCB allocated for them at compile time while dynamic threads have them allocated at run time.
Static threads
The ThreadC component provides a Thread interface for manipulating static threads:
Thread.nc:
interface Thread { | ||
command error_t start(void* arg); | ||
command error_t stop(); | ||
command error_t pause(); | ||
command error_t resume(); | ||
command error_t sleep(uint32_t milli); | ||
event void run(void* arg); | ||
command error_t join(); | ||
} |
Calling start(...) on a thread signals to the TOSThreads thread scheduler that the thread should begin executing (at some time later, the run(...) event will be signaled). The argument is a pointer to a data structure passed to the thread once it starts executing. Calls to start(...) return either SUCCESS or FAIL.
Calling stop() on a thread signals to the TOSThreads thread scheduler that the thread should stop executing. Once a thread is stopped it cannot be restarted. Calls to stop() return SUCCESS if a thread was successfully stopped, and FAIL otherwise. stop() MUST NOT be called from within the thread being stopped; it MUST be called from either the TinyOS thread or another application thread.
Calling pause() on a thread signals to the TOSThreads thread scheduler that the thread should be paused. Unlike a stopped thread, a paused thread can be restarted later by calling resume() on it. pause() MUST ONLY be called from within the thread itself that is being paused.
Calling sleep(...) puts a thread to sleep for the interval specified in its single 'milli' parameter. sleep(...) MUST ONLY be called from within the thread itself that is being put to sleep. SUCCESS is returned if the thread was successfully put to sleep, FAIL otherwise.
We will now discuss how to write code to create and manipulate static threads with the nesC API, and we will use apps/tosthreads/apps/RadioStress as an example. The application creates three threads to stress the radio operations. Depending on your mote platform, type make telosb threads to compile. Let's take a look at RadioStress' files.
RadioStressAppC.nc:
..... | ||
components new ThreadC(300) as RadioStressThread0; | Statically create a thread that has 300-byte stack space |
|
components new BlockingAMSenderC(220) as BlockingAMSender0; | Blocking wrapper for AM Sender (AM ID is 220) |
|
components new BlockingAMReceiverC(220) as BlockingAMReceiver0; | Blocking wrapper for AM Receiver (AM ID is 220) |
|
RadioStressC.RadioStressThread0 -> RadioStressThread0; | ||
RadioStressC.BlockingAMSend0 -> BlockingAMSender0; | ||
RadioStressC.BlockingReceive0 -> BlockingAMReceiver0; | ||
..... |
RadioStressC.nc:
..... | ||
event void Boot.booted() { | ||
call RadioStressThread0.start(NULL); | Singal the thread scheduler to start executing thread's main function with NULL arguments |
|
call RadioStressThread1.start(NULL); | ||
call RadioStressThread2.start(NULL); | ||
} | ||
..... | ||
event void RadioStressThread0.run(void* arg) { | RadioStressThread0 thread's main function |
|
call BlockingAMControl.start(); | Start the radio. The thread will be blocked until the operation completes |
|
for(;;) { | ||
if(TOS_NODE_ID == 0) { | ||
call BlockingReceive0.receive(&m0, 5000); | Try to listen for an incoming packet for 5000 ms. The thread is blocked until the operation completes |
|
call Leds.led0Toggle(); | ||
} else { | ||
call BlockingAMSend0.send(!TOS_NODE_ID, &m0, 0); | Send a packet m0 of length 0 byte. The thread is blocked until the operation completes |
|
call Leds.led0Toggle(); | ||
} | ||
} | ||
} | ||
..... |
Dynamic threads
The nesC API can also create dynamic threads at run time, and the nesC interface is DynamicThread:
DynamicThread.nc:
interface DynamicThread { | |
command error_t create(tosthread_t* t, void (*start_routine)(void*), void* arg, uint16_t stack_size); | |
command error_t destroy(tosthread_t* t); | |
command error_t pause(tosthread_t* t); | |
command error_t resume(tosthread_t* t); | |
command error_t sleep(uint32_t milli); | |
} |
create(...) takes a function pointer as the second argument, which is the starting point of the thread execution. Blink_DynamicThreads is an example application that demostrates how to use dynamic threads.
C API
As mentioned before, in addition to the nesC APIs, TOSThreads provides the C APIs. With the C APIs, users do not need to write TOSThreads application in nesC, but in C. The TOSThreads library provides several C header files that the TOSThreads application needs to include to make system calls. These header files are in tos/lib/tosthreads/csystem directory.
We will use the C version of RadioStress in apps/tosthreads/capps/RadioStress directory. Depending on your mote platform, type make telosb cthreads to compile.
RadioStress.c:
#include "tosthread.h" | Header file that defines thread-related functions |
||||
#include "tosthread_amradio.h" | Header file that defines radio-related functions |
||||
#include "tosthread_leds.h" | Header file that defines LED-related functions |
||||
tosthread_t radioStress0; | Declare a thread object |
||||
..... | |||||
void tosthread_main(void* arg) { | Main thread's main function. This is run after the system successfully boots |
||||
while( amRadioStart() != SUCCESS ); | Starts the radio. The main thread will be blocked until the operation completes |
||||
tosthread_create(&radioStress0, radioStress0_thread, &msg0, 200); | Create a thread with 200-byte stack space. radioStress0_thread is the main function. |
||||
..... | |||||
} | |||||
..... | |||||
void radioStress0_thread(void* arg) { | radioStress0 thread's main function |
||||
message_t* m = (message_t*)arg; | |||||
for(;;) { | |||||
if(TOS_NODE_ID == 0) { | |||||
amRadioReceive(m, 2000, 20); | Try to listen for an incoming packet for 2000 ms. The thread is blocked until the operation completes |
||||
led0Toggle(); | |||||
} | |||||
else { | |||||
if(amRadioSend(!TOS_NODE_ID, m, 0, 20) == SUCCESS) | Send a packet m0 of length 0 byte, and specify the AM ID to be 20. The thread is blocked until the operation completes |
||||
led0Toggle(); | |||||
} | |||||
} | |||||
} | ..... |
Similarily, tosthread.h provides commands to manipulate threads.
Support new system services
Adding TOSThreads support to new system services requires writing blocking wrappers for any interfaces that the service provides. The main responsibility of blocking wrappers is to maintain states for application threads as they make split-phase calls. We will use the log abstraction as an example. As mentioned before, TOSThreads overlays a blocking wrapper on top of the split-phase system service.
The first step is to define interfaces provided by the blocking wrapper. The TOSThreads applications use these interfaces to make the system call. The interface file for log abstraction is tos/lib/tosthreads/interfaces/BlockingLog.nc.
With the interface file, you then can write the blocking wrapper. The blocking wrapper for log abstraction is implemented by tos/lib/tosthreads/system/BlockingLogStorageC.nc, tos/lib/tosthreads/system/BlockingLogStorageP.nc, and tos/lib/tosthreads/system/BlockingLogStorageImplP.nc. Let's look at what is inside the blocking wrapper:
BlockingLogStorageImplP.nc:
..... | ||
typedef struct read_params { | System call arguments passed from the user thread to the kernel thread |
|
void *buf; | ||
storage_len_t* len; | ||
error_t error; | ||
} read_params_t; | ||
..... | ||
void readTask(syscall_t *s) { | TinyOS kernel thread executes this function to carry out the system call |
|
read_params_t *p = s->params; | ||
p->error = call LogRead.read[s->id](p->buf, *(p->len)); | The split-phase system call |
|
if(p->error != SUCCESS) { | ||
call SystemCall.finish(s); | ||
} | ||
} | ||
command error_t BlockingLog.read[uint8_t volume_id](void *buf, storage_len_t *len) { | ||
syscall_t s; | Contain a pointer to a structure used when making system calls into a TOSThreads kernel. This structure is readable by both a system call wrapper implementation and the TinyOS kernel thread. |
|
read_params_t p; | ||
atomic { | ||
if(call SystemCallQueue.find(&vol_queue, volume_id) != NULL) | ||
return EBUSY; | ||
call SystemCallQueue.enqueue(&vol_queue, &s); | ||
} | ||
p.buf = buf; | Save the system call argument, buf |
|
p.len = len; | Save the system call argument, len |
|
call SystemCall.start(&readTask, &s, volume_id, &p); | Pause the user thread and pass the control to the TinyOS kernel thread |
|
atomic { | ||
call SystemCallQueue.remove(&vol_queue, &s); | ||
return p.error; | Return the error code to the user thread |
|
} | ||
} | ||
event void LogRead.readDone[uint8_t volume_id](void *buf, storage_len_t len, error_t error) { | ||
syscall_t *s = call SystemCallQueue.find(&vol_queue, volume_id); | ||
read_params_t *p = s->params; | ||
if (p->buf == buf) { | ||
p->error = error; | Save the error code returned by the system call |
|
*(p->len) = len; | ||
call SystemCall.finish(s); | ||
} | ||
} | ||
..... |
What we have done so far is adding the nesC APIs for the new service. You can now write a TOSThreads that uses the new service through this nesC APIs. The next step is to add C API as well. You can think of the C APIs as a bridge that enables the TOSThreads application to call the nesC API we just wrote. Therefore, writing the C API requires writing a C header file to define the functions, and also some files to translate the C APIs to nesC APIs. For the log abstraction, these files are in tinyos-2.x/tos/lib/tosthreads/csystem/. Let's look at the related files.
CLogStorageP.nc:
..... | ||
error_t volumeLogRead(uint8_t volumeId, void *buf, storage_len_t *len) @C() @spontaneous() { | ||
return call BlockingLog.read[volumeId](buf, len); | ||
} | ||
..... |
tosthread_logstorage.h:
#ifndef TOSTHREAD_LOGSTORAGE_H | ||
#define TOSTHREAD_LOGSTORAGE_H | ||
..... | ||
extern error_t volumeLogRead(uint8_t volumeId, void *buf, storage_len_t *len); | ||
..... |
Based on what C API header files are included in the user application, TOSThreads conditionally includes components. This step is done in tos/lib/tosthreads/csystem/TosThreadApiC.nc, and here is the code snippet.
TosThreadApiC.nc:
#if defined(TOSTHREAD_BLOCKSTORAGE_H) || defined(TOSTHREAD_DYNAMIC_LOADER) | ||
components CLogStorageC; | ||
#endif |
With the code above, CLogStorageC is automatically included when tosthread_logstorage.h or TinyLD is used.
Synchronization primitives
TOSThreads supports the following synchronization primitives:
- Mutex: The interface file is tos/lib/tosthreads/interfaces/Mutex.nc.
- Semaphore: This is an implementation of counting semaphore. The interface file is tos/lib/tosthreads/interfaces/Semaphore.nc.
- Barrier: All threads that call Barrier.block() are paused until v threads have made the block call. The interface file is tos/lib/tosthreads/interfaces/Barrier.nc.
- Condition variable: The interface file is The interface file is tos/lib/tosthreads/interfaces/ConditionVariable.nc.
- Blocking reference counter: A thread can wait for the maintained counter to reach count. The interface provides a way for other threads to increment and decrement the maintained counter. The interface is tos/lib/tosthreads/interfaces/ReferenceCounter.nc.
Bounce is an example application that demostrates how to use the barrier synchronization primitive.