Lesson 4: Component Composition and Radio Communication

Last updated 31 July 2003

This lesson introduces two concepts: hierarchical decomposition of component graphs, and using radio communication. The applications that we will consider are CntToLedsAndRfm and RfmToLeds. CntToLedsAndRfm is a variant of Blink that outputs the current counter value to multiple output interfaces: both the LEDs, and the radio communication stack. RfmToLeds receives data from the radio and displays it on the LEDs. Programming one mote with CntToLedsAndRfm will cause it to transmit its counter value over the radio; programming another with RfmToLeds causes it to display the received counter on its LEDs - your first distributed application!

If you're using mica2 or mica2dot motes, you will need to ensure that you've selected a radio frequency compatible with your motes (433MHz vs 916MHz motes). If your motes are unlabeled, see "How to determine the operating frequency range of a MICA2 or MICA2DOT mote" for information on recognizing which kind of mote you have. To tell the compiler which frequency you are using, edit the Makelocal file in the apps directory, defining either CC1K_DEF_PRESET (see tinyos-1.x/tos/platform/mica2/CC1000Const.h for preset values), or CC1K_DEF_FREQ with an explicit frequency (see example in Makelocal).

The CntToRfmAndLeds Application

Look at CntToRfmAndLeds.nc. Note that this application only consists of a configuration; all of the component modules are located in libraries!

CntToLedsAndRfm.nc
configuration CntToLedsAndRfm {
}
implementation {
  components Main, Counter, IntToLeds, IntToRfm, TimerC;

  Main.StdControl -> Counter.StdControl;
  Main.StdControl -> IntToLeds.StdControl;
  Main.StdControl -> IntToRfm.StdControl;
  Main.StdControl -> TimerC.StdControl;
  Counter.Timer -> TimerC.Timer[unique("Timer")];
  IntToLeds <- Counter.IntOutput;
  Counter.IntOutput -> IntToRfm;
}

The first thing to note is that a single interface requirement (such as Main.StdControl or Counter.IntOutput) can be fanned out to multiple implementations. Here we wire Main.StdControl to the Counter, IntToLeds, IntToRfm, and TimerCcomponents. (All of these components can be found in the tos/lib/Counters directory.) The names of the various components tell you what they do: Counter receives Timer.fired() events to maintain a counter. IntToLeds and IntToRfm provide the IntOutput interface, that has one command, output(), which is called with a 16-bit value, and one event, outputComplete(), which is called with result_t . IntToLeds displays the lower three bits of its value on the LEDs, and IntToRfm broadcasts the 16-bit value over the radio.

So we're wiring the Counter.Timer interface to TimerC.Timer, and Counter.IntOutput to both IntToLeds and IntToRfm. The NesC compiler generates code so that all invocations of the Counter.IntOutput.output() command will invoke the command in both IntToLeds and IntToRfm. Also note that the wiring arrow can go in either direction: the arrow always points from a used interface to a provided implementation.

Assuming you are using a Mica mote, try building and installing this application with make mica install; you should see a 3-bit binary counter on the mote's LEDs. Of course the mote is also transmitting the value over the radio, which we describe next.
 
IntToRfm: Sending a message
IntToRfm is a simple component that receives an output value (through the IntOutput interface) and broadcasts it over the radio. Radio communication in TinyOS follows the Active Message (AM) model, in which each packet on the network specifies a handler ID that will be invoked on recipient nodes. Think of the handler ID as an integer or "port number" that is carried in the header of the message. When a message is received, the receive event associated with that handler ID is signaled. Different motes can associate different receive events with the same handler ID.

In any messaging layer, there are 5 aspects involved in successful communication:

  1. Specifying the message data to send;
  2. Specifying which node is to receive the message;
  3. Determining when the memory associated with the outgoing message can be reused;
  4. Buffering the incoming message; and,
  5. Processing the message on reception
In Tiny Active Messages, memory management is very constrained as you would expect from a small-scale embedded environment.

Let's look at IntToRfm.nc:

IntToRfm.nc
configuration IntToRfm
{
  provides interface IntOutput;
  provides interface StdControl;
}
implementation
{
  components IntToRfmM, GenericComm as Comm;

  IntOutput = IntToRfmM;
  StdControl = IntToRfmM;

  IntToRfmM.Send -> Comm.SendMsg[AM_INTMSG];
  IntToRfmM.StdControl -> Comm;
}

