munotes®

nesC: Modules, Configurations, Interfaces and Wiring

Get access to whole semester resourcesSemester Pass

Chapter Twenty-Eight

Syllabus topic Module 1, "WSN Operating Systems and Ad-hoc Networks: Examples of WSN operating systems (case/examples)" (and the paired practical, "Implementation of nesC Programming Model ... using modules and configurations to understand component wiring and interface binding")

Pages 152 to 156 of 862

In one line

nesC is C extended with components: modules hold code, configurations wire components together, interfaces are two-way contracts of commands and events, and the whole program is assembled at compile time from its wiring.

In the wording a student can write in an examination: nesC is the language TinyOS is written in: an extension to C "designed to embody the structuring concepts and execution model of TinyOS", in its manual's words. A nesC program is built from components, of two kinds: modules, which implement their behaviour in C-like code, and configurations, which build a component from other components by wiring them together. Components communicate only through interfaces, which are bidirectional: an interface declares commands, implemented by the component that provides it, and events, implemented by the component that uses it. Wiring uses -> (a used interface to a provided one), <- (the same, reversed) and = (equating a configuration's own interface with an inner one). Generic components are instantiated with new. Components are statically linked, which allows whole-program compilation and compile-time detection of data races.

Why a language of components

[Why a Sensor Node Needs an Operating System] listed "diversity in design and usage": every application needs a different set of drivers and protocols, and the node has no room for any it does not need. nesC's answer is to make the application a wiring diagram. The manual lists the basic idea first: "separation of construction and composition: programs are built out of components, which are assembled ('wired') to form whole programs". Only the components that are wired in are compiled in.

Interfaces: two-way contracts

An interface is a named set of commands and events. The manual explains the two directions: "Interfaces may be provided or used by the component. The provided interfaces are intended to represent the functionality that the component provides to its user, the used interfaces represent the functionality the component needs to perform its job."

And they are two-way: "Interfaces are bidirectional: they specify a set of functions to be implemented by the interface's provider (commands) and a set to be implemented by the interface's user (events)." So wiring a user to a provider connects both directions at once: the user's calls go to the provider's commands, and the provider's signals come back to the user's event handlers. This is what makes split-phase operation ([Commands, Events and Split-phase Operation]) safe: a user of an interface cannot forget to handle its completion event, because the compiler requires it.

Modules and configurations

A module has a specification (the interfaces it provides and uses) and an implementation in C with nesC additions: the command handlers for what it provides, the event handlers for what it uses, its variables (its frame), and its tasks.

munotes.in152

nesC: Modules, Configurations, Interfaces and Wiring

A configuration has a specification too, but its implementation contains no code: it names components and wires them. A configuration is how components are assembled into bigger components, and a whole application is one top-level configuration.

Wiring: the three statements

The manual gives exactly three wiring statements.

  1. endpoint1 -> endpoint2, a link wire: connects a used interface (on the left) to a provided interface (on the right), both on components inside the configuration. "If these two conditions do not hold, a compile-time error occurs."
  2. endpoint1 <- endpoint2: the same connection written the other way round.
  3. endpoint1 = endpoint2, an equate wire: connects one of the configuration's own interfaces to an inner component's interface, making them "effectively ... equivalent". This is how a configuration exports an interface that one of its components really implements.

Two further rules from the manual: the two ends must be compatible (the same interface type, or commands and events with the same signature), and "a configuration's external specification elements must all be wired or a compile-time error occurs". If an interface name is left out, as in SenseSendC.Boot -> MainC, the compiler finds the only interface of the right type on MainC.

Two more features every TinyOS program uses

Generic components. Some components need one private copy per user: every module that wants a timer needs its own timer. These are generic components, instantiated in a configuration with new. The manual: generic components "must be instantiated in a configuration before they can be used", while "components without parameters exist as a single instance which is implicitly instantiated". So new TimerMilliC() creates a timer for this module alone, while MainC and LedsC exist once and are shared.

Fan-out and fan-in. One interface may be wired to several others. Then, in the manual's words, the multiple wiring "will lead to multiple signalers ('fan-in') for the events" and "multiple functions being executed ('fan-out') when commands ... are called". A boot signal wired to three components reaches all three.

Parameterised interfaces. A component can provide many copies of one interface, told apart by a number: the scheduler's TaskBasic interface in [TinyOS: Components, Tasks and the Scheduler] is declared that way, and each task gets its number from the unique() function at compile time.

Worked example: an application that uses every feature

The program below has five files. It defines an interface, Tick, with one command and one event; a module, TickerP, that provides Tick by counting timer fires and signalling when it reaches a limit; a configuration, TickerC, that exports Tick and wires TickerP to a generic timer; a module, CountC, that uses Tick; and the top-level configuration, CountAppC, that wires the application together.

munotes.in153

nesC: Modules, Configurations, Interfaces and Wiring

CountAppC's wiring: arrows run from each used interface to its provider

Figure 28.1 The wiring of the worked application, drawn from its source

The interface. The provider implements start; the user implements reached.

// A bidirectional interface: the provider implements the command,
// the user implements the event.
interface Tick {
  command void start(uint16_t limit);
  event void reached(uint16_t count);
}

The provider module. It provides Tick and uses a timer. Its command starts the timer; its handler for the timer's event counts, and signals Tick's event when the count reaches the goal.

module TickerP {
  provides interface Tick;
  uses interface Timer<TMilli>;
}
implementation {
  uint16_t count = 0;
  uint16_t goal = 0;

  command void Tick.start(uint16_t limit) {
    goal = limit;
    count = 0;
    call Timer.startPeriodic(100);
  }

  event void Timer.fired() {
    count++;
    if (count == goal) {
      call Timer.stop();
      signal Tick.reached(count);
    }
  }
}

