本文档将介绍 Token,并展示如何在 EmberZNet PRO、Silicon Labs Flex 和 Silicon Labs Thread 应用中将使用 Token 进行非易失性数据存储。
Token 是一种抽象数据常量,它对于应用具有特殊持久化意义。Token 在重启和断电期间用于保存某些重要数据。这些 Token 存储在非易失性存储器中。Token 具有两个部分:token key
和 token data
。Token key
是一个唯一的标识符,用于存储和检索 token data
。在多数情况下,“token” 一词用于表示 token key
、token data
或 key + data
。通常根据上下文可以清楚地知道 token 要表示的意思。在本文档中,token 总是表示为 key + data
。
Token 的写入取决于它们的用途。在芯片的生命周期中,Manufacturing token
写入的次数极少,它们存储在 Flash 的绝对地址上。
Dynamic token
(经常需要访问的)存储在 Flash 的一个专用区域上,并使用循环存储(memory-rotation)算法来防止 Flash 的过度使用。Silicon Labs 提供了三种不同的 dynamic token
实现:SimEEv1(模拟 EEPROM 第一版)、SimEEv2(模拟 EEPROM 第二版)和 NVM3(第三代非易失性存储器)。有关非易失性数据存储概念的概述及三种实现的描述,请参见 UG103.7: Non-Volatile Data Storage Fundamentals。
Dynamic token
有两种类型:
Non-indexed/basic dynamic token
。这种类型可以视为一个简单的 char 变量类型。它们可以用来存储数组,但如果其中一个元素发生了更改,则必须重写整个数组。Counter token
是一种特殊类型的non-indexed dynamic token
,它们用来存储每次递增 1 的数字。
Indexed dynamic token
可以视为一个 char 变量的链接数组(linked array),其中每个元素都应该独立地进行更改,因此这种类型在内部被存储为独立的 token,并通过 token API 显式访问。
与一般的 RAM 用法相比,dynamic token
系统的根本目的是允许 token data
在重启和断电期间持久化。通过使用 token key
来标识相应的数据,请求 token data
的应用不需要知道数据的确切存储位置。这简化了应用设计和代码重用。
由于 EM3x 和 EFR32 不提供内部 EEPROM,因此使用一部分内部 Flash 来实现 dynamic token
存储机制,以供协议栈和应用使用。对于 SimEEv1,EM35x 和 EFR32 需要 4kB 或 8kB 以上的 Flash 进行非易失性数据存储。SimEEv2 需要 36kb 以上的 Flash。使用 SimEEv2 需要一个来自 Silicon Labs 的特殊密钥,这样做的目的是防止从第一版意外升级到第二版。降级会导致所有数据丢失,而升级则可能不会保留所有 token。使用 NVM3,可以从 3 个 Flash 页上配置存储大小。
使用 dynamic token
存储非易失性数据的器件具有不同的 Flash 性能(可保障擦除周期)。EM35x-I Flash 可以保证 2,000 次擦除周期;其他 EM35x Flash 可以保证 20,000 次擦除周期;EFR32 Flash 可以保证 10,000 次擦除周期,具体情况请参阅器件的数据手册。由于擦除周期的限制,dynamic token
存储机制实现了一种耗损平衡(wear-leveling)算法,该算法有效地扩展了单个 token 的擦除周期数。
Silicon Labs 建议应用设计者熟悉不同的 dynamic token
存储机制,以便他们设计的应用的令牌使用达到最佳 Flash 擦除周期。有关 SimEEv1/v2 的详情,请参阅文档 AN703: Using Simulated EEPROM version 1 and version 2 for the EM35x and EFR32 SoC Platforms;有关 NVM3 的详情,请参阅 AN1135: Using Third Generation Non-Volatile Memory (NVM3) Data Storage。
网络协议栈提供了一组简单的 API 用来访问 token data
。完整的文档可以在 stack API reference 中找到。Non-indexed/basic token
的 API 函数包括:
void halCommonGetToken(data, token);
void halCommonSetToken(token, data);
在本例中 “token” 是 token key
,“data” 是 token data
。
Indexed token
的 API 函数包括:
void halCommonGetIndexedToken(data, token, index);
void halCommonSetIndexedToken(token, index, data);
还有一个特殊的 API 用来增量 counter token
:
void halCommonIncrementCounterToken(token);
注意:尽管可以使用
halCommonSetToken()
写入counter token
,但是这样做的效率很低,并且违背了使用counter token
的目的。
将 dynamic token
添加到头文件包括三个步骤:
- 定义 token 的名称。
- 如果 token 使用的是应用定义的(application-defined)类型,则添加 token 所需的任何 typedef。
- 定义 token 的存储。
在定义名称时,不要在前面添加单词 TOKEN
。对于 SimEEv1/v2 dynamic token
,使用单词 CREATOR
:
/**
* Custom Application Tokens
*/
// Define token names here
#define CREATOR_DEVICE_INSTALL_DATA (0x000A)
#define CREATOR_HOURLY_TEMPERATURES (0x000B)
#define CREATOR_LIFETIME_HEAT_CYCLES (0x000C)
对于 NVM3 dynamic token
,使用单词 NVM3KEY
。对于不同的协议栈,NVM3 域是不同的。
注意:下面示例假定为一个 ZigBee 应用。
另请注意:
HOURLY_TEMPERATURES
的NVM3KEY
值被设置为一个值,其后面的 0x7F 值未被使用,因为这是一个indexed token
。有关 NVM3 默认实例密钥空间的详情以及为indexed token
选择NVM3KEY
值的限制条件,请参阅 AN1135: Using Third Generation Non-Volatile Memory (NVM3) Data Storage。
/**
* Custom Zigbee Application Tokens
*/
// Define token names here
#define NVM3KEY_DEVICE_INSTALL_DATA (NVM3KEY_DOMAIN_USER | 0x000A)
// This key is used for an indexed token and the subsequent 0x7F keys are also reserved
#define NVM3KEY_HOURLY_TEMPERATURES (NVM3KEY_DOMAIN_USER | 0x1000)
#define NVM3KEY_LIFETIME_HEAT_CYCLES (NVM3KEY_DOMAIN_USER | 0x000C)
这些示例定义了 token key
并将其链接到一个程序化变量。Token 的名称实际上是 DEVICE_INSTALL_DATA
、hourly_temperature
和 LIFETIME_HEAT_CYCLES
。根据使用情况,将不同的标签添加到名称的前面。因此,在示例代码中将它们参考为 TOKEN_DEVICE_INSTALL_DATA
等。
Token key
值在此设备中必须是唯一的。Token key
是应用程序的使用与对应数据的链接的关键,因此在定义新 token 甚至在更改现有 token 的结构时,应该始终保证 token key
的唯一性。CREATOR
代码值为 16-bit;NVM3KEY
代码值为 20-bit。
对于 SimEEv1/v2,第 1 位预留给 manufacturing token
、stack token
和应用框架定义的 application token
,因此所有 custom token
的 token key
都应该小于 0x8000;对于 NVM3,custom token
应该使用 NVM3KEY_DOMAIN_USER
范围,以避免与 NVM3KEY_DOMAIN_ZIGBEE
范围内的 stack token
发生冲突。
前面的 CREATOR
代码示例中的每个 token 都是不同的类型,其中 HOURLY_TEMPERATURES
和 LIFETIME_HEAT_CYCLES
的类型是 C 中的内置类型,DEVICE_INSTALL_DATA
的类型是自定义的数据结构。
Token 的类型使用 4.2 Custom Token 中介绍的结构来定义,然后根据如下示例。
注意:Token 的类型必须只在一个地方定义,因为如果定义了两次相同的数据结构,编译器将会报错。
#ifdef DEFINETYPES
// Include or define any typedef for tokens here
typedef struct {
int8u install_date[11]; /** YYYY-mm-dd + NULL */
int8u room_number; /** The room where this device is installed */
} InstallationData_t;
#endif //DEFINETYPES
在定义了任何的自定义类型后,将定义 token 的存储。这将通知 token 管理软件已定义的 token。每个 token,无论是自定义的还是默认的,在这一部分中都拥有自己的条目:
#ifdef DEFINETOKENS
// Define the actual token storage information here
DEFINE_BASIC_TOKEN(DEVICE_INSTALL_DATA, InstallationData_t, {0, {0,...}})
DEFINE_INDEXED_TOKEN(HOURLY_TEMPERATURES, int16u, HOURS_IN_DAY, {0,...})
DEFINE_COUNTER_TOKEN(LIFETIME_HEAT_CYCLES, int32u, 0}
#endif //DEFINETOKENS
下面将详细介绍此过程中的每个步骤。
DEFINE_BASIC_TOKEN
有三个参数:名称(DEVICE_INSTALL_DATA
)、数据类型(InstallationData_t
),以及 token 的默认值({0, {0, ...}}
)。
默认值采用与 C 默认初始化相同的语法。在这种情况下,第一个值(room_number
)初始化为 0,下一个值(install_date
)设置为全 0,因为 {0, ...}
语法用 0 填充数组的其余部分。
DEFINE_COUNTER_TOKEN
的语法与 DEFINE_BASIC_TOKEN
相同。
DEFINE_INDEXED_TOKEN
需要数组的长度(在本例中为 HOURS_IN_DAY
或 24)。其最终参数是数组中每个元素的默认值。同样,在这种情况下,它被初始化为全 0。
某些应用可能需要在安装时存储配置数据。这通常使用的是 basic token
。假设已经定义了一个 basic token
来使用 token key
DEVICE_INSTALL_DATA
,并且数据结构如下所示:
typedef struct {
int8u install_date[11]; /** YYYY-mm-dd + NULL */
int8u room_number; /** The room where this device is installed */
} InstallationData_t;
然后您可以使用这样的代码片段来访问它:
InstallationData_t data;
// Read the stored token data
halCommonGetToken(&data, TOKEN_DEVICE_INSTALL_DATA);
// Set the local copy of the data to new values
data.room_number = < user input data >
MEMCOPY(data.install_date, < user input data>, 0, sizeof(data.install_date));
// Update the stored token data with the new values
halCommonSetToken(TOKEN_DEVICE_INSTALL_DATA, &data);
计算恒温器(thermostat)启动的加热循环次数通常使用 counter token
。假设它被命名为 LIFETIME_HEAT_CYCLES
,并且是一个 int32u
。
void requestHeatCycle(void)
{
/// < application logic to initiate heat cycle >
halCommonIncrementCounterToken(TOKEN_LIFETIME_HEAT_CYCLES);
}
int32u totalHeatCycles(void)
{
int32u heatCycles;
halCommonGetToken(&heatCycles, TOKEN_LIFETIME_HEAT_CYCLES);
return heatCycles;
}
要存储一组类似的值,例如一天中首选温度设置的数组,应使用默认数据类型 int16s
来存储要求的温度,并定义一个名为 HOURLY_TEMPERATURES
的 indexed token
。
整个数据集的本地副本应该是这样的:
int16s hourlyTemperatures[HOURS_IN_DAY]; /** 24 hours per day */
在应用代码中,您可以使用 indexed token
的函数访问或更新一天中的某个值:
int16s getCurrentTargetTemperature(int8u hour)
{
int16s temperatureThisHour = 0; /** Stores the temperature for return */
if (hour < HOURS_IN_DAY) {
halCommonGetIndexedToken(&temperatureThisHour, TOKEN_HOURLY_TEMPERATURES, hour);
}
return temperatureThisHour;
}
void setTargetTemperature(int8u hour, int16s targetTemperature)
{
if (hour < HOURS_IN_DAY) {
halCommonSetIndexedToken(TOKEN_HOURLY_TEMPERATURE, hour, &temperatureThisHour);
}
}
Manufacturing token
被定义并视为 basic (non-indexed) token
。主要区别在于 token 的存储及访问方式,manufacturing token
在专用的 Flash 页面上(具有固定的绝对地址)。当芯片启用了读保护特性时,只能从片上代码读取 manufacturing token
;如果未启用读保护特性,则可以通过外部编程工具来读取它们。仅当 token 处于擦除状态时,才能从片上代码中写入 manufacturing token
。Manufacturing token
应使用专用的 API halCommonGetMfgToken()
和 halCommonSetMfgToken()
来访问,这些 API 的参数与 basic token
API 相同。使用 MfgToken
API的两个主要目的是:
- 更快地访问
- 在调用
emberInit()
之前的 boot 过程的早期访问
Manufacturing token
也可以通过 basic token
API halCommonGetToken()
和 halCommonSetToken()
来访问。
Manufacturing token
只能用外部编程工具覆盖(而不能用片上代码覆盖),因为要覆盖已写入的任何 manufacturing token
需要擦除 manufacturing token
的专用 Flash 页面。如果启用了读保护,则必须先禁用它,否则将会擦除芯片的内容(作为副作用)。Manufacturing token
没有耗损平衡,所以尽量不要覆盖它们。
网络协议栈包含了一些 default token
。这些 token 根据它们的软件用途进行分组:
Stack Token
是由协议栈设置的运行时配置选项,应用不应该更改这些内容。Application Framework Token
是由应用框架使用并由 AppBuilder 生成的application token
,在项目生成后应用不应该更改这些内容。这些示例包括ZCL attribute token
和plugin token
。Manufacturing Token
在制造时设置,不能通过应用更改。
要查看 stack token
,请参阅文件:
<install-dir>/stack/config/token-stack.h
要查看 Application Framework token
,请在 AppBuilder 中生成项目后,在项目目录下参阅文件 <project_name>_tokens.h
和 afv2-token.h
。<project_name>_tokens.h
包含了应用选择存储在非易失性存储器中的 ZCL attributes token
。文件 afv2-token.h
包括了 plugin token
头文件和 custom application token
头文件。
要查看 EFR32 或 EM3x 系列芯片的 manufacturing token
,请分别参阅以下文件:
<install-dir>/hal/micro/cortexm3/efm32/token-manufacturing.h
<install-dir>/hal/micro/cortexm3/token-manufacturing.h
搜索 CREATOR
以查看已定义的名称。如果整个文件看起来难以阅读,则请只关注描述 token 的部分。在创建板子时,供应商可以设置一些固定的 manufacturing token
。例如,可以设置自定义 EUI-64 地址,以覆盖 Silicon Labs 提供的内置 EUI-64 地址。其他 token(如内部 EUI-64)不能被覆盖。
有关制造和 token 编程的详情,请参阅文档 AN710: Bringing up Custom Devices for the Ember® EM35x SoC or NCP Platform。
在 Simplicity Studio 中,可以通过 .isc
文件中的 “Includes” 选项卡下的 “Token Configuration” 部分添加 custom application dynamic token
和 manufacturing token
。有关定义 custom application dynamic token
的详细信息,请参阅 2.1 定义 Token。定义 custom manufacturing token
较为罕见,您可以参考 default manufacturing token
定义文件(请参阅 4.1 Default Token 部分)以获取完整的说明和示例。
Custom application dynamic token
文件应该具有以下结构:
/**
* Custom Application Tokens
*/
// Define token names here
#ifdef DEFINETYPES
// Include or define any typedef for tokens here
#endif //DEFINETYPES
#ifdef DEFINETOKENS
// Define the actual token storage information here
#endif //DEFINETOKENS
您可以参考 stack token
定义文件 <install-dir>/stack/config/token-stack.h
,以作为创建 application token
的指南。
注意:对于
custom token
文件,不应该使用token-stack.h
中的包含保护(#ifndef
)。