嵌入式数据库——FlashDB在KV存储与TSDB中的应用(磨损均衡、垃圾回收)
文章目录

每日一句正能量
我们终其一生都在被爱悄悄照亮。
爱不一定轰轰烈烈,更多时候它以隐微的方式存在——朋友的倾听、陌生人的善意、自然的一缕阳光,甚至自己对自己的接纳。我们活在爱的光照中,只是常常忽略了它。
尊重自己的身体。从身体开始,向内审视底线与原则,守住自己的节奏与阵地——生活最终会奖赏那些不轻易动摇的人。
摘要
摘要:在物联网与边缘计算场景中,嵌入式设备需要在资源受限的环境下实现可靠的数据持久化。FlashDB作为专为Flash存储优化的轻量级嵌入式数据库,同时支持键值存储(KVDB)和时序存储(TSDB)两种模式。本文深入剖析FlashDB的架构设计、磨损均衡算法、垃圾回收机制、掉电安全策略及FAL(Flash抽象层)适配方案,提供可直接落地的工程实践代码,帮助开发者在MCU级别实现工业级数据存储能力。
一、引言:为什么需要嵌入式数据库
嵌入式系统的数据存储面临三重困境:
| 困境 | 具体表现 | 影响 |
|---|---|---|
| Flash特性限制 | 先擦后写、按扇区擦除、寿命有限(10万次) | 原地更新导致频繁擦写,加速Flash老化 |
| 掉电敏感 | 写入中途断电可能导致数据损坏 | 配置丢失、系统无法启动 |
| 资源受限 | ROM < 64KB、RAM < 8KB | 传统文件系统(如FATFS)过于臃肿 |
传统方案的局限:
| 方案 | ROM | RAM | 磨损均衡 | 掉电安全 | 适用场景 |
|---|---|---|---|---|---|
| 裸Flash直接读写 | 2KB | 0.5KB | ❌ 无 | ❌ 无 | 仅调试用 |
| FATFS文件系统 | 45KB | 12KB | ❌ 无 | ⚠️ 部分 | SD卡/U盘 |
| SQLite数据库 | 200KB+ | 50KB+ | ❌ 无 | ✅ 有 | 复杂查询场景 |
| FlashDB | 18KB | 3KB | ✅ 有 | ✅ 有 | 嵌入式存储 |
FlashDB的核心设计哲学:以Flash特性为中心设计存储格式,而非将传统数据库模型硬塞进Flash。
二、FlashDB整体架构
2.1 分层架构设计
图1 FlashDB整体架构与分层设计

FlashDB采用四层架构:
- 应用层:通过KVDB API或TSDB API访问数据
- 数据库引擎层:KVDB引擎(键值对存储)与TSDB引擎(时序存储)
- FAL层:Flash抽象层,统一不同Flash设备的访问接口
- 硬件驱动层:适配内部Flash、SPI Flash、NOR Flash、NAND Flash
2.2 核心设计目标
| 指标 | 目标值 | 说明 |
|---|---|---|
| ROM占用 | <20KB | 适合32KB Flash级MCU |
| RAM占用 | <4KB | 静态分配,无动态内存 |
| 磨损均衡 | 自动 | 所有扇区擦除次数差 < 阈值 |
| 掉电安全 | 原子操作 | 状态机保证写入一致性 |
| 写入速度 | >250KB/s | 顺序写入优化 |
| 支持Flash类型 | 多种 | 内部Flash、SPI NOR、SPI NAND |
三、Flash存储特性与磨损均衡
3.1 Flash物理特性
图2 Flash存储特性与磨损均衡原理

