Flash访问模块FDS用法及常见问题—nRF5 SDK模块系列一
FDS,全称Flash Data Storage,用来访问芯片内部Flash的。当你需要把数据存储在Flash中,或者读取Flash中的用户数据,或者更新或者删除Flash中的数据,那么FDS模块是你最好的选择。FDS采用文件和记录方式来组织Flash数据,也就是说,真正的数据是放在一条记录中,而多条记录组成一个文件。根据应用的需要,整个系统可以只有一个文件,也可以包含多个文件。文件采用文件ID来标示,文件ID为2个字节(注:不能取值为0xFFFF)。一个文件下面可以放一条记录,也可以放多条记录,记录是通过记录key来标示的,记录key也是2个字节长度(注:不能取值为0x0000)。这里需要注意的是,同一个文件下面的两条或者多条记录他们的key可以是一样的,比如我们可以建立如下文件系统:文件1包含2条记录,文件2包含3条记录,文件2包含2条key为0x0003的记录
注:如果你可以保证一个文件下面所有记录的key都不一样,那么文件系统会变得更简洁一些,尤其在find记录的时候,只会返回一条记录,可以简化很多应用逻辑。如前所述,这个不是强制要求:同一个文件下记录key可以相同。
FDS用法
一般而言,按照如下步骤使用FDS模块:
- 修改FDS的默认配置参数,比如总共分配多少Flash空间(默认只分配了8kB Flash空间给用户使用),请到sdk_config.h文件中修改如下默认配置项:
- 通过fds_register注册FDS事件回调函数及通过fds_init初始化FDS模块。FDS模块的初始化,写记录,更新记录,删除记录以及垃圾回收,这些API都是异步的。也就是说调用这些FDS操作的API,只是把相应操作放入队列然后立即返回(队列大小由上述的FDS_OP_QUEUE_SIZE控制),真正的Flash操作结果是通过事件回调函数通知你的。注:现在的FDS模块可以进行多次初始化。示例代码如下所示:
// Simple event handler to handle errors during initialization. static void fds_evt_handler(fds_evt_t const * p_fds_evt) { switch (p_fds_evt->id) { case FDS_EVT_INIT: if (p_fds_evt->result != FDS_SUCCESS) { // Initialization failed. } break; default: break; } } ret_code_t ret = fds_register(fds_evt_handler); if (ret != FDS_SUCCESS) { // Registering of the FDS event handler has failed. } ret_code_t ret = fds_init(); if (ret != FDS_SUCCESS) { // Handle error. }
- 通过fds_record_write创建新的记录,即写记录。注意写记录的时候,必须保证输入参数是全局变量或者static的局部变量,推荐使用全局变量!由于record key可以重复,所以连续调用两次相同的fds_record_write,将生成两条同样key的记录。前面也提及过,fds_record_write是异步的,所以它的返回值为success只是表示操作入队成功,真正的flash操作结果是通过前面注册的fds_evt_handler来通知的。示例代码如下所示:
#define FILE_ID 0x0001 /* The ID of the file to write the records into. */ #define RECORD_KEY_1 0x1111 /* A key for the first record. */ #define RECORD_KEY_2 0x2222 /* A key for the second record. */ static uint32_t const m_deadbeef = 0xDEADBEEF; static char const m_hello[] = "Hello, world!"; fds_record_t record; fds_record_desc_t record_desc; // Set up record. record.file_id = FILE_ID; record.key = RECORD_KEY_1; record.data.p_data = &m_deadbeef; record.data.length_words = 1; /* one word is four bytes. */ ret_code_t rc; rc = fds_record_write(&record_desc, &record); if (rc != FDS_SUCCESS) { /* Handle error. */ } // Set up record. record.file_id = FILE_ID; record.key = RECORD_KEY_2; record.data.p_data = &m_hello; /* The following calculation takes into account any eventual remainder of the division. */ record.data.length_words = (sizeof(m_hello) + 3) / 4; rc = fds_record_write(&record_desc, &record); if (rc != FDS_SUCCESS) { /* Handle error. */ }
- 通过fds_record_open来读记录。读记录之前必须先找到这条记录,这个是通过fds_record_find来实现的,由于同一个文件可以包含多条key相同的记录,所以通过多次调用同一个fds_record_find,可以找到所有相关记录。示例代码如下所示:
#define FILE_ID 0x1111 #define RECORD_KEY 0x2222 fds_flash_record_t flash_record; fds_record_desc_t record_desc; fds_find_token_t ftok; /* It is required to zero the token before first use. */ memset(&ftok, 0x00, sizeof(fds_find_token_t)); /* Loop until all records with the given key and file ID have been found. */ while (fds_record_find(FILE_ID, RECORD_KEY, &record_desc, &ftok) == FDS_SUCCESS) { if (fds_record_open(&record_desc, &flash_record) != FDS_SUCCESS) { /* Handle error. */ } /* Access the record through the flash_record structure. */ /* Close the record when done. */ if (fds_record_close(&record_desc) != FDS_SUCCESS) { /* Handle error. */ } }
- 操作记录,比如fds_record_update,fds_record_delete等,update和delete操作,必须先找到相应记录,然后才能去update或者delete。fds_record_delete不是真得把记录删除,而是将记录标示为无效。而fds_record_update实际包含2步:先找到之前的记录然后将其标记为无效(即delete操作),然后write一条新记录。记住:delete并不会回收Flash空间,无效记录仍然占据着Flash空间,这些无效记录占据着的Flash空间只有经过垃圾回收(fds_gc)才能再次给新记录使用。请注意fds_record_find只会去寻找有效记录,而不会将无效记录返回给用户的。另外,fds_record_ update和fds_record_delete是异步的,所以它们的返回值为success只是表示操作入队成功,真正的flash操作结果是通过前面注册的fds_evt_handler来通知的。示例代码如下所示:
fds_record_desc_t desc = {0}; fds_find_token_t tok = {0}; rc = fds_record_find(CONFIG_FILE, CONFIG_REC_KEY, &desc, &tok); if (rc == FDS_SUCCESS) { /* A config file is in flash. Let's update it. */ fds_flash_record_t config = {0}; /* Open the record and read its contents. */ rc = fds_record_open(&desc, &config); APP_ERROR_CHECK(rc); /* Copy the configuration from flash into m_dummy_cfg. */ memcpy(&m_dummy_cfg, config.p_data, sizeof(configuration_t)); NRF_LOG_INFO("Config file found, updating boot count to %d.", m_dummy_cfg.boot_count); /* Update boot count. */ m_dummy_cfg.boot_count++; /* Close the record when done reading. */ rc = fds_record_close(&desc); APP_ERROR_CHECK(rc); /* Write the updated record to flash. */ rc = fds_record_update(&desc, &m_dummy_record);
if (rc == FDS_ERR_NO_SPACE_IN_FLASH) fds_gc();
else APP_ERROR_CHECK(rc);
} ret_code_t ret = fds_record_delete(&desc); if (ret != FDS_SUCCESS) { /* Error. */ }
- 当Flash不够用时,即FDS写记录或者更新记录操作返回错误FDS_ERR_NO_SPACE_IN_FLASH,请调用垃圾回收函数:fds_gc进行垃圾回收。fds_gc是一个非常耗时的操作过程(请确保操作过程中不会掉电,否则Flash行为未知),它会一个page一个page操作,然后将该page中的有效记录拷贝到swap page,然后擦除该page,并标记该page为swap page,而之前的swap page则变为data page,如此往复,直到把所有page都回收完。只有经过fds_gc后,之前无效记录占据的Flash空间才会释放,这个时候才会有多余的Flash空间给用户去操作。
建议大家直接参考SDK里面自带的fds例子来编写自己的fds应用代码,SDK自带的fds例子所在目录为:SDK安装目录\examples\peripheral\flash_fds (注:从SDK14之后才有fds例子)
理解FDS
FDS作为上层模块,它是通过调用fstorage API来实现自己的功能,fstorage又是通过调用NVMC外设驱动或者softdevice Flash访问API来达到操作Flash的目的,调用关系图如下所示:
当softdevice存在的时候,建议使用nrf_fstorage_sd后端;没有softdevice的时候,请使用nrf_fstorage_nvmc后端。
根据有无bootloader,FDS将操作不同的Flash空间,如下:
当你通过FDS把数据写入Flash中,除了数据本身,FDS还会在这条记录中加入额外的信息:记录头header,一条记录在Flash中完整的格式如下所示:
字段 |
大小 |
描述 |
Record key |
16 bits |
Key that can be used to find the record. The value FDS_RECORD_KEY_DIRTY (0x0000) is reserved by the system to flag records that have been invalidated. See Restrictions on keys and IDs for further restrictions. |
Data length |
16 bits |
Length of the data that is stored in the record (in 4-byte words). |
File ID |
16 bits |
ID of the file that the record is associated with. The value FDS_FILE_ID_INVALID (0xFFFF) is used by the system to identify records that have not been written correctly. See Restrictions on keys and IDs for further restrictions. |
CRC value |
16 bits |
CRC value of the whole record (checks can be enabled by setting the FDS_CRC_ENABLED compile flag, see Configuration). |
Record ID |
32 bits |
Unique identifier of the record. 注:对用户不可见 |
所以,在计算记录总共占用多少Flash空间的时候,记得一定要把每条记录的header(3个word)也加上。
FDS使用常见问题
大家在使用FDS模块时,经常碰到的问题有如下几种:
- FDS不支持掉电保护,所以在Flash操作过程中出现了掉电,FDS行为将未知
- OTA的时候,新固件的FDS page数目一定要等于老固件的FDS page数,否则将出现不可知行为
- fds_record_write或者fds_record_update后,强烈建议回读该记录,以确保记录的确write或者update成功
- 忘了给参数清0。Nordic提供的API输入参数很多都是结构体变量,这些变量使用之前,记得一定要通过memset先清0。如果忘了清0,就会出现一些匪夷所思的现象。
fds_record_desc_t desc; //= {0}; //错误,忘了清0 fds_find_token_t tok; //= {0}; //错误,忘了清0
- 忘了使用全局变量或者静态局部变量。因为write和update操作都是异步的,所以record.data.p_data必须指向全局变量或者静态局部变量,以保证Flash操作过程中p_data指向的内容不会更改。
- 变量起始地址必须字对齐。Flash操作是以word为单位的,所以要求write和update操作的p_data指向的变量的起始地址必须word对齐,大家可以使用伪汇编指令“__ALIGN(sizeof(uint32_t))”来保证该变量起始地址是word对齐的。
- Update或者delete之前必须先find。fds_record_update或者fds_record_delete会用到参数descriptor,这个descriptor必须是通过fds_record_find返回的。
- 忘了使用fds_gc导致Flash fatal error或者其他奇奇怪怪的问题。当write或者update报FDS_ERR_NO_SPACE_IN_FLASH错误时,记得一定要调用fds_gc。或者当delete record或者update record达到一定次数后,主动调用fds_gc。或者通过查看fds_stat得到dirty record数目达到某个值后,主动调用fds_gc。
- SDK已知问题。每个版本SDK都有或多或少的问题,这些问题都可以在Nordic devzone上查到。比如SDK12.2.0 fds_gc在某些情况下,就会有问题,请参考:https://devzone.nordicsemi.com/question/93241/what-are-sdk-12x0-known-issues/,所以,一般建议大家使用最新版SDK,最新版SDK会把之前发现的问题都修复掉,它的稳定性和可靠性都是最高的。
- 最后再次强调一遍:FDS不支持掉电保护,所以在FDS操作过程中,尤其是垃圾回收过程中,发生了掉电,那么Flash内容将变得不可靠。所以强烈建议大家:在每一次write或者update之后,都把相应记录读出来,跟原始内容进行比对,以确保记录真的写成功或者更新成功了