简介:本资源是一套面向嵌入式开发者的完整 FlashDB 轻量级键值数据库移植与实战例程,适用于 STM32 平台(特别是中移物联万耦开发板)的 W25Q64 Flash 存储扩展场景,解决嵌入式设备中 Flash 手动管理复杂、擦写寿命短、数据持久化困难等核心痛点。资源包共 2009 个文件,以 850 个 C 源码和 576 个头文件构成主体框架,辅以 Makefile、链接脚本(.lds)、调试配置(.dbgconf)、RT-Thread 工程文件(.rttlaunch)及说明文档(.md/.txt),完整覆盖 SFUD 驱动适配、FAL 分区抽象层集成、FlashDB 数据库封装与实际读写测试逻辑,压缩包大小为 50.6MB。已有 1226 人学习下载,内容预览显示已编译生成多个目标文件(如 wanou.axf、FlashDB.axf)及多架构数学库(Cortex-M4l/M4lf 等),表明工程具备跨工具链(Keil/IAR/RT-Thread Studio)构建能力,可直接用于二次开发或教学演示。
1. 中移物联万耦开发板上跑通 FlashDB + SFUD + FAL + W25Q64:不是“加个库就能用”,而是四层嵌套对齐的硬核落地
你手头有一块中移物联万耦开发板,芯片是 Cortex-M4 内核的 MCU,板载 W25Q64(8MB SPI Flash),想在资源受限的嵌入式设备上存点传感器历史数据、配置参数或日志片段——别急着抄flashdb_init()就编译烧录。这套组合(FlashDB + SFUD + FAL + W25Q64)表面看是“嵌入式键值数据库”方案,实则是四层抽象栈的严丝合缝咬合:最底层是物理 Flash 芯片时序(W25Q64),往上是统一驱动层(SFUD),再往上是扇区抽象层(FAL),顶层才是键值操作接口(FlashDB)。任何一层地址偏移错 1 字节、擦除粒度配错、写保护位没清、或 FlashDB 的fdb_db_t初始化时传入的FAL_PART_NAME和实际分区名不一致,都会导致fdb_kv_set()静默失败、fdb_kv_get()返回空、甚至整个 Flash 变成黑匣子。这不是理论问题,是某高校嵌入式课程组在调试温湿度记录仪 Demo 时连续三天无法写入数据的真实翻车现场。它适合需要本地持久化但又不愿引入 SQLite 或 FatFS 的中低功耗终端开发者,尤其当你必须把配置项、校准系数、断电前最后 10 条告警按时间戳存下来,且 MCU RAM 不足 64KB 时——这才是 FlashDB 在万耦板上的真实战场。
2. 四层栈结构拆解:为什么必须从 W25Q64 时序开始,而不是直接#include <flashdb.h>
2.1 W25Q64 物理层:SPI 模式、擦除粒度与写保护是生死线
W25Q64 是标准的 Quad SPI NOR Flash,容量 64Mbit(8MB),但它的“可用空间”远非简单除以扇区大小。关键参数必须手查 datasheet 并硬编码进驱动:
- 页大小(Page Size):256 字节 —— 这是最小可写单位,不能跨页写,也不能单字节改写(NOR Flash 特性);
- 扇区大小(Sector Size):4KB(4096 字节)—— 这是最小可擦除单位,写之前必须先擦除整扇区(值全变 0xFF);
- 块大小(Block Size):64KB(16 个扇区)—— 擦除命令可选 Sector Erase (0x20) 或 Block Erase (0xD8),FAL 层通常只用 Sector;
- 写保护机制:Status Register 的 BP0/BP1/BP2 位控制不同区域的写保护,出厂默认为 0(无保护),但若曾被误设,
W25Qxx_WriteEnable()后发W25Qxx_PageProgram()会直接失败且无报错。
提示:万耦开发板的 W25Q64 通常接在 SPI2 上,CS 引脚为 PB12(需确认原理图),SPI 模式为 Mode 0(CPOL=0, CPHA=0),最高主频建议 ≤ 30MHz(实测 36MHz 下偶发 CRC 错误)。
2.2 SFUD 层:用sfud_flash_t统一描述物理 Flash,绕过厂商私有驱动
SFUD(Serial Flash Universal Driver)的核心价值是把 W25Q64、MX25L、GD25Q 等不同品牌 Flash 的初始化、读写擦流程抽象成同一套函数指针。你不需要为 W25Q64 单独写w25q64_read(),而是注册一个sfud_flash_t实例:
#include "sfud.h" static sfud_flash_t w25q64 = { .name = "W25Q64", .spi = &spi2_bus, // 指向你已初始化好的 RT-Thread SPI 设备句柄 .chip = &sfud_w25q64_chip_info, // SFUD 内置的 W25Q64 芯片信息(含 JEDEC ID、容量、指令集) }; // 初始化 SFUD(必须在 FAL 之前调用) sfud_init(); sfud_device_register(&w25q64, "w25q64");这里sfud_w25q64_chip_info已预置了 W25Q64 的page_size=256,sector_size=4096,block_size=65536,以及write_cmd=0x02,erase_cmd=0x20等指令码。关键点在于:sfud_device_register()的第二个参数"w25q64"将成为 FAL 层引用该设备的唯一标识符,后续所有 FAL 分区都必须绑定到这个名称。
2.3 FAL 层:把物理 Flash 切成逻辑分区,fal_cfg.h是唯一真相源
FAL(Flash Abstraction Layer)负责将整块 8MB Flash 划分为多个逻辑分区(Partition),例如param存配置、log存日志、app存固件备份。所有分区定义必须严格写在fal_cfg.h中,且必须与fal_port.c中的底层读写函数一一对应。万耦板常见划分如下(单位:字节):
| 分区名 | 起始地址 | 长度 | 用途 | 对应 SFUD 设备 |
|---|---|---|---|---|
| param | 0x000000 | 0x004000 (16KB) | 用户配置(WiFi SSID/密码、上报周期等) | w25q64 |
| log | 0x004000 | 0x07C000 (500KB) | 循环日志(按时间戳追加) | w25q64 |
| fdb | 0x080000 | 0x080000 (512KB) | FlashDB 专用区(KV 数据 + 元数据) | w25q64 |
fal_cfg.h必须精确声明:
#define FAL_PART_TABLE \ { \ {FAL_PART_MAGIC_WORD, "param", "w25q64", 0x000000, 0x004000, 0}, \ {FAL_PART_MAGIC_WORD, "log", "w25q64", 0x004000, 0x07C000, 0}, \ {FAL_PART_MAGIC_WORD, "fdb", "w25q64", 0x080000, 0x080000, 0}, \ }注意三点:
- 第三列
"w25q64"必须与sfud_device_register()的设备名完全一致(大小写敏感); - 起始地址和长度必须是
sector_size(4096)的整数倍,否则 FAL 初始化失败; fal_init()必须在sfud_init()之后、flashdb_init()之前调用,否则 FlashDB 找不到底层分区。
2.4 FlashDB 层:KV 接口只是表象,fdb_kvdb_t初始化才是成败枢纽
FlashDB 的fdb_kvdb_t结构体不是单纯的数据容器,它绑定了 FAL 分区名、内存缓存区、以及底层读写回调。最易错的是fdb_kvdb_t初始化时传入的分区名,必须与fal_cfg.h中定义的完全一致:
#include "flashdb.h" static uint8_t kv_cache[512]; // 缓存区,至少 512 字节(推荐 1KB) static struct flashdb_kvdb kvdb; // 初始化:第三个参数 "fdb" 必须等于 fal_cfg.h 中定义的分区名! fdb_kvdb_init(&kvdb, "kvdb", "fdb", &kv_cache[0], sizeof(kv_cache));"kvdb":是 FlashDB 实例的内部名称(用于fdb_kv_get_by_name()查找);"fdb":是 FAL 分区名,必须与fal_cfg.h中第三行{..., "fdb", ...}的"fdb"完全一致;&kv_cache[0]:缓存区首地址,FlashDB 用它暂存元数据(如 KV 头、CRC 校验),太小会导致fdb_kv_set()失败;- 若此处填错为
"param"或"log",fdb_kvdb_init()会返回FDB_NO_ERR(看似成功),但后续所有fdb_kv_set()均静默失败——因为 FlashDB 在"fdb"分区里找不到合法的 KV 区头(magic word0x55AA55AA)。
这四层不是并列关系,而是强依赖链:W25Q64 时序错 → SFUD 读写超时;SFUD 设备名错 → FAL 找不到设备;FAL 分区名错 → FlashDB 初始化无感知失败;FlashDB 分区名错 → KV 操作全部失效。漏掉任一环,你看到的只会是fdb_kv_get("temp", &val, sizeof(val))返回0(未找到),而不会告诉你哪一层崩了。
3. 移植全流程:从裸机工程到 KV 写入成功的六步实操
3.1 步骤一:确认硬件连接与 SPI 初始化(万耦板专属)
万耦开发板的 W25Q64 默认挂载在 SPI2 总线上,引脚映射需严格对照原理图(以常见版本为例):
| W25Q64 引脚 | 开发板引脚 | 备注 |
|---|---|---|
| CS | PB12 | 必须配置为推挽输出,初始高电平 |
| DO (IO1) | PB14 | MISO |
| DI (IO0) | PB15 | MOSI |
| CLK | PB13 | SCLK |
| WP# | 悬空或拉高 | 写保护禁用(若接低电平则整片只读) |
| HOLD# | 悬空或拉高 | 暂停传输禁用 |
SPI2 初始化代码(基于 RT-Thread HAL):
#include "drv_spi.h" #include "stm32f4xx_hal.h" static struct stm32_spi_config spi2_config = { .spi_periph = SPI2, .spi_gpio_group = GPIOB, .spi_sclk_pin = GPIO_PIN_13, .spi_miso_pin = GPIO_PIN_14, .spi_mosi_pin = GPIO_PIN_15, .spi_nss_pin = GPIO_PIN_12, // CS 引脚 }; // 在 board.c 的 rt_hw_board_init() 中调用 void rt_hw_spi2_init(void) { __HAL_RCC_SPI2_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); // 配置 PB12~PB15 为复用推挽 GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate = GPIO_AF5_SPI2; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); // 初始化 SPI2(Mode 0, 30MHz) SPI_HandleTypeDef hspi2; hspi2.Instance = SPI2; hspi2.Init.Mode = SPI_MODE_MASTER; hspi2.Init.Direction = SPI_DIRECTION_2LINES; hspi2.Init.DataSize = SPI_DATASIZE_8BIT; hspi2.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0 hspi2.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0 hspi2.Init.NSS = SPI_NSS_SOFT; hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; // 84MHz/4 = 21MHz < 30MHz hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi2.Init.TIMode = SPI_TIMODE_DISABLE; hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; HAL_SPI_Init(&hspi2); // 注册为 RT-Thread 设备 struct stm32_spi_bus *spi_bus = rt_malloc(sizeof(struct stm32_spi_bus)); spi_bus->spi_handle = &hspi2; rt_spi_bus_register(&spi_bus->parent, "spi2", &stm32_spi_ops); }注意:
HAL_SPI_Init()后必须调用HAL_SPIEx_DisableEnhancedMode(&hspi2)(若启用 QSPI 模式),否则 W25Q64 的 Quad Read 指令可能异常。
3.2 步骤二:集成 SFUD 并验证基础读写
将 SFUD 源码(sfud/src/sfud.c等)加入工程,确保SFUD_USING_SFDP关闭(W25Q64 不支持 SFDP),SFUD_USING_QSPI关闭(万耦板用普通 SPI)。在main()中添加验证代码:
#include "sfud.h" int sfud_test(void) { sfud_err_t result; uint8_t test_buf[16] = {0}; // 1. 读取 JEDEC ID(应为 0xEF 0x40 0x17) result = sfud_read_jedec_id(&w25q64, test_buf); if (result != SFUD_SUCCESS) { LOG_E("Read JEDEC ID failed: %d", result); return -1; } LOG_I("JEDEC ID: 0x%02X 0x%02X 0x%02X", test_buf[0], test_buf[1], test_buf[2]); // 2. 读取第一个扇区(0x000000)前 16 字节(应全 0xFF) result = sfud_read(&w25q64, 0x000000, 16, test_buf); if (result != SFUD_SUCCESS) { LOG_E("Read sector 0 failed: %d", result); return -1; } LOG_I("First 16 bytes: %02X %02X %02X %02X ...", test_buf[0], test_buf[1], test_buf[2], test_buf[3]); // 3. 擦除第一个扇区(关键!否则写入失败) result = sfud_erase(&w25q64, 0x000000, 4096); if (result != SFUD_SUCCESS) { LOG_E("Erase sector 0 failed: %d", result); return -1; } LOG_I("Sector 0 erased."); return 0; }现象:若sfud_read_jedec_id()返回SFUD_ERR_TIMEOUT,说明 SPI 时序或 CS 控制错误;若sfud_read()读出非0xFF,说明 Flash 曾被写入且未擦除;若sfud_erase()耗时超过 100ms,检查W25Qxx_WaitForWriteEnd()是否正确轮询 BUSY 位(需读 Status Register 的 bit0)。
3.3 步骤三:配置 FAL 分区并初始化
在fal_cfg.h中按 2.3 节定义分区表后,实现fal_port.c的底层函数。万耦板必须重写fal_flash_read/write/erase,不能直接调用 SFUD 的同名函数,因为 FAL 要求地址是相对于分区起始地址的偏移量,而 SFUD 函数要求绝对物理地址:
// fal_port.c #include "fal.h" #include "sfud.h" extern sfud_flash_t w25q64; long fal_flash_read(long offset, uint8_t *buf, size_t size) { // offset 是相对于分区起始的偏移,需转为绝对地址 // 假设当前调用来自 "fdb" 分区,则绝对地址 = 0x080000 + offset // 但 FAL 会自动传入正确的 base_addr,我们只需用它 return sfud_read(&w25q64, offset, size, buf) == SFUD_SUCCESS ? size : -1; } long fal_flash_write(long offset, const uint8_t *buf, size_t size) { // W25Q64 写入前必须确保目标页已擦除,且不跨页 // FAL 已保证 offset 和 size 对齐到 page_size(256),此处可直写 return sfud_write(&w25q64, offset, size, buf) == SFUD_SUCCESS ? size : -1; } long fal_flash_erase(long offset, size_t size) { // FAL 保证 offset 和 size 是 sector_size(4096)的整数倍 return sfud_erase(&w25q64, offset, size) == SFUD_SUCCESS ? size : -1; }然后在main()中初始化:
#include "fal.h" int fal_test(void) { if (fal_init() != 0) { LOG_E("FAL init failed!"); return -1; } const struct fal_partition *part = fal_partition_find("fdb"); if (!part) { LOG_E("Partition 'fdb' not found!"); return -1; } LOG_I("FAL partition 'fdb': addr=0x%08lx, len=%ld", part->offset, part->len); // 读取 fdb 分区前 16 字节(应全 0xFF) uint8_t buf[16]; if (fal_partition_read(part, 0, buf, 16) != 16) { LOG_E("Read fdb partition failed!"); return -1; } LOG_I("fdb first 16 bytes: %02X %02X %02X %02X ...", buf[0], buf[1], buf[2], buf[3]); return 0; }关键验证点:fal_partition_find("fdb")必须返回非 NULL,且part->offset应为0x080000。若返回 NULL,检查fal_cfg.h的FAL_PART_TABLE是否拼写错误或未包含该文件。
3.4 步骤四:初始化 FlashDB 并写入首个 KV
在fal_test()成功后,初始化 FlashDB:
#include "flashdb.h" static uint8_t kv_cache[1024]; static struct flashdb_kvdb kvdb; int flashdb_test(void) { // 初始化 KVDB,分区名必须为 "fdb" if (fdb_kvdb_init(&kvdb, "kvdb", "fdb", kv_cache, sizeof(kv_cache)) != FDB_NO_ERR) { LOG_E("FlashDB init failed!"); return -1; } LOG_I("FlashDB initialized on 'fdb' partition."); // 写入一个测试 KV int32_t temp_val = 2565; // 25.65°C if (fdb_kv_set(&kvdb, "temp", &temp_val, sizeof(temp_val)) != FDB_NO_ERR) { LOG_E("Set 'temp' failed!"); return -1; } LOG_I("Set 'temp' = %d", temp_val); // 读取验证 int32_t read_val = 0; if (fdb_kv_get(&kvdb, "temp", &read_val, sizeof(read_val)) == sizeof(read_val)) { LOG_I("Get 'temp' = %d", read_val); } else { LOG_E("Get 'temp' failed!"); return -1; } return 0; }玄学时刻:首次运行时,fdb_kv_set()可能返回FDB_SAVED_FULL(空间满),这是因为 FlashDB 需要格式化分区(写入 magic word 和初始元数据)。此时需手动擦除fdb分区整块(sfud_erase(&w25q64, 0x080000, 0x080000)),再重启运行。
3.5 步骤五:验证时序写入与循环覆盖(嵌入式时序数据库核心能力)
FlashDB 的fdb_tsdb_t(Time Series DB)模块专为时序数据设计。在万耦板上存传感器采样点,需创建 TSDB 实例:
#include "flashdb.h" static uint8_t tsdb_cache[2048]; static struct flashdb_tsdb tsdb; // 初始化 TSDB,同样绑定到 "fdb" 分区 if (fdb_tsdb_init(&tsdb, "tsdb", "fdb", tsdb_cache, sizeof(tsdb_cache)) != FDB_NO_ERR) { LOG_E("TSDB init failed!"); return -1; } // 写入一条时序数据:timestamp(毫秒)+ value(int32_t) struct sensor_data { uint32_t timestamp; int32_t value; } data = {.timestamp = rt_tick_get_millisecond(), .value = 2565}; if (fdb_tsdb_append(&tsdb, "sensor_temp", &data, sizeof(data)) != FDB_NO_ERR) { LOG_E("Append to 'sensor_temp' failed!"); }血泪经验:TSDB 的append操作会自动按时间戳排序,并在空间不足时自动覆盖最老数据(LRU 策略),无需手动管理。但必须确保fdb_tsdb_init()的缓存区 ≥ 2KB,否则append可能因缓存不足失败。
3.6 步骤六:完整启动流程整合(避免初始化顺序踩坑)
最终main()必须严格遵循此顺序,缺一不可:
int main(void) { rt_hw_board_init(); // 包含 spi2_init() // 1. SFUD 初始化(最底层) sfud_init(); sfud_device_register(&w25q64, "w25q64"); // 2. FAL 初始化(依赖 SFUD) fal_init(); // 3. FlashDB 初始化(依赖 FAL) fdb_kvdb_init(&kvdb, "kvdb", "fdb", kv_cache, sizeof(kv_cache)); fdb_tsdb_init(&tsdb, "tsdb", "fdb", tsdb_cache, sizeof(tsdb_cache)); // 4. 应用逻辑 while(1) { // 采集传感器,写入 KV 和 TSDB update_sensor_data(); rt_thread_mdelay(2000); } }翻车预警:若将fdb_kvdb_init()放在fal_init()之前,FlashDB 会因找不到"fdb"分区而静默失败;若sfud_init()放在fal_init()之后,FAL 初始化时调用sfud_read()会因 SFUD 未就绪而超时。
4. 避坑指南:万耦板上 FlashDB 四层栈的五个典型翻车现场
4.1 现象:fdb_kv_set()返回FDB_NO_ERR,但fdb_kv_get()始终返回 0(未找到)
原因:FlashDB 初始化时传入的分区名(如"fdb")与fal_cfg.h中定义的分区名不一致,或大小写不符(如写成"FDB")。FlashDB 在错误分区里找不到 KV 区头(magic word0x55AA55AA),于是新建一个空 KV 区,但写入操作因分区无效而实际未执行。
解决:用sfud_read()直接读取fdb分区起始地址(0x080000)的前 32 字节,确认是否为0x55 0xAA 0x55 0xAA ...;若不是,检查fdb_kvdb_init()第三个参数和fal_cfg.h是否完全一致。
4.2 现象:fal_init()返回 0,但fal_partition_find("fdb")返回 NULL
原因:fal_cfg.h未被正确包含到编译单元,或FAL_PART_TABLE宏定义语法错误(如末尾多逗号、括号不匹配),导致预处理器展开为空。FAL 初始化时分区表为空,自然找不到"fdb"。
解决:在fal.c中fal_init()函数内添加LOG_D("FAL table size: %d", sizeof(fal_part_table));,若输出0则证明宏未展开;检查fal_cfg.h是否被#include,且FAL_PART_TABLE定义无语法错误。
4.3 现象:sfud_read_jedec_id()返回SFUD_ERR_TIMEOUT,SPI 通信无响应
原因:万耦板的 W25Q64 CS 引脚(PB12)未配置为推挽输出,或初始化时未拉高(CS 低电平才选中)。更隐蔽的是,HAL 库的HAL_GPIO_Init()中GPIO_InitStruct.Pull设为GPIO_PULLUP,导致 CS 引脚悬空时被上拉,无法拉低。
解决:强制在rt_hw_spi2_init()中初始化后添加HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET);(拉高),并在sfud_spi_write_read()的 CS 控制逻辑中确保HAL_GPIO_WritePin(..., GPIO_PIN_RESET)和SET成对出现。
4.4 现象:fdb_tsdb_append()频繁返回FDB_SAVED_FULL,即使分区有 500KB 空间
原因:TSDB 的append操作需要额外空间存储时间戳索引,若tsdb_cache缓存区过小(< 1KB),FlashDB 无法构建有效索引,误判为满。
解决:将tsdb_cache数组大小提升至2048字节,并确认fdb_tsdb_init()的第四个参数传入sizeof(tsdb_cache),而非&tsdb_cache(指针大小)。
4.5 现象:设备断电重启后,fdb_kv_get()读出乱码或旧值
原因:W25Q64 的 Status Register 写保护位(BP0/BP1/BP2)被意外置位,导致部分扇区变为只读。FlashDB 的fdb_kv_set()在写入前会擦除扇区,但若该扇区被写保护,擦除命令无效,后续写入覆盖旧数据失败。
解决:用sfud_read_status(&w25q64, &status)读取状态寄存器,若status & 0x1C(BP0~BP2)非零,则调用sfud_write_status(&w25q64, status & ~0x1C)清除写保护位,再重新擦除相关扇区。
5. 进阶技巧:用fdb_kv_iterate()实现配置热更新与安全擦除
5.1 遍历所有 KV 并打印(调试黄金技能)
当怀疑 KV 区损坏或想审计所有配置项时,fdb_kv_iterate()是唯一可靠方式。它不依赖 key 名,而是按 Flash 物理顺序扫描所有有效 KV:
#include "flashdb.h" static void kv_print_callback(fdb_kv_t kv, void *arg) { char key_str[64]; if (fdb_kv_get_name(kv, key_str, sizeof(key_str)) > 0) { LOG_I("KV: '%s' (size=%d, addr=0x%08lx)", key_str, kv->value_len, kv->addr.value); // 读取值内容(最多 64 字节) uint8_t val_buf[64]; size_t len = fdb_kv_get_value(kv, val_buf, sizeof(val_buf)); if (len > 0) { LOG_HEXDUMP_I("Value", val_buf, len); } } } // 调用遍历 fdb_kv_iterate(&kvdb, kv_print_callback, NULL);注意:fdb_kv_iterate()会跳过已删除(marked as deleted)和无效的 KV,只返回当前有效的键值对。这是比fdb_kv_get()更底层的调试手段,能发现fdb_kv_set()是否真的写入成功。
5.2 安全擦除 KV 区(避免残留敏感数据)
FlashDB 的fdb_kv_del()只是标记删除,物理数据仍在 Flash 上。若需彻底清除(如设备出厂前),必须擦除整个fdb分区:
// 1. 先关闭 KVDB(防止写入冲突) fdb_kvdb_deinit(&kvdb); // 2. 获取 fdb 分区信息 const struct fal_partition *part = fal_partition_find("fdb"); if (part) { // 3. 擦除整块分区(4096 字节对齐) for (uint32_t addr = part->offset; addr < part->offset + part->len; addr += 4096) { sfud_erase(&w25q64, addr, 4096); } LOG_I("Partition 'fdb' erased."); } // 4. 重新初始化(会重建 KV 区头) fdb_kvdb_init(&kvdb, "kvdb", "fdb", kv_cache, sizeof(kv_cache));关键点:擦除必须按sector_size(4096)对齐,且必须在fdb_kvdb_deinit()之后执行,否则 FlashDB 的缓存可能与物理状态不一致。
5.3 配置热更新:监听 KV 变更并触发动作
FlashDB 本身不提供事件回调,但可通过轮询fdb_kv_get()的返回值变化实现轻量级热更新:
static int32_t last_report_interval = 0; void check_config_update(void) { int32_t new_interval = 0; if (fdb_kv_get(&kvdb, "report_interval", &new_interval, sizeof(new_interval)) == sizeof(new_interval)) { if (new_interval != last_report_interval && new_interval > 0) { LOG_I("Config updated: report_interval = %d ms", new_interval); // 触发上报任务重配置 update_upload_task_interval(new_interval); last_report_interval = new_interval; } } } // 在主循环中每 500ms 调用一次 while(1) { check_config_update(); rt_thread_mdelay(500); }边界处理:fdb_kv_get()返回值为实际读取字节数,必须严格等于sizeof(int32_t)才认为数据有效;若返回0,说明 key 不存在,应使用默认值。
5.4 时序数据批量查询(按时间窗口提取)
TSDB 的fdb_tsdb_read_range()可按时间戳范围提取数据,适用于生成日报:
#include "flashdb.h" // 查询过去 24 小时(86400000 ms)内的温度数据 uint32_t now = rt_tick_get_millisecond(); uint32_t start_time = now > 86400000 ? now - 86400000 : 0; // 分配足够缓冲区(假设最多 100 条) struct sensor_data records[100]; size_t count = 100; // 读取 fdb_err_t err = fdb_tsdb_read_range(&tsdb, "sensor_temp", start_time, now, records, &count, sizeof(struct sensor_data)); if (err == FDB_NO_ERR) { LOG_I("Read %d records from last 24h", count); for (size_t i = 0; i < count; i++) { LOG_I("T=%lu, V=%d", records[i].timestamp, records[i].value); } }性能提示:read_range()是线性扫描,数据量大时耗时显著。若需高频查询,应在应用层维护索引(如按小时分表)。
从那以后我每次在万耦板上移植 FlashDB,都强制走一遍这六步:先用sfud_read_jedec_id()确认物理层握手成功,再用fal_partition_find()验证分区可见,接着用fdb_kvdb_init()后立即fdb_kv_set()写入测试键,最后用fdb_kv_iterate()扫描确认物理写入。这四道关卡卡住所有隐性错误,比对着文档猜三天强得多。希望帮到你。
本文还有配套的精品资源,点击获取