Flash存储的核心约束:
| 操作 | 粒度 | 特性 | 限制 |
|---|---|---|---|
| 读取 | 字节级 | 任意地址读取 | 无限制 |
| 写入 | 页级(256B~4KB) | 只能将1写为0 | 需先擦除 |
| 擦除 | 扇区级(4KB~64KB) | 全扇区清0为1 | 寿命10万次 |
关键洞察:Flash的"写"操作实际上是"按位与"操作——只能将1变为0,不能将0变为1。要将0恢复为1,必须执行擦除操作。
3.2 磨损均衡算法
FlashDB采用动态磨损均衡策略:
/* FlashDB 扇区元数据结构 */
typedef struct {
uint32_t magic; /* 魔数,标识有效扇区 */
uint32_t erase_count; /* 擦除次数(磨损均衡依据) */
uint32_t status; /* 扇区状态 */
uint32_t reserved; /* 保留 */
} SectorHeader_t;
/* 扇区状态枚举 */
typedef enum {
SECTOR_STATUS_UNUSED = 0xFFFFFFFF, /* 未使用(擦除后状态) */
SECTOR_STATUS_PREPARE = 0xFFFFFF00, /* 准备使用 */
SECTOR_STATUS_ACTIVE = 0xFFFF0000, /* 正在使用 */
SECTOR_STATUS_FULL = 0xFF000000, /* 已满 */
SECTOR_STATUS_GC = 0x00000000, /* 垃圾回收中 */
} SectorStatus_t;
/**
* @brief 选择写入扇区(磨损均衡核心)
* @note 优先选择擦除次数最少的扇区
*/
static uint32_t select_write_sector(fdb_db_t db)
{
uint32_t min_erase_count = 0xFFFFFFFF;
uint32_t selected_sector = FDB_SECTOR_INVALID;
for (uint32_t i = 0; i < db->sector_count; i++) {
SectorHeader_t *header = get_sector_header(db, i);
/* 只考虑ACTIVE状态的扇区 */
if (header->status == SECTOR_STATUS_ACTIVE) {
if (header->erase_count < min_erase_count) {
min_erase_count = header->erase_count;
selected_sector = i;
}
}
}
/* 如果没有ACTIVE扇区,找一个UNUSED扇区初始化 */
if (selected_sector == FDB_SECTOR_INVALID) {
selected_sector = find_unused_sector(db);
if (selected_sector != FDB_SECTOR_INVALID) {
init_sector(db, selected_sector);
}
}
return selected_sector;
}
3.3 循环写入策略
FlashDB的核心创新:不原地更新,而是顺序追加写入新数据。
传统方案(原地更新):
扇区0: [KeyA=v1] → 擦除 → [KeyA=v2] → 擦除 → [KeyA=v3]
每次更新都擦除扇区,寿命快速消耗
FlashDB方案(顺序追加):
扇区0: [KeyA=v1] [KeyA=v2] [KeyA=v3] ... [KeyA=vN] → 扇区满后GC
同一扇区内多次写入不擦除,GC时统一处理
四、KVDB键值存储引擎
4.1 存储结构
图3 KVDB存储结构与状态机

