Practical 4: The nesC Programming Model: Modules, Configurations and Wiring
Chapter Six
Syllabus topic Module 1, "Implementation of nesC Programming Model: Create a basic nesC application using modules and configurations to understand component wiring and interface binding."
Pages 47 to 54 of 232
Aim
To create a basic nesC application out of modules and configurations, and through it to understand component wiring and interface binding.
What you need to know before you start
A nesC program is not one file of functions. It is a set of components joined together, and the joining is written separately from the code. That separation is the whole idea of the language, and six words carry it.
Interface. A named set of functions two components use to talk to each other. An interface has two kinds of function in it: commands, which the user of the interface calls, and events, which the provider of the interface signals back. Timer<TMilli> is an interface: its user calls the command startPeriodic and receives the event fired.
Provides and uses. A component that provides an interface implements its commands and may signal its events. A component that uses an interface may call its commands and must implement its events. One interface, two sides, and each side has obligations.
Module. A component with code in it: variables, functions, and the bodies of the commands and events it is responsible for.
Configuration. A component with no code of its own, only a list of other components and the connections between them. The application itself is a configuration, the one the Makefile names in COMPONENT=.
Wiring. A connection between one component's use of an interface and another component's provision of it, written in a configuration as User.Interface -> Provider.Interface. The arrow points from user to provider. Wiring is how a call to a command in one component reaches the code that implements it in another.
Binding. Wiring is checked and fixed when the program is compiled, not while it runs. A call is bound to exactly the code the configuration connected it to, and nesC checks the interface types on both sides of every arrow. There are no function pointers to go wrong at run time.
Four more pieces of syntax appear in this practical, each explained where it is used: = in a configuration, as to rename, generic components created with new, and fan-out.
The application
A timer ticks every quarter of a second. On every tick the application adds one to two tallies, a fast one that announces every 2nd count and a slow one that announces every 5th. The tallies are two copies of one component we write ourselves, joined to the application through an interface we also write.
Figure 6.1 CountingAppC: components and their wiring
Make a folder for it:
$ mkdir ~/Counting
$ cd ~/CountingStep 1: the interface
Tally.nc:
interface Tally {
command void add();
command uint16_t total();
event void reached(uint16_t value);
}Practical 4: The nesC Programming Model: Modules, Configurations and Wiring
Two commands and one event. The component that uses Tally may call add and total, and must write a handler for reached. The component that provides Tally must implement add and total, and may signal reached. An interface file has no code: it is the contract between the two sides.
Step 2: a module that provides it
TallyP.nc:
generic module TallyP(uint16_t limit) {
provides interface Tally;
}
implementation {
uint16_t count = 0;
command void Tally.add() {
count++;
if (count % limit == 0)
signal Tally.reached(count);
}
command uint16_t Tally.total() {
return count;
}
}The first part, before implementation, is the module's signature: what it provides and uses, and nothing else of it is visible outside. The implementation holds its state, count, and the bodies of the two commands the interface obliges it to implement. When the count reaches a multiple of limit, it signals the event reached to whoever uses this interface.
generic with a parameter makes this a template rather than a single component. Every time a configuration writes new TallyP(5), the compiler makes a separate copy with its own count and its own limit. Without generic, there would be exactly one TallyP in the whole program, and every user would share its count.
The name ends in P because TinyOS's own convention is that a component ending in P is private, an implementation, and one ending in C is the public component other code should wire to.
Step 3: a configuration that hands it out
TallyC.nc:
generic configuration TallyC(uint16_t limit) {
provides interface Tally;
}
implementation {
components new TallyP(limit) as P;
Tally = P.Tally;
}This is the public face of the tally. It provides Tally, but has no code, so it cannot implement it itself: it creates a TallyP and says, with the equals sign, that the Tally it provides is TallyP's Tally.
That is the difference between the two operators, and it is the most asked question about nesC:
-> | = | |
|---|---|---|
| Connects | a component that uses an interface to one that provides it | an interface in THIS configuration's own signature to one inside it |
| Reads as | "is served by" | "is the same as" |
| Direction | from user to provider | between an outside and an inside |
| Example here | CountingC.Fast -> Fast; | Tally = P.Tally; |
as P gives the new component a local name, so the wiring can refer to it.
Step 4: a module that uses it, twice
CountingC.nc:
module CountingC {
uses interface Boot;
uses interface Timer<TMilli>;
uses interface Tally as Fast;
uses interface Tally as Slow;
}
implementation {
event void Boot.booted() {
call Timer.startPeriodic(256);
}
event void Timer.fired() {
call Fast.add();
call Slow.add();
if (call Slow.total() == 10)
call Timer.stop();
}
event void Fast.reached(uint16_t value) {
dbg("Count", "%s Fast reached %u\n", sim_time_string(), value);
}
event void Slow.reached(uint16_t value) {
dbg("Count", "%s Slow reached %u\n", sim_time_string(), value);
}
}Practical 4: The nesC Programming Model: Modules, Configurations and Wiring
The module uses the Tally interface twice, so each use needs its own name: as Fast and as Slow. Inside the module the names are all that is visible. call Fast.add() means "the add of whatever Fast is wired to"; the module does not know, and does not need to know, which component that will be. And because it uses both, it must implement the reached event of both, Fast.reached and Slow.reached.
startPeriodic(256) is 256 of TinyOS's binary milliseconds, a quarter of a second. After ten ticks the slow tally's total is 10 and the timer is stopped.
Step 5: the configuration that is the application
CountingAppC.nc:
configuration CountingAppC {}
implementation {
components MainC, CountingC, new TimerMilliC();
components new TallyC(2) as Fast;
components new TallyC(5) as Slow;
CountingC.Boot -> MainC;
CountingC.Timer -> TimerMilliC;
CountingC.Fast -> Fast;
CountingC.Slow -> Slow;
}Its signature is empty, {}, because nothing uses the application. Its implementation lists every component in it and wires every interface CountingC uses:
Bootto MainC, TinyOS's component that boots the system and signalsBoot.booted.Timerto a new TimerMilliC: TinyOS's timers are generic too, so eachnewis a timer of its own.Fastto a TallyC made with limit 2, andSlowto one made with limit 5.
The figure at the top of this practical is exactly these four lines drawn out.
And the Makefile names this configuration as the application:
COMPONENT=CountingAppC
override GCC = gcc-10
export GCC
PFLAGS += -fgnu89-inline -fsigned-char
include $(MAKERULES)Step 6: build and run
A script that runs one mote for three simulated seconds, run.py:
from TOSSIM import *
import sys
t = Tossim([])
t.randomSeed(1)
t.addChannel("Count", sys.stdout)
m = t.getNode(1)
m.bootAtTime(0)
while t.time() < 3 * t.ticksPerSecond():
t.runNextEvent()$ make micaz sim 2>&1 | tail -1
*** Successfully built micaz TOSSIM library.
$ python2 run.py
DEBUG (1): 0:0:0.500000010 Fast reached 2
DEBUG (1): 0:0:1.000000010 Fast reached 4
DEBUG (1): 0:0:1.250000010 Slow reached 5
DEBUG (1): 0:0:1.500000010 Fast reached 6
DEBUG (1): 0:0:2.000000010 Fast reached 8
DEBUG (1): 0:0:2.500000010 Fast reached 10
DEBUG (1): 0:0:2.500000010 Slow reached 10The ticks come every quarter of a second, and the log is exactly what the wiring says it should be. The fast tally announces every second tick (counts 2, 4, 6, 8, 10 at 0.5, 1.0, 1.5, 2.0 and 2.5 seconds) and the slow tally every fifth (5 at 1.25 seconds, 10 at 2.5). After the tenth tick the slow tally's total is 10 and the timer stops, so nothing prints after 2.5 seconds although the script ran for three.
Practical 4: The nesC Programming Model: Modules, Configurations and Wiring
Two copies of one component, each with its own count and its own limit: that is what new did. And CountingC could tell their events apart because they arrive through two differently named uses, Fast.reached and Slow.reached, of one interface type.
Step 7: the rules, each shown by breaking it
Every rule in the first section of this practical is enforced by the compiler, and the quickest way to learn them is to break each one and read what nesC says. Keep a copy of the two files about to be broken:
$ cp CountingAppC.nc CountingAppC.good
$ cp TallyP.nc TallyP.goodRule: every interface a component uses must be wired. Delete the line CountingC.Slow -> Slow; from the configuration and build:
$ sed -i '/CountingC.Slow -> Slow;/d' CountingAppC.nc
$ make micaz sim 2>&1 | grep -E 'not connected|Error'
CountingC.nc:14: Slow.add not connected
CountingC.nc:15: Slow.total not connected
make: *** [/home/student/tinyos-2.1.2/support/make/sim.extra:69: sim-exe] Error 1
$ cp CountingAppC.good CountingAppC.ncCountingC calls Slow.add on line 14 and Slow.total on line 15, and nothing now provides them, so the build stops. nesC reports it at the call, in the module, although the mistake is in the configuration. That is worth remembering: "not connected" means look at the wiring, not at the line it names. (A module can write a default body for a command it uses, which runs when nothing is wired; without one, an unwired call is an error.)
Rule: a provider must implement every command of the interface it provides. Delete the total command from TallyP and build:
$ sed -i '/command uint16_t Tally.total/,/^ }/d' TallyP.nc
$ make micaz sim 2>&1 | grep -E 'not implemented|Error'
TallyP.nc:4: `Tally.total' not implemented
make: *** [/home/student/tinyos-2.1.2/support/make/sim.extra:69: sim-exe] Error 1
$ cp TallyP.good TallyP.ncTallyP promised, by providing Tally, to implement both of its commands. The error names the line of the promise, line 4, provides interface Tally;.
Rule: both ends of a wire must be the same interface. Wire Slow, a Tally, to the timer, which provides Timer<TMilli>:
$ sed -i 's/CountingC.Slow -> Slow;/CountingC.Slow -> TimerMilliC;/' CountingAppC.nc
$ make micaz sim 2>&1 | grep -E 'no match|Error'
CountingAppC.nc:10: no match
make: *** [/home/student/tinyos-2.1.2/support/make/sim.extra:69: sim-exe] Error 1
$ cp CountingAppC.good CountingAppC.ncLine 10 is the wire, and "no match" is nesC saying it found no interface of type Tally on the timer to connect it to. This is what binding at compile time buys: a wrong connection cannot reach the mote.
Not an error: one use wired to two providers. Keep the line for Slow and add a second wire for Fast, to the slow tally as well:
Practical 4: The nesC Programming Model: Modules, Configurations and Wiring
$ sed -i 's/CountingC.Slow -> Slow;/CountingC.Slow -> Slow;\n CountingC.Fast -> Slow;/' CountingAppC.nc
$ tail -4 CountingAppC.nc
CountingC.Fast -> Fast;
CountingC.Slow -> Slow;
CountingC.Fast -> Slow;
}
$ make micaz sim 2>&1 | grep -E 'fan out|Success'
nesc1: warning: calls to Fast.total in CountingC fan out, but there is no combine function specified for the return type
*** Successfully built micaz TOSSIM library.
$ python2 run.py
DEBUG (1): 0:0:0.500000010 Fast reached 2
DEBUG (1): 0:0:0.750000010 Slow reached 5
DEBUG (1): 0:0:0.750000010 Fast reached 5
DEBUG (1): 0:0:1.000000010 Fast reached 4
DEBUG (1): 0:0:1.250000010 Slow reached 10
DEBUG (1): 0:0:1.250000010 Fast reached 10
$ cp CountingAppC.good CountingAppC.ncThis is fan-out: Fast is now wired to both tallies. It builds, and the log changes in two ways that show exactly what fan-out means.
A command on a fanned-out use is called on every provider. Each call Fast.add() now adds to the fast tally AND the slow one, so the slow tally counts twice per tick and reaches 5 at the third tick, 0.75 seconds, instead of 1.25.
An event from a provider reaches every user wired to it. When the slow tally reached 5 it signalled reached once, and both Slow.reached and Fast.reached in CountingC ran, because both uses are now wired to it: two lines at 0.75 seconds, "Slow reached 5" and "Fast reached 5".
The warning is about total, which returns a value. When one call reaches two providers there are two results, and a program must say how to combine them. The nesC reference manual's rule is that the result type must have a combining function "or a compile-time error occurs"; TinyOS's error_t has one, ecombine, which is why commands returning error_t fan out safely all over TinyOS. uint16_t has none. The nesC reference manual calls this an error, and nesC 1.3.5 lets it through with a warning, which is the more dangerous of the two: the build succeeds, and a call to Fast.total() would return a value the program never chose. Treat that warning as an error.
Step 8: a name TinyOS already uses
Suppose the interface had been called Counter, which is what most people would call it. Add a file of that name, even one the application never uses:
$ printf 'interface Counter {\n command void add();\n}\n' > Counter.nc
$ make micaz sim 2>&1 | grep -m4 -E 'In component|type arguments'
In component `AlarmCounterMilliP':
/home/student/tinyos-2.1.2/tos/platforms/mica/AlarmCounterMilliP.nc:29: unexpected type arguments
In component `Atm128AlarmAsyncC':
/home/student/tinyos-2.1.2/tos/chips/atm128/timer/Atm128AlarmAsyncC.nc:27: unexpected type arguments
$ rm Counter.nc
$ make micaz sim 2>&1 | tail -1
*** Successfully built micaz TOSSIM library.The errors are not in the application at all. They are in AlarmCounterMilliP and Atm128AlarmAsyncC, parts of TinyOS's own timer system, which use a TinyOS interface also called Counter, one that takes type arguments. nesC looks for every component and interface in the application's own folder first, so the file Counter.nc sitting there replaced TinyOS's Counter for the whole build, and every TinyOS component that uses it broke. Deleting the file mends it.
Practical 4: The nesC Programming Model: Modules, Configurations and Wiring
nesC has no namespaces: a component or interface name means one thing across the whole program. Before naming anything, check it is not already a TinyOS name:
$ find $TOSDIR -name 'Counter.nc' | head -3
/home/student/tinyos-2.1.2/tos/lib/timer/Counter.nc
$ find $TOSDIR -name 'Tally.nc' | wc -l
0Procedure
- Write the interface
Tally, with two commands and one event. - Write
TallyP, a generic module that providesTally. - Write
TallyC, a generic configuration that providesTallyby exporting TallyP's with=. - Write
CountingC, a module that usesTallytwice, as Fast and as Slow, with a timer and the boot event. - Write
CountingAppC, the top-level configuration, making two tallies withnewand wiring everything with->. - Build with
make micaz simand run three simulated seconds. - Break the wiring four ways and read what nesC says each time; restore the file after each.
- Name an interface of your own
Counterand read what happens.
Observations
| Run | What nesC or the simulator said |
|---|---|
| The application as written | Fast reached 2, 4, 6, 8, 10 at 0.5 to 2.5 s; Slow reached 5 at 1.25 s and 10 at 2.5 s; the timer stopped after ten ticks |
CountingC.Slow left unwired | error: Slow.add not connected, Slow.total not connected |
total missing from TallyP | error: `Tally.total' not implemented |
Slow wired to the timer | error: no match |
Fast wired to both tallies | warning: fan out with no combine function; built; the slow tally counted twice per tick and both handlers received its events |
A file named Counter.nc in the folder | errors in TinyOS's own timer components: unexpected type arguments |
Result
A nesC application was built from an interface, two modules and two configurations. TallyP, a generic module, provides the interface Tally; TallyC, a generic configuration, provides it by exporting TallyP's with =; CountingC uses it twice under the names Fast and Slow; and CountingAppC, the application's configuration, creates two tallies with new and wires every used interface to a provider with ->. The run printed exactly the events the wiring determined. Breaking the wiring showed that nesC binds every call at compile time: an unwired use, an unimplemented command and a wire between different interfaces are all compile-time errors, and fan-out calls every provider and delivers a provider's events to every user. An interface named after a TinyOS interface replaced it for the whole program.
Practical 4: The nesC Programming Model: Modules, Configurations and Wiring
Where marks are lost
Getting the arrow backwards. User.Interface -> Provider. The arrow points at the component that implements the commands.
Writing -> for an export, or = for a connection. = joins this configuration's own interface to one inside it; -> joins a user to a provider.
Implementing the events of an interface you provide. It is the other way round: a provider implements the commands and signals the events; a user calls the commands and implements the events.
Using one interface twice without as. Two uses of Tally need two names.
Forgetting new for a generic component. components TallyC; is not a component; components new TallyC(2) as Fast; is.
Naming an interface or component after a TinyOS one. Counter, Timer, Leds, Send: check with find $TOSDIR -name.
Reading "not connected" as a mistake in the module. It is reported at the call; the fault is in the configuration.
Ignoring the fan-out warning. It builds, and the result of the call is not one the program chose.
For the journal
Aim; interface, provides, uses, module, configuration, wiring and binding, a line each; the table of -> against =; all five source files, the Makefile and the script; the wiring diagram; the output with its explanation; the four broken variants, each with the change made and the exact message nesC gave; the Counter clash; the observation table; the result.
Quick revision
- An interface is a contract of commands (called by its user) and events (signalled by its provider).
- A provider implements the commands and signals the events; a user calls the commands and implements the events.
- A module has code; a configuration has only components and wiring.
User.Interface -> Providerconnects;Interface = Inner.Interfaceexports.asrenames: two uses of one interface need two names.genericmakes a template;newmakes a copy with its own state.- Wiring is bound at compile time: unwired uses, unimplemented commands and mismatched wires are errors.
- Fan-out calls every provider; a non-void result needs a combining function (
error_thasecombine). - Names are global: a file named after a TinyOS interface replaces it for the whole build.
- By convention a name ending in P is an implementation and one ending in C is the component to wire to.
Questions you must be able to answer
1. What is the difference between a module and a configuration? A module contains code: variables and the bodies of commands and events. A configuration contains no code, only the list of components it uses and the wiring between them.
Practical 4: The nesC Programming Model: Modules, Configurations and Wiring
2. Which side of an interface implements the commands, and which the events? The provider implements the commands and may signal the events. The user may call the commands and must implement the events.
3. What is the difference between -> and =? -> connects a component that uses an interface to one that provides it. = says that an interface in this configuration's own signature is the same as one inside it, which is how a configuration hands out an interface it has no code to implement.
4. What does new TallyC(2) as Fast do? It creates a separate copy of the generic configuration TallyC, and through it of TallyP, with the parameter 2, and gives that copy the local name Fast.
5. Why is wiring called binding at compile time, and what does it give you? Because every call is connected to its implementation when the program is compiled, and nesC checks the interface types at both ends. A wrong or missing connection is a compile-time error rather than a crash on the mote.
6. What happened when Fast was wired to both tallies? Every Fast.add() was called on both, so the slow tally counted twice per tick, and when the slow tally signalled reached, both CountingC handlers ran.
7. What is a combining function? The function that merges the results when a call fans out to several providers. error_t has ecombine; a type with none, such as uint16_t, gives a warning in nesC 1.3.5, and the reference manual calls it an error.
8. Why did a file called Counter.nc break TinyOS's timers? Because nesC searches the application's folder first and has no namespaces, so that file replaced TinyOS's own Counter interface for every component in the build.
The rest of this subject
These notes are cut from the University's printed syllabus. Open the syllabus itself for the same subject.