0%

【nRF52832】学习笔记 --> Flash Data Storage (FDS)存储

😊 转载请全文转载,并标明文章出处

环境介绍

  • 操作系统: Window10 企业版
  • IDE开发环境: ARM_MDK5 (Keil5)
  • SDK版本: SDK_17.0.0
  • 硬件开发板: 官方开发板 PCA10040

Flash Data Storage (FDS)存储

1. FDS概述介绍

  FDS (Flash Data Storage) 模块是用于芯片上闪存存储的一个最小文件系统,可以最大限度地降低数据损坏的风险,并简化与持久存储的交互。Nordic芯片内部FLASH的存储官方提供了两种方式,一种是 FS (Flash Storage)、另一种是FDS (Flash Data Storage) 。相对于FS (Flash Storage)来说,FDS (Flash Data Storage)对数据的组织性和安全性更强。

FDS模块的设计也提供了以下优点:

  • 通过不断验证最小化访问损坏数据的风险:

    • 在断电的情况下,数据可能写入不完整。验证确保FDS能够识别无效数据,并且不会将损坏的数据返回给用户。
  • 在打开记录时提供(可选的)CRC验证,以确保数据自写入以后没有改变。

  • 最小化Flash操作(更新和删除):

    • FDS不是删除整个页面,而是存储新数据的副本,并通过一个字写入来使过时的数据失效。
  • 基本损耗均衡:

    • Flash的使用寿命是有限的,Flash具有最大可擦写次数,一旦对Flash的某个块的擦写超过最大擦写次数,这个块就会损坏。所以对Flash的擦写应该均匀的分布在快上,将块充分利用,减少擦写的次数,提高Flash的使用寿命。
  • 不需要复制数据就可以轻松访问数据,这使得访问数据的影响与数据大小无关。

  • 通过灵活选择数据块的大小来最小化的使用内存。

  • 对数据的内容不加限制 (这意味着它可以包含特殊字符) 。

    Flash数据存储支持同步读取操作和异步写入。

2. FDS功能介绍

  Flash Data Storage API 公开了操作文件和记录的功能,文件由一条或多条包含实际数据的记录组成,每一条记录都由一个 KEY 标志并通过 File ID 关联到文件。File ID 和 Record Key 都不要求必须是唯一的,并且文件可以包含具有相同 Key 的多个记录,可以通过 File ID 和 Record Key 的任意组合访问记录。

For Example,an application could use the following two files:

  • File 1 with two record: 0x1111 = “Phone1”, 0x2222 = “data:123456”

  • File 2 with three record: 0x1111 = “Tables1”, 0x2222 = “data:123456”, 0x2222 = “data: adcsdf”

  可以遍历File 1中的所有记录,所有Key为0x2222的记录。可以遍历File 2 中 Key 为0x1111 的记录。

2.1 Creating Record (创建记录)
  • 新记录写入Flash时,必须提供Record Key、File ID 和 将要写入的数据。
  • 更新记录 (写记录)成功后,返回一个记录描述符,可以用它来访问记录。在访问之前,等待表明写入操作成功完成的事件。
2.2 Manipulating Record (处理记录)
  • 读取、更新或者删除记录的内容,必须通过记录的描述符访问记录。首次记录写入Flash时,FDS会创建并返回一个此记录的描述符。
  • 获取记录描述符函数:
    • fds_record_find
    • fds_record_find_by_key
    • fds_record_find_in_file
  • 以上函数可以根据 Record key 和 File ID 查找记录。
  • FDS 并没有要求 Record Key 或 File ID 是唯一的。因此,在查找记录的时候可能会找到多个匹配的记录。记路查找函数每次返回一个匹配的记录并保存当前查找的进度,返回的 Token 记录了最新匹配的记录位置,通过 Token 在接下来的查找中就可以不必从头开始,直接从 Token 记录的位置开始即可。
