news 2026/11/6 3:02:32

u-boot flash子系统深度解析:从SPI-NOR驱动到MTD分区实战避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
u-boot flash子系统深度解析:从SPI-NOR驱动到MTD分区实战避坑

1. 从一次量产翻车说起:为什么我要把u-boot的flash子系统彻底梳理一遍

去年冬天,产线上有一批板子回板后死活起不来,串口只打印到U-Boot SPL就卡死,偶尔能进到U-Boot命令行,但sf probe一敲就报SF: Failed to send command。当时第一反应是硬件焊接问题,换了三块板子现象一致,才意识到事情没那么简单。后来用示波器抓SPI波形,发现CS拉低之后时钟线上只有零星几个脉冲,明显是控制器初始化阶段就出了问题。顺着代码一路追下去,最后定位到drivers/mtd/spi/spi-nor-core.c里对某颗国产NOR的SFDP表解析异常,导致read_op被错误地设成了SPI_NOR_QUAD_READ,而板子上那颗flash的QE位默认是关闭的。

这件事让我意识到,u-boot的flash子系统远比想象中复杂。它不像Linux内核那样有完善的设备模型和调试手段,很多问题只能靠读代码和抓波形。更麻烦的是,u-boot阶段一旦flash读写异常,整个系统就彻底变砖,连恢复的机会都没有。所以后来我花了两周时间,把u-boot里跟flash相关的代码从board_init_r一路追到mtd层、spi-nor层、nand层,把整个调用链和数据结构画了一遍。这篇文章就是那次梳理的产物,适合正在做u-boot移植、flash驱动适配、或者被量产烧录问题折磨的嵌入式工程师参考。不管你是刚接触u-boot的新手,还是已经调过几颗flash的老手,应该都能从里面找到一些之前没注意到的细节。

2. u-boot flash子系统的整体架构与设计思路

2.1 为什么u-boot要单独搞一套flash框架

很多人第一次看u-boot代码时会疑惑:Linux内核已经有成熟的MTD子系统了,为什么u-boot不直接搬过来用?我一开始也有这个疑问,后来在移植过程中才慢慢理解。u-boot的运行环境跟内核完全不同,它跑在SRAM或者DDR的早期阶段,堆栈空间极其有限,很多板子的SPL阶段只有几十KB的可用RAM。Linux那套MTD框架光struct mtd_info加上各种注册机制就够呛,更别说mtdblock、ubi这些上层模块了。

所以u-boot的flash子系统走的是另一条路:极简、直接、面向启动。它的核心目标只有三个——能把镜像从flash读到内存、能把环境变量存到flash、能在需要时擦除或写入flash。至于文件系统、坏块管理、磨损均衡这些,u-boot要么不做,要么只做最基础的版本。比如jffs2在u-boot里只有读支持,写支持基本没人用;ubi也是精简版,只够挂载rootfs。

这种设计取舍带来的直接后果就是:u-boot的flash驱动跟硬件耦合更紧,可移植性不如内核。但反过来,它的代码路径短、启动快、占用资源少,非常适合作为bootloader。理解这一点,后面看代码时就不会纠结“为什么这里不抽象一层”了。

2.2 三层结构:从命令到硬件

u-boot的flash子系统大致可以分成三层,我用一个表格来对比它们的职责和典型文件:

层级职责典型文件关键数据结构
命令层提供用户交互接口cmd/sf.c、cmd/nand.c、cmd/mtd.ccmd_tbl_t
中间层统一设备抽象drivers/mtd/mtdcore.c、drivers/mtd/mtdpart.cstruct mtd_info
驱动层操作具体硬件drivers/mtd/spi/spi-nor-core.c、drivers/mtd/nand/raw/nand_base.cstruct spi_nor、struct nand_chip

命令层最直观,你在串口敲的sf probe、sf read、nand erase都在这层。中间层是u-boot后来引入的,目的是让不同flash类型能共用一套上层接口,比如mtd read可以同时操作SPI-NOR和NAND。驱动层就是跟硬件寄存器打交道的地方,也是出问题最多的地方。

这里有个容易踩的坑:u-boot的MTD中间层并不完整。它没有内核那样的mtd_device_register自动分区机制,很多板子还是靠CONFIG_MTD_PARTITIONS和mtdparts环境变量手动划分。如果你在dts里写了分区但发现mtd list看不到,大概率是没开对应的配置项。

2.3 配置宏的迷宫:Kconfig与头文件的双重控制