This component provides the IntOutput and StdControl interfaces. This is the first time that we have seen a configuration provide an interface. In the previous lessons we have always used configurations just to wire other components together; in this case, the IntToRfm configuration is itself a component that another configuration can wire to. Got it?

In the implementation section, we see:

  components IntToRfmM, GenericComm as Comm;
The phrase "GenericComm as Comm" is stating that this configuration uses the GenericComm component, but gives it the (local) name Comm. The idea here is that you can easily swap in a different communication module in place of GenericComm, and only need to change this one line to do so; you don't need to change every line that wires to Comm.

We also see some new syntax here in the lines:

  IntOutput = IntToRfmM;
  StdControl = IntToRfmM;
The equal sign (=) is used to indicate that the IntOutput interface provided by IntToRfm is "equivalent to" the implementation in IntToRfmM. We can't use the arrow (->) here, because the arrow is used to wire a used interface to a provided implementation. In this case we are "equating" the interfaces provided by IntToRfm with the implementation found in IntToRfmM.

The last two lines of the configuration are:

  IntToRfmM.Send -> Comm.SendMsg[AM_INTMSG];
  IntToRfmM.StdControl -> Comm;
The last line is simple; we're wiring IntToRfmM.StdControl to GenericComm.StdControl. The first line shows us another use of parameterized interfaces, in this case, wiring up the Send interface of IntToRfmM to the SendMsg interface provided by Comm.

The GenericComm component declares:

  provides {
    ...
    interface SendMsg[uint8_t id];
    ...
  }
In other words, it provides 256 different instances of the SendMsg interface, one for each uint8_t value. This is the way that Active Message handler IDs are wired together. In IntToRfm, we are wiring the SendMsg interface corresponding to the handler ID AM_INTMSG to GenericComm.SendMsg. (AM_INTMSG is a global value defined in tos/lib/Counters/IntMsg.h.) When the SendMsg command is invoked, the handler ID is provided to it, essentially as an extra argument. You can see how this works by looking at tos/system/AMStandard.nc (the implementation module for GenericComm):
  command result_t SendMsg.send[uint8_t id]( ... ) { ... };
Of course, parameterized interfaces aren't strictly necessary here - the same thing could be accomplished if SendMsg.send took the handler ID as an argument. This is just an example of the use of parameterized interfaces in nesC.
 
IntToRfmM: Implementing Network Communication

Now we know how IntToRfm is wired up, but we don't know how message communication is implemented. Take a look at the IntOutput.output() command in IntToRfmM.nc:

IntToRfmM.nc
  bool pending;
  struct TOS_Msg data;

  /* ... */ 

  command result_t IntOutput.output(uint16_t value) {
    IntMsg *message = (IntMsg *)data.data;

    if (!pending) {
      pending = TRUE;

      message->val = value;
      atomic {
      message->src = TOS_LOCAL_ADDRESS;
      }

      if (call Send.send(TOS_BCAST_ADDR, sizeof(IntMsg), &data))
        return SUCCESS;

      pending = FALSE;
    }  
    return FAIL;
  }

The command is using a message structure called IntMsg, declared in tos/lib/Counters/IntMsg.h. It is a simple struct with val and src fields; the first being the data value and the second being the message's source address. We assign these two fields (using the global constant TOS_LOCAL_ADDRESS for the local source address) and call Send.send() with the destination address (TOS_BCAST_ADDR is the radio broadcast address), the message size, and the message data.

The "raw" message data structure used by SendMsg.send() is struct TOS_Msg, declared in tos/system/AM.h. It contains fields for the destination address, message type (the AM handler ID), length, payload, etc. The maximum payload size is TOSH_DATA_LENGTH and is set to 29 by default; you are welcome to experiment with larger data packets but some nontrivial hacking of the code may be required :-) Here we are encapsulating an IntMsg within the data payload field of the TOS_Msg structure.

The SendMsg.send() command is split-phase; it signals the SendMsg.sendDone() event when the message transmission has completed. If send() succeeds, the message is queued for transmission, and if it fails, the messaging component was unable to accept the message.

TinyOS Active Message buffers follow a strict alternating ownership protocol to avoid expensive memory management, while still allowing concurrent operation. If the message layer accepts the send() command, it owns the send buffer and the requesting component should not modify the buffer until the send is complete (as indicated by the sendDone() event).

