☰
Zynq裸机实战:LittleFS在NAND Flash上的移植与掉电安全设计
2026/10/3 7:03:44 网站建设 项目流程

最近在Zynq-7020上做项目,ARM核跑的是裸机程序,需要把运行日志、设备配置参数、还有几份固件镜像存下来。最开始方案是SD卡,但工业现场环境差、振动大,SD卡座和金属触点总出问题,最后决定换NAND Flash,文件系统则选了ARM开源的LittleFS。这套组合折腾了大概两周,把软硬件相关配置都摸了一遍,踩了不少坑,也积累了不少经验。这篇文章把完整过程写下来,给后面做类似方案的同行一个参考。

可能有人会问,Zynq本身能跑Linux,直接挂UBIFS不是更省事吗。没错,如果产品上跑的就是Linux,那UBI/UBIFS是更成熟的选择。但不少Zynq项目是裸机加RTOS的轻量方案,或者只在启动早期需要管理一小块存储,这时候为了存几个配置文件就引入完整的内核MTD驱动栈,实在有点重。LittleFS的出现正好补了这个位置:掉电安全、损耗均衡、低RAM占用,跑在NAND上完全够用。这篇博文会从硬件选型讲到软件移植,再到实际调试中遇到的各种坑,尽量把“为什么”也说清楚,而不是只给一份能跑的代码。

如果你正准备在Zynq上做类似方案,或者正在为裸机环境下的NAND存储发愁,这篇文章应该能帮你省下不少时间。

1. 为什么在Zynq上折腾LittleFS+NAND Flash

1.1 先说结论:这套组合解决什么问题

LittleFS加NAND Flash,本质上解决的是“没有操作系统文件系统栈”的嵌入式环境里,如何可靠地存储和读取数据。NAND Flash容量大、单位成本低、写入速度快,适合存日志文件、配置参数、固件备份、算法模型这些动辄几MB到几百MB的数据。

而LittleFS是一个专为嵌入式设计的文件系统,它在设计上把掉电安全放在了第一位。数据写入过程中突然断电,不会把整个文件系统搞挂,最多丢失最后几次操作,重启后文件系统还能正常挂载。这一点在工业设备、电力终端、车载设备里非常关键,因为现场不可能每次都优雅关机。

组合起来的效果就是:裸机或者RTOS环境下,也能拥有一个类似“小型SD卡文件系统”的体验。我用这套方案在Zynq上实现了固件备份和日志落盘,稳定性测试跑了几百次掉电,文件系统没有出现过一次不可恢复的损坏。

1.2 NAND Flash绕不开的三个坑

NAND Flash和NOR Flash、SD卡的最大区别,藏着三个绕不开的坑。

第一个坑是“只能整块擦除”。NAND的物理结构分为页和块,页是读写的最小单位,块是擦除的最小单位。一个块里通常有64页、128页或者256页,你要修改任意一页里的数据,就必须先把整个块擦掉,再重新写入。这就好比你要改一本书里的一页,得先把整本书撕了重印,听起来很离谱,但这就是NAND的物理特性。

第二个坑是“天生带坏块”。NAND出厂的时候就可能有坏块,而且用着用着还会产生新的坏块。SD卡和eMMC里都有主控芯片在做坏块管理和逻辑地址映射,但裸NAND没有,所有坏块管理逻辑都得自己写,或者交给文件系统处理。

第三个坑是“数据会翻转”。NAND存储单元存在电荷泄漏、读干扰、写干扰等问题,存储的数据位有可能随机翻转,0变1、1变0。所以必须配ECC校验,小到每512字节配几个校验字节,大到硬件引擎实时纠错。如果没有ECC,文件系统元数据哪怕只错了一个bit,整个目录结构都可能崩掉。

这三个坑都直接影响了文件系统的选择。普通的FAT文件系统在NAND上跑,很快就会发现效率低、损耗大、掉电容易坏。LittleFS从设计层面就考虑了这些限制。

1.3 LittleFS凭什么能扛住掉电

LittleFS的核心设计是“写时复制”加“元数据双重校验”。写入新数据的时候,不直接覆盖旧数据,而是写到新的块里,再通过元数据更新指向新块。掉电了怎么办?旧数据的元数据还在,文件系统能回滚到上一个完整状态,不会出现半个文件覆盖这种中间态。

