nRF52832闪存FDS使用(SDK17.1.0)_nordic--nrf52832--fds-程序员宅基地

技术标签: nRF  FDS  SDK17.1.0  Flash  nRF52832  

陈拓 2022/10/29-2022/11/22

1. 简介

对于Nordic芯片内部FLASH存储管理有两种方式,FS (Flash Storage)和FDS (Flash Data Storage) 。FS是FDS的底层实现,FDS是对FS的封装,使用更容易。

Flash Data Storage(FDS)模块是用于芯片上闪存的极简文件系统,可将数据损坏的风险降至最低,并简化与持久存储的交互。它通过在文件中组织数据来实现这一点,文件由一个或多个记录组成。记录包含实际数据,可以写入、删除、更新或检索。

将数据视为文件的概念提供了高度抽象。您可以在不详细了解内部使用的实际数据格式的情况下使用FDS模块。您可以只处理文件和记录,并将模块用作黑盒。

该模块旨在提供以下好处:

通过不断验证将访问损坏数据的风险降至最低:在断电的情况下,数据可能会写入不完整。验证可确保FDS识别无效数据,并且不会将损坏的数据返回给用户。

在打开记录时提供(可选)CRC验证,以确保数据自写入后未发生更改。

最小化闪存操作(更新和删除):FDS存储新数据的副本,而不是删除整个页面,并通过单字写入使过时数据无效。

基本磨损均衡:顺序写入和垃圾收集提供了均匀的闪存使用水平。

在不复制数据的情况下轻松访问数据,这使得访问数据的影响与数据的大小无关。

通过允许灵活的数据大小,最大限度地减少内存使用。

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

FDS使用Flash Storage(fstorage)作为后端写入闪存。闪存又依赖SoftDevice执行写入。闪存数据存储支持同步读取和异步写入操作。

Flash Storage(fstorage)的详细说明:

https://infocenter.nordicsemi.com/index.jsp?topic=%2Fsdk_nrf5_v17.1.0%2Flib_fstorage.html

有关FDS提供的API函数说明,请参阅:

https://infocenter.nordicsemi.com/index.jsp?topic=%2Fsdk_nrf5_v17.1.0%2Flib_fds_functionality.html

存储格式显示记录如何存储在闪存中,说明见:

https://infocenter.nordicsemi.com/index.jsp?topic=%2Fsdk_nrf5_v17.1.0%2Flib_fds_format.html

用法展示了代码示例,见:

https://infocenter.nordicsemi.com/index.jsp?topic=%2Fsdk_nrf5_v17.1.0%2Flib_fds_usage.html

官方例程:

https://infocenter.nordicsemi.com/index.jsp?topic=%2Fsdk_nrf5_v17.1.0%2Ffds_example.html

 shows how to use FDS in an application.

2. 开发环境

  • 操作系统: Window10
  • 编译环境: ARM GCC
  • IDE: VSCode
  • SDK版本: SDK_17.1.0
  • 硬件开发板: 项目定制
  • 开发环境构建

《WSL构建nRF5 SDK + ARM GCC开发环境》

https://blog.csdn.net/chentuo2000/article/details/125933307?spm=1001.2014.3001.5502

《WSL构建nRF5 SDK + ARM GCC开发环境 – RTT打印调试日志》

https://blog.csdn.net/chentuo2000/article/details/126104346?spm=1001.2014.3001.5502

3. nRF52832存储和Flash闪存

  • 存储大小

  • Flash大小

Flash - Non-volatile memory

 

  • Flash的地址范围

 

  • Flash布局

 

CPU可以无限次读取Flash,但对它的写入和擦除次数有限制。

写入闪存由非易失性存储器控制器(Non-volatile memory controller, NVMC)管理。

Flash被分成多个页面,CPU可以通过ARM Cortex-M4的ICODE和DCODE总线访问这些页面。

  • Icode和Dcode 总线

ICode的作用是取指令。DCode的作用是对数据读写访问。ICode和DCODE总线是基于AHB-Lite总线协议的32位总线,可以访问的地址范围是0x00000000 - 0x1FFFFFFF。取指和读写数据以字(32位)的长度执行。

  • NVMC-非易失内存控制器(Non-volatile memory controller)

NVMC用于写入和擦除内部闪存和UICR。在执行写入之前,NVMC必须使能CONFIG寄存器中的WEN设置。同样,在执行擦除之前,NVMC必须使能在CONFIG寄存器中的EEN中的设置。用户必须确保写入和擦除不会同时启用,否则可能导致不可预测的行为。

  • 写入闪存

当启用写入后,通过将完整的32位字写入闪存中的字对齐地址来写入闪存。

NVMC只能将“0”写入闪存中已擦除的位(即设置为“1”的位),而不能将一个位写回“1”。

如内存布局所示,闪存被划分为多个页面,这些页面又被划分为多个块。闪存中的同一块只能在擦除(必须使用ERASEPAGE或ERASEALL擦除)前写入nWRITE次。

使用NVMC接口只能将完整的32位字写入闪存。为了向闪存写入少于32位,应将数据写入一个字,并将该字中保持不变的所有位设置为“1”。

将字写入闪存所需的时间由tWRITE指定。当NVMC写入闪存时,CPU停止。

只允许字对齐写入。字节或半字对齐写入将导致硬错误。

  • 擦除闪存中的页

启用擦除后,可以使用ERASEPAGE寄存器逐页擦除闪存。

擦除Flash页面后,页面中的所有位都设置为“1”。擦除页面所需的时间由tERASEPAGE指定。NVMC执行擦除操作时CPU停止。

  • 写用户信息配置寄存器(UICR)

用户信息配置寄存器(UICR)的写方式与Flash相同。UICR写入后,新的UICR配置在重启后生效。

在使用ERASEUICR或ERASEALL执行擦除之前,UICR只能写入nWRITE次。

将一个字写入UICR所需的时间由tWRITE指定。当NVMC写入UICR时,CPU停止。

