ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32F407+FreeRTOS存储方案:LittleFS+SUFD+EasyLogger+RTT实战

STM32F407+FreeRTOS存储方案:LittleFS+SUFD+EasyLogger+RTT实战

简介:本资源面向STM32嵌入式开发者与FreeRTOS学习者,聚焦在STM32F407平台上集成LittleFS文件系统、SFUD串行Flash驱动、EasyLogger日志组件与Segger RTT调试输出,解决片外25Q64 Flash上可靠存储与实时日志调试的问题。压缩包共1515个文件,约2.35MB,以C源码与头文件为主体,含342个h、308个c,另有大量js、png、gif等文档与网页资源,以及s、asm汇编启动文件、uvproj/uvopt工程文件、hex与bin固件、bat脚本和readme说明,覆盖驱动移植、文件系统挂载、日志配置与RTT调试的完整工程结构。已有1112人学习下载,适合需要参考LittleFS与SFUD移植细节、EasyLogger接入方式及Segger RTT调试配置的读者,可直接对照工程目录理解各模块组织与调用关系,快速搭建自己的Flash存储与日志调试方案。

1. 从一块 25Q64 说起:这套 STM32F407 工程到底在解决什么问题

如果你手头有一块 STM32F407 的板子,外挂了一颗 W25Q64 SPI Flash,同时又要跑 FreeRTOS,那你大概率会遇到一个很现实的问题:文件系统选谁?FATFS 太重、要移植、要调 diskio;裸写 Flash 又要自己管擦除、磨损、掉电保护。这套工程给出的答案是 LittleFS + SUFD 组合,把 25Q64 当成一个可读写的块设备,挂上 LittleFS,再叠一层 SUFD 做掉电安全与磨损均衡的兜底,日志走 EasyLogger,调试输出走 SEGGER RTT,全程不占用一个串口。

这套东西适合谁?适合正在做数据采集、参数存储、日志落盘这类需要“断电不丢文件”的嵌入式开发者。它不是一个玩具 Demo,而是一套可以直接往产品上靠的存储栈骨架。我第一次拿到类似结构的时候,最直观的感受是:终于不用在 FATFS 的 f_mount 和 disk_write 之间反复怀疑人生了。

2. 架构拆解:从 SPI 时序到文件句柄的完整分层

2.1 五层结构,每一层各管什么

这套工程的分层非常清晰,从上到下依次是:

层级模块职责
应用层业务任务读写文件、记录日志
日志层EasyLogger格式化日志、分级输出
文件系统层LittleFS文件/目录抽象、元数据管理
块设备适配层SUFD掉电安全、磨损均衡、坏块兜底
驱动层SPI + W25Q64扇区擦除、页编程、状态轮询

关键点在于 SUFD 这一层。LittleFS 本身已经有 copy-on-write 和元数据对,但它假设底层块设备是“相对可靠”的。SPI Flash 的现实是:擦除次数有限、写过程中掉电会留下半写页、某些扇区可能出厂就坏。SUFD 就是在 LittleFS 和裸 Flash 之间加了一层缓冲和校验,把“不可靠的物理块”包装成“看起来可靠的逻辑块”。

2.2 为什么不用 FATFS

FATFS 在 SD 卡上很成熟,但在 SPI Flash 上有几个硬伤:一是 FAT 表本身需要频繁更新,掉电时容易整盘挂掉;二是 FATFS 的磨损均衡基本靠用户自己实现;三是 FATFS 的目录项和簇链在随机写场景下性能很差。LittleFS 的设计目标就是为微控制器上的 Flash 准备的,它的元数据是日志式追加的,掉电后最多丢最后一次操作,不会出现“整个文件系统打不开”的情况。

2.3 目录结构长什么样

一个典型的工程目录会是这样:

project/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── FreeRTOSConfig.h │ │ └── stm32f4xx_hal_conf.h │ └── Src/ │ ├── main.c │ ├── freertos.c │ └── stm32f4xx_it.c ├── Drivers/ │ ├── STM32F4xx_HAL_Driver/ │ └── CMSIS/ ├── Middlewares/ │ ├── Third_Party/ │ │ ├── FreeRTOS/ │ │ ├── littlefs/ │ │ ├── EasyLogger/ │ │ └── segger_rtt/ │ └── SUFD/ │ ├── sufd.c │ └── sufd.h ├── BSP/ │ ├── spi_flash.c │ ├── spi_flash.h │ └── w25q64.c └── MDK-ARM/ └── project.uvprojx

