news 2026/10/1 1:37:59

嵌入式Linux SPI NOR Flash调试全解析:以W25Q128为例

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式Linux SPI NOR Flash调试全解析:以W25Q128为例

在嵌入式Linux开发里,SPI NOR Flash是最常见的存储介质之一,很多板卡上跑u-boot、kernel、根文件系统都靠它。我前阵子调的一块板子用的正是一颗W25Q128,128Mbit,也就是16MB,挂在CPU的SPI控制器上,要扛起bootloader、内核镜像和一份只读根文件系统的存储任务。从内核配置、设备树编写,到读JEDEC ID、分区、擦写校验,整个过程踩了不少坑,也把Linux SPI子系统和MTD驱动栈完整过了一遍。这篇文章不绕弯子,直接讲在Linux kernel环境下调试W25Q128的完整思路和操作流程,适合刚接手内核驱动、对SPI NOR Flash还不太熟的工程师,也适合只想快速让板上Flash跑起来的朋友。

1. 调试前,先摸清这颗Flash和它背后的软件栈

1.1 W25Q128的硬件底细:容量、页、擦除粒度

W25Q128是一颗标准的SPI NOR Flash,容量128Mbit,换算出来就是16MB,地址范围从0x000000到0xFFFFFF,刚好24位地址线。它的基本访问单位有三个,很多人一开始会搞混:页(Page)256字节,扇区(Sector)4KB,块(Block)64KB。16MB的空间一共分成4096个扇区、256个块,每个块里包含16个扇区。

这三层结构决定了你操作Flash时的三种命令:写数据是按页编程,一次最多写256字节,而且不允许跨页;擦除最小单位是扇区,4KB起步;块擦除是64KB,专门用来擦更大范围的数据。NOR Flash的特性就是“可以随机读,但写之前必须先擦除”,而且擦除会把对应的区域重置为0xFF,所以每一次写入前,你都要先算清楚数据落在哪个扇区、有没有擦过。调这种Flash,脑子里必须时刻有一张地址比例尺,否则写到一半发现越界或者擦错区域,恢复起来很头疼。

W25Q128的引脚也很关键,标准8脚封装下就是CS片选、CLK时钟、DI(MOSI)、DO(MISO),外加WP写保护和HOLD保持。调试时最容易翻车的是WP和HOLD不能悬空,严格来说要上拉到高电平。HOLD一旦被拉低,芯片直接忽略时钟线上的变化,表现就是读ID读到一半卡死、写操作毫无反应。

1.2 SPI时序里最容易出问题的几个点

SPI是同步串行协议,没有标准地址概念,全靠片选加时钟配合。W25Q128支持的常见模式是Mode 0和Mode 3,绝大多数Linux驱动默认配置成Mode 0,即CPOL=0、CPHA=0,空闲时钟线为低,数据在时钟上升沿采样。如果你的板子上Flash接的引脚和别的外设复用,或者控制器本身特性决定了必须用Mode 3,就会出现一种很诡异的现象:芯片偶尔能读、偶尔不能读,或者能读ID但擦写错误。

调试SPI NOR Flash时,至少要把片选、时钟、DI、DO四根线搞清楚。片选相当于电话接通——CS拉低,表示“我要跟这颗芯片说话”;CLK是节奏;DI是主机发给芯片的数据,DO是芯片回给主机的数据。一次完整的读ID操作,就是CS拉低、发送0x9F命令,芯片从DO逐位吐出3个字节的厂商ID和设备ID,最后CS拉高结束传输。整个过程看着简单,但CS拉低的时机、时钟的边沿、命令后是否要dummy周期,任何一个不对,读回来的都是0xFF或者垃圾数据。

还有一个特别容易被忽略的细节:W25Q128这类器件不仅有普通读命令0x03,还有Fast Read命令0x0B。Linux SPI NOR驱动在匹配到具体型号后,会优先使用更快的方式,比如Fast Read或四线Fast Read。调试初期如果发现读数据不稳定,我的建议是先别急着上高性能模式,用标准单线SPI把链路验证通了再说,否则很难判断是时序问题还是模式切换的问题。

1.3 内核里的数据通路:SPI控制器、SPI-NOR、MTD各管什么