2.3 Reading Record (读取记录)
  • 读取记录可以直接在Flash中读取内容 (存储数据和元数据),这意味着应用程序将决定时复制数据,将数据存储在 RAM 中还是在本地使用数据。
  • 访问记录内容 (读取数据),需要打开记录以检索指向记录数据和元数据存储在Flash中的位置的指针。使用 fds_record_open() 函数打开记录,fds_record_open() 函数确保在访问记录时不会修改记录或记录移动到 Flash 中的其他位置。访问记录完成后,必须关闭记录 fds_record_close() 以释放对记录的锁定。
2.4 Updating Record (更新记录)
  • FDS 更新记录,即所谓的写数据。FDS 并不是将新数据写到原来的记录中去覆盖旧数据,而是创建一个新记录,然后将原有的记录作废,更新记录时,记录更新函数会返回一个新的记录描述符。
  • FDS 更新记录的方式是作废旧记录,创建新记录,更新记录频繁的话,每次都会创建新的记录,这样的话会占用Flash的空间,所以通过碎片整理收集功能来释放Flash。
2.5 Deleting Record (删除记录)
  • 记录删除并不是真正的删除记录数据和擦除Flash空间,而是通过设置标志将需要删除的记录作废。每一条被删除的记录,就不能再对它进行打开、读、定位等相关的操作。
  • 记录删除后,记录所使用的Flash空间不会立即释放,如果想要释放Flash空间,必须通过碎片收集来实现。
2.6 Garbage collection (垃圾收集)
  • 不管是更新数据还是擦除数据,都会产生无效数据,这些数据都是实际存在的,占用Flash空间,因此可以调用 fds_gc() 函数API进行垃圾回收。通过执行此操作来释放这些占用Flash的无效数据。
  • 垃圾收集不会自动通过FDS收集。理想情况下,应该在Ble处于低功耗的情况下进行垃圾收集,长期更新记录,产生的无效数据会将内存占满,再一次请求数据更新时,会直接返回 FDS_ERR_NO_SPACE_IN_FLASH (Flash 无空间) 错误,然后必须进行垃圾回收并等待完成后,再更新数据。
2.7 Restrictions on keys and IDs (FDS对 KEY 和 File ID 的限制)
  • Record Key 应在 0x0001 - 0xBFFF 范围内,0x0000 值是由系统保留。从 0xC000 到 0xFFFF 的值保留供Peer Manager模块使用,并且只能在不包含 Peer Manager 的应用程序中使用。

  • File ID 的应在0x0000 - 0xBFFF 范围内, 0xFFFF 是被系统使用的,从 0xC000 到 0xFFFE 的值保留供Peer Manager模块使用,并且只能在不包含 Peer Manager 的应用程序中使用

3. SDK_Config FDS配置宏介绍

3.1 FDS_VIRTUAL_PAGES
  • 配置使用虚拟页的数量及其大小。
  • 使用的 Flash Memory 总量取决于虚拟页的大小和数量。
3.2 FDS_VIRTUAL_PAGE_SIZE
  • 虚拟页的大小。
  • 默认情况下,虚拟页与物理页大小相同。
  • 虚拟页的大小必须是物理页大小的倍数。
  • 已字为单位 (4-Byte)。
3.3 FDS_VIRTUAL_PAGES_RESERVED
  • 其他模块使用的虚拟FLASH页数。
  • FDS模块将其数据存储在FLASH的最后一页中,通过设置此值,可以移动FDS使用的FLASH结束地地址。保留的空间可以被其他模块使用。
3.4 FDS_OP_QUEUE_SIZE
  • FDS 程序模块内部使用的队列大小,如果FDS用户较多或者应用程序中希望一次排队更多的操作而不等待先前的操作完成,可加大队列的大小。
  • 一般情况下,如果应用程序中经常接受到错误代码 FDS_ERR_NO_SPACE_IN_QUEUES,则需要增加队列大小。
3.5 FDS_CRC_CHECK_ON_READ
  • 如果使能该配置项,FDS 将会启动读取操作的CRC检查 (fds_record_open)。
3.6 FDS_CRC_CHECK_ON_WRITE
  • 可选的配置项,如果使能,FDS 写操作会进行校验。
  • 使能该配置时,FDS_CRC_CHECK_ON_READ 必须打开。
