Linux 系统(SPI NOR)驱动开发指南
编制日期 2026-10-05
面向 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 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 给 A[30: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) 看内核版本号 Rev 1.4 -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 的几何参数抄进表格并核对这三者一致。
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
把本项目需要的配置写成一个 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 看是否变化 |
像 0xB7(EN4B)这类命令切换的 4 字节模式是易失状态——
硬件复位或掉电后器件自动回到 3 字节模式。如果 bootloader 在 4 字节模式下写了数据、然后硬件复位、
内核再按 3 字节模式去读同一地址,会读出错误数据但不报错。
这正是第 6.9 节要专门讲「U-Boot 与内核模式一致性」的原因,
而专用 4B opcode(第 6.2 节方案 A)之所以更省心,就是因为它无状态。
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 器件的输入阈值比 3.3V 低很多,上拉电阻阻值要相应减小(4.7k~10k 更常见,取决于总线电容与频率)。
若 1.8V 器件在你板子上「低速正常、高速随机错」,先怀疑上拉过弱 + 走线过长,
其次才是试 spi-max-frequency。另外确认 SoC 侧引脚配置里有没有开施密特触发(Schmitt),
开上以后上升沿的鲁棒性差别很大。
2.4 必留测试点与联调顺序
硬件联调按「先量电平、再抓波形、最后改驱动」的顺序做,不要一上来就改代码:
「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 根本没被重新编译(板级 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 用 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 软件分层总览
从「现象」往「根因」下钻的顺序是:用户态报错 → 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 执行全流程
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(那只是期望值)。
顺序是:① 先确认 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 + 数据方向」:
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 里对称还原。
文档编号 LINUX-SPINOR-DRV-001 · V1.0 ·
命令码、状态寄存器位、器件容量码与 4 字节地址方式均以对应料号的正式 datasheet 为准;
内核结构体与函数名随版本变化,以实际源码 drivers/mtd/spi-nor/ 为准。
配套文档:《SPI_NOR_Flash_电路设计指南.html》《SPI_NOR_Flash_软件设计规范.html》、
《Linux_SPI_NAND_驱动开发指南.html》。