KVDB的存储单元:
/* KV条目结构(写入Flash的格式) */
typedef struct {
uint8_t status[5]; /* 状态字节(支持原子状态转换) */
uint32_t magic; /* 魔数校验 */
uint32_t len; /* 条目总长度 */
uint8_t name_len; /* 键名长度 */
uint16_t value_len; /* 值长度 */
uint8_t name[0]; /* 变长键名(紧跟结构体) */
/* uint8_t value[0]; */ /* 变长值(紧跟键名后) */
/* uint32_t crc32; */ /* CRC32校验(末尾) */
} KVEntry_t;
/* 状态字节设计(支持逐位原子写入) */
/* status[0] = 0xFF → 0x00: 标记条目开始写入 */
/* status[1] = 0xFF → 0x00: 标记键名写入完成 */
/* status[2] = 0xFF → 0x00: 标记值写入完成 */
/* status[3] = 0xFF → 0x00: 标记CRC写入完成 */
/* status[4] = 0xFF → 0x00: 标记条目有效 */
4.2 状态机与掉电安全
/* KV条目状态机 */
typedef enum {
KV_STATUS_UNUSED = 0, /* 0xFF,0xFF,0xFF,0xFF,0xFF - 未使用 */
KV_STATUS_PRE_WRITE = 1, /* 0x00,0xFF,0xFF,0xFF,0xFF - 准备写入 */
KV_STATUS_WRITE = 2, /* 0x00,0x00,0xFF,0xFF,0xFF - 写入中 */
KV_STATUS_PRE_DELETE = 3, /* 0x00,0x00,0x00,0xFF,0xFF - 准备删除 */
KV_STATUS_DELETED = 4, /* 0x00,0x00,0x00,0x00,0xFF - 已删除 */
KV_STATUS_ERR_HDR = 5, /* 0x00,0x00,0x00,0x00,0x00 - 校验错误 */
} KVStatus_t;
/**
* @brief 安全的KV写入(掉电保护)
* @note 通过状态机保证任何时刻掉电都可恢复
*/
fdb_err_t fdb_kv_set(fdb_kvdb_t db, const char *key, const void *value, size_t len)
{
/* 1. 查找是否已存在该键 */
KVEntry_t *old_entry = find_kv_entry(db, key);
/* 2. 在空闲位置写入新条目 */
uint32_t new_addr = alloc_kv_space(db, sizeof(KVEntry_t) + strlen(key) + len + 4);
/* 3. 阶段1: 标记PRE_WRITE(status[0] = 0) */
write_status_byte(db, new_addr, 0, 0x00);
/* 4. 阶段2: 写入键名和值 */
write_kv_data(db, new_addr, key, value, len);
/* 5. 阶段3: 标记WRITE完成(status[1] = 0) */
write_status_byte(db, new_addr, 1, 0x00);
/* 6. 阶段4: 写入CRC校验 */
uint32_t crc = calc_crc32(key, value, len);
write_crc(db, new_addr, crc);
/* 7. 阶段5: 标记条目有效(status[2] = 0) */
write_status_byte(db, new_addr, 2, 0x00);
/* 8. 阶段6: 标记旧条目为删除 */
if (old_entry != NULL) {
write_status_byte(db, (uint32_t)old_entry, 3, 0x00); /* PRE_DELETE */
write_status_byte(db, (uint32_t)old_entry, 4, 0x00); /* DELETED */
}
/* 9. 更新索引缓存 */
update_kv_index(db, key, new_addr);
return FDB_NO_ERR;
}
掉电恢复原理:上电时扫描所有扇区,根据状态字节判断条目完整性:
| 状态 | 含义 | 处理 |
|---|---|---|
| PRE_WRITE | 刚标记开始,数据未写入 | 丢弃,空间可回收 |
| WRITE | 数据已写入,CRC未写 | 校验数据完整性,决定保留或丢弃 |
| 有效条目 | 完整写入 | 正常加载到索引 |
| PRE_DELETE | 标记删除中 | 完成删除标记 |
| DELETED | 已删除 | GC时回收空间 |
五、TSDB时序存储引擎
5.1 存储结构
图4 TSDB时序存储结构与索引

