FLASHDB实战详解 - 嵌入式KV/TSD数据库开发全攻略
作者说:我当年学嵌入式的时候,数据存储是个头疼的事儿。想存个配置参数,得自己写一堆Flash读写逻辑;想存个操作日志,还得自己管理循环缓冲区... 直到我发现了FlashDB,整个人都轻松了。今天就带大家一起深入了解这个专为嵌入式设计的数据库!
0、前言
相关工程源码:
- 480KW直流充电桩完整工程
- FlashDB库在工程中的路径:
code/Application/User/FlashDB/
1、FlashDB是什么?
1.1 嵌入式存储的痛点
做嵌入式的都知道,Flash存储是个技术活儿:
❌ 痛点1:配置参数存储
- 参数个数不固定
- 键名要能随便起
- 还得支持默认值
❌ 痛点2:日志存储
- 要支持时间戳
- 要支持循环覆盖
- 还要能按时间查询
❌ 痛点3:Flash坏块处理
- 突然断电数据丢了
- Flash有坏块不知道
- 写入粒度不对齐
1.2 FlashDB横空出世
FlashDB 是国产大佬 Armink 开发的嵌入式Flash数据库,绝对是嵌入式存储领域的神器!
1.3 FlashDB核心特性
| 特性 | 说明 |
|---|---|
| 🎯 KV模式 | Key-Value数据库,像操作字典一样存数据 |
| ⏱️ TSD模式 | Time Series时序数据库,支持时间戳和循环覆盖 |
| 🛡️ 掉电安全 | 掉电不丢数据,CRC校验保证数据完整 |
| 📦 坏块管理 | 自动跳过坏块,垃圾回收机制 |
| ⚡ 高效缓存 | KV缓存表 + 扇区缓存,速度嗖嗖的 |
| 🔧 两种模式 | 支持FAL Flash抽象层模式 + 文件模式 |
2、FlashDB架构设计
2.1 整体架构图
┌─────────────────────────────────────────────────────────────────┐
│ FlashDB 架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 应用层 │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ KVDB API │ │ TSDB API │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ │ │
│ └──────────┼──────────────────┼──────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 数据库核心层 │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ KV Manager │ TSL Manager │ GC Manager │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 存储抽象层 │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ FAL Mode │ │ File Mode │ │ │
│ │ │ (Flash分区) │ │ (文件系统) │ │ │
│ │ └────────┬────────┘ └────────┬────────┘ │ │
│ └────────────┼─────────────────────┼─────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Flash │ │ File │ │
│ │ (W25Q256)│ │ System │ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────────┘
2.2 KVDB vs TSDB
┌─────────────────────────────────────────────────────────────────┐
│ KVDB vs TSDB │
├────────────────────────────┬────────────────────────────────────┤
│ KVDB │ TSDB │
├────────────────────────────┼────────────────────────────────────┤
│ Key-Value 数据库 │ Time Series 数据库 │
│ │ │
│ "boot_count" = 100 │ [2024-01-01 10:00:00] 日志1 │
│ "device_name" = "CCU" │ [2024-01-01 10:00:01] 日志2 │
│ "version" = "1.0.0" │ [2024-01-01 10:00:02] 日志3 │
│ │ │
│ ✅ 适合存储配置参数 │ ✅ 适合存储操作日志 │
│ ✅ 键名随意定义 │ ✅ 自动时间戳 │
│ ✅ 支持任意长度的值 │ ✅ 循环覆盖 │
│ ✅ 默认值支持 │ ✅ 按时间范围查询 │
└────────────────────────────┴────────────────────────────────────┘
3、FlashDB源码文件结构
3.1 文件架构
FlashDB/
├── flashdb.h ← 主头文件,导出API
├── fdb_def.h ← 类型定义和错误码
├── fdb_cfg.h ← 👈 用户配置
├── fdb_low_lvl.h ← 底层操作抽象
├── fdb.c ← 公共基础实现
├── fdb_kvdb.c ← KVDB核心实现
├── fdb_tsdb.c ← TSDB核心实现
├── fdb_utils.c ← 工具函数(CRC32等)
└── samples/
├── kvdb_basic_sample.c ← KVDB基础示例
├── kvdb_type_string_sample.c ← 字符串类型示例
├── kvdb_type_blob_sample.c ← BLOB类型示例
└── tsdb_sample.c ← TSDB示例
3.2 核心数据结构
c
/**
* KV节点结构
* 存储在Flash中的KV单位
*/
struct fdb_kv {
fdb_kv_status_t status; // 节点状态
bool crc_is_ok; // CRC校验是否通过
uint8_t name_len; // 键名长度
uint32_t magic; // 魔数 ('K','V','4','0')
uint32_t len; // 节点总长度
uint32_t value_len; // 值长度
char name[FDB_KV_NAME_MAX]; // 键名
struct {
uint32_t start; // 节点起始地址
uint32_t value; // 值起始地址
} addr;
};
/**
* 扇区状态信息
*/
struct kvdb_sec_info {
bool check_ok; // 扇区头校验是否通过
struct {
fdb_sector_store_status_t store; // 存储状态
fdb_sector_dirty_status_t dirty; // 脏状态
} status;
uint32_t addr; // 扇区起始地址
uint32_t magic; // 魔数 ('E','F','4','0')
uint32_t combined; // 联合的下一扇区号
size_t remain; // 剩余空间
uint32_t empty_kv; // 下一个空KV地址
};
经验:FlashDB的数据结构设计得非常精妙!每个扇区都有头部信息,包含状态表、魔数、剩余空间等,这样设计的好处是支持断电恢复和垃圾回收。
4、用户配置详解
4.1 配置文件解析
c
// ==================== fdb_cfg.h ====================
/* 使用KVDB特性 */
#define FDB_USING_KVDB
/* 使用TSD(时序)数据库特性 */
#define FDB_USING_TSDB
/* 使用FAL存储模式 👈 本项目使用这个 */
#define FDB_USING_FAL_MODE
#ifdef FDB_USING_FAL_MODE
/* 写入粒度,单位: bit
* 仅支持 1(Nor Flash) / 8(STM32F2/F4) / 32(STM32F1) */
#define FDB_WRITE_GRAN 1
#endif
/* 开启调试信息 */
// #define FDB_DEBUG_ENABLE
4.2 KVDB配置项
c
/* KV名称最大长度 */
#ifndef FDB_KV_NAME_MAX
#define FDB_KV_NAME_MAX 64
#endif
/* KV缓存表大小,提高KV搜索速度 */
#ifndef FDB_KV_CACHE_TABLE_SIZE
#define FDB_KV_CACHE_TABLE_SIZE 64
#endif
/* 扇区缓存表大小,提高保存速度 */
#ifndef FDB_SECTOR_CACHE_TABLE_SIZE
#define FDB_SECTOR_CACHE_TABLE_SIZE 8
#endif
/* 开启缓存功能 */
#if (FDB_KV_CACHE_TABLE_SIZE > 0) && (FDB_SECTOR_CACHE_TABLE_SIZE > 0)
#define FDB_KV_USING_CACHE
#endif
5、在项目中的应用
5.1 工程中的初始化
在这个480KW充电桩项目中,FlashDB被用于存储充电日志和错误日志:
c
// ==================== errcodehz.c ====================
/* TSDB数据库对象定义 */
static fdb_tsdb_t ChgLogtsdb; // 充电日志数据库
static fdb_tsdb_t ErrLogtsdb; // 错误日志数据库
/**
* 获取充电日志时间戳
*/
static fdb_time_t get_ChgLogTime(void)
{
return (fdb_time_t)Get_RTC_SecondCount(); // 返回RTC秒计数
}
/**
* FlashDB初始化
*/
fdb_err_t FlashDB_Init(void)
{
fdb_err_t result;
/* 初始化充电日志TSD */
result = fdb_tsdb_init(&ChgLogtsdb, // 数据库对象
"ChgLog", // 数据库名称
"fdb_tsdb1", // FAL分区名称
get_ChgLogTime, // 获取时间戳的函数
128, // 每个日志最大长度
NULL); // 用户数据
if (result != FDB_NO_ERR) {
FDB_INFO("ChgLog TSD init failed!\n");
return result;
}
/* 初始化错误日志TSD */
result = fdb_tsdb_init(&ErrLogtsdb,
"ErrLog",
"fdb_tsdb2",
get_ErrLogTime,
128,
NULL);
return result;
}
5.2 存储充电记录
项目中,FlashDB用于存储充电过程数据:
c
// 充电日志结构体
typedef struct {
uint32_t timestamp; // 时间戳
uint16_t voltage; // 电压 (0.1V)
uint16_t current; // 电流 (0.1A)
uint8_t SOC; // 荷电状态
uint8_t status; // 状态
} ChgLogDef;
/**
* 保存充电日志
*/
fdb_err_t Save_ChargeLog(ChgLogDef *log)
{
struct fdb_blob blob;
/* 使用fdb_blob打包数据 */
fdb_tsl_append(ChgLogtsdb,
fdb_blob_make(&blob, log, sizeof(ChgLogDef)));
return FDB_NO_ERR;
}
/**
* 读取所有充电日志
*/
void Load_ChargeLogs(void)
{
/* 反向遍历所有日志(从新到旧) */
fdb_tsl_iter_reverse(ChgLogtsdb,
tsl_read_cb,
&logs);
}
/**
* 日志读取回调
*/
bool tsl_read_cb(fdb_tsl_t tsl, void *arg)
{
ChgLogDef log;
struct fdb_blob blob;
/* 解析日志数据 */
fdb_blob_read((fdb_db_t)ChgLogtsdb,
fdb_tsl_to_blob(tsl,
fdb_blob_make(&blob, &log, sizeof(log))));
/* 处理日志 */
Process_Log(&log);
return true; // 继续遍历
}
5.3 存储配置参数
c
/* KVDB数据库对象 */
static fdb_kvdb_t kvdb;
/* 默认KV定义 */
struct fdb_default_kv default_kvs[] = {
{"device_id", "CCU-001", strlen("CCU-001")},
{"soft_version", "V1.0.0", strlen("V1.0.0")},
{"max_power", "480", strlen("480")},
};
/**
* KVDB初始化
*/
fdb_err_t KVDB_Init(void)
{
struct fdb_default_kv default_kv = {
.kvs = default_kvs,
.num = sizeof(default_kvs) / sizeof(default_kvs[0])
};
return fdb_kvdb_init(&kvdb,
"env", // 数据库名称
"fdb_kvdb1", // FAL分区名称
&default_kv, // 默认KV
NULL);
}
/**
* 保存设备参数
*/
fdb_err_t Save_Device_Param(const char *key, const void *value, size_t len)
{
struct fdb_blob blob;
return fdb_kv_set_blob(kvdb, key,
fdb_blob_make(&blob, value, len));
}
/**
* 读取设备参数
*/
size_t Load_Device_Param(const char *key, void *buf, size_t buf_len)
{
struct fdb_blob blob;
return fdb_kv_get_blob(kvdb, key,
fdb_blob_make(&blob, buf, buf_len));
}
6、KVDB核心API详解
6.1 初始化
c
/**
* KVDB初始化
*
* @param db KVDB对象
* @param name 数据库名称
* @param path FAL分区名称
* @param default_kv 默认KV数组(首次初始化会自动写入)
* @param user_data 用户数据
*/
fdb_err_t fdb_kvdb_init(fdb_kvdb_t db,
const char *name,
const char *path,
struct fdb_default_kv *default_kv,
void *user_data);
6.2 写入KV
c
/**
* 设置字符串值
*/
fdb_err_t fdb_kv_set(fdb_kvdb_t db, const char *key, const char *value);
/**
* 设置二进制值(支持任意类型)
*/
fdb_err_t fdb_kv_set_blob(fdb_kvdb_t db, const char *key, fdb_blob_t blob);
/**
* 使用示例:保存整数
*/
int boot_count = 100;
struct fdb_blob blob;
fdb_kv_set_blob(kvdb, "boot_count",
fdb_blob_make(&blob, &boot_count, sizeof(boot_count)));
6.3 读取KV
c
/**
* 获取字符串值
* @return 字符串指针,NULL表示不存在
*/
char *fdb_kv_get(fdb_kvdb_t db, const char *key);
/**
* 获取二进制值
* @return 实际读取的长度,0表示失败
*/
size_t fdb_kv_get_blob(fdb_kvdb_t db, const char *key, fdb_blob_t blob);
/**
* 使用示例:读取整数
*/
int boot_count = 0;
struct fdb_blob blob;
if (fdb_kv_get_blob(kvdb, "boot_count",
fdb_blob_make(&blob, &boot_count, sizeof(boot_count))) > 0) {
printf("boot_count = %d\n", boot_count);
}
6.4 删除KV
c
/**
* 删除KV(逻辑删除,实际数据仍保留直到GC)
*/
fdb_err_t fdb_kv_del(fdb_kvdb_t db, const char *key);
6.5 遍历所有KV
c
/**
* 遍历回调函数类型
*/
typedef bool (*fdb_kv_iterator_t)(fdb_kvdb_t db, fdb_kv_t kv, void *arg);
/**
* 初始化遍历器
*/
fdb_kv_iterator_t fdb_kv_iterator_init(fdb_kvdb_t db, fdb_kv_iterator_t itr);
/**
* 执行遍历
*/
bool fdb_kv_iterate(fdb_kvdb_t db, fdb_kv_iterator_t itr);
/**
* 使用示例:打印所有KV
*/
bool kv_print_cb(fdb_kvdb_t db, fdb_kv_t kv, void *arg)
{
char value[128] = {0};
struct fdb_blob blob;
size_t len = fdb_kv_get_blob(db, kv->name,
fdb_blob_make(&blob, value, sizeof(value)));
if (len > 0) {
FDB_INFO("%s = %s\n", kv->name, value);
}
return true; // 返回true继续遍历
}
/* 调用 */
fdb_kv_iterate(kvdb, kv_print_cb);
7、TSDB核心API详解
7.1 追加日志
c
/**
* 追加一条时序日志
* @param db TSDB对象
* @param blob 日志数据的blob
*/
fdb_err_t fdb_tsl_append(fdb_tsdb_t db, fdb_blob_t blob);
/**
* 使用示例:保存传感器数据
*/
typedef struct {
float temperature;
float humidity;
uint32_t timestamp;
} SensorData;
SensorData data = {25.5, 60.0, get_timestamp()};
fdb_tsl_append(tsdb, fdb_blob_make(&blob, &data, sizeof(data)));
7.2 遍历日志
c
/**
* 正向遍历(从旧到新)
*/
void fdb_tsl_iter(fdb_tsdb_t db, fdb_tsl_cb cb, void *cb_arg);
/**
* 反向遍历(从新到旧)👈 常用
*/
void fdb_tsl_iter_reverse(fdb_tsdb_t db, fdb_tsl_cb cb, void *cb_arg);
/**
* 按时间范围遍历
*/
void fdb_tsl_iter_by_time(fdb_tsdb_t db,
fdb_time_t from,
fdb_time_t to,
fdb_tsl_cb cb,
void *cb_arg);
/**
* 回调函数类型
*/
typedef bool (*fdb_tsl_cb)(fdb_tsl_t tsl, void *arg);
/**
* 使用示例:读取最近100条日志
*/
#define MAX_LOGS 100
ChgLogDef logs[MAX_LOGS];
int log_count = 0;
bool read_logs_cb(fdb_tsl_t tsl, void *arg)
{
ChgLogDef *pLog = (ChgLogDef *)arg;
struct fdb_blob blob;
fdb_blob_read((fdb_db_t)tsdb,
fdb_tsl_to_blob(tsl,
fdb_blob_make(&blob, pLog, sizeof(ChgLogDef))));
log_count++;
return log_count < MAX_LOGS; // 读取100条后停止
}
/* 反向遍历(最新的在前) */
fdb_tsl_iter_reverse(tsdb, read_logs_cb, logs);
7.3 按时间查询
c
/**
* 按时间范围查询日志数量
*/
size_t fdb_tsl_query_count(fdb_tsdb_t db,
fdb_time_t from,
fdb_time_t to,
fdb_tsl_status_t status);
/**
* 使用示例:查询今天产生的日志数量
*/
fdb_time_t today_start = Get_Today_Start_Time();
fdb_time_t now = get_ChgLogTime();
size_t today_count = fdb_tsl_query_count(tsdb, today_start, now, FDB_TSL_UNUSED);
7.4 清理日志
c
/**
* 清理所有日志(格式化)
*/
void fdb_tsl_clean(fdb_tsdb_t db);
8、Blob机制详解
FlashDB的blob机制是其核心亮点之一,真正做到了"存任意类型数据"!
8.1 blob原理
┌─────────────────────────────────────────────────────────┐
│ fdb_blob 结构 │
├─────────────────────────────────────────────────────────┤
│ const void *buf; // 数据缓冲区指针 │
│ size_t buf_len; // 缓冲区长度 │
│ size_t saved_len; // 👈 实际保存/读取的长度 │
└─────────────────────────────────────────────────────────┘
使用前:buf指向你的变量,buf_len是变量大小,saved_len无用
使用后:buf不变,saved_len是实际操作的字节数
8.2 blob使用示例
c
/* ========== 保存自定义结构体 ========== */
typedef struct {
uint16_t id;
char name[32];
float value;
} MyStruct;
MyStruct data = {
.id = 1,
.name = "sensor_1",
.value = 3.14f
};
struct fdb_blob blob;
/* 一行代码搞定保存 */
fdb_kv_set_blob(kvdb, "my_data",
fdb_blob_make(&blob, &data, sizeof(data)));
/* ========== 读取自定义结构体 ========== */
MyStruct read_data;
struct fdb_blob blob;
size_t len = fdb_kv_get_blob(kvdb, "my_data",
fdb_blob_make(&blob, &read_data, sizeof(read_data)));
if (len == sizeof(MyStruct)) {
printf("ID: %d, Name: %s, Value: %.2f\n",
read_data.id, read_data.name, read_data.value);
}
8.3 字符串存储
c
/* ========== 保存字符串 ========== */
fdb_kv_set(kvdb, "device_name", "480KW_CCU");
/* ========== 读取字符串 ========== */
char *name = fdb_kv_get(kvdb, "device_name");
if (name != NULL) {
printf("Device: %s\n", name);
}
9、存储结构设计
9.1 Flash布局
┌─────────────────────────────────────────────────────────────────┐
│ Flash 存储布局 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 0x000000 ┌─────────────────────────────────────┐ │
│ │ FAL Partition 1 │ │
│ │ (fdb_tsdb1) │ │
│ │ │ │
│ │ ┌───────────────────────────────┐ │ │
│ │ │ Sector 0 │ │ │
│ │ │ ┌─────────────────────┐ │ │ │
│ │ │ │ Sector Header │ │ │ │
│ │ │ │ Magic: 0x46534454 │ │ │ │
│ │ │ │ Status: USING │ │ │ │
│ │ │ │ Combined: 0xFFFFFFFF│ │ │ │
│ │ │ └─────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────────┐ │ │ │
│ │ │ │ TSL Node 1 │ │ │ │
│ │ │ │ Status: WRITE │ │ │ │
│ │ │ │ Time: 0x5A... │ │ │ │
│ │ │ │ Len: 128 │ │ │ │
│ │ │ │ Data: ... │ │ │ │
│ │ │ └─────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────────┐ │ │ │
│ │ │ │ TSL Node 2 │ │ │ │
│ │ │ │ ... │ │ │ │
│ │ │ └─────────────────────┘ │ │ │
│ │ └───────────────────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────────────────┐ │ │
│ │ │ Sector 1 │ │ │
│ │ │ ... │ │ │
│ │ └───────────────────────────────┘ │ │
│ │ │ │
│ 0x0FFFFF └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
9.2 扇区状态机
┌─────────────────┐
│ UNUSED │
│ (空扇区) │
└────────┬────────┘
│ 写入第一条日志
▼
┌─────────────────┐
┌─────│ EMPTY │←─────────┐
│ │ (可用扇区) │ │
│ └────────┬────────┘ │
│ │ 空间不足 │ GC后
│ ▼ │
│ ┌─────────────────┐ │
│ │ USING │──────────┘
│ │ (使用中) │
│ └────────┬────────┘
│ │ 存满
│ ▼
│ ┌─────────────────┐
│ │ FULL │
│ │ (已满) │
│ └────────┬────────┘
│ │ GC清理
└──────────────┘
10、垃圾回收机制
FlashDB的垃圾回收(GC)机制确保Flash空间的高效利用!
10.1 GC触发条件
c
/* 当剩余空间小于阈值时触发GC */
#ifndef FDB_SEC_REMAIN_THRESHOLD
#define FDB_SEC_REMAIN_THRESHOLD (KV_HDR_DATA_SIZE + FDB_KV_NAME_MAX)
#endif
/* 当空扇区数量小于阈值时触发GC */
#ifndef FDB_GC_EMPTY_SEC_THRESHOLD
#define FDB_GC_EMPTY_SEC_THRESHOLD 1
#endif
10.2 GC工作流程
┌─────────────────────────────────────────────────────────────┐
│ 垃圾回收流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 发现空间不足 (remain < threshold) │
│ │
│ 2. 标记最旧的扇区为"脏" │
│ ┌────────────────────────┐ │
│ │ Sector[0] - DIRTY │ ← 标记为待回收 │
│ │ Sector[1] - USING │ │
│ │ Sector[2] - FULL │ │
│ └────────────────────────┘ │
│ │
│ 3. 将有效数据迁移到新扇区 │
│ ┌────────────────────────┐ │
│ │ Sector[3] - NEW │ ← 有效数据写入新扇区 │
│ └────────────────────────┘ │
│ │
│ 4. 擦除脏扇区 │
│ ┌────────────────────────┐ │
│ │ Sector[0] - ERASED │ ← 擦除完成,可再次使用 │
│ └────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
11、断电保护与数据校验
11.1 CRC32校验
FlashDB使用CRC32保证数据完整性:
c
/**
* KV节点头部
*/
struct kv_hdr_data {
uint8_t status_table[KV_STATUS_TABLE_SIZE]; // 状态表
uint32_t magic; // 魔数
uint32_t len; // 节点总长度
uint32_t crc32; ← 👈 CRC校验值 // CRC32(name_len + value_len + name + value)
uint8_t name_len; // 键名长度
uint32_t value_len; // 值长度
};
/* 读取时校验 */
if (calc_crc32 != kv_hdr.crc32) {
// CRC校验失败,数据损坏
kv->crc_is_ok = false;
}
11.2 状态机保护
┌─────────────────────────────────────────────────────────────┐
│ KV节点状态机 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ 写入Header ┌─────────────┐ │
│ │ UNUSED │─────────────────→│ PRE_WRITE │ │
│ └─────────┘ └──────┬──────┘ │
│ │ 写入Data完成 │
│ ▼ │
│ ┌─────────────┐ │
│ │ WRITE │ │
│ └──────┬──────┘ │
│ │ │
│ ┌──────────────────────────┼──────────────────┐ │
│ │ │ │ │
│ ▼ ▼ │ │
│ ┌─────────────┐ ┌─────────────┐ │ │
│ │PRE_DELETE │ │ DELETED │ │ │
│ └──────┬──────┘ └─────────────┘ │ │
│ │ │ │
│ │ 彻底删除(GC) │ │
│ └─────────────────────────────────────────────────→│
│ (回到UNUSED) │
└─────────────────────────────────────────────────────────────┘
11.3 断电恢复流程
c
/**
* 初始化时的恢复检查
*/
static void recovery_check(fdb_kvdb_t db)
{
/* 1. 遍历所有扇区 */
for (每个扇区) {
/* 2. 检查是否有PRE_WRITE状态的节点 */
if (发现节点状态为PRE_WRITE) {
/* 3. 检查数据是否完整 */
if (CRC校验失败) {
/* 数据损坏,标记为错误 */
标记为ERR_HDR;
} else {
/* 数据完整,继续保持PRE_WRITE状态 */
}
}
}
/* 4. 如果发现PRE_WRITE节点,上电后继续完成写入 */
}
12、完整使用示例
12.1 系统初始化
c
/* 数据库对象 */
static fdb_kvdb_t kvdb;
static fdb_tsdb_t tsdb;
/* 默认参数 */
struct fdb_default_kv default_kvs[] = {
{"system.version", "V1.0.0", 7},
{"system.power", "480", 3},
{"system.interval", "100", 3},
};
/**
* FlashDB系统初始化
*/
fdb_err_t FlashDB_Init(void)
{
fdb_err_t result;
struct fdb_default_kv default_kv = {
.kvs = default_kvs,
.num = sizeof(default_kvs) / sizeof(default_kvs[0])
};
/* 初始化KVDB */
result = fdb_kvdb_init(&kvdb, "env", "fdb_kvdb1", &default_kv, NULL);
if (result != FDB_NO_ERR) {
FDB_INFO("KVDB init failed!\n");
return result;
}
/* 初始化TSDB */
result = fdb_tsdb_init(&tsdb, "log", "fdb_tsdb1",
get_timestamp, 128, NULL);
if (result != FDB_NO_ERR) {
FDB_INFO("TSDB init failed!\n");
return result;
}
FDB_INFO("FlashDB init OK!\n");
return FDB_NO_ERR;
}
12.2 参数读写
c
/* 保存设备参数 */
void Save_Parameters(void)
{
int32_t power = 480;
float voltage = 750.0f;
/* 保存整数 */
struct fdb_blob blob1;
fdb_kv_set_blob(kvdb, "rated_power",
fdb_blob_make(&blob1, &power, sizeof(power)));
/* 保存浮点数 */
struct fdb_blob blob2;
fdb_kv_set_blob(kvdb, "rated_voltage",
fdb_blob_make(&blob2, &voltage, sizeof(voltage)));
/* 保存字符串 */
fdb_kv_set(kvdb, "device_name", "480KW_CCU");
}
/* 读取设备参数 */
void Load_Parameters(void)
{
int32_t power;
float voltage;
char name[64];
/* 读取整数 */
struct fdb_blob blob1;
if (fdb_kv_get_blob(kvdb, "rated_power",
fdb_blob_make(&blob1, &power, sizeof(power))) > 0) {
FDB_INFO("Power: %d kW\n", power);
}
/* 读取浮点数 */
struct fdb_blob blob2;
if (fdb_kv_get_blob(kvdb, "rated_voltage",
fdb_blob_make(&blob2, &voltage, sizeof(voltage))) > 0) {
FDB_INFO("Voltage: %.1f V\n", voltage);
}
/* 读取字符串 */
char *name_str = fdb_kv_get(kvdb, "device_name");
if (name_str) {
FDB_INFO("Name: %s\n", name_str);
}
}
12.3 日志存储
c
/* 充电日志结构 */
typedef struct {
uint32_t timestamp;
uint16_t voltage;
uint16_t current;
uint8_t SOC;
uint8_t status;
} ChgLogItem;
/* 保存充电日志 */
void Log_Charge_Data(uint16_t voltage, uint16_t current, uint8_t SOC)
{
ChgLogItem log = {
.timestamp = get_timestamp(),
.voltage = voltage,
.current = current,
.SOC = SOC,
.status = CHARGING
};
struct fdb_blob blob;
fdb_tsl_append(tsdb, fdb_blob_make(&blob, &log, sizeof(log)));
}
/* 读取最近的充电日志 */
void Read_Recent_Logs(int count)
{
ChgLogItem logs[100];
int log_count = 0;
/* 反向遍历 */
fdb_tsl_iter_reverse(tsdb,
bool callback(fdb_tsl_t tsl, void *arg) {
ChgLogItem *pLog = (ChgLogItem *)arg;
struct fdb_blob blob;
fdb_blob_read((fdb_db_t)tsdb,
fdb_tsl_to_blob(tsl,
fdb_blob_make(&blob, pLog, sizeof(ChgLogItem))));
log_count++;
return log_count < count;
}, logs);
}
13、常见问题与调试
13.1 初始化失败
| 可能原因 | 解决方案 |
|---|---|
| FAL分区不存在 | 检查fal_cfg.h中的分区配置 |
| Flash驱动问题 | 先测试SFUD是否能正常读写 |
| 写入粒度配置错误 | STM32F2/F4配置为8,F1配置为32 |
| 分区空间不足 | 增大分区大小 |
13.2 数据丢失
| 可能原因 | 解决方案 |
|---|---|
| 写入时突然断电 | 检查写入粒度是否正确 |
| Flash坏块 | 使用fdb_tsdb_init的not_formatable模式 |
| CRC校验失败 | 检查Flash是否损坏 |
13.3 调试技巧
c
/* 开启调试模式 */
// #define FDB_DEBUG_ENABLE
/* 打印所有KV */
fdb_kv_print(kvdb);
/* 获取TSDB状态 */
fdb_tsdb_control(tsdb, FDB_TSDB_CTRL_GET_STATUS, &status);
/* 获取最后一条日志时间 */
fdb_time_t last_time;
fdb_tsdb_control(tsdb, FDB_TSDB_CTRL_GET_LAST_TIME, &last_time);
14、总结
┌─────────────────────────────────────────────────────────────────┐
│ FlashDB 核心要点 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 🎯 两种数据库模式: │
│ - KVDB: 键值存储,适合配置参数 │
│ - TSDB: 时序存储,适合日志记录 │
│ │
│ 🛡️ 安全特性: │
│ - CRC32数据校验 │
│ - 状态机保护 │
│ - 断电恢复机制 │
│ │
│ ⚡ 高效特性: │
│ - KV缓存表加速查询 │
│ - 扇区缓存加速写入 │
│ - 垃圾自动回收 │
│ │
│ 🔧 移植简单: │
│ - 依赖FAL Flash抽象层 │
│ - 支持SFUD作为底层驱动 │
│ │
└─────────────────────────────────────────────────────────────────┘
碎碎念:做嵌入式存储这么多年,从裸写Flash到用FlashDB,真的是鸟枪换炮的感觉。以前存个参数要写一大坨代码,现在几行就搞定了。关键是FlashDB还自带垃圾回收和断电保护,再也不用担心数据丢失了!
📚 参考资料
- FlashDB官方仓库 :https://github.com/armink/FlashDB
- FAL Flash抽象层 :https://github.com/RT-Thread/FAL
- SFUD串行Flash驱动:参考我之前的文章