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).
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 { |
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.
In any messaging layer, there are 5 aspects involved in successful communication:
Let's look at IntToRfm.nc:
IntToRfm.nc
configuration IntToRfm |
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;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.
StdControl = IntToRfmM;
The last two lines of the configuration are:
IntToRfmM.Send -> Comm.SendMsg[AM_INTMSG];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.
IntToRfmM.StdControl -> 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.
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; |
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.
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.
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
/* ... */ |
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.
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.38compiles the application for a mica and programs the mote with ID 38. Read Programming Devices for additional information.