ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

裸板Nand Flash开发全攻略:接口时序、存储结构与实操避坑

裸板Nand Flash开发全攻略:接口时序、存储结构与实操避坑 做嵌入式裸板开发迟早要跟 Nand Flash 打交道。这枚小小的存储芯片承担着整个系统的代码存放和数据保存任务却又不像内存那样能直接地址访问——它的读写要严格按照芯片手册上的时序流程来操作多等一拍、少拉一个信号读回来的可能就是一串 FF 或者乱码。这篇文章就从 ARM 裸板开发视角把 Nand Flash 最核心的存储结构、接口信号和关键时序完整梳理一遍用实操经验帮你避开那些最容易踩的坑。无论你是刚开始接触裸板驱动的新手还是被读 ID 恒为 FF、回读数据偶发翻转这类问题折磨过的老手这篇内容都值得收藏。1. Nand Flash 在裸板里到底承担什么角色1.1 为什么选 Nand Flash 而不是 Nor Flash在裸板系统里存储方案无非几种Nor Flash、Nand Flash、SD/eMMC、SPI Flash。Nor Flash 支持片上执行XIP代码可以直接在 Flash 里跑但容量做不大价格也贵常见的就是几 MB 级别。Nand Flash 正好反过来容量从几百 MB 到几 GB 都能做价格便宜得多但有一个非常显著的特点不可以按字节随机读写只能按页Page读、按页写按块Block擦除。这两种介质的设计思路完全不同选型时心里要有数。ARM 裸板场景里Nand Flash 通常承担两类任务一是存放 BootLoader、内核镜像、文件系统二是保存用户数据、配置参数。因为容量大可以撑起完整的嵌入式 Linux 系统因为按块擦除的特点也更适合大块连续写入的数据场景。很多经典 ARM 核心板都把 Nand Flash 作为默认的存储介质配合片内 SRAM 或 BootROM 完成启动流程上电后由 CPU 内部的固化代码把 Nand 前几页的数据搬运到 SRAM 中执行然后再逐步加载主程序。这个搬运过程就必须依赖裸板驱动正确操作 Nand Flash任何时序问题都会导致系统跑不起来。1.2 裸板场景下的时序敏感是什么概念有操作系统的时候驱动层之上还有 MTD、文件系统这些抽象层一些时序小问题可能被掩盖住。但裸板环境下你写的是最底层的东西直接操作 CPU 的 Nand 控制器寄存器直接往数据端口填地址、写命令、读数据所有节奏全靠代码控制。芯片手册上写着某个信号要保持多久的建立时间、释放时间如果你的代码节奏快了芯片根本反应不过来节奏慢了性能又浪费。理解 Nand Flash 的时序本质上就是理解芯片手册上的那些参数然后把它们换算成 CPU 时钟周期写进控制器的时序寄存器。我打一个比方帮助你理解时序Nand Flash 就像一台有条不紊的机器传送带上每个工位都有固定的操作时间。你作为操作员按下启动按钮命令阶段之后必须在规定时间内把工件放到位地址阶段放到位后再按开始加工执行阶段最后等机器亮起完成信号R/B 引脚变高。任何一个步骤早了机器没反应过来晚了整个产线空转。裸板开发的任务就是精确掌握这些早与晚的边界。2. 必须先搞懂的存储基础块、页和 OOB2.1 存储单元结构块和页怎么划分Nand Flash 的存储结构是分层的最小可读单元是页Page最小可擦除单元是块Block。一个块通常包含 64 页或 128 页具体看芯片世代。以常见的 SLC Nand 为例一个小容量芯片可能是 2KB 页 64 字节 OOB一个块 128 页一共 2048 个块容量算下来是 2KB × 128 × 2048 512 Mb 64MB。新一代 SLC 常见 4KB 页 128 字节 OOB 或 8KB 页 448 字节 OOB。MLC/TLC 的页更大但裸板开发基本都是 SLC因为 SLC 时序整齐、操作简单、寿命长适合驱动验证和关键数据存储。这里有个关键点擦除是按块进行的而读和写是按页进行的。因为 Nand 的存储单元在编程写时只能把 bit 从 1 变为 0无法把 0 恢复成 1所以如果某页写错了不能单独改写那一页必须把整块擦除成全 1 再重新写。这也是 Nand Flash 驱动必须处理先擦后写、擦除对齐这些逻辑的原因。设计文件系统或者日志存储时也要考虑块擦除带来的磨损均衡问题。2.2 OOB 区到底存什么每一页除了主数据区Main Area之外还有一小块附加区域叫 OOBOut Of Band也叫 spare area。主数据区存用户数据OOB 区存放辅助信息。裸板开发中OOB 最常见用途有三个坏块标记Nand 出厂时可能有坏块厂商会在块内第一页或第二页的 OOB 特定位置写入非 FF 的值做标记。驱动在擦除、写入之前要检查这个标记跳过坏块。ECC 校验数据Nand 在读写过程中可能发生 bit 翻转尤其是 MLC/TLCSLC 也有小概率需要 ECC 算法纠错。控制器在写入时计算校验值把校验值存在 OOB 区读取时再从 OOB 取出校验值与重新计算的值比对。常见粒度是 512 字节配 3 字节 ECC 或者 1KB 配 4 字节 ECC不同主控策略不同。文件系统元信息日志型文件系统如 JFFS2 等会把块状态、日志节点信息写到 OOB用于掉电恢复和磨损均衡。裸板开发里最容易被忽略的就是 OOB。很多初学者在读页数据时只读主数据区不处理 OOB结果发现数据偶尔不对或者坏块没标记出来根源就在于 OOB 没被正确解析。实际操作中要养成页读写必然带 OOB的思维至少要在驱动层把 OOB 的读写通道打通。2.3 接口信号与数据/地址复用Nand Flash 的接口不算复杂但对时序敏感的正是这些信号的切换顺序。常见的信号有DQ[7:0] 或 DQ[15:0]数据总线同时复用来传输命令、地址和数据。裸板开发大多用 8bit 模式也就是 DQ0-DQ7 共 8 根线。CLECommand Latch Enable命令锁存使能。当 CLE 为高、WE 上升沿采样时数据总线上的值被当作命令。ALEAddress Latch Enable地址锁存使能。当 ALE 为高、WE 上升沿采样时数据总线上的值被当作地址。CE#Chip Enable片选低有效。RE#Read Enable读使能低有效。每次拉低数据总线上输出一字节或一个字数据。WE#Write Enable写使能低有效。每次拉低数据总线上的命令/地址/数据被锁存。R/B#Ready/Busy就绪/忙指示开漏输出。低电平时表示芯片在执行擦除、编程等内部操作高电平时表示空闲或数据已就绪。WP#Write Protect写保护低有效。拉低时禁止编程和擦除。这里最常见的问题是复用。命令、地址、数据都是通过同一组数据总线传输的区别就在于 CLE 和 ALE 的电平状态。发送命令时CLE1、ALE0发送地址时CLE0、ALE1传输数据时CLE0、ALE0。三个阶段对应的 WE# 都要产生完整的脉冲。很多新手第一次写驱动时容易搞混命令和地址阶段的控制信号读 ID 时返回怪异值往往就是这个原因。3. Nand Flash 核心操作与时序拆解3.1 读 ID驱动正确性的第一道验证裸板驱动 Nand Flash第一件事就是读 ID。读 ID 用来确认芯片连接正常、配置正确也是后续操作的基础。标准流程是发送命令 0x90发送地址 0x00然后连续读取最多 5 个字节依次是厂商 ID、设备 ID、内部结构信息页大小、块大小等。某些芯片需要先发 0x9F早期三星系常用 0x90具体以 datasheet 为准。这个流程看起来简单但它把命令阶段和地址阶段都覆盖了CLE 拉高、送 0x90接着 ALE 拉高、送 0x00然后 CLE/ALE 都拉低连续产生 RE# 时钟从数据总线上读取 ID 字节。如果读出来的厂商 ID 是预期的值比如某些常见厂商固定是 0xEC、0xAD、0x2C 等说明芯片识别成功如果全是 0xFF说明 CE、WE、RE、数据线连接或者控制器配置有问题。我在实际项目里遇到过一种情况读 ID 时第一个字节正确第二个字节正确后面几个字节是随机值。排查后发现是地址周期发少了或者读最后一个字节后继续发了读时钟但芯片内部已经没有数据可输出。Nand 的读 ID 虽然是连续读但本质上输出有效包络是有限的驱动应该在正确的字节窗口之外停止读取。3.2 页读取的完整时序流程页读取是嵌入式系统里使用频率最高的操作。以传统 SLC 芯片为例读取一页大致分几步发送命令 0x00表示页读取发送列地址页内偏移和行地址页号地址周期数量取决于芯片容量。小容量芯片通常 5 个周期前 2 个是列地址后 3 个是行地址大容量或 16bit 接口的芯片会到 6 个周期发送命令 0x30表示读确认或开始读取等待 R/B# 引脚变高或者轮询状态寄存器发送 0x70读状态字节连续拉低 RE#每个 RE# 下降沿之后 DQ 总线上出现一字节数据依次读取整页。这里有一个核心时序参数tRRead access time也就是从命令 0x30 发出后到数据可以开始读取的时间。SLC 芯片 tR 通常在 25us 到 50us 之间。驱动在等待时怎么判断数据是否就绪最简单的方式是等 R/B# 引脚变高但裸板开发时 R/B# 往往接在 GPIO 上为了省引脚也有不接的情况。不接 R/B# 时就只能用延时函数死等 tR 余量或者用状态寄存器查询。页读取的数据阶段还有个细节每读取 512 字节之后如果要跳过 OOB 的位置必须额外发送列地址重定义命令 0x05/0xE0才能随机访问页内任意位置。如果只是顺序读取整页数据和 OOB就不需要额外命令。很多 U-Boot 驱动在读取 OOB 时实际是先读主数据区再单独读取 OOB 区两种策略各有开销裸板阶段建议先把整页顺序读跑通。3.3 页编程写入时序的注意点页编程和页读取流程上有几分对称但节奏截然不同。标准流程是发送命令 0x80表示页编程准备发送列地址和行地址写入完整的一页数据包括 OOB如果驱动需要发送命令 0x10表示编程执行等待 R/B# 变高或轮询状态寄存器确认编程完成。页编程有一个关键时序参数tPROGPage programming timeSLC 典型值 200us 到 700usMLC 更长甚至到 1ms 以上。等待期间芯片内部在注入电荷此时任何读操作都不能进行只能等 R/B# 变高。页编程最容易犯的错误是一次写入的数据不满一页。Nand Flash 的页编程必须以页为单位你必须向芯片提供整整一页的数据量包括 OOB芯片才会真正执行内部编程流程。如果你只写部分数据就发 0x10 命令芯片会按写命令时所在的行地址对整页进行编程缺失的部分会被当作垃圾数据写入进而破坏原有内容。正确的做法是如果只改页内某一段数据需要先把整页读到 SRAM在内存中修改再把整页写回这就是裸板文件系统常见的 read-modify-write 操作。另外页编程通常在 0x10 之前数据是一个字节一个字节地通过 WE# 脉冲写入控制器的写缓存这个阶段对 WE# 脉冲宽度的要求比较严格时序寄存器配得不够会有数据写错位的问题。后续文章会展开讲参数换算。3.4 块擦除时序与细节块擦除是三个操作里最粗暴但也不可或缺的。流程相对短发送命令 0x60表示块擦除准备发送行地址块地址通常是 3 个周期块内页地址位填任意值发送命令 0xD0表示擦除执行等待 R/B# 变高或状态寄存器确认擦除完成。块擦除的等待参数是tBERSBlock erase timeSLC 典型值 2ms 到 3msMLC 可能到 5ms 以上。擦除操作会把块内所有页变为全 0xFF同时 OOB 区域也恢复为 0xFF除非写保护。擦除后可以用读页命令读几个页检查是否全 FF做擦除校验。块擦除容易出问题的场景是擦除保护Write Protection如果 WP# 引脚被拉低芯片会忽略擦除和编程命令。裸板启动初期为了保险很多设计会把 WP# 拉低直到系统启动完成才释放。如果驱动测试时发现擦除不生效、状态寄存器一直报错优先检查 WP# 引脚电平。还有一个经验点块擦除的地址周期里块内页地址位应该置 0虽然多数芯片会忽略这些位但按手册写总没错。某些小容量芯片地址周期不是 3 个行周期而是 2 个要根据 datasheet 调整写错地址周期不仅擦除不了还可能擦错块这属于灾难性错误务必在驱动初始化时把地址周期配置明确打印出来核对一遍。3.5 状态查询等 R/B 还是轮询状态寄存器上面反复提到等待 R/B# 变高但实际工程里还有两种玩法可供选择。第一种最简单R/B# 接 GPIO开漏输出外部接上拉电阻。启动后循环读 GPIO变高才继续。这种方式的缺点是如果板子设计时没把 R/B# 接到 GPIO你就没得等。第二种就是状态寄存器Status Register查询先发送命令 0x70然后读一个字节bit 0 为 0 表示 Busy为 1 表示 Readybit 6 为 0 表示编程/擦除操作成功为 1 表示失败。这种方式的优点是只需要数据接口不需要额外的 GPIO 引脚很多控制器驱动甚至用硬件自动查询状态。裸板阶段我建议把两种方式都实现——调试时可以用 GPIO 等待更直观生产板上如果 R/B# 没接就用状态寄存器。如果两种都没条件那就只能把典型等待参数tR、tPROG、tBERS加上足够的余量做死等但这样效率低大文件读写一下就能感觉到明显卡顿。4. 控制器初始化与时序参数计算4.1 Nand 控制器初始化的大致步骤ARM 裸板上访问 Nand Flash通常不是直接拿 GPIO 模拟时序而是用 CPU 内部集成的 Nand 控制器。控制器会帮你管理 CLE/ALE/WE#/RE# 这些信号的切换你要做的只是往寄存器里填命令、地址再读写数据端口也就是所谓的 寄存器映射 访问方式。初始化步骤一般长这样配置引脚复用确认 GPIO 引脚被复用为 Nand 控制器功能这个错了系统直接不工作读 ID 全是 FF 的第一大原因。配置数据总线宽度8bit 还是 16bit 必须和芯片实际接法一致。配置页大小和地址周期告诉控制器每页多少字节几周期地址这决定后续读写时的自动地址递增逻辑。配置时序寄存器把 HCLK/PCLK 时钟周期数换算成 Nand 芯片要求的建立时间、保持时间、脉冲宽度。使能控制器解除复位、打开时钟门控。做一次复位序列发送 0xFF 命令给 Nand 芯片让它回到默认状态。读 ID 验证确认以上配置无误。这个顺序里第 4 步是大家最容易糊弄过去的。很多参考代码把时序寄存器值写死照抄到自己的板子上换了主频或者换了 Nand 芯片就出现读写不稳定根源就在这里。下面专门用一个例子说明怎么算时序参数。4.2 时序参数的换算原理与示例不同 ARM 处理器的 Nand 控制器时序寄存器格式差异很大但换算思路是通用的。以某款经典 ARM 处理器为例它要求你配置三个周期数CLE/ALE 建立时间TACLS、WE#/RE# 脉冲宽度TWRPH0、写使能/读使能数据建立时间TWRPH1单位是 HCLK 周期数。假设你的系统主频 HCLK 100MHz那么 1 个 HCLK 周期是 10ns。Nand 芯片 datasheet 上写着几个关键参数tCLS命令/地址建立时间即 CLE/ALE 拉高到 WE# 下降沿的最短时间≥ 12nstCS片选建立时间≥ 20nstWPWE# 脉冲低电平宽度≥ 15nstDS数据建立时间≥ 10ns换算方法很直接每个参数除以 HCLK 周期向上取整再加一点余量。比如 tCLS 12ns10ns 一个周期取整是 2 周期稳妥起见建议取 2~3 周期对应 20~30ns留有充分余量。TWRPH0 对应 tWP15ns 取 2 周期20ns。TWRPH1 对应数据建立时间10ns 取 2 周期20ns。换算完毕写入寄存器即可。这里有个经验规律时序参数宁可多留余量也不要卡着最小值。Nand 芯片的标注值通常是常温下的典型值或最大值板子上的走线延迟、电平转换都会增加实际时序开销卡着最小值容易出现偶发性的读错、写失败。如果系统性能要求不苛刻我通常会把所有时序参数都放大到理论值的 1.5~2 倍。但也不能无脑放大。TWRPH0 太大读数据时整个页读耗时变长大文件系统遍历会慢得让人抓狂TWRPH1 太大写数据吞吐也会受影响。均衡的做法是先用紧凑参数跑性能测试再用宽松参数跑稳定性测试如果两者在需求区间内都能通过选中间值。4.3 硬件 ECC 还是软件 ECC裸板读写 Nand不开 ECC 等于裸奔。SLC 芯片虽然比 MLC 稳但位翻转依然存在尤其在高温、老化、电压波动环境下。ECC 方案有两种路线硬件 ECC很多 ARM 处理器的 Nand 控制器集成了 ECC 引擎。写入时控制器在数据写入 Nand 的同时按固定粒度比如 512 字节、1KB计算 ECC 值并自动写入 OOB 指定位置读取时控制器重新计算 ECC与存储在 OOB 里的值比对发现错误可以纠正通常是 1bit但不能纠正的会标记出错。这种方法速度快不占 CPU裸板阶段我强烈建议优先启用硬件 ECC。软件 ECC如果没有硬件 ECC或者控制器规格太老可以用软件实现汉明码Hamming code校验通常每 512 字节生成 3 字节 ECC能够检测并纠正单个 bit 翻转。软件 ECC 的缺点是速度慢反复对大页做计算时会明显拖慢读写。如果只是存少量配置参数软件 ECC 也够用如果要跑文件系统还是要想办法用硬件 ECC。选 ECC 之前先确认芯片 OOB 大小够不够存 ECC。典型配置是每 512 字节配 3 字节 ECC一个 2KB 页配 12 字节 ECCOOB 是 64 字节完全够但如果你用的是逐页全数据映射的策略还要留坏块标记、文件系统元信息等OOB 就可能紧张。规划 OOB 分区时要先把 ECC 数据格固定下来否则换驱动版本后新旧格式不兼容数据会全部读不出来——这个坑我亲眼见过不少次。5. 实操中的常见问题与排查实录5.1 读 ID 恒为 0xFF怎么一步步排查这是裸板 Nand 调试最经典的问题无论怎么操作读回来的 ID 都是全 FF。遇到这种问题按下面的顺序排查基本能定位到九成以上的原因。第一步检查引脚复用和时钟配置。ARM 处理器的 GPIO 往往多种功能复用如果引脚没有切换到 Nand 控制器功能或者外设时钟没有使能控制器根本不会工作数据总线一直处于高阻态读到的自然全是 FF。很多参考板初始化代码里把引脚复用配置和时钟门控放在很隐蔽的位置新人特别容易漏。第二步检查片选信号。CE# 有没有被拉低有些板子通过 GPIO 控制片选需要在访问前手动拉低有些处理器的 Nand 控制器会自动控制片选但需要配置 CS 控制寄存器。如果 CE# 一直处于高电平芯片根本没被选中读什么都是 FF。第三步检查 WE# 和 RE# 的极性。Nand 的 WE#、RE# 是低有效不少新手把控制器寄存器的信号极性配置反了变成高有效。正常时序里WE# 应该是从高到低再回到高的脉冲你可以拿示波器量一下操作瞬间的波形看极性对不对。第四步检查读数据时序。如果 ID 不是全 FF而是乱码问题往往在 TWRPH 配置太短数据建立时间不够控制器在芯片输出还没稳定时就把数据采走了。把 TWRPH1 调大一两个时钟周期通常就能出正确 ID。5.2 写入后回读不一致是时序还是 ECC还有一种典型现象写入一页数据回读发现某些位置不对——有时是固定的某一位翻转有时是整页错位。先说结论如果错误是偶发且随机的优先怀疑 ECC 和坏块如果错误是规律性的比如固定偏移、固定字节错位优先怀疑时序。规律性错位常见于 16bit 模式配置错误或页大小配置错误。芯片实际是 8bit 总线控制器配成 16bit读数据时会把两个字节顺序搞反页大小配置错误则会导致地址递增不对读出来的数据整体偏移。这种情况直接看控制器配置不要查芯片。偶发性位翻转在排除供电纹波、走线干扰后大概率是 ECC 没启用或者 ECC 校验值存储位置不匹配。写入时控制器自动计算 ECC 存在 OOB读取时却按另一个偏移读 OOB比对自然失败。解决方法是固定 ECC 在 OOB 中的位置布局并保证读写两端使用同一套布局。还有一种可能是使用了坏块。写入之前没有检查坏块标记写着写着写到坏块上那个块的数据自然不可靠。读回数据不一致时优先检查当前块有没有坏块标记。5.3 坏块管理从跳过到标记的完整方法Nand Flash 的坏块管理裸板阶段可以做得很朴素但几条原则要守住。第一条原则坏块标记必须和厂商标记对齐。出厂坏块的标记位置通常在块内第 1 页有些大页芯片是第 1 页和第 2 页的 OOB 区典型 SLC 芯片是 OOB 第 5 个字节为非 0xFF就算坏块。擦除或者写入之前驱动要先读那个固定位置的字节如果非 0xFF直接跳过该块。第二条原则擦除操作前必须先确认块状态。因为 Nand 的擦除是不可逆操作如果对一个出厂坏块执行擦除可能把坏块标记擦成 0xFF导致它看起来是好的后续使用必然出问题。因此一切写入和擦除之前先检查坏块标记。第三条原则运行时发现新坏块要现场标记。使用过程中产生的坏块不会自己标记驱动在写入后应该做写后校验Program Verify如果校验失败就要把该块 OOB 对应位置标记为坏块更新坏块表。裸板上的坏块表可以存在内存里启动时扫描一遍所有块建立一个数组运行期间查询。扫描 Nand 全部块要读大量页数据启动时间会长一些但换来的是可靠性。对小容量 SLC扫描一次是可接受的对大容量 Nand可以只扫描系统分区所在区域用户数据区按需扫描。5.4 时序余量、引脚干扰和供电噪声的经验补充最后补充几条实战中总结出来的经验都是踩过坑之后才真正理解的。关于时序余量我遇到过一块板子常温下读写完全正常环境温度一升高就出现偶发读错。用示波器测发现是控制器的 WE# 脉冲宽度本身就不足温度升高后芯片响应变慢时序不再满足。把时序寄存器余量从 1.2 倍调到 2 倍之后再没复现。嵌入式产品如果要在恶劣环境运行时序余量务必充足这点钱不能省。关于引脚干扰数据总线旁边的走线如果与高频信号靠太近读数据时可能出现偶发跳变。这种问题光调时序参数解决不了只能改板优化走线或者在驱动里增加重读机制。裸板调试初期先不要盲目怀疑芯片量一下波形再说。关于供电噪声Nand 编程和擦除时需要较大的电流脉冲如果供电走线窄、去耦电容不足VCC 跌落会引发擦写失败。表现是状态寄存器回读错误或者擦除后校验不通过。排查时可以抓 VCC 波形看擦除瞬间有没有明显跌落有的话在 Nand 电源附近补一个 10uF 陶瓷电容通常能改善很多。我做裸板驱动这些年最深的体会是Nand Flash 这套东西基本原理翻来覆去就那些命令、地址、数据、时序但每一个环节都值得较真。很多时候不是你代码写错了而是参数没配好、时序余量不够、坏块没处理干净。把这些细节理顺之后它就是你系统里最可靠的存储伙伴。如果后面有时间我准备再写一篇关于裸板文件系统与 Nand Flash 配合的文章把 OOB 管理、日志型写入、磨损均衡讲透那部分内容对量产产品和数据完整性至关重要有兴趣的读者可以先关注。
RELATED READING

延伸阅读

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