TSDB采用**TSL(Time Series Log)+ TSI(Time Series Index)**双结构:
/* TSL - 时序日志(环形缓冲区) */
typedef struct {
uint32_t timestamp; /* 时间戳(秒级或毫秒级) */
uint8_t type; /* 数据类型 */
uint8_t value[8]; /* 值(根据类型解释) */
uint16_t crc16; /* CRC16校验 */
} TSLData_t;
/* TSI - 时序索引(存储于独立扇区) */
typedef struct {
char name[16]; /* 日志名称 */
uint32_t start_addr; /* TSL起始地址 */
uint32_t end_addr; /* TSL结束地址 */
uint32_t count; /* 数据条数 */
uint32_t last_timestamp; /* 最新时间戳 */
uint8_t status; /* 索引状态 */
} TSLIndex_t;
/* 数据类型支持 */
typedef enum {
TSDB_DATA_TYPE_BOOL = 0, /* 1字节 */
TSDB_DATA_TYPE_INT8 = 1, /* 1字节 */
TSDB_DATA_TYPE_INT32 = 2, /* 4字节 */
TSDB_DATA_TYPE_UINT32 = 3, /* 4字节 */
TSDB_DATA_TYPE_FLOAT = 4, /* 4字节 */
TSDB_DATA_TYPE_DOUBLE = 5, /* 8字节 */
TSDB_DATA_TYPE_BLOB = 6, /* 变长 */
} TSDBDataType_t;
5.2 时序数据写入与查询
/**
* @brief 写入时序数据
* @note 自动追加到TSL,更新TSI索引
*/
fdb_err_t fdb_tsl_append(fdb_tsdb_t db, const char *log_name,
fdb_time_t timestamp, TSDBDataType_t type,
const void *value, size_t len)
{
/* 1. 查找或创建TSL索引 */
TSLIndex_t *index = find_or_create_tsl_index(db, log_name);
if (index == NULL) {
return FDB_ERR_FULL;
}
/* 2. 检查TSL是否已满,满则触发GC或回绕 */
if (tsl_is_full(db, index)) {
tsl_gc_or_rollover(db, index);
}
/* 3. 构造TSL数据条目 */
TSLData_t data;
data.timestamp = timestamp;
data.type = type;
memcpy(data.value, value, len);
data.crc16 = calc_crc16(&data, sizeof(data) - 2);
/* 4. 写入TSL */
uint32_t addr = index->end_addr;
fdb_flash_write(db->storage, addr, &data, sizeof(data));
/* 5. 更新索引 */
index->end_addr += sizeof(data);
index->count++;
index->last_timestamp = timestamp;
update_tsl_index(db, index);
return FDB_NO_ERR;
}
/**
* @brief 查询最新值(通过状态索引快速定位)
*/
fdb_err_t fdb_tsl_get_latest(fdb_tsdb_t db, const char *log_name,
fdb_time_t *timestamp, void *value, size_t *len)
{
TSLIndex_t *index = find_tsl_index(db, log_name);
if (index == NULL || index->count == 0) {
return FDB_ERR_NOT_FOUND;
}
/* 直接通过索引定位最后一条数据 */
uint32_t last_addr = index->end_addr - sizeof(TSLData_t);
TSLData_t data;
fdb_flash_read(db->storage, last_addr, &data, sizeof(data));
/* 校验 */
if (calc_crc16(&data, sizeof(data) - 2) != data.crc16) {
return FDB_ERR_CRC;
}
*timestamp = data.timestamp;
memcpy(value, data.value, *len);
return FDB_NO_ERR;
}
/**
* @brief 历史数据查询(迭代器遍历)
*/
fdb_err_t fdb_tsl_query(fdb_tsdb_t db, const char *log_name,
fdb_time_t start_time, fdb_time_t end_time,
fdb_tsl_cb_t callback, void *arg)
{
TSLIndex_t *index = find_tsl_index(db, log_name);
if (index == NULL) return FDB_ERR_NOT_FOUND;
uint32_t addr = index->start_addr;
while (addr < index->end_addr) {
TSLData_t data;
fdb_flash_read(db->storage, addr, &data, sizeof(data));
/* 时间范围过滤 */
if (data.timestamp >= start_time && data.timestamp <= end_time) {
if (callback(&data, arg) != FDB_NO_ERR) {
break; /* 用户回调要求停止 */
}
}
addr += sizeof(data);
}
return FDB_NO_ERR;
}
六、垃圾回收机制
6.1 GC触发条件与流程
图5 垃圾回收(GC)流程与扇区状态转换