这个结构的好处是 Middlewares 和 BSP 完全解耦。换一颗 Flash 只需要改 BSP 里的读写擦函数,SUFD 和 LittleFS 一行不用动。

3. 跑起来:从零搭建到第一个文件写入

3.1 硬件连接与 CubeMX 配置

W25Q64 走 SPI1,典型接法是:

  • PA5 -> SPI1_SCK
  • PA6 -> SPI1_MISO
  • PA7 -> SPI1_MOSI
  • PB0 -> CS(软件控制)
  • 3.3V 和 GND

CubeMX 里开 SPI1 为 Full-Duplex Master,预分频先设 8 分频(约 10.5MHz),数据宽度 8bit,CPOL=Low,CPHA=1Edge。FreeRTOS 用 CMSIS-RTOS v2,堆栈给足,因为 LittleFS 的递归操作会吃栈。

注意:SPI 时钟不要一上来就拉满。W25Q64 在 84MHz 下虽然标称支持,但杜邦线飞线时很容易读到 0xFF,先降速跑通再提速。

3.2 移植 SUFD 和 LittleFS

LittleFS 官方提供了lfs_port模板,核心是实现这几个函数:

// lfs_port.c static int lfs_flash_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) { uint32_t addr = block * c->block_size + off; return w25q64_read(addr, buffer, size); } static int lfs_flash_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { uint32_t addr = block * c->block_size + off; return w25q64_page_program(addr, buffer, size); } static int lfs_flash_erase(const struct lfs_config *c, lfs_block_t block) { uint32_t addr = block * c->block_size; return w25q64_sector_erase(addr); } static int lfs_flash_sync(const struct lfs_config *c) { return 0; }

参数说明:block_size对 W25Q64 来说通常设 4096(一个扇区),block_count是 2048(8MB / 4KB)。prog_size设 256,因为 W25Q64 一页是 256 字节。cache_size和lookahead_size各给 256 和 16 就够用。

SUFD 层夹在中间,它的接口和 lfs_config 对齐,但内部多了一个写缓冲和 CRC 校验:

// sufd.c int sufd_write(sufd_t *dev, uint32_t addr, const void *buf, uint32_t len) { // 先写数据到临时缓冲,计算 CRC uint32_t crc = crc32(buf, len); // 写入数据区 int ret = dev->flash_write(addr, buf, len); if (ret != 0) return ret; // 写入 CRC 到元数据区 return dev->flash_write(addr + len, &crc, sizeof(crc)); }

逻辑说明:每次写入都把 CRC 追加在数据后面。读取时先读数据再读 CRC,校验不过就返回错误,LittleFS 会触发重试或标记坏块。这样即使写一半掉电,下次挂载时也能识别出脏数据。

3.3 挂载与第一个文件

在 FreeRTOS 任务里挂载:

lfs_t lfs; lfs_file_t file; struct lfs_config cfg = { .read = lfs_flash_read, .prog = lfs_flash_prog, .erase = lfs_flash_erase, .sync = lfs_flash_sync, .read_size = 16, .prog_size = 256, .block_size = 4096, .block_count = 2048, .cache_size = 256, .lookahead_size = 16, .block_cycles = 500, }; void storage_task(void *argument) { int err = lfs_mount(&lfs, &cfg); if (err) { lfs_format(&lfs, &cfg); lfs_mount(&lfs, &cfg); } lfs_file_open(&lfs, &file, "boot.log", LFS_O_WRONLY | LFS_O_CREAT | LFS_O_APPEND); const char *msg = "system boot ok\n"; lfs_file_write(&lfs, &file, msg, strlen(msg)); lfs_file_close(&lfs, &file); }

block_cycles设 500 是告诉 LittleFS 每 500 次擦除做一次块迁移,配合 SUFD 的磨损统计,基本能覆盖 W25Q64 的 10 万次擦除寿命。

3.4 EasyLogger 对接 RTT

EasyLogger 默认输出到串口,改成 RTT 只需要实现一个输出函数:

void elog_port_output(const char *log, size_t size) { SEGGER_RTT_Write(0, log, size); }

然后在初始化时调用elog_init()、elog_set_fmt(),最后elog_start()。RTT 的好处是零中断开销,用 J-Link 的 RTT Viewer 就能看日志,不占串口也不影响实时性。

4. 避坑记录:那些让我熬夜的存储问题

4.1 挂载失败但格式化后正常