关于UICR的用法另文详述。

  • 擦除用户信息配置寄存器(UICR)

启用擦除时,可以使用ERASEUICR寄存器擦除UICR。

擦除UICR后,UICR中的所有位都设置为“1”。擦除UICR所需的时间由tERASEPAGE指定。NVMC执行擦除操作时CPU停止。

  • 擦除全部

启用擦除后,可以使用ERASEALL寄存器在一次操作中擦除整个闪存和UICR。ERASEALL不会擦除工厂信息配置寄存器(FICR)。

执行ERASEALL命令所需的时间由tERASEAL指定NVMC执行擦除操作。NVMC执行擦除操作时,CPU停止。

4. FDS的API函数

FDS API提供了操作文件和记录的函数。文件由一个或多个记录组成,其中包含实际数据。

每个记录都由一个key标识,并通过文件ID分配给文件。文件基本上是记录组。记录密钥和文件ID都不是唯一的,并且文件可以包含具有相同key的多个记录。可以通过文件ID和记录key的任意组合访问记录。

例如,应用程序可以使用以下两个文件:

  • 文件1有2条记录:

0x1111="Phone1",

0x2222="data: 12345"

  • 文件2有3条记录:

0x1111="Tablet1",

0x2222="data: abcdef",

0x2222="data: 67890"

现在你可以遍历文件1中的所有记录,或遍历键为0x1111的所有记录,或文件2中键为0x2222的所有记录。

4.1 创建记录

将新记录写入闪存时,必须提供记录key、文件ID和要存储的数据。您也可以保留存储,并使用生成的保留token稍后写入记录或取消保留,而不是立即写入记录。

write函数返回可用于访问记录的记录描述符。在访问它之前,请等待表明写入操作成功完成的事件。

4.2 操纵记录

要读取、更新或删除记录的内容,必须通过其描述符访问记录。该描述符是在您首次将记录写入闪存时创建并返回的。创建记录后,可以使用下面查找记录函数fds_record_find、

fds_record_find_by_key

fds_record.find_in_file

之一检索其描述符。这些函数允许您根据记录key和文件ID搜索记录。

不要求key或ID必须唯一。因此,可能有多个记录与查询匹配。查找记录函数一次返回一个匹配项,并跟踪操作的进度。它们返回编码最新匹配位置的状态token;该token可以在后续调用中使用,以从该位置继续搜索。因此,要遍历所有匹配项,可以使用相同的token重复对find记录函数的调用,直到没有找到更多匹配项。有关如何枚举具有给定密钥和文件ID的所有记录的示例,请参阅检索数据。

4.3 读取记录

您可以直接从闪存中读取记录的内容(存储的数据和元数据)。这意味着应用程序决定数据是复制、存储在RAM中还是就地使用。

要访问记录内容,请打开记录以检索指向闪存中存储记录数据和元数据的位置的指针。fds_record_open函数可确保在访问记录时不会修改或移动到闪存中的其他位置。记住在读取记录后关闭记录以释放锁定。

4.4 更新记录

当您更新记录时,FDS实际上会创建一个新记录并使旧记录无效。此方案确保在操作过程中发生断电时数据不会丢失。

update函数为更新的记录返回一个新的记录描述符。请记住,由于FDS处理更新的方式,频繁更改记录数据、key或文件ID可能会填满闪存,并可能需要释放空间(请参阅垃圾收集)。

4.5 删除记录

删除记录实际上不会删除记录数据并清除已使用的闪存空间,但会使记录无效。删除记录后,无法再打开、读取或定位该记录。

但是,记录使用的闪存空间不会立即释放。要释放无效记录使用的空间,必须运行垃圾收集(请参阅垃圾收集)。

4.6 垃圾收集

FDS不是立即删除记录,而是依靠垃圾收集来回收已失效记录所使用的闪存空间。FDS确保在垃圾收集过程中发生断电时不会丢失数据。垃圾收集不会由FDS自动运行,但必须由应用程序启动。最好在必要时运行垃圾收集,即当闪存中的空间(接近)满时。当空间耗尽时,写请求返回错误FDS_ERR_NO_SPACE_IN_FLASH,您必须运行垃圾收集并等待完成,然后再重复对写函数的调用。函数fds_stat可以返回有用的信息,以确定闪存中是否有可以垃圾收集的脏记录。理想情况下,您应该在BLE活动较低时运行垃圾收集,否则操作可能会超时。当垃圾收集超时并且FDS_EVT_GC事件返回FDS_ERR_TIMEOUT时,系统可以继续正常操作。对fds_gc的重新调用将恢复垃圾收集。

4.7 配置

FDS模块有几个配置选项,您可以在编译时进行配置。

fds_config.h中的以下宏可以更改以适合您对FDS模块的使用:

  • FDS_OP_QUEUE_SIZE:FDS操作的内部队列的大小。如果有许多用户,或者如果您的应用程序将一次对许多操作进行排队,而不等待前面的操作完成,请增加大小。通常,如果经常收到FDS_ERR_NO_SPACE_IN_QUEUES 错误,则应增加队列大小。
  • FDS_VIRTUAL_PAGE_SIZE:虚拟页面的大小。默认情况下,虚拟页面的大小与物理页面的大小相同,但您可以增加虚拟页面大小,以便能够存储大于物理页面的数据(请参阅存储格式的最大长度)。
  • FDS_VIRTUAL_PAGES:要使用的虚拟页面数。使用的闪存总量取决于虚拟页面的大小和数量。
  • FDS_MAX_USERS:可以注册的最大回调数,它定义了可以同时使用FDS的模块数。如果收到FDS_ERR_USER_LIMIT_REACHED错误,请增加该数字,以允许更多用户注册FDS。
  • FDS_CRC_CHECK_ON_READ:如果启用,FDS将对读取操作(FDS_record_open)启用CRC检查。
  • FDS_CRC_CHECK_ON_WRITE:如果启用,FDS将启用写操作的CRC检查。必须启用FDS_CRC_CHECK_ON_READ。