3.7 FDS_MAX_USERS
  • 可以注册的最大回调数。
  • 如果收到 FDS_ERR_USER_LIMIT_REACHED 错误,请增加此数值。

4. Storage format (存储格式)

  FDS 以记录的方式将数据存储到 Flash 中,这些记录被分组到文件中,在大多数用例中我们无需详细了解 FDS 如何将数据存储在 Flash 中。以下是FDS使用的数据格式相关信息,如果你对 FDS 详细信息不感兴趣,可以跳过。

4.1 Record layout (记录布局)

  Record 由 Record Header (Record 元数据) 和实际内容组成,Record Header 用来标记Record,记录内容是实际存储的数据,它们按照如图所示的顺序连续存储在 Flash 中。

Layout of a record

4.2 Record header (记录头)

  Record header 由 3个字 (12个Byte) 组成,各部分描述如下:

Record header

  FDS 将 Record Header 写入 Flash 中,首先写入Record Key 和数据长度,接着写入Record ID,最后写入 File ID 和 CRC 校验值,并完成成功的写入操作。检索Record的时候,FDS会忽略掉所有Record Header中第二个字美音写入的 Record,因为缺少Key意味着这条Record没有写入完整。

4.3 Maximum length (最大存储数据长度)

Record 的最大长度取决于三个值:

  • 虚拟 Flash 页的大小 (FDS_VIRTUAL_PAGE_SIZE)
  • 页面标记大小 (2 Words)
  • Record Header的大小 (3 Words)

sdk_config.h 中默认的设置是虚拟 Flash 页的大小和物理页大小一样 (对于nRF52832来说是1024个字,即4096个字节),对于默认值来说去掉页面标记和记录头后,最大数据长度是1019个字。

4.4 Page tag (页面标签)

  FDS 使用的每个虚拟页面都标有一个页面标记,系统使用该标记来存储有关该页面的信息。两个字长的页面标签包含了页面用途 ( 数据存储或垃圾收集)以及页面上安装的文件系统版本的信息。

Page_tags

5. FDS 使用方法

5.1 Initializing the module (初始化FDS)

  Before initializing FDS, you must initialize the SoftDevice and register a callback handler to handle FDS events. (初始化FDS之前,必须初始化SoftDevice并注册回调函数来处理FDS事件。)

  以下代码区展示了如何注册FDS事件的处理函数以及如何初始化 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 != NRF_SUCCESS)
{
/* Initialization failed. */
}
break;
default:
break;
}
}
ret_code_t ret = fds_register(fds_evt_handler);
if (ret != NRF_SUCCESS)
{
/* Registering of the FDS event handler has failed. */
}
ret_code_t ret = fds_init();
if (ret != NRF_SUCCESS)
{
/* Handle error. */
}
5.2 Writing a record

  以下代码区展示了如何写记录:

#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 != NRF_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 != NRF_SUCCESS)
{
/* Handle error. */
}

  此命令已存入队列中,并通过时间回调指示成功或失败,如果成功。fds_record_write函数将返回记录的描述符,该描述符可哦用于进一步操作记录。

5.3 Retrieving data

  以下实例显示了如何查找Record功能来检索Record Key 和 File ID 匹配的所记录的记录描述符,并读取它们的数据内容。

#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) == NRF_SUCCESS)
{
if (fds_record_open(&record_desc, &flash_record) != NRF_SUCCESS)
{
/* Handle error. */
}
/* Access the record through the flash_record structure. */
/* Close the record when done. */
if (fds_record_close(&record_desc) != NRF_SUCCESS)
{
/* Handle error. */
}
}
5.4 Deleting a record

  以下实例显示了如何删除记录:

/* Assume a descriptor returned by a call to fds_record_write() or fds_record_find(),
as shown in the previous example. */
fds_record_desc_t descriptor;
ret_code_t ret = fds_record_delete(&descriptor);
if (ret != NRF_SUCCESS)
{
/* Error. */
}

  调fds_record_delete不会释放此记录使用的 Flash ,要回收删除记录使用的闪存空间,才能释放此记录的Flash,碎片收集运行 fds_gc()。