FlashDB:为嵌入式系统量身定制的轻量级键值数据库
在嵌入式开发中,数据的持久化存储(如配置参数、状态记录、日志等)一直是一个棘手的问题。开发者通常面临两个极端:要么使用简单的 struct 直接写入 Flash 某个固定地址,导致缺乏灵活性且容易损坏 Flash;要么引入庞大的文件系统(如 LittleFS 或 SPIFFS),但对于简单的键值对存储来说,文件系统的开销过大且 API 复杂。
FlashDB 正是为了填补这一空白而生的。它是一个用 C/C++ 编写的轻量级、高性能、支持掉电保护的键值数据库,旨在让开发者能够像操作 std::map 或 Redis 一样,在 Flash 中存储和检索数据。
一、 为什么选择 FlashDB?
1. 核心痛点解决
- 磨损均衡(Wear Leveling): Flash 的擦写次数有限。FlashDB 通过内部机制分散写入压力,避免频繁擦写同一扇区。
- 掉电保护(Power-fail Safety): 在写入过程中突然掉电是嵌入式设备的常态。FlashDB 采用事务性更新机制,确保数据要么全部写入,要么保持原样,不会出现损坏的中间状态。
- 灵活的存储结构: 支持多组(Group)管理。你可以将“系统配置”放在一组,“用户数据”放在另一组,互不干扰。
- 极低的资源占用: 针对 RAM 和 ROM 进行了极致优化,能够运行在资源受限的 Cortex-M 系列单片机上。
2. 架构设计
FlashDB 采用了分层设计,使其具有极强的可移植性:
* API 层: 提供简单的 set, get, delete 接口。
* 逻辑层: 处理键值映射、组管理和版本控制。
* 适配层(HAL): 开发者只需实现简单的 read, write, erase 接口,即可将 FlashDB 移植到任何 Flash 介质(内部 Flash, SPI Flash, NOR Flash 等)。
二、 快速上手实例
为了让你直观感受 FlashDB 的便捷,下面是一个模拟的集成实例。
1. 基础初始化
首先,你需要定义 Flash 的存储区域并初始化数据库。
#include "flashdb.h"
// 假设我们定义了两个存储组
// Group 0: 系统配置 (System Config)
// Group 1: 用户设置 (User Settings)
fdb_group_t system_group = {
.name = "system",
.address = 0x08010000, // Flash 起始地址
.size = 64 * 1024 // 64KB
};
fdb_group_t user_group = {
.name = "user",
.address = 0x08020000,
.size = 128 * 1024 // 128KB
};
void flashdb_init_example() {
// 1. 初始化 FlashDB 实例
fdb_init();
// 2. 注册存储组
fdb_group_add(&system_group);
fdb_group_add(&user_group);
// 3. 挂载数据库(扫描 Flash 建立索引)
fdb_mount();
}
2. 数据的写入与读取
FlashDB 支持多种数据类型,包括整数、浮点数、字符串以及自定义结构体。
void data_operation_example() {
// --- 写入数据 ---
// 存储一个整数 (设备 ID)
int device_id = 12345;
fdb_set("system", "dev_id", &device_id, sizeof(device_id));
// 存储一个字符串 (设备名称)
const char* device_name = "Smart_Sensor_01";
fdb_set("system", "dev_name", device_name, strlen(device_name) + 1);
// 存储一个自定义结构体 (网络配置)
struct net_config {
uint8_t ip[4];
uint16_t port;
} config = {{192, 168, 1, 10}, 8080};
fdb_set("user", "net_cfg", &config, sizeof(config));
// --- 读取数据 ---
int read_id;
if (fdb_get("system", "dev_id", &read_id, sizeof(read_id)) == FDB_OK) {
printf("Device ID: %d\n", read_id);
}
char read_name[32];
if (fdb_get("system", "dev_name", read_name, sizeof(read_name)) == FDB_OK) {
printf("Device Name: %s\n", read_name);
}
}
3. 数据的删除与清理
当某个配置不再需要,或者需要重置设备时,可以使用删除接口。
void cleanup_example() {
// 删除单个键值对
fdb_del("user", "net_cfg");
// 清空整个组(相当于出厂设置)
fdb_group_clear("user");
}
三、 核心特性深度解析
1. 键值对(KV)存储机制
FlashDB 并不像传统文件系统那样维护复杂的目录树,而是采用一种追加写(Append-only)的日志结构。当你更新一个键 dev_id 时,它不会在原位置覆盖,而是在 Flash 的末尾写入一条新记录。在读取时,FlashDB 会根据索引找到最后一次写入的有效记录。这种方式极大地延长了 Flash 的寿命。
2. 垃圾回收(GC)机制
由于采用追加写,Flash 中会产生大量过时的冗余数据。FlashDB 内置了智能的垃圾回收机制:当存储空间达到一定阈值时,它会自动将当前有效的最新数据迁移到新扇区,并擦除旧扇区,从而释放空间。
3. 强类型与弱类型的平衡
虽然 FlashDB 存储的是二进制 blob(字节流),但它通过简单的 API 封装,让开发者可以轻松地将任何 C 结构体映射到键值对中,无需编写复杂的序列化/反序列化代码(如 JSON 或 Protobuf),这在资源极度受限的 MCU 上至关重要。
四、 移植指南:如何将 FlashDB 接入你的硬件?
要将 FlashDB 运行在你的硬件上,你只需要实现 fdb_hal 接口。通常包括以下三个函数:
fdb_hal_read(uint32_t addr, uint8_t *buf, uint32_t len)- 实现方式:直接调用 Flash 的读取函数(如
memcpy或 SPI 读取)。
- 实现方式:直接调用 Flash 的读取函数(如
fdb_hal_write(uint32_t addr, const uint8_t *buf, uint32_t len)- 实现方式:调用 Flash 的编程函数。注意:写入前必须确保该区域已被擦除。
fdb_hal_erase(uint32_t addr, uint32_t len)- 实现方式:调用 Flash 的扇区擦除函数。
移植建议:
* 对齐要求: 确保你的 write 函数符合 Flash 的最小写入单位(如 4 字节或 8 字节对齐)。
* 扇区大小: 在配置 fdb_group_t 时,确保 size 是 Flash 擦除扇区大小的整数倍。
五、 总结与适用场景
适用场景 \(\checkmark\)
- IoT 设备配置: 存储 WiFi SSID、密码、MQTT 服务器地址。
- 传感器校准参数: 存储设备出厂时的校准偏移量。
- 状态机持久化: 记录设备最后一次运行的状态,以便掉电重启后恢复。
- 轻量级日志: 记录关键的错误代码或事件时间戳。
不适用场景 \(\times\)
- 超大规模数据存储: 如果你需要存储数 MB 的结构化数据,建议使用 LittleFS。
- 频繁的大文件读写: FlashDB 针对的是小块 KV 存储,不适合作为通用文件系统使用。
FlashDB 将复杂的 Flash 管理逻辑抽象为简单的键值操作,极大地降低了嵌入式开发者的心智负担。如果你厌倦了手动计算 Flash 地址和处理擦写循环,FlashDB 是一个绝佳的选择。



还没有评论,来说两句吧...