并行 NOR Flash(PPI NOR)软件设计规范
编制日期 2026-10-05 | 版本号 Rev 1.4
面向 异步 SRAM 类并行总线 NOR(CFI Primary Command Set 0x0002 / AMD-Fujitsu 命令集,x8 / x16, TSOP-56 与 BGA-64)的固件驱动设计约定 —— 覆盖 解锁周期与命令序列、DQ7/DQ6/DQ5 状态轮询、 写缓冲与 Unlock Bypass、扇区保护(PPB / DYB / WP#)、XIP 冲突处置 与 掉电数据完整性,逐条给出可落地的命令序列、轮询判据与超时预算。
| 文档编号 | PPI-NOR-SW-SPEC-001 |
|---|---|
| 版本 / 状态 | V1.0 · 正式发布 |
| 适用器件 | 芯天下 XTX XT29F(XT29F512ATSHGA / XT29F01GATSHGA)、M29F(M29F512BTSJGA / M29F512BBGJGA / M29F01GBTSJGA / M29F01GBBGJGA / M29F02GBBGJGA)及命令集兼容的第三方并行 NOR |
| 参考标准 | JEDEC JEP137 / JESD68(CFI)· CFI Primary Command Set 0x0002 · 各型号 datasheet 与 Errata |
| 配套文档 | 并行NOR_PPI_NOR_电路设计指南.html(原理图 / 布线 / 时序 / 信号完整性) |
0. 目的与范围
本规范约束并行 NOR 器件在嵌入式固件中的软件行为:命令序列、地址换算、状态轮询、扇区保护、 XIP 冲突处置、掉电保护。凡本文与器件 datasheet 冲突,以 datasheet 为准。
- 统一命令序列:读(无命令,直接寻址)、编程(解锁 + A0h + 地址 + 数据)、擦除(六周期)各自固化为一套标准流程。
- 统一状态判读:只用 RY/BY# 或 DQ7 / DQ6 判定完成,并强制配 DQ5 超时二次确认,禁止固定延时。
- 统一地址换算:明确 x8 / x16 下的命令地址与「字地址 ↔ 字节地址」关系,消灭移植时最高频的地址错位 bug。
- 统一 XIP 规则:擦写期间禁止从本器件取指,给出三种可执行方案与各自的适用边界。
- 统一掉电保护:给出写窗口、A/B 双区提交协议与 IAP 双 Bank 流程,明确哪些操作是原子的。
一句话结论:并行 NOR 是「读像 SRAM、写像命令」的器件 —— 读只要拉低 CE#/OE# 就出数据, 任何写都是解锁周期(0x555 ← AA / 0x2AA ← 55)+ 命令码;擦写完成只看 RY/BY# 或 DQ7/DQ6, 并且必须用 DQ5 兜底判超时。 不做轮询的写会写出半字,不配 DQ5 的轮询会死循环,XIP 下不隔离取指的擦写会让程序直接跑飞。
文档使用说明
- 本文是《并行 NOR Flash(PPI NOR)电路设计指南》的软件配套篇:硬件篇解决「怎么接、怎么供电、怎么走线、等待周期怎么算」,本文解决「驱动怎么写、命令怎么发、状态怎么判、XIP 怎么保命」。
- 命令码以 CFI Primary Command Set = 0x0002(AMD / Fujitsu)为准,这是并行 NOR 的主流命令集;Intel 命令集(0x0001)器件不在本文范围内,须另行核对。
- 文中代码为 x16 总线示例(
volatile uint16_t *),x8 总线需按 1.3 节的换算规则调整地址与数据宽度。 - 文中 C 代码为协议骨架伪代码,演示流程与判断条件;
nor_cmd()、systick_ms()、delay_us()为平台相关底层。
① 绝不用固定延时替代状态轮询——擦写时间随 P/E 次数、温度、个体差异成倍变化,典型值只能用来安排轮询节奏; ② 写/擦前必须确认扇区已解保护且已擦除——DYB 上电默认为保护态,且 NOR 只能把 1 写成 0; ③ XIP 下擦写期间禁止取指——轮询本身也是「读器件」,读到的是状态位而非指令。
1 · 器件定位与阵列组织
并行 NOR 与串行器件最大的差别是地址线直连:CPU 的地址总线就是存储器的地址,读操作不需要任何命令。 软件设计的第一件事,是把「擦除粒度」和「地址换算」这两件事彻底搞清楚。
1.1 器件定位与关键参数
上述为跨厂商、跨工艺的典型值区间,仅用于方案评估;定稿前必须用目标型号 datasheet 对应表格替换(XTX 系列以 datasheet 为准)。
| 项目 | 取值 / 范围 | 软件影响 |
|---|---|---|
| 接口形态 | 异步 SRAM 类并行总线,40 ~ 50 根引脚 | 读无需命令;需配置总线等待周期 |
| 位宽 | x8 / x16(BYTE# 引脚选择) | 地址换算与命令地址随位宽变化(1.3 节) |
| 容量范围 | 1 Mb ~ 2 Gb(XTX 当前 512 Mb ~ 2 Gb) | 容量从 CFI 0x27 读取,不要硬编码 |
| 速度等级 | tACC = 70 / 90 / 100 / 110 ns | 决定 SoC 等待周期;配错表现为偶发读错 |
| 工作电压 | 3 V 型 2.7 ~ 3.6 V;1.8 V 型 1.65 ~ 1.95 V | CFI 0x1B/0x1C 以 BCD 给出,上电自检应核对 |
| 命令集 | CFI Primary Command Set = 0x0002 | 本文全部命令基于此;0x0001(Intel)不适用 |
| 坏块 / ECC | 出厂基本无坏块;不需要 ECC | 只需维护「运行期失败扇区表」(7.6 节) |
| 封装 | TSOP-56(14×20 mm)/ BGA-64(8×10 ~ 9×11 mm) | 仅影响硬件,软件无差异 |
1.2 区块(Region)与扇区组织
并行 NOR 的擦除粒度不是全片统一的:器件把不同大小的扇区分成若干个「区块区域(Erase Block Region)」。 典型布局是「底部 / 顶部若干个 8 KB 小扇区 + 中间大量 64 KB 大扇区」,小扇区通常留给 bootloader 与参数区。
把 sector_size = 64 * 1024 写进驱动的后果:在「8 KB × 8 + 64 KB × 254」布局的器件上,擦除底部 8 KB 小扇区时会把相邻 56 KB 一起擦掉;
而在 128 KB 大扇区器件上,一次擦除只覆盖了半个扇区,擦除永远「擦不干净」。
正确做法:上电遍历 CFI 区块区域表,构建一张「扇区起始地址 → 扇区大小」的查找表,所有擦写 API 只接受扇区索引。
1.3 位宽、地址错位与字节序
这是并行 NOR 移植时出错率最高的一节。同一条命令序列,在 x8 与 x16 下落到 SoC 地址总线上的数值并不相同。
| 模式 | BYTE# | 编程单位 | 解锁地址(SoC 侧) | 数据宽度 | DQ15 作用 |
|---|---|---|---|---|---|
| x8 | 0(下拉) | 1 字节 | 字节地址 0x555 / 0x2AA | 8 bit | 复用为 A-1(最低位地址) |
| x16 | 1(上拉) | 1 字(2 字节) | 字节地址 0xAAA / 0x554 | 16 bit | 数据最高位 DQ15 |
BYTE# 由外部电阻固定(x16 上拉 10 kΩ 到 VCC,x8 下拉到地),绝不允许浮空。软件侧无法补救: 浮空时器件可能间歇性地在两种位宽间跳变,表现为「大部分时间能读,偶尔读出错位数据」,极难定位。
驱动初始化时应读取 CFI 0x28 接口描述字段确认实际位宽(0 = x8 only / 1 = x16 only / 2 = 可切换),与编译期配置不符即报错停机。
1.4 与 SPI NOR / 并行 NAND 的关键差异
| 维度 | 并行 NOR(本文) | SPI NOR | 并行 NAND |
|---|---|---|---|
| 读方式 | 地址直连,拉 CE#/OE# 即出数据,无命令 | 发 03h + 地址 + dummy | 发命令 + 地址,页读进 cache |
| 随机读延迟 | 70 ~ 110 ns | 首字节 60 ~ 100 ns + 指令开销 | 25 ~ 50 µs(页搬 SRAM) |
| 写方式 | 解锁周期 + A0h + 地址 + 数据 | 06h + 02h + 地址 + 数据 | 命令 + 页装载 + 编程执行 |
| 状态判读 | DQ7 / DQ6 / DQ5 + RY/BY# 引脚 | 读状态寄存器 05h | 读状态 / R/B# 引脚 |
| 坏块 / ECC | 出厂无坏块,不需要 ECC | 不需要 | 必须 BBT + 硬件 ECC |
| 擦除粒度 | 扇区 4 KB ~ 128 KB(多区域混合) | 4 KB / 32 KB / 64 KB | 块 128 KB ~ 1 MB |
| XIP | 原生最优(地址直映射) | 好(需 cache / 预取) | 不可,需影子 RAM |
| 主谓引脚数 | 40 ~ 50 根 | 4 ~ 6 根 | 复用 8 / 16 位,28 ~ 48 根 |
2 · 术语与缩写
本章统一术语口径,避免「扇区 / 块 / 区域 / 字」在文档与代码里指的不是同一件事。
2.1 接口与命令术语
| 缩写 / 术语 | 全称 | 本文中的含义 |
|---|---|---|
| Sector / Block | 扇区 / 块 | 擦除的最小单位,4 KB ~ 128 KB,大小由器件决定 |
| Region | Erase Block Region | 同尺寸扇区的集合;CFI 0x2C 给出区域数,0x2E 起逐区描述 |
| Word | — | x16 模式下的编程单位(2 字节);x8 模式下为 Byte |
| PA / PD | Program Address / Data | 编程命令最后两个周期给出的目标地址与数据 |
| SA | Sector Address | 目标扇区内任意地址;擦除 / 保护命令用它定位扇区 |
| Unlock Cycle | 解锁周期 | 命令序列前置的 0x555←AA / 0x2AA←55 写操作 |
| DQ7 | Data# Polling | 操作进行中读出「写入值 DQ7 的反码」,完成后读出真值 |
| DQ6 | Toggle Bit 1 | 操作进行中连续读会翻转,完成后停止翻转 |
| DQ5 | Error / Timeout | 超出内部时序限制时置 1;须二次确认后才判失败 |
| DQ3 | Erase Timer | 擦除命令窗口内 = 0,真正开始擦除后 = 1 |
| DQ2 | Toggle Bit 2 | 擦除挂起期间持续翻转,与 DQ6 组合判断挂起状态 |
| DQ1 | Write Buffer Abort | 写缓冲装载了非法地址 / 跨扇区时置 1,本次缓冲作废 |
| RY/BY# | Ready / Busy | 开漏输出,低 = 忙;最省 CPU 的完成判据 |
| CFI | Common Flash Interface | JEDEC JEP137 / JESD68 定义的自描述查询区 |
| PPB | Persistent Protection Bit | 非易失保护位,写次数有限,适合永久保护 |
| DYB | Dynamic Protection Bit | 易失保护位,上电默认保护,擦写前必须显式解除 |
| XIP | eXecute In Place | CPU 直接从 NOR 取指执行,无需搬到 RAM |
| RWW | Read-While-Write | 一个 Bank 读的同时擦写另一个 Bank(需器件支持) |
| A-1 | — | x8 模式下由 DQ15 复用充当的最低位地址 |
2.2 本文书写约定
- 命令码用小写十六进制带后缀
h书写(如A0h、F0h);写周期写作0x555 ← 0x00AA,表示「向地址 0x555 写入数据 0x00AA」。 - x16 模式为默认示例;x8 模式下的差异在 1.3 节与附录 A 单独标注。
- 状态位写作
DQ7/DQ6形式,判据用掩码表示,如(d1 ^ d2) & 0x40判断 DQ6 是否翻转。 - 时序符号沿用 datasheet:tACC(地址到数据有效)、tSE(扇区擦除)、tWHWH1(单字编程)、tWHWH2(写缓冲编程)。「典型值 / 最大值」成对给出时,超时必须按最大值预算。
- 代码示例为 C 伪代码,省略 SoC 总线控制器配置,只表达命令序列与判据。
3 · 驱动架构与初始化
并行 NOR 的驱动比串行器件多两件事:总线时序配置与 XIP 隔离。这两件事没做对,后面的命令序列写得再标准也跑不起来。
3.1 分层职责
| 层 | 职责 | 禁止做的事 |
|---|---|---|
| 应用层 | 业务调用 read/write/ioctl;不感知扇区与命令 | 直接拼解锁周期、直接算扇区地址 |
| 存储管理层 | 扇区表、坏扇区表、保护状态、提交协议、IAP 流程 | 绕过扇区表直接擦写物理地址 |
| 命令协议层 | 解锁周期、命令码、状态轮询、Reset 恢复 | 省略解锁、用固定延时代替轮询 |
| 总线抽象层 | 等待周期、位宽、地址移位、XIP 隔离(RAM 驻留) | 在命令序列中途切换总线时序或频率 |
发送解锁周期与轮询状态的代码本身不能放在被操作的 NOR 上执行——擦写期间从 NOR 取指读到的是状态位,不是指令。
工程做法:把 nor_cmd()、nor_wait_ready()、nor_program()、nor_erase() 用链接脚本放到 RAM 段(如 __attribute__((section(".ramfunc")))),或整段用 __ramfunc 修饰。
3.2 上电初始化流程
初始化顺序不能颠倒:先有时序,后有识别。总线等待周期没配好就去读 CFI,读回来的一定是噪声。
/* 上电初始化:返回 0 表示成功 */
int nor_init(void)
{
/* 1. 复位释放后等待 tRH(按 datasheet,通常 µs ~ ms 级) */
board_reset_release(); delay_us(100);
/* 2. 总线:先给足等待周期,后续可收敛 */
bus_config(BUS_WIDTH_16, WAIT_STATES_SAFE); /* 例如 30 个等待周期 */
/* 3. 读复位 */
nor_cmd(0x0000, 0x00F0);
/* 4. CFI 查询 —— 读不到 'QRY' 直接失败 */
if (cfi_probe(&g_cfi) != 0) return -E_NODEV;
if (g_cfi.interface == 2 && g_cfi.interface != BUS_WIDTH_16)
return -E_WIDTH; /* 位宽与配置不符 */
/* 5. Autoselect 校验厂商 / 器件 ID */
if (autoselect_check(EXPECT_MID, EXPECT_DID) != 0) return -E_NODEV;
/* 6. 构建扇区表 + 坏扇区表 */
sector_table_build(&g_cfi);
bad_sector_table_load();
return 0;
}
① 等待周期给得太紧。首版调试时先把等待周期设到最大(例如 30 个),确认 CFI 能读出来,再二分收敛。 反过来做的话,「读不到 QRY」会被误判成器件或焊接问题,排查方向完全跑偏。
② 忘了发 F0h 读复位。若上一次运行停在 Autoselect 或 CFI 模式(例如看门狗复位前正在读 ID), 本次上电后器件仍在特殊模式,读出来的是 ID 或查询表而不是阵列数据。F0h 是唯一能把它拉回来的通用手段。
3.3 器件识别:CFI / Autoselect 双通道
- CFI 查询(首选):向
0x55写0x0098(x8 模式为向0xAA写0x98)进入查询模式,按偏移读取自描述信息。它给出容量、写缓冲深度、区块区域表、编程/擦除典型与最大超时、VCC 范围,是驱动自适应多料号的唯一可靠来源。 - Autoselect(校验用):三周期进入(
0x555←AA/0x2AA←55/0x555←90),读偏移 0x00 得制造商 ID、0x01 得器件 ID,用于防贴错料。读完必须发 F0h 退出。 - 交叉校验:CFI 0x27 给出的容量应与 Autoselect 器件 ID 推断的容量一致;不一致说明读到了噪声或贴错料,应停机而非继续。
只要驱动需要兼容两家以上供应商或两种以上容量,扇区布局、写缓冲深度、超时参数就必须来自 CFI。 硬编码的代价不是「换料要改代码」,而是「换料后静默擦错扇区」——后者会烧出批量事故。
4 · 命令体系与解锁周期
并行 NOR 的所有写操作(编程、擦除、识别、保护)都遵循同一个骨架:解锁周期 → 命令码 → 地址 / 数据。 命令码写错、解锁地址写错,器件都会静默忽略——既不执行也不报错。
4.1 命令集总览
| 命令 | 周期数 | 地址序列(x16) | 数据序列(x16) |
|---|---|---|---|
| 读阵列 / 复位 | 1 | 任意 | 0x00F0(部分器件 0x00FF) |
| Autoselect(读 ID) | 3 | 0x555 / 0x2AA / 0x555 | 0x00AA / 0x0055 / 0x0090 |
| CFI 查询 | 1 | 0x0055(x8:0x00AA) | 0x0098 |
| 字 / 字节编程 | 4 | 0x555 / 0x2AA / 0x555 / PA | 0x00AA / 0x0055 / 0x00A0 / PD |
| Unlock Bypass | 2 | 0x555 / 0x2AA | 0x00AA / 0x0020 |
| 旁路编程 | 2 | 任意 / PA | 0x00A0 / PD |
| 旁路复位 | 2 | 任意 / 任意 | 0x0090 / 0x0000 |
| 写缓冲编程 | 3 + N | 0x555 / SA / SA | 0x00AA / 0x0025 / (N−1);随后 N 字;末周期 0x0029 |
| 扇区 / 块擦除 | 6 | 0x555 / 0x2AA / 0x555 / 0x555 / 0x2AA / SA | AA / 55 / 80 / AA / 55 / 30 |
| 整片擦除 | 6 | 同上,末地址为 0x555 | … / 0x0010 |
| 擦除挂起 / 恢复 | 1 | 任意 | 0x00B0 / 0x0030 |
| 状态判读(DQ 轮询) | 1 | 任意 | DQ7/DQ6/DQ5 轮询 (AMD 无 0x70/0x71 状态命令) |
扇区保护(PPB / DYB / Password)为厂商自定义扩展,命令序列各厂商不同,必须查各自的 application note;本文只给出软件层面的使用原则(第 9 章)。
4.2 解锁周期与地址错位
/* 16 位总线:命令写宏(wa 为字地址,SoC 侧自动右移 1 位输出) */
static volatile uint16_t *const nor = (volatile uint16_t *)NOR_BASE;
static void nor_cmd(uint32_t wa, uint16_t d) { nor[wa] = d; }
/* 单字编程:解锁 → A0h → PA/PD */
int nor_program_word(volatile uint16_t *pa, uint16_t pd)
{
nor_cmd(0x555, 0x00AA);
nor_cmd(0x2AA, 0x0055);
nor_cmd(0x555, 0x00A0);
*pa = pd; /* 第 4 周期:目标地址 + 数据 */
return nor_wait_ready(pa, pd, T_PROG_US);
}
4.3 读复位 F0h:万能恢复键
F0h 是单周期命令,可在任意地址写入,作用是让器件从任何特殊模式(Autoselect / CFI / 状态读 / 旁路)
退回读阵列状态,同时中止正在进行的编程或擦除。
- 它是异常恢复的第一动作:状态位异常、读不到正确数据、操作超时,先发 F0h 再排查。
- 部分器件也接受
FFh;优先用 F0h,它是 CFI 0x0002 命令集的标准复位码。 - 中止擦除后,被操作的扇区处于不确定状态,必须重新擦除,不能直接使用。
- 写缓冲编程中途发 F0h,本次缓冲内容作废,需重新装载。
4.4 Unlock Bypass
连续编程多个字时,每次都发三个解锁周期会浪费约 30 % 的总线带宽。Unlock Bypass 让器件进入「已解锁」状态,后续每个字只需 2 个周期。
/* Unlock Bypass 批量编程:进入 → N 个字 → 退出 */
int nor_program_bypass(volatile uint16_t *pa, const uint16_t *buf, size_t n)
{
nor_cmd(0x555, 0x00AA);
nor_cmd(0x2AA, 0x0020); /* 进入旁路 */
for (size_t i = 0; i < n; i++) {
*pa = 0x00A0; /* 旁路编程命令 */
pa[i] = buf[i]; /* 第 2 周期:地址 + 数据 */
if (nor_wait_ready(&pa[i], buf[i], T_PROG_US) != 0) {
nor_bypass_reset(); /* 异常也要退出,别把器件留在旁路 */
return -E_PFAIL;
}
}
nor_bypass_reset(); /* 0x0090 → 0x0000 */
return 0;
}
static void nor_bypass_reset(void)
{
nor_cmd(0x0000, 0x0090);
nor_cmd(0x0000, 0x0000);
}
4.5 写缓冲编程
写缓冲是最高效的大块写入方式:一次装载最多 16 ~ 32 字(由 CFI 0x2A 给出),再统一执行内部编程。
- 装载的字数 N 必须写在第 3 周期的
N−1位置,写错会导致缓冲长度不符。 - 所有装载地址必须落在同一个扇区内;跨扇区会触发 DQ1 放弃标志,本次缓冲作废。
- 装载完成后必须写「缓冲编程确认」
0x0029到扇区地址 SA,内部编程才真正开始。 - 缓冲编程的地址连续性要求各厂商略有差异(是否要求严格连续递增),必须查 datasheet。
4.6 命令下发五步准则
- 进入临界区:XIP 下禁止取指(RAM 执行 / 关中断 / 双 Bank),并禁止其它任务抢占总线。
- 下发解锁周期:
0x555 ← 0x00AA、0x2AA ← 0x0055(x8 模式地址与数据宽度按 1.3 节换算)。 - 下发命令码与地址 / 数据:严格按命令表的周期数,不多发也不少发。
- 轮询等待完成:RY/BY# 或 DQ7 / DQ6,并强制配 DQ5 二次确认(第 8 章)。
- 退出与校验:Autoselect / CFI / 旁路必须显式退出;编程后回读比对,擦除后做 Blank Check。
解锁周期与命令码之间是状态机相关的连续写。若中间被中断服务程序插进一次对同一片选的访问(哪怕只是一次读), 解锁状态可能被破坏,器件会把后续字节当成普通写——结果是「命令没生效,而且扇区里多了几个莫名其妙的字」。
对策:把整个命令序列封装成关中断的临界区,临界区内只访问 NOR 的命令地址,不做任何其它总线操作。