nRF52832闪存FDS使用(SDK17.1.0)

news2024/11/29 6:30:08

陈拓 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/26557.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

容器与容器编排系统

Docker公司发明的「容器镜像」技术&#xff0c;创造性地解决了应用打包的难题。改变了一大批诸如容器编排、服务网格和云原生等技术&#xff0c;深刻影响了云计算领域的技术方向。 一、Docker 容器技术 概括起来&#xff0c;Docker 容器技术有3个核心概念容器、镜像和镜像仓库…

当3A射击游戏遇上Play to Earn,暴躁兔带你了解MetalCore

MetalCore是一款具有机甲风格的战斗射击类的Play to Earn & Free to Play游戏&#xff0c;暴躁兔对这款游戏之前也有做过分析&#xff0c;MetalCore在近期启动了alpha开放世界测试&#xff0c;之前有NFT的玩家获得key code之后可以在PC端下载后进行体验。alpha阶段在10月20…

如何使IOT2050成为PN设备

Profinet Driver&#xff08;PNDriver&#xff09;从V2.3开始支持IO设备(IOD)功能&#xff0c;支持通用网络接口和Linux操作系统&#xff0c;最小支持2ms的通讯周期。本文介绍如何编译PNDriver并运行在IOT2050上。 1. 编译PNDriver 因为PNDriver只支持32位模式&#xff0c;因…

TiDB ——TiKV

TiDB ——TiKV TiKV持久化 TiKV架构和作用TiKV数据持久化和读取TiKV如何提供MVCC和分布式事务支持TiKV基于Raft算法的分布式一致性TiKV的coprocessor TiKV架构和作用 数据持久化分布式一致性MVCC分步式事务Coprocessor RocksDB 单机持久化引擎&#xff0c;单机key-value的…

L2十档行情API接口的开发原理是什么?

L2十档行情API接口的开发原理不知道大家有没有了解过&#xff0c;其实在现实的股市量化交易中&#xff0c;就有不少的投资者也在思考这个问题&#xff0c;并且也有的部分交易者会选择自己开发来使用&#xff0c;不仅支持A股所有的股票数据&#xff0c;也能对期货、外汇、黄金等…

个人项目-部署手册

前言 一、RDS和ECS购买与配置 https://www.aliyun.com/?spm5176.12818093.top-nav.dlogo.3be916d0u0Ncp9 购买RDS(MYSQL)和ECS(规格族&#xff1a;突发性能实例 t6 )的时候尽量选择一个大区》如&#xff1a;华东&#xff08;杭州&#xff09;配置不需要太高(够自己使用就行了…

干货分享 | B站SLO由失败转成功,B站SRE做对了什么?

最近几年&#xff0c;Google SRE在国内非常流行。 Google SRE方法论中提出了SLO是SRE实践的核心&#xff0c;SLO为服务可靠性设定了一个目标级别&#xff0c;它是量化线上质量的关键因素&#xff0c;它是用来回答一个服务到底“什么时候叫做挂了”的根本依据&#xff0c;也是可…

Python网络爬虫入门篇

1. 预备知识 学习者需要预先掌握Python的数字类型、字符串类型、分支、循环、函数、列表类型、字典类型、文件和第三方库使用等概念和编程方法。 2. Python爬虫基本流程 a. 发送请求 使用http库向目标站点发起请求&#xff0c;即发送一个Request&#xff0c;Request包含&am…

xxl-job 执行成功,但是报“任务结果丢失,标记失败“错误

问题1:使用xxl定时更新数据,发现执行结果是失败的 打开日志查看,发现没报错,结果是200 打开备注,上面写着"结果丢失". 再仔细对比下,发现外面日志列表中的执行时间是00:20:18;而日志记录中的最后时间是00:39:32;也就是说线程还没执行完,就先报结果错误了. 对比日志时…

[附源码]Python计算机毕业设计宠物寄养管理系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

Jmeter压力测试教程(上)

