u-boot flash子系统深度解析:MTD分层与SPI-NOR持久化机制

发布时间:2026/11/6 1:02:20
u-boot flash子系统深度解析:MTD分层与SPI-NOR持久化机制 1. 从一次量产翻车说起为什么我要把u-boot的flash子系统翻个底朝天前两年做一款工业网关硬件那边选了一颗16MB的SPI-NOR软件跑的是u-boot 2018.03加Linux 4.19。样机阶段一切正常环境变量能存、内核能引导、rootfs挂载也没问题。结果小批量试产200台有7台在老化测试里反复重启后配置丢失环境变量回滚成了出厂默认值。产线那边第一反应是是不是flash坏了换了三颗新的还是复现。后来抓了SPI波形才发现问题出在u-boot保存环境变量时对sector的擦除策略上——我们用的saveenv默认走的是整块擦除再回写而那颗NOR的sector是4KB环境变量区跨了两个sector掉电窗口正好卡在第二个sector擦除完成、数据还没写回的那一瞬间。这件事之后我把u-boot的flash子系统从drivers/mtd一路啃到cmd/flash.c把MTD分层、SPI-NOR驱动模型、环境变量持久化路径全部梳理了一遍。这篇东西就是那次梳理的沉淀写给正在做嵌入式存储、需要搞明白u-boot里flash到底怎么被管起来的人。不管你是刚接手BSP的新人还是被环境变量存不住jffs2挂载报错折腾过的老手下面这些内容应该都能对上你的场景。核心关键词就几个u-boot、flash子系统、持久化、MTD、SPI-NOR我会围绕它们把整条链路讲透。2. u-boot flash子系统的整体设计与分层思路2.1 为什么u-boot要单独搞一套MTD抽象很多人第一次看u-boot源码会疑惑Linux内核已经有成熟的MTD子系统了u-boot为什么还要自己实现一套答案其实很朴素——u-boot是bootloader它运行的时候内核还没起来不可能复用内核的代码。但u-boot又必须能读写flash因为要保存环境变量、要升级固件、要读取内核镜像。所以u-boot自己实现了一个精简版的MTD层接口风格刻意向内核靠拢方便驱动代码在两边迁移。u-boot的MTD抽象核心就三个结构体mtd_info、mtd_dev、erase_info。mtd_info描述一个MTD设备的能力包括类型NAND/NOR/SPI-NOR、擦除块大小、写粒度、总容量、读写擦回调函数。mtd_dev是u-boot自己加的一层把mtd_info和具体的分区信息绑在一起因为u-boot里经常需要按分区名操作比如mtd erase kernel。erase_info则是擦除操作的描述符记录起始地址、长度和回调。这套设计的好处是上层命令sf、nand、mtd和中间层环境变量、固件加载都不需要关心底层是SPI还是并行接口只要拿到mtd_info就能干活。坏处是u-boot的MTD比内核简化了很多比如没有坏块管理的完整实现NAND的BBT处理相对粗糙这也是为什么很多NAND项目在u-boot阶段容易出问题的根源。2.2 SPI-NOR在u-boot里的驱动模型SPI-NOR是u-boot里最常见的一类flash尤其在中小容量场景。它的驱动模型分三层最底层是SoC的SPI控制器驱动比如drivers/spi/fsl_qspi.c、drivers/spi/spi-mem.c中间层是SPI-NOR通用框架drivers/mtd/spi/spi-nor-core.c最上层是MTD接口封装drivers/mtd/spi/spi-nor.c或spi-flash.c视版本而定。这里有个版本差异要特别注意。u-boot 2020.01之前的SPI-NOR代码主要在drivers/mtd/spi/spi_flash.c里之后逐步迁移到spi-nor-core.c并且引入了spi-mem框架来统一SPI内存设备的访问。如果你在移植老代码看到spi_flash_probe和spi_nor_scan两个函数名不要慌它们本质是同一件事的不同阶段产物。SPI-NOR驱动初始化的关键流程是这样的SPI控制器注册后spi_flash_probe被调用它先读JEDEC ID命令0x9F拿到厂商ID和设备ID然后在spi_nor_ids表里匹配。匹配成功后调用spi_nor_scan这个函数会读SFDP如果支持或者用默认参数填充mtd_info包括sector大小、page大小、总容量、擦除命令0x20是4KB擦除0xD8是64KB块擦除。最后把mtd_info注册到u-boot的MTD设备链表里供上层使用。注意JEDEC ID匹配失败是SPI-NOR最常见的设备识别不了问题。如果ID读出来是0xFFFFFF或0x000000先查SPI模式CPOL/CPHA和片选时序再查供电和复位引脚最后才怀疑芯片本身。2.3 环境变量持久化的路径选择u-boot环境变量的持久化是整个flash子系统里最贴近持久化这个词的部分。环境变量存储位置由CONFIG_ENV_IS_IN_*系列宏决定常见的有CONFIG_ENV_IS_IN_SPI_FLASH、CONFIG_ENV_IS_IN_NAND、CONFIG_ENV_IS_IN_MMC、CONFIG_ENV_IS_IN_FAT等。选哪个取决于你的启动介质和可靠性要求。以SPI-NOR为例环境变量区通常放在flash的最后一个sector或者一个独立的4KB/64KB区域。CONFIG_ENV_OFFSET指定偏移CONFIG_ENV_SIZE指定大小CONFIG_ENV_SECT_SIZE指定擦除单元。这三个参数必须和实际flash的sector布局对齐否则saveenv会失败或者破坏相邻数据。我见过最典型的错误配置是CONFIG_ENV_SIZE设成0x1000064KB但CONFIG_ENV_SECT_SIZE设成0x10004KB而flash的64KB块擦除命令又没启用。结果saveenv时u-boot按4KB擦除了16次每次擦除后写回掉电窗口被放大了16倍。正确做法是让CONFIG_ENV_SECT_SIZE等于实际使用的擦除命令对应的块大小要么统一用4KB sector要么统一用64KB block。3. 核心细节拆解MTD分区、SPI-NOR参数与持久化机制3.1 MTD分区表在u-boot里的两种来源u-boot的MTD分区信息有两个来源一是设备树arch/arm/dts/下的partition节点二是板级文件里的mtdparts环境变量或CONFIG_MTDPARTS_DEFAULT。设备树方式更现代推荐优先使用mtdparts方式更灵活适合需要现场调整分区的场景。设备树里的分区节点长这样spi_flash { status okay; partitions { compatible fixed-partitions; #address-cells 1; #size-cells 1; partition0 { label u-boot; reg 0x0 0x80000; }; partition80000 { label env; reg 0x80000 0x10000; }; partition90000 { label kernel; reg 0x90000 0x300000; }; }; };u-boot在board_init_r阶段会调用mtd_probe_devices或board_mtdparts_default来解析这些信息生成mtd_dev链表。之后mtd list命令就能看到所有分区。mtdparts方式则是在环境变量里写mtdpartsspi0.0:512k(u-boot),64k(env),3m(kernel),-(rootfs)两种方式不要混用否则会出现分区重叠或覆盖。我踩过的坑是设备树里定义了env分区但mtdparts里又写了一遍结果u-boot用了mtdparts的定义而Linux用了设备树的定义两边env偏移不一致导致内核读到的环境变量是乱的。3.2 SPI-NOR的关键参数与计算过程SPI-NOR有几个参数直接决定持久化行为必须搞清楚参数含义典型值影响sector size最小擦除单元4KB决定擦除粒度page size最大写入单元256B决定写缓冲大小block size大块擦除单元64KB决定批量擦除效率total size总容量8/16/32MB决定分区规划address width地址字节数3B或4B超过16MB需4B模式以16MB SPI-NOR为例地址空间是0x000000到0xFFFFFF3字节地址刚好够用。但如果换成32MB就必须启用4字节地址模式命令0xB7进入0xE9退出否则高16MB无法访问。u-boot里通过CONFIG_SPI_FLASH_BAR或spi_nor_set_4byte来处理具体取决于版本。环境变量区的大小计算也有讲究。假设CONFIG_ENV_SIZE0x20008KBCONFIG_ENV_SECT_SIZE0x10004KB那么env区占2个sector。saveenv时u-boot会先擦除这2个sector再把环境变量数据写进去。如果CONFIG_ENV_SIZE超过一个sectoru-boot默认会做擦除-写-擦除-写的循环这就是掉电风险的来源。更稳妥的做法是启用CONFIG_ENV_IS_IN_SPI_FLASH的冗余备份CONFIG_ENV_OFFSET_REDUND把env存两份一份坏了用另一份。3.3 环境变量持久化的完整调用链saveenv的执行路径值得完整走一遍这样出问题时才知道在哪一层打断点。命令层cmd/nvedit.c里的do_saveenv被调用。环境层env/saveenv根据CONFIG_ENV_IS_IN_*选择具体实现SPI-NOR走env/sf.c里的env_sf_save。擦除env_sf_save调用spi_flash_erase传入CONFIG_ENV_OFFSET和CONFIG_ENV_SIZE。驱动层spi_flash_erase最终调用spi_nor_erase根据地址范围选择4KB或64KB擦除命令。写入擦除完成后调用spi_flash_write把环境变量数据按page大小分片写入。校验部分版本会读回校验确认写入成功。这条链路上最容易出问题的是第3步和第5步。擦除失败通常是写保护WP引脚或状态寄存器BP位被置位写入失败通常是page边界跨越或写使能WREN命令没发。调试时可以在spi_nor_erase和spi_nor_write里加打印看实际下发的命令和地址。提示如果saveenv返回0但重启后环境变量没变先确认CONFIG_ENV_OFFSET是否和实际flash布局一致再确认u-boot启动时是否从同一个位置读取。有些板子有多个启动介质u-boot可能从MMC启动但env存在SPI-NOR里这种交叉配置最容易出错。4. 实操过程从零配置一个SPI-NOR持久化环境4.1 硬件确认与SPI模式配置动手之前先把硬件确认清楚。需要知道的信息包括SPI-NOR型号、容量、sector大小、SPI控制器编号、片选引脚、时钟频率。这些信息通常在原理图和芯片手册里。以一颗常见的W25Q12816MB为例sector是4KBpage是256B支持标准SPI、Dual SPI、Quad SPI。u-boot里对应的配置是CONFIG_SPI_FLASHy CONFIG_SPI_FLASH_WINBONDy CONFIG_SPI_FLASH_SFDP_SUPPORTy CONFIG_SPI_FLASH_BARy CONFIG_SF_DEFAULT_SPEED50000000 CONFIG_SF_DEFAULT_MODE0x0CONFIG_SF_DEFAULT_MODE0x0表示CPOL0、CPHA0这是大多数SPI-NOR的默认模式。如果读JEDEC ID失败可以尝试0x3CPOL1、CPHA1。时钟频率不要一上来就拉满先用10MHz确认能识别再逐步提到50MHz或更高。设备树里SPI控制器的配置spi0 { status okay; num-cs 1; flash0 { compatible winbond,w25q128, jedec,spi-nor; reg 0; spi-max-frequency 50000000; spi-cpol; spi-cpha; }; };注意spi-cpol和spi-cpha这两个属性它们对应SPI模式。如果硬件设计是Mode 0这两个属性都不要加如果是Mode 3两个都加。加错了就是读不到ID。4.2 分区规划与环境变量区设置分区规划要结合启动流程来定。典型布局是u-boot放在0x0env放在u-boot之后kernel再往后rootfs占剩余空间。以16MB为例分区偏移大小用途u-boot0x0000000x80000bootloaderenv0x0800000x10000环境变量kernel0x0900000x300000内核镜像rootfs0x3900000xC70000根文件系统env区给64KBCONFIG_ENV_SIZE0x10000CONFIG_ENV_SECT_SIZE0x1000。这样env区占16个4KB sectorsaveenv时会擦除16次。如果觉得掉电风险大可以改成CONFIG_ENV_SECT_SIZE0x10000让u-boot用64KB块擦除命令一次擦完。但前提是flash支持64KB块擦除命令0xD8W25Q128是支持的。对应的u-boot配置CONFIG_ENV_IS_IN_SPI_FLASHy CONFIG_ENV_OFFSET0x80000 CONFIG_ENV_SIZE0x10000 CONFIG_ENV_SECT_SIZE0x1000 CONFIG_ENV_OFFSET_REDUND0x90000启用冗余后env会存两份CONFIG_ENV_OFFSET_REDUND指向第二份的偏移。两份env轮流写每次saveenv更新其中一份并记录一个标志位。这样即使一份写坏了另一份还能用。4.3 编译、烧录与验证配置改完后重新编译u-bootmake ARCHarm CROSS_COMPILEarm-linux-gnueabihf- board_defconfig make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -j8烧录时注意如果env区在u-boot之后烧录u-boot镜像时不要覆盖env区。用sf probe和sf erase、sf write命令手动烧录sf probe 0:0 50000000 0 sf erase 0x0 0x80000 sf write 0x80000000 0x0 0x800000x80000000是u-boot镜像在DDR里的加载地址具体值看你的CONFIG_SYS_TEXT_BASE或CONFIG_TEXT_BASE。验证持久化setenv testvar hello saveenv reset # 重启后 printenv testvar如果重启后testvar还在说明持久化链路通了。如果不在按第3.3节的调用链逐层排查。4.4 与Linux侧的衔接u-boot把env存在SPI-NOR里Linux侧通常通过mtdblock或ubi来访问。如果Linux也要读写env需要保证两边的分区定义一致。Linux设备树里的分区节点要和u-boot的mtdparts或设备树分区完全对应。Linux侧挂载jffs2到某个MTD分区的命令mtdinfo /dev/mtd2 mount -t jffs2 /dev/mtdblock2 /mnt这里/dev/mtd2对应的是第三个分区从0开始计数。如果挂载报jffs2: Magic bitmask 0x1985 not found说明该分区不是jffs2格式或者分区偏移错了。先用mtd_debug info /dev/mtd2确认分区大小和擦除块大小再用flash_eraseall /dev/mtd2格式化最后挂载。注意u-boot和Linux对同一个MTD分区的并发访问要避免。u-boot阶段写完env后Linux启动时不要再对env分区做擦除操作否则会把u-boot的env格式破坏掉。5. 常见问题与排查技巧实录5.1 环境变量保存失败问题速查表现象可能原因排查方法解决saveenv返回-1写保护引脚拉高量WP引脚电平拉低WP或改硬件saveenv返回-1状态寄存器BP位锁定读SR1/SR2发WRR命令清BPsaveenv成功但重启丢失env偏移不一致对比u-boot和Linux分区统一分区定义saveenv成功但重启丢失冗余标志位错误读两份env的头部清空冗余区重写读JEDEC ID为0xFFFFFFSPI模式错误改CPOL/CPHA匹配硬件模式读JEDEC ID为0x000000片选或供电问题量CS和VCC检查硬件连接擦除超时时钟太快降SPI频率先用10MHz验证写入数据错乱page边界跨越检查写入地址按page对齐写入5.2 几个我踩过的坑和独家技巧第一个坑是env区跨64KB边界。有一次我把CONFIG_ENV_OFFSET设成0xF0000CONFIG_ENV_SIZE设成0x10000结果env区从0xF0000到0x100000正好跨了一个64KB块的边界。saveenv时u-boot先擦除0xF0000所在的64KB块再擦除0x100000所在的块但0x100000已经超出了16MB flash的范围导致擦除命令被忽略写入的数据部分丢失。后来把env区对齐到64KB边界就好了。技巧env偏移和大小都按最大擦除块对齐能省很多事。第二个坑是冗余env的标志位不更新。启用CONFIG_ENV_OFFSET_REDUND后u-boot会在env数据头部写一个标志表示当前哪份是有效的。如果手动用sf write烧录了env数据但没写标志u-boot会认为两份都无效回退到默认环境变量。技巧手动烧录env后用env import命令导入让u-boot自己写标志位。第三个坑是SPI-NOR的4字节地址模式。32MB的flash必须用4字节地址但有些u-boot版本默认还是3字节导致访问高16MB时地址回绕。表现是读写高地址数据时实际操作的是低地址。技巧在spi_nor_scan里确认nor-addr_width是否为4如果不是检查CONFIG_SPI_FLASH_BAR是否启用以及flash是否支持4B模式。5.3 调试手段与工具推荐u-boot里调试flash最直接的手段是sf命令和mtd命令。sf probe能确认设备是否识别sf read和sf write能验证读写sf erase能验证擦除。mtd list能看到所有分区mtd read和mtd write能按分区操作。如果sf命令不可用检查CONFIG_CMD_SF是否启用。如果mtd命令不可用检查CONFIG_CMD_MTD和CONFIG_MTD_PARTITIONS。硬件层面逻辑分析仪是排查SPI问题的利器。抓CS、CLK、MOSI、MISO四根线看命令字节、地址字节、数据字节是否符合预期。比如读JEDEC ID应该看到0x9F命令然后3字节ID返回。如果命令发出去了但没返回查MISO线是否被拉低或浮空。软件层面可以在spi_nor_erase和spi_nor_write里加printf打印地址、长度、命令。但注意u-boot的串口输出会拖慢时序调试完记得去掉。6. 持久化方案的选型对比与扩展思路6.1 SPI-NOR、NAND、MMC三种持久化介质对比维度SPI-NORNANDMMC/eMMC容量1-64MB128MB-4GB4GB-128GB擦除块4KB/64KB128KB512KB坏块极少常见有管理写入寿命10万次1万次1万次成本中低中高u-boot支持成熟较复杂成熟适用场景小容量配置存储大容量数据大容量系统选型逻辑很简单如果只需要存环境变量和小固件SPI-NOR最省心如果需要存内核和rootfs且容量超过64MBNAND或eMMC更合适如果对可靠性要求极高且容量不大SPI-NOR加冗余env是最稳的方案。6.2 环境变量持久化的可靠性增强除了冗余env还有几个增强可靠性的手段。一是CRC校验u-boot默认会对env数据做CRC32校验如果校验失败会回退到默认环境。确保CONFIG_ENV_CRC或相关宏启用。二是写前擦除确认在env_sf_save里加一个擦除后的读回校验确认擦除区域全是0xFF再写入。三是掉电检测如果硬件有PMIC或电压监测可以在saveenv前确认电压稳定。对于特别关键的配置可以考虑把env存到多个介质比如SPI-NOR存一份、eMMC存一份u-boot启动时按优先级读取。这种方案在工业设备里比较常见代价是启动时间略长。6.3 从u-boot到Linux的持久化一致性最后说一个容易被忽略的点u-boot和Linux对flash的访问要有一致的假设。比如u-boot用4KB sector擦除envLinux的mtd驱动也要用4KB sectoru-boot用3字节地址Linux也要用3字节地址。如果两边不一致会出现u-boot能存、Linux读不到或者反过来。验证一致性的方法在u-boot里sf read一段数据到DDR然后在Linux里用mtd_debug read读同一段对比内容。如果一致说明两边对flash的访问是统一的。如果不一致检查两边的分区表、地址宽度、擦除块大小。这个内容后续还可以这样扩展把u-boot的env持久化和Linux的fw_setenv/fw_printenv工具链打通实现两边都能读写同一份env或者研究u-boot的fastboot和dfu模式下flash子系统的行为差异。我个人的体会是flash子系统的问题十有八九出在参数配置和分区对齐上真正驱动本身的bug反而少见。把参数表列清楚、把分区图对齐好能省掉一大半调试时间。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询