ret_code_t fds_register(fds_cb_t cb);
该函数注册 fds的事件处理函数,fds提供了写/更新/删除等api,不过这些api都是异步的,即调用后函数函数会立刻返回,但是实际的flash操作可能不会立刻执行。协议栈内部会在合适的时候去执行实际操作。并最终返回给上层事件,fds模块内部处理后再返回 fds的事件,并调用fds_register函数注册的这个回调函数。
ret_code_t fds_init(void);
初始化FDS模块, FDS模块首先应该调用 fds_register 之后才调用fds_init 来初始化fds模块。因为fds的初始化完成后也会返回 init 完成事件并调用回调函数。
ret_code_t fds_record_write(fds_record_desc_t * const p_desc,
fds_record_t const * const p_record);
写记录,第一次写入一个记录时需要用该函数,p_record 参数中需要给出该记录的file id,record key以及记录的内容指针。FDS内部不会缓存需要写入flash的内容,所以开发者需要自己保证传入的数据指针指向的内容在flash操作完成回调执行之前一直都是有效的。
第一次写入其实也同时就是创建过程。所以fds模块会返回这个创建的记录的现相关信息。
P_desc即为fds返回的该记录的描述符。其中的内容为fds模块内部使用。比如其中的record id字段,因为file id和record key不要求唯一性,所以fds模块内部定义了一个record id来保证每个记录块的唯一性,即使他们fiel id,record key相同fds模块内部也可以同过record id来区分。
ret_code_t fds_reserve(fds_reserve_token_t * const p_token, uint16_t length_words);
ret_code_t fds_reserve_cancel(fds_reserve_token_t * const p_token);
ret_code_t fds_record_write_reserved(fds_record_desc_t * const p_desc,
fds_record_t const * const p_record,
fds_reserve_token_t const * const p_token);
这三个函数放在一起说明,这三个函数的作用类似延迟写的作用。有些应用场景,你明确知道你需要存某个内容,但是这个内容可能需要一定时间后你才能知道其真正内容。但是如果等到那个时间再存,可能flash中已经没有空间来存储你的内容了。所以fds提供了预定写相关的操作。
即可以通过fds_reserve 函数来直接申请预留一部分flash空间将来用。该函数返回的参数p_token即记录了申请的空间。
等将来需要实际写入数据到之前申请预留的flash空间时,调用fds_record_write_reserved 函数并传入 之前获取到的p_token即可。 P_desc和P_record参数同fds_record_write函数
如果后面不想写了,也可以通过fds_reserve_cancel 释放之前申请的预留空间。
ret_code_t fds_record_update(fds_record_desc_t * const p_desc,
fds_record_t const * const p_record);
更新记录内容,FDS的更新并不是真的直接修改原记录的内容,而是直接创建一个新的记录,内容即为新内容,即p_record参数指定的新内容。从FDS的更新角度来看,其实更新一个记录等于就是创建一个记录,只是多了一个无效旧记录的操作。所以p_record参数中的file id,record key可以和旧记录一样也可以不一样。如果和旧记录不一样那怎么找到旧记录去无效它? 其实就算和旧记录一样,FDS内部也不是依靠file id和record key去找这个旧记录的,因为fds不需要保证file id 和record key的唯一性,所以不能以这两个参数去找。 FDS模块内部是通过 record id这个内部使用的参数去找的。这个参数即在 p_desc 参数中。那个怎么知道传入的p_desc参数中的record id应该设置成什么值?即怎么知道旧记录的record id? 其实不需要设置,因为是更新操作,所以前面一定调用过fds_record_write 函数第一次去写这个记录。而fds_record_write 的第一个就是FDS模块内部返回的关于这个记录的内部信息,其中就有record id这个字段。
所以综上,更新一个旧记录的内容,首先需要知道其旧记录的描述符,该描述符就是fds_record_write第一次写时返回的。 之后设置新记录的内容即可。 FDS内部直接创建一个新记录填写新内容。并利用旧记录的描述符找到旧记录然后去无效它。
同时因为update会直接创建一个新的记录填写新内容,所以以前fds_record_write返回的描述符就没用了,因为旧记录已经无效了。所以update函数也同时会返回新记录的描述符存在在p_desc 变量中。
ret_code_t fds_record_delete(fds_record_desc_t * const p_desc)
该函数用来删除某个记录,这里也实际并不是真的删除,fds内部只是设置使这个记录无效。同样因为fds并不要求file id和record key唯一性。所以删除不能依据这两个数据。而是需要用 fds_record_write 或者fds_record_update 函数返回的描述符。这个描述符中有记录的唯一标识record id。Fds内部会依据这个值来查找记录。
还是因为fds的file id和record key的不唯一性。所以当我们需要读某个记录时怎么办? 当然可以直接用write/update操作返回的变量p_desc,这个变量中除了有这个记录的record id这个fds内部使用的记录唯一标识。还有p_record,这个变量记载了这个记录的存储地址。
上面的方法可以用,但是fds提供了读相关的接口。另外fds有自己的存储结构,如果直接用访问地址的方式读,你需要自己解析一下数据头。总之直接用fds提供的读api 更方便。
另外应用场景中的确有多个记录有相同的record key和file id。怎么读取他们?
Fds提供了下面的接口
ret_code_t fds_record_find(uint16_tfile_id,
uint16_t record_key, fds_record_desc_t * const p_desc,
fds_find_token_t * const p_token);
该接口通过file id和record key找到flash中符合file id和record key的第一个记录,并通过p_desc返回这个记录的描述符(包含了记录的record id 和存储地址)
那么如何找到剩下的相同file id和record key的记录? 这需要用到该函数返回的p_token。
这个结构体变量保存的是找到的这个记录所在的页和地址。
那么就明白了。找到了第一个记录,并且知道了这个记录所在的页和地址。那么下一个记录直接从这只有找就可以了。
综上,只需设置p_token初始值为0,然后迭代调用这个函数,即可找到所有file id和record key相同的记录了。
memset(&ftok, 0x00, sizeof(fds_find_token_t));
while (fds_record_find(FILE_ID, REC_KEY, &record_desc, &ftok) == FDS_SUCCESS)
{
// 找到了一个
}
找到了记录,那么就可以通过返回的描述符来读取记录了。
ret_code_t fds_record_open(fds_record_desc_t * const p_desc,
fds_flash_record_t * const p_flash_record);
ret_code_t fds_record_close(fds_record_desc_t * const p_desc);
fds_record_open通过之前获取到的描述符去打开这个记录,并通过p_flash_record这个结构体返回这个记录数据,包括它的file id,record key,长度,实际内容,如果使能了crc校验还会有crc值。当对flash的访问结束后通过fds_record_close来结束访问。
为什访问flash需要有一个open和colse操作?这与下面要说的fds的垃圾回收机制有关。上面提到过fds的删除操作只是无效了这个记录(update对于旧块也有使其无效的操作),其实并没有删除。这样的操作多了会导致flash中很多没有释放的无效记录,那么就没有空间来存储新的记录了。Fds的垃圾回收机制会将一页有这些无效记录的脏页中的有效数据写入到一个全新的交换页,之后这个交换页就作为数据页了。而之前有无效记录的数据页会被直接整页擦除然后作为新的数据页。
所以为了在访问flash内容时,flash的数据不会变动。所以fds提供的读操作会有open和close接口, open函数会打开这个记录,之后访问这个记录内容。访问结束后调用close来关闭访问。实际上Open操作会设置fds内部标记,从而阻止垃圾回收对该记录所在也做处理,保证在访问期间flash数据不会变动。所以访问完后需要调用close释放,从而可以是垃圾回收能正常工作。
综上,如果想访问所有相同file id 和record key的记录块的内容。则可以通过如下方式:
memset(&ftok, 0x00, sizeof(fds_find_token_t));
// 迭代查找所有file id和record key相同的记录
while (fds_record_find(FILE_ID, REC_KEY, &record_desc, &ftok) == FDS_SUCCESS)
{
if (fds_record_open(&record_desc, &flash_record) != FDS_SUCCESS)
{
//打开成功,访问数据,flash_record保存了记录的相关信息和内容。
//访问结束关闭记录
if (fds_record_close(&record_desc) != FDS_SUCCESS)
{
//错误处理
}
}
}
关于查找记录fds还提供了另外两个接口,可以只通过file id或者record key来查找某个记录,或者迭代查找其值相同的所有记录。
ret_code_t fds_record_find_by_key(uint16_t record_key,
fds_record_desc_t *const p_desc,
fds_find_token_t *const p_token);
ret_code_t fds_record_find_in_file(uint16_t file_id,
fds_record_desc_t * const p_desc,
fds_find_token_t * const p_token);
使用方法和上面介绍的fds_record_find 是一样的。
ret_code_t fds_gc(void);
fds 的删除记录/更新记录相对以前的flash操作方式都比较快。因为不论是直接的flash删除记录操作,还是更新操作中的无效旧记录的操作。fds并不会真的去执行删除操作,只是设置一下无效标记即将记录的record key置位无效的0x0000表示该记录无效。从而实现删除的操作。
这样操作避免了flash局部更新时的麻烦,否则你需要去读取整个flash的内容,修改局部后再写回。
而fds的机制则避免了这类麻烦,但带来另一个问题,如果无效记录累计越来越多,最终导致要写一个新记录时返回空间不足。这个时候就需要做一次总的回收,回收所有无效记录占用的flash 的空间。以便新纪录写入。
Fds的回收机制需要一个额外的 交换页,该页是一个全新页。在执行垃圾回收时,fds模块会找到有无效记录的脏页,然后将该页中的有效的记录都写到这个交换页中,这个交换页作为新的数据页。最后整页擦除之前的脏页,并将擦除后的脏页作为新的交换页。并且继续查找有无效数据的脏页做相同处理。直到所有数据页都没有无效数据。
PS:前面提到过fds_record_open函数会阻止垃圾回收的处理,如果一个页中有无效数据,但是其中某个或多个有效数据被打开了并且还未释放。那么垃圾回收会跳过该页不做处理。
垃圾回收机制会将所有没有打开记录的有无效记录的脏页都做一次回收处理,释放其中的无效记录所占的flash空间,所以fds的垃圾回收比较耗时,因此fds不会主动做垃圾回收的处理。因此需要开发这自己在必要的时候才去调用。比如写操作返回 FDS_ERR_NO_SPACE_IN_FLASH 错误时调用ret_code_t fds_gc(void); 进行无效记录的空间释放。并在收到垃圾回收完成的事件之后再进行写操作。
ret_code_t fds_stat(fds_stat_t * const p_stat);
最后介绍一下fds的状态获取函数。该函数可以获取当前fds模块的总状态。该函数返回的p_stat 记录了fds管理的flash存储空间的状态。例如有多少个文件打开了,flash使用了多少,有多少个有效记录,有多少个无效记录,有多少空间可回收等信息。