前置条件
这个简单数据库可以以任意存储介质作为实际空间。
对于 flash,通常的最小擦除单位是扇区。
如果是片上 flash,至少需要规划预留一个扇区对齐的区域供 FlashDB 使用。
如果是外部 flash,除了扇区对齐的规划,需要实现并测试读、写、擦除的 API 作为准备工作。
如果是 eeprom,同样的,实现并测试读、写、擦除的 API 作为准备工作,此时会简单很多,不必考虑扇区对齐问题。擦除 API 是为了兼容 FlashDB 默认存储介质是 flash,采用符合 flash 的行为逻辑,实际上可以将擦除 API 做空操作。
FlashDB 会自行处理好扇区对齐问题,所以在实现 API 时不必添加扇区对齐的检查,以减小开销。
引入源码包
在 armink/FlashDB 可以下载到原始源码包。
但是他没有 CMakeLists,需要了解其源码划分,才能正确引入所需的源码。
其实际上必须引入的源码文件夹如下,其他的则可以在工程中删除。下面用更清晰的 ASCII 树展示,方便在 Markdown 中阅读和复制。
FlashDB/
├─ demos/
├─ docs/
├─ inc/ # 需要加入到 include path
├─ port/
│ └─ fal/
│ ├─ docs/
│ ├─ inc/ # 需要加入到 include path
│ ├─ samples/
│ └─ src/ # 需要加入到编译的 .c 文件
├─ samples/
├─ src/ # 需要加入到编译的 .c 文件
├─ tests/
└─ zephyr/
其中标识的 .c 文件需要加入编译,同时加入以下的 includePATH
FlashDB/inc
FlashDB/port/fal/inc
FlashDB/inc 中有 fdb_cfg_template.h,复制一份并重命名为 fdb_cfg.h 放在自己的工程中,比如原地,在 includePATH 范围内就好。
最小起步时其中的默认配置不必修改,只要考虑编写 FDB_PRINT 即可,例如我编写为使用 RTT 打印,或者留空。
#define FDB_PRINT(...) SEGGER_RTT_printf(0, __VA_ARGS__)
// #define FDB_PRINT(...) \
// do \
// { \
// } \
// while(0)
新建一份 fal_cfg.h 放在自己的工程中,例如
为工程引入 fal
注意到上面的 fal_cfg.h 其中有这样的内容,其中 FAL_FLASH_DEV_TABLE 是一个必须定义的 falsh 设备表,这里有一个 nor_flash0 设备描述对象。
这些描述对象与期望操作的 flash 抽象一一对应,比如我这里只有一个 spi-norflash,所以只使用了一个抽象对象。
在现在,虽然使用了声明,但是这个 flash 抽象依然还没有定义,定义这个抽象内容是使其真正具备真实 flash 操作能力的关键。
这里我们可以新建一个源码进行整体绑定,使其具有 读/写/擦 接口。比如如下的核心定义,其中
.len是完整 flash 空间大小,.blk_size是擦除最小单位在 flash 中通常是扇区大小。.ops是 读/写/擦 回调,在回调中使用前置条件中已实现的 API。.write_gran是写最小字节数粒度,例如 norflash 此值定义为 1,而片上 flash 可能要求 4/8 字节对齐。
下面会展示为了支持这个抽象内容的完整源码,在这里有一个重要的问题需要处理。因为 fal 似乎并没有按页逐步写的操作,所以在 norflash 的回调处理中,需要注意不要跨页写,分为多次写使能和写请求。
为工程引入 FlashDB
有了此前由下至上的引入和支持,现在可以正式处理 FlashDB 的操作流程了。
现在需要处理的主要就是按序初始化了,一个最小的可用初始化顺序为
此后就可以使用这样的原子操作以读写键值
需要自己做的包装
- 依据你的工程框架,可以使用线程锁来额外包装一套 API,以简单支持跨线程的键值读写。
- 进阶的,在包装中可以额外增加一个读 API 传参,在未命中键时使用默认值返回。类似 esp32 的 Preferences 数据库,
int canBaud = prefs.getInt("canbtaud", 250000);。 - 更加进阶的,由于 FlashDB 是一套阻塞操作、SPI 收发相当耗时,在线程中直接使用原子操作将导致毫秒级的阻塞,这是不太能接受的。可以采用独立线程和请求队列,专用于管理 FlashDB 的读写操作。在通常的场景,采用异步使得业务逻辑更通顺。而在必须立即读写并获值参与逻辑时,也可以直接调用原子操作。