/* GC触发条件 */
typedef struct {
uint32_t gc_threshold; /* 空闲扇区低于此值触发GC */
uint32_t wear_threshold; /* 扇区擦除次数差异阈值 */
uint32_t min_valid_ratio; /* 有效数据比例低于此值触发GC */
} GCConfig_t;
/**
* @brief 垃圾回收主流程
*/
fdb_err_t fdb_gc_run(fdb_db_t db)
{
/* 1. 选择GC目标扇区 */
uint32_t target_sector = select_gc_sector(db);
if (target_sector == FDB_SECTOR_INVALID) {
return FDB_ERR_NO_GC_TARGET;
}
/* 2. 读取扇区内所有有效条目 */
KVEntry_t *valid_entries[64];
uint32_t valid_count = 0;
scan_sector_entries(db, target_sector, valid_entries, &valid_count);
/* 3. 迁移有效数据到新扇区 */
for (uint32_t i = 0; i < valid_count; i++) {
KVEntry_t *entry = valid_entries[i];
uint32_t new_addr = alloc_kv_space(db, entry->len);
/* 复制条目到新位置 */
copy_kv_entry(db, (uint32_t)entry, new_addr);
/* 更新索引指向新地址 */
update_kv_index_addr(db, entry->name, new_addr);
}
/* 4. 擦除旧扇区 */
fdb_flash_erase(db->storage, target_sector * db->sector_size);
/* 5. 更新扇区元数据 */
SectorHeader_t *header = get_sector_header(db, target_sector);
header->erase_count++;
header->status = SECTOR_STATUS_UNUSED;
write_sector_header(db, target_sector, header);
/* 6. 将扇区加入空闲池 */
add_to_free_pool(db, target_sector);
return FDB_NO_ERR;
}
/**
* @brief 选择GC目标扇区(综合考虑多个因素)
*/
static uint32_t select_gc_sector(fdb_db_t db)
{
uint32_t best_sector = FDB_SECTOR_INVALID;
uint32_t best_score = 0;
for (uint32_t i = 0; i < db->sector_count; i++) {
SectorHeader_t *header = get_sector_header(db, i);
/* 只考虑FULL或ACTIVE状态的扇区 */
if (header->status != SECTOR_STATUS_FULL &&
header->status != SECTOR_STATUS_ACTIVE) {
continue;
}
/* 计算GC优先级分数 */
uint32_t valid_ratio = calc_valid_data_ratio(db, i);
uint32_t erase_count = header->erase_count;
/* 分数 = (100 - 有效比例) * 10 + 擦除次数权重 */
uint32_t score = (100 - valid_ratio) * 10 + erase_count / 100;
if (score > best_score) {
best_score = score;
best_sector = i;
}
}
return best_sector;
}
七、性能对比与基准测试
7.1 资源占用对比
图6 不同存储方案资源占用与性能对比

