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之后,都把相应记录读出来,跟原始内容进行比对,以确保记录真的写成功或者更新成功了

posted on 2018-07-19 20:36  iini  阅读(14150)  评论(2编辑  收藏  举报

导航