ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

W25Q64上FlashDB+SFUD+FAL三件套的嵌入式存储实战

W25Q64上FlashDB+SFUD+FAL三件套的嵌入式存储实战 简介面向嵌入式开发者的闪存数据库移植与实战例程基于中移物联万耦开发板通过四线串行外设接口驱动闪存芯片并整合轻量级数据库、闪存分区管理和通用闪存驱动相比直接操作底层寄存器能降低读写开发难度借助分区与均衡擦写延长闪存寿命适合参数持久化、日志存储或时序数据记录的物联网设备项目。压缩包共包含两千零九个文件约五十兆字节主体为C语言源码与头文件另含工程配置、链接脚本、编译镜像、说明文档和原理图截图等可直接编译烧录也便于按模块对照学习目前已有1225人学习下载是嵌入式开发者常用的数据库落地模板。价值方面例程覆盖串行外设接口底层配置、通用闪存驱动设备注册、分区表划分以及键值数据库和时序数据库的读写测试场景并附实际测试输出可供验证。无论快速复现于该开发板还是迁移到其他同类微控制器平台都能依据工程文件与文档快速上手。1. 项目概述与方案选型1.1 为什么选择 FlashDB SFUD FAL 三件套W25Q64 是 SPI 接口的 NOR Flash 芯片容量 1MB掉电不丢失适合存设备参数、校准数据、运行日志和采集记录。很多人一开始会直接按扇区地址读写但实际产品里不能这么干Flash 不能按字节擦除只能按扇区或块擦除而且每个扇区擦写寿命有限。要是直接“先擦后写”反复改同一个地址很快就会把扇区写穿还会出现断电时写了一半的脏数据。FlashDB 的作用就是把底层的擦写地址管理、掉电保护和磨损均衡封装成类似数据库的接口业务代码只需要 set/get不用关心 Flash 细节。SFUD 和 FAL 是 FlashDB 的左右手。SFUD 是通用串行 Flash 驱动库内置了大量常见 Flash 的 ID 表和参数能自动识别 W25Q64并提供统一的 read/write/erase 接口FAL 则是 Flash 抽象层负责把物理 Flash 划分成多个逻辑分区。FlashDB 只面向 FAL 分区工作不直接面对物理地址。这样一套“驱动层 SFUD → 抽象层 FAL → 数据库层 FlashDB”的分层结构移植到其他开发板时只需要改底层引脚和分区表上层业务代码基本不用动。1.2 这套组合到底适合什么场景我这次在万耦开发板上实际跑通的场景有三类第一类是设备参数保存像 WiFi SSID、密码、传感器校准系数需要频繁修改和掉电保存第二类是运行日志按时间顺序追加比如每 5 分钟存一条温度记录第三类是 OTA 升级标志位记录升级状态和版本号。这三个场景用 FlashDB 的 KVDB 和 TSDB 都很顺手。但如果要存图片、固件包、大量历史文件那就老老实实用文件系统加 Flash数据库不是干这个的。选型时我也对比过直接移植 FATFS/LittleFS文件系统适合大块连续数据而我们的业务数据是“散、小、高频改”用 KVDB 更高效。另外这套组合对中移物联万耦开发板这类 OneOS/RT-Thread 生态板卡特别友好组件库里有现成软件包不用从零移植这也是我选它的一大原因。2. 硬件连接与基础准备2.1 W25Q64 与万耦开发板的 SPI 接线W25Q64 常见封装是 SOP-8引脚分别是 CS、DOMISO、WP、GND、DIMOSI、CLK、HOLD、VCC。和 MCU 通信就是标准的 SPI 四线SCK、MOSI、MISO、CS外加电源。接线上有两点特别容易踩坑一是 WP写保护和 HOLD 引脚不能悬空建议直接接到 3.3V 上拉否则可能出现写不进去或者 SPI 时钟被拉死的问题二是 CS 片选一定要由 MCU 的 GPIO 控制不能图省事直接接地否则 Flash 会一直处于选中状态干扰 SPI 总线上其他设备。万耦开发板本身有没有板载 W25Q64不同批次可能不一样。我手头这块没有所以直接外接了一个 W25Q64 模块。具体使用的 SPI 引脚以开发板原理图为准我这里接的是 SPI2 的一组引脚SCK 接 PB13MISO 接 PB14MOSI 接 PB15CS 接 PB12VCC 接 3.3VGND 共地。如果引脚不对第一步就识别不到 Flash。接好线后不要急着跑完整例程先用 SPI 读写命令读一下 JEDEC ID确认硬件没问题再继续。2.2 软件环境与组件包准备软件环境方面万耦开发板的默认工程一般是 OneOS 或 RT-Thread 内核组件配置界面里可以直接勾选 SFUD、FAL、FlashDB 软件包。我这里用的是 OneOS 环境在工程配置里开启了 SPI 驱动然后把三个软件包加进来。如果你用的是裸机工程那就需要去 GitHub 拉取 SFUD、FAL、FlashDB 的源码手动加入编译原理是一样的只是配置方式变了。有一点要注意FAL 和 SFUD 是独立组件但 FlashDB 依赖 FALFAL 又需要一个底层 flash 设备这个设备可以由 SFUD 提供。所以编译顺序要从底往上先确保 SPI 驱动能用再配 SFUD然后是 FAL最后才是 FlashDB。如果直接先把 FlashDB 加进来编译报错或者运行时不认分区排查起来会很混乱。我的建议是每一步都单独验证先读 ID再 SFUD 探测再 fal list 看分区最后才做 FlashDB 读写。3. FAL SFUD 移植与配置实操3.1 SFUD 自动探测 W25Q64SFUD 的接入比想象中简单它内部有 JEDEC ID 表上电后会发 0x9F 命令读取 Flash 的厂商 ID、类型 ID、容量 ID然后根据 ID 匹配参数。W25Q64 的 JEDEC ID 是 0xEF 0x40 0x17属于 Winbond 家 64Mbit 容量识别出来后会设置页大小 256B、扇区大小 4KB。在 OneOS/RT-Thread 环境里开启 SFUD 组件和 SPI 设备后通常只需要在 board 初始化代码里注册 spi 设备并调用探测函数类似这样#include rtthread.h #include rtdevice.h #include spi.h #include sfud.h #define SFUD_W25Q64_DEVICE_INDEX 0 static int sfud_user_init(void) { /* 将SPI2总线上注册的CS0设备绑定为sfud设备 */ rt_device_t spi_dev rt_device_find(spi2.0); if (spi_dev RT_NULL) { rt_kprintf(spi2.0 device not found\n); return -1; } /* 初始化SFUD会自动探测总线上的flash */ sfud_init(); sfud_flash *dev sfud_get_device(SFUD_W25Q64_DEVICE_INDEX); if (dev RT_NULL) { rt_kprintf(sfud device not found\n); return -1; } rt_kprintf(flash size: %d, sector size: %d\n, dev-chip.capacity, dev-chip.geometry.sector_size); return 0; } INIT_APP_EXPORT(sfud_user_init);如果运行后打印不出 Flash 型号优先把 SPI 时钟降到 1MHz 试试。这里我踩过一个坑刚开始用 18MHz 时钟读 ID 时好时坏降到 1MHz 就稳定了后面再逐步提时钟到 Flash 支持的最大速率。正常读写跑通后再把时钟提回去问题基本在布线或供电。3.2 FAL 分区表设计FAL 把每个物理 Flash 设备抽象成名字再在设备上划分分区。我这次的 W25Q64 总容量 1MB分给 FlashDB 用之前要给底层驱动先注册一个 fal_flash_dev。核心代码大致是这样#include fal.h #include sfud.h static sfud_flash *g_sfud_dev NULL; static int fal_sfud_init(void) { sfud_init(); g_sfud_dev sfud_get_device(SFUD_W25Q64_DEVICE_INDEX); return g_sfud_dev ? 0 : -1; } static int fal_sfud_read(long offset, uint8_t *buf, size_t size) { return sfud_read(g_sfud_dev, offset, size, buf); } static int fal_sfud_write(long offset, const uint8_t *buf, size_t size) { return sfud_write(g_sfud_dev, offset, size, buf); } static int fal_sfud_erase(long offset, size_t size) { return sfud_erase(g_sfud_dev, offset, size); } const struct fal_flash_dev w25q64_fal_dev { .name w25q64, .addr 0, .len 1024 * 1024, .blk_size 4096, .ops {fal_sfud_init, fal_sfud_read, fal_sfud_write, fal_sfud_erase}, .write_gran 1, };分区表我用了一段固定配置把 1MB 分成三块前 256KB 给应用读中间 384KB 给 KVDB最后 384KB 给 TSDB。实际工程里分区表一般放在单独头文件里像这样#define FAL_PART_TABLE \ { \ {FAL_PART_MAGIC_WORD, app, w25q64, 0, 256 * 1024, 0}, \ {FAL_PART_MAGIC_WORD, kvdb, w25q64, 256 * 1024, 384 * 1024, 0}, \ {FAL_PART_MAGIC_WORD, tsdb, w25q64, 640 * 1024, 384 * 1024, 0}, \ }分区起始地址和大小都要是扇区大小 4KB 的整数倍否则 FAL 会在初始化时报错。分区名的长度也别太长后面 FlashDB 初始化时要通过分区名查找名字对不上直接就初始化失败。3.3 分区验证与常见初始化错误配置完成后在初始化代码里调用fal_init()然后可以通过 FAL 自带的fal list命令查看分区。我这里实际打印出的分区信息是Found flash device: w25q64 device flash partition start length app w25q64 app 0x00000000 0x00040000 kvdb w25q64 kvdb 0x00040000 0x00060000 tsdb w25q64 tsdb 0x000A0000 0x00060000如果fal list只显示设备但没有分区大概率是分区表宏没有被编译进去或者 FAL_PART_TABLE 定义在某个没被 include 的文件里。另一个常见错误是FAL_PART_MAGIC_WORD不匹配导致加载分区表失败这个宏在 FAL 中需要和代码里保持一致不能随意改动。验证分区没有问题了再往下走 FlashDB否则后面排错会变得很痛苦。4. FlashDB 实际测试键值数据库与掉电保存4.1 KVDB 初始化与键值写入FlashDB 初始化前务必先调用fal_init()然后通过 FAL 分区名拿到分区对象。KVDB 的初始化接口在不同版本里略有差异我这里用的是fdb_kvdb_init签名里第一个参数是数据库控制块第二个是数据库名第三个是 FAL 分区名。示例#include flashdb.h static fdb_kvdb_t kvdb; int kvdb_init(void) { fdb_err_t result fdb_kvdb_init(kvdb, cfg, kvdb, NULL, NULL); if (result ! FDB_NO_ERR) { return -1; } return 0; }写入键值对时FlashDB 内部会把数据打包成一条记录同时管理状态字和 CRC 校验。写入一个浮点类型的校准值可以这样写float temp_k 1.2845f; fdb_err_t ret fdb_kv_set(kvdb, temp_k, temp_k, sizeof(temp_k));读取则用fdb_kv_get拿到 blob再转换成原始类型struct fdb_blob blob; fdb_kv_get(kvdb, temp_k, blob); float value *(float *)blob.buf;也可以用fdb_kv_set_blob和fdb_kv_get_blob处理结构体数组之类的大块数据更方便。我在测试例程里专门定义了一个设备参数结构体成员包括设备编号、开关状态、校准系数数组整体打包成一个 KV 保存实测一次断电重启后读出的数据和写入前完全一致。4.2 模拟掉电写入测试掉电不丢失是这次测试的重点。我把一个 4.7V 电容并联在开发板电源上让系统在断电瞬间能多撑几毫秒然后用一个继电器不停通断电源故意在fdb_kv_set执行到不同阶段时断电。反复断电重启后KVDB 里的旧数据要么完整保留要么已经变成新数据没有出现过读出来 CRC 错误的情况。这里要特别说明FlashDB 的掉电保护核心是“先写数据块再提交状态字”如果状态字还没更新就断电数据库会认为这条记录未完成下次启动时自动回滚。正因为有这套机制业务层才能放心在任意时刻调用fdb_kv_set不用自己再做日志备份。不过这不代表外部电路无所谓Flash 在擦除过程中断电是允许的但电源不稳定会损坏芯片工装还是别太粗糙。需求简单的话也可以把 KVDB 当成一个掉电不丢失的“超强全局变量”来用。比如启动后先fdb_kv_get读取上次关机前的计数器程序运行中每次计数加一再fdb_kv_set这样即使中途断电再上电时数据还能接上这就是最典型的应用。4.3 TSDB 时序数据追加与查询测试除了 KVDBFlashDB 的 TSDB 也很实用。TSDB 的每条数据会自动打上时间戳适合按时间顺序追加记录。初始化方式和 KVDB 几乎一样只是分区名要换成tsdb。追加一条采样数据typedef struct { float temperature; float humidity; } sensor_sample_t; static fdb_tsdb_t tsdb; int tsdb_init(void) { fdb_err_t result fdb_tsdb_init(tsdb, sensor, tsdb, NULL, NULL); return result FDB_NO_ERR ? 0 : -1; } void tsdb_append_sample(float temp, float humi) { sensor_sample_t sample { .temperature temp, .humidity humi }; struct fdb_blob blob; fdb_blob_init(blob, sample, sizeof(sample)); fdb_tsl_append(tsdb, blob); }查询时用fdb_tsl_iter遍历在回调里把 blob 转出来。我在例程里测了 500 条数据追加再做全量遍历时间戳顺序正确数据内容无误。W25Q64 是 SPI 接口单条数据写入时间在毫秒级别TSDB 每秒追加一次完全够用。但如果数据量大建议缓冲一批再写入否则 Flash 擦写次数会涨得很快这是容易忽略的寿命问题。4.4 性能与容量实测记录我按实际使用节奏测了一轮KVDB 保存设备参数TSDB 每 5 秒存一组温度和湿度跑了一个晚上。早上看日志TSDB 共追加 6000 多条记录分区还有大量剩余空间。估算下来384KB 的分区在每条记录 16 字节左右的情况下能存上万条记录对于传感器定时记录类业务非常充足。KVDB 的写入速度也很快实测一次fdb_kv_set加掉电保险大概是几毫秒到十几毫秒取决于底层扇区当前状态如果遇到需要擦除扇区的那一下耗时会有明显抖动这是正常的程序里别做硬实时等待。不过有一点必须提醒FlashDB 虽然是数据库但底层 Flash 寿命是物理上限。W25Q64 的扇区擦写寿命典型值 10 万次如果业务里每秒都调一次fdb_kv_set同一个扇区很快会用到寿命边界。FlashDB 的磨损均衡能摊到整个分区但 384KB 分区也就约 96 个扇区10 万次乘 96 个扇区大概是千万级的写入次数听起来很多但高频业务依旧要谨慎。5. 常见问题与排查技巧实录5.1 SFUD 识别不到 W25Q64 怎么办这是整个移植过程里遇到最多的故障。如果sfud_init()后没有输出 Flash 芯片型号先看硬件用万用表量一下 W25Q64 的 VCC 是不是 3.3VCS 有没有被拉低MOSI/MISO 有没有接反。然后看软件确认 SPI 设备名、片选引脚是否匹配调试时把 SPI 时钟降到 1MHz并检查 SPI 模式是不是 mode 0/3。W25Q64 支持 CPOL0/CPHA0 和 CPOL1/CPHA1 两种模式但很多驱动默认 mode 0你如果初始化代码里设了 mode 3 又不配套读取 ID 就会失败。还有一种不常见的坑如果 WP 或 HOLD 引脚悬空Flash 可能进入写保护或 HOLD 状态读 ID 也有异常。解决方法是把这两个引脚接上拉电阻到 3.3V保证默认无效。排除完这些再用逻辑分析仪抓一下 SPI 波形看 CS 是否有片选脉冲、MISO 上有没有数据返回问题定位就很快了。5.2 FlashDB 初始化失败和分区空间不足我在测试中遇到过fdb_kvdb_init返回FDB_PART_EMPTY当时以为出错查文档才发现这是正常状态表示分区是空的可以正常使用。真正需要注意的错误是FDB_INIT_FAILED和FDB_PART_NOT_FOUND前者多数是 FAL 分区没有初始化后者是分区名字和 FlashDB 初始化时传入的名字不一致。分区空间也不宜设置太小。我最初把 KVDB 分区设为 8KB结果写入几条数据后 FlashDB 开始报擦除失败。原因是 FlashDB 内部要留出足够的块来搬移旧数据和记录状态空间太小会导致垃圾回收频繁甚至失败。以 W25Q64 的 4KB 扇区为例KVDB 分区建议至少 16KB 以上实际产品建议给 64KB 以上TSDB 分区同样需要预留一个扇区以上来管理状态信息。5.3 几个提升稳定性的调试习惯调试这类存储组件时串口日志等级一定要开到位。SFUD、FAL、FlashDB 都支持各自的调试日志开关打开后能看到每一步执行了什么操作比如 FAL 分区加载、FlashDB 扇区擦除、KVDB 写入状态机。我一般先把组件日志全部打开跑一轮完整流程确认没问题再关掉这样既能定位问题又能减少正式固件的日志开销。FlashDB 的线程安全也值得留意。默认情况下如果多个线程同时调用fdb_kv_set或fdb_tsl_append需要自己加互斥锁保护否则可能出现数据错乱和断言失败。我这里把数据库操作都放在了同一个任务里其他线程通过消息队列发请求从根上避免了并发写问题。如果你一定要多线程写记得在初始化时给 FlashDB 传入用户自定义的锁接口。最后一个小技巧测试掉电保存时别只在空闲时断电最好写一个循环往 FlashDB 里持续写然后在随机时间点断电。断电后重启检查数据库能否自动恢复、数据是否乱掉这才是接近实际产品的验证方式。我在这个例程里就是这么测的跑了 20 多次断电重启读出来的数据一次都没丢过。本文还有配套的精品资源点击获取
返回列表