元数据本身也做了冗余存储。LittleFS会把关键的目录信息、文件信息同时存两份,一份损坏了,能通过另一份恢复。再加上内部实现了简单的损耗均衡,每个块的擦写次数会尽量平均分布,不会总盯着几个块使劲写。

这些特性对NAND非常重要。NAND的擦写寿命虽然比NOR低,但SLC类型的NAND少说也有几万到十万次擦写寿命,配合LittleFS的损耗均衡,正常产品生命周期里基本不用担心Flash被写穿。

不过我在这里也要说句公道话:LittleFS不是万能的,它适合单线程或简单多任务的嵌入式场景,如果要做多线程高并发访问,性能会明显下降。但用在Zynq裸机环境下,完全够用。

2. 硬件层:Zynq平台NAND Flash选型与连接

2.1 Zynq支持的NAND Flash型号到底有哪些

Zynq-7000系列有两个可以接NAND Flash的通道。一个是PS端的静态存储控制器,专门用于并行NAND接口;另一个是PS端的SPI控制器,可以接SPI接口的NAND Flash。

先说并行NAND。Zynq的硬件NAND控制器兼容ONFI标准,主要支持SLC类型的并行NAND,数据位宽可以是8位或16位。根据UG585的说明,Xilinx在参考设计里验证过的型号主要集中在Micron、Spansion、Toshiba这几家。我自己用过并且确认能正常工作的有:

  • Micron MT29F2G08ABAEA,256MB,2KB页,64页/块
  • Micron MT29F4G08ABADA,512MB,2KB页,128页/块
  • Spansion S34ML02G100TFI00,256MB,2KB页
  • Toshiba TC58NVG1S3HTA00,256MB,2KB页
  • Winbond W29N02GVSIAA,256MB,2KB页

这些型号都是市场上很常见的工业级SLC并行NAND,驱动和FSBL里都有对应的兼容参数,接上去基本都能识别。

再说SPI NAND。Zynq的SPI控制器只是通用的主机控制器,不是专门的NAND控制器,但不妨碍通过SPI协议访问SPI NAND。常见的型号有Winbond W25N01GV、W25N02KV,GigaDevice GD5F1GQ5、GD5F4GQ4,Macronix MX35LF1GE4,Micron MT29F2G01等。SPI NAND的好处是引脚少、PCB布线简单,缺点是读写速度受限于SPI总线频率,而且数据校验要么用芯片自带ECC,要么自己用软件算,Zynq的SPI控制器做不了硬件ECC。

这里插一句,我在选型时最终选了并行NAND,主要原因是Zynq的SMC控制器自带硬件BCH ECC引擎,能节省CPU开销,可靠性也更有保障。如果你的产品对板上空间和走线要求很高,那SPI NAND也是可以做的方案,只是后面要在软件上多下功夫。

2.2 并行NAND与SPI NAND,差别比想象中大

很多人一开始觉得SPI NAND接线少、更好做,但我实际对比下来,发现这个选择没有那么简单。下面这个表是我整理的对比项,基本涵盖了我做选型决策时关心的问题:

对比项并行NANDSPI NAND
引脚数量8位数据线加控制线,约20根左右6根左右(CS、CLK、DI、DO、WP、HOLD)
最大接口速度并行总线,几十MB/s级别SPI单线,约50MHz到104MHz,实际2-5MB/s
硬件ECC支持Zynq SMC控制器支持BCH引擎多数靠内置ECC或软件算法
坏块管理需要自己处理或借助控制器辅助芯片常内置部分坏块信息,仍需软件配合
软件复杂度控制器初始化稍复杂命令序列稍繁琐,但逻辑简单
PCB布局难度引脚多,走线占面积极简,适合小尺寸板卡
成本并行NAND价格透明,货源广近年价格也逐步降低

从这张表能看出来,并行NAND最大的优势是接口带宽和硬件ECC。SPI NAND最大的优势是布线简单、占用资源少。

我个人的建议是:如果Zynq的PS端IO和BANK数量充足、PCB空间不紧张,优先用并行NAND。如果板子尺寸很小、布线空间有限、对读写速度要求不高,SPI NAND也没问题。两种方案我都跑通过LittleFS,各有各的调法,后面软件部分会分别讲。