u-boot的配置系统是出了名的绕。同一个功能,可能同时受CONFIG_XXX、CONFIG_SPL_XXX、CONFIG_TPL_XXX三个宏控制,而且有些宏在include/configs/xxx.h里定义,有些在defconfig里,还有些在Kconfig里自动生成。我整理了一个常见的flash相关配置对照表:

配置项作用常见取值
CONFIG_MTD启用MTD中间层y
CONFIG_DM_SPI_FLASH启用SPI flash驱动模型y
CONFIG_SPI_FLASH启用SPI flash支持y
CONFIG_SPI_FLASH_SFDP_SUPPORT启用SFDP解析y
CONFIG_SPI_FLASH_BAR启用3字节/4字节地址切换y
CONFIG_MTD_SPI_NOR启用SPI-NOR驱动y
CONFIG_CMD_SF启用sf命令y
CONFIG_CMD_MTD启用mtd命令y

注意:CONFIG_DM_SPI_FLASH和CONFIG_SPI_FLASH不是一回事。前者是驱动模型版本,后者是旧版非DM版本。新板子建议一律用DM版本,否则后面加新flash型号会很痛苦。

我见过最坑的情况是:defconfig里开了CONFIG_DM_SPI_FLASH,但include/configs/xxx.h里又定义了CONFIG_SPI_FLASH,结果编译时两个驱动都编进去了,运行时到底用哪个取决于链接顺序。这种问题不会报错,但行为完全不可预测。所以我的建议是:新项目一律用DM,旧头文件里的flash配置全部删掉。

3. SPI-NOR驱动核心细节与实操要点

3.1 从spi_nor_scan看flash识别流程

spi_nor_scan是SPI-NOR驱动的入口函数,整个识别流程可以概括为:读ID、查表、解析SFDP、配置操作集。我把它拆成几个关键步骤:

第一步是spi_nor_read_id,通过0x9F命令读回3字节或4字节的JEDEC ID。这里有个细节:有些flash在复位后需要发0xAB或者0x66+0x99才能退出深度掉电模式,否则读ID全是0xFF。我在国产GD25系列上就遇到过,后来在spi_nor_scan前面加了一段spi_nor_soft_reset才解决。

第二步是查spi_nor_ids表。这个表在spi-nor-ids.c里,每项包含id、name、page_size、sector_size、flags等字段。如果表里没有你的flash型号,可以临时加一项,但更推荐的做法是让SFDP自动识别。SFDP是JEDEC标准,只要flash支持,就能读出容量、擦除粒度、地址字节数等信息。

第三步是spi_nor_parse_sfdp。这里最容易出问题的是BFPT表的版本兼容性。BFPT是SFDP里的基本flash参数表,不同厂商对某些字段的解释不一致。比如quad_enable的bit位置,有的厂商放在BFPT的DWORD 15,有的放在DWORD 16。u-boot的代码里有一堆if (bfpt.dwords[0] & ...)的判断,就是在处理这些差异。

第四步是spi_nor_setup,根据前面读到的参数配置read_op、write_op、erase_op。这里有个关键点:如果flash支持4字节地址,但你的控制器只支持3字节,就必须启用CONFIG_SPI_FLASH_BAR,通过0x17命令切换bank。否则访问超过16MB的区域会回绕到0地址,读出来的数据全是错的。

3.2 读写擦除的底层命令与时序

SPI-NOR的基本操作其实就几条命令,但每条都有坑:

  • 读数据(0x03):最基础的读命令,支持任意地址,但速度慢。如果flash支持0x0B快速读,建议在read_op里优先用。
  • 页编程(0x02):每次最多写256字节,且必须保证目标区域已经擦除。如果没擦除就写,结果是与旧数据按位与,这是新手最容易犯的错误。
  • 扇区擦除(0x20):擦除4KB,最常用的擦除粒度。有些flash支持0xD8块擦除(64KB),批量擦除时能快不少。
  • 写使能(0x06):每次写或擦之前都必须发,否则命令会被忽略。u-boot里通过spi_nor_write_enable封装,但要注意有些flash在写使能后如果CS拉高超过一定时间,WREN位会自动清除。

我实测过一颗Winbond W25Q128,用0x02逐页写1MB数据大概要8秒,换成0xD8块擦除加0x02页写能降到3秒左右。如果对烧录速度有要求,还可以考虑0x32四线页编程,但需要先配置QE位。

