FLASHDB实战详解 - 嵌入式KV/TSD数据库开发全攻略

FLASHDB实战详解 - 嵌入式KV/TSD数据库开发全攻略

作者说:我当年学嵌入式的时候,数据存储是个头疼的事儿。想存个配置参数,得自己写一堆Flash读写逻辑;想存个操作日志,还得自己管理循环缓冲区... 直到我发现了FlashDB,整个人都轻松了。今天就带大家一起深入了解这个专为嵌入式设计的数据库!

0、前言

相关工程源码


1、FlashDB是什么?

1.1 嵌入式存储的痛点

做嵌入式的都知道,Flash存储是个技术活儿:

复制代码
❌ 痛点1:配置参数存储
   - 参数个数不固定
   - 键名要能随便起
   - 还得支持默认值

❌ 痛点2:日志存储
   - 要支持时间戳
   - 要支持循环覆盖
   - 还要能按时间查询

❌ 痛点3:Flash坏块处理
   - 突然断电数据丢了
   - Flash有坏块不知道
   - 写入粒度不对齐

1.2 FlashDB横空出世

FlashDB 是国产大佬 Armink 开发的嵌入式Flash数据库,绝对是嵌入式存储领域的神器!

项目地址https://github.com/armink/FlashDB

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还自带垃圾回收和断电保护,再也不用担心数据丢失了!


📚 参考资料

  1. FlashDB官方仓库https://github.com/armink/FlashDB
  2. FAL Flash抽象层https://github.com/RT-Thread/FAL
  3. SFUD串行Flash驱动:参考我之前的文章

相关推荐
SUNNYSPY0012 小时前
120R016-ASEMI解锁电力电子的效率革命
单片机
芯希望2 小时前
芯伯乐XOPA340/XOPA2340/XOPA4340系列11MHz低噪声CMOS运放,高性能与低功耗的理想平衡
单片机·嵌入式硬件·dc-dc·工业控制·国产替代·电源管理·xblw芯伯乐
LCMICRO-133108477462 小时前
长芯微LCMDC8588完全P2P替代ADS8588,是一款16位、8通道同步采样的逐次逼近型(SAR)模数转换器
stm32·单片机·嵌入式硬件·fpga开发·硬件工程·模数转换器
VBsemi-专注于MOSFET研发定制3 小时前
面向车载冰箱高效可靠需求的功率器件选型策略与器件适配手册
单片机
进击的小头3 小时前
第17篇:嵌入式通用串行外设:UART_SPI_I2C接口原理与外设扩展应用
单片机·嵌入式硬件
LCG元3 小时前
STM32实战:基于FreeRTOS的LVGL嵌入式GUI移植(智能手表界面)
stm32·嵌入式硬件·智能手表
振浩微433射频芯片4 小时前
低功耗无线遥控新选择:深度解析VI520R ASK/OOK接收芯片与433MHz方案优势
网络·单片机·嵌入式硬件·物联网·智能家居
leo__5204 小时前
STM32 DMA程序(标准外设库版本)
stm32·单片机·嵌入式硬件
三佛科技-134163842124 小时前
电动牙刷方案开发-基于FT61E131B-RB单片机
单片机·嵌入式硬件·智能家居·pcb工艺