永久性存储允许节点即使在断电或者重新编程的情况下页能够保持数据。掌握该技术需要熟悉不同的数据存储类型,包括小数据对象类型(small objects)、日志数据对象类型(logs)、大数据对象类型(arge objects)。具体需要掌握的有: (1)把Flash芯片划分为卷,允许多种不同类型数据的存储 (2)如何存储能够存活一个电力循环(Power Cycle)的配置数据 (3)如何使用日志存储方式 1、简介 Tinyos2.x提供了三种基本数据存储类型,也提供了相应的接口与组件来对底层的存储服务进行抽象。 2、接口 通过一些相关接口(/tos/interfaces)与类型定义(/tos/types),可以了解到存储系统的大体功能。主要有以下几个重要的相关文件: BlockRead BlockWrite Mount ConfigStorage LogRead LogWrite Storage.h 3、 组件 以下组件提供了上述接口的具体实现: ConfigStorageC LogStorageC BlockStorageC 3、实现 在不同的硬件平台,同一存储抽象的组件虽然名字相同,但实际的实现代码却可能不同。幸运的是,TinyOs的变异系统会自动包含相关芯片的正确驱动、你不需要担心这些相关驱动的文件位置,只需要知道这些组件的名称即可。 下面是以Mica2/MicaZ节点上的Atmel AT45DB系列Flash存储的具体实现:
下面是M25P系列Flash存储芯片的具体实现,即telosb所用的Flash芯片:
4、分卷 tinyos2.x系统中,编译时可以用XML文件指明把Flash芯片划分成一个或多个固定大小的卷。这个XML文件,称作卷表(volume table),允许应用开发者指明卷的名称、大小以及在flash中的起始地址。每个卷只能提供一种存储类型(如配置存储、日志存储或块存储)。存储类型定义了flash存储体上数据的物理布局。下面给出了一个卷表实例: <volume_table> <volume name="CONFIGLOG" size="65536"/> <volume name="PACKETLOG" size="65536"/> <volume name="SENSORLOG" size="131072"/> <volume name="CAMERALOG" size="524288"/> </volume_table> 该卷表定义了CONFIGLOG卷,其大小为65536字节,起始位置为0字节;而PACKETLOG卷大小为65536字节,起始位置为65536字节,因为前面的65536字节属于CONFIGLOG卷。此外,改卷表还定义了131072字节的SENSORLOG卷和524288字节的CAMERALOG卷。 特定应用的卷表必须放在该应用程序的当前目录下,且必须重命名为volumes-CHIPNAME.xml,其中CHIPNAME为flash存储芯片的名称。例如,telos节点使用的是M25P系列flash存储体,其芯片驱动可以在tos/chips/stm25p目录找到。因此,一个基于telos平台且使用存储功能的应用程序需要一个名为volumes-stm25p.xml文件。 5、配置数据的存储 配置数据时指用于配置节点属性的一组参数。它们的字节数大小是不确定的,可以小到几十字节,也可以达到几百字节,一般都属于小数数据对象。另外,它们的值也不确定,在各个节点可能不一样,甚至是未知的。 配置数据要求必须能够在复位、电力循环(关闭电源,然后重启电源,算一个电力循环)或者重编程时存活下来。在很多场合,配置数据的保存能力是非常有用的。 5.1、刻度校正。传感器的校准系数在节点出厂时就已配置好,并存储下来,所以它们不会在节点掉电或者重编程时丢失。例如,温度传感器利用校准系数将输出电压信号转换成直观的温度数据。传感器校准系数的结构定义可参考下面:
typedef struct calibration_config_t { int16_t temp_offset; int16_t temp_gain;} calibration_config_t;
5.2、身份信息,即设备辨认信息。如IEEE兼容的MAC地址或TinyOs的TOS_NODE_ID参数在各个节点都是不一样的,但一旦被分配到节点上,这些值就会被保存,即使碰到复位、电力循环和重编程,也不会丢失。
typedef struct radio_config_t { ieee_mac_addr_t mac; uint16_t tos_node_id;} radio_config_t; 5.3、位置信息。节点的位置数据可能在编译时是位置的,只有在部署节点时才确定。例如,一个应用程序可以按按下面的数据形式存储节点的坐标信息:
typedef struct coord_config_t { uint16_t x; uint16_t y; uint16_t z;} coord_config_t;
5.4、传感器参数。与信号检测和发送有关的参数,如采样周期、滤波系数以及可调的报警阈值。这些配置数据的结构形式可能如下所示:
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;
配置数据的存储 现在,已经讨论过为什么使用配置数据,那应该如何存储和读它呢?下面将演示一个简单的例子程序:节点周期性地从flash中读出配置数据,处理后再写回法拉盛。但在深入分析代码前,请仔细阅读 tinyos-2.x/apps/tutorials/BlinkConfig/的代码。 卷在第一次使用之前不包含任何有效数据。因此,代码应当检测卷是否是初次使用,并作出适当的行为(比如预加载默认值)。类似的,当卷中的数据布局发生改变时(例如应用程序需要新的或不同的配置变量),应用程序应当侦查到变更,并作出相应的响应(例如擦写卷,并重新加载默认值)。就这些情况而言,建议我们应该跟踪卷的版本。为此,在配置数据的结构中,引入了版本控制变量。BlinkConfig应用中配置数据的字段包括卷的版本和读取周期:
typedef struct config_t { uint16_t version; uint16_t period;} config_t;
下面将分析BlinkConfig例子程序的设计要点: 1、创建volume-CHIPNAME.xml文件,即建立卷表,并保存在应用程序所在目录。注意,CHIPNAME是目标平台上的flash芯片名字。卷表的具体内容如下:
<volume_table> <volume name="LOGTEST" size="262144"/> <volume name="CONFIGTEST" size="131072"/></volume_table>
编译工具会利用卷表自动创建一个StorageVolumes.h头文件。然而,在有COnfigStorageC组件声明的配件(比如BlinkConfigAppC组件)里,需要手动添加该头文件:
#include "StorageVolumes.h"
2、在BlinkConfigC模块的规范说明里,声明Mount接口和ConfigStorage接口。
module BlinkConfigC { uses { ... interface ConfigStorage as Config; interface Mount; ... }}
3、每个接口必须绑定到提供该接口的组件
configuration BlinkConfigAppC {}implementation { components BlinkConfigC as App; components new ConfigStorageC(VOLUME_CONFIGTEST); ... App.Config -> ConfigStorageC.ConfigStorage; App.Mount -> ConfigStorageC.Mount; ...}
4、在使用flash存储芯片前,必须先挂载卷。在Boot.booted事件处理函数中调用Mount.mount命令,启动挂载:
event void Boot.booted() { conf.period = DEFAULT_PERIOD; if (call Mount.mount() != SUCCESS) { // Handle failure } }
5、如果卷挂载陈宫,就会触发Mount.mountDone事件。接下来,需要检查挂载的卷是否有效。如果卷有效,使用Config.read命令读取卷中的数据。否则,调用Config.commit命令使其有效。
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 } }
6、如果Config.read命令成功,就会触发Config.read/readDone事件。这时,先检查是否成功读取了卷中的内容,如果数据读取成功,再检查版本号。如果版本号与预期的一样,就可以复制配置数据到本地的配置变量,并调节变量period的值。如果版本号不对,就将配置变量设为默认值。最后,调用Config.write函数将新的配置变量写回到flash存储。
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. } }
7、当调用ConfigStore.write命令并触发Config.writeDone事件时,并不能保证数据一定写入了flash。为了确保数据被保存到flash,就有必要调用Config.commit命令:
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 } }
8、最后,当Config.commitDone事件触发时,数据就被写入到flash,并能存活一个电力循环。
event void Config.commitDone(error_t err) { call Leds.led0Off(); call Timer0.startPeriodic(conf.period); if (err != SUCCESS) { // Handle failure } }
|