此外,还可以设置以下编译标志:

  • FDS_THREADS:如果设置,则启用代码中的一些关键部分。启用FDS_THREADS会增加代码大小。在启用此标志之前,请确保了解禁用中断的含义,并确保在应用程序中使用了适当的编程模型。

4.8 key和ID限制

记录key应在0x0001-0xBFFF范围内。值0x0000由系统保留。从0xC000到0xFFFF的值保留供对等管理器模块使用,只能在不包含对等管理器的应用程序中使用。

文件ID应在0x0000-0xBFFF范围内。系统使用值0xFFFF。从0xC000到0xFFFE的值保留供对等管理器模块使用,只能在不包含对等管理器的应用程序中使用。

5. FDS存储格式

闪存数据存储将数据存储为记录,并将其分组为文件。在大多数使用情况下,您不需要详细了解FDS如何在闪存中存储数据。以下信息提供了有关FDS使用的数据格式的一些见解,但如果您对详细信息不感兴趣,可以跳过此部分。

  • 记录布局

记录由头(记录元数据)和实际内容组成。它们按写入顺序连续存储在闪存中。当在大记录之后写入小记录时,该规则可能出现例外,小记录可能会放在上一个闪存页面的末尾不适合较大记录处。

 

  • 记录头

记录头由三个字(12字节)组成,其使用方式如下:

 

将记录头写入闪存时,FDS首先写入记录key和数据长度,然后写入记录ID。最后写入文件IDCRC值,并完成成功写操作。在扫描记录时,FDS模块会忽略记录头的第二个字未写入的所有记录。

  • 最大长度

记录的最大长度取决于虚拟闪存页的大小(在fds_config.h中定义,请参阅前面的FDS_VIRTUAL_PAGE_SIZE说明)、页标记的大小(2个字)和记录头的大小(3个字)。默认情况下,虚拟页面大小设置为物理页面大小(1024个字),这样最大数据长度就是1019个字。

要存储更大的数据,请增加虚拟页面大小或用FS(Flash Storage)代替FDS

  • 页面标记

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

页面标记的使用:

 

页面标记在FDS首次初始化时写入,仅在垃圾收集期间更新。

  • File ID、Record key和Record ID的例子

下图是存储2条记录数据时的File ID、Record key和Record ID。

其中,File ID和Record key是我们自己定义的,Record ID是系统产生的。

adv data是BLE扫描收到的广播数据。Event是异步触发的写事件。

 

  • DS使用的闪存大小

在sdk_config.h中有说明,DS使用的闪存大小为:

FDS_VIRTUAL_PAGES * FDS_VIRTUAL_PAGE_SIZE * 4 bytes.

默认值是:3*1024*4 = 12KB

  • 写和擦除所需要的时间

 

6. FDS的使用

以下代码示例显示了Flash数据存储在应用程序中的典型用法。

  • 初始化FDS模块

初始化与FDS中涉及写入或擦除闪存的所有其他操作一样,是一种异步操作。操作的完成通过回调报告给应用程序。

注释:在初始化FDS之前,必须初始化SoftDevice并注册回调处理程序以处理FDS事件。

// 用于处理初始化期间错误的简单事件处理回调程序。
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)
            {
                // 初始化失败。
            }
        break;

        default:
        break;
    }
}

ret_code_t ret = fds_register(fds_evt_handler);
if (ret != NRF_SUCCESS)
{
    // 注册FDS事件处理回调程序失败。
}
ret_code_t ret = fds_init();
if (ret != NRF_SUCCESS)
{
    // 处理错误。
}

初始化操作成功后将返回事件通知。

在对等管理器中,fds_init是初始化函数pm_init的一部分。模块可以多次初始化,没有副作用。

  • 写记录

以下示例代码显示了如何写记录:

#define FILE_ID 0x0001 /* 要写入记录的文件ID。 */
#define RECORD_KEY_1 0x1111 /* 第一条记录的key。 */
#define RECORD_KEY_2 0x2222 /* 第二条记录的key。 */
static uint32_t const m_deadbeef = 0xDEADBEEF;
static char const m_hello[] = "Hello, world!";
fds_record_t record;
fds_record_desc_t record_desc;
// 设置记录。
record.file_id = FILE_ID;
record.key = RECORD_KEY_1;
record.data.p_data = &m_deadbeef;
record.data.length_words = 1; /* 1个字是4个字节 */
ret_code_t rc;
rc = fds_record_write(&record_desc, &record);
if (rc != NRF_SUCCESS)
{
    /* 处理错误。 */
}
// 设置记录。
record.file_id = FILE_ID;
record.key = RECORD_KEY_2;
record.data.p_data = &m_hello;
/* 以下计算考虑了除法的最终余数。 */
record.data.length_words = (sizeof(m_hello) + 3) / 4;
rc = fds_record_write(&record_desc, &record);
if (rc != NRF_SUCCESS)
{
    /* 处理错误。 */
}

命令进入队列顺序执行,通过事件回调指示成功或失败。成功后,fds_record_write函数返回记录的描述符,可用于进一步操作记录。

  • 检索数据

以下示例代码显示了如何使用查找记录功能来检索与特定key和文件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;
/* 首次使用前需要将token归清零。 */
memset(&ftok, 0x00, sizeof(fds_find_token_t));
/* 循环,直到找出具有给定密钥和文件ID的所有记录。 */
while (fds_record_find(FILE_ID, RECORD_KEY, &record_desc, &ftok) == NRF_SUCCESS)
{
    if (fds_record_open(&record_desc, &flash_record) != NRF_SUCCESS)
    {
        /* 处理错误。 */
    }
    /* 通过flash_record结构访问记录。 */
    /* 完成后关闭记录 */
    if (fds_record_close(&record_desc) != NRF_SUCCESS)
    {
        /* 处理错误。 */
    }
}