JMeter压力测试一、 简介1.1优点1.2缺点二、安装2.1下载2.2解决中文乱码问题2.5配置环境变量2.4启动入门案例三、线程组相关3.1 创建多个线程组3.2 并发和顺序执行3.3 两个特殊的线程组&#xff08;setUp/tearDown&#xff09;线程细节设置默认http请求新增接口信息头管理器四、…

SAP ADM100-1.2之系统登录过程(ABAP)

1、SAP登录过程 为了在前端最终用户和SAP系统实例之间创建连接,sapgui.exe程序需要启动参数。参数字符串是由saplogon .exe程序使用为登录选择的SAP GUI的信息创建。 SAP登录信息有以下两个来源:SAP Logon的配置文件,以及对所选系统的消息服务器的直接请求(下图中的步骤1和…

使用Go+Lua解决Redis秒杀中库存与超卖问题

1、简介 Go语言连接go-redis进行数据库的连接&#xff0c;如果你对这部分尚不了解&#xff0c;建议你先学习这部分知识。另外&#xff0c;本秒杀主要解决两个问题&#xff0c;第一个就是超卖问题&#xff0c;另一个就是库存问题。没有设计专门的页面来模拟并发&#xff0c;我们…

布谷蓝途:易知微「可视大脑助力智慧教育」主题分享精彩实录

如今&#xff0c;大数据技术在教育领域的应用与普及正驶入“快车道”&#xff0c;但仍然存在资源管理分散、数据各自为阵、运营模式传统等痛点&#xff0c;如何借助新技术、新机遇并充分发挥大数据在教育教学中的支撑作用成为重中之重。 布谷蓝途作为国内前沿的大数据方案与服…

网分测试线缆怎么选?

如何在众多选择中寻找到最佳的测试电缆?以下内容由普科科技PRBTEK整理&#xff0c;以下内容将阐述电缆与电缆组件的机械及电气性能&#xff0c;以及如何选择您理想的测试电缆。 2004年5月&#xff0c;美国时代微波系统公司的测试工程师对50欧姆测试电缆的要求作出以下概述&…

银河麒麟桌面操作系统V10安装过程

文章目录下载镜像导入VMware启动安装下载镜像导入VMware 首先去麒麟生态网站注册登录&#xff0c;找到适合自己版本的操作系统 打开VMware新建虚拟机 把镜像放进来 选择Liunx的ubuntu版本 分配处理器和内核 分配内存 后面的就网络、I/O、硬盘按照默认配置就行 启动安装 …

二叉树的建立和遍历

目录创建二叉树中的引用使用遍历顺序创建二叉树使用先序遍历和中序遍历创建二叉树使用中序和后序创建二叉树中序求二叉树用栈实现非递归遍历先序遍历中序遍历后序遍历用栈通过出栈次数进行遍历中序遍历后序遍历队列进行层次遍历思路代码判断是否是满二叉树和完全二叉树递归非递…

面向开发者的开源低代码开发工具,强烈推荐!

每家公司在发展过程中都需要构建大量的内部系统&#xff0c; 比如运营使用的用户管理后台&#xff0c;销售线索后台&#xff0c;双十一活动后台&#xff0c;圣诞节活动后台等。 许多公司内部也都有专门的研发团队负责开发各种各样的后台和内部工具&#xff0c;大量的公司为此付…

Qt开发-QT Quick

前言 QT Quick和Qt widgets这两种技术&#xff0c;官方是强推QT Quick的。 QT Quick中布局一般有如下四种方式&#xff0c; 绝对坐标&#xff1a;x、y、z、width、height、top、left锚(anchors) 布局定位器&#xff08;Row、Column、Grid、Flow&#xff09;布局管理器&#…

(微信开发)Laya转发H5网页到微信,带图片

网页转发到微信时&#xff0c;带图片和自定义标题。2022年11月22号 关键解说 _wx.config({ debug: _wx_configdebug, appId: e.appId, timestamp: e.timestamp, nonceStr: e.nonceStr, signature: e.signature, jsApiList: [ // 所有要调用的 API 都要加到这个列表中 ‘onMen…