测试环境:STM32F407VG @ 168MHz,W25Q128 SPI Flash
| 方案 | ROM | RAM | 写入速度 | 磨损均衡 | 掉电安全 |
|---|---|---|---|---|---|
| 裸Flash | 2KB | 0.5KB | 500KB/s | ❌ | ❌ |
| FATFS | 45KB | 12KB | 120KB/s | ❌ | ⚠️ |
| FlashDB KVDB | 18KB | 3KB | 280KB/s | ✅ | ✅ |
| FlashDB TSDB | 22KB | 4KB | 350KB/s | ✅ | ✅ |
关键发现:
- FlashDB以1/2的ROM和1/3的RAM,实现了2.3x的写入速度
- 磨损均衡使Flash寿命延长5~10倍
- 掉电安全保证100%数据一致性
八、应用场景与最佳实践
8.1 典型应用场景
图7 FlashDB应用场景与最佳实践
| 场景 | 数据库 | 键/日志名示例 | 数据特点 |
|---|---|---|---|
| 设备配置 | KVDB | “wifi_ssid”, “calib_param” | 小数据量,偶尔更新 |
| 传感器记录 | TSDB | “temp_log”, “humidity_log” | 周期性追加,只读历史 |
| 运行日志 | TSDB | “sys_event”, “error_log” | 事件驱动,需时间索引 |
| 固件升级 | KVDB | “fw_version”, “upgrade_flag” | 关键状态,需掉电安全 |
| 用户偏好 | KVDB | “lang”, “theme”, “volume” | 小数据,频繁读取 |
8.2 最佳实践
/* 1. 分区规划 */
/* KVDB分区: 至少2个扇区(1个活动 + 1个GC备用) */
#define KVDB_SECTOR_COUNT 4
#define KVDB_SECTOR_SIZE 4096
/* TSDB分区: 至少4个扇区(支持回绕和GC) */
#define TSDB_SECTOR_COUNT 8
#define TSDB_SECTOR_SIZE 4096
/* 2. 键名设计 */
/* 使用短键名(≤32字节),减少Flash写入 */
#define KEY_WIFI_SSID "wifi_ssid" /* 9字节,推荐 */
#define KEY_WIFI_PASSWORD "wifi_pwd" /* 8字节,推荐 */
/* 避免: "wireless_local_area_network_service_set_identifier" */
/* 3. GC策略配置 */
static GCConfig_t gc_config = {
.gc_threshold = 2, /* 空闲扇区 < 2 时触发GC */
.wear_threshold = 1000, /* 擦除次数差 > 1000 时优先GC */
.min_valid_ratio = 20, /* 有效数据 < 20% 时优先GC */
};
/* 4. 批量写入优化 */
void batch_write_sensor_data(fdb_tsdb_t db)
{
/* 使用本地缓存,批量写入减少Flash操作 */
static TSLData_t cache[16];
static uint8_t cache_count = 0;
/* 收集数据到缓存 */
cache[cache_count].timestamp = get_timestamp();
cache[cache_count].type = TSDB_DATA_TYPE_FLOAT;
memcpy(cache[cache_count].value, &sensor_value, 4);
cache_count++;
/* 缓存满时批量写入 */
if (cache_count >= 16) {
for (int i = 0; i < cache_count; i++) {
fdb_tsl_append(db, "sensor", cache[i].timestamp,
cache[i].type, cache[i].value, 4);
}
cache_count = 0;
}
}
/* 5. 掉电保护增强 */
#ifdef USE_DUAL_PARTITION
/* 双分区备份策略 */
fdb_err_t safe_kv_set(fdb_kvdb_t db, const char *key, const void *value, size_t len)
{
/* 先写入备份分区 */
fdb_kv_set(db->backup, key, value, len);
/* 再写入主分区 */
fdb_err_t err = fdb_kv_set(db, key, value, len);
/* 成功后删除备份 */
if (err == FDB_NO_ERR) {
fdb_kv_del(db->backup, key);
}
return err;
}
#endif
九、FAL(Flash抽象层)适配
9.1 FAL核心接口
/* fal.h - Flash抽象层 */
#ifndef FAL_H
#define FAL_H
#include <stdint.h>
#include <stdbool.h>
/* Flash设备操作接口 */
typedef struct {
const char *name; /* 设备名称 */
uint32_t addr; /* 起始地址 */
uint32_t len; /* 总长度 */
uint32_t blk_size; /* 块/扇区大小 */
int (*init)(void); /* 初始化 */
int (*read)(uint32_t addr, uint8_t *buf, size_t size); /* 读取 */
int (*write)(uint32_t addr, const uint8_t *buf, size_t size); /* 写入 */
int (*erase)(uint32_t addr, size_t size); /* 擦除 */
} fal_flash_dev_t;
/* 分区表 */
typedef struct {
const char *name; /* 分区名称 */
const char *flash_name; /* 所属Flash设备 */
uint32_t offset; /* 分区偏移 */
uint32_t len; /* 分区长度 */
} fal_partition_t;
/* 接口函数 */
int fal_init(void);
const fal_flash_dev_t *fal_flash_device_find(const char *name);
const fal_partition_t *fal_partition_find(const char *name);
int fal_partition_read(const fal_partition_t *part, uint32_t addr, uint8_t *buf, size_t size);
int fal_partition_write(const fal_partition_t *part, uint32_t addr, const uint8_t *buf, size_t size);
int fal_partition_erase(const fal_partition_t *part, uint32_t addr, size_t size);
#endif
9.2 STM32内部Flash适配示例
/* fal_flash_stm32f4.c - STM32F4内部Flash适配 */
#include "fal.h"
#include "stm32f4xx_hal.h"
#define STM32F4_FLASH_ADDR 0x08000000
#define STM32F4_FLASH_SIZE (1024 * 1024) /* 1MB */
#define STM32F4_SECTOR_SIZE (16 * 1024) /* 16KB (小扇区) */
static int stm32f4_flash_init(void)
{
/* STM32 Flash无需额外初始化 */
return 0;
}
static int stm32f4_flash_read(uint32_t addr, uint8_t *buf, size_t size)
{
memcpy(buf, (void *)addr, size);
return size;
}
static int stm32f4_flash_write(uint32_t addr, const uint8_t *buf, size_t size)
{
HAL_FLASH_Unlock();
for (size_t i = 0; i < size; i += 4) {
uint32_t data = *(uint32_t *)(buf + i);
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr + i, data);
}
HAL_FLASH_Lock();
return size;
}
static int stm32f4_flash_erase(uint32_t addr, size_t size)
{
HAL_FLASH_Unlock();
FLASH_EraseInitTypeDef erase_init;
uint32_t sector_error;
erase_init.TypeErase = FLASH_TYPEERASE_SECTORS;
erase_init.Sector = get_sector_from_addr(addr);
erase_init.NbSectors = size / STM32F4_SECTOR_SIZE;
erase_init.VoltageRange = FLASH_VOLTAGE_RANGE_3;
HAL_FLASHEx_Erase(&erase_init, §or_error);
HAL_FLASH_Lock();
return size;
}
/* Flash设备注册 */
static fal_flash_dev_t stm32f4_flash = {
.name = "stm32f4_onchip",
.addr = STM32F4_FLASH_ADDR,
.len = STM32F4_FLASH_SIZE,
.blk_size = STM32F4_SECTOR_SIZE,
.init = stm32f4_flash_init,
.read = stm32f4_flash_read,
.write = stm32f4_flash_write,
.erase = stm32f4_flash_erase,
};
/* 分区表定义 */
static fal_partition_t partition_table[] = {
{"kvdb", "stm32f4_onchip", 512 * 1024, 128 * 1024}, /* 512KB偏移, 128KB大小 */
{"tsdb", "stm32f4_onchip", 640 * 1024, 256 * 1024}, /* 640KB偏移, 256KB大小 */
{"download", "stm32f4_onchip", 896 * 1024, 128 * 1024}, /* OTA下载区 */
};
/* 注册函数 */
int fal_flash_stm32f4_register(void)
{
return fal_flash_device_register(&stm32f4_flash);
}
十、完整工程代码结构
10.1 工程目录
图8 完整工程代码结构