fds_record_close关闭记录不会使记录描述符或fds_flash_record_t结构无效。记录描述符仍然可以用于操作记录,例如,再次打开或删除记录。然而,fds_flash_record_t结构所指向的数据可能会在记录关闭后的任何时间发生变化。因此,如果在关闭记录后需要访问数据,则必须再次打开它。

  1. 删除记录

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

/* 假设调用fds_record_write()或fds_record_find()有返回描述符,
 如前一示例所示。 */
fds_record_desc_t descriptor;
ret_code_t ret = fds_record_delete(&descriptor);
if (ret != NRF_SUCCESS)
{
    /* 错误。 */
}

操作队列顺序执行,通过事件回调指示成功或失败。

调用fds_record_delete不会释放记录使用的闪存。要回收已删除记录使用的闪存空间,请运行垃圾回收(fds_gc)。

7. FDS示例

下面是一个使用FDS的例子。写两条记录,读出保存的数据并显示,然后删除保存的数据,再进行垃圾回收。同时显示统计数据。

代码主要来自:https://github.com/zk017/NRF52832_FDS

7.1 sdk_config.h设置

  • define FDS_ENABLED 1

FDS进行使能,在使用FDS库函数之前,需要首先将其设置为1

  • define FDS_VIRTUAL_PAGES 3

要使用的虚拟 Flash 页面的数量。系统为垃圾收集预留了一个虚拟页面,因此,最少是两个虚拟页面:一个用于存储数据的页面和一个用于系统垃圾收集的页面。

  • define FDS_VIRTUAL_PAGE_SIZE 1024

虚拟 Flash 页面的大小。FDS 使用的闪存总量为 FDS_VIRTUAL_PAGES * FDS_VIRTUAL_PAGE_SIZE * 4 字节。用 4 字节的倍数表示。默认情况下,虚拟页面的大小与物理页面相同。虚拟页面的大小必须是物理页面大小的倍数。

  • define FDS_BACKEND 2

配置 nrf_fstorage 后台被 FDS 模式用于写入 Flash

参数选择为 NRF_FSTORAGE_NVMC 时,FDS_BACKEND 定义为 1。没有使用蓝牙协议栈工程时,使用这个。

参数选择为 NRF_FSTORAGE_SD 时,FDS_BACKEND 定义为 2。使用蓝牙协议栈工程时,使用这个。

  • define FDS_OP_QUEUE_SIZE 4

内部队列的大小。如果经常得到同步的 FDS_ERR_NO_SPACE_IN_QUEUES 错误,请增加这个值。

  • define FDS_CRC_CHECK_ON_READ 1

FDS_CRC_CHECK_ON_READ:使能 CRC 检查。

  • define FDS_CRC_CHECK_ON_WRITE 0

当记录写入闪存时保存记录的 CRC,并在记录打开时检查它。使用 FDS 函数用户仍然可以看到不正确的 CRC 记录,但是不能打开它们。此外,它们在被删除之前不会被垃圾收集。

FDS_CRC_CHECK_ON_WRITE:对新记录进行 CRC 检查。此设置可用于确保记录数据在写入 Flash 时不会发生更改。

  • define FDS_MAX_USERS 4

可以注册的回调的最大数量。

关于sdk_config.h设置,后面附有我额项目设置。

7.2 声明和定义

/* Array to map FDS events to strings. */
static char const * fds_evt_str[] =
{
    "FDS_EVT_INIT",
    "FDS_EVT_WRITE",
    "FDS_EVT_UPDATE",
    "FDS_EVT_DEL_RECORD",
    "FDS_EVT_DEL_FILE",
    "FDS_EVT_GC",
};

#define FILE_ID     (0x0001)
#define RECORD_KEY  (0x1111)

const char *fds_err_str(ret_code_t ret)
{
    /* Array to map FDS return values to strings. */
    static char const * err_str[] =
    {
        "FDS_ERR_OPERATION_TIMEOUT",
        "FDS_ERR_NOT_INITIALIZED",
        "FDS_ERR_UNALIGNED_ADDR",
        "FDS_ERR_INVALID_ARG",
        "FDS_ERR_NULL_ARG",
        "FDS_ERR_NO_OPEN_RECORDS",
        "FDS_ERR_NO_SPACE_IN_FLASH",
        "FDS_ERR_NO_SPACE_IN_QUEUES",
        "FDS_ERR_RECORD_TOO_LARGE",
        "FDS_ERR_NOT_FOUND",
        "FDS_ERR_NO_PAGES",
        "FDS_ERR_USER_LIMIT_REACHED",
        "FDS_ERR_CRC_CHECK_FAILED",
        "FDS_ERR_BUSY",
        "FDS_ERR_INTERNAL",
    };

    return err_str[ret - NRF_ERROR_FDS_ERR_BASE];
}

static volatile uint8_t write_flag = 0;
static volatile uint8_t delete_all_flag = 0;
static volatile uint8_t delete_flag = 0;
static volatile uint8_t gc_flag = 0;
static volatile bool fds_write_ok = false;
static volatile bool fds_read_ok = false;
static volatile uint16_t i_valid_records = 0;
static volatile bool fds_del_ok = false;

7.3 初始化FDS

初始化FDS之前,必须初始化SoftDevice并注册回调函数来处理FDS事件。

  • 回调函数
