芯参谋(87):Linux 系统(SPI NOR)驱动开发指南

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 把工具链前缀换成对应即可。核心流程如下:

  1. 拉 SDK / 内核源码 :厂商 SDK 通常带 kernel/、u-boot/、buildroot/ 或 yocto/。本文以 Linux 6.6 为基准线。

  2. 配置并交叉编译内核 :

    复制代码
    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
  3. 单独编译 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 ...
  4. 部署到板子 :scp mtd-utils/flash_* root@板子:/usr/sbin/,或打进 rootfs 镜像。

  5. 设置交叉工具链环境 (把日志重定向到主机,方便剪贴板):

    复制代码
    # 主机侧
    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

相关报告 · 2

相关推荐
wuminyu7 小时前
JVM规范学习总结:解释器与JIT编译器如何处理JVM字节码
java·linux·c语言·jvm·c++
海宇AI8 小时前
零信任架构实战:基于海宇个人风险报告构建自动化分期商城信用额度初审网关
运维·人工智能·架构·自动化
IMPYLH8 小时前
Linux 的时间为什么会被拨回去
linux·运维·服务器
2601_963282779 小时前
寒地窄带通信工程实践:黑龙江哈尔滨对讲机选型、组网与合规运维总结
运维·服务器·网络
叼烟扛炮10 小时前
[Git 实战] 学习笔记:从生成 SSH 密钥到解决 TortoiseGit 推送报错全指南
linux·运维·笔记·git·学习
xbzb10 小时前
OpenStack 网络管理(理论):Linux 网络虚拟化是 Neutron 的地基
linux·服务器·网络·openstack·network·neutron
我要见SA姐111 小时前
GitHub Actions 自动化:5 个真实运维提效案例
运维·数据库·机器学习·oracle·回归
起个网名真难~11 小时前
STM32F103_Bootloader_从零到UART升级成功_博客原稿
stm32·单片机·嵌入式硬件
Android系统攻城狮12 小时前
Linux FFmpeg深度解析之avcodec_flush_buffers调用流程与实战(十九)
linux·运维·服务器·ffmpeg·车载音视频·ffmpeg音视频进阶