只读分享解决方案文档原理方案设计📅 2026-09-21🔖 V1.7✅ 长期有效
🔧解决方案&应用市场分析 -> 原理方案设计 -> SPI_NOR_Flash_软件设计规范

SPI_NOR_Flash_软件设计规范

生成时间 2026-09-21 10:58:30 长期有效

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 特有能力
不在本规范范围:PCB 硬件时序设计、具体厂商指令码差异明细表(应以 Datasheet / SFDP 为准)。
45ms / 2.5s
扇区擦除 typ / max
超时阈值必须按 max 设计
100K 次
每扇区 P/E 寿命
不要反复擦同一物理扇区
256B / 4KB
页 / 扇区
编程按页对齐,擦除按扇区
128bit
出厂唯一 UID (4BH)
防克隆与密钥派生
3×1024B
Security Register
OTP,LB 锁定不可逆
3 / 4 字节
地址模式
>128Mb 必须解决寻址
2.5μA
Deep Power-Down 电流
B9H 进入,ABH 唤醒
0B 60 1A
JEDEC ID (9FH)
XTX · 512Mb · XT25Q512F

一句话结论:所有写类操作遵循「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 代码执行 · 参数/资源读写 · 日志存储管理层磨损均衡 · A/B 双区与掉电恢复 · 擦写计数 · 坏区标记命令协议层WREN/WRSR · Read/PP/SE/BE · WIP 轮询与超时 · 4-byte 地址传输抽象层SPI/QUAD SPI 控制器 · DMA · 片选与时序参数本规范的覆盖范围第 4~12 章读写擦协议、保护与安全功能第 6 章磨损管理在本层实现第 3 章轮询/超时/防抖在本层实现仅需重写的薄层换平台只改传输层纵向调用、逐层单向依赖;命令层以下与具体容量/厂商解耦,靠 SFDP + JEDEC ID 自适应
图 1 · SPI NOR 驱动分层架构

各层职责边界:

职责本文对应章节
应用层文件系统 / 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 下限1.7V(XT25Q512F)等待 tVSL ≥ 200 μs上电后此期间不要拉低 CS#发 9Fh 读 JEDEC IDXT25Q512F = 0B 60 1AID 匹配且连续 2 次一致?(软件防抖:防上电期总线毛刺)重试 ≤ 3 次间隔 1ms 递增仍失败 → 报告无器件/接线错误读 SR1/SR2/SR3 同步状态检查 WEL/WIP/QE/ADS/SUS按需配置地址模式与 QEB7h 进 4-byte / WRSR 置 QE读 SFDP(5Ah) 固化参数容量/扇区/页大小/4-byte 支持驱动就绪参数结构体填充完成上电初期器件会拒绝写类命令,只做读操作是安全且必要的出厂状态全片 FFH(已擦态),编程只能 1→0
图 2 · 上电初始化流程(含识别防抖)
两条硬性时序约束(违反必出偶发故障)

① VCC 到达 datasheet 下限(XT25Q512F 为 1.7V)后,至少等 200μs(tVSL)才能拉低 CS#;② 上电初始化期间器件会拒绝写类命令,初始化流程只做读操作。在电源爬坡未完成时发命令,是最常见的「偶尔上电认不到 Flash」根因,且往往只在低温 / 慢电源轨的机器上复现。

1.3 器件识别三级策略

识别按「新 → 旧 → 参数」三级降级,保证从新量产型号到老批次都能正确握手:

优先级命令返回内容(XT25Q512F)用途
① JEDEC ID9Fh0B 60 1A(厂商 0BH · 类型 60H · 容量 1AH=512Mb)主识别:厂商 + 容量一把抓
② 旧式 Device ID90h / ABhMID = 0BH,DID = 19H兼容不支持 9Fh 的老控制器
③ SFDP5Ah参数表(容量/扇区/命令支持位)参数级自适应(见第 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 / BUSYStatus Register 忙标志,擦写进行中为 1
WELWrite Enable Latch,写使能锁存
BPBlock Protect,块保护位
SRP / WP#Status Register Protect / 硬件写保护管脚
SFDPSerial Flash Discoverable Parameters(JESD216)
UIDUnique ID,芯片唯一序列号
OTP / Security Register一次性可编程区,或可锁定的安全寄存器
4BA4-Byte Addressing,4 字节寻址模式
ADS / ADPAddress Status / Address Default Power-up
tVSLVCC 达到最小值后,到允许首条 SPI 命令的等待时间
Continuous ReadFast 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 / EEProgram / Erase Error:编程 / 擦除出错标志(SR3 只读位)
DPDDeep Power-Down,深掉电模式(B9H 进 / ABH 出)
RMWRead-Modify-Write,读-改-写:改部分页数据前的必要动作

2.1 容量换算约定

位(b)与字节(B)必须分清
  • 128Mb = 16MB = 0x1000000 字节(恰好需 24 位地址;>128Mb 才需要超过 24 位)。
  • 256Mb = 32MB,必须使用 4 字节寻址或等效扩展地址机制。
  • 换算关系:1B = 8b1Mb = 128KB1MB = 8Mb
  • 本文中容量一律写作 Mb(兆位),文件大小一律写作 MB(兆字节),不混用。

2.2 命令码书写约定

H 后缀统一表示十六进制

06H = 0x06 = 十进制 6。表格与图内一律带 H 后缀;代码块内按 C 语言习惯写作 0x06。命令码不同厂商、不同容量档可能有差异,实现前必须与目标型号 datasheet 核对(见附录 A)。

3 · 状态寄存器与轮询协议

这一章是全文的地基:状态寄存器只有读对了、轮询等对了,后面所有读写擦的「完成判定」才可靠。

3.1 位图与关键位

XT25Q512F 有三个状态寄存器 + 一个扩展地址寄存器(图 3)。驱动需要直接打交道的位用蓝色标出。

SR105H 读 / 01H 写SRP0S7BP4S6BP3S5BP2S4BP1S3BP0S2WELS1WIPS0SR235H 读 / 31H 写SUS1S15WPSS14LB3S13LB2S12LB1S11SUS2S10QES9ADSS8SR315H 读 / 11H 写LC1S23DRV1S22DRV0S21ADPS20EES19PES18LC0S17SRP1S16EARC8H / C5HSECEA7PSEA6EA5EA4DLPEA3EA2A25EA1A24EA0蓝色高亮 = 驱动直接依赖位:WIP/WEL(轮询)、QE、ADS/ADP(地址)、PE/EE(错误)、A25/A24(bank)LB1/LB2/LB3 = Security Register OTP 锁(S13/S12/S11),置 1 后不可逆
图 3 · 状态寄存器与扩展地址寄存器位图(XT25Q512F;EA6=PS 定义见 AN10071)

三读三写的命令配对必须记牢:SR1 用 05h 读 / 01h 写,SR2 用 35h / 31h,SR3 用 15h / 11h写非易失状态寄存器前发 06H(WREN),且 WRSR 自身要等 tW(最长 10ms);运行期频繁改保护改用 50H 易失写使能(见 3.2)。

3.2 WIP / WEL 语义:一忙一锁

位置语义驱动规则
WIPSR1.0Write In Progress:1 = 器件正在执行编程/擦除/写寄存器所有写类命令之后的完成判定唯一依据;器件忙时只有读状态寄存器有效,不要发其他命令
WELSR1.1Write 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,四个设计点缺一不可:

  1. 命令接受性检查:写命令发出后立刻读一次 WIP,应为 1。若为 0 且 WEL 已被清,说明命令根本没被接受(地址/数据被总线毛刺截断、或被保护位拦截),直接走错误路径,不要傻等超时。
  2. 先延时再轮询:按 typ 值的一半先睡(页编程 typ 0.4ms → 先睡 300μs),避免高速空转轮询。
  3. 超时阈值 = datasheet max × 1.5(页编程因 max 极短,推荐取 max×2 → 2ms):typ 只决定轮询节奏,max 才是失败判据(详见 6.2)。
  4. 两次确认防抖:WIP 从 1 变 0 后再读一次,两次一致才认定完成。SPI 线上 EMI 毛刺可能让单次读回瞬时错误值,把「其实还在擦」误判成「已完成」,接下来读出的就是半擦除的脏数据。
发出写类命令PP / SE / BE / WRSR(06H 已置 WEL)命令接受性检查读 WIP 应为 1(防命令被截断)先延时 t_typ × 0.5不必立刻轮询(见右表节奏)读 RDSR1,WIP == 1 ?检查超时计数 + 间隔后再读超时 → ETIMEDOUT读 PE/EE 归因(30H 清除)防抖:再读一次仍为 0 ?连续 2 次一致才认定完成按毛刺处理,继续轮询读 SR3 检查 PE / EE错误位只在 WIP=0 时可读返回成功(有错误则走重试/复位路径)推荐轮询起始延时与间隔Page Program300 μs / 100 μsSector Erase20 ms / 5 msBlock Erase100 ms / 20 msChip Erase80 s / 2 s超时阈值 = datasheet max × 1.5(分级表见 6.2 节)
图 4 · WIP 轮询:超时分级 + 两次确认防抖

轮询骨架(第 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(WREN)写使能:置位 SR1 的 WEL② 读回确认 WEL=105H 读 SR1:WEL=0 说明未被接受,直接报错③ 发写寄存器命令01H(SR1) / 31H(SR2) / 11H(SR3),紧跟数据字节④ 轮询 WIP 直到 005H 读 SR1 bit0;忙时只有读状态有效⑤ 超时判超与归因超时 → 读 SR3 查 PE/EE,30H 清标志后返回反复轮询超时必须按 max,不能按 typ写寄存器 tWtyp 3 ms / max 10 ms推荐 Timeoutmax × 1.5 = 15 ms(保守取 20 ms)轮询节奏先睡 typ×0.5 ≈ 1.5 ms,再 200 μs 一次失败归因SR3 的 PE(S18) / EE(S19),30H 清除读写命令必须配对SR105H 读 / 01H 写SR235H 读 / 31H 写SR315H 读 / 11H 写易失写50H(不改非易失位,掉电回退)频繁改保护位?用 50H 易失写· 06H + 01H:非易失写,掉电保持· 但每次都消耗一次状态寄存器擦写· 写前解锁 / 写后上锁这类动作· 一律走 50H,非易失留给产线
图 5 · 写状态寄存器的五步流程与超时设定(06H → 写命令 → 轮询 WIP → 超时归因)

五条准则逐条说明:

步骤动作命令为什么必须这么做
① 写使能06H(WREN),把 WEL 置 106HWEL 是一次性的:任何写操作结束后都会自动清零。「上次已经使能过」这个前提永不成立
② 确认 WEL读 SR1 检查 WEL=1,为 0 则直接返回错误05HWP# 拉低且 SRP=1 时状态寄存器被硬件锁死,此时 WEL 置不上去;不确认就写,命令会被静默丢弃
③ 发写命令发寄存器写命令 + 数据字节,然后立刻释放片选01H / 31H / 11H三者分别对应 SR1 / SR2 / SR3,命令与寄存器必须配对;写 SR1 时可连续跟两字节同时写 SR1+SR2
④ 轮询归零反复读 SR1 的 WIP,直到为 005H禁止用固定延时代替轮询;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 易失写;必要时整片更换
📥 芯参谋客户端下载