2.3 硬件设计中的几个关键引脚与时序

并行NAND的硬件连接,重点看这几个信号:

  • 数据线DQ0到DQ7(或DQ15),双向,传输命令、地址和数据
  • 命令锁存使能CLE、地址锁存使能ALE,用来区分总线上当前是命令还是地址
  • 芯片使能CE#、写使能WE#、读使能RE#,基本控制信号
  • 就绪/忙引脚R/B#,用于判断NAND控制器是否空闲,建议接上并配置中断或轮询
  • 写保护WP#,通常拉高或者由GPIO控制,软件初始化前可以拉低防止误写
  • 片选信号CE#,如果板上有多片NAND,需要区分片选

时序方面,NAND芯片手册里会给一堆时间参数,比如tRC、tWC、tWP、tRP、tADL等。Zynq的SMC NAND控制器初始化时会根据这些参数配置时序寄存器。如果只求能用,直接使用Xilinx SDK里提供的默认参数,大部分主流Flash都能正常跑。如果发现读写不稳定,就需要针对具体Flash调整这些时序参数。我遇到过一块老款Flash,默认时序下读ID正常、读写数据偶尔出错,把tRC和tWP调宽一档之后完全稳定了。

还有一点很容易忽略:NAND芯片的电源去耦。NAND在擦除和写入瞬间电流峰值比较大,VCC引脚旁边至少放一个1uF瓷片电容,靠近引脚摆放,必要时再并联一个0.1uF的高频去耦电容。电源不稳是NAND数据翻转的重要诱因之一。

3. 软件层:LittleFS在NAND上的移植与参数配置

3.1 拿到LittleFS源码后先做哪些事

LittleFS的源码托管在GitHub上,直接搜littlefs就能找到。核心文件就三个:lfs.h、lfs.c、lfs_util.h,加上lfs_util.c配置文件。整个库非常精简,全部编进来大概不到15KB的代码量,RAM消耗主要看缓存配置。

把几个文件加到工程里之后,第一件要做的事是确认几个宏:

  • LFS_NAME_MAX,文件名最大长度,默认255,如果内存紧张可以缩到64或128
  • LFS_FILE_MAX,文件最大大小,默认是2的31次方减1,一般不用动
  • LFS_ATTR_MAX,文件属性最大长度,默认1022

这几个宏会影响内部缓冲区大小和代码分支,但一般情况下用默认值就行,不用去动它们。真正需要认真配置的是struct lfs_config里的各种参数,这才是LittleFS适配底层存储的关键。

LittleFS官方提供了一个配置模板,在源码的README里能找到。但我们不能直接复制粘贴,因为NAND Flash的参数必须跟芯片的实际结构对上,否则就算挂载成功,读写性能也会很难看。

3.2 lfs_config参数逐个解读,别照抄

struct lfs_config里最核心的字段就这些,我结合自己项目的实际配置逐个说。

struct lfs_config lfs_cfg = { .read_size = 2048, .prog_size = 2048, .block_size = 131072, .block_count = 2048, .cache_size = 4096, .lookahead_size = 4096, .block_cycles = 500, };

read_size表示底层驱动单次读取的最小数据单元。在NAND上,应该设置为NAND页大小,常见的是2048字节。如果设得比页大小小,比如512字节,虽然也能工作,但每次读操作都要进入读命令、隔一段地址、读取数据这种低效路径,性能损失明显。

prog_size是单次写入的最小数据单元,在NAND上同样应该等于页大小。这一点极其重要。NAND页编程有个特点:数据必须从页内某个固定位置开始写入,通常就是从页首开始,而且不能在同一页里反复写入不连续的碎片数据。把prog_size设成比页大小更小,比如256字节,那么要写入256字节时,驱动不得不先把整页读回来,在SRAM里把新数据合并进去,再擦除一整页重新写入,这就是一次读改写操作,性能会大幅度下降,Flash损耗也翻倍。所以在NAND上,prog_size老老实实等于页大小。

block_size是擦除单元的大小,对应NAND的块大小。比如一块芯片,每页2048字节,每块64页,那么block_size就是2048乘以64,等于131072字节即128KB。这个值可以从芯片手册里查,数据手册会明确写Block Size: 128KB或者类似字样。

