SPI NAND Flash 软件设计规范
编制日期 2026-09-21
面向 XT26G01D / XT26G11D(3.3V 1Gb SLC SPI NAND)的固件驱动设计约定 —— 覆盖 Feature 寄存器体系、两段式读、三段式编程、坏块管理、 片内 ECC 与 掉电数据完整性,逐条给出可落地的命令序列、轮询判据与超时预算。
| 文档编号 | SPI-NAND-SW-SPEC-001 |
|---|---|
| 版本 / 状态 | V1.0 · 正式发布 |
| 适用器件 | XTX XT26G01D(版本号 Rev 1.1)、XT26G11D(Rev 1.0)及同系列 3.3V 1Gb SPI NAND |
| 参考标准 | XT26G01D Spec v1.1 · XT26G11D Spec v1.0 · ONFI Parameter Page 定义 |
| 配套文档 | SPI_NAND_Flash_电路设计指南.html(硬件 / 原理图 / 布线约束) |
0. 目的与范围
本规范约束 SPI NAND 器件在嵌入式固件中的软件行为:命令序列、状态判读、 超时预算、坏块与 ECC 处理、掉电保护。凡本文与器件 datasheet 冲突,以 datasheet 为准。
- 统一命令序列:读(两段式)、编程(三段式)、擦除(WREN + D8h)各自固化为一套标准流程。
- 统一状态判读:只用
0Fh+C0h轮询 OIP,并同时检查 P_FAIL / E_FAIL / ECCS。 - 统一坏块策略:上电扫描建立坏块表(BBT),NVB = 1004,运行时增量标记。
- 统一掉电保护:给出写窗口、提交协议与刷新阈值,明确哪些操作是原子的。
一句话结论:SPI NAND 是「先搬进 cache 再操作」的器件 ——
所有读都是 13h 把整页搬进 cache 再用 03h/0Bh 读出;所有写都是
02h 装数据进 cache 再用 10h 落阵列。
没有 WREN 的 10h 会被静默忽略,没有 OIP 轮询的 10h 会写出半页数据,没有 ECCS 判读的读会带回已损坏的数据。
文档使用说明
- 本文是《SPI NAND Flash 电路设计指南》的软件配套篇:硬件篇解决「怎么接、怎么供电、怎么走线」,本文解决「驱动怎么写、协议怎么等、错误怎么救」。
- 命令码、寄存器位、时序值均取自 XT26G01D Spec v1.1 与 XT26G11D Spec v1.0。其它容量 / 其它厂商型号存在差异,移植前必须逐条核对。
- 文中 C 代码为协议骨架伪代码,演示流程与判断条件,非特定平台可直接编译的完整实现;
spi_transfer()、now_us()等为平台相关底层。
① 绝不用固定延时替代 OIP 轮询——读写擦时间随温度、磨损、个体差异波动巨大; ② 每次写/擦操作前必须重新 WREN——WEL 在每次操作结束后自动清零; ③ 超时门限一律取 datasheet max 以上——typ 值只用来安排轮询节奏,不能当失败判据。
1 · 器件定位与阵列组织
SPI NAND 不是「带坏块的 NOR」。它保留了 NAND 的页/块结构与坏块约束,只是把接口换成了 SPI。软件设计的第一件事,是把「地址空间」和「ECC 边界」搞清楚。
1.1 器件定位与关键参数
数据取自 XT26G01D Spec v1.1 第 2 节 Features 与第 14.7 节 Performance Timing;XT26G11D 参数一致。
| 项目 | XT26G01D | XT26G11D | 软件影响 |
|---|---|---|---|
| 容量 / 结构 | 1Gb · 1024 Block × 64 Page × 2176 B | 同左 | 驱动无需区分 |
| JEDEC MID | 0Bh(XTX) | 0Bh(XTX) | 识别阈值一致 |
| Device ID(9Fh) | 31h | 34h | 识别表必须同时收录 |
| 片内 ECC | 常开(ECC_EN 关断时状态位回默认值) | 可由 ECC_EN 位开关 | XT26G01D 不得关 ECC |
| 封装 | WSON8 / LGA8 / BGA24 | WSON8 | 仅影响硬件,软件无差异 |
| 手册版本 | Rev 1.1(2024-11) | Rev 1.0(2023-12) | 以各自 Rev 为准 |
1.2 阵列组织与地址映射
SPI NAND 的地址分为两套,软件最容易在这里出错:
| 地址类型 | 位宽 | 出现在哪些命令 | 含义 |
|---|---|---|---|
| 行地址(Row Address) | 16 bit 有效(24 bit 中高 8 位为 dummy) | 13h 页读、10h 编程执行、D8h 块擦除 | 块号 + 页号:PA[15:6] = 块号(0~1023),PA[5:0] = 块内页号(0~63) |
| 列地址(Column Address) | 12 bit 有效(16 bit 中高 4 位为 dummy) | 03h/0Bh 读 cache、02h/32h 装数据、84h 随机装数据 | 页内字节偏移 000H~87FH,指向 cache 寄存器 |
1Gb 器件只需要 16 bit 行地址(1024 块 × 64 页 = 65536 = 216)。13h / 10h / D8h 后面的 24 bit 里,最高 8 位必须为 0 且被器件忽略。把它当成「24 位地址」直接填 3 字节地址,在擦除时会擦错块。
块擦除 D8h 填的是 块首页的行地址(页号部分为 0),即 block << 6。
1.3 页、备用区与 ECC 扇区边界
片内 ECC 以 528 字节为一个扇区(512 B 主区 + 16 B 备用区),每页 4 个扇区,另有 64 B 存放 ECC 校验码:
这 64 字节由器件在 Program Execute 时自动写入 ECC 校验码,用户写入会被忽略(datasheet 原文:WRITEs to the ECC area are ignored)。软件若在「写整页」时把 2176 字节全填进去,前 2112 字节有效,后 64 字节被丢弃——这不会报错,但会让「我写了什么」与「实际存了什么」不一致,回读比对时误判为数据损坏。
建议:把 2176 字节的页拆成 4 个 528 字节扇区提交,第 5 段(840H~87FH)不参与传输。
1.4 与 SPI NOR 的关键差异(移植必读)
| 维度 | SPI NOR | SPI NAND | 移植后果 |
|---|---|---|---|
| 读 | 发地址直接出数据 | 先 13h 搬到 cache,再读 cache | 读函数必须拆成两段 |
| 写 | 02h 直接写阵列 | 02h 装 cache → 06h → 10h 落阵列 | 少发 10h 数据就丢了 |
| 状态寄存器 | 05h 读 SR | 0Fh + C0h 读状态 | 用 05h 轮询永远读不到 OIP |
| 坏块 | 无 | 出厂即可能有坏块,须建 BBT | 不做坏块管理必然丢数据 |
| ECC | 无 | 片内 8bit/528B,须读 ECCS 判读 | ≥6 bit 即整块迁移 + 标坏(第 11 章) |
| 局部编程 | 可字节编程 | 同一页最多 4 次(NOP ≤ 4),且须按 sector 对齐 | 超次数导致编程失败 |
| 块内顺序 | 任意 | 块内页必须从 LSPage 到 MSPage 顺序编程 | 随机页编程被禁止 |
把 NOR 驱动改两行就拿去驱动 SPI NAND,最常见的死法是用 05h 轮询 busy:SPI NAND 根本没有 05h 读状态寄存器命令,读回来的是无效字节,表现为「轮询立刻返回成功,但数据没写进去」。
2 · 术语与缩写
本章统一术语口径,避免「页/块/sector/cache」在文档与代码里指的不是同一件事。
2.1 结构与命令术语
| 缩写 / 术语 | 全称 | 本文中的含义 |
|---|---|---|
| Page | — | 最小读写单位,2176 B(2048 主 + 128 备用) |
| Block | — | 最小擦除单位,64 Page = 128 KB + 8 KB |
| Sector | — | ECC 粒度,512 B 主区 + 16 B 备用区 = 528 B,每页 4 个 |
| Cache Register | 页缓存寄存器 | 2176 B 的 SRAM 缓冲区;阵列与 SPI 之间所有数据都要经过它 |
| PL | Program Load | 02h/32h,把数据装进 cache(不写阵列) |
| PE | Program Execute | 10h,把 cache 内容写入阵列 |
| NOP | Number Of (partial) Programs | 同一页允许的局部编程次数,上限 4 |
| OIP | Operation In Progress | 状态位 bit0;1 = 器件忙,0 = 就绪 |
| WEL | Write Enable Latch | 状态位 bit1;PE / 擦除前必须先 06h 置位 |
| P_FAIL | Program Fail | 状态位 bit3;编程失败(含写保护区域) |
| E_FAIL | Erase Fail | 状态位 bit2;擦除失败(含擦除锁定区域) |
| ECCS | ECC Status | 状态位 bit7~4;回读数据的纠错结果编码 |
| BBM | Bad Block Mark | 出厂坏块标记,位于每块首页备用区首字节(列地址 800H),非 FFh 即坏块 |
| NVB | Number of Valid Blocks | 最小有效块数,本系列为 1004(总 1024) |
| BBT | Bad Block Table | 软件维护的坏块表,上电扫描建立,运行时增量更新 |
| UID | Unique ID | 128 bit 器件唯一 ID,附反码校验,共 16 副本 |
| IDM | Internal Data Move | 片内数据搬移:13h → 84h 改字节 → 06h → 10h |
2.2 本文书写约定
- 命令码用小写十六进制带后缀
h书写(如13h),与 datasheet 保持一致;datasheet 中出现的13H大写形式含义相同。 - Feature 地址用两位十六进制 +
H(如A0H、C0H),指 Get/Set Features 命令后的那个地址字节。 - 状态位写作
C0H[0]形式,方括号内为 bit 位序(bit0 为最低位)。 - 时序符号沿用 datasheet:tRD(页读)、tPROG(页编程)、tERS(块擦除)、tRST(复位恢复)。「典型值 / 最大值」成对给出时,软件超时必须按最大值预算。
- 代码示例为 C 伪代码,省略 SPI 传输底层实现,只表达命令序列与判据。
3 · 驱动架构与初始化
SPI NAND 的驱动比 NOR 多两层:坏块映射和 ECC 决策。这两层放错位置,后面所有优化都是白做。
3.1 分层职责
| 层 | 职责 | 禁止做的事 |
|---|---|---|
| 应用层 | 业务调用 read/write/ioctl;不感知坏块 | 直接拼 SPI 命令、直接算行地址 |
| 存储管理层 | BBT 维护、磨损均衡、提交协议、ECC 刷新决策 | 绕过 BBT 直接写物理块 |
| 命令协议层 | 13h/03h 读、02h/06h/10h 写、D8h 擦除、0Fh 轮询 | 省略 WREN、省略 OIP 轮询 |
| 硬件抽象层 | SPI 传输、CS# 时序、Quad 使能 | 在传输中途切换时钟频率 |
3.2 上电初始化流程
上电后器件处于整片写保护状态(BP2/BP1/BP0 默认均为 1,对应「All」),且 cache 内容未定义。初始化必须按下列顺序执行:
/* 上电初始化:返回 0 表示成功 */
int spinand_init(void)
{
uint8_t id[2];
delay_ms(3); /* tVSL: VCC(min) -> CS# low 至少 3 ms */
spi_cs_high(); spi_cs_low(); /* 上电后必须经历一次 CS# 高->低 */
if (spinand_read_id(id) != 0) return -E_NODEV; /* 9Fh */
if (id[0] != 0x0B) return -E_NODEV; /* MID = XTX */
if (id[1] != 0x31 && id[1] != 0x34)
return -E_NODEV; /* DID: G01D / G11D */
/* 解锁:A0H 的 BP2:0 写 0;BRWD 保持 0,避免 WP# 硬件锁死 */
if (set_feature(0xA0, 0x00) != 0) return -E_LOCK;
/* 工作模式:B0H —— QE=1(bit0) / ECC_EN=1(bit4) / HSE=1(bit1) */
if (set_feature(0xB0, 0x13) != 0) return -E_FEATURE;
return bbt_scan(); /* 扫描 1024 块首页 800H,建立坏块表 */
}
① CS# 必须先高后低。datasheet 明确要求:上电后 CS# 必须经历一次从高到低的跳变,器件才开始接受命令。若硬件上 CS# 一直被拉低(例如 GPIO 默认输出低),第一条 9Fh 就会失败。
② 不解锁就去写,失败码是 08h 而不是「超时」。上电默认 BP[2:0]=111 全片保护,此时发编程命令返回 P_FAIL(08h),发擦除命令返回 E_FAIL(04h)。很多「SPI NAND 写不进去」的问题,根因就是这一行解锁代码没写。
3.3 器件识别:三级策略
- 9Fh 读 ID(2 字节):MID =
0Bh判定为 XTX;DID =31h(XT26G01D)或34h(XT26G11D)。这是最快的路径,用于初始化早期筛选。 - 13h + 000001h 读 Parameter Page:读回 ONFI 签名
4Fh 4Eh 46h 49h("ONFI"),可拿到页大小 2048、spare 128、页/块 64、块/LUN 1024、tPROG 700 µs、tERS 10 ms、tRD 185 µs 等真实参数,并把器件型号字符串(byte 44-63,XT26G01D)读出来。 - CRC 校验:Parameter Page 的 byte 254/255 为 CRC,多项式 X16+X15+X2+1,初值
4F4Eh。XT26G01D 的 CRC 为1Ch/13h。校验不过说明读到的是噪声,应重试而非继续初始化。
如果固件要兼容多家器件的同一份驱动,应以 Parameter Page 为准:它给出真实的页/块几何与时序上限,可以避免把「1Gb 器件按 2Gb 地址空间访问」这类错误。只读 ID 的方案,等于把所有几何参数硬编码在驱动里,换料就要改代码。
4 · Feature 寄存器体系
SPI NAND 没有「状态寄存器 + 配置寄存器」的 NOR 式划分,所有配置与状态都通过 0Fh / 1Fh 加一个Feature 地址访问。理解这四个地址,就理解了整颗器件。
4.1 四张表总览
| Feature 地址 | 名称 | 关键位 | 上电默认值 | 是否易失 |
|---|---|---|---|---|
A0H | Block Lock | BRWD / BP2 / BP1 / BP0 / INV / CMP | BP2:0 = 111(全片保护) | 非易失(配置保持) |
B0H | Feature | OTP_PRT / OTP_EN / ECC_EN / CRM / HSE / QE | HSE = 1,其余 0 | 易失(OTP_PRT 除外) |
C0H | Status | ECCS3~0 / P_FAIL / E_FAIL / WEL / OIP | ECCS 反映 Block0 Page0 内容 | 只读 |
D0H | Drive Strength | DS_IO[1:0] | 01 = 50% 驱动强度 | 非易失(配置保持) |
4.2 A0H —— 块保护与 BRWD 硬件锁
块保护由 CMP、INV、BP[2:0] 五个位组合决定保护区域。完整组合表见第 8 章。软件侧只需记住三条:
- 上电默认
BP[2:0] = 111→ 全片保护;写入前必须显式解锁。 BRWD = 1且 WP# 引脚为低时,A0H 的所有可写位都无法再改动——这是防软件误操作的硬件锁,一旦生效只能靠断电或拉高 WP# 解除。- 向被保护区域发擦除命令 → 返回
E_FAIL(状态字节04h);发编程命令 → 返回P_FAIL(08h)。
4.3 B0H —— 功能位(OTP / ECC / Quad / 高速)
| 位 | 名称 | 作用 | 软件约定 |
|---|---|---|---|
| bit7 | OTP_PRT | OTP 区永久锁定;一旦置 1 不可恢复 | 产线烧录完成后最后一步才置位 |
| bit6 | OTP_EN | 使能访问 OTP 区 / UID / Parameter Page | 读 UID 与参数页前必须先置 1 |
| bit4 | ECC_EN | 片内 ECC 开关(XT26G11D) | XT26G01D 保持 1;关断后 ECCS 为无效默认值 |
| bit3 | CRM | Continuous Read Mode | 默认 0;如需连续读请联系原厂确认 |
| bit1 | HSE | 顺序读高速模式 | 默认 1;随机页读时建议清 0(否则 tRD 变长) |
| bit0 | QE | Quad 使能 | 使用 32h/6Bh/EBh 等 Quad 命令前必须置 1 |
XT26G01D(Rev 1.1):datasheet 注明「Internal ECC is always on」,即使把 ECC_EN 清 0,片内 ECC 仍在计算,只是 ECCS 状态位回落到默认值 0000b,不再反映真实纠错情况。也就是说:关掉它,你既没省下什么,还失去了唯一的错误可见性。
XT26G11D(Rev 1.0):ECC_EN 是真正的开关,清 0 后片内 ECC 停止工作。
结论:无论哪颗,固件都应保持 ECC_EN = 1。把 ECC_EN 当可调项的驱动,在 G01D 上会静默失去错误检测能力。
4.4 C0H —— 状态寄存器
状态字节的 8 个位分工明确:bit7~4 是 ECC 结果,bit3~2 是失败标志,bit1 是写使能,bit0 是忙标志。轮询只看 bit0,判成败必须看 bit3~2 与 bit7~4。
/* 读状态:0Fh + C0H + 1 字节 */
uint8_t spinand_get_status(void)
{
uint8_t tx[2] = {0x0F, 0xC0}, rx = 0;
spi_cs_low();
spi_transfer(tx, 2);
spi_read(&rx, 1);
spi_cs_high();
return rx;
}
#define ST_OIP (1u << 0) /* 1 = 忙 */
#define ST_WEL (1u << 1) /* 1 = 已写使能 */
#define ST_EFAIL (1u << 2) /* 1 = 擦除失败 */
#define ST_PFAIL (1u << 3) /* 1 = 编程失败 */
#define ST_ECCS_MASK (0xF0u) /* bit7~4:ECC 状态 */
4.5 D0H —— 输出驱动强度
| DS_IO[1:0] | 驱动强度 | 适用场景 |
|---|---|---|
| 00 | 25% | 短走线、单负载、低 EMI 要求 |
| 01(默认) | 50% | 常规应用 |
| 10 | 75% | 长走线或负载较重 |
| 11 | 100% | 多负载、高速 Quad 模式(120MHz)下仍不稳定时 |
调整驱动强度是信号完整性手段而非性能手段:Quad 模式 120MHz 读数据出错时,先用示波器看眼图,再决定降频还是加驱动。
4.6 写 Feature 的四步准则
/* Set Features 标准实现:06h -> 1Fh -> 轮询 -> 回读校验 */
int set_feature(uint8_t addr, uint8_t data)
{
uint32_t t0 = now_ms();
/* ① 写使能 */
spinand_wren(); /* 06h */
/* ② 下发 */
uint8_t tx[3] = {0x1F, addr, data};
spi_cs_low(); spi_transfer(tx, 3); spi_cs_high();
/* ③ 轮询 OIP */
while (spinand_get_status() & ST_OIP) {
if (now_ms() - t0 > FEATURE_TIMEOUT_MS) /* 建议 10 ms */
return -E_TIMEOUT;
}
/* ④ 回读校验 */
if (get_feature(addr) != data)
return -E_VERIFY;
return 0;
}