FDS(Flash Data Storage 模块是sdk中提供的操作flash的模块。
前面介绍过fs(Flash Storage)模块,这个模块也是sdk中提供的操作flash的模块。为什么要提供两个呢?
这需要了解一下关于flash操作的问题。 因为操作的是片上flash,所以在擦写flash的时候CPU会暂停,这对于ble来说很可能影响底层的链路上的时序。所以对于ble应用来说,不能通过flash相关的寄存器来直接操作flash,因为你不知道你操作的时机对不对会不会对ble协议的底层交互时序造成影响。所以nordic协议栈封装了flash操作,对外提供了flash操作的接口,形如sd_flash_write 所以对于ble应用来说,我们能操作的最底层函数api就是sd开头的flash操作函数,这些sd开头的flash操作函数是异步的,即调用后不会立刻就操作flash,底层协议栈会在合适的时候,不影响或说不至于影响到ble的正常连接的时候才会实际去执行flash操作。操作结束后协议栈再抛出底层事件,告诉你flash操作的结果。
底层函数毕竟需要了解一些nordic协议栈的机制,以及flash操作的方式。所以nordic提供的sdk,在这些sd开头的底层函数上封装了一层来方便用户使用。即fs(Flash Storage)模块,这个模块封装了底层sd开头的API的调用以及协议栈返回的flash操作完成事件的处理。
开发者只需要注册自己的回调函数,调用初始化函数,之后就直接调用可以读/写/擦除操作来操作flash了,而不需要注意和底层flash的事件的交互,flash执行真正完成时会自动调用注册的回调函数。
只是fs封装后提供的操作接口还是保留了flash操作的特性—flash必须是要先擦除后才能写。
这就导致了还是有一定的不便性,想更新一些数据的时候,你得先把flash数据读出来,然后修改,擦除flash最后再写回flash。
关于fs 模块这里不再多介绍,之前的fs几篇文章已经详细描述了fs相关原理和操作。
综上为了解决fs的不便性,sdk又在fs的基础又封装了一层。形成了FDS,FDS的好处就是不仅屏蔽了和底层协议栈的事件交互(以为他依赖fs),同时也封装了更新这样的操作。Fds帮用户处理好了flash的更新,而不需要自己再去擦除flash再重写。 当然fds还提供了其他方便的特性,比如抽象了文件和记录的概念。这对上层的一些数据的组织提供了方便。比如你要存一个学校的学生记录,可以将一个班级的学生file ID设置相同,record id来区分不同的学生。
PS:那为什么还需要fs?直接提供一个fds不就可以了? 因为你不能限定别人的使用方式,FDS的操作方便,那就引入了一定的局限性。如果操作方式,数据格式等。当然封装的层次越多,速度上也会稍慢一点。 Fs虽然有一些不便性,但是相对更灵活。当然最灵活的还是协议栈直接开放的api SD开头的flash操作函数。 即sdk提供了3中大类的flash操作方式,操作约方便操作的方式约受限。所以为了满足不同的可能的需求,所以sdk才将这三种操作flash的方式都提供出来。
我们画一下FDS,FS以及 协议栈提供的flash操作API的关系,方便后面理解。
下面详细介绍FDS相关的一些原理和操作。
FDS将对flash操作抽象成了类似PC上的对文件的操作。抽象出了file ID和record ID这两个概念。 第一次写flash时也同时是这个写数据的创建的时候,所以需要设置file id和record key
Fds组织的数据在flash上以记录的形式组织,实际存储结构如下图:
File ID和 record key 是我们使用FDS时需要使用的。对于开发者来说file id和record用来标识一个记录。在第一次将这个记录写入flash时必须指定。FDS并没有要求file id和record key的唯一性。也就是你可以创建几个记录,他们的file id和record key都指定相同都不会有问题。
record key 不能设置为0x0000,0x0000表示该record是无效的。File ID不能设置为0xFFFF,0xFFFF标识无效的file id。
因为FDS并没有限制file id和record key的唯一性,那么如果使用的场景中的确需要有几个记录有同样的file id和record key,那么当想删除这几个记录怎么办?所以FDS内部维护了record id,该字段就是每个记录再flash中的唯一标识,由FDS模块内部维护。不需要开发者操作。当删除几个file id和record key都相同的记录时,需要开发者自己调用函数 枚举找到这几个记录,虽然每次查找时传入的file id 和record key都一样,但是fds返回的记录描述符中的record id是不同的。再依次使用该返回的描述符就可以正确删除这些同样file id和record key的记录了。
前面介绍了FDS相对于FS来说更方便了开发者的调用,因为它封装了更新flash的函数接口,想更新某个记录的数据可以直接调用fds_record_update 函数即可。FDS的内部实现对于update操作也很快。因为FDS的更新操作并不是真的更新这个旧的数据块,以前的旧SDK比如SDK9,更新一个数据会先将数据块读取出来保存到交换区,再更新数据,最后将交换区数据写回。 FDS的实现,不会做这些操作。FDS更新数据时,直接当成一个写操作,它会找到flash中没用的空间,然后写入这个更新的数据内容。最后直接将旧的数据块的 record key设置成脏数据标记0x0000,标识改记录无效的。所以FDS的更新操作相对以前旧的SDK很快。
同理删除操作的原理也是一样,并不是真正的擦掉flash中的数据而是直接将数据块的record key
需要注意的是工程中并不是可以任意使用file id和record key。除了前面说的record key =0x0000标识该记录无效,file id=0xffff标识无效外如果工程中存在 Peer Manager.模块,那么record key只能使用0x0001 - 0xBFFF.0xC000 to 0xFFFF是PM模块内部使用的,因为PM模块内部也会使用FDS进行flash操作。 File id只能使用0x0000 - 0xBFFF. 0xC000 to 0xFFFE也是PM模块使用了。
FDS模块内部又很多宏都是可配置的,开发者可以更具自己实际情况配置。一般这些宏都是在sdk_config.h 配置文件中。
FDS_OP_QUEUE_SIZE: 设置flash操作队列的大小,flash操作都是异步的,所以调用fds提供api时,其内部实际都是放入一个操作队列然后一个个执行。所以如果应用中可能有一些地方不会等待flash操作返回结果就调用多次fds的flash操作接口,那么可能需要适当增大该宏的大小。否则可能因为队列中缓存了太多flash操作而导致新调用fds的api时返回FDS_ERR_NO_SPACE_IN_QUEUES的错误。
FDS_CHUNK_QUEUE_SIZE:设置允许缓存的记录的块的个数。这里的缓存并不是缓存内容而是缓存的地址,同样如果fds经常返回FDS_ERR_NO_SPACE_IN_QUEUES错误时,也可尝试增大该宏大小
FDS_VIRTUAL_PAGE_SIZE: FDS模块层面的虚拟页的大小。通常就是物理页大小不需要改。如果应用中的一个记录大小超过该大小,这里需要修改增大。
FDS_VIRTUAL_PAGES: 定义了虚拟页的个数,通常也不需要修改。如果存储的总数据量比较大,则需要适当增大。
FDS_MAX_USERS: 回调函数最大可注册数。可能有不同的模块都需要存储自己的Flash数据,所以每个模块都需要注册自己的flash回调函数。