static void fds_evt_handler(fds_evt_t const * p_evt)
{
    if (p_evt->result == NRF_SUCCESS) {
        SEGGER_RTT_printf(0, "Event: %s received (NRF_SUCCESS)\n\n",
                      fds_evt_str[p_evt->id]);
    } else {
        SEGGER_RTT_printf(0, "Event: %s received (%s)\n\n",
                      fds_evt_str[p_evt->id],
                      fds_err_str(p_evt->result));
    }

    switch (p_evt->id) {
        case FDS_EVT_INIT:
            if (p_evt->result == NRF_SUCCESS) {
                m_fds_initialized = true;
            }
            break;

        case FDS_EVT_WRITE: {
            if (p_evt->result == NRF_SUCCESS) {
                write_flag = 1;
                SEGGER_RTT_printf(0, "Write Record ID:\t0x%04x\n\n",  p_evt->write.record_id);
                SEGGER_RTT_printf(0, "File ID:\t0x%04x\n",    p_evt->write.file_id);
                SEGGER_RTT_printf(0, "Record key:\t0x%04x\n", p_evt->write.record_key);
            }
        } break;

        case FDS_EVT_DEL_RECORD: {
            if (p_evt->result == NRF_SUCCESS) {
                delete_flag = 1;
                SEGGER_RTT_printf(0, "Delete Record ID:\t0x%04x\n\n",  p_evt->del.record_id);
                SEGGER_RTT_printf(0, "File ID:\t0x%04x\n",    p_evt->del.file_id);
                SEGGER_RTT_printf(0, "Record key:\t0x%04x\n", p_evt->del.record_key);
            }
        } break;

        case FDS_EVT_GC: {
            if (p_evt->result == NRF_SUCCESS) {
                gc_flag = 1;
                SEGGER_RTT_printf(0, "Garbage Collection ok.\n\n");
            }
        } break;

        default:
            break;
    }
}

读操作是同步的,所以不触发回调函数。

我用SEGGER_RTT_printf打印日志,而未用NRF_LOG_GREEN或NRF_LOG_INFO。因为SEGGER_RTT_printf响应更快,功能更强大,用法和printf类似。

要注意的是:在中断或回调函数中使用过多会导致打印异常,或不打印。

  • 初始化函数
static ret_code_t i_fds_init (void)
{
    /* Register first to receive an event when initialization is complete. */
    ret_code_t ret = fds_register(fds_evt_handler);
    if (ret != NRF_SUCCESS) {
        return ret;
    }
    ret = fds_init();
    if (ret != NRF_SUCCESS) {
        return ret;
    }
    /* Wait for fds to initialize. */
    wait_for_fds_ready();

    return NRF_SUCCESS;
}

初始过程:

  • 用fds_register(fds_evt_handler)注册回调函数fds_evt_handler。
  • 用fds_init初始FDS。
  • 等待初始化完成wait_for_fds_ready

在main.c的main()函数的主循环for(;;)之前调用i_fds_init进行初始化:

    // FDS
    err_code = i_fds_init();
    APP_ERROR_CHECK(err_code);

来自回调函数的初始化响应日志:

 

SEGGER_RTT_printf函数还可以控制LOG的前景色和背景色,例如修改回调函数

fds_evt_handler中的语句:

        SEGGER_RTT_printf(0, "%s%sEvent: %s received (NRF_SUCCESS)\n\n", RTT_CTRL_TEXT_BRIGHT_GREEN, RTT_CTRL_BG_BRIGHT_RED, 
                      fds_evt_str[p_evt->id]);
        SEGGER_RTT_printf(0, "%s", RTT_CTRL_RESET);

可以得到如下的效果:

 

所有前景色和背景色的定义见:

/home/ccdc/nrf/nRF5_SDK_17.1.0_ddde560/external/segger_rtt/SEGGER_RTT.h

7.4 写操作

uint32_t i_fds_write(uint16_t file_id, uint16_t record_key, char wrt_data[], uint16_t bytes)
{
    fds_record_t record;
    fds_record_desc_t record_desc;

    SEGGER_RTT_printf(0, "%swrite_data = %s\n", RTT_CTRL_TEXT_BRIGHT_GREEN, wrt_data);
    SEGGER_RTT_printf(0, "%s", RTT_CTRL_RESET);
    // Set up record.
    record.file_id = file_id;
    record.key = record_key;
    record.data.p_data = wrt_data;
    record.data.length_words = (bytes+3)/ sizeof(uint32_t);

    ret_code_t ret = fds_record_write(&record_desc, &record);
    if (ret != NRF_SUCCESS) {
        return ret;
    }
    SEGGER_RTT_printf(0, "Writing Record ID = %d, words=%d\n", record_desc.record_id, record.data.length_words);
    return NRF_SUCCESS;
}
  • 在J-Link RTT Viewer中查看

7.5 读操作

uint32_t i_fds_read(uint16_t file_id, uint16_t record_key)
{
    fds_flash_record_t flash_record;
    fds_record_desc_t record_desc = {0};
    fds_find_token_t ftok = {0}; // Important, make sure you zero init the ftok token
    uint8_t data[13]={0};
    uint32_t err_code;

    SEGGER_RTT_printf(0, "\nStart searching... \n");

    fds_stat_t stat = {0};
    err_code = fds_stat(&stat);
    APP_ERROR_CHECK(err_code);
    i_valid_records = stat.valid_records;
    SEGGER_RTT_printf(0, "%sFound %d valid records.\n\n", RTT_CTRL_TEXT_BRIGHT_YELLOW, i_valid_records);
    SEGGER_RTT_printf(0, "%s", RTT_CTRL_RESET);

    // 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) {
        err_code = fds_record_open(&record_desc, &flash_record);
        if ( err_code != NRF_SUCCESS) {
            SEGGER_RTT_printf(0, "error: fds_record_open returned %s\n", fds_err_str(err_code));
            return err_code; 
        }
        SEGGER_RTT_printf(0, "Record_ID=%d \n", record_desc.record_id);
        uint16_t nsize = 4 * flash_record.p_header->length_words;
        memcpy(data, flash_record.p_data, nsize);    
        SEGGER_RTT_printf(0, "read_data=%s\n\n", data);

        err_code = fds_record_close(&record_desc);
        if (err_code != NRF_SUCCESS) {
            return err_code; 
        }
    }

    return NRF_SUCCESS;
}
  • 在J-Link RTT Viewer中查看

卡在这里不动了?

打开

/home/ccdc/nrf/nRF5_SDK_17.1.0_ddde560/examples/ble_central/ble_app_uart_c/pca10040/s132/config/sdk_config.h

设置一下SEGGER_RTT_CONFIG_BUFFER_SIZE_UP,将默认值512改为2048