在Linux内核中,SPI NOR Flash的驱动栈分了好几层。最底层是SPI控制器驱动,负责跟硬件寄存器打交道,把你用SPI协议传出去的数据变成引脚上的时钟和电压;中间层是SPI核心和SPI NOR驱动,具体路径在新版本内核里一般在drivers/spi/spi-nor目录下,老版本则在drivers/mtd/spi-nor目录,它们负责解析JEDEC ID、识别具体型号、初始化芯片状态;最上层是MTD子系统,也就是Memory Technology Device,提供/dev/mtd0、/dev/mtdblock0这类设备节点,供文件系统和应用层直接读写。

这套分层设计的核心价值,是把“怎么跟Flash芯片说话”和“怎么管理存储分区”解耦开。换一颗不同品牌的Flash,只要它兼容JEDEC标准,上层MTD分区和文件系统完全不用动。反过来,如果你的需求只是想在某个SPI总线上挂一个自定义传感器,那就不需要MTD,直接走SPI设备的spidev接口就够了。调试W25Q128时,知道数据在哪一层断掉很重要:读ID失败,大概率在SPI物理链路或控制器配置;能识别到型号但读写文件失败,问题可能出在MTD层的擦写逻辑或上层分区方案。

2. 内核配置与设备树:让Linux真正“认出”这颗Flash

2.1 kernel config需要打开哪些选项

要把W25Q128跑起来,内核需要打开SPI控制器、SPI NOR驱动和MTD相关选项。这里具体选项取决于你用的内核版本和平台,但思路是一致的。打开内核配置菜单后,至少要确认这几项:

  • MTD支持,比如CONFIG_MTD=y;
  • SPI NOR Flash支持,新内核叫CONFIG_MTD_SPI_NOR=y,老内核可能有CONFIG_MTD_M25P80;
  • SPI控制器驱动,这个跟具体平台绑定,比如CONFIG_SPI_DESIGNWARE、CONFIG_SPI_ROCKCHIP之类;
  • SPI核心支持,通常是CONFIG_SPI=y和CONFIG_SPI_MASTER=y。

经常有人把SPI控制器驱动漏了,结果设备树写了一大堆,板上Flash纹丝不动,dmesg里连spi控制器都没注册。还有一点,新版内核把很多选项从模块改成内建会更省心。我自己调试时习惯直接把MTD_SPI_NOR编进内核而不是作为模块,因为模块加载顺序有时候会带来不必要的麻烦,比如rootfs都还没挂载时模块文件可能都还没就绪。

2.2 设备树节点详解:compatible、reg、spi-max-frequency

设备树是把硬件拓扑描述给内核的关键。一个典型的W25Q128节点长这样:

&spi1 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_spi1>; w25q128: flash@0 { compatible = "jedec,spi-nor"; reg = <0>; spi-max-frequency = <50000000>; spi-tx-bus-width = <1>; spi-rx-bus-width = <1>; }; };

这里最核心的compatible是"jedec,spi-nor",而不是直接写"w25q128"。原因很简单:SPI NOR框架会主动发送读取JEDEC ID的命令,然后在内核的匹配表里查型号,自动识别出这颗芯片叫w25q128、容量16MB。这样写的好处是你的设备树不需要跟着Flash型号走,即使某天供应商把W25Q128换成了GD25Q128这类兼容芯片,软件基本不用动。

reg = <0>表示这颗Flash挂在SPI控制器的第0个片选上,也就是CS0。如果板子上用了多个片选,reg的数值要跟硬件连线对应上。spi-max-frequency这里要注意,不要一上来就写104MHz或者更高。调试期我习惯先写50MHz左右,稳定后再尝试提频。spi-tx-bus-width和spi-rx-bus-width默认是1,也就是标准SPI单线模式,等单线通了再考虑四线QSPI。

还有一处非常容易出错:如果SPI控制器用GPIO做片选而不是硬件片选,设备树里要显式声明cs-gpios。比如cs-gpios = <&gpio0 25 GPIO_ACTIVE_LOW>;很多平台默认用硬件片选,你一旦写了gpio片选控制器但GPIO没配对,CS引脚电平就一直对不上,Flash自然没有响应。

2.3 分区规划:从bootloader到文件系统的存放策略