block_count就是整个NAND区域里可用于LittleFS的块数量。注意,如果NAND有坏块,这个值应该等于可用的好块数量,而不是芯片标称的总块数。

cache_size是LittleFS内部使用的RAM缓冲大小,它必须大于等于read_size和prog_size,而且最好是两者的整数倍。我这里设成了4096,因为Zynq的L1/L2 cache line大小是32字节,4096对齐也能保证DMA操作时缓存一致性处理简单。如果你内存紧张,设成2048也能跑,但连续读写性能会差一些。

lookahead_size用于损耗均衡的预扫描窗口,是8的倍数即可,一般设置为cache_size的整数倍。4096这个值在多数NAND容量下表现都不错。如果Flash容量很大,比如512MB以上,可以适当增大到8192或16384,以提升损耗均衡的全局视野,但RAM开销也会相应增加。

block_cycles是最容易被忽略的参数,它表示一个块在被重复擦写的次数达到这个值时,LittleFS会触发强制损耗均衡,主动把该块的数据迁移到别的块上。这个值设得太小会频繁触发迁移,写入性能下降;设得太大则损耗均衡不够积极。对SLC NAND芯片,寿命一般是十万次擦写,设500到2000都合理。如果用的是MLC或TLC NAND,寿命只有几千次,这个值建议设到500以下。

read_size、prog_size、block_size、block_count这四个参数如果设错了,最典型的症状就是文件系统挂载后读写数据错乱,或者格式化时直接失败。所以我的建议是,先把芯片手册里页大小、块大小这两项查清楚,再填参数。

3.3 封好read/prog/erase,让NAND驱动和LittleFS说上话

LittleFS通过lfs_config里的四个函数指针直接访问底层Flash。无论你用的是并行NAND还是SPI NAND,最终都要封装成这四种操作。

先看读取接口。LittleFS会传入block、off、buffer、size四个参数,分别是块号、块内偏移、目标缓冲区和读取长度。我们的NAND驱动需要根据block和off计算出物理页号,然后一次读一页或连续多页。

static int nand_flash_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) { uint32_t pages_per_block = c->block_size / c->prog_size; uint32_t page = block * pages_per_block + off / c->prog_size; uint32_t offset_in_page = off % c->prog_size; uint32_t remaining = size; uint8_t *p = (uint8_t *)buffer; while (remaining > 0) { uint32_t chunk = c->prog_size - offset_in_page; if (chunk > remaining) chunk = remaining; int ret = nand_page_read(page, offset_in_page, p, chunk); if (ret != 0) return LFS_ERR_IO; p += chunk; remaining -= chunk; page++; offset_in_page = 0; } return LFS_ERR_OK; }

写入接口nand_flash_prog的逻辑和读取类似,但有一点需要额外注意:NAND页编程要求写入长度是对齐的,而且不能跨越页边界。所以在实现里要确保每次写入的起始偏移和长度都与NAND页对齐。LittleFS内部设计本来就是在prog_size边界上发起的,但驱动层面做一次防御性检查没有坏处,非法参数直接返回LFS_ERR_IO比在硬件上跑飞要好。

擦除接口很简单,输入一个块号,底层直接发擦除命令,等待擦除完成。擦除完成后,建议读回该块第一页的第一个字节,确认已经是0xFF,防止擦除命令实际没有生效。这个检查不会花太多时间,但能避免很多诡异问题。

