SPI NOR Flash 软件设计规范
编制日期 2026-09-21
嵌入式 SPI / QSPI / OSPI NOR 驱动与中间件设计约束 · 含 XT25Q512F ECC 与 62h 自刷新
| 文档编号 | SPI-NOR-SW-SPEC-001 |
|---|---|
| 版本 | V1.7 |
| 状态 | 正式发布 |
| 适用范围 | 嵌入式系统 SPI / QSPI / OSPI NOR Flash 驱动与中间件 |
| 参考标准 | JEDEC JESD216 (SFDP)、厂商 Datasheet、Linux spi-nor / Zephyr spi-nor 实践 |
0. 目的与范围
本规范规定 SPI NOR Flash 软件层的设计约束与推荐实现,目标是:
- 保证擦写可靠性(超时、状态轮询、掉电安全)
- 控制物理擦写次数(软件防抖 / 磨损均衡)
- 正确访问 >128 Mb(>16 MB)容量空间
- 规范使用 UID、Security Register、BP 保护、SFDP、OTP 等 NOR 特有能力
4BH)B9H 进入,ABH 唤醒9FH)一句话结论:所有写类操作遵循「WREN → 命令 → 轮询 WIP(带超时(默认 max×1.5,页编程取 max×2)+ 连续两次确认)→ 查 PE/EE」的固定协议;擦除粒度对齐数据更新单元、磨损在多扇区间轮转;>128Mb 首选 4-byte 地址模式并在启动链路上显式交接;保护用 BP+SRP 固化启动区、IBL 管运行时;UID/Security/OTP 一次性写入后必须 LB 锁死。
适用范围与阅读约定
- 本文是《SPI NOR Flash 电路设计指南》的软件配套篇:硬件篇解决「怎么接、怎么供电、怎么走线」,本文解决「驱动怎么写、协议怎么等、错误怎么救」。
- 命令码、寄存器位、时序值均取自 XT25Q512F 版本号 Rev 1.4 datasheet(2023-05-18,Preliminary)。其他容量 / 其他厂商型号存在差异,移植前必须逐项核对。
- 文中 C 代码为协议骨架伪码,演示流程与判断条件,非特定平台可直接编译的完整实现;
tick_ms()、spi_xfer()等为平台相关底座。
① 绝不用固定延时替代 WIP 轮询——擦写时间随温度、磨损、个体差异波动巨大;② 每次写操作前必须重新 WREN——WEL 在每次操作结束后自动清零;③ 超时阈值一律取 datasheet max 以上——typ 值只用来安排轮询节奏,不能当失败判据。
1 · 驱动架构与初始化
这一章回答:驱动分几层、各层管什么;上电后到第一次读写之前,软件必须按什么顺序做哪些事。
1.1 分层架构
推荐四层结构(图 1)。划层的意义在于把「协议规则」与「平台细节」隔离:换 MCU / 换 OS 只重写传输层;换 Flash 型号只更新命令层的参数表(容量、扇区、地址模式、超时常量),上层磨损管理与掉电恢复逻辑完全不动。
各层职责边界:
| 层 | 职责 | 本文对应章节 |
|---|---|---|
| 应用层 | 文件系统 / XIP / 资源读写 / 日志,感知不到 Flash 协议 | —(本文不展开) |
| 存储管理层 | 磨损均衡、擦写计数、A/B 双区与掉电恢复、坏区标记、擦除挂起调度 | 6.4、6.5、6.7、11.1~11.7 |
| 命令协议层 | WREN/WRSR、Read/PP/SE/BE 封装、WIP 轮询、超时与防抖、地址模式管理 | 3~7、13 |
| 传输抽象层 | SPI/QUAD 控制器配置、DMA、片选管理、时钟与模式(CPOL/CPHA 0/3 均支持) | 电路设计指南配套章 |
1.2 上电初始化流程
上电到「驱动就绪」必须完成五件事(图 2):等 tVSL、读 ID 识别、同步状态寄存器、配置地址模式与 QE、固化参数。
① VCC 到达 datasheet 下限(XT25Q512F 为 1.7V)后,至少等 200μs(tVSL)才能拉低 CS#;② 上电初始化期间器件会拒绝写类命令,初始化流程只做读操作。在电源爬坡未完成时发命令,是最常见的「偶尔上电认不到 Flash」根因,且往往只在低温 / 慢电源轨的机器上复现。
1.3 器件识别三级策略
识别按「新 → 旧 → 参数」三级降级,保证从新量产型号到老批次都能正确握手:
| 优先级 | 命令 | 返回内容(XT25Q512F) | 用途 |
|---|---|---|---|
| ① JEDEC ID | 9Fh | 0B 60 1A(厂商 0BH · 类型 60H · 容量 1AH=512Mb) | 主识别:厂商 + 容量一把抓 |
| ② 旧式 Device ID | 90h / ABh | MID = 0BH,DID = 19H | 兼容不支持 9Fh 的老控制器 |
| ③ SFDP | 5Ah | 参数表(容量/扇区/命令支持位) | 参数级自适应(见第 10 章) |
识别结果固化进驱动参数结构体,后续所有命令封装都从这里取地址宽度、页大小、扇区大小、超时常量:
/* SPI NOR 驱动参数结构体:初始化时一次性填充,运行期只读 */
struct spinor_params {
uint16_t page_size; /* 256,来自 SFDP / 静态表 */
uint32_t sector_size; /* 4096 */
uint32_t capacity; /* 0x04000000 = 64MB */
uint8_t addr4; /* 当前地址模式:0 = 3 字节,1 = 4 字节(对应 ADS 位) */
uint32_t t_se_max_ms; /* 扇区擦除超时:2500 × 1.5 → 4000ms */
uint32_t t_be_max_ms; /* 64KB 块擦除超时:6000 × 1.5 → 9000ms */
uint32_t t_pp_max_ms; /* 页编程超时:max 0.75ms ×2(PP 例外,其余操作用 ×1.5)→ 2ms */
uint32_t t_ce_max_s; /* 整片擦除超时:300 × 1.5 → 450s */
};
2 · 术语与缩写
全篇统一用词。先扫一遍这张表,后文不再逐处展开。
| 术语 | 说明 |
|---|---|
| SPI NOR | 串行 NOR Flash,按位编程、按扇区 / 块擦除 |
| Page | 编程最小逻辑单位,常见 256B |
| Sector | 最小擦除单位,常见 4KB |
| Block | 大块擦除单位,常见 32 / 64 / 256KB |
| WIP / BUSY | Status Register 忙标志,擦写进行中为 1 |
| WEL | Write Enable Latch,写使能锁存 |
| BP | Block Protect,块保护位 |
| SRP / WP# | Status Register Protect / 硬件写保护管脚 |
| SFDP | Serial Flash Discoverable Parameters(JESD216) |
| UID | Unique ID,芯片唯一序列号 |
| OTP / Security Register | 一次性可编程区,或可锁定的安全寄存器 |
| 4BA | 4-Byte Addressing,4 字节寻址模式 |
| ADS / ADP | Address Status / Address Default Power-up |
| tVSL | VCC 达到最小值后,到允许首条 SPI 命令的等待时间 |
| Continuous Read | Fast Read 等命令中的连续读 / 模式位(Mode Bits)状态 |
| Hybrid Sector | 同一器件内并存多种擦除粒度(如 4KB + 64KB) |
| XIP / Memory-Map | 将 NOR 映射到 CPU 地址空间,直接取指 / 取数 |
| WREN(06H) | Write Enable,允许对主阵列及非易失 Status 等编程 |
| Volatile SR WREN(50H) | Write Enable for Volatile Status Register,仅允许写易失 Status,不消耗 NV Status 寿命 |
| PE / EE | Program / Erase Error:编程 / 擦除出错标志(SR3 只读位) |
| DPD | Deep Power-Down,深掉电模式(B9H 进 / ABH 出) |
| RMW | Read-Modify-Write,读-改-写:改部分页数据前的必要动作 |
2.1 容量换算约定
- 128Mb = 16MB = 0x1000000 字节(恰好需 24 位地址;>128Mb 才需要超过 24 位)。
- 256Mb = 32MB,必须使用 4 字节寻址或等效扩展地址机制。
- 换算关系:1B = 8b,1Mb = 128KB,1MB = 8Mb。
- 本文中容量一律写作 Mb(兆位),文件大小一律写作 MB(兆字节),不混用。
2.2 命令码书写约定
如 06H = 0x06 = 十进制 6。表格与图内一律带 H 后缀;代码块内按 C 语言习惯写作 0x06。命令码不同厂商、不同容量档可能有差异,实现前必须与目标型号 datasheet 核对(见附录 A)。
3 · 状态寄存器与轮询协议
这一章是全文的地基:状态寄存器只有读对了、轮询等对了,后面所有读写擦的「完成判定」才可靠。
3.1 位图与关键位
XT25Q512F 有三个状态寄存器 + 一个扩展地址寄存器(图 3)。驱动需要直接打交道的位用蓝色标出。
三读三写的命令配对必须记牢:SR1 用 05h 读 / 01h 写,SR2 用 35h / 31h,SR3 用 15h / 11h。写非易失状态寄存器前发 06H(WREN),且 WRSR 自身要等 tW(最长 10ms);运行期频繁改保护改用 50H 易失写使能(见 3.2)。
3.2 WIP / WEL 语义:一忙一锁
| 位 | 位置 | 语义 | 驱动规则 |
|---|---|---|---|
| WIP | SR1.0 | Write In Progress:1 = 器件正在执行编程/擦除/写寄存器 | 所有写类命令之后的完成判定唯一依据;器件忙时只有读状态寄存器有效,不要发其他命令 |
| WEL | SR1.1 | Write Enable Latch:1 = 写使能已置位 | 每次 PP/SE/BE/WRSR 前必须重新发 06h;WEL=0 时一切写类命令被静默拒绝 |
WEL 的自动清零时机是新手驱动最常见的坑:它在每一次写类操作结束后自动回 0(上电 / 软复位 / WRDI / WRSR / WEAR / WCR / PP / SE / BE / CE / 擦写 Security Register 之后都会清)。所以「上次已经 WREN 过了」这句话永远不成立——每次写操作前重新发 06h,并读回确认 WEL=1,这就是写路径防抖的第一步。
06h 还是 50h?——非易失与易失写使能的区别
用 06h(WREN)写状态寄存器,写入的是非易失位(BP、QE、LC、ADP 等),掉电保持,但每次写状态寄存器本身就是一次对状态寄存器单元的擦写,且要等 tW(max 10ms)。
用 50h(Write Enable for Volatile Status Register)则做易失写:BP 等保护位立即生效但复位/掉电后回到非易失值,不消耗寿命、无等待。运行期需要频繁开关保护(比如写前解锁、写后上锁)的场景,一律用 50h + 易失写,把非易失写留给产线一次性配置。
3.3 轮询:超时分级 + 两次确认防抖
标准轮询循环见图 4,四个设计点缺一不可:
- 命令接受性检查:写命令发出后立刻读一次 WIP,应为 1。若为 0 且 WEL 已被清,说明命令根本没被接受(地址/数据被总线毛刺截断、或被保护位拦截),直接走错误路径,不要傻等超时。
- 先延时再轮询:按 typ 值的一半先睡(页编程 typ 0.4ms → 先睡 300μs),避免高速空转轮询。
- 超时阈值 = datasheet max × 1.5(页编程因 max 极短,推荐取 max×2 → 2ms):typ 只决定轮询节奏,max 才是失败判据(详见 6.2)。
- 两次确认防抖:WIP 从 1 变 0 后再读一次,两次一致才认定完成。SPI 线上 EMI 毛刺可能让单次读回瞬时错误值,把「其实还在擦」误判成「已完成」,接下来读出的就是半擦除的脏数据。
轮询骨架(第 3 章核心,6~11 章复用)
/* 通用「等待完成」:超时 + 防抖 + 错误位检查 */
int spinor_wait_idle(uint32_t timeout_ms, uint32_t poll_ms)
{
uint32_t t0 = tick_ms();
uint8_t stable = 0;
/* 先按典型值的一半睡一段,再开始轮询(调用方传入 poll_ms) */
delay_ms(poll_ms / 2);
for (;;) {
uint8_t sr = spinor_rdsr1(); /* 05H:忙时也允许读 SR */
if ((sr & 0x01) == 0) { /* WIP = 0 */
if (++stable >= 2) break; /* 防抖:连续 2 次一致 */
} else {
stable = 0;
}
if (tick_ms() - t0 > timeout_ms) {
/* 超时归因:读 SR3 看 PE/EE,30H 清除后交上层决定重试/复位 */
uint8_t sr3 = spinor_rdsr3(); /* 15H */
spinor_clear_flags(); /* 30H */
return (sr3 & 0x08) ? -EE_FAIL /* S19 = EE(bit3,0x08) */
: (sr3 & 0x04) ? -PE_FAIL /* S18 = PE(bit2,0x04) */
: -ETIMEDOUT;
}
delay_ms(poll_ms);
}
return 0;
}
注:示例中 PE/EE 位置按 SR3 的 S18/S19 返回;不同型号错误位定义有差异,以 datasheet 为准。
3.4 写状态寄存器:WREN → 写命令 → 轮询 → 超时,五步缺一不可
3.1 解决了「状态怎么读」,3.3 解决了「轮询怎么写」。这一节把两者合起来,给出写状态寄存器(SR1 / SR2 / SR3 / EAR)的标准五步。请注意:写状态寄存器本身也是一次擦写过程——它消耗状态寄存器单元的寿命,也要花 tW(典型 3 ms,最长 10 ms)才能落定。因此它既不能跳过忙闲判定,也不能用固定延时糊弄过去。
06H → 写命令 → 轮询 WIP → 超时归因)五条准则逐条说明:
| 步骤 | 动作 | 命令 | 为什么必须这么做 |
|---|---|---|---|
| ① 写使能 | 发 06H(WREN),把 WEL 置 1 | 06H | WEL 是一次性的:任何写操作结束后都会自动清零。「上次已经使能过」这个前提永不成立 |
| ② 确认 WEL | 读 SR1 检查 WEL=1,为 0 则直接返回错误 | 05H | WP# 拉低且 SRP=1 时状态寄存器被硬件锁死,此时 WEL 置不上去;不确认就写,命令会被静默丢弃 |
| ③ 发写命令 | 发寄存器写命令 + 数据字节,然后立刻释放片选 | 01H / 31H / 11H | 三者分别对应 SR1 / SR2 / SR3,命令与寄存器必须配对;写 SR1 时可连续跟两字节同时写 SR1+SR2 |
| ④ 轮询归零 | 反复读 SR1 的 WIP,直到为 0 | 05H | 禁止用固定延时代替轮询;typ 3 ms 只是典型值,低温、老化、电压偏低时都可能逼近 max 10 ms |
| ⑤ 超时判超 | 超过 Timeout 强制退出并归因,不能无限等待 | 15H / 30H | 没有超时的 while(1) 是现场死机的经典来源;超时后读 SR3 的 PE/EE 定位,用 30H 清除后才可重试 |
把五条准则串起来,就是一个可以直接落地的写函数:
/* 写状态寄存器:WREN → WRSR → 轮询 WIP → 超时归因,五步缺一不可 */
#define SR_WRITE_TIMEOUT_MS 20 /* tW max 10ms × 1.5,保守取整 20ms */
#define SR_POLL_US 200 /* 轮询间隔 */
#define SR_FIRST_DELAY_US 1500 /* 先睡 tW typ(3ms) 的一半,避免空转 */
int spinor_write_sr(uint8_t cmd, const uint8_t *data, int len)
{
uint32_t t0;
uint8_t sr;
/* ── 准则①:先发 06H 写使能 ───────────────────────── */
spinor_wren(); /* 06H */
/* ── 准则②:读回确认 WEL 真的置上了 ────────────────── */
sr = spinor_rdsr1(); /* 05H */
if (!(sr & 0x02)) /* WEL(SR1.1) */
return -EACCES; /* 未被接受:WP#/SRP 锁死或总线毛刺 */
/* ── 准则③:发写寄存器命令 + 数据 ─────────────────── */
spi_select();
spi_tx_byte(cmd); /* 01H / 31H / 11H */
spi_tx(data, len);
spi_deselect();
/* ── 准则④⑤:轮询 WIP 清零(两次确认)+ 超时归因 ───── */
delay_us(SR_FIRST_DELAY_US);
t0 = tick_ms();
{
uint8_t stable = 0;
for (;;) {
sr = spinor_rdsr1();
if ((sr & 0x01) == 0) { /* WIP = 0 */
if (++stable >= 2)
return spinor_check_errors(); /* 顺带查 SR3 的 PE/EE */
} else {
stable = 0;
}
if (tick_ms() - t0 > SR_WRITE_TIMEOUT_MS) {
uint8_t sr3 = spinor_rdsr3(); /* 15H:读失败归因 */
spinor_clear_flags(); /* 30H:清 PE/EE 才能恢复 */
return (sr3 & 0x04) ? -PE_FAIL /* S18 = PE */
: (sr3 & 0x08) ? -EE_FAIL /* S19 = EE */
: -ETIMEDOUT;
}
delay_us(SR_POLL_US);
}
}
}
① 忘记发 06H,或以为写使能会保持。WEL 在每次写操作结束后自动清零——包括上一次 WRSR 自身。每次写都要重新发 06H,并读回确认 WEL=1。
② 没有超时,或阈值取了 typ。写寄存器的 tW typ 是 3 ms 但 max 是 10 ms,拿 typ 当超时会在低温 / 老化的机器上随机失败;不设超时则会因为一次总线异常把整个系统挂死。
③ 用非易失写做运行期频繁动作。写前解锁、写后上锁这类每帧都做的动作,若走 06H + 01H,每次都在消耗状态寄存器的擦写寿命。改用 50H(易失写):保护位立即生效、掉电回退到原值,且不占用非易失写的次数。
现象反查表:
| 现象 | 根因 | 定位手法 |
|---|---|---|
| 写寄存器后读回仍是原值 | 没发 06H,或 WEL 已被上一次操作自动清零 | 写前用 05H 读 SR1,确认 WEL=1 再继续 |
| 写完全无效果、也不报错 | WP# 被拉低且 SRP1/SRP0=1,状态寄存器硬件锁定 | 量 WP# 电平;读 SR3 的 SRP1 与 SR1 的 SRP0 |
| 偶尔成功偶尔失败 | 没等 WIP 清零就发下一条命令 | 每条写寄存器之后必须轮询到 WIP=0 |
| 驱动卡死在 while | 没设超时,或阈值小于 10 ms | 按 tW max × 1.5 设 15~20 ms,并做超时归因 |
| 长期运行后某位再也写不进 | 非易失位擦写寿命耗尽 | 该位改用 50H 易失写;必要时整片更换 |