实操心得:在spi_nor_write里,u-boot默认会先读回数据做校验。这个校验很耗时,量产时可以关掉CONFIG_SPI_FLASH_WRITE_CHUNK或者把校验改成抽样。但开发阶段千万别关,否则写坏了都不知道。

3.3 4字节地址模式与bank切换

当flash容量超过16MB时,3字节地址就不够用了。SPI-NOR有两种解决方案:一种是进入4字节地址模式(通过0xB7命令),之后所有命令都用4字节地址;另一种是保持3字节地址,通过0xC5读bank寄存器、0x17写bank寄存器来切换。

u-boot里这两种模式都有支持,但默认走的是bank切换,因为兼容性更好。具体代码在spi_nor_set_4byte和spi_nor_set_4byte_addr_mode。我遇到过一颗Micron的flash,进入4字节模式后0x03命令不认了,必须用0x13(4字节读)。后来查datasheet才发现,这颗flash的4字节模式只对特定命令生效,不是全局切换。

所以我的建议是:如果flash容量在16MB以内,别折腾4字节模式;如果超过16MB,优先用bank切换,除非你的控制器明确支持4字节地址。另外,bank切换是有状态的,切换后所有后续命令都受影响,所以在spi_nor_read和spi_nor_write里要成对调用spi_nor_set_4byte和spi_nor_set_3byte。

4. MTD中间层与分区管理的实操细节

4.1 mtd_info结构体里哪些字段最关键

struct mtd_info是MTD层的核心,字段很多,但真正影响使用的就那么几个:

  • type:MTD_NORFLASH、MTD_NANDFLASH等,决定上层用什么方式访问。
  • size:总容量,单位字节。
  • erasesize:擦除块大小,SPI-NOR通常是4KB或64KB。
  • writesize:写粒度,SPI-NOR是1字节,NAND是页大小。
  • _read、_write、_erase:函数指针,最终调到驱动层。
  • priv:指向具体驱动的私有数据,比如struct spi_nor *。

我调试时最常看的是erasesize和writesize。有一次mtd erase报Offset not aligned,查了半天发现是erasesize被错误地设成了1,原因是SFDP解析时把erase_size的单位搞错了(有的flash返回的是KB,有的是字节)。

4.2 分区表的三种配置方式

u-boot里配置MTD分区有三种方式,我按推荐程度排序:

第一种:设备树分区(推荐)

在dts里写:

