十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

SPIFS:面向w25qXX SPI NOR Flash的精简文件系统设计

SPIFS:面向w25qXX SPI NOR Flash的精简文件系统设计 简介SPIFS是一套为W25Q32、W25Q64、W25Q128等SPI Flash器件设计的极简文件系统核心代码约500行面向资源有限的嵌入式场景目标是让开发者以低成本获得基础的文件管理能力可用于日志存储、参数保存、设备配置读写等轻量应用。资源包共23个文件主要由9个C源文件、8个H头文件构成辅以CodeBlocks演示工程、README说明及依赖配置等压缩包仅27KB结构清晰便于直接查阅。项目按src与demo两个目录组织src提供文件系统实现并包含w25q32.c模拟器件demo提供可在gcc-4.8.2 x64环境下运行的演示项目方便快速验证读写流程。已有831人学习下载。该文件系统实现文件创建、写入、追加、重新读取等操作删除采用标记清除方式并采用84文件名布局逻辑简明适合对Flash文件系统原理感兴趣或需要快速嵌入的工程师参考。 做嵌入式项目的朋友应该都有过这种纠结MCU内部Flash不够用外挂一颗EEPROM容量又太小数据量稍微大点就开始想尽办法压缩。后来大家普遍会挂一颗SPI NOR Flash像w25q32、w25q64、w25q128容量从4Mb到16Mb串口读写价格又不贵几百KB的日志记录、固件升级、配置文件都能放下。但这时候第二个问题就来了数据到底怎么存直接操作地址显然不现实总不能每次升级固件都先把整个Flash读回PC再重新组织。于是需要一个轻量文件系统——SPIFS就是为这个场景写的。我最初只是想给传感器网关做一个简单的日志存储模块结果发现市面上常见的文件系统要么太重要么对NOR Flash的擦写特性适配不好。折腾几个月后我整理出了一套面向w25qXX系列的设计方案代码精简、内存占用低、行为可预测今天把这套思路完整分享出来。1. 项目背景为什么不敢直接往Flash里塞数据1.1 SPI NOR Flash的性格决定了上层设计想给Flash做文件系统先得摸清它的脾气。W25Q系列是典型的SPI NOR Flash有三个特点直接影响上层软件设计第一擦除粒度远大于读写粒度。w25q32/w25q64/w25q128的扇区擦除单位都是4KB但页编程单位只有256字节。也就是说你可以按字节读、按页写但不能按字节擦除想改一个字节得先把整个扇区读出来、改掉目标字节、再整扇区擦掉写回去。这不叫修改叫搬砖。第二写入只能把1变成0。NOR Flash编程的本质是电荷注入只能将bit从1写成0。如果某一位已经是0想把它恢复成1唯一办法就是擦除整个扇区。这个特性决定了文件系统不能像普通磁盘那样任意覆写必须设计出一套先擦后写、整块回收的策略。第三擦除寿命有限。w25q系列标称10万次擦除寿命听起来不少但如果系统每隔几秒就写一次日志集中在固定几个扇区上一块Flash几个月就能报废。所以文件系统必须引入磨损均衡把擦写压力均匀分摊到所有扇区。还有一点容易被忽略掉电不安全。嵌入式设备随时可能断电文件系统如果先把目录改了再写数据或者写到一半掉电Flash里就会出现半截数据和损坏的目录项。这个问题在普通PC磁盘上靠日志文件系统解决在MCU上就得自己想办法。1.2 FatFS、LittleFS和SPIFS三选一怎么挑在决定自研之前我把嵌入式领域常用的方案都过了一遍方案定位优点痛点FatFS通用FAT文件系统生态成熟、跨平台、PC可直接读取面向块设备设计用在NOR Flash上需额外加FTL层做磨损均衡和掉电保护LittleFS面向Flash的日志结构文件系统掉电安全、磨损均衡内置、官方维护代码量较大SRAM占用高小MCU上跑起来有点吃力直接裸存自己算地址、固定偏移简单直接无法动态管理文件名和大小升级固件时数据迁移极其痛苦SPIFS面向w25qXX的精简文件系统代码量小、内存占用低、针对NOR Flash特性定制功能相对基础适合日志和配置类数据FatFS虽好但它的块设备层假设底层是以扇区为单位可覆写的设备比如SD卡。NOR Flash不能覆写只能擦除后重写如果直接用FatFS每次修改文件都要承担全扇区搬移的成本还得自己在Flash驱动里做坏块管理和分配策略等于把FTL层重写了一遍。LittleFS在资源充足的平台上是很好的选择但我的手头项目用的是Cortex-M0SRAM只有8KBLittleFS跑起来虽不至于崩溃内存余量已经非常紧张。SPIFS的定位很明确只解决w25qXX这一种Flash的存储问题不做通用性承诺换取的是极低的代码量和内存开销。整个文件系统核心代码不到1500行RAM占用在500字节以内不含文件读写缓冲非常适合资源受限的MCU。2. SPIFS设计思路先拆需求再定结构2.1 数据区如何划分超级块、目录区、数据区我对文件系统的需求很朴素支持几十个文件、文件名不超过15个字符、文件大小可以动态增长、断电后目录尽量不坏。基于这些约束SPIFS的存储布局分为三个区域超级块区占用第0扇区记录文件系统的魔数、版本号、总扇区数、目录区起始位置、数据区起始位置等元信息。目录区紧跟在超级块之后以固定大小目录项存放文件名、起始扇区、文件长度和CRC。一个目录项32字节按最大文件数倒推扇区数。数据区剩余所有扇区按4KB为单位分配给文件存储内容。实际使用的效率如何w25q32是4MB容量扇区数1024个假设预留256个扇区做目录和超级块数据区就有768个扇区也就是3MB空间按一个日志文件10KB算能存300多份独立检查点。对嵌入式设备来说完全够用。目录区为什么不用链表而是固定大小这是刻意做的取舍。文件系统启动时需要快速遍历全部文件固定大小目录项可以直接用数组下标索引遍历一遍就是顺序读几个扇区的事性能和简单性都兼顾了。删除文件时只需要把目录项的第1位标记位改成已删除不需要立刻回收数据扇区回收留到垃圾清理阶段统一做。2.2 磨损均衡不能让某一扇区独自加班磨损均衡是NOR Flash文件系统和普通磁盘文件系统最大的区别点。SPIFS采用了一种简单有效的动态均衡策略每个数据扇区头部额外存一个4字节的擦除计数分配新扇区时遍历数据区找出擦除次数最少的扇区优先使用。这套策略本质上是把擦写压力摊开避免热点扇区提前报废。实际测试中持续写日志的情况下各扇区擦写次数的标准差能控制在平均值的20%以内。对于日志类应用已经足够。但动态均衡有个盲区如果某个文件长期不被修改它的扇区擦除次数会一直偏低而频繁写入的扇区擦除次数持续上涨。为了补齐这个缺口SPIFS在启动时和空闲时各做一次静态均衡扫描把长期不动的冷数据搬到擦除次数较低的扇区腾出热点区域给频繁写入的文件。这个搬运动作虽然耗时但只在空闲时触发不影响正常运行。2.3 掉电安全目录更新留后手掉电安全问题我栽过好几次跟头。最初版本是直接修改目录项结果有次测试时在写入目录项过程中断电重启后整个目录区的CRC校验全乱所有文件全部丢失。后来参考了嵌入式系统常见的双缓冲思路目录区保存两份目录表主目录区在扇区1~N备份目录区紧跟着主目录区。每次更新目录时先同步修改备份区校验成功后再修改主目录区。这样做有两个好处一是主目录区损坏时文件系统能自动回退到备份区二是掉电窗口大大缩短因为只有当备份区写入完成、主目录区写入前的瞬间断电才会出问题。这样最坏情况下只是丢失一次文件操作不会导致整个文件系统不可用。文件写入流程也遵循先写数据后更新目录的日记账原则。spifs_write先把数据写进空闲数据扇区数据落盘并校验通过后才更新目录项的文件大小和起始扇区号。万一断电数据扇区里会有孤立的数据碎片但目录项一致性没有被破坏文件系统依然可用。孤立碎片在垃圾回收时统一清理相当于给日志系统留了一条恢复路径。3. 从底层驱动到API实现把设计落到代码3.1 底层三个函数读、写、擦SPIFS不依赖特定厂商的SDK只要底层提供三个最基本的操作原语int spifs_hal_read(uint32_t addr, void *buf, uint32_t len); int spifs_hal_write(uint32_t addr, const void *buf, uint32_t len); int spifs_hal_erase(uint32_t addr, uint32_t len);这三个函数直接对接w25qXX的SPI驱动。读操作走0x03命令写操作走0x02页编程命令擦除走0x20扇区擦除命令。关键点在底层驱动里要处理好两个细节发送写命令前必须发送0x06写使能指令否则Flash直接忽略写入写操作前还要轮询状态寄存器等上一次擦除或写入彻底完成后才能进行下一次操作。有一个容易忽略的坑是如果使用DMA传输写操作完成后DMA可能已经返回但Flash还在内部编程。此时读状态寄存器忙标志位仍然是1必须等Flash内部编程结束才能进行下一步。我在调试时遇到过一次诡异现象写入后立即读取回读数据一直是0xFF排查半天发现是没等忙标志位释放就开始读了。3.2 核心API结构mount/open/write/readSPIFS对外暴露的API刻意设计得很精简保持和标准C文件操作相近的语义方便移植typedef struct { const char *name; // 文件名 uint32_t start_sector;// 起始数据扇区 uint32_t size; // 文件大小字节 } spifs_file_info_t; int spifs_mount(void); int spifs_open(const char *name, spifs_file_info_t *info); int spifs_create(const char *name); int spifs_write(const char *name, const void *data, uint32_t len); int spifs_read(const char *name, void *buf, uint32_t len); int spifs_delete(const char *name); int spifs_sync(void);这里没有句柄概念直接以文件名作为操作对象对大多数日志和配置场景已经够了。write接口在内部自动处理跨扇区拼接、页缓存flush和目录更新用户不需要关心文件对应哪个扇区。spifs_sync的语义和嵌入式Linux的sync类似强制把当前所有缓存写入最终位置。这个函数在掉电保护里很关键写完一批日志后调用一次sync能保证数据在掉电时不丢失。3.3 一个实操例子记录一条传感器日志以典型的温湿度记录为例假设要每秒记录一组数据到/sensor.log#define LOG_INTERVAL_SEC 1 typedef struct { uint32_t timestamp; int16_t temperature; uint16_t humidity; } sensor_record_t; void sensor_log_task(void) { sensor_record_t rec; spifs_mount(); while (1) { rec.timestamp get_timestamp(); rec.temperature read_temp(); rec.humidity read_humidity(); spifs_write(/sensor.log, rec, sizeof(rec)); spifs_sync(); delay_ms(LOG_INTERVAL_SEC * 1000); } }每条记录8字节每秒写一次一天产生691200字节大约169个扇区。在w25q64上即使不做磨损均衡单扇区写入次数也远低于寿命上限。但有了磨损均衡和sync机制整块Flash的寿命能稳定跑好几年。要特别注意spifs_sync不能每条记录都调用。我在初版代码里开了sync结果每秒都触发一次全量目录更新Flash磨损明显加快。后来改成每10条记录sync一次配合数据区页缓冲寿命提升了近一个数量级。这个细节是实测出来的不是拍脑袋定的。4. 容量适配w25q32/w25q64/w25q128怎么自动适配4.1 容量检测与布局计算不同型号的w25qXX容量和扇区数不一样。w25q32是4MB1024个4KB扇区w25q64是8MB2048个扇区w25q128是16MB4096个扇区。SPIFS在mount阶段会通过读取Flash的JEDEC ID来识别芯片容量然后动态计算存储布局。布局计算的核心是确定目录区大小。公式很简单目录区扇区数 ceil(最大文件数 * 32字节 / 4096字节)比如最多支持64个文件那么目录区就是64 * 32 / 4096 0.5向上取整为1个扇区再加一个备份区总共2个扇区。如果最大文件数提升到256个目录区就需要2个扇区备份区也跟着翻倍。这部分参数全部放在超级块里mount时按实际值分配内存。4.2 实际适配时要注意的参数配置参数我整理了一张速查表方便大家对照使用芯片型号容量扇区数常用目录区扇区数适用场景w25q324MB10242~4小型配置存储、启动日志w25q648MB20484~8传感器日志、OTA暂存w25q12816MB40968~16大容量数据记录、固件多版本备份这些数值不是拍脑袋定的我实际测试过不同参数组合下的内存占用和性能表现。目录区太大浪费Flash空间太小则文件数受限。256个文件的配置4个目录扇区就绰绰有余RAM占用反而由文件缓冲决定每多开一个文件缓冲就多占几十字节SRAM。5. 实测验证与踩坑记录5.1 掉电测试怎么做才靠谱掉电测试是文件系统验证里最重要的一环也是最容易被忽略的一环。我试过最粗暴也最有效的方法用一个继电器控制开发板电源写一个死循环脚本不断执行写入文件-sync-读取校验然后随机烧断继电器重启后检查文件系统能否正常挂载、文件能否完整读回。这套测试跑了200多次最终暴露了几个问题。其中最典型的是在目录区更新过程中断电备份区完好但主目录区损坏重启后SPIFS能回退到备份区但会丢失最近一次文件变更。这个属于设计上可接受的损失只在极端断电窗口下发生。还有一次意外发现是在page program写入过程中断电Flash内部编程电路可能会产生意外状态导致该扇区后续可读但不可写。处理方法是mount时对每个扇区做一次空写测试把异常扇区标记为坏块从分配表中剔除。这个机制虽然简单但在工业现场非常有用。5.2 常见问题速查表把这段时间积累的排障经验整理成速查表供同样在搞Flash文件系统的朋友参考问题现象可能原因排查手段挂载失败magic数错误Flash初始化失败或第0扇区数据被破坏用调试工具读取第0扇区原始内容确认SPI读写命令是否正确写文件返回成功但重启后文件消失目录项未刷新sync逻辑没生效检查所有路径是否在close前调用了sync确认是否提前擦除了数据扇区某个扇区擦除次数异常偏高动态磨损均衡失效新扇区分配逻辑有bug打印所有扇区擦除计数观察分配热点SPI读取偶尔出现0xFF时钟频率过高或Flash状态寄存器轮询时机不对降低SPI频率确认写使能和忙标志位轮询的顺序文件系统容量急剧减少垃圾回收不及时删除文件后数据扇区未回收查看触发垃圾回收的条件确认是否被写操作阻塞大文件写入时进度极慢频繁跨扇区目录更新导致擦除操作过多检查页缓冲大小是否小于256字节减少sync调用频率这些坑很多是遇到问题后才慢慢总结出来的。比如文件系统容量急剧减少这个问题有一次在客户现场跑了一周文件系统突然报错空间不足查下来是删除的日志文件只改了目录项数据扇区压根没有回收。垃圾回收的触发条件最初设置得太保守后来改成当空闲扇区低于10%时强制执行一次回收这个问题就彻底消失了。最后再分享一个我实测下来的经验无论你的文件系统设计得多完善给Flash供电的电源质量一定要保证。SPI NOR Flash对电压波动很敏感电压跌落会导致擦写时序异常进而产生坏块或数据损坏。我用带掉电检测的电源模块后原来偶尔出现的莫名其妙文件损坏问题基本绝迹。这个经验不是从任何文档里看到的是实实在在踩过坑之后才知道的。本文还有配套的精品资源点击获取
返回列表