This lesson introduces permanent (non-volatile) data storage in TinyOS. Permanent storage allows a node to persist data even if power is disconnected or the node is reprogrammed with a new image. You will become familar with different kinds of data storage including small objects, logs, and large objects. You will be exposed to the TinyOS interfaces and components that support permanent data storage on motes and you will learn how to:
tos/interfaces
directory and the types defined in the
tos/types
to familiarize ourselves with the general
functionality of the storage system:
Components provide concrete implementations of the interfaces. You should be familiar with these components because your code needs to specify both the interfaces your application uses as well as the components which provide (implement) these interfaces:
The preceding components are actually naming wrappers. Since TinyOS supports multiple platforms, each of which might have its own implementation of the storage drivers, platform-specific naming wrappers are used to bridge the general storage interfaces to their underlying, platform-specific implementations.
For example, the preceding links are all specific to the ST
Microelectronics M25Pxx family of flash memories used in the Telos and
Tmote Sky motes. You do not need to worry about the details
of where these files reside because TinyOS's make system includes the
correct drivers automatically. However, you do need to know
what these components are called because your code must list them in a
components
declaration.
If you are curious, the following links will let you browse the naming wrappers for the Atmel AT45DB family of flash memories used in the Mica2/MicaZ motes:
Finally, the following links will let you browse the naming wrappers for the Intel imote2 flash memory:
TinyOS 2.x divides a flash chip into one or more fixed-sized volumes that are specified at compile-time using an XML file. This file, called the volume table, allows the application developer to specify the name, size, and (optionally) the base address of each volume in the flash. Each volume provides a single type of storage abstraction (e.g. configuration, log, or block storage). The abstraction type defines the physical layout of data on the flash memory. A volume table might look like:
<volume_table> <volume name="CONFIGLOG" size="65536"/> <volume name="PACKETLOG" size="65536"/> <volume name="SENSORLOG" size="131072"/> <volume name="CAMERALOG" size="524288"/> </volume_table>
The volume table for a particular application must be placed in the
application's directory (where one types 'make') and must be named
volumes-CHIPNAME.xml
where CHIPNAME is replaced with the
platform-specific flash chip's name. For example, the Telos mote uses
the ST Microelectronics M25P family of flash memories. The drivers
for these chips can be found in the tos/chips/stm25p
directory. Therefore, a Telos-based application that uses the storage
abstractions needs a file named volumes-stm25p.xml
.
Note that the size parameter is a multiple of the erase unit for a particular flash chip. See Section 4.1 in TEP 103 for more details.
This lesson shows how configuration data can be written to and read from non-volatile storage. Configuration data typically exhibit some subset of the following properties. They are limited in size, ranging from a fews tens to a couple hundred bytes. Their values may be non-uniform across nodes. Sometimes, their values are unknown prior to deployment in the field and sometimes their values are hardware-specific, rather than being tied to the software running on a node.
Because configuration data can be non-uniform across nodes or unknown a priori, their values may be difficult to specify at compile-time and since the data are sometimes hardware-specific, their values must survive reprogramming, suggesting that encoding these values in the program image is not the simplest approach. Storing configuration data in volatile memory is also problematic since this data would not survive a reset or power cycle.
In summary, configuration data must persist through node resets, power cycles, or reprogramming, and then be restored afterward. The ability to persist and restore configuration data in this manner is useful in many scenarios.
typedef struct calibration_config_t { int16_t temp_offset; int16_t temp_gain; } calibration_config_t;
typedef struct radio_config_t { ieee_mac_addr_t mac; uint16_t tos_node_id; } radio_config_t;
typedef struct coord_config_t { uint16_t x; uint16_t y; uint16_t z; } coord_config_t;
typedef struct sense_config_t { uint16_t temp_sample_period_milli; uint16_t temp_ema_alpha_numerator; uint16_t temp_ema_alpha_denominator; uint16_t temp_high_threshold; uint16_t temp_low_threshold; } sense_config_t;
Now that we have discussed why one might use this type of
storage, let's see how to use it. We will implement a simple
demo application that illustrates how to use the Mount
and ConfigStorage
abstractions. A timer period will be
read from flash, divided by two, and written back to flash. An LED is
toggled each time the timer fires. But, before diving into code,
let's discuss some high-level design considerations.
See
tinyos-2.x/apps/tutorials/BlinkConfig/
for the
accompanying code.
Prior to its first usage, a volume does not contain any valid data. So, our code should detect the first usage of a volume and take any appropriate actions (e.g. preload it with default values). Similarly, when the data layout of the volume changes (for example, if the application requires new or different configuration variables), then application code should detect this and take appropriate actions (e.g. migrate the old data to the new layout or erase the volume and reload the defaults). These requirements suggest that we should have a way of keeping track of the volume version. We will use a version number for this purpose (and will need to maintain a discipline of updating the version number when the data layout changes incompatibly). Our configuration struct might have the following fields for the version number and blink period:
typedef struct config_t { uint16_t version; uint16_t period; } config_t;
volumes-CHIPNAME.xml
file, enter the volume
table in this file, and place the file in the application directory.
Note that CHIPNAME
is the flash chip used on your target
plaform. For example, CHIPNAME
will be
stm25p
for the Telos platform and at45db
for
the MicaZ platform. Our file will have the following contents:
<volume_table> <volume name="LOGTEST" size="262144"/> <volume name="CONFIGTEST" size="131072"/> </volume_table>This volume information is used by the toolchain to create an include file. The auto-generated file, however, has to be included manually. Place the following line in the configuration file which declares the ConfigStorageC component (e.g.
BlinkConfigAppC.nc
):
#include "StorageVolumes.h"
Mount
and
ConfigStorage
interfaces (note that we rename
ConfigStorage
to Config
).
module BlinkConfigC { uses { ... interface ConfigStorage as Config; interface Mount; ... } }
configuration BlinkConfigAppC { } implementation { components BlinkConfigC as App; components new ConfigStorageC(VOLUME_CONFIGTEST); ... App.Config -> ConfigStorageC.ConfigStorage; App.Mount -> ConfigStorageC.Mount; ... }
event void Boot.booted() { conf.period = DEFAULT_PERIOD; if (call Mount.mount() != SUCCESS) { // Handle failure } }
Mount.mountDone
event will be signaled. The following code shows how to check if the
volume is valid, and if it is, how to initiate a read from the volume
using the ConfigStore.read
command. If the volume is
invalid, calling Config.commit
will make it valid (this
call is also used to flush buffered data to flash much like the UNIX
fsync system call is supposed to flush buffered writes to disk):
event void Mount.mountDone(error_t error) { if (error == SUCCESS) { if (call Config.valid() == TRUE) { if (call Config.read(CONFIG_ADDR, &conf, sizeof(conf)) != SUCCESS) { // Handle failure } } else { // Invalid volume. Commit to make valid. call Leds.led1On(); if (call Config.commit() == SUCCESS) { call Leds.led0On(); } else { // Handle failure } } } else{ // Handle failure } }
Config.readDone
event will occur. In this case, we first check for a successful read,
and if successful, we then check the version number. If the version
number matches what we expected, we copy of the configuration data to
a local variable, and adjust its values. If there is a version
mismatch, we set the value of the configuration information to a
default value. Finally, we call the the Config.write
function:
event void Config.readDone(storage_addr_t addr, void* buf, storage_len_t len, error_t err) __attribute__((noinline)) { if (err == SUCCESS) { memcpy(&conf, buf, len); if (conf.version == CONFIG_VERSION) { conf.period = conf.period/2; conf.period = conf.period > MAX_PERIOD ? MAX_PERIOD : conf.period; conf.period = conf.period < MIN_PERIOD ? MAX_PERIOD : conf.period; } else { // Version mismatch. Restore default. call Leds.led1On(); conf.version = CONFIG_VERSION; conf.period = DEFAULT_PERIOD; } call Leds.led0On(); call Config.write(CONFIG_ADDR, &conf, sizeof(conf)); } else { // Handle failure. } }
ConfigStore.write
is called and
Config.writeDone
is signaled. To ensure data is
persisted to flash, a ConfigStore.commit
call is
required:
event void Config.writeDone(storage_addr_t addr, void *buf, storage_len_t len, error_t err) { // Verify addr and len if (err == SUCCESS) { if (call Config.commit() != SUCCESS) { // Handle failure } } else { // Handle failure } }
Config.commitDone
event is
signaled, data has been durably written to flash and will survive a
node power cycle:
event void Config.commitDone(error_t err) { call Leds.led0Off(); call Timer0.startPeriodic(conf.period); if (err == SUCCESS) { // Handle failure } }
The TinyOS LogStorage abstraction supports these requirements. The log is record based: each call to LogWrite.append (see below) creates a new record. On failure (a crash or power cycle), the log only loses whole records from the end of the log. Additionally, once a circular log wraps around, log writes only lose whole records from the beginning of the log.
A demo application called PacketParrot
shows how to use
the LogWrite
and LogRead
abstractions. A
node writes received packets to a circular log and retransmits the
logged packets (or at least the parts of the packets above the AM
layer) when power is cycled.
See
tinyos-2.x/apps/tutorials/PacketParrot/
for the
accompanying code.
The application logs packets it receives from the radio to flash. On a subsequent power cycle, the application transmits any logged packets, erases the log, and then continues to log packets again. The red LED is on when the log is being erased. The blue (yellow) LED turns on when a packet is received and turns off when a packet has been logged successfully. The blue (yellow) LED remains on when packets are being received but are not logged (because the log is being erased). The green LED flickers rapidly after a power cycle when logged packets are transmitted.
typedef nx_struct logentry_t { nx_uint8_t len; message_t msg; } logentry_t;
LogRead.read
:
event void AMControl.startDone(error_t err) { if (err == SUCCESS) { error_t e; do { e = call LogRead.read(&m_entry, sizeof(logentry_t)); } while (e != SUCCESS); } else { call AMControl.start(); } }
LogRead.read
returns SUCCESS, then a
LogRead.readDone
event will be signaled shortly
thereafter. When that happens, we check if the data that was returned
is the same length as what we expected. If it is, we use the data but
if not, we assume that either the log is empty or that we have lost
synchronization, so the log is erased:
event void LogRead.readDone(void* buf, storage_len_t len, error_t err) { if ( (len == sizeof(logentry_t)) && (buf == &m_entry) ) { call Send.send(&m_entry.msg, m_entry.len); call Leds.led1On(); } else { error_t e; do { e = call LogWrite.erase(); } while (e != SUCCESS); call Leds.led0On(); } }
PacketParrot
application stores packets received
over the radio to flash by first saving the message_t
and
its length to a log_entry_t
struct and then calling
LogWrite.append
:
event message_t* Receive.receive(message_t* msg, void* payload, uint8_t len){ call Leds.led2On(); if (!m_busy) { m_busy = TRUE; m_entry.len = len; m_entry.msg = *msg; if (call LogWrite.append(&m_entry, sizeof(logentry_t)) != SUCCESS) { m_busy = FALSE; } } return msg; }
LogWrite.write
returned SUCCESS, then a short
time later, a LogWrite.appendDone
will be signaled. This
event returns the details of the write including the source buffer,
length of data written, whether any records were lost (if this is a
circular buffer) and any error code. If no errors occurred, then the
data was written to flash with atomicity, consistency, and durability
guarantees (and will survive node crashes and reboots):
event void LogWrite.appendDone(void* buf, storage_len_t len, bool recordsLost, error_t err) { m_busy = FALSE; call Leds.led2Off(); }
See
tinyos-2.x/apps/tests/storage/Block/
for an example
of code that uses the Block storage abstraction.
< Previous Lesson | Top | Next Lesson >