#define SEGGER_RTT_CONFIG_BUFFER_SIZE_UP 2048

再编译就可以了:

 

7.6 删除

bool record_delete_next(void)
{
    fds_find_token_t  tok   = {0};
    fds_record_desc_t desc  = {0};

    if (fds_record_iterate(&desc, &tok) == NRF_SUCCESS)
    {
        ret_code_t rc = fds_record_delete(&desc);
        if (rc != NRF_SUCCESS)
        {
            return false;
        }

        return true;
    }
    else
    {
        /* No records left to delete. */
        return false;
    }
}

uint32_t i_fds_delete_all(void)
{
    for (uint8_t i = 0; i < i_valid_records; i++) {
        delete_flag = 0;
        fds_del_ok = record_delete_next();
        if (fds_del_ok == false) {return 1;}
        while (delete_flag == 0);
    }

    delete_all_flag = 1;
    return NRF_SUCCESS;
}
  • 在J-Link RTT Viewer中查看

 

7.7 垃圾回收

uint32_t i_fds_gc(void)
{
    ret_code_t rc = fds_gc();
    switch (rc)
    {
        case NRF_SUCCESS:
            SEGGER_RTT_printf(0, "garbage collection ok.\n");
            break;

        default:
            SEGGER_RTT_printf(0, "error: garbage collection returned %s\n", fds_err_str(rc));
            return rc;
            break;
    }

    return NRF_SUCCESS;
}
  • 在J-Link RTT Viewer中查看

 

7.8 文件系统统计信息查询

~/nrf/nRF5_SDK_17.1.0_ddde560/components/libraries/fds/fds.h

中定义了用于统计信息查询的结构体:

/**@brief   文件系统统计信息。 */
typedef struct
{
    uint16_t pages_available;   //!< 可用页数。
    uint16_t open_records;      //!< 打开的记录数。
    uint16_t valid_records;     //!< 有效记录的数量。
    uint16_t dirty_records;     //!< 已删除(“脏”)记录的数量。
    uint16_t words_reserved;    //!< 由fds_reserve()保留的字数。

    /**@简介 写入闪存(已使用)的字数,包括为将来写入而保留的字数。*/
    uint16_t words_used;

    /**@简介 文件系统中最大的可用连续字数。
     *
     * 此数字表示FDS可以存储的最大记录。
     * 它考虑了未来写入的所有保留。
     */
    uint16_t largest_contig;

    /**@简介 垃圾收集可以回收的最大字数。
     *
     * 如果在垃圾收集运行时打开记录,则垃圾收集释放的实际空间量可能小于此值。
     */
    uint16_t freeable_words;

    /**@简介 检测到文件系统损坏。
     *
     * 检测到一个或多个损坏的记录。下次运行垃圾收集时,FDS将自动修复文件系统,但某些数据可能会丢失。
     *
     * @note: 此标志与CRC故障无关。
     */
    bool corruption;
} fds_stat_t;
  • 示例代码
void i_fds_statistics(void)
{
    fds_stat_t stat = {0};

    err_code = fds_stat(&stat);
    APP_ERROR_CHECK(err_code);
    SEGGER_RTT_printf(0, "%sStatistics info:\n", RTT_CTRL_TEXT_BRIGHT_BLUE, i_valid_records);
    SEGGER_RTT_printf(0, "%s", RTT_CTRL_RESET);
    SEGGER_RTT_printf(0, "Found %d valid records.\n", stat.valid_records);
    SEGGER_RTT_printf(0, "Found %d dirty records (ready to be garbage collected).\n", stat.dirty_records);    
    SEGGER_RTT_printf(0, "Found %d used words.\n", stat.words_used);
    SEGGER_RTT_printf(0, "Found %d largest contig.\n", stat.largest_contig);
    SEGGER_RTT_printf(0, "Found %d freeable words.\n\n", stat.freeable_words);
}

下面是删除所有记录之前和之后,以及垃圾回收之后的统计信息:

 

7.9 Flash的写、读、删除、垃圾回收测试

下面的代码综合演示了Flash的操作。

uint32_t fds_test()
{
    // 测试FDS写
    char *b_buf = "hello world!";
    write_flag = 0;
    err_code = i_fds_write(FILE_ID, RECORD_KEY, b_buf, (uint16_t)strlen(b_buf));
    while (write_flag == 0); // 异步操作,需要等待写完成. 在 fds_evt_handler 函数的 FDS_EVT_WRITE 事件中设置为 1

    char c_buf[10] = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x00};
    write_flag = 0;
    err_code = i_fds_write(FILE_ID, RECORD_KEY, c_buf, sizeof(c_buf));
    while (write_flag == 0);

    // 测试FDS读,读是同步操作不用等待
    err_code = i_fds_read(FILE_ID, RECORD_KEY);
    APP_ERROR_CHECK(err_code);

    // 查看统计信息
    i_fds_statistics();

    // 删除所有记录
    delete_all_flag = 0;
    err_code = i_fds_delete_all();
    APP_ERROR_CHECK(err_code);
    while (delete_all_flag == 0); // 异步操作需要等待本操作完成后再进行下一步操作。
    nrf_delay_ms(200);
    SEGGER_RTT_printf(0, "Deleted all records.\n\n");

    // 查看统计信息
    i_fds_statistics();

    // 垃圾回收
    // 调fds_record_delete不会释放此记录使用的Flash ,要回收删除记录使用的闪存空间,才能释放此记录的Flash,碎片收集运行 fds_gc()。
    gc_flag = 0;
    err_code = i_fds_gc();
    APP_ERROR_CHECK(err_code);
    while (gc_flag == 0);
    nrf_delay_ms(200);

    // 查看统计信息
    i_fds_statistics();
}

在main.c的main()函数中的主循环之前调用fds_test()即可。

8. 可能遇到的编译问题

#ifndef CRC16_ENABLED
#define CRC16_ENABLED 1
#endif

编译出现错误

