在这里插入图片描述

每日一句正能量

我们终其一生都在被爱悄悄照亮。
爱不一定轰轰烈烈,更多时候它以隐微的方式存在——朋友的倾听、陌生人的善意、自然的一缕阳光,甚至自己对自己的接纳。我们活在爱的光照中,只是常常忽略了它。
尊重自己的身体。从身体开始,向内审视底线与原则,守住自己的节奏与阵地——生活最终会奖赏那些不轻易动摇的人。

摘要

摘要:在物联网与边缘计算场景中,嵌入式设备需要在资源受限的环境下实现可靠的数据持久化。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, &sector_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访问接口 跨平台适配

未来演进方向

  1. 压缩存储:对时序数据采用Delta编码或Gorilla压缩,提升存储密度
  2. 加密支持:集成AES硬件加密,保护敏感配置数据
  3. 云同步:增量同步机制,将本地TSDB数据上传云端
  4. AI边缘推理:基于TSDB历史数据训练轻量级模型,实现本地预测

FlashDB的本质是将Flash的物理约束转化为软件的设计优势——利用"只能写1为0"的特性设计状态机,利用"擦除寿命有限"的特性设计磨损均衡。这是HAL设计最佳实践中"理解硬件、顺应硬件、超越硬件"原则在数据存储领域的完美体现。


转载自:https://blog.csdn.net/u014727709/article/details/162606097
欢迎 👍点赞✍评论⭐收藏,欢迎指正

更多推荐