W25N01G SPI NAND坏块管理实战:从识别到跳过

发布时间:2026/10/28 15:33:12
W25N01G SPI NAND坏块管理实战:从识别到跳过 1. 一块SPI NAND让我在深夜两点重新认识了坏块两个字W25N01G这颗料做嵌入式存储的朋友大概率不陌生——1Gbit容量、SPI接口、标准ONFI之外的简化命令集常见于各类需要中等容量非易失存储又不想上eMMC的场景。我第一次用它是在一个数据记录设备上硬件同事把原理图甩过来的时候说就一个SPI Flash你按W25Q系列那套写就行。结果第一批样机跑了两周日志里开始零星出现读回数据全FF、写入后校验失败的情况换了几片板子现象还不一样。折腾到深夜才反应过来这玩意儿是NAND不是NOR它有坏块而且出厂就可能带坏块。这个认知差是很多从NOR Flash或者EEPROM迁移过来的开发者最容易踩的坑。W25Q系列NOR你几乎可以认为全片可用擦写十万次以内基本不用操心块管理但W25N01G是NAND工艺物理特性决定了它天生存在坏块厂商在出厂测试时就会标记一部分块为不可用使用过程中还可能新增坏块。如果你按NOR的思路直接线性寻址、不做任何坏块识别和跳过轻则数据丢失重则文件系统直接崩掉。这篇内容就是把我在这颗料上踩过的坑、验证过的方案完整梳理一遍。从怎么读坏块标记、怎么建立坏块表、读写时怎么跳过到实际代码里那些文档不会告诉你的细节。适合正在用W25N01G或者类似SPI NAND、已经能跑通基本读写但还没处理坏块问题的朋友。如果你还在纠结SPI时序能不能通那建议先把基础通信调通再回来看这篇。2. 先搞清楚W25N01G的坏块标记到底长什么样2.1 坏块信息存在哪备用区与第0页的约定NAND的坏块标记不像NOR那样有一个独立的寄存器告诉你第几块坏了。W25N01G的做法是每个块Block的第一个页Page 0的备用区Spare Area里有一个特定字节位置用来存放坏块标记。具体来说W25N01G每页2048字节主区加64字节备用区坏块标记位于备用区的第一个字节Spare Area的Column 0值非0xFF即视为坏块。这里有个关键细节标记只写在块内第0页的备用区不是每页都写。所以你检查坏块时只需要读每个块第0页的备用区首字节不需要遍历所有页。W25N01G总共1024个块每块64页如果你逐页检查那是65536次读纯属浪费按块检查只有1024次启动时一次性建表完全可接受。注意出厂坏块标记是厂商在产线测试时写入的值通常是0x00或其他非0xFF值。但不同批次、不同封装可能标记值不完全一致判断逻辑统一用不等于0xFF最稳妥不要假设一定是0x00。2.2 读坏块标记的命令链路不是直接读主区很多人第一反应是用普通的Page Read命令0x13去读第0页然后看备用区。这条路能走通但要注意W25N01G的读操作分两步先把页数据读到芯片内部的Cache Buffer0x13命令再从Cache Buffer读出0x03或0x0B命令。备用区的64字节在Cache Buffer里是紧跟在2048字节主区之后的所以你从Cache里读的时候地址偏移要算对。我实际用的方式是发0x13命令带目标页地址等待忙状态结束然后发0x03读命令起始地址设为2048即主区末尾连续读1字节拿到的就是备用区首字节。这里容易错的地方是地址位宽——W25N01G的页内地址是12位0到2111含备用区但很多驱动里地址是按16位传的如果你直接把2048塞进一个只处理低8位的函数里读出来永远是主区某个位置的数据看起来全是0xFF误判为全片好块。/* 读某块第0页备用区首字节判断坏块标记 */ uint8_t nand_read_badblock_mark(uint16_t block) { uint16_t page block * 64; /* 每块64页 */ uint8_t mark 0xFF; uint8_t cmd; /* 1. 页读到Cache */ cmd 0x13; spi_cs_low(); spi_write(cmd, 1); spi_write((uint8_t*)page, 2); /* 页地址注意字节序 */ spi_cs_high(); nand_wait_busy(); /* 2. 从Cache读备用区首字节偏移2048 */ uint16_t col 2048; cmd 0x03; spi_cs_low(); spi_write(cmd, 1); spi_write((uint8_t*)col, 2); spi_write(mark, 1); /* dummy byte视时序而定 */ spi_read(mark, 1); spi_cs_high(); return mark; }上面代码里那个dummy byte要不要加取决于你的SPI模式和芯片时序要求。W25N01G的Fast Read0x0B需要dummy普通Read0x03在部分时序配置下也需要一个空周期。我建议先用逻辑分析仪抓一次确认别凭感觉。2.3 为什么不能只信出厂标记运行期坏块的现实出厂标记只代表芯片离开产线时的状态。NAND在擦写循环中会逐渐产生新的坏块这是物理特性不是质量问题。W25N01G的标称擦写寿命是10万次每块但实际使用中如果某个块被频繁擦写比如你拿它当日志环形缓冲区它可能远早于标称寿命就出现编程失败或擦除失败。运行期坏块的识别逻辑和出厂坏块不同你不是去读标记而是在擦除或编程操作后检查状态寄存器的失败位。W25N01G的状态寄存器第0位OIP是忙标志第2位P_FAIL是编程失败第3位E_FAIL是擦除失败。一旦某次擦除或编程返回失败你就应该把对应块标记为坏块写入坏块表后续跳过。这里有个实操经验单次擦除失败不一定要立刻判死刑。NAND允许对同一块重试擦除有时候是时序或电压波动导致的偶发失败。我的做法是重试最多3次3次都失败才标记为坏块。但重试之间要加适当延时并且重新发一次Write Enable别连着猛发命令。3. 坏块表的设计放RAM还是放Flash这是个问题3.1 1024个块的位图RAM占用与启动扫描的权衡W25N01G有1024个块用位图表示坏块状态只需要1024/8128字节。这个尺寸放RAM里毫无压力所以最直接的做法是启动时扫描全片、建立128字节的坏块位图运行期所有读写都查这张表。扫描1024个块的第0页备用区按每次读操作几十微秒算总耗时大概几十到一百多毫秒对于大多数设备的上电初始化来说完全可以接受。但这里有个容易被忽略的问题如果你的设备要求极快启动比如上电后几十毫秒内必须开始记录数据这一百多毫秒的扫描可能就成了瓶颈。我遇到过一款手持设备用户按下电源键到屏幕亮起要求300ms以内存储初始化被压缩到50ms全片扫描根本来不及。这种场景下就得换策略把坏块表持久化到Flash的某个保留块里启动时只读那一块几十微秒搞定。3.2 持久化坏块表的存放位置与更新时机持久化坏块表的核心矛盾是坏块表本身也要存在NAND里而NAND有坏块你怎么保证存坏块表的那块是好的答案是不能保证所以要做冗余。常见做法是预留两个块专门存坏块表比如块0和块1启动时两块都读取校验通过且版本号较新的那份。如果两块都坏概率极低但理论上存在退化为全片扫描重建。更新时机上我倾向于运行期发现新坏块时先更新RAM表在安全下电流程或定期比如每写入1000次时刷入Flash。不要每次发现坏块都立刻写Flash因为写Flash本身可能触发新的坏块形成恶性循环。另外写坏块表的那两个保留块要纳入磨损均衡考虑不能一直只写块0。方案RAM占用启动耗时可靠性适用场景全片扫描建表128字节约80-150ms高实时反映启动时间宽松的设备持久化表扫描兜底128字节少量约1-5ms高需冗余快速启动设备纯持久化不扫描128字节约1ms中依赖表完整性极少见不推荐3.3 坏块表的数据结构别只存位图只存128字节位图够用但我建议多存几个字段魔数用于识别这是有效坏块表、版本号用于双备份选新、CRC校验防止读到半写状态的数据、以及坏块计数。这些额外字段加起来不到16字节但能在出问题时救你一命。我曾经遇到过掉电导致坏块表只写了一半的情况没有CRC的话启动时读到一张部分有效的表后果比没有表还糟糕——因为你会以为某些坏块是好块。typedef struct { uint32_t magic; /* 0x42424C4B BBLK */ uint16_t version; /* 每次更新递增 */ uint16_t bad_count; /* 坏块总数快速判断 */ uint8_t bitmap[128]; /* 1024块位图 */ uint32_t crc32; /* 前面所有字段的CRC */ } badblock_table_t;结构体对齐要注意别让编译器在magic和version之间插填充字节导致CRC计算范围和Flash里实际布局不一致。用__attribute__((packed))或者手动序列化都行我倾向手动序列化跨平台移植时少踩坑。4. 读写跳过逻辑不是简单加个if就完事4.1 逻辑地址到物理地址的映射层坏块跳过的本质是引入一层地址映射上层文件系统或应用看到的是连续的逻辑块号底层驱动负责把逻辑块号翻译成实际可用的物理块号。最简单的实现是顺序映射跳过维护一个从逻辑块到物理块的查找遇到坏块就顺延到下一个好块。但这里有个设计选择映射表是运行时算还是持久化如果只做跳过坏块、逻辑块紧凑排列那映射关系依赖坏块表每次地址翻译都要遍历位图找第N个好块1024个块遍历一次也就几十条指令性能可接受。如果做完整的逻辑到物理映射表1024项×2字节2KBRAM占用上去了但翻译是O(1)。对于W25N01G这种小容量芯片我建议用前者省RAM且实现简单。/* 逻辑块号转物理块号跳过坏块 */ int16_t logic_to_phys(uint16_t logic_block, const uint8_t *bitmap) { uint16_t phys 0; uint16_t count 0; for (phys 0; phys 1024; phys) { if (bitmap[phys 3] (1 (phys 7))) continue; /* 坏块跳过 */ if (count logic_block) return phys; count; } return -1; /* 逻辑块号超出可用范围 */ }这个函数每次调用都从头遍历最坏情况1024次循环。如果你在高速写入路径上频繁调用可以加个缓存记录上次翻译的logic_block和phys顺序访问时直接1。实际测试中加了这层缓存后地址翻译开销基本可以忽略。4.2 跨块写入的边界处理一个块写满了怎么办W25N01G每块64页每页2048字节主区所以一个块的主区容量是128KB。当上层连续写入跨越块边界时驱动必须处理当前块写满、下一块是坏块的情况。这里最容易出的bug是写到块末尾时计算下一页地址直接phys_block1结果撞上坏块数据写进了坏块的页里读回来全是错的。正确做法是在每次跨页、跨块时都重新做一次地址翻译。具体来说维护当前逻辑页号每次写入前把逻辑页号换算成逻辑块号块内页号再翻译物理块号。不要试图用物理地址递增的方式偷懒坏块的存在让物理地址不连续是常态。实操心得调试阶段我建议在每次跨块时打一条日志记录逻辑块号、物理块号、是否跳过。跑一轮全片写入测试看日志里跳过的位置是否和坏块表一致。这个笨办法帮我抓到过一个位图索引算错的bug——phys 3写成了phys 4导致每16个块才检查一次坏块中间8个块的坏块全被漏掉。4.3 擦除操作的坏块处理失败重试与降级擦除是NAND操作里最容易暴露坏块的环节。W25N01G的块擦除命令是0xD8发命令后等忙状态结束查状态寄存器E_FAIL位。如果失败按前面说的重试3次仍失败则标记坏块。但有个细节擦除失败后这个块里的数据状态是不确定的你不能假设它还是擦除前的数据也不能假设它已经擦干净。如果这个块之前存着有效数据擦除失败意味着数据可能部分损坏。所以对于存有关键数据的块擦除前应该先确认数据已经备份或不再需要。这也是为什么坏块管理要和上层的数据组织策略配合——如果你把文件系统元数据放在一个容易坏的块上擦除失败就是灾难。我的做法是把坏块率高的区域通过运行期统计标记为低可靠性区关键数据优先写到低坏块率区域。W25N01G总共1024块出厂坏块通常个位数到几十个运行期新增的坏块往往集中在被频繁擦写的区域这个统计有实际意义。5. 那些文档里不会写的坑时序、缓存与掉电5.1 读坏块标记时的忙等待陷阱W25N01G的页读命令0x13发出后芯片进入忙状态OIP位置1。很多驱动里用轮询方式等OIP清零但这里有个坑如果你在发0x13之后立刻发0x05读状态SPI总线上前一个命令的尾部可能还没处理完读到的状态是旧的。我遇到过读状态一直返回忙、实际芯片早就准备好了的情况原因是CS拉高的时机不对命令没被完整接收。稳妥的做法是发完0x13、拉高CS后先延时至少1微秒再开始轮询状态。轮询时每次读状态之间也要有间隔别用紧密循环猛读SPI时钟快的时候紧密轮询反而容易读到错误状态。另外W25N01G的忙状态最长可能持续几百微秒页读到几毫秒擦除超时阈值要设够别用固定循环次数用毫秒级超时。5.2 Cache Buffer的读写顺序约束W25N01G内部有一个204864字节的Cache Buffer页读是把Flash阵列的数据搬到Cache页编程是把Cache的数据写进阵列。这里的关键约束是你不能在读Cache的同时触发新的页读否则Cache内容会被覆盖。在坏块扫描阶段如果你为了提速用DMA连续读多个块的标记要确保每个块的页读到Cache和从Cache读标记严格串行中间不能插入另一个页读命令。我最初的扫描代码为了快发完块0的0x13后没等它完成就发了块1的0x13结果块0的Cache被块1的页读覆盖读出来的标记全是块1的。这种bug很隐蔽因为大部分块都是好块标记0xFF偶尔几个坏块的标记读错也不容易发现直到某天数据写进坏块才暴露。5.3 掉电时坏块表的完整性保护前面提过坏块表要加CRC这里展开说掉电场景。写坏块表的过程是擦除目标块→编程第0页存表数据→可选地编程第1页存备份。如果在擦除后、编程完成前掉电这个块就处于擦除了一半的状态下次上电读出来可能是全FF看起来像空表或者部分数据。有CRC的话校验失败就回退到另一份备份没有CRC的话你可能把一张全FF的表当成零坏块加载然后所有坏块都被当成好块用。更稳妥的做法是双备份版本号块A和块B轮流写每次写之前先确认另一份是完好的。写的时候先写新数据到非当前份成功后再更新当前份指针指针可以存在一个固定的、不参与磨损的小区域或者干脆用版本号比较。这样任何时刻至少有一份完整可用的表。注意W25N01G的页编程是一次性的同一页不能重复编程除非先擦除整块。所以坏块表的更新是整块擦除再写不是原地改几个字节。这意味着更新坏块表的代价是一次块擦除别太频繁。6. 一套可复现的验证流程从空片到压力测试6.1 上电初始化序列的推荐顺序把前面所有内容串起来一个完整的初始化流程应该是这样的复位芯片0xFF命令等待复位完成读JEDEC ID确认通信正常0x9F应返回EF AA 21之类尝试从保留块读取持久化坏块表校验CRC和魔数如果表无效执行全片坏块扫描建立RAM位图如果表有效加载位图可选地做抽样校验随机抽几个标记为坏块的块确认读出来确实非0xFF计算可用块数如果低于阈值比如低于总块数的90%报警或降级第5步的抽样校验是我加的成本很低但能发现表存在但内容过期的情况——比如你换了芯片但没清表或者表被意外改写。抽10个块验证每个块读一次标记耗时可以忽略。6.2 全片写入-读回-比对测试验证坏块跳过逻辑是否正确的黄金标准是全片测试从逻辑块0开始连续写入递增数据写满所有可用块然后从头读回逐字节比对。这个测试能同时验证地址翻译、跨块边界、坏块跳过三个环节。测试时要注意写入的数据模式要有区分度别用全0或全FF否则坏块返回的全FF会和正常数据混淆。我用的是每页填充页号相关的伪随机序列读回时重新生成比对。另外测试前要确保坏块表是最新的测试后对比实际写入的物理块集合和坏块表标记的坏块集合两者应该没有交集。# 伪代码全片测试的比对逻辑 def full_chip_test(driver): total_logic_blocks driver.usable_block_count() for lb in range(total_logic_blocks): data generate_pattern(lb) driver.write_block(lb, data) for lb in range(total_logic_blocks): expected generate_pattern(lb) actual driver.read_block(lb) if actual ! expected: print(fmismatch at logic block {lb}, fphys {driver.logic_to_phys(lb)}) return False return True6.3 人为制造坏块的测试方法出厂芯片的坏块数量有限想充分测试跳过逻辑需要人为制造坏块。安全的方法是找几个好块手动往它们的第0页备用区写非0xFF值先擦除块再编程第0页备用区首字节为0x00然后重新扫描建表看这些块是否被正确识别和跳过。测试完记得把这些块恢复擦除后备用区会回到0xFF。这个操作有风险如果你写坏了备用区但没正确恢复这些块就真的被当成坏块了。所以测试要在可丢弃的样片上进行别拿正式产品练手。另外人为标记坏块后要验证逻辑块数减少、地址翻译正确跳过、跨块写入不越界三个点缺一不可。7. 写在最后几个我反复验证过的经验点坏块管理这件事说复杂也复杂说简单也简单。核心就三件事认得出坏块、记得住坏块、绕得开坏块。但每一件在W25N01G这种小容量SPI NAND上都有具体的实现细节差一个字节偏移或者一个时序周期结果就完全不同。我个人在实际操作中体会最深的有三点。第一别信这颗料和W25Q一样这种话NAND就是NAND坏块是物理属性不是缺陷从设计第一天就要把坏块管理纳入架构而不是等出了问题再补。第二坏块表的可靠性比坏块识别本身更容易出问题加CRC、做双备份、留扫描兜底这三层防护缺一不可我见过太多项目在识别逻辑上做得很好结果掉电一次表坏了前功尽弃。第三测试要覆盖坏块路径正常路径跑通不代表坏块跳过正确人为制造坏块做全片测试是唯一能让你睡安稳觉的办法。如果你正在用W25N01G做项目建议先把第2节的坏块标记读取调通用逻辑分析仪确认时序然后按第6节的流程跑一遍全片测试。这套流程我在几个不同主控平台上都验证过移植时主要改SPI底层和延时函数上层逻辑基本不用动。后续如果要扩展到更大容量的SPI NAND比如2Gbit、4Gbit块数和页大小会变但坏块管理的框架是通用的把常量改掉就能复用。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询