IntToRfmM uses a pending flag to keep track of the status of the buffer. If the previous message is still being sent, we cannot modify the buffer, so we drop the output() operation and return FAIL. If the send buffer is available, we can fill in the buffer and send a message.
 
The GenericComm Network Stack

Recall that IntToRfm's SendMsg interface is wired to GenericComm, a "generic" TinyOS network stack implementation (found in tos/system/GenericComm.nc). If you look at the GenricComm.nc, you'll see that it makes use of a number of low-level interfaces to implement communication: AMStandard to implement Active Message sending and reception, UARTNoCRCPacket to communicate over the mote's serial port, RadioCRCPacket to communicate over the radio, and so forth. You don't need to understand all of the details of these modules but you should be able to follow the GenericComm.nc wiring configuration by now.

If you're really curious, check out AMStandard.nc for some details on how the ActiveMessage layer is built. For example, it implements SendMsg.send() by posting a task to take the message buffer and send it over the serial port (if the destination address is TOS_UART_ADDR or the radio radio (if the destination is anything else). You can dig down through the various layers of code until you see the mechanism that actually transmits a byte over the radio or UART.
Receiving Messages with RfmToLeds

The RfmToLeds application is defined by a simple configuration that uses the RfmToInt component to receive a message, and the IntToLeds component to display the received value on the LEDs. Like IntToRfm, the RfmToInt component uses GenericComm to receive messages. Most of RfmToInt.nc should be familiar to you by now, but look at the line:

  RfmToIntM.ReceiveIntMsg -> GenericComm.ReceiveMsg[AM_INTMSG];
This is how we specify that Active Messages received with the AM_INTMSG handler ID should be wired to the RfmToIntM.ReceiveMsg interface. The direction of the arrow might be a little confusing here. The ReceiveMsg interface (found in tos/interfaces/ReceiveMsg.nc)only declares an event: receive(), which is signaled with a pointer to the received message. So RfmToIntM uses the ReceiveMsg interface, although that interface does not have any commands to call -- just an event that can be signaled.

Memory management for incoming messages is inherently dynamic. A message arrives and fills a buffer, and the Active Message layer decodes the handler type and dispatches it. The buffer is handed to the application component (through the ReceiveMsg.receive() event), but, critically, the application component must return a pointer to a buffer upon completion.

For example, looking at RfmToIntM.nc,

RfmToIntM.nc
  /* ... */
  event TOS_MsgPtr ReceiveIntMsg.receive(TOS_MsgPtr m) {
    IntMsg *message = (IntMsg *)m->data;
    call IntOutput.output(message->val);

    return m;
  }

Note that the last line returns the original message buffer, since the application is done with it. If your component needs to save the message contents for later use, it needs to copy the message to a new buffer, or return a new (free) message buffer for use by the network stack.
Underlying Details

TinyOS messages contain a "group ID" in the header, which allows multiple distinct groups of motes to share the same radio channel. If you have multiple groups of motes in your lab, you should set the group ID to a unique 8-bit value to avoid receiving messages for other groups. The default group ID is 0x7D. You can set the group ID by defining the preprocessor symbol DEFAULT_LOCAL_GROUP.

  DEFAULT_LOCAL_GROUP = 0x42    # for example...

Use the Makelocal file to set the group ID for all your applications.

In addition, the message header carries a 16-bit destination node address. Each communicating node within a group is given a unique address assigned at compile time.  Two common reserved destination addresses we've introduced thus far are TOS_BCAST_ADDR (0xfff) to broadcast to all nodes or TOS_UART_ADDR (0x007e) to send to the serial port.

The node address may be any value EXCEPT the two reserved values described above.  To specify the local address of your mote, use the following install syntax:

  make mica install.<addr>
where <addr> is the local node ID that you wish to program into the mote.  For example,
  make mica install.38
compiles the application for a mica and programs the mote with ID 38. Read Programming Devices for additional information.
Exercises
  1. Question: What would happen if you built multiple CntToLedsAndRfm nodes and turned them on?
  2. Program one mote with CntToLedsAndRfm and another with RfmToLeds. When you turn on CntToLedsAndRfm, you should see the count displayed on the RfmToLeds device. Congratulations - you are doing wireless networking. Can you change this to implement a wireless sensor? (Hint: Check out apps/SenseToRfm.)

< Previous Lesson | Next Lesson > | Top