现象:第一次上电lfs_mount返回 -2,格式化后能用,但断电再上电又失败。

原因:W25Q64 的某些扇区在出厂时就有随机数据,LittleFS 的元数据对校验不过。SUFD 如果没有做上电擦除或标记,就会一直卡在挂载失败。

解决:在 SUFD 初始化时读一遍所有块的 CRC,校验失败的块标记为坏块,LittleFS 的block_count动态减去坏块数。或者第一次上电强制lfs_format,之后正常挂载。

4.2 写入速度慢到怀疑人生

现象:写一个 1KB 文件要几百毫秒。

原因:LittleFS 每次写都会触发元数据更新,而元数据更新又会触发擦除。如果block_cycles设得太小,擦除频率会非常高。

解决:把block_cycles调到 500 以上,cache_size从 256 加到 512,lookahead_size加到 32。另外确认 SPI 时钟不是 2MHz 这种龟速。

4.3 RTT 日志丢包

现象:RTT Viewer 里日志断断续续,高频率打印时丢一大段。

原因:RTT 的默认缓冲区只有 1KB,FreeRTOS 任务优先级又低,日志还没写完就被其他任务抢占。

解决:把BUFFER_SIZE_UP改成 4096,日志任务优先级提到中高,或者在 EasyLogger 里开异步输出模式。

4.4 掉电后文件变成 0 字节

现象:正常写入后断电,再上电文件存在但大小为 0。

原因:LittleFS 的文件关闭操作会写元数据,如果掉电发生在lfs_file_close之前,文件头已经写了但数据没落盘。

解决:SUFD 层加一个“提交”机制,所有写操作先写日志区,确认后再更新元数据。或者业务层每次写完主动调用lfs_file_sync。

4.5 FreeRTOS 任务栈溢出

现象:跑一段时间后 HardFault,栈使用率 100%。

原因:LittleFS 的lfs_file_write内部有递归调用,加上 EasyLogger 的格式化缓冲,默认 128 字的栈根本不够。

解决:存储任务栈至少给 1024 字(不是字节),日志任务给 512 字。用uxTaskGetStackHighWaterMark定期检查。

5. 二次开发:换 Flash、加加密、做 OTA 的改造思路

5.1 换一颗 Flash 要改哪里

如果从 W25Q64 换成 W25Q128,只需要改三个地方:block_count从 2048 改成 4096,BSP 里的地址位宽从 24 位改成 24 位(W25Q128 还是 3 字节地址),擦除命令如果一样就不用动。SUFD 的坏块表大小要跟着block_count走,别写死。

5.2 给文件加一层透明加密

在 SUFD 的读写函数里插 AES:

int sufd_write_encrypted(sufd_t *dev, uint32_t addr, const void *buf, uint32_t len) { uint8_t tmp[256]; aes_encrypt(buf, tmp, len, dev->key); return sufd_write(dev, addr, tmp, len); }

注意加密后的长度要按 16 字节对齐,LittleFS 的prog_size也要相应调整。密钥可以存在 STM32F407 的 OTP 区或者外部加密芯片里。

5.3 做 OTA 时的分区规划

W25Q64 有 8MB,可以这样分:

区域起始地址大小用途
bootloader0x000000128KB引导程序
app slot A0x0200003MB当前固件
app slot B0x3200003MB升级固件
lfs0x6200001.5MB文件系统
config0x7E0000128KB参数区

OTA 时把新固件写到 slot B,校验 CRC 后改 bootloader 的标志位,重启后跳转。LittleFS 区域不动,用户数据不丢。

5.4 一个我踩过的坑

有一次为了省事,把 SUFD 的 CRC 校验关掉了,想着“反正 LittleFS 自己有校验”。结果一批板子在客户现场跑了三个月,陆续出现文件系统挂载失败。拿回来一查,是 SPI Flash 的某个扇区在高温下出现了位翻转,LittleFS 的元数据对校验过了,但数据区的 CRC 对不上,导致文件读出来是乱码。从那以后我每次做存储方案,都强制走一遍“上电全盘 CRC 扫描 + 坏块标记”的流程,宁可开机慢 200ms,也不让脏数据进文件系统。

如果你也在用 STM32F407 做数据存储,这套 FreeRTOS + LittleFS + SUFD + EasyLogger + RTT 的组合可以直接拿去改。先跑通 SPI 读写,再挂 LittleFS,最后叠 SUFD 和日志,每一步都验证一遍,比一次性全接上再调试要快得多。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进