undefined reference to `crc16_compute'

在/home/ccdc/nrf/nRF5_SDK_17.1.0_ddde560/examples/ble_central/ble_app_uart_c/pca10040/s132/armgcc/Makefile

里添加

  $(SDK_ROOT)/components/libraries/crc16/crc16.c \

附:sdk_config.h中FDS相关的设置及说明

// <h> 页面-虚拟页面设置

// <i> 配置要使用的虚拟页面的数量及其大小。
//==========================================================
// <o> FDS_VIRTUAL_PAGES - 要使用的虚拟闪存页面数。
// <i> 系统保留其中一个虚拟页面用于垃圾收集。
// <i> 因此,最少有两个虚拟页面:一个页面用于存储数据,一个页面供系统用于垃圾收集。
// <i> FDS使用的闪存总量为FDS_VIRTUAL_PAGES * FDS_VIRTUAL_PAGE_SIZE * 4 bytes.

#ifndef FDS_VIRTUAL_PAGES
#define FDS_VIRTUAL_PAGES 3
#endif

// <o> FDS_VIRTUAL_PAGE_SIZE  - 虚拟闪存页面的大小。
 

// <i> 以4字节字的字表示。
// <i> 默认情况下,虚拟页面与物理页面的大小相同。
// <i> 虚拟页面的大小必须是物理页面大小的倍数。如果应用中的一个记录大小超过该大小,这里需要修改增大。
// <1024=> 1024 
// <2048=> 2048 

#ifndef FDS_VIRTUAL_PAGE_SIZE
#define FDS_VIRTUAL_PAGE_SIZE 1024
#endif

// <o> FDS_VIRTUAL_PAGES_RESERVED - 其他模块使用的虚拟闪存页面数。
// <i> FDS模块将其数据存储在闪存的最后几页。
// <i> 通过设置此值,您可以移动FDS使用的闪存结束地址。
// <i> 因此,保留的空间可以被其他模块使用。

#ifndef FDS_VIRTUAL_PAGES_RESERVED
#define FDS_VIRTUAL_PAGES_RESERVED 0
#endif

// </h> 
//==========================================================

// <h> Backend - 后端配置

// <i> 配置FDS用于写入闪存的nrf_fstorage后端。
//==========================================================
// <o> FDS_BACKEND  - FDS 闪存后端。
 

// <i> NRF_FSTORAGE_SD 使用SoftDevice API的nrf_fstorage_sd后端实现。如果您有SoftDevice,请使用此选项。
// <i> NRF_FSTORAGE_NVMC 使用nrf_fstorage_nvmc实现。如果不使用SoftDevice,请使用此设置。
// <1=> NRF_FSTORAGE_NVMC 
// <2=> NRF_FSTORAGE_SD 

#ifndef FDS_BACKEND
#define FDS_BACKEND 2
#endif

// </h> 
//==========================================================

// <h> Queue - 队列设置

//==========================================================
// <o> FDS_OP_QUEUE_SIZE - 内部队列的大小。
// <i> 如果经常出现同步FDS_ERR_NO_SPACE_IN_QUEUES错误,请增加此值。flash操作都是异步的,所以调用fds提供api时,其内部实际都是放入一个操作队列然后一个个执行。

#ifndef FDS_OP_QUEUE_SIZE
#define FDS_OP_QUEUE_SIZE 4
#endif

// </h> 
//==========================================================

// <h> CRC - CRC functionality

//==========================================================
// <e> FDS_CRC_CHECK_ON_READ - 启用CRC检查。

// <i> 将记录写入闪存时保存其CRC,并在打开记录时进行检查。
// <i> CRC不正确的记录仍可被用户使用FDS功能“看到”,但无法打开。
// <i> 此外,它们在被删除之前不会被垃圾收集。
//==========================================================
#ifndef FDS_CRC_CHECK_ON_READ
#define FDS_CRC_CHECK_ON_READ 1
#endif
// <o> FDS_CRC_CHECK_ON_WRITE  - 对新写入的记录执行CRC检查。
 

// <i> 对新写入的记录执行CRC检查。
// <i> 此设置可用于确保记录数据在写入闪存时未被更改。
// <1=> Enabled 
// <0=> Disabled 

#ifndef FDS_CRC_CHECK_ON_WRITE
#define FDS_CRC_CHECK_ON_WRITE 0
#endif

// </e>

// </h> 
//==========================================================

// <h> Users - 用户数量

//==========================================================
// <o> FDS_MAX_USERS - 可注册的最大回调数。可能有不同的模块都需要存储自己的Flash数据,所以每个模块都需要注册自己的flash回调函数。
#ifndef FDS_MAX_USERS
#define FDS_MAX_USERS 4
#endif

参考文档

  1. Flash Data Storage Example
    https://infocenter.nordicsemi.com/topic/sdk_nrf5_v17.0/fds_example.html
  2. Flash Data Storage (FDS)
    https://infocenter.nordicsemi.com/topic/sdk_nrf5_v17.1.0/lib_fds.html
  3. NRF51822 如何使用RTT 实时终端调试(翻译教程)Debugging with Real Time Terminal
    https://www.cnblogs.com/lqy-/p/7802005.html
    https://devzone.nordicsemi.com/nordic/nordic-blog/b/blog/posts/debugging-with-real-time-terminal
  4. [nrf52][SDK17] 弄懂FDS
    https://blog.csdn.net/qq_29246181/article/details/122325393
  5. https://github.com/zk017/NRF52832_FDS
  6. https://github.com/hubuhubu/nRF52-fds-example

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/chentuo2000/article/details/127987450

智能推荐

稀疏编码的数学基础与理论分析-程序员宅基地

文章浏览阅读290次,点赞8次,收藏10次。1.背景介绍稀疏编码是一种用于处理稀疏数据的编码技术,其主要应用于信息传输、存储和处理等领域。稀疏数据是指数据中大部分元素为零或近似于零的数据,例如文本、图像、音频、视频等。稀疏编码的核心思想是将稀疏数据表示为非零元素和它们对应的位置信息,从而减少存储空间和计算复杂度。稀疏编码的研究起源于1990年代,随着大数据时代的到来,稀疏编码技术的应用范围和影响力不断扩大。目前,稀疏编码已经成为计算...

EasyGBS国标流媒体服务器GB28181国标方案安装使用文档-程序员宅基地

文章浏览阅读217次。EasyGBS - GB28181 国标方案安装使用文档下载安装包下载,正式使用需商业授权, 功能一致在线演示在线API架构图EasySIPCMSSIP 中心信令服务, 单节点, 自带一个 Redis Server, 随 EasySIPCMS 自启动, 不需要手动运行EasySIPSMSSIP 流媒体服务, 根..._easygbs-windows-2.6.0-23042316使用文档

【Web】记录巅峰极客2023 BabyURL题目复现——Jackson原生链_原生jackson 反序列化链子-程序员宅基地

文章浏览阅读1.2k次,点赞27次,收藏7次。2023巅峰极客 BabyURL之前AliyunCTF Bypassit I这题考查了这样一条链子:其实就是Jackson的原生反序列化利用今天复现的这题也是大同小异,一起来整一下。_原生jackson 反序列化链子

一文搞懂SpringCloud,详解干货,做好笔记_spring cloud-程序员宅基地

文章浏览阅读734次,点赞9次,收藏7次。微服务架构简单的说就是将单体应用进一步拆分,拆分成更小的服务,每个服务都是一个可以独立运行的项目。这么多小服务,如何管理他们?(服务治理 注册中心[服务注册 发现 剔除])这么多小服务,他们之间如何通讯?这么多小服务,客户端怎么访问他们?(网关)这么多小服务,一旦出现问题了,应该如何自处理?(容错)这么多小服务,一旦出现问题了,应该如何排错?(链路追踪)对于上面的问题,是任何一个微服务设计者都不能绕过去的,因此大部分的微服务产品都针对每一个问题提供了相应的组件来解决它们。_spring cloud

Js实现图片点击切换与轮播-程序员宅基地

文章浏览阅读5.9k次,点赞6次,收藏20次。Js实现图片点击切换与轮播图片点击切换<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title></title> <script type="text/ja..._点击图片进行轮播图切换

tensorflow-gpu版本安装教程(过程详细)_tensorflow gpu版本安装-程序员宅基地

文章浏览阅读10w+次,点赞245次,收藏1.5k次。在开始安装前,如果你的电脑装过tensorflow,请先把他们卸载干净,包括依赖的包(tensorflow-estimator、tensorboard、tensorflow、keras-applications、keras-preprocessing),不然后续安装了tensorflow-gpu可能会出现找不到cuda的问题。cuda、cudnn。..._tensorflow gpu版本安装

随便推点

物联网时代 权限滥用漏洞的攻击及防御-程序员宅基地

文章浏览阅读243次。0x00 简介权限滥用漏洞一般归类于逻辑问题,是指服务端功能开放过多或权限限制不严格,导致攻击者可以通过直接或间接调用的方式达到攻击效果。随着物联网时代的到来,这种漏洞已经屡见不鲜,各种漏洞组合利用也是千奇百怪、五花八门,这里总结漏洞是为了更好地应对和预防,如有不妥之处还请业内人士多多指教。0x01 背景2014年4月,在比特币飞涨的时代某网站曾经..._使用物联网漏洞的使用者

Visual Odometry and Depth Calculation--Epipolar Geometry--Direct Method--PnP_normalized plane coordinates-程序员宅基地

文章浏览阅读786次。A. Epipolar geometry and triangulationThe epipolar geometry mainly adopts the feature point method, such as SIFT, SURF and ORB, etc. to obtain the feature points corresponding to two frames of images. As shown in Figure 1, let the first image be ​ and th_normalized plane coordinates

开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先抽取关系)_语义角色增强的关系抽取-程序员宅基地

文章浏览阅读708次,点赞2次,收藏3次。开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先关系再实体)一.第二代开放信息抽取系统背景​ 第一代开放信息抽取系统(Open Information Extraction, OIE, learning-based, 自学习, 先抽取实体)通常抽取大量冗余信息,为了消除这些冗余信息,诞生了第二代开放信息抽取系统。二.第二代开放信息抽取系统历史第二代开放信息抽取系统着眼于解决第一代系统的三大问题: 大量非信息性提取(即省略关键信息的提取)、_语义角色增强的关系抽取

10个顶尖响应式HTML5网页_html欢迎页面-程序员宅基地

文章浏览阅读1.1w次,点赞6次,收藏51次。快速完成网页设计,10个顶尖响应式HTML5网页模板助你一臂之力为了寻找一个优质的网页模板,网页设计师和开发者往往可能会花上大半天的时间。不过幸运的是,现在的网页设计师和开发人员已经开始共享HTML5,Bootstrap和CSS3中的免费网页模板资源。鉴于网站模板的灵活性和强大的功能,现在广大设计师和开发者对html5网站的实际需求日益增长。为了造福大众,Mockplus的小伙伴整理了2018年最..._html欢迎页面

计算机二级 考试科目,2018全国计算机等级考试调整,一、二级都增加了考试科目...-程序员宅基地

文章浏览阅读282次。原标题:2018全国计算机等级考试调整,一、二级都增加了考试科目全国计算机等级考试将于9月15-17日举行。在备考的最后冲刺阶段,小编为大家整理了今年新公布的全国计算机等级考试调整方案,希望对备考的小伙伴有所帮助,快随小编往下看吧!从2018年3月开始,全国计算机等级考试实施2018版考试大纲,并按新体系开考各个考试级别。具体调整内容如下:一、考试级别及科目1.一级新增“网络安全素质教育”科目(代..._计算机二级增报科目什么意思

conan简单使用_apt install conan-程序员宅基地

文章浏览阅读240次。conan简单使用。_apt install conan