干过嵌入式Linux开发的朋友,十有八九都跟SPI NOR Flash打过交道。W25Q128这颗芯片更是出镜率极高,128Mbit容量、SPI接口、四个引脚就能搞定一个根文件系统或者U-Boot环境变量区,性价比在8MB-16MB这个区间里几乎没有对手。但越是常见的芯片,调起来越容易翻车——我见过不少人在内核里把驱动选项勾上了、设备树也写了,断电重启后却死活读不到Flash,或者读到ID却擦写失败,最后折腾半天发现是某个细节没注意到。
这篇文章就围绕Linux kernel环境下调试W25Q128这条主线,把从硬件确认、内核配置、设备树编写,到驱动框架拆解、读写验证、波形分析这一整套流程完整走一遍。内容适合正在做平台bringup的工程师,也适合刚接触内核驱动、想搞懂SPI NOR Flash完整工作路径的学生。我会把实际调试中那些手册里写不清楚、文档里找不到的坑一并讲透。
1. 调试W25Q128之前,先把这几件事搞清楚
1.1 为什么W25Q128是调试SPI NOR的首选芯片
W25Q128来自华邦(Winbond),容量128Mbit,换算过来就是16MB。它支持标准SPI、Dual SPI和Quad SPI三种访问模式,标准模式下时钟最高可以跑到133MHz(快速读指令)。具体到内核调试阶段,我们关心的核心信息就几个:JEDEC ID是0xEF 0x40 0x18,页大小256字节,扇区大小4KB,块大小64KB,支持SFDP(Serial Flash Discoverable Parameters)协议。
这颗芯片在Linux内核的drivers/mtd/spi-nor/目录下有非常成熟的支持,基本属于“插上就能识别”的类型。但也正因为支持太成熟,一旦板子上没跑起来,反而要怀疑是不是自己的配置问题。它的兼容型号也很多,比如GD25Q128(兆易创新)、XM25QH128C(复旦微)等,寄存器指令集几乎完全兼容,这给选型留了很大余地。
1.2 硬件连接与原理图检查清单
调试的第一步不是写代码,而是对照原理图把SPI引脚确认清楚。W25Q128标准接线是六根线:CLK(时钟)、DI(数据输入,MOSI)、DO(数据输出,MISO)、CS#(片选)、WP#(写保护)、HOLD#(保持)。
这里有两个极其容易踩的坑。
第一,WP#和HOLD#不能悬空。很多开发板的原理图为了省事,把WP#和HOLD#直接悬空,结果内核读写Flash一切正常,但执行擦除操作时芯片始终返回0x80状态错误。原因很简单:WP#悬空时电平不确定,一旦被拉低,状态寄存器里的块保护位(BP3-BP0)就生效了,芯片拒绝任何擦写指令。正确的做法是把WP#和HOLD#上拉到VCC,或者用GPIO控制。
第二,片选引脚的GPIO极性。如果片选是用GPIO模拟的,设备树里cs-gpios属性必须声明正确的标志。SPI片选是低有效,所以GPIO标志是GPIO_ACTIVE_LOW。写成高有效的话,内核访问Flash时会看到片选永远拉不高、也拉不低,总线上的设备响应逻辑完全错乱。
1.3 内核配置:少了这几个选项,驱动根本不工作
Linux内核里SPI NOR Flash的驱动路径经过了几次重构。老版本(4.x早期)用的是m25p80驱动,新版本(4.18以后)统一走spi-nor框架,再配合spi-mem接口层。不管哪个版本,内核配置里这几个选项都必须开启:
CONFIG_MTD=y CONFIG_MTD_SPI_NOR=y CONFIG_SPI=y CONFIG_SPI_MASTER=y如果用的不是mtdblock而是直接挂根文件系统,还需要根据文件系统类型开启对应的支持。比如用JFFS2就开CONFIG_JFFS2_FS,用UBIFS开CONFIG_UBIFS_FS。
这里有个细节很多人忽略:老内核里CONFIG_MTD_M25P80这个选项独立存在,新内核里它被合并进CONFIG_MTD_SPI_NOR了。如果你在旧内核上照着新教程配选项,会发现根本找不到对应的配置项。所以调试前先确认内核版本,别把网上搜到的配置方法直接照抄。
另外,设备树里必须写对compatible属性。当前内核推荐写法是"jedec,spi-nor",这个compatible是万能兼容串,无论是W25Q128还是GD25Q128,只要符合JEDEC规范,内核都会尝试通过读取SFDP和JEDEC ID来识别芯片:
&ecspi1 { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_ecspi1>; cs-gpios = <&gpio4 9 GPIO_ACTIVE_LOW>; status = "okay"; w25q128: flash@0 { compatible = "jedec,spi-nor"; reg = <0>; spi-max-frequency = <40000000>; spi-tx-bus-width = <1>; spi-rx-bus-width = <1>; #address-cells = <1>; #size-cells = <1>; }; };spi-max-frequency这个参数直接影响稳定性,我调试时习惯先从25MHz起跳,确认完全稳定再往上提。原因后面章节展开讲。
2. SPI协议里最容易翻车的四个细节
2.1 CPOL和CPHA决定了能不能通
SPI通信的四种模式本质是时钟极性和相位排列组合。CPOL决定空闲时时钟电平,CPHA决定数据采样沿。W25Q128支持Mode 0(CPOL=0,CPHA=0)和Mode 3(CPOL=1,CPHA=1)。
实际调试中,Mode 0用得最多,绝大多数SPI控制器默认也是Mode 0。但问题出在设备树或驱动里显式指定模式时:比如你在设备树里写了spi-cpol或spi-cpha属性,而控制器驱动实现又不够严谨,就可能产生模式错配。现象是读ID读出来的值每个字节都带移位,或者像0x00这种全零数据,因为采样点落在了翻转沿上。
另外要注意,spi-mem框架在发起传输前会自动根据flash的mode requirements做适配,声明为"jedec,spi-nor"的芯片默认在Mode 0或Mode 3下都能工作。反倒是某些自研的SPI控制器驱动,没有正确处理mode位。
2.2 硬件片选与软件片选,谁更可靠
SPI片选分为硬件片选(控制器自动拉CS)和软件片选(GPIO控制CS)。调试W25Q128这种低频外设,两条路都走得通,但优先级不同。
硬件片选的优点是响应快,控制器在总线事务开始前自动拉低CS,结束后自动拉高,不需要CPU干预。缺点是对控制器驱动的要求高:如果在一次spi_message里包含多个spi_transfer,而驱动在每段transfer之间错误地拉了CS,Flash就会认为这是两次独立操作,传输直接断掉。尤其是一些继承了旧DMA路径的驱动,容易出现片选毛刺。
软件片选则相对“无脑但可靠”。设备树里用cs-gpios声明GPIO之后,spi_controller会在每次传输前后通过GPIO操作来模拟CS行为,事务边界控制得非常好。缺点是CS动作由CPU执行,高频下会有几微秒的延迟,但对于W25Q128这种读ID、页编程级别的操作,完全感知不到差别。
我个人的建议:读数据量小的控制通路用硬件片选,涉及大数据块读写、又对时序抖动敏感的场景,用软件片选更稳。前提是整个SPI控制器驱动对两种模式都支持,这个在板级调试阶段就要确认清楚。
2.3 时钟频率上限怎么定
W25Q128的手册上写着快速读(0x0B指令)最高133MHz,但那是四线快读模式下的理论极限。内核调试阶段,我们通常先跑标准读(0x03指令),这个指令模式下最高只能到50MHz。
设备树里的spi-max-frequency只是声明芯片能承受的上限,实际跑多快还取决于SPI控制器本身的时钟树分频。比如某平台SPI根时钟80MHz,分频系数2就是40MHz,分频系数3是26.7MHz。很多平台在设备树里写着133MHz,内核实际算出的波特率却是向下取整到最近的可配置值。
调试时我建议按这个顺序来:先设25MHz确认通路,再用50MHz验证标准读,最后如果实际场景需要大吞吐量,再检查PCB走线质量、上拉电阻阻值,尝试上100MHz+的Quad模式。千万别一上来就怼高频,SPI协议本身没握手机制,频率过高时数据直接错,而且错得毫无规律。
2.4 DMA到底要不要用
热搜词里“spi需要两个dma吗”是个高频问题,说明很多人对SPI DMA通道配置有困惑。结论是:W25Q128这类NOR Flash的调试阶段,先别急着开DMA,确认功能正确后再考虑性能优化。
SPI是全双工总线,理论上同时有发送和接收两条数据线,所以如果要用DMA,确实需要TX DMA和RX DMA两个通道,一个负责把数据从内存搬到SPI TX FIFO,一个负责把SPI RX FIFO的数据搬到内存。开DMA之前必须确认两件事:DMA的snoop/缓存策略配置正确,以及spi_transfer里的tx_buf、rx_buf都满足DMA对齐要求。否则数据缓存不一致会导致发送内容少几个字节,或者接收缓冲区里出现陈旧缓存数据。
我在一个Cortex-A7平台上就遇到过:开了DMA后读Flash ID偶尔成功偶尔失败,同一包命令发出去,回包的第一字节是上一次传输的残留内容。排查下来是DMA描述符的缓冲去使能配置不对,CPU cache没有invalidate。这种问题在功能验证阶段会浪费大量时间。所以先把DMA关掉、用PIO模式跑通,再开DMA优化,这是最务实的路径。
3. Linux内核SPI NOR驱动框架:从设备树到MTD设备
3.1 三层结构:控制器驱动、spi-mem层、spi-nor层
要理解调试步骤,先看清内核里这条数据链路长什么样。整个SPI NOR的支持分三层:
- SPI控制器驱动:位于
drivers/spi/目录,负责具体芯片的寄存器操作,比如i.MX平台的spi-imx.c。对外提供spi_controller抽象,内核通过它收发SPI数据。 - spi-mem层:位于
drivers/spi/spi-mem.c。它是SPI控制器和存储设备之间的适配层,把read、write、erase这些操作翻译成SPI协议里的命令序列。它的存在让同一个flash驱动能跑在不同控制器上,不用关心底层时序实现。 - spi-nor层:位于
drivers/mtd/spi-nor/,这是Flash逻辑核心。管理JEDEC ID识别、SFDP解析、四字节地址模式、状态寄存器操作、各种erase粒度。这一层向上注册成MTD设备。
调试时看到的内核日志输出基本来自三层里的任意一层。比如控制器传输超时会报在spi层,Flash状态寄存器检查失败会报在spi-nor层,MTD分区创建失败会报在MTD层。学会从日志关键字定位到具体层级,排查效率能翻倍。
3.2 设备树节点里的隐藏信息
设备树并不仅仅是把芯片挂在总线上那么简单。reg = <0>表示片选序号,对应控制器下的第几个CS引脚。spi-max-frequency是给控制器驱动用的频率上限。spi-tx-bus-width和spi-rx-bus-width是声明四线模式的关键:想启用Quad读,需要把这俩改成<4>,并且芯片必须支持对应的读指令。
这里有个容易踩的坑:只改spi-tx-bus-width而不改spi-rx-bus-width,或者只改设备树不确认内核是否开启CONFIG_MTD_SPI_NOR的四线支持,Quad模式是不会自动生效的。而且有些闪存厂商在芯片出厂时要手动发指令进入Quad Enable状态,比如W25Q128需要往状态寄存器2的QE位写1。这个操作在Linux内核里由spi_nor框架内部处理,但前提是内核能正确识别到芯片支持Quad模式。如果SFDP解析失败,默认只走标准SPI,四线性能优化就成了空谈。
另一个细节是m25p,fast-read属性。老内核里这个属性用来启用Fast Read指令,新内核里判断标准变了,如果没有显式配置,0x03标准读会被优先使用。这使得同样设备树在升级内核后,读性能出现明显差异,排查起来相当隐蔽。
3.3 内核启动日志怎么确认驱动挂载成功
板子启动后,第一步看dmesg | grep spi。正常的日志应该类似:
spi-master spi0: spi0.0 registered spi-nor spi0.0: w25q128 (16384 Kbytes)第一行来自spi控制器驱动,表示总线上的0号设备注册成功。第二行来自spi-nor框架,w25q128是识别出的芯片名,16384 Kbytes是容量。如果只看到第一行没有第二行,说明spi-mem层发起的读ID命令没有得到正确响应,问题可能出在硬件连接、设备树compatible配置或控制器驱动上。
确认注册成功后,再看分区表是否生效。cat /proc/mtd会列出所有MTD分区:
dev: size erasesize name mtd0: 01000000 00001000 "spi.flash"如果分区表和预期不符,检查设备树里flash节点下的partitions子节点,或者检查内核开启了CONFIG_MTD_CMDLINE_PARTS、CONFIG_MTD_OF_PARTS。这俩选项没开,设备树里写了分区也不会生效。
4. 调试手段:从内核日志到示波器的一整套组合拳
4.1 打开内核的调试开关
调试SPI相关问题时,建议开启这几个内核配置,能提供大量细节日志:
CONFIG_SPI_DEBUG=y CONFIG_MTD_DEBUG=y CONFIG_DYNAMIC_DEBUG=y其中CONFIG_DYNAMIC_DEBUG尤其有用,它允许你在运行时动态打开某个文件的打印。比如只想看spi-mem层的传输细节,就可以:
echo -n 'file drivers/spi/spi-mem.c +p' > /sys/kernel/debug/dynamic_debug/control这个技巧能省掉反复编译内核的时间,我调驱动时几乎必用。注意要提前挂载debugfs:mount -t debugfs none /sys/kernel/debug。
另外,查看SPI控制器注册了哪些设备和参数,可以看/sys/bus/spi/devices/目录。每个设备目录下的of_node链接指向设备树节点,而statistics文件里记录了传输次数、错误次数、超时次数,对判断总线健康状况很有参考价值。
4.2 用MTD工具验证读写
驱动挂载成功后,就要验证实际读写功能。最常用的是一套mtd-utils工具包里的mtd_debug和flashcp。
先擦除4KB扇区:
mtd_debug erase /dev/mtd0 0 0x1000再写入一段数据:
mtd_debug write /dev/mtd0 0 0x1000 data.bin最后读回来比对:
mtd_debug read /dev/mtd0 0 0x1000 dump.bin cmp data.bin dump.bin注意mtd_debug的offset和length必须对齐erase block size,否则返回EINVAL。很多新手在这里吃亏:写入时用了任意字节偏移,结果擦除失败。4KB扇区大小可以通过cat /proc/mtd里的erasesize列看到,W25Q128一般是0x1000(4KB)。
如果想模拟更真实的文件系统操作,可以用flashcp:
flashcp -v data.bin /dev/mtd0flashcp会自动先擦后写,还会校验,比手动三条命令省事。但注意它不保留分区表逻辑,只做裸分区操作。
4.3 用逻辑分析仪看SPI波形
当软件层面看起来一切正常,但读写结果依然不对时,就该上逻辑分析仪了。SPI总线是四根线全都能测到,除了MISO、MOSI、CLK、CS之外,建议把WP#和HOLD#也一起挂上,因为这两个信号的电平决定了芯片的写入权限。
判断时序是否正常的核心是看CS在每帧传输前后是否干净拉低拉高,以及CLK的上升沿与数据线的采样关系。可以用一个非常简单的读JEDEC ID命令来验证时序:主机发0x9F,芯片返回3字节ID。逻辑分析仪上应该能看到:CS拉低,CLK输出8个时钟拍子,MOSI上出现0x9F,然后继续24个时钟拍子,MISO上依次出现0xEF 0x40 0x18。
如果MISO上没数据,可能的原因有:DI/DO接反、WP#/HOLD#电平异常、芯片根本没供电。如果MISO有数据但值不对,优先怀疑CPOL/CPHA配置。
4.4 常见错误码的含义与定位思路
调试中常见的错误码和排查方向我整理成了一张表,方便快速对照:
| 错误/现象 | 可能原因 | 排查思路 |
|---|---|---|
spi-nor spi0.0: unreachable 0xef4018 | 芯片ID识别失败 | 检查设备树compatible、SPI模式、硬件接线 |
spi_master spi0: I/O Error | 控制器传输失败 | 检查DMA配置、时钟分频、CS GPIO映射 |
| 擦除操作超时 | 状态寄存器WIP位一直不置0 | 查WP#引脚电平、分区是否被FUSE保护 |
| 写入后读回全0xFF | 擦写命令未真正执行 | 查状态寄存器、HOLD/WP引脚、bus-width配置 |
| 读回数据错位 | SPI模式不匹配 | 用逻辑分析仪确认采样沿 |
4.5 用SPI环回测试验证控制器通路
如果硬件、设备树、驱动都查过,还是不确定问题出在flash芯片还是控制器总线,可以用一个物理层的技巧验证SPI控制器本身:把MOSI和MISO短接,然后从用户态发起一个简单的spidev读写操作,全双工模式下从MISO读到的数据应该和MOSI发送的完全一致。
要使用这种方法,需要在设备树里额外加一个spidev节点:
&ecspi1 { spidev@1 { compatible = "rohm,dh2228fv"; /* 任意spidev兼容串 */ reg = <1>; spi-max-frequency = <1000000>; }; };然后在内核里开CONFIG_SPI_SPIDEV,编译完后在用户态用spidev_test工具测试:
spidev_test -D /dev/spidev0.1 -v如果自测数据一致,说明控制器底层收发没问题,问题锁定在flash芯片侧。
5. 实测中踩过的坑与完整排查链路
5.1 读ID正常但擦除失败:从状态寄存器入手
一次在某平台调试W25Q128,内核日志显示芯片识别成功,cat /proc/mtd也能看到分区,写数据也没报错,唯独mtd_debug erase命令执行后,读回来的数据还是全0xFF,而且命令超时。
排查链路如下:
第一步,先排除软件问题。换个环境用flash_erase再擦一次,现象依旧。用mtd_debug info /dev/mtd0看擦除块大小,确认是0x1000,参数没错。
第二步,怀疑芯片状态寄存器有保护。通过mtd_debug没法直接读状态寄存器,我写了一个小的内核模块或直接用devmem工具读取SPI控制器寄存器,手动发起0x05(读状态寄存器1)命令。软件模拟出来的结果是0x3C,二进制为00111100,其中BP0-BP3位全是1,说明芯片被设置为全部扇区写保护。
第三步,查硬件原理。翻看板子原理图,发现WP#引脚直接连到地,没有做上拉。这个接法等于芯片永远处在写保护使能状态。解决方法:要么硬件上把WP#改接到VCC,要么在内核驱动每次操作前获取状态寄存器、清除BP位。对于产品化方案,肯定优先改硬件。
这个案例说明:Flash芯片识别正常不代表擦写权限正常,状态寄存器的检查必须纳入基础调试流程。后期我在新板卡调试时,会先跑一遍mtd_debug erase再读回验证,作为快速验证芯片写能力的环境冒烟测试项。
5.2 写数据后读回来全是0xFF:从时序找根因
另一个案例是芯片能读ID、能擦除、写操作也不报错,但写入任意数据后,读回的内容永远是0xFF。这个问题比全盘写保护更隐蔽,因为芯片没有报告错误,只是数据根本没写进去。
排查过程:
用逻辑分析仪抓写命令。观察到主机发完页编程(0x02)命令和256字节数据后,CS按预期拉高,但紧接着主机没有发“读状态寄存器”的轮询命令,而是直接认为编程完成,然后开始发擦除命令。进一步分析发现,flash的页编程需要时间,常见值是0.4ms到3ms,控制器必须在发完数据后不断轮询状态寄存器的WIP位,等WIP变0后才算编程完成。
问题定位到spi-nor驱动的write流程,在一个老旧BSP上,FIFO的预取逻辑和DMA的中断回调配合不好,导致轮询状态寄存器的请求被错误地合并到上一次传输的尾部,芯片收到的实际命令序列是“页编程 + 读状态”连着执行,读状态这一拍返回的其实是编程结束后的真值,但驱动程序误判状态。
解决思路是做数据保持时间补偿、或者关闭FIFO预取,启用更保守的传输方式。最终调稳后,读写校验跑了几百遍没再出错。这类问题在PIO模式下几乎不会出现,又在用DMA的平台上,一定要保留怀疑空间。
5.3 系统启动挂载根文件系统失败
最后一个坑是开在把根文件系统直接放在W25Q128上,选用JFFS2。系统能识别MTD分区,但启动时内核报VFS: Unable to mount root fs。
排查链路:
先确认分区名和根设备参数。root=/dev/mtdblock3这种写法,在内核新版本里经常失效,因为mtdblock和mtdblock_ro在驱动层面不再等同于mtd设备。实际上启动参数应该用root=mtd:分区名配合CONFIG_MTD_BLOCK、CONFIG_MTD_BLOCK_RO开启。还有一种方式是直接root=31:03,用主设备号:次设备号指定。
确认内核命令行无误后,再看文件系统镜像本身。JFFS2镜像的制作要和MTD分区的eraseblock大小匹配,如果制作工具用了错误的页大小,刷进去后挂载时系统读取超级块失败,就会报类似jffs2: Node with stale CRC的错误。重新用mkfs.jffs2 --eraseblock=4KiB -p制作镜像后问题解决。
这类问题的核心教训是:SPI NOR的根文件系统挂载,链路涉及bootloader传参、内核MTD子系统、文件系统的对齐参数,任何一环的错误表现都是挂载失败,但根因可能相去甚远。建议用排除法先把参数逐一对齐。
6. 一点收尾心得
把W25Q128在Linux kernel下调通这件事,前期准备占到六成功夫。硬件上确认WP#、HOLD#的上拉电平,确认片选极性和SPI模式;软件上确认内核配置选项、设备树compatible和频率参数;再配合一个逻辑分析仪做波形兜底,多半的问题都能在两小时内定位到根因。反而是那些最容易忽略的小细节——状态寄存器保护位、DMA缓存一致性问题、Quad模式的QE位,才是真正容易卡住人的地方。
调试工具和技巧都分享完了,最后分享一个我自己的操作习惯:每到一个新平台,我做的第一件事不是调驱动逻辑,而是先用spidev环回测试把控制器底层的收发完整性验证一遍,再挂上Flash芯片跑一组标准的“读ID→擦除→写入→读回”测试。这组流程走完了,整个SPI链路的基本盘就稳了,剩下的都是细节优化。这个习惯帮我省下了大量排查时间,希望对你也一样有用。