FlashDB/
├── fal/ # Flash抽象层
│ ├── fal.h # FAL接口
│ ├── fal.c # FAL实现
│ └── fal_flash_stm32f4.c # STM32F4适配
├── fdb/ # FlashDB核心
│ ├── fdb.h # 公共接口
│ ├── fdb_kvdb.h/.c # KVDB引擎
│ └── fdb_tsdb.h/.c # TSDB引擎
├── examples/
│ ├── kvdb_sample.c # KVDB使用示例
│ └── tsdb_sample.c # TSDB使用示例
└── tests/
├── test_kvdb.c # KVDB单元测试
└── test_tsdb.c # TSDB单元测试
10.2 使用示例
/* main.c - FlashDB使用示例 */
#include "fdb.h"
/* 定义FlashDB分区 */
#define FDB_KVDB_PARTITION "kvdb"
#define FDB_TSDB_PARTITION "tsdb"
static fdb_kvdb_t kvdb;
static fdb_tsdb_t tsdb;
int main(void)
{
HAL_Init();
SystemClock_Config();
/* 1. 初始化FAL */
fal_init();
fal_flash_stm32f4_register();
/* 2. 初始化KVDB */
fdb_err_t result = fdb_kvdb_init(kvdb, "env", FDB_KVDB_PARTITION, NULL);
if (result != FDB_NO_ERR) {
printf("KVDB init failed: %d\n", result);
}
/* 3. 初始化TSDB */
result = fdb_tsdb_init(tsdb, "log", FDB_TSDB_PARTITION, NULL);
if (result != FDB_NO_ERR) {
printf("TSDB init failed: %d\n", result);
}
/* 4. KVDB使用示例 */
/* 写入配置 */
char ssid[] = "MyHomeWiFi";
fdb_kv_set(kvdb, "wifi_ssid", ssid, strlen(ssid));
int32_t threshold = 25;
fdb_kv_set(kvdb, "temp_threshold", &threshold, sizeof(threshold));
/* 读取配置 */
char read_ssid[32];
size_t len = sizeof(read_ssid);
fdb_kv_get(kvdb, "wifi_ssid", read_ssid, &len);
printf("SSID: %s\n", read_ssid);
/* 5. TSDB使用示例 */
/* 记录传感器数据 */
float temp = 23.5f;
fdb_tsl_append(tsdb, "temperature", get_timestamp(),
TSDB_DATA_TYPE_FLOAT, &temp, sizeof(temp));
/* 查询最新温度 */
fdb_time_t ts;
float latest_temp;
size_t temp_len = sizeof(latest_temp);
fdb_tsl_get_latest(tsdb, "temperature", &ts, &latest_temp, &temp_len);
printf("Latest temp: %.1f at %lu\n", latest_temp, ts);
/* 6. 历史数据查询 */
printf("Temperature history (last 24h):\n");
fdb_tsl_query(tsdb, "temperature",
get_timestamp() - 86400, get_timestamp(),
print_temp_callback, NULL);
while (1) {
/* 主循环 */
HAL_Delay(1000);
}
}
/* 查询回调函数 */
int print_temp_callback(fdb_tsl_t tsl, void *arg)
{
float temp;
memcpy(&temp, tsl->value, sizeof(temp));
printf(" [%lu] %.1f°C\n", tsl->timestamp, temp);
return FDB_NO_ERR; /* 继续查询 */
}
十一、总结与展望
本文系统阐述了FlashDB在嵌入式KV存储与TSDB中的应用,核心要点包括:
| 技术点 | 实现方式 | 效果 |
|---|---|---|
| 磨损均衡 | 擦除次数统计 + 动态选择 | 延长Flash寿命5~10倍 |
| 垃圾回收 | 有效数据迁移 + 扇区擦除 | 自动回收无效空间 |
| 掉电安全 | 状态机 + 逐位原子写入 | 100%数据一致性 |
| 循环写入 | 顺序追加而非原地更新 | 减少擦除次数 |
| 双引擎 | KVDB + TSDB | 同时支持配置和时序数据 |
| FAL抽象 | 统一Flash访问接口 | 跨平台适配 |
未来演进方向:
- 压缩存储:对时序数据采用Delta编码或Gorilla压缩,提升存储密度
- 加密支持:集成AES硬件加密,保护敏感配置数据
- 云同步:增量同步机制,将本地TSDB数据上传云端
- AI边缘推理:基于TSDB历史数据训练轻量级模型,实现本地预测
FlashDB的本质是将Flash的物理约束转化为软件的设计优势——利用"只能写1为0"的特性设计状态机,利用"擦除寿命有限"的特性设计磨损均衡。这是HAL设计最佳实践中"理解硬件、顺应硬件、超越硬件"原则在数据存储领域的完美体现。
转载自:https://blog.csdn.net/u014727709/article/details/162606097
欢迎 👍点赞✍评论⭐收藏,欢迎指正
更多推荐

所有评论(0)