&spi_flash { partitions { compatible = "fixed-partitions"; #address-cells = <1>; #size-cells = <1>; partition@0 { label = "u-boot"; reg = <0x0 0x100000>; }; partition@100000 { label = "env"; reg = <0x100000 0x10000>; }; }; };

这种方式最清晰,但需要CONFIG_OF_CONTROL和CONFIG_MTD_PARTITIONS都打开。

第二种:mtdparts环境变量

在defconfig里设CONFIG_MTDIDS_DEFAULT和CONFIG_MTDPARTS_DEFAULT,或者在启动时手动setenv mtdparts。格式是mtdparts=spi0.0:1M(u-boot),64K(env),-(rootfs)。这种方式灵活,但依赖环境变量,如果env被擦除就丢了。

第三种:硬编码在板级文件里

在board/xxx/xxx.c里调用mtd_device_register手动注册。这种方式最不推荐,改起来麻烦,而且容易跟dts冲突。

注意:如果同时用了dts分区和mtdparts,u-boot会优先用dts。我见过有人改了mtdparts但没生效,就是因为dts里已经定义了分区。

4.3 环境变量在flash里的存储与持久化

环境变量的持久化是flash子系统最实际的应用之一。u-boot默认把env存在CONFIG_ENV_OFFSET指定的偏移处,大小由CONFIG_ENV_SIZE决定。这里有几个关键点:

  • env必须独占一个擦除块。如果CONFIG_ENV_SIZE大于erasesize,u-boot会报错。SPI-NOR的4KB扇区通常够用,但NAND的页大小可能只有2KB,就需要跨页存储。
  • env有冗余备份。CONFIG_ENV_OFFSET_REDUND可以指定备份区,u-boot写入时会交替更新两个区,防止掉电导致env损坏。
  • env的CRC校验。每次读取env都会校验CRC,如果失败会尝试从备份区恢复。如果两个区都坏了,就会用CONFIG_ENV_DEFAULT里的默认值。

我踩过的一个坑是:CONFIG_ENV_OFFSET设在了u-boot镜像的范围内,结果每次saveenv都会把u-boot自己擦掉一块。后来用mtd read把整个flash读出来对比,才发现偏移算错了。所以改env偏移后一定要用mtd dump确认不跟其他分区重叠。

5. 常见问题排查与实战避坑指南

5.1 flash识别失败的五种典型原因

我把这些年遇到的识别失败案例整理成一张速查表:

现象可能原因排查方法
读ID全0xFFflash未上电、CS接错、深度掉电量电压、查原理图、发0xAB唤醒
读ID全0x00MISO被拉低、控制器配置错误查引脚复用、看SPI模式
ID正确但容量不对SFDP解析错误、地址字节数不对打印SFDP表、手动指定size
probe成功但读写失败操作集配置错误、QE位未开看read_op/write_op、读SR寄存器
偶发识别失败时序问题、电源纹波降频、加电容、查上电时序

其中最常见的是深度掉电。很多flash出厂时处于深度掉电模式,必须发0xAB才能唤醒。u-boot的spi_nor_scan里其实有spi_nor_soft_reset,但有些老版本没有,需要自己加。

5.2 写保护与状态寄存器操作

SPI-NOR的状态寄存器(SR)里有几个关键位:WIP(写进行中)、WEL(写使能)、BP0-BP3(块保护)、QE(四线使能)。写操作前必须检查WIP是否为0,否则会丢数据。

我遇到过一个诡异问题:sf write偶尔失败,但重试就好。后来发现是spi_nor_wait_till_ready的超时时间设得太短,flash还没擦完就返回了。默认超时是CONFIG_SF_DEFAULT_SPEED相关的,我把它从100ms改到500ms就稳定了。

实操心得:如果flash的BP位被设了,写操作会静默失败(不报错但数据没写进去)。排查时先读SR,确认BP全是0。有些flash的BP位是易失的,上电默认保护,需要在spi_nor_scan里主动清除。

5.3 掉电保护与持久化可靠性

“持久化”这个词在flash场景下有两层含义:一是数据写到flash后掉电不丢,二是掉电时正在写的操作不会破坏已有数据。第二点更关键。

SPI-NOR的擦除和写入都需要时间,擦除一个4KB扇区典型要50-100ms。如果在这期间掉电,这个扇区可能处于半擦除状态,数据全乱。所以关键数据一定要用冗余备份,比如env的REDUND机制,或者自己实现一个双区交替写的方案。

我做过一个测试:在sf erase执行到一半时断电,重新上电后读该扇区,发现前2KB是0xFF,后2KB是旧数据。这种半擦除状态用CRC是能检测出来的,但如果没有备份就彻底丢了。所以对于u-boot env这种关键数据,CONFIG_ENV_OFFSET_REDUND一定要开。

5.4 性能优化:从8秒到3秒的烧录提速

前面提到过,默认的逐页写很慢。我总结了几条提速经验:

  • 用块擦除代替扇区擦除。如果一次要擦64KB以上,用0xD8比16次0x20快得多。
  • 开启四线读写。需要先写SR的QE位,然后把read_op改成SPI_NOR_QUAD_READ(0x6B),write_op改成SPI_NOR_QUAD_PP(0x32)。
  • 关闭写校验。量产时用CONFIG_SPI_FLASH_WRITE_CHUNK跳过逐字节校验。
  • 提高SPI时钟。在控制器允许的范围内尽量拉高,但要注意信号完整性,线太长或干扰大时反而会出错。

我实测一颗128MB的flash,全片擦除加写入,优化前要90秒,优化后降到35秒左右。如果只是更新env这种小数据,优化意义不大,因为擦除时间占主导。

6. 从u-boot到内核:flash子系统的衔接与差异

6.1 设备树里的flash节点怎么传

u-boot和内核共用同一份设备树,但解析方式不同。u-boot的spi_nor驱动只认jedec,spi-nor这个compatible,而内核还支持m25p,fast-read等旧式写法。如果dts里写的是旧式compatible,u-boot可能识别不了。

另外,u-boot对spi-max-frequency的处理比内核简单,它只用来配置控制器分频,不做时序补偿。所以如果内核里能跑50MHz但u-boot跑不了,大概率是u-boot的控制器驱动没配好。

6.2 分区表的传递

u-boot识别到的分区可以通过mtdparts环境变量传给内核,内核的mtdparts解析器会读这个变量。但更推荐的做法是直接在dts里写分区,两边都认。如果两边分区不一致,会出现内核挂载rootfs失败但u-boot能读的情况,排查时先对比mtd list和/proc/mtd。

6.3 常见差异与兼容性问题

  • 4字节地址模式:u-boot默认用bank切换,内核默认用4字节模式。如果flash容量超过16MB,两边行为可能不一致。
  • QE位:u-boot在spi_nor_scan里会尝试设置QE,内核在spi_nor_init里也会设。如果u-boot设了但内核又清掉,四线读写就会失效。
  • SFDP解析:内核的SFDP解析比u-boot完善,支持更多厂商的quirks。如果u-boot识别有问题但内核正常,可以对比两边的SFDP解析代码。

我在一个项目里遇到过u-boot能读flash但内核读不了的情况,最后发现是u-boot把flash设成了4字节模式,但内核的控制器驱动不支持4字节命令。解决办法是在u-boot的board_init_r最后把flash切回3字节模式。

7. 我个人在实际操作中的几点体会

梳理完这一整套,最大的感受是:u-boot的flash子系统虽然代码量不大,但细节极其密集,而且很多行为依赖具体flash型号和控制器。官方文档基本没有,全靠读代码和实测。我现在的习惯是每适配一颗新flash,都先用sf probe、sf read、sf erase、sf write跑一遍基本流程,再用mtd list确认分区,最后用saveenv验证持久化。这套流程走下来,基本能覆盖90%的问题。

另外一个小技巧:如果手头没有逻辑分析仪,可以用u-boot的sf命令配合md命令做简易调试。比如sf read 0x80000000 0 0x1000把flash前4KB读到内存,再用md 0x80000000看数据。如果读出来全是0xFF,基本就是识别或配置问题;如果有数据但不对,可能是地址或bank问题。这个方法虽然原始,但在没有专业工具时很管用。

最后再分享一个我踩过的坑:不要随便改CONFIG_ENV_OFFSET而不改CONFIG_ENV_SIZE。有一次我把offset往后挪了64KB,但size没变,结果env区跟后面的分区重叠,saveenv时把分区表擦了一半。后来每次改env配置,我都会用mtd dump把整个flash读出来,用十六进制编辑器确认各分区边界。这个习惯救了我好几次。

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

光伏逆变器参数不会看?一文读懂MPPT电压、组串配置与选型避坑

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

作者头像 李华
网站建设 2026/11/6 2:58:30

电路直觉不是天赋:一套可训练的电路理解方法论

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

作者头像 李华
网站建设 2026/11/6 2:57:51

基于Hadoop+Spark的智能图书分类系统设计与实践

1. 项目背景与核心价值图书分类一直是图书馆、电商平台和内容管理系统的痛点。传统人工分类方式效率低下且容易出错&#xff0c;尤其当面对海量图书数据时。这个项目通过整合Hadoop、Spark和Django三大技术栈&#xff0c;构建了一个完整的机器学习流水线&#xff0c;实现了图书…

作者头像 李华
网站建设 2026/11/6 2:57:32

ECC纠错码与智能代理:数据完整性保障的工程实践

1. 25.9k Star背后的真实问题&#xff1a;静默数据损坏与纠错困境1.1 一次印象深刻的线上事故&#xff0c;让我开始正视ECC原理先讲一个我自己的经历。有一年做分布式存储系统的性能调优&#xff0c;业务方反馈说文件偶尔打开会损坏&#xff0c;但概率极低&#xff0c;大概几百…

作者头像 李华
网站建设 2026/11/6 2:56:44

零基础网络安全入门:HackTheGame游戏化靶场通关与复盘

1. 新手想碰网络安全&#xff0c;为什么游戏化靶场是最省力的入口很多人对网络安全的兴趣&#xff0c;都是从一部电影、一段新闻或者一次"我的账号是不是被盗了"的焦虑开始的。我也不例外。真正开始动手之后才发现&#xff0c;最难受的不是知识难&#xff0c;而是没有…

作者头像 李华
网站建设 2026/11/6 2:55:31

EXE解压工具全解析:从容器识别到批量处理与完整性验证

简介&#xff1a;这是一款面向开发者、逆向工程学习者与软件分析人员的EXE可执行文件解压工具&#xff0c;主要用于提取非安装类EXE程序内部嵌入的资源与数据&#xff0c;如位图、文本、音频及其他二进制文件&#xff0c;便于查看、调试或复用程序原始素材。资源包共167个文件&…

作者头像 李华