The exporting configuration. TickerC offers Tick to the outside world, but TickerP is the one that implements it, so the two are equated with =. TickerP's used Timer is linked with -> to a fresh timer made with new.

configuration TickerC {
  provides interface Tick;
}
implementation {
  components TickerP, new TimerMilliC();

  Tick = TickerP.Tick;              // export: TickerC's Tick is TickerP's
  TickerP.Timer -> TimerMilliC;     // wire: TickerP uses what the timer provides
}

The user module. It uses Tick, so it must implement Tick's event, reached. On boot it asks for four ticks; each time the goal is reached it lights an LED and asks for twice as many.

module CountC {
  uses interface Boot;
  uses interface Tick;
  uses interface Leds;
}
implementation {
  event void Boot.booted() {
    call Tick.start(4);
  }

  event void Tick.reached(uint16_t count) {
    call Leds.led0On();
    call Tick.start(count * 2);     // go again, waiting twice as long
  }
}

The application. The top-level configuration names the components and links each used interface of CountC to a provider.

configuration CountAppC { }
implementation {
  components MainC, LedsC, CountC, TickerC;

  CountC.Boot -> MainC.Boot;
  CountC.Tick -> TickerC.Tick;
  CountC.Leds -> LedsC.Leds;
}

What the compiler checked, and what it said. Built for the TelosB inside the TinyOS 2.1.2 laboratory, the application compiled with no warning from its own files and reported 2,388 bytes of program memory and 40 bytes of RAM. In compiling it, nesC verified every rule of this chapter: that CountC implements reached because it uses Tick; that TickerP implements start because it provides Tick; that every -> runs from a used interface to a provided one of the same type; and that TickerC's own interface is wired. Break any of those and the program does not compile.

Following one call through the wiring. When CountC runs call Tick.start(4), the wiring sends it to TickerC's Tick, which the = has made the same as TickerP's Tick, so TickerP's start command runs. When TickerP runs signal Tick.reached(count), the same wires carry it back, and CountC's reached handler runs. Neither module names the other anywhere: they know only the interface. Replace TickerP with a different module that provides Tick, change one line of TickerC, and CountC does not change at all.

munotes.in154

nesC: Modules, Configurations, Interfaces and Wiring

Distinctions

ModuleConfiguration
ContainsCode: handlers, variables, tasksComponents and wiring only
ImplementsCommands of provided interfaces, events of used onesNothing directly; delegates by wiring
In the exampleTickerP, CountCTickerC, CountAppC
providesuses
The component offersThe interface's commandsNothing; it needs the interface
The component must implementThe commandsThe events
-> (link)= (equate)
ConnectsA used interface to a provided one, both insideThe configuration's own interface to an inner one
PurposeConnect componentsExport an interface
In the exampleTickerP.Timer -> TimerMilliCTick = TickerP.Tick
Generic componentNon-generic component
InstancesOne per newExactly one
Examplenew TimerMilliC()MainC, LedsC

What it does not mean

A configuration is not a module with no code. It has no code because its job is different: assembling components, not implementing behaviour.

Wiring is not a function call. It is a compile-time connection; the calls it enables cost no more than an ordinary function call, because the compiler resolves them.

provides does not mean "calls". A component that provides an interface implements its commands and signals its events; the component that uses it calls the commands and handles the events.

new does not allocate memory at run time. A generic component is instantiated when the program is compiled; nothing is created while it runs.

Quick revision

  • nesC: C with components; programs "built out of components, which are assembled ('wired')".
  • Module: code. Configuration: components and wiring only.
  • Interfaces are bidirectional: commands implemented by the provider, events by the user.
  • Wiring: -> used to provided (link); <- reversed; = equate, to export an interface; ends must be compatible; a configuration's own interfaces must all be wired.
  • Generic components with new (one per instance, e.g. TimerMilliC); others are single instances (MainC, LedsC).
  • Fan-out and fan-in from multiple wiring; parameterised interfaces numbered with unique().
  • Worked CountApp: interface Tick, TickerP provides, TickerC exports with =, CountC uses; compiled for TelosB: 2,388 bytes ROM, 40 bytes RAM.

Test yourself

1. Distinguish a module and a configuration in nesC. A module implements behaviour in C-like code: command and event handlers, variables and tasks. A configuration contains no code; it names components and wires their interfaces together, building a larger component or a whole application.

2. What does it mean that nesC interfaces are bidirectional? An interface contains commands, which the providing component implements, and events, which the using component implements. Wiring a user to a provider connects both directions, so the provider can call back into the user, which is how split-phase completion events reach the caller.

munotes.in155

nesC: Modules, Configurations, Interfaces and Wiring

3. Explain the three wiring statements. endpoint1 -> endpoint2 links a used interface to a provided interface inside a configuration; <- is the same written in reverse; endpoint1 = endpoint2 equates one of the configuration's own interfaces with an inner component's, exporting it.

4. In the worked example, why does TickerC use = for Tick but -> for Timer? Tick is TickerC's own provided interface, implemented by the inner module TickerP, so it is equated to export it. Timer is an interface TickerP uses and an inner timer component provides, so it is linked from user to provider.

5. What is a generic component? Give an example. A component with parameters that must be instantiated in a configuration with new, each instantiation a separate copy, such as new TimerMilliC(), which gives each user its own timer. A non-generic component such as MainC exists once.

munotes.in156

The rest of this subject

These notes are cut from the University's printed syllabus. Open the syllabus itself for the same subject.

Issue
Done!