static int nand_flash_erase(const struct lfs_config *c, lfs_block_t block) { int ret = nand_block_erase(block); if (ret != 0) return LFS_ERR_IO; // 可选:读回验证擦除是否成功 uint8_t check[4]; ret = nand_page_read(block * (c->block_size / c->prog_size), 0, check, 4); if (ret != 0 || check[0] != 0xFF || check[1] != 0xFF) { return LFS_ERR_IO; } return LFS_ERR_OK; }

sync接口在LittleFS里表示缓存中的数据要落盘。对裸NAND来说,如果前面几个接口都是同步完成的,即函数返回时数据已经真正写入Flash且等待硬件不再忙,那sync直接返回0就行。但如果你的驱动用了DMA异步方式,就必须在sync里等待DMA传输完成、并且把缓存数据刷到Flash后再返回。

这里特别强调一下Zynq上的缓存一致性。Zynq的ARM Cortex-A9带有L2 cache,当DMA和CPU在访问同一块内存时,如果CPU写了几行数据,DMA去读时可能读到旧数据;反过来DMA把数据写进内存后,CPU再去读也可能读的是旧缓存。所以DMA操作前后必须手动执行Xil_DCacheFlushRange和Xil_DCacheInvalidateRange,否则文件系统读写会出现那种时好时坏、看不出规律的错误。这个坑我栽过不止一次。

3.4 坏块与ECC:哪些该让硬件管,哪些该让文件系统管

很多人刚接触LittleFS时有个误解,以为文件系统自带坏块管理,就能完全不管NAND坏块了。实际不是这样。LittleFS能处理的坏块,是那种在擦除或写入时即时报错的坏块;它管理不了“读出来数据是错的但硬件没报错”的情况。所以ECC校验必须在NAND驱动层完成,确保提交给LittleFS的数据在经过校验和纠错后是正确的。

如果你的Zynq接的是并行NAND,SMC控制器自带BCH ECC引擎。在初始化时使能ECC,之后用DMA读写页数据时,硬件会自动完成ECC计算和校验,对于ECC可纠正的bit翻转,硬件直接纠正,读取出来的数据是正确的;对于ECC不可纠正的错误,控制器会把状态寄存器里的错误标志位置位,驱动层需要检查这个标志并返回错误,让LittleFS知道这个块可能坏了。

如果你的Zynq接的是SPI NAND,情况稍微麻烦一些。多数SPI NAND芯片内部自带ECC引擎,比如Winbond的W25N系列就有内部ECC。手册里有个状态寄存器位,专门用来指示当前读出的页有没有发生可纠正错误。驱动在读页命令后,需要额外读一下状态寄存器,如果发现ECC错误,要么自己纠正返回的数据,要么向上层报告错误。更保险的做法是同时开启芯片内部ECC,并在驱动层做一次简单的数据校验,双保险。

坏块信息的处理,取决于你底层的设计思路。一种思路是把坏块“隐藏”起来,在NAND驱动内部维护一张逻辑块到物理块的映射表,初始化时扫描全片,把出厂坏块剔除掉,后续给LittleFS呈现的全是好块。这种方案的优点是LittleFS层不用感知坏块,缺点是驱动要多占一块RAM放映射表,而且新产生的坏块处理逻辑要自己定制。

另一种思路是让LittleFS直接面对坏块。NAND驱动在擦除块时如果返回错误,或者ECC错误标志置位,就返回LFS_ERR_IO,LittleFS会把这一个块视为坏块,把上面的数据迁移出去,然后标记为不可用。两种方案我都试过,实际项目中我采用了第二种:把坏块信息交给LittleFS去管理,驱动只负责准确报告错误。这样代码量少,维护也方便。

有一点需要说清楚,无论是哪种思路,绿坏块的“出厂坏块标记”都要在驱动初始化时做一次全片扫描。并行NAND的坏块标记位置通常在每个坏块的第一个页的OOB区第一个字节,值如果是0x00就表示出厂坏块。SPI NAND每个块也有类似标记。如果初始化时不做扫描,后续文件系统访问到这些坏块时,直接报错或数据错误,问题会更难定位。

4. 实操过程:在Zynq上从零跑到文件系统读写

4.1 环境准备与NAND控制器初始化

我用的是Xilinx Vivado加SDK这套老牌工具链。Vivado里先创建一个Zynq PS配置,在MIO配置里把NAND接口使能。如果用的是并行NAND,在PS的SMC控制器配置里勾选NAND即可。如果用的是SPI NAND,则在SPI模块里选择一个支持的MIO引脚组合。

硬件导出到SDK后,BSP会自动携带xnandps驱动。应用工程里包含xparameters.h,里面有NAND控制器基地址和设备ID。第一步先查找配置并初始化控制器:

#include "xnandps.h" static XNandPs g_nand; static int nand_controller_init(void) { XNandPs_Config *cfg; cfg = XNandPs_LookupConfig(XPAR_XNANDPS_0_DEVICE_ID); if (cfg == NULL) { return -1; } return XNandPs_CfgInitialize(&g_nand, cfg, cfg->BaseAddress); }

初始化之后,需要检查Flash ID,确认BSP里的时序参数跟当前芯片匹配。XNandPs_GetDeviceId或者直接读取ID寄存器都行。如果读到ID全0xFF,先别急着怀疑驱动,检查一下引脚、供电和片选信号,八成是硬件没通。如果ID能读出来但跟芯片手册对不上,就要重点看时序配置了。

并行NAND使用硬件ECC时,要在驱动初始化后显式使能BCH引擎并设置纠错能力:

XNandPs_SetECCEnable(&g_nand, XNANDPS_ECC_TYPE_BCH_8);

这里的XNANDPS_ECC_TYPE_BCH_8表示采用BCH算法,可纠正8bit错误。如果Flash页内有更多数据需要保护,可以选更高的等级,但要注意,纠错能力越强,OOB中占用的校验字节也越多,而且硬件计算时间会稍微变长。对工业产品来说,BCH-8已经能满足绝大多数场景需求。

如果你用的是SPI NAND,初始化流程就变成了SPI控制器初始化,然后通过命令序列读取Flash ID。SPI NAND需要按照芯片手册的命令集来操作,比如Winbond的W25N系列,读ID命令是0x9F,读状态寄存器命令是0x05,读页命令是0x13加0x03的组合,页编程命令是0x02加0x10的组合。这些命令序列要封装成nand_read_page、nand_write_page、nand_erase_block三个基础函数,对应前面3.3节里的底层调用。

4.2 格式化与挂载

在把LittleFS接上去之前,要先把文件系统格式化和挂载的流程走通。LittleFS的接口很简洁,格式化一个尚未初始化的NAND分区,只需要通过lfs_format函数:

static lfs_t g_lfs; static int littlefs_init(void) { int err; // 这是前面定义好的lfs_config,参数已按芯片实际情况填好 err = lfs_format(&g_lfs, &lfs_cfg); if (err < 0) { // 格式化失败 return err; } err = lfs_mount(&g_lfs, &lfs_cfg); if (err < 0) { return err; } return 0; }

初次格式化时,如果NAND分区里已经有坏块,LittleFS会跳过那些坏块。格式化完成后,就可以通过lfs_mount挂载了。挂载成功后,文件系统的基本接口如lfs_mkdir、lfs_open、lfs_write、lfs_read就能直接使用。

但是这里有一个实际项目中经常遇到的细节:如果每次上电都lfs_format,那文件系统会被反复重建,之前存的数据全部丢失。正常流程应该是先lfs_mount,挂载失败再考虑格式化。之前存了重要数据,挂载失败又自动格式化,等于把救命数据清掉了,这样不太合理。实际中我推荐这样处理:先尝试挂载,如果挂载失败返回LFS_ERR_INVAL或者类似错误,再提示上层,由业务逻辑决定是格式化还是重新烧录固件,而不是在初始化函数里默默格式化。

挂载后再写一个简单的版本信息文件,内容类似“v1.0.0”,每次启动时读出来判断文件系统是否正常工作。这个方法简单实用,比看打印日志更直接。

4.3 性能测试与参数调优

文件系统能挂载能读写之后,首先跑一个简单的读写测试,确认基本功能没问题,再做性能测试。

我写了个简单的性能测试,大概逻辑是这样:

  • 连续写入一个2MB的文件,记录耗时,计算平均写入速度
  • 再把这个文件读回来,逐字节校验,记录读取速度
  • 在文件系统里创建和删除100个小文件,用脚本或循环计时,统计每秒能完成多少次创建删除操作

第一次跑测试时,我这边并行NAND的连续写入速度大约在2.8MB/s左右,读取速度约6.1MB/s。这个速度对日志存储和固件备份完全够用。

如果想提高连续写性能,重点关注以下几点。

一是cache_size。LittleFS的缓存越大,单次调用底层驱动的写入块就越大,写放大越小,性能越好。把cache_size从2048提到4096,连续写性能大概能提升20%到30%,代价是多占2KB RAM。

二是block_cycles。如果设得太小,比如100,每次写入几十KB就会触发一次块迁移,连续写性能会被垃圾回收拖累。设到500以上,性能明显平稳。

三是底层的nand_page_write实现。如果是并行NAND,建议使用控制器自带的DMA模式写整页,不要用CPU逐字节操作寄存器的方式。用DMA一次性搬2048字节,跟CPU写循环相比,速度差距能到3倍以上。

四是NAND的写等待时间。每写一页,硬件要等一段时间让内部电荷稳定下来,轮询R/B引脚或状态寄存器。有些早期实现偷懒,写一页后固定delay一个较大的值,比如等1毫秒,这对数据可靠性和性能都是双重浪费。正确做法是写完页后立刻查询R/B引脚翻转,或者查询状态寄存器就绪位,硬件一就绪就立刻处理下一页。

4.4 创建目录结构和业务代码对接

文件系统跑通后,就是要和业务代码对接。一个比较稳妥的目录规划是:

  • /config目录,存放设备配置参数,每个参数一个文件,更新时先写临时文件再重命名,配合LittleFS的掉电安全特性,几乎不会出现配置写一半的问题
  • /log目录,存放运行日志,按天滚动生成,方便后续日志分析
  • /firmware目录,存放固件升级包,升级时先校验整个文件的CRC32,校验通过才覆盖当前版本

这里强烈建议在固件备份场景下使用“双分区交替写”的策略。比如有两个固定文件路径,firmware_a.bin和firmware_b.bin,升级时先写其中一个,写入完成后在元数据文件里记录当前有效版本。启动时根据元数据选择正确的固件文件。这种方式配合LittleFS的掉电安全特性,基本能做到“即使升级过程中掉电,设备还能用旧固件启动”。

LittleFS接口还有一个很实用的属性功能,lfs_setattr可以给文件挂自定义属性。我在日志文件上挂了一个“写入时间戳”属性,用来做日志轮转判断,省去了在文件内容里解析时间戳的麻烦。别小看这个功能,用好了能省不少解析代码。

5. 踩坑实录:常见问题与排查技巧

5.1 挂载失败,格式化也报错

现象:首次上电跑lfs_format直接返回错误,或者执行完lfs_mount返回LFS_ERR_INVAL。

排查步骤,优先级从高到低:

第一步,确认lfs_config里的block_size跟芯片实际的块大小一致。很多NAND芯片虽然页大小是2KB,但块大小可能是64页或128页,导致块大小是128KB或256KB。如果块大小填错,LittleFS计算块地址时就会错位,格式化都做不了。检查办法就是翻开芯片数据手册,找到“Block Size”那一项。

第二步,确认block_count没有超出真正的块数量。如果芯片是512MB,块大小128KB,那应该是4096块。有些人拿到512MB芯片却填了2048,等于只格式化了一半容量,反过来填多了也会出问题。

第三步,确认初始化时有没有扫描坏块。如果驱动没有做坏块扫描,那么block_count里可能包含了坏块,LittleFS第一次格式化时大概率在这个块上报错。需要把坏块从block_count里扣除。

第四步,查看NAND读到的是不是还在返回0xFF。如果所有读操作都返回0xFF,大概率是片选、供电或者时序配置问题,跟文件系统没半毛钱关系。这时候要回到最底层,先跑一个裸读Flash ID的例程,把硬件链路确认好再谈文件系统。

5.2 写入正常、读出来乱码

现象:文件系统能挂载,可以写文件,但读回来的数据跟写入的不一致,有时是固定字节错位,有时是随机bit翻转。

这种问题在并行NAND上,十有八九是ECC没有使能或者配置不对。检查XNandPs_SetECCEnable是否真正调用了,调用时机对不对。有些情况下控制器的ECC需要配合DMA使用,如果用的是PIO模式读写,硬件ECC引擎可能根本没参与计算。

另一个高发原因是DMA缓存一致性问题。我在4.1节说过,DMA前后必须刷cache和失效cache。如果写方向漏了Xil_DCacheFlushRange,那DMA搬运的是CPU上一轮留在缓存里的旧数据;如果读方向漏了Xil_DCacheInvalidateRange,那CPU读出来的可能是缓存里的旧数据而不是DMA刚搬运来的新数据。这种问题非常隐蔽,往往表现为“首次运行正常,反复读写后开始出错”,因为缓存里的状态在不断积累。

排查方法很简单:在每次底层读写前后都加上cache操作,再用稳定复现的测试遍历验证。Zynq上推荐统一使用Xil_DCacheFlushRange和Xil_DCacheInvalidateRange,不要依赖SDK自动生成的优化代码。

SPI NAND上出现乱码,优先检查单片机端的SPI时钟极性和相位配没配对,以及NAND内部的状态寄存器有没有防写保护命令没关掉。Winbond家的W25N系列在上电后默认可能处于保护状态,必须先发送解锁命令,否则某些区域读正常、写异常。

5.3 掉电测试文件损坏

现象:实际断电测试几十次或者上百次后,文件系统出现文件打不开、目录列表不完整的情况。

第一反应不要怀疑LittleFS,先检查你的掉电硬件链路。NAND的供电和控制器供电是不是同一个电源网络?掉电时会不会出现电压跌落到某个中间区间,导致NAND正在擦除块时电源一下子拉掉,数据处于半擦除状态?LittleFS能做到的是文件系统结构不被破坏,但如果底层NAND因为电压问题产生了数据翻转,底层驱动又没检测出来,那任何文件系统都没办法兜底。

推荐在硬件上加一个掉电检测电路。用一个比较器监控主电源电压,当电压掉到阈值之下,立刻触发CPU的中断或让GPIO翻转,程序在中断里做紧急处理:停止写入、关闭DMA、等待当前操作结束,必要时把WP#拉低阻止后续误写入。这比单纯指望文件系统掉电安全要可靠得多。

第二个检查点是WP#引脚。如果WP#引脚没有被外部拉高,或者初始化时没有正确设置,NAND控制器在上电初期可能处于写保护状态。这种情况下,写入操作实际上没有真正落盘,但LittleFS可能已经认为写入成功了。掉电后自然什么都丢了。这个过程非常隐蔽,因为读写操作本身不报错。

第三个要点是把sync函数真正做到位。LittleFS的写操作默认不是每次lfs_write都立刻落盘的,需要lfs_file_sync或者lfs_unmount时才把缓存刷下去。如果你在测试里写完就马上断电,没有调用sync,那丢失最后一部分数据是符合预期的,不算bug。但如果文件系统结构都损坏了,那还是要往前查驱动。

5.4 性能远低于预期

现象:连续写速度很慢,只有几百KB/s,或者读写速度忽快忽慢,有明显的卡顿感。

我先怀疑的是prog_size是不是小于页大小。如果在驱动里把prog_size设成512字节,那每次写入都要做页面读改写,性能会被拖到极低。改回2048,立即提升一个数量级。

第二个常见瓶颈是lookahead_size和block_cycles搭配不当。如果Flash比较大,而lookahead_size很小,磨损均衡的视野就窄,某个区域满了之后频繁触发块迁移,写入速度就会周期性掉下去。把lookahead_size调大,同时把block_cycles调到合理区间,速度会平稳很多。

第三个坑是DMA传输粒度。有一次我测试并行NAND,发现连续写速度一直上不去,后来发现驱动里每次DMA只传了512字节。虽然NAND页大小是2048,但DMA被拆成了四次512字节传输,页编程变成了四次,每次都有额外命令开销和等待时间。后来改成整页一次性DMA传2048字节,速度才恢复正常。这条经验几乎不会出现在官方示例代码里,但影响非常大。

最后提一个老生常谈的经验:在写性能测试之前,先把芯片擦写过一遍,让整个Flash处于干净状态再测。如果Flash里老数据参差不齐,垃圾回收会频繁参与,测试出来的性能数据会非常难看,容易误导优化方向。

最后再分享一个我个人的体会。LittleFS加NAND Flash这套方案,在Zynq裸机或RTOS环境下,确实能把“大容量存储”这个短板补上,而且稳定性经过足够验证之后是可以放心上量产的。但前提是底层驱动要扎实,ECC要可靠,缓存一致性要处理到位,文件系统参数要理解后按芯片实际情况来配置,而不是随手抄一份网上配置。把这几件事做好,LittleFS会是你嵌入式项目里非常省心的一块基石。如果遇到这几种常见问题都排查不出结果,建议先回到最底层,把Flash的裸读写、ECC校验、掉电测试各拆开来验证,问题通常就藏在那几个不起眼的边界条件里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询