MTD层最大的优势之一就是支持分区。分区表可以直接放在设备树里,用fixed-partitions来描述。比如我这次把16MB分成三个区:

partitions { compatible = "fixed-partitions"; #address-cells = <1>; #size-cells = <1>; bootloader@0 { label = "bootloader"; reg = <0x0 0x100000>; }; kernel@100000 { label = "kernel"; reg = <0x100000 0x400000>; }; rootfs@500000 { label = "rootfs"; reg = <0x500000 0xB00000>; }; };

bootloader分1MB,kernel分4MB,剩下的11MB给rootfs。这个方案在16MB Flash上很常见。要注意的是,u-boot这类bootloader通常会被CPU在复位后直接镜像到内存执行,所以一般放在偏移0的位置,分区大小要留足,否则后续升级镜像都放不下。rootfs如果是只读的squashfs,可以省去日志空间;如果要跑可写文件系统,还得再分一个data区,并考虑挂载时的读写参数。

分区规划不只影响内核视角,还会影响uboot、烧录脚本、远程升级工具。一块Flash在uboot里看到的分区和kernel里看到的分区如果不一致,后续OTA或者命令行擦写时很容易误操作,把bootloader区冲掉。所以分区表一定要统一维护,别在uboot里改一套、设备树里写另一套。

3. 实操全流程:用最小步骤验证整条链路

3.1 确认设备枚举:dmesg、/proc/mtd、/sys/bus/spi

板子启动后,第一件事不是急着去读文件,而是先确认内核到底有没有看到这颗Flash。几条命令就能定位:

dmesg | grep -i spi dmesg | grep -i mtd cat /proc/mtd ls -l /sys/bus/spi/devices/ cat /proc/partitions

正常情况下,dmesg里会有类似这样的输出:

spi-nor spi1.0: w25q128 (16384 Kbytes)

/proc/mtd里也会列出分区表和大小。看到这行输出,就说明内核已经通过JEDEC ID识别出这颗芯片了。如果dmesg里只有SPI控制器注册的信息,没有spi-nor相关输出,那说明设备树节点和驱动没有对上,或者Flash链路还没通。

还有一种情况,/sys/bus/spi/devices/下面看不到spi1.0节点,却能看到spi_master。这说明SPI控制器注册了,但片选上没有设备。这时候优先检查设备树里的status是否设为okay、reg值是否和硬件片选对应、引脚复用是否配好。

3.2 用mtd-utils做完整的擦写校验

确认设备识别无误后,就可以上mtd-utils工具。多数发行版直接安装或板端busybox里也带了精简版。几个核心命令我先列出来:

# 查看分区信息 mtd_debug info /dev/mtd0 # 擦除第一个分区,偏移0,长度0表示整个分区 flash_erase /dev/mtd0 0 0 # 写文件进分区 flashcp -v /tmp/test.bin /dev/mtd0 # 读出来校验 mtd_debug read /dev/mtd0 0 4096 /tmp/readback.bin cmp /tmp/test.bin /tmp/readback.bin

我实际调这块板子时,第一次读ID、分区都是正常,但往rootfs分区里写文件时总在最后几秒报错。后来排查发现是分区大小没算对,rootfs分区分配的空间比实际文件系统稍微小了一点,写到最后溢出了。所以拿到Flash第一步,先认真算一遍自己的分区边界和文件系统镜像大小,别只盯着分区是否挂载成功。

写的过程中还要记住NOR Flash的一个硬性规则:写之前必须擦除。用flashcp的好处是它会自动帮你完成擦除和校验。如果你直接用dd往/dev/mtdblock0里写,就得自己保证目标区域已经擦除过,否则数据会错乱。

3.3 性能摸底:读、擦、写到底多少时间

调试一个存储设备,不摸清它的性能底线,后面做OTA或者量产烧录时就会很被动。W25Q128在50MHz时钟下单线SPI模式下,读速度大概能到6MB/s左右,擦除整个16MB芯片通常需要几十秒到一两分钟,写入全部空间由于要反复页编程,时间更长。我的习惯是先测一轮基线数据:

# 全片读测速,读取16MB time dd if=/dev/mtd0 of=/dev/null bs=4096 count=4096 # 擦除整个rootfs分区(11MB) time flash_erase /dev/mtd3 0 0 # 写入一个固定大小的镜像 time flashcp -v /tmp/rootfs.squashfs /dev/mtd3

实测下来,读16MB大概2到3秒,擦除11MB大约20到30秒,写入11MB镜像大约需要40到60秒,跟你用的是页编程还是双线模式直接相关。如果测出来的读速度只有几十KB/s,那多半是SPI时钟没跑到预期值,或者每次读取的块大小太小,SPI传输在反复切换CS,吃掉了大量开销。

3.4 进阶验证:绕过MTD直接发SPI命令读ID

在个别疑难场景下,比如dmesg里一直识别不到Flash,我会选择绕过MTD层,直接把SPI设备挂成spidev,手动发命令测试物理链路。做一个临时设备树节点,把compatible改成"spidev",然后重新编译设备树。启动后,用spidev_test发一条读ID命令:

spidev_test -D /dev/spidev1.0 -p "\x9f"

不过这个方法有局限,发送一个字节时,读回来的第一个字节是无效数据,因为SPI全双工模式下数据是边发边收的。想直接看到ID,最好多发送几个字节,读命令后面跟几个0x00填充位,这样ID会出现在响应的后续字节里:

spidev_test -D /dev/spidev1.0 -p "\x9f\x00\x00\x00\x00"

正常响应里会出现0xEF、0x40、0x18这组数据,对应Winbond厂商ID和W25Q128的设备ID。这个方法属于底层链路探测的兜底手段,一切正常后记得把设备树改回来,因为spidev是通用设备接口,内核不会把这块设备当存储分区使用。

4. 调试中常见的坑与排查思路

4.1 读出来全是0xFF,Flash像“没接”一样

这个现象在SPI NOR Flash调试中太常见了。如果读ID都是0xFF,基本可以断定主控和Flash之间没有任何有效数据传输。排查顺序我建议是先硬件后软件:先看CS脚是否拉低、CLK是否有波形、DI是否正常、DO有没有输出。用示波器或逻辑分析仪去抓CS下降沿和后面的时钟波形,触发条件设置为CS下降沿,很容易看出问题。

软件方面,最常见的原因是pinctrl配置不对。很多平台的SPI引脚和GPIO是复用的,设备树里如果不配pinctrl,或者配了但状态不对,引脚仍然停留在GPIO模式,CLK和MOSI根本出不来波形。另一个原因是compatible写成具体型号但驱动表里没这个条目,导致spi-nor驱动没有绑定到设备上。统一用"jedec,spi-nor"能省掉很多这种坑。

4.2 能识别型号但擦写报错

能识别出w25q128,说明JEDEC链路没大问题。这时候擦写失败,优先考虑你擦写的地址和大小有没有对齐。NOR Flash页编程要求起始地址和长度对齐到256字节边界,擦除至少要按4KB扇区对齐。很多上层工具做了封装,但底层调用如果不对齐,Flash会直接忽略命令或者报错。

还有一种可能:Flash芯片处于写保护状态,这是很大的坑,单独拿出来说。

4.3 只能读不能写:WP引脚和状态寄存器怎么查

W25Q128有WP引脚,低电平有效。硬件上WP必须拉高,否则芯片硬件写保护生效。软件侧还有一个状态寄存器的写保护位,BP0到BP3,这几位的组合决定了哪些区域受保护。Linux SPI NOR驱动在探测芯片时通常会尝试把整个Flash设为unprotected,但如果你用的内核版本比较老,或者uboot启动阶段改了状态寄存器没恢复,就会出现系统里看着一切正常,一写就报错。

排查时可以先看寄存器状态,最简单的方式是顺着mtd-utils工具走:

flash_unlock /dev/mtd0

如果flash_unlock已经成功,但写操作仍然失败,那就手工检查WP引脚电平。我有一次是PCB上WP引脚没接上拉,芯片手册要求内部上拉但实际没生效,结果写一会好一会坏,问题特别隐蔽。这类硬件细节,调试时不能只盯着代码看,万用表点一下引脚电平会快很多。

4.4 写进去读出来不一致

写入验证失败,未必是Flash坏了。首先要排查的是时序问题,SPI频率设置过高,信号完整性跟不上,数据在某一位上翻转,读出来就会隔三差五出现一个字节不对。把spi-max-frequency降到33MHz甚至25MHz再测,如果问题消失,那就是信号质量的问题。

其次要排查电源。NOR Flash在擦除和编程时瞬间电流很大,如果板上的电源去耦电容不足,电压跌落就会导致写入过程中芯片内部状态错乱。W25Q128的供电脚旁边至少要放一颗0.1uF陶瓷电容靠近芯片引脚,条件允许再加一颗4.7uF或10uF电容做储能。这个细节在原理图审查阶段就该盯住,省得后续调试时找半天原因。

还有一点容易被忽略:如果使用了DMA传输,要留意DMA缓存一致性问题。尤其在老内核或某些架构下,DMA缓冲区没有正确flush/invalidate,主机这边传过去的数据可能根本不是内存里那部分内容。

4.5 设备树分区不生效,或启动参数覆盖分区表

有时候你设备树里分区写得清清楚楚,cat /proc/mtd却发现分区不对,甚至只有一个整块分区。这种问题在从uboot启动内核时尤其常见,uboot通过bootargs传入的mtdparts参数会优先于设备树分区表。

mtdparts=spi1.0:1m(bootloader),4m(kernel),-(rootfs)

内核如果解析到这个参数,会直接使用cmdline分区配置,设备树里的fixed-partitions就会被忽略。排查方法是去掉bootargs里的mtdparts,或者在设备树里不混合使用两种分区方式。我自己现在更推荐统一用设备树管理分区,因为uboot和kernel可以共用设备树即可,减少一份配置漂移的风险。

4.6 与新内核版本的兼容性问题

内核版本对调试的影响很大。4.x到5.x之间,SPI NOR驱动从drivers/mtd/spi-nor迁移到了drivers/spi/spi-nor,同时引入了spi-mem框架。你搜到的一些老文章里提到的CONFIG_MTD_M25P80在5.10以后基本就没了,替换成了CONFIG_MTD_SPI_NOR。如果你按老教程打开配置却找不到对应选项,先确认内核版本和Kconfig路径,不然会白白浪费很多时间。

同样,设备树里的compatible用法也随内核演进有过调整。老内核里写具体型号如"w25q128"可能也能工作,但新内核里推荐统一使用"jedec,spi-nor",让驱动通过JEDEC ID自动匹配。搞清楚你手里的内核版本,再对照源码里的匹配表去排查,比在网上零散找答案要靠谱得多。

写在最后

调完这颗W25Q128,我最深的体会是,SPI NOR Flash的调试本质上就是一条数据通路的验证:从物理引脚到SPI控制器,再到SPI NOR驱动,最后到MTD分区和文件系统。每一层都有办法单独验证,只要你按照“先硬件波形、再内核识别、最后数据读写”这个顺序来,大多数问题都能快速缩小范围,而不是一堆错误混在一起互相干扰。另外一个小经验,凡是碰NOR Flash,我习惯在开发阶段先把频率压低、关闭各种加速模式,让系统以最朴素的模式跑通,之后再做性能优化。这个习惯帮我避开了不少信号完整性和模式切换带来的隐性坑,也推荐你们试试。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 1:37:49

uni-app HBuilderX与手机端SDK版本不匹配排查修复

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 1:37:30

YOLOv8手势检测实战:数据集转换、训练调参与部署

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 1:36:31

Python深度学习文本相似度检测系统:从源码解压到实战复现

简介&#xff1a;面向毕业设计及深度学习实践者的完整项目包&#xff0c;实现基于BERT模型的文本相似度检测系统。系统综合欧氏距离、余弦相似度、曼哈顿距离等算法&#xff0c;并配备文件管理模块&#xff1a;支持创建文件夹、按指定目录上传、批量删除/下载、搜索及收藏&…

作者头像 李华
网站建设 2026/10/1 1:35:36

RTOS线程优先级原理:FreeRTOS与Zephyr底层调度机制对比

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 1:35:32

Windows Server 2016 安装 OpenSSH 全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 1:34:44

STARLIMS V11深度解析:实验室信息管理系统的架构、功能与落地实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华