Linux 系统(SPI NOR)驱动开发指南
编制日期 2026-10-28
面向 XTX XT25Q512F / XT25F512B / XT55Q1G / XT25Q256F 等串行 NOR 器件,覆盖 开发步骤 → 内核配置 → 驱动架构 → 设备树与分区 → 大容量 4 字节地址(>128Mbit)→ 新器件适配 → 测试 → 调试排障 → 性能优化 → 可靠性 → 量产运维 的完整链路。调试章节按 P0~P5 优先级 + 故障树 组织,每一条给出现象 → 判据 → 根因 → 修法; 大容量章节给出 4 字节地址的三种实现方案与可直接复制的 DTS / 源码改动。
| 文档编号 | LINUX-SPINOR-DRV-001 |
| 版本 / 状态 | V1.0 · 正式发布 |
| 适用器件 | XTX XT25Q512F / XT25F512B / XT55Q1G / XT25W1G / XT25Q256F / XT25F256B 等 SPI NOR 系列(16Mbit~2Gbit,3.3V 与 1.8V 电压档);文中方法同样适用于 Winbond / GigaDevice / Macronix / Micron / ST / Spansion 等品牌器件 |
| 内核版本 | 主线 Linux 5.10 / 5.15 / 6.1 / 6.6 / 6.12 为主(drivers/mtd/spi-nor/ 原生 spi-nor 框架 + spi-mem);4.19~5.4 的差异见 0.3 节 |
| 硬件平台 | ARM/ARM64 SoC(SPI 控制器走 spi-mem 框架)+ 板载 SPI NOR;示例以 3.3V 512Mbit 器件、104MHz Quad 模式为基准 |
| 配套文档 | 《SPI_NOR_Flash_电路设计指南.html》(硬件 / 原理图 / 布线)、《SPI_NOR_Flash_软件设计规范.html》(命令 / 寄存器 / 保护机制协议层) |
|---|
0.1 编写目的与读者对象
SPI NOR 在 Linux 下几乎不需要从零写驱动 ------主线内核已有完整的 spi-mem + spi-nor 框架、SFDP 自动探测与厂商器件表。真正的工作量集中在: 写对设备树、打开 4 字节地址模式把容量用满、适配新器件、把 JFFS2/UBI 跑通、 以及量产阶段的可靠性与运维。本文把这些工作按可执行的顺序固化下来,并把调试经验系统化。
- 驱动工程师:按第 3~7 章完成配置、DTS、大容量适配与新器件支持;按第 10 章定位问题。
- 硬件工程师:重点读第 2 章(WP#/HOLD#/RESET#、电压域、测试点)与第 6.10 节(容量只读到一半的硬件侧原因)。
- 测试工程师:按第 9 章执行功能 / 大容量专项 / 压力 / 性能测试,按第 14.2 节交付清单验收。
- 量产与运维:按第 12 章做可靠性设计,按第 15 章做日志采集、寿命监控与 A/B 分区 OTA。
不在本文范围:SPI NAND 驱动开发(见同目录《Linux_SPI_NAND_驱动开发指南.html》); eMMC / SDIO 驱动;裸机与 RTOS 下的 NOR 驱动实现;JESD216 SFDP 标准的逐字段规范解读。
本文贯穿的四条铁律
① SPI NOR 可以 XIP,但 Linux 下基本不用 :内核把它当 MTD 设备管理;SoC 启动阶段由 bootloader 决定是否 XIP。
② >128Mbit 必须显式处理 4 字节地址 :3 字节地址只能覆盖 16MiB。容量上到 256Mbit 就必须靠 4B opcode 或 EN4B 命令,配置错了「容量只认一半」甚至「读不到」(第 6 章)。
③ 分区表必须在 U-Boot、设备树、内核命令行三处一致 ------量产阶段最高频的故障源(见 5.3)。
④ 写保护三道闸:硬件 WP# 引脚、器件 BP 位、驱动层 ro 挂载。只查一处就会漏(见 12.2)。
结论:SPI NOR 在 Linux 里的「驱动开发」,本质是 把器件能力(JEDEC ID / 容量 / 擦除粒度 / 地址宽度 / 读协议 / 保护机制)正确喂给 spi-nor 框架, 再把 MTD → 文件系统这条链路打通 ;80% 的工作量在设备树、4 字节地址、分区表、量产烧录与可靠性验证上, 而不是在 drivers/mtd/spi-nor/ 里写新代码。
0.2 术语与缩略语
下表是本文(以及内核 MTD 邮件列表、datasheet)里高频出现的词。驱动调试时大量日志来自这些子系统, 先建立词与层级的对应关系,能省掉一半读日志的时间。
| 缩写 / 术语 | 中文 | 所在层级 | 含义与要点 |
|---|---|---|---|
| MTD | 存储技术设备 | 内核 subsystem | Linux 对裸 Flash 的统一抽象层,向上提供字符设备 /dev/mtdX 与只读块设备 /dev/mtdblockX,向下接 raw nand / spinand / nor。 |
| spi-mem | SPI 存储器抽象 | drivers/spi | 把 Flash 操作抽象成「命令-地址-dummy-数据」四段式 spi_mem_op,屏蔽单/双/四线差异。spi-nor 框架完全依赖它。 |
| spi-nor | SPI NOR 框架 | drivers/mtd/spi-nor | 内核 SPI NOR 驱动框架,模块名 spi_nor(CONFIG_MTD_SPI_NOR)。注意 6.6 起文件名从 spi-nor.c 改为 core.c。 |
| SFDP | 串行 Flash 可发现参数 | JESD216 规范 | 器件内部只读参数表,描述容量、擦除类型、读协议、4 字节地址能力等。读命令 0x5A(RSFDP)。 |
| BFPT | 基本闪存参数表 | SFDP 子表 | SFDP 的第一张子表,DW16 的 bit31:29 就是 4 字节地址模式支持位(见 6.4)。 |
| JEDEC ID | 器件身份码 | 读命令 0x9F |
3 或 5 字节:厂商 ID + 器件类型 + 容量码。容量码按 2 的幂次编码(0x18=128Mbit,0x19=256Mbit,0x20=512Mbit...)。 |
| 4 字节地址 | 4-Byte Addressing | 器件命令层 | 用 4 个字节传地址,突破 3 字节地址 16MiB 的天花板。三种实现见 6.2。 |
| 4B opcode | 专用四字节命令 | 协议变体 | 一组独立 opcode 实现 4 字节地址(0x13 / 0x03 / 0x12 / 0x21 / 0xDC...)。无状态,CPU 复位后自动回到 3 字节,最省心。 |
| EN4B / EX4B | 进入 / 退出四字节模式 | 命令 0xB7 / 0xE9 |
Winbond / Macronix / GigaDevice 风格:发一条命令切换地址宽度。有状态,复位后失效。 |
| BRWR / bank reg | bank 寄存器 | 命令 0x17(Spansion 风格) |
写 1 字节到 bank 寄存器,bit7 使能 4 字节模式、bit6:0 给 A30:24。有状态。 |
| addr_nbytes | 地址字节数 | 内核 struct spi_nor |
当前实际发出的地址字节数。容量 >16MiB 且无 4B opcode 时内核自动置 4。 |
| SNOR_F_4B_OPCODES | 支持专用 4B 命令 | ID 表 flags | 置位后内核把读/写/擦 opcode 换成 4 字节版本(SNOR_F_4B_OPCODES = BIT(3))。 |
| SNOR_F_HAS_4BAIT | 一次性可编程 4B | ID 表 flags | 器件通过一次性熔丝固化 4 字节模式,内核不 再替换 opcode(SNOR_F_HAS_4BAIT = BIT(4))。 |
| hwcaps | 硬件能力位 | 内核能力协商 | 控制器与器件能力的交集决定最终用哪种读协议(1-1-1 / 1-1-4 / 1-4-4 / 1-8-8...)。 |
| dummy cycles | 空周期 | 读命令时序 | 读命令地址之后、数据之前的等待周期数。配错直接读出垃圾。 |
| BP 位 | 块保护位 | SR1 / SR2 | 器件内部的写保护位,flash_erase / flashcp 报 EIO 十有八九是它。 |
| DTR | 双沿传输 | 读协议变体 | Double Transfer Rate,时钟双边沿都传输数据,理论带宽翻倍(如 0x0B / 0xEE)。 |
| JFFS2 | 日志型 Flash 文件系统 | fs/jffs2 | NOR 上最常见的可写文件系统,带磨损均衡与掉电保护。 |
| UBI / UBIFS | 无序块镜像 / 文件系统 | drivers/mtd/ubi | UBI 做卷管理与磨损均衡,UBIFS 是其上的文件系统。NOR 上也可用。 |
| XIP | 片内执行 | boot 阶段 | 把 Flash 映射到内存地址空间直接执行。SPI NOR 支持 XIP,但需 bootloader 配合。 |
| mtdparts | 命令行分区 | 内核 cmdline | 用内核命令行指定分区表的老方式,与设备树 fixed-partitions 二选一。 |
最容易混淆的四组概念
① /dev/mtdX ≠ /dev/mtdblockX :前者是字符设备(正确用法),后者是无坏块管理的「假块设备」,只能挂只读或 JFFS2,不能挂 ext4 。
② 容量「Mb」与「MiB」 : datasheet 标 512Mbit = 64MiB,不是 512MB。分区表算错会直接导致分区越界。
③ 4 字节地址 ≠ 4 字节数据 :地址字节数增加后,读命令的 dummy 周期与时序可能变,严格按 datasheet 的 4B 时序表核对。
③ JEDEC ID 读不到 ≠ 器件坏了 :器件处于 4 字节模式时,0x9F 后面要多发字节才能读对(见 6.7)。
0.3 内核版本与接口差异
spi-nor 子系统在 4.x → 5.x → 6.x 之间做过多次重构,照抄旧教程会直接编译不过或行为异常。 下表按版本给出关键差异,适配前先确认自己的内核基线。
| 内核版本 | 关键状态 | 对 SPI NOR 驱动的影响 |
|---|---|---|
| 4.14 ~ 4.19 | 单文件 spi-nor.c(约 5000 行) |
ID 表、厂商 fixups、SFDP 全在一个文件里;DT 属性少。本文 6.3 的决策链在这几个版本字段名不同 (nor->addr_width 而非 addr_nbytes)。 |
| 5.4 ~ 5.10 | 引入 spi-mem;spi_nor_hwcaps 体系 |
开始有 params->hwcaps;厂商表从 spi-nor.c 拆到各厂商文件。DT 增加 jedec-id 系列覆盖。 |
| 5.15 | spi_nor_set_4byte_addr_mode() 公开化 |
厂商驱动可作为「最后手段」强制进入 4 字节模式(2023 年 3 月合入,源自 6.1 周期)。 |
| 6.1 | SFDP 大改:BFPT/基本表解析重构 | struct spi_nor_flash_parameter 成为能力载体;spi_nor_post_bfpt_fixups() 钩子可用。 |
| 6.6 | 单文件彻底拆分 | spi-nor.c 更名为 core.c,厂商文件独立(winbond.c / gigadevice.c / macronix.c / micron-st.c...),新增 controllers/ 子目录。写 patch 时路径要跟着改。 |
| 6.9 ~ 6.12 | SR 掩码、spi_nor_helpers、OTP 独立文件 |
状态寄存器位操作收敛到 spi_nor_get_feature();otp.c 提供一次性可编程区支持。 |
| 主线 master | DT binding 文档化 | 官方要求新增 flash 提交时附上 sfdp / jedec_id / manufacturer / partname 的 sysfs 转储与校验和(见附录 A)。 |
怎么确认自己的内核是哪一版布局
# 1) 看内核版本号 uname -r
# 2) 看模块名与是否存在 core.o
ls /lib/modules/$(uname -r)/kernel/drivers/mtd/spi-nor/
# 6.6+ : core.ko winbond.ko gigadevice.ko ... controllers/
# 5.15 : spi-nor.ko
# 3) 看驱动打印的 banner(开 dynamic_debug 可见)
dmesg | grep -i "spi-nor\|spi_mem"
配置阶段就要确认,不要等写 patch 时才发现路径不对。
0.4 硬件平台与参考器件
本文以「ARM64 SoC + 单片 SPI NOR + 独立 SPI 控制器」为基准平台。为便于对照,列出 XTX 主力量产料号的 容量/电压/封装矩阵,调试时可在 /sys/.../spi-nor/ 下核对实际识别结果。
| 料号 | 容量 | 电压 | 封装 | 典型用途 | 4B 地址要求 |
|---|---|---|---|---|---|
| XT25F32FSOIGT | 32Mbit / 4MiB | 2.7~3.6V | SOP8 150mil | Bootloader、小容量参数区 | 不需要(<16MiB) |
| XT25F64FSOIGT | 64Mbit / 8MiB | 2.7~3.6V | SOP8 150mil | 引导 + 配置 + 日志 | 不需要 |
| XT25F128FSSIGT | 128Mbit / 16MiB | 2.7~3.6V | SOP8 208mil | 3 字节地址的临界点 | 临界(>16MiB 才需要) |
| XT25F256BSFIGT | 256Mbit / 32MiB | 2.7~3.6V | SOP16 300mil | 多镜像 / A/B OTA / rootfs | 必须 |
| XT25F512BSFIGT | 512Mbit / 64MiB | 2.7~3.6V | SOP16 300mil | 大 rootfs + 双备份 + 日志轮转 | 必须 |
| XT25F1GBSFIGT | 1Gbit / 128MiB | 2.7~3.6V | SOP16 300mil | 大数据缓存 / 双系统 | 必须 |
| XT25Q512FSFIGA | 512Mbit / 64MiB | 1.7~2.0V | SOP16 300mil | 低电压平台主推 | 必须 |
| XT55Q1GFSFIGA | 1Gbit / 128MiB | 1.7~2.0V | SOP16 300mil | 1.8V 大容量 | 必须 |
| XT25Q256FSFIGA | 256Mbit / 32MiB | 1.65~2.0V | SOP16 300mil | 1.8V 容量升级 | 必须 |
| XT55Q2GFBGIGA | 2Gbit / 256MiB | 1.7~2.0V | BGA24 6x8 | 大存储 / 双系统 | 必须 |
料号与容量的换算务必一次做对
Mbit(兆比特)→ MiB(兆字节)= ÷ 8 。256Mbit = 32MiB,不是 256MB。 分区表 size 属性与 U-Boot mtdparts 都按字节 写,写错就是分区越界或最后一块识别不到。 调试时用 cat /proc/mtd 交叉核对:size 那一列应当等于 分区大小 × 分区数(约等于器件容量)。
1 · 前置知识与开发环境
SPI NOR 的调试难度不在「写代码」,而在对器件行为与内核行为的双向理解。 本章把后续反复用到的三个前提讲清楚:器件本质、驱动基础要求、构建与调试环境。
1.1 SPI NOR 器件本质
SPI NOR Flash 是随机可读、页可编程、块可擦除的存储介质。这三句话决定了后面所有设计:
| 特性 | 含义 | 对软件栈的约束 |
|---|---|---|
| 随机可读(Random Read) | 任意字节地址可以直接读,不需要按页/块对齐 | 可以像内存一样访问 → 支持 XIP;文件系统的「读」不需要 FTL 参与 |
| 页可编程(Page Program) | 一页(典型 256B)内可写,但只能从 0 写 1,不能从 1 写 0 | 修改数据前必须先擦除;这就是 JFFS2 / UBI 存在的根本原因 |
| 块可擦除(Block Erase) | 擦除粒度典型 4KB / 32KB / 64KB,擦除后全变 0xFF | 文件系统按 erase block 管理;MTD 的 erasesize 就是它 |
存储几何的关键换算
以 XTX 512Mbit 器件为例:64MiB = 256 页 × 256B/页 = 4096 个 64KB 块, 也等于 1024 个 64KB 块 × 64 个 4KB 扇区。三个数必须能互相推导, 否则分区表和擦除命令会算错。调试第一步就是把 datasheet 的几何参数抄进表格并核对这三者一致。
512Mbit 器件的存储几何(XTX XT25F512B 系列为例)整个阵列 64MiB = 256 页页 0页 1页 2...... 中间 252 页 ......页 255每页 256 字节,只能写不能擦4KB 扇区 = 16 页 64KB 块 = 256 页 = 16 个扇区 4KB 扇区共 16384 个擦除粒度由 JEDEC 4BA 表描述,内核从 SFDP 解析得到读:任意地址0x03 / 0x0B / 0x6B不需对齐,可 XIP写:页内 0→10x02 / 0x32 / 0x42写前必须 WREN擦:块级归 0xFF0x20 / 0x52 / 0xD8擦除很慢,需轮询 WIP三条命令序列决定了驱动代码的全部形态读:CS↓ → opcode → addr(3/4B) → dummy(4/6) → data → CS↑写:CS↓ → WREN(0x06) → CS↑ → CS↓ → opcode → addr → data(≤页大小) → CS↑ → 轮询 WIP擦:CS↓ → WREN(0x06) → CS↑ → CS↓ → opcode → addr → CS↑ → 轮询 WIP 直到 0
图 1 · SPI NOR 存储几何与三类命令序列
为什么 NOR 也要「文件系统」
NOR 虽然可以随机读,但不能把任意已写数据改写成另一份数据 。 若要存放结构化文件(配置、数据库、用户数据),必须由文件系统来管理「日志写入 + 垃圾回收」, 否则写一次配置就要擦整个扇区。JFFS2 / UBIFS 就是干这个的------ 直接把 ext4 挂到 NOR 上会立即损坏文件系统,这是规范性要求,不是可选项。
1.2 Linux 驱动基础要求
上手本文需要具备以下内核侧基础。缺失哪一块,第 10 章对应小节会额外展开:
| 知识点 | 必须掌握到什么程度 | 对应章节 |
|---|---|---|
| platform / spimem 驱动模型 | 看懂 probe 回调、设备与驱动的匹配机制(compatible) |
4.3 |
| 设备树 DTS / DTBO | 能独立写出 SPI 控制器 + Flash 子节点 + 分区表,并能编译验证 | 3.3 / 5.1 / 5.2 |
| spi-mem 四段式操作 | 理解 struct spi_mem_op 的 opcode / addr / dummy / data 四个 phase |
4.6 |
| MTD 子系统接口 | 知道 mtd_info、mtd_device_register()、erase / write / read 三条回调 |
4.6 / 8.1 |
| dynamic_debug / kmsg_dump | 会开内核调试日志,会过滤关键子系统日志 | 10.1 |
| JEDEC SFDP 概念 | 知道 SFDP 是器件自描述表,能读懂 BFPT 的关键 DW 字段 | 4.5 / 6.4 |
1.3 交叉编译与构建环境
本文所有示例以 AArch64 为例,ARM32 / RISC-V 把工具链前缀换成对应即可。核心流程如下:
-
拉 SDK / 内核源码 :厂商 SDK 通常带
kernel/、u-boot/、buildroot/或yocto/。本文以 Linux 6.6 为基准线。 -
配置并交叉编译内核 :
export ARCH=arm64 export CROSS_COMPILE=/path/to/toolchain/bin/aarch64-linux-gnu- make O=out mrproper make O=out defconfig # 合并板级配置片段(推荐用 scripts/kconfig/merge_config.sh) ./scripts/kconfig/merge_config.sh -O out out/.config board.fragment make O=out olddefconfig make O=out -j$(nproc) Image dtbs modules -
单独编译 MTD 工具 (强烈建议静态编译,方便拷到板子上直接跑):
cd mtd-utils ./autogen.sh ./configure --enable-static --disable-werror \ CROSS_COMPILE=aarch64-linux-gnu- \ CFLAGS="-I${KERNEL_OUT/include -I${KERNEL_SRC/include}" \ LDFLAGS="-L${KERNEL_OUT}" make -j$(nproc) # 产物:flash_erase flashcp flashdump nanddump mtdinfo ... -
部署到板子 :
scp mtd-utils/flash_* root@板子:/usr/sbin/,或打进 rootfs 镜像。 -
设置交叉工具链环境 (把日志重定向到主机,方便剪贴板):
# 主机侧 picocom -b 115200 /dev/ttyUSB0 | tee boot.log # 或 screen /dev/ttyUSB0 115200
用 fragment 而不是直接改 .config
把本项目需要的配置写成一个 board.fragment 文本文件(内容是 CONFIG_XXX=y 这样的行), 用 merge_config.sh 合并。好处是:配置可版本化、可复用、可在 CI 里校验, 换 SoC 平台时不用重新勾一遍。完整 fragment 模板见 3.1。
1.4 调试环境搭建
调试 SPI NOR,「软件日志 + 硬件波形」双线并行效率最高。硬件侧只需三样便宜工具:
必备工具
- 逻辑分析仪 (8 通道 24MHz 级别即可,淘宝 50 元档):抓 CS# / SCLK / MOSI / MISO 波形,是定位 90% 硬件问题的唯一高效手段。
- 万用表:量 VCC 实际电压、量 WP#/HOLD# 电平、量信号是否短到地。
- 可编程 SPI 嗅探器 / 逻辑分析仪的 SPI 协议解码:把波形直接解成 opcode 字节,省去人工数位。
软件侧准备
- 串口日志实时重定向到主机文件(
tee)。 dynamic_debug已打开(见 10.1)。/proc/mtd、/sys/class/mtd/可读。- 主机上跑
mtd-utils与flashrom备用(用flashrom -p linux_spi:/dev/spidevX.Y做交叉验证)。 - 一个能改 DTS 的构建流程(overlay 或直接改板级 dts)。
调试阶段的信息闭环
现象(dmesg / 命令返回) → 读内核内部状态(debugfs params / capabilities) → 读器件真实状态(RDSR / SFDP / JEDEC ID) → 抓总线波形确认时序。 这四层从上到下依次排除,不要跳步:第 2 层能告诉你内核「以为」器件是什么, 第 3 层告诉你器件「实际」是什么,两者不一致就说明 probe 阶段出了问题------此时去抓波形往往白费力气。
2 · 硬件设计规范(驱动视角)
驱动工程师不需要画原理图,但必须能读懂原理图并指出哪些地方会让驱动跑不起来 。 SPI NOR 硬件上最关键的三根线是 WP#、HOLD#、RESET#------它们决定了「写不进去」「读会停住」 这类诡异故障的物理原因。本章只讲与驱动行为直接相关的硬件约定;器件级电气参数以《SPI_NOR_Flash_电路设计指南》与 datasheet 为准。
2.1 原理图信号设计要点
| 信号 / 项目 | 设计要求 | 驱动侧症状(设计错时的表现) |
|---|---|---|
| SCLK | 尽量短、少过孔;与其他高速信号保持间距;串联阻尼电阻(典型 22~33Ω)靠近 SoC 端 | 高频下读回数据错位、ECC 报错;降低 spi-max-frequency 后恢复正常 → 基本可判定为 SI 问题。 |
| MOSI / MISO | 与 SCLK 大致等长;避免形成长分支(Stub);DTR 模式下两者严格等长要求 | 特定命令(如 Quad 读 0x6B/0xEB、DTR 0x0B)返回全 FF 或全 00,但 0x03 正常。 |
| CS# | 每器件独占;必须有确定的上拉(避免上电/复位期间误选中);不要复用其他功能 | 多器件时 ID 读错;上电偶发 probe 到「不存在的器件」。 |
| IO0~IO3 / WP# / HOLD# | WP# 与 HOLD# 绝不可浮空;接了 GPIO 的要保证上电默认电平正确 | WP# 浮空 → 写入被随机禁止或随机允许;HOLD# 浮空 → 读数据中途停住、返回半截数据。 |
| RESET# | 建议接 GPIO 或硬件 RC 复位;上电复位脉冲宽度须满足 datasheet tVSL | 掉电/热复位后器件状态机卡死,驱动只能等 WIP 超时(spi-nor: probe failed 或 Timeout)。 |
| VCC / 去耦 | 按 datasheet 放 1~2 个去耦(0.1µF + 4.7µF),紧靠器件电源脚 | 擦除大块(64KB)瞬间电流跌落 → 擦除后数据是错的或干脆失败。 |
| 走线长度 | Quad 模式下四根数据线等长(±5mm),避免 IO2/IO3 比 IO0/IO1 短很多 | 1-4-4 读偶尔出错,1-1-1 正常。这是典型的「四线不等长」特征。 |
2.2 WP# / HOLD# / RESET# 三根关键线
这三根线各自对应一类完全不同的软件故障。调试时按下表对号入座,可以省掉大量试错:
| 引脚 | 低电平时器件行为 | 典型故障现象 | 快速判据 |
|---|---|---|---|
| WP#(IO2) | 禁止 Page Program 与 Erase(状态寄存器仍可读) | flash_erase 报 Error: unable to write MTD;flashcp 报 EIO;但 flashdump 一切正常 |
万用表量 WP# 是否为低;flash_erase 前 RDSR 的 BP 位是否非 0 |
| HOLD#(IO3) | 暂停输出但保持当前指令状态(相当于把时钟冻结) | 读数据在某一位置停住或全 FF;注意 WP#/HOLD# 在 IO2/IO3 上,是四线模式的复用引脚,用四线命令时如果没置 QE 就会暴露 | 抓 MISO 波形:CS# 拉低后数据中途停止、SCLK 仍在跳 |
| RESET#(RST#) | 硬件复位整个状态机(含 4 字节地址模式、QE 位、SR 保护位) | 掉电重启后 容量只认出 16MiB、写入仍被禁、或 probe 完全失败(器件还在 WIP 状态) | 抓 RESET# 是否有效;掉电重启后立即 cat /sys/.../jedec_id 看是否变化 |
最经典的坑:RESET# 与 4 字节地址模式
像 0xB7(EN4B)这类命令切换的 4 字节模式是易失状态 ------ 硬件复位或掉电后器件自动回到 3 字节模式。如果 bootloader 在 4 字节模式下写了数据、然后硬件复位、 内核再按 3 字节模式去读同一地址,会读出错误数据但不报错。 这正是第 6.9 节要专门讲「U-Boot 与内核模式一致性」的原因, 而专用 4B opcode(第 6.2 节方案 A)之所以更省心,就是因为它无状态。
三根控制线与「读/写/容量」三类故障的对应关系WP# 拉低禁止 Program 与 Erase读操作完全正常SR 的 BP 位被硬件强制表现为:写入一律 EIOHOLD# 拉低输出暂停,时钟继续指令状态保持不变表现为:读到一半变 FF多发一字节地址即可恢复RESET# 拉低状态机整体复位4B 模式 / QE / 保护位全部回到默认表现为:重启后容量变 16M设计建议(三条都写进硬件评审 checklist)1. WP# / HOLD# 各加 10k 上拉到 VCC,量产可选 0Ω 下拉占位以便调试期强制拉低2. 需要软件可控写保护的场合,WP# 接 GPIO 且上电默认高,DTS 里配 gpio 控制器3. RESET# 接 GPIO 并在 probe 前复位一次;RC 方案要核算上电斜率与最小脉宽4. 三个测试点:WP#、HOLD#、RESET# 各引一根测试针,便于逻辑分析仪直接夹
图 2 · WP# / HOLD# / RESET# 与故障现象的对应关系
2.3 电压域与 IO 电平
SPI NOR 常见两个电压档:2.7~3.6V (XT25F 系列)与 1.65/1.7~2.0V(XT25Q / XT55Q 系列)。 混用是硬件评审的高频问题:
| 项目 | 3.3V 器件(XT25F) | 1.8V 器件(XT25Q / XT55Q) | 混用后果 |
|---|---|---|---|
| VCC | 2.7~3.6V | 1.7~2.0V | 3.3V 供 1.8V 器件:永久损坏风险(超绝对最大额定值) |
| 输入阈值 Vih | 0.7×VCC ≈ 2.31V | 0.7×VCC ≈ 1.26V | 1.8V 器件误把 1.8V 输出当高,判定随机 → 容量认错、ID 读错 |
| SoC IO 电平 | 需 3.3V IO;若 SoC 是 1.8V 域需电平转换 | 可直接接 1.8V 域 | 缺电平转换:QPI 读全 FF,SPIDEV 完全不通 |
| 上拉电阻阻值 | 典型 10k@3.3V | 典型 10k@1.8V(不宜太大,高阻时上升沿慢) | 上拉过弱 → 高频下上升沿不达标,读错数据 |
1.8V 平台的一条实战经验
1.8V 器件的输入阈值比 3.3V 低很多,上拉电阻阻值要相应减小 (4.7k~10k 更常见,取决于总线电容与频率)。 若 1.8V 器件在你板子上「低速正常、高速随机错」,先怀疑上拉过弱 + 走线过长, 其次才是试 spi-max-frequency。另外确认 SoC 侧引脚配置里有没有开施密特触发(Schmitt), 开上以后上升沿的鲁棒性差别很大。
2.4 必留测试点与联调顺序
硬件联调按「先量电平、再抓波形、最后改驱动」的顺序做,不要一上来就改代码:
硬件联调五步法(顺序不可颠倒)第 1 步 量电平VCC 是否在范围WP#/HOLD#/RESET#是否被拉高对地无短路第 2 步 读 ID逻辑分析仪抓0x9F 事务回读 3~5 字节与 datasheet 比对第 3 步 读 SFDP抓 0x5A 事务确认 SFDP 头是否 0x50444653看 DW16 的 4B 位第 4 步 测边界在 0 与 16MiB附近各读写一次确认 4B 地址真的生效第 5 步 压测全盘写读校验长稳压测高频与降频对比记录基线数据第 2~4 步的具体抓包判据读 ID(0x9F):CS↓ → 0x9F → 3 字节 dummy(0x00)→ 3~5 字节 ID → CS↑读 SFDP(0x5A):CS↓ → 0x5A → 3 字节地址(0x00 00 00)→ 1 字节 dummy → 表数据 → CS↑判据一:ID 第一个字节 = 厂商码(XTX 一般为 0x0B 或 0x5E 系列,以 datasheet 为准)判据二:SFDP 头四字节应回读 0x53 0x46 0x44 0x50(小端 ASCII 字母 SFDP)
图 3 · 硬件联调五步法与抓包判据
为什么必须先抓 ID 再改驱动
「probe 失败」这一个现象,根因可能有十几种:pinctrl 配错、时钟频率超规格、CS# 上拉缺失、 器件处于 4 字节模式、WP# 被拉低、SPI 控制器 mem_ops 不支持 4 线...... 但抓一次 0x9F 波形就能砍掉一大半:能读出正确的 ID 说明硬件通路没问题, 问题在软件配置;读不出则先解决硬件或时钟。这比在 DTS 里反复试参数快得多。
2.5 BOM 选型约束
| 约束项 | 要求 | 违反时的症状 |
|---|---|---|
| 电压档必须与 SoC IO 域匹配 | 3.3V IO → XT25F;1.8V IO → XT25Q / XT55Q | 轻则 ID 读错,重则器件损坏 |
| 最大时钟频率 | 主频看 datasheet 的 fCLK(Quad 模式典型 104~133MHz;DTR 模式典型 80~100MHz) | 超规格时数据错乱,且降频就正常------这是最有价值的判据 |
| 封装与球位 | SOP8 / SOP16 / WSON8 / DFN8 / BGA24,注意 BGA 需 X-Ray 检测 | 焊接不良表现为间歇性读错,返修后可能"就好了"(掩盖问题) |
| 温度等级 | 工业级 -40~85°C 或增强级 -40~105°C | 高温环境下超时、复位、写入失败 |
| 是否带 SFDP | 主流器件都带;不带 SFDP 的必须在内核 ID 表里加条目 | 不带 SFDP 且 ID 表也没有 → unrecognized JEDEC id bytes |
| 4 字节地址能力 | 容量 >128Mbit 必须确认是 4B opcode 还是 EN4B 模式 | 容量只认出 16MiB,或 >16MiB 区域读写乱码 |
3 · 内核配置与编译构建
SPI NOR 的内核配置项不多,但有两个必须打开且常被忽略 的开关 (CONFIG_MTD_OF_PARTS 与 CONFIG_SPI_NOR_SFDP_RUNTIME)。本章给出可直接使用的 fragment 模板与逐项说明。
3.1 内核 config 清单
把下面内容保存为 board.fragment,用 merge_config.sh 合并:
# === SPI NOR 核心 ===
CONFIG_MTD=y
CONFIG_MTD_BLOCK=y
CONFIG_MTD_CFI=y
CONFIG_MTD_CFI_GEOMETRY=y
CONFIG_MTD_SPI_NOR=y
CONFIG_MTD_OF_PARTS=y # 关键:设备树 fixed-partitions 分区表
CONFIG_MTD_PARTITION=y
CONFIG_MTD_OF_IOMUX=y # 若用 pinctrl 绑定 flash 节点
# === SPI / spi-mem 框架 ===
CONFIG_SPI=y
CONFIG_SPI_MASTER=y
CONFIG_SPI_MEM=y # 关键:四段式内存操作抽象
CONFIG_SPI_LOOPBACK_TEST=y # 建议开:spidev 自测
# === 文件系统(按实际需要裁剪) ===
CONFIG_JFFS2_FS=y
CONFIG_JFFS2_FS_WRITEBUFFER=y # 强烈建议:写缓冲显著降低挂载时间与磨损
CONFIG_JFFS2_SUMMARY=y
CONFIG_UBIFS_FS=y
CONFIG_UBIFS_FS_ADVANCED_COMPR=y
CONFIG_UBIFS_FS_LZO=y
CONFIG_UBIFS_FS_ZLIB=y
CONFIG_SQUASHFS=y
CONFIG_SQUASHFS_ZLIB=y # 只读 rootfs 推荐
CONFIG_SQUASHFS_FILE_DIRECT=y
# === 调试(量产前可关) ===
CONFIG_DEBUG_FS=y # 关键:/sys/kernel/debug/spi-nor/* 下有 params 视图
CONFIG_DYNAMIC_DEBUG=y
CONFIG_DEBUG_KERNEL=y
CONFIG_MTD_SPI_NOR_DEBUG=y # 若存在该旧选项(旧内核用)
CONFIG_JFFS2_DEBUG=y # 排 JFFS2 问题时有日志(较吵)
CONFIG_MTD_CFI_GEOMETRY_DEBUG=y
# === MTD 工具需要的支持 ===
CONFIG_COMPAT=y # 32 位系统跑 64 位工具时需要(可选)
| 配置项 | 级别 | 不开会怎样 |
|---|---|---|
| MTD_SPI_NOR | 必须 | 没有 spi-nor 驱动,SPI 控制器上挂的 Flash 完全不被识别 |
| SPI_MEM | 必须 | spi-nor 依赖它表达读/写/擦事务;不开会编译失败 |
| MTD_OF_PARTS | 必须 | 设备树里的 partitions 子节点被忽略 → 只有一个裸 mtd0,没有分区 → 一切文件系统操作失败。这是最典型的「配错」故障 |
| DEBUG_FS | 强烈建议 | 看不到 /sys/kernel/debug/spi-nor/<dev>/params,等于调试瞎了一半(本文 10 章大量依赖它) |
| JFFS2_FS_WRITEBUFFER | 强烈建议 | 不带写缓冲的 JFFS2 挂载极慢且磨损翻倍 |
| JFFS2_SUMMARY | 建议 | JFFS2 挂载时扫描全盘找擦除块,缺 SUMMARY 会显著变慢 |
| DYNAMIC_DEBUG | 建议 | 无法按模块开日志,只能全开或全关 |
| SQUASHFS | 按需 | 只读 rootfs 方案必备 |
确认配置生效的三个检查点
# 1) 内核命令行确认
cat /proc/config.gz | gunzip | grep -E "MTD_SPI_NOR|SPI_MEM|MTD_OF_PARTS|DEBUG_FS"
# 或(未开 IKCONFIG 时)在编译目录查
grep -E "CONFIG_(MTD_SPI_NOR|SPI_MEM|MTD_OF_PARTS|DEBUG_FS)" .config
# 2) 运行期确认模块与设备
lsmod | grep -i spi_nor
ls -l /dev/mtd* /dev/mtdblock* 2>/dev/null
# 3) 设备树分区是否被采纳(关键)
cat /proc/mtd
第三步如果只看到 mtd0 一个分区且没有 name,说明 fixed-partitions 没生效, 回去查 CONFIG_MTD_OF_PARTS 与节点写法(见 5.2)。
3.2 内置 vs ko 模块
| 方式 | 写法 | 适用场景 | 注意 |
|---|---|---|---|
| 内置(推荐) | CONFIG_MTD_SPI_NOR=y |
量产产品、启动早期就要挂载 rootfs | 无加载顺序问题;modprobe 依赖 udev 的场景下更可靠 |
| 模块 | CONFIG_MTD_SPI_NOR=m |
多平台共用一份内核、调试期快速替换 | 需把 spi_mem 一起编成模块;insmod 顺序不能反 |
| 模块 + 依赖 | modinfo spi-nor.ko 查 depends |
--- | 6.6 起文件名从 spi-nor 改 spi-nor(模块名仍是 spi_nor),厂商表独立成 ko 时要注意配套安装 |
# 模块方式的加载顺序
modprobe spi_mem # 若单独编成模块
modprobe spi_nor
# 或直接 insmod 并检查依赖
modinfo spi_nor | head -5
3.3 DTS 编译与 overlay
板级 DTS 里 Flash 节点的改动频率最高(换料、加密位、4B 属性),因此建议用 overlay 而不是改主 DTS:
# 1) 把 Flash 节点抽成独立片段,便于复用
# board/flash/xtx-spi-nor-512mbit.dtsi
&qspi0 {
flash@0 {
compatible = "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <104000000>;
spi-rx-bus-width = <4>;
spi-tx-bus-width = <4>;
};
};
# 2) 编译设备树
make O=out ARCH=arm64 dtbs
# 产物:out/arch/arm64/boot/dts/<厂商>/<板子>.dtb
# 3) 反查 DTB 里的实际内容(验证语法与继承是否正确)
dtc -I dtb -O dts out/arch/arm64/boot/dts/<厂商>/<板子>.dtb | grep -A 20 "flash@0"
# 4) 启动时确认内核收到的是同一个 dtb
cat /proc/device-tree/$(cat /proc/device-tree/aliases)/spi*/flash@0/compatible; echo
改 DTS 后 90% 的「改了没生效」
① DTS 根本没被重新编译 (板级 dts 被 dtsi 覆盖了但你改错了文件);
② bootloader 传的是另一个 dtb (很多平台从 eMMC/SD 读 dtb,改了源码里的板级 dts 没用);
③ 改的是 .dts 但实际生效的是 .dtb 且没 make dtbs ;
④ 有 overlay 覆盖了它。
最快的自检:cat /proc/device-tree/.../flash@0/jedec-id 或用 dtc -I dtb 反编译 板子正在跑的那个 dtb,确认节点内容与源码一致。不一致就是 ①②③。
3.4 Rootfs 与 mtd-utils
SPI NOR 上放什么,取决于启动方案。三种典型形态:
| 形态 | 布局 | 优点 | 风险 |
|---|---|---|---|
| 只读 rootfs + 可写数据卷 | boot(SquashFS) + rootfs(SquashFS) + data(JFFS2/UBIFS) |
根分区不可被写坏,安全;可写区磨损可控 | 根分区升级需 OTA 机制(不能原地覆盖正在运行的分区) |
| 全可写 JFFS2 | 单个 rootfs(JFFS2) |
简单,升级方便 | 升级过程掉电会砖机;必须做双备份或 A/B |
| 纯数据器件(不挂 rootfs) | 多个 JFFS2/UBIFS 数据分区 | 风险最低 | 需要 bootloader 本身不从 NOR 启动 |
Rootfs 里必须包含的工具:
- mtd-utils :
mtdinfo/flash_erase/flashcp/flashdump/nanddump(NOR 上也能用做只读 dump)。 - e2fsprogs :
mkfs.jffs2/jffs2dump/jffs2loader(flashcp -j写 JFFS2 镜像时需要)。 - mtd-utils-ubifs :
mkfs.ubifs/ubinize(用 UBI 方案时)。 - mtdinfo 替代品 :
cat /proc/mtd任何时候可用,做自动化脚本时优先用它。
3.5 U-Boot 配套配置
U-Boot 与内核必须在三处保持一致(详见 5.3)。与 SPI NOR 相关的 U-Boot 配置:
# ===== U-Boot Kconfig =====
CONFIG_MTD_NOR_FLASH=y
CONFIG_SPI_FLASH=y
CONFIG_SPI_FLASH_MTD=y # 让 sf 命令走 MTD 框架,分区表与内核统一
CONFIG_CMD_SF=y
CONFIG_CMD_SF_TEST=y # sf test 可做全盘擦写校验,产线自检很有用
CONFIG_ENV_IS_IN_SPI_FLASH=y
CONFIG_SPI_FLASH_SHOW_PROGRESS=y
# DT 中给 U-Boot 用的快读使能(非标准属性,注意厂商差异)
# 很多平台用 m25p,fast-read 打开 0x0B 快读;具体看该平台 U-Boot 代码
U-Boot 阶段的高频坑(按出现频率排序)
① U-Boot 用 3 字节、内核用 4 字节(或反过来) → 大容量分区数据错乱。见 6.9。
② U-Boot 分区表与内核不一致 → 内核挂载错分区。见 5.3。
③ bootcmd 里 bootargs 的 mtdparts 覆盖了设备树分区 → 内核命令行优先级高于设备树,写错就以命令行为准。见 5.3。
④ U-Boot 打开 CONFIG_SPI_FLASH_MTD 但没在 DT 里写 partitions → U-Boot 里 sf probe 能识别,但 mtd list 是空的。
⑤ env 放在 NOR 上且与分区重叠 → env 被 flashcp 覆盖,设备变砖。
4 · 驱动架构与代码框架
理解 spi-nor 框架的内部结构,是能读懂 dmesg 报错、能写对 vendor patch、 能判断「内核认为器件是什么」的前提。本章按「分层 → 文件 → probe 流程 → 结构体 → SFDP → 三条路径」的顺序展开。
4.1 软件分层总览
SPI NOR 在 Linux 中的软件分层与可观测点用户态/dev/mtdX(字符) /dev/mtdblockX(块) mtd-utils(JFFS2 / UBIFS / SquashFS)可观测:cat /proc/mtd ls /sys/class/mtd/ strace 应用的 ioctl 调用MTD 子系统mtd_info 注册、分区管理(fixed-partitions)、dirmap、擦写类型统一可观测:/proc/mtd /proc/partitions 内核日志里的 Bad partition table 警告spi-nor 框架JEDEC ID 匹配、SFDP 解析、能力协商(hwcaps)、4 字节地址、QE、保护位drivers/mtd/spi-nor/core.c(6.6+) 厂商文件 winbond.c / gigadevice.c ...可观测:dmesg /sys/kernel/debug/spi-nor/<dev>/{params,capabilities} /sys/bus/spi/devices/.../spi-nor/{jedec_id,manufacturer,partname,sfdp}spi-mem四段式事务(opcode / addr / dummy / data)、控制器 hwcaps 上报可观测:/sys/bus/spi/devices/spiX.Y/mode spidev 测试SPI 控制器驱动片选控制、时钟分频、DMA / 中断、XIP 映射(部分平台支持)可观测:控制器寄存器 debugfs / debugfs 下的时钟与 hwcaps硬件层:SPI NOR 器件 + WP# / HOLD# / RESET#(第 2 章)
图 4 · SPI NOR Linux 软件分层与每层的可观测点
调试时的下钻方向
从「现象」往「根因」下钻的顺序是:用户态报错 → MTD 层状态 → spi-nor 层的 params 视图 → spi-mem/控制器 hwcaps → 总线波形。 第 3 层是性价比最高的一层 :cat /sys/kernel/debug/spi-nor/spi0.0/params 一条命令就能看到 内核最终选的读 opcode、dummy 周期、地址字节数、擦除类型表。不用它,等于每次都在猜。
4.2 源码目录与文件职责
以 6.6 / 6.12 为例(5.15 及以前只有 spi-nor.c 单文件,逻辑都在一起):
| 文件 | 职责 | 你改它做什么 |
|---|---|---|
| core.c | 框架主体:probe、scan、ID 匹配、SFDP 调度、读写擦实现、4 字节地址、sysfs/debugfs 注册 | seldom 直接改;只有要修框架 bug 或加通用能力时才动 |
| core.h | struct spi_nor、struct spi_nor_flash_parameter、SNOR_F_* 标志、op 宏 |
读它才能知道字段含义与标志位 |
| sfdp.c / sfdp.h | SFDP 表解析:BFPT、4BA、JEDEC 4 字节地址参数表(FF84) | 解析结果不对时看这里 |
| winbond.c / gigadevice.c / macronix.c / micron-st.c / spansion.c / xmc.c / eon.c / esmt.c / fujitsu.c / sst.c / atmel.c / intel.c / issi.c / xilinx.c / catalyst.c / everspin.c | 各厂商 ID 表与 fixups(厂商私有行为修正) | 加新型号最常改的就是这里 |
| sysfs.c | 暴露 jedec_id / manufacturer / partname / sfdp 供用户态读取 |
一般不改 |
| debugfs.c | 暴露 params / capabilities 视图 |
调试必备,不要裁掉 DEBUG_FS |
| otp.c | 一次性可编程区(OTP / fuses)支持,关系到 4BAIT 固化 | 器件通过 OTP 固化 4 字节模式时相关 |
| swp.c | 软件写保护(Software Write Protection)状态机 | 做全局写保护的固件可能用到 |
| controllers/ | 直接实现 spi_nor 接口的控制器(hisi-sfc、nxp-spifi 等) | 用的是 spi_mem 的平台不需要碰 |
4.3 probe 执行全流程
spi_nor_probe() 执行流程与常见失败点1. devm_kzalloc 分配 struct spi_nor → 失败即 OOM(罕见)nor->spimem / nor->dev 绑定,spi_mem_set_drvdata() 挂私有数据2. 读 JEDEC ID(0x9F / 0x90 / 0xAB)失败高发:pinctrl 错、CS# 上拉缺失、频率超规格、器件在 4B 模式、WP# 拉低报错特征:spi-nor: probe failed / unrecognized JEDEC id bytes xx xx xx3. 用 ID 查厂商表匹配 flash_info → 决定容量 / 擦除粒度 / flags无匹配则走 SFDP;都没有则报 unrecognized4. 读 SFDP 表(0x5A)并解析:BFPT / 4BA / 4 字节地址参数表得到 size、erase_types、读写协议能力、是否支持 4 字节地址及其进入方式失败高发:器件 SFDP 表内容被改过 / 魔改 → 需要厂商 fixups 修正5. spi_nor_setup():与控制器求 hwcaps 交集,选读/写/擦命令失败高发:控制器不支持 4 线 → 退回 1-1-1(功能正常但性能低,不会报错)6. spi_nor_set_addr_nbytes():决定地址字节数(本文第 6 章的核心)size > 0x1000000(16MiB)且当前是 3 字节 → 自动置 4随后若有 4B opcode 则替换 opcode;否则在 init 阶段发 EN4B 命令7. spi_nor_init():解保护位 → 置 QE(Quad Enable)→ 进入 4 字节模式失败高发:QE 置位后仍用 1-1-1 读(性能低);或 4B 模式进入失败8. mtd_device_register():注册 mtd 设备并解析设备树分区表失败高发:CONFIG_MTD_OF_PARTS 未开 → 分区表被忽略(见 3.1)
图 5 · spi_nor_probe() 的八个步骤与各步失败特征
读 dmesg 时按这个顺序定位
probe 失败时 dmesg 里一定有一条明确报错,往上找到第一条 error 即可,后面都是级联噪声 。 常见首条报错与含义:
spi-nor: probe failed → 通路问题(pinctrl / CS / 频率 / WP#),去看硬件;
unrecognized JEDEC id bytes: xx xx xx → 读到了数据但不匹配,要么 ID 真读错,要么器件没在 ID 表里 ; 若 xx 是 ff ff ff 基本是硬件,若像合法厂商码则需要加 ID 表条目;
Error: unable to read JEDEC ID → 连 ID 都没读出来,纯硬件;
spi-nor: SFDP: unexpected magic → SFDP 头不对,可能是 4B 模式导致(见 6.7);
Bad partition table → 分区表问题,与器件无关。
4.4 关键结构体与 flags
调试时最常查的是 struct spi_nor 的几个字段与 SNOR_F_* 标志位。 下表按「与 >128Mbit 相关」的程度排序,6.x 内核源码为准:
| 字段 / 标志 | 取值 / 位置 | 含义 | 调试用法 |
|---|---|---|---|
| addr_nbytes | u8 | 当前实际发出的地址字节数:3 或 4 | debugfs/params 里的 address nbytes。容量只认出 16MiB 时先看它是不是 3 |
| addr_mode_nbytes | u8 | 「当前地址模式」的字节数,进/退 4B 模式时同步更新 | 与 addr_nbytes 不一致说明模式切换出过问题 |
| SNOR_F_4B_OPCODES | BIT(3) | 器件支持专用 4 字节 opcode(0x13/0x03/0x12/0x21/0xDC...),内核直接替换 opcode | 置位时 params 里的 read opcode 应是 0x13 而非 0x03 |
| SNOR_F_HAS_4BAIT | BIT(4) | 器件的 4 字节模式可通过 OTP/熔丝一次性固化 ,内核不替换 opcode | 置位后地址宽度由器件自身决定,调试时优先确认这个 flag 有没有设对 |
| SNOR_F_HAS_LOCK | BIT(5) | 器件有块保护锁,上电时需清 SR 才能写 | 不设则开机后写入可能 EIO |
| SNOR_F_HAS_SR_TB | BIT(0) | SR1 有 TB 状态位 | 仅影响状态解析 |
| SNOR_F_HAS_16BIT_SR | BIT(6) | 状态寄存器是 16 位(读 1 字节 + dummy) | 影响写保护位判断位置 |
| SNOR_F_NO_WP | BIT(16) | 器件有独立的 no-wp DT 属性 / 芯片无 WP# 引脚 |
与硬件强相关,见 2.2 |
| SNOR_F_SOFT_RESET | BIT(12) | 支持软复位命令 0x66+0x99 |
与 RESET# 等效,可用于救活卡死器件 |
| read_opcode / read_dummy | u8 / u32 | 最终选中的读命令与 dummy 周期 | debugfs/params 直接可见;读出垃圾先核这两个值 |
| erase_opcode / erase_mask | u8 / loff_t | 统一擦除命令与大小掩码 | params 里有完整擦除类型表 |
| params->size | u64 | 器件字节容量 | params 的 size;小于实际容量 = 大容量配置错了 |
4.5 SFDP 解析与能力协商
SFDP(0x5A 命令)是 JESD216 定义的自描述参数表,内核优先依赖它, ID 表只是「SFDP 不可靠时的兜底」。理解 SFDP 是做大容量适配的前提:
SFDP 空间结构
- 地址
0x000000:8 字节参数头(版本号、表数量、各表偏移)。 - 首字节回读
0x53('S')→ 有效 SFDP;回读0xFF→ 器件不支持。 - 头之后是各张参数表:标准表(BFPT、4BA、4 字节地址指令表 FF84)+ 厂商私有表。
BFPT 里对大容量最关键的两个字段
- Density :器件总位数,
size = Density / 8字节。 - DW16 bit31:29:4 字节地址支持方式(详见 6.4)。
能力协商(hwcaps)
内核把「器件支持的读协议」与「控制器支持的读协议」求交集 ,选出最好的一个。 典型优先级:1-8-8 > 1-4-4 > 1-1-4 > 1-1-2 > 1-1-1。
关键理解:控制器不支持四线时内核会静默退回单线,不会报错 。 所以「设备能读能写但很慢」要去查 params 里的 read proto,而不是查 DTS 的 spi-rx-bus-width 是否写了 4(那只是期望值)。
SFDP 读不到时怎么办
顺序是:① 先确认 0x5A 事务本身正确 (用逻辑分析仪抓,注意 dummy 字节数); ② 确认器件是否处于 4 字节模式 (此时 0x5A 也要多发地址字节,见 6.7); ③ 抓 /sys/bus/spi/devices/.../spi-nor/sfdp 原始 hex ------ 6.x 已把这个文件暴露出来,直接 xxd 出来分析,不必自己写读函数; ④ 都不行就查厂商是否魔改了 SFDP 内容 (某些器件为了省成本故意写坏 SFDP), 这时必须在 spi_nor_fixups 里用 post_bfpt 钩子修正。
4.6 读/写/擦三条路径
三条路径都由 spi_mem_exec_op() 执行,区别只在「opcode + 地址字节数 + dummy + 数据方向」:
三条路径的 spi_mem_op 事务结构(4 字节地址模式示例)读路径(以 1-1-4 为例,opcode 0x6B;若支持 4B opcode 则为 0xEC)opcode1 字节addr 4 字节A31~A24~A16~A8~A0dummy4 周期data(从器件读出,4 线)len 可任意(内核会按 bouncebuf/页大小分批)写路径(Page Program,opcode 0x02 / 1-1-4 为 0x32 / 4B 为 0x12)第 1 个事务:WREN(0x06),1 字节,无地址第 2 个事务:opcode → addr 4 字节 → data(长度不得超过页大小,典型 256B ) 第 3 步:轮询 RDSR(0x05)直到 WIP(bit0) 清零,超时由 max_timeout 控制 跨页写入内核会自动拆分;用户态 flashcp 也做了分页,但手写 ioctl 时必须自己分 擦除路径( Sector Erase 0x20 / 4B 为 0x21;Block 0xD8 / 4B 为 0xDC) WREN → opcode → addr 4 字节 → CS↑ → 轮询 WIP(64KB 块典型 300~1500ms,不能用固定延时 ) 内核按 erase_mask 拆分跨块擦除请求;擦除后该区域全为 0xFF 若 WIP 一直不灭且报超时,先查是否有写保护位未清(BP≠0 时器件直接不启动擦除)
图 6 · 读 / 写 / 擦三条路径的事务结构
4.7 编码规范与 remove
需要改内核代码时(加 ID 表条目、加 fixups),遵守以下规范,避免被 maintainer 打回:
- 注释用英文,且说明「为什么」不只「是什么」 。引用 datasheet 章节号与页码,例如
/* See section 4.2.1 of DS-xxxx Rev.3 */。 - 一行不超过 80 列,用 tab 缩进(内核风格)。
- 不新增无必要的中文注释;与主线风格冲突的本地化注释不进上游。
- 新增 ID 条目优先加到厂商文件 (
winbond.c/gigadevice.c...),不要塞进core.c。 - 提交信息带
Signed-off-by与Tested-by(含控制器名与实测频率),这是主线 review 的硬性要求。 - 提交前跑
scripts/checkpatch.pl --no-tree。 - 本地维护与上游提交分成两个 patch 序列:本地 hack 明确标注,方便后续 rebase。
remove / shutdown 路径上,框架会调用 spi_nor_restore() 把器件恢复到「复位后的默认状态」(退出 4 字节模式、关掉连续读模式)。这段代码是 厂商 fixups 里最需要小心的地方 :如果你的器件有非易失的状态被错误地「恢复」了, 下次启动行为就会变。写 fixups 时若操作了厂商私有位,务必在 fixups->restore 里对称还原。
5 · 设备树与分区设计
设备树是 SPI NOR 调试中最高频的修改点。一个字符写错(大小写、下划线与连字符、 单位写错)就可能导致「容量认不出」「分区表不生效」「性能没跑满」,而且往往没有任何报错。 本章把每个字段的语义、常见写错的后果都给出来。
5.1 spi-nor 节点字段全解
完整 spi-nor 节点示例与字段分组flash@0 {/* ① 身份与总线基础 */compatible = "jedec,spi-nor"; /* 走通用探测;也可加厂商串做优选 */reg = <0>; /* CS 号,不可省 */spi-max-frequency = <104000000>; /* 单位 Hz,不可写 104 */spi-rx-bus-width = <4>; /* 支持 4 线接收 */spi-tx-bus-width = <4>; /* 支持 4 线发送 *//* ② 大容量与 4 字节地址相关(第 6 章详解) */jedec-id = 0b 20 19 ; /* 覆盖 JEDEC ID(厂商/类型/容量码) */size = <0x4000000>; /* 字节数,64MiB 写十六进制更稳 */broken-flash-reset; /* 复位有问题的器件 */no-wp; /* 器件无 WP# 引脚时声明 *//* ③ 分区表子节点(必须开 CONFIG_MTD_OF_PARTS) */};
图 7 · spi-nor 节点字段的三组划分
| 字段 | 必填 | 正确写法 | 写错的后果 |
|---|---|---|---|
| compatible | 是 | "jedec,spi-nor";若希望优先匹配厂商表,可在前面加厂商串,如 "winbond,w25q512" |
写成 "jedec,spi" 等错误串 → 完全不会 probe(无任何日志提示) |
| reg | 是 | <0> 表示 CS0 |
缺省或写错 → 绑到不存在的 CS 上,probe 静默失败 |
| spi-max-frequency | 是 | 单位 Hz ,如 <104000000> |
写 <104> → 频率低到只有 104Hz,表现为「慢得离谱」但不报错 |
| spi-rx-bus-width | 否 | <4> / <2> / <1> |
不写则默认 1 线,性能损失但功能正常;控制器不支持时内核自动降级 |
| spi-tx-bus-width | 否 | <4>(写数据用四线可省时间) |
不写则用单线写,性能略降 |
| jedec-id | 否 | 字节数组,如 [ 0x0B 0x60 0x19 ];用厂商 ID 字节而不是「厂商字符串」 |
字节序或长度写错 → 与实际读到的 ID 不匹配,反而导致认不出器件 |
| size | 否 | 字节数 ,如 <0x4000000>(64MiB) |
与实际容量不符 → 分区表边界错位,写超容量区会静默丢失 |
| m25p,fast-read | 否 | 布尔,用 0x0B 快读(部分控制器/平台) | 属性名带厂商前缀,属历史遗留;非必要不写 |
| broken-flash-reset | 否 | 布尔。器件复位后行为异常时加 | 该加不加 → 掉电重启后 probe 失败 |
| no-wp | 否 | 布尔。器件无 WP# 引脚时声明 | 不声明但硬件有 WP# 且被拉低 → 写入 EIO |
| partitions | 否 | 子节点,见 5.2 | 需 CONFIG_MTD_OF_PARTS=y 才生效 |
最贵的三个笔误
① spi-max-frequency 忘了写单位 :<104> = 104Hz。 现象是「能读能写但慢到像坏了」,且 dmesg 完全正常。排查时直接 cat 一下这个属性确认。
② compatible 写错 :"jedec-spi-nor"(连字符)"jedec,spi-nor"(逗号)。 拼错的代价是 probe 静默不执行,dmesg 里一条日志都没有。
③ jedec-id 当成字符串写 :jedec-id = "0B6019" 是错的, 必须是字节数组 [ 0x0B 0x60 0x19 ]。
5.2 fixed-partitions 写法
flash@0 {
compatible = "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <104000000>;
spi-rx-bus-width = <4>;
spi-tx-bus-width = <4>;
partitions {
/* name 分区类型 偏移(字节) 大小(字节) */
/* --- --- --- --- */
partition-name0 { /* bootloader 所在分区 */
label = "boot";
reg = <0x00000000 0x00100000>; /* 0 - 1MiB */
};
partition-name1 {
label = "env";
reg = <0x00100000 0x00010000>; /* 1MiB - 1.0625MiB,64KiB */
};
partition-name2 {
label = "factory";
reg = <0x00110000 0x00800000>; /* 1.0625MiB - 9.0625MiB,8MiB 出厂参数 */
};
partition-name3 {
label = "rootfs";
reg = <0x00910000 0x01000000>; /* 9.0625MiB - 25.0625MiB,16MiB */
};
partition-name4 {
label = "data";
reg = <0x01910000 0x026F0000>; /* 25.0625MiB - 64MiB,剩余全部 */
};
};
};
分区表四大约束(违反任一项都会出问题)
① 起始偏移必须按擦除块对齐。 上例 0x00110000 起始的分区,前面 64KiB 正好对齐 64KB 块边界; 若写到 0x00108000(非对齐),内核会告警 Bad partition table 或该分区无法使用。
② 分区不能重叠,重叠会被内核拒绝 (partition overlaps),表现为后续分区全丢。
③ 所有分区之和不得超过器件容量。 超出部分内核告警并丢弃最后一个分区------ 这是「最后一个分区莫名消失」的标准原因 ,尤其在大容量器件上(4 字节地址没配好导致容量被当作 16MiB 时反之亦然)。
④ CONFIG_MTD_OF_PARTS=y 必须打开 ,否则整个 partitions 子节点被忽略。
手工核算分区表的小脚本(强烈建议写进 CI 校验):
# 把上例的分区写成脚本核算,确认无重叠、无越界、全对齐
python3 - <<'EOF'
FLASH = 64*1024*1024 # 器件容量(字节),从 datasheet 换算
ALIGN = 64*1024 # 对齐粒度 = 最大擦除块
parts = [
("boot", 0x00000000, 0x00100000),
("env", 0x00100000, 0x00010000),
("factory", 0x00110000, 0x00800000),
("rootfs", 0x00910000, 0x01000000),
("data", 0x01910000, 0x026F0000),
]
err = 0
prev_end = 0
for name, off, size in parts:
if off % ALIGN: print("[X] %s 偏移未对齐 0x%x" % (name, off)); err += 1
if off < prev_end: print("[X] %s 与前一分区重叠" % name); err += 1
if off + size > FLASH: print("[X] %s 超出器件容量" % name); err += 1
prev_end = off + size
print(" %-8s 0x%08x - 0x%08x (%.2f MiB)" % (name, off, off+size, size/1048576))
print("分区总数 %d,覆盖 %.4f MiB / %.4f MiB" % (len(parts), prev_end/1048576, FLASH/1048576))
print("结果:" + ("通过" if err == 0 else "有 %d 处错误" % err))
EOF
5.3 分区表三处一致性
这是量产阶段最高频的故障源。同一份分区表可能出现在三个地方,必须完全一致:
分区表的三个来源与生效优先级设备树 fixed-partitionsarch/arm64/boot/dts/...partitions 子节点推荐作为唯一真源内核命令行 mtdpartsbootargs 里形如mtdparts=spi0:1M(boot),64K(env)优先级高于设备树!U-Boot 分区表CONFIG_CMD_SF + 同一 DT或 mtdparts 环境变量与内核不一致则启动错位经典故障:bootargs 里的 mtdparts 悄悄覆盖了设备树现象:/proc/mtd 显示的分区与 DTS 里写的不一样 → rootfs 挂不上或挂错位置 自检:cat /proc/cmdline → 若有 mtdparts,先改这里,再谈设备树 推荐做法:单一真源 + 自动生成 用一个 partitions.csv 描述全部分区,脚本生成 DTS 片段 + U-Boot 环境变量 + 产线烧录清单 三者由同一次生成,天然一致;换料改容量时只改 CSV 一处
图 8 · 分区表三处来源与生效优先级
# 三处一致性自检清单(每次出版本前逐条执行)
# 1) 设备树里的分区(反编译正在运行的 dtb)
dtc -I dtb -O dts /proc/device-tree 2>/dev/null | grep -A3 label || \
dtc -I dtb -O dts out/arch/arm64/boot/dts/vendor/board.dtb | sed -n '/flash@0/,/};/p'
# 2) 内核实际生效的分区
cat /proc/mtd
# 3) 内核命令行是否带 mtdparts
cat /proc/cmdline
# 4) U-Boot 侧(在板子上执行)
sf probe; mtd list; printenv mtdparts
# 5) 对比三者的 name 与 offset/size 是否逐一相等(脚本化)
5.4 pinctrl / 供电 / 复位 GPIO
SPI NOR 节点的完整形态(含 pinctrl、供电、可选复位):
&qspi0 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&qspi0_default>;
#address-cells = <1>;
#size-cells = <0>;
flash@0 {
compatible = "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <104000000>;
spi-rx-bus-width = <4>;
spi-tx-bus-width = <4>;
/* 可选:由 GPIO 控制的硬件复位。绑定驱动为 reg + reset-gpios */
reset-gpios = <&gpio 12 GPIO_ACTIVE_LOW>;
/* 可选:显式声明供电,便于内核做电源域开关(DPD 前必关) */
vcc-supply = <&vcc_3v3>;
/* 可选:标记为 bootloader 专用分区(新版内核支持 bootlinux 风格属性) */
label = "nor0";
};
};
&pinctrl {
qspi0_default: qspi0_default {
/* 关键:四线模式的 IO2/IO3 即 WP#/HOLD#,此处必须配成 GPIO 复用而非浮空 */
pinmux = <pinmux SPI0_CLK >, <pinmux SPI0_MOSI>,
<pinmux SPI0_MISO>, <pinmux SPI0_CS >,
<pinmux SPI0_IO2 >, <pinmux SPI0_IO3 >;
bias-disable; /* 外部已有上拉时关闭内部上下拉 */
drive-strength = <8>; /* 按 SI 情况调,缺省即可 */
input-schmitt-enable; /* 1.8V 平台建议开,见 2.3 */
};
};
| 配置项 | 缺失 / 配错的症状 | 自检命令 |
|---|---|---|
| pinctrl 中 IO2/IO3 的复用 | 四线读返回全 FF 或数据错乱;但 0x03 单线读正常 |
grep -i qspi /sys/kernel/debug/pinctrl/*/pinmux-pins |
| CS# 上拉 | 上电或高阻态时误选中,probe 到错误 ID | 万用表量 CS# 空闲电平应为高 |
| reset-gpios | 不复位时,掉电重启后器件状态残留(4B 模式、QE、保护位) | 示波器看 RESET# 是否有低脉冲 |
| vcc-supply | 进 DPD 前不关电 → 电流偏高、DPD 失效 | `cat /sys/kernel/debug/regulator/*/summary |
| 时钟源与分频 | 实际频率与 spi-max-frequency 不符(分频只能取 2 的幂) |
控制器 debugfs 或示波器测 SCLK 周期 |
为什么 spi-rx-bus-width=<4> 写了却还是单线
DT 里的 spi-rx-bus-width 只是告诉内核「器件支持四线」 ; 最终能不能用四线,取决于 SoC 的 SPI 控制器驱动是否上报了 SNOR_HWCAPS_READ_1_1_4 / 1_4_4 能力 , 以及 pinctrl 里 IO2/IO3 是否配好。内核取二者交集。 所以「写了 4 还是慢」时,先看 /sys/kernel/debug/spi-nor/<dev>/params 里的 read 协议与 /sys/kernel/debug/spi-nor/<dev>/capabilities 里控制器支持的能力, 而不是反复改 DT。
5.5 overlay 与多板型
同一 SoC 平台常有多个容量/电压档的板型。用 overlay 隔离差异,避免 #ifdef 式 DTS 混乱:
/* 1) 抽取公共部分:board-common.dtsi 里放 pinctrl 与控制器本体 */
&qspi0 { status = "okay"; /* pinctrl, #address-cells 等 */ };
/* 2) 每个板型一个 overlay:overlay-xtx-512mbit-3v3.dts */
/dts-v1/;
/plugin/;
/ {
fragment@0 {
target = <&qspi0>;
__overlay__ {
flash@0 {
compatible = "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <104000000>;
spi-rx-bus-width = <4>;
spi-tx-bus-width = <4>;
/* 512Mbit 需要 4 字节地址:此处在内核已有 ID 条目时无需额外属性;
若是自研器件且需强制,写 jedec-id 覆盖,见 6.6 */
};
};
};
};
# 编译与合并 overlay
dtc -@ -I dts -O dtb -o overlay.dtb overlay-xtx-512mbit-3v3.dts
fdtoverlay -i base.dtb -o final.dtb overlay.dtb
# 在 U-Boot 中应用
load ${fdt_addr_r} ${ramdisk_addr_r}:#dtb-${model}
fdt addr ${fdt_addr_r}; fdt overlay ${ramdisk_addr_r}:#dtb-${model}
bootm ${kernel_addr_r} ${ramdisk_addr_r} ${fdt_addr_r}
多板型管理的两条纪律
① 每块板子都要有唯一的「容量 / 电压 / 4B 方式」三元组记录 ,写进板卡丝印或产线数据库; 否则同一型号 Flash 换料后,板子外观一样但行为不同,现场无法区分。
② 容量与 4 字节地址能力必须与「实物」绑定验证------ 产线首件一定要执行「跨越 16MiB 边界的写读校验」(见 9.2),不能只验前 1MiB。
文档编号 LINUX-SPINOR-DRV-001 · V1.0 · 命令码、状态寄存器位、器件容量码与 4 字节地址方式均以对应料号的正式 datasheet 为准; 内核结构体与函数名随版本变化,以实际源码 drivers/mtd/spi-nor/ 为准。 配套文档:《SPI_NOR_Flash_电路设计指南.html》《SPI_NOR_Flash_软件设计规范.html》、 《Linux_SPI_NAND_驱动开发指南.html》。
后续章节请下载「芯参谋」查看
......(篇幅原因, 还有17章· 详见芯参谋→方案设计)
打开路径:🔧解决方案&应用市场分析 -> 原理方案设计 -> 嵌入式驱动&系统开发 -> Linux_SPI_NOR_驱动开发指南
相关报告 · 1
- 博通(Broadcom)平台_SPI_NOR_NAND_eMMC_量产排查调试&解决方案
- 烧录成功但不开机_PPI_NAND_量产排查调试&解决方案
- 雅特力(AT32)_开发指南原理方案设计
- KTV点播器(KTV Player)_市场分析市场应用分析
- 擦窗机器人(Window Cleaning Robot)_市场分析市场应用分析
相关报告 · 2
- UPS电源(UPS)_市场分析市场应用分析
- 芯驰(SemiDrive)_开发指南原理方案设计
- 树莓派(Raspberry Pi)平台_QSPI_NOR_eMMC_量产排查调试&解决方案
- 扫拖一体机器人(Vacuum-Mop Robot)_市场分析市场应用分析
- 合宙(Luat)平台_SPI_NOR_量产排查调试&解决方案