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:
- Specifying the message data to send;
- Specifying which node is to receive the message;
- Determining when the memory associated with the outgoing message can be
reused;
- Buffering the incoming message; and,
- 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.
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.
- Question: What would happen if you built multiple CntToLedsAndRfm
nodes and turned them on?
- 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.)
posted on 2005-11-28 01:08
井冈山 阅读(345)
评论(0) 编辑 收藏 引用 所属分类:
tinyOS