资讯详情

u-boot flash子系统深度解析:MTD、SPI-NOR与env持久化实战

📅 2026/10/2 12:17:42 | 华诺云谱 👁 阅读
u-boot flash子系统深度解析:MTD、SPI-NOR与env持久化实战
1. 从一次量产翻车说起为什么我要把u-boot的flash子系统翻个底朝天前阵子帮一个做工业网关的朋友救火产线上三百多台设备烧录完固件之后有将近四十台在低温老化测试里起不来。串口打出来的log停在Uncompressing Linux...之前u-boot阶段就挂了报的是SF: Failed to read page。当时第一反应是SPI-NOR颗粒批次有问题换了一批料还是复现最后定位到是u-boot里SPI flash的读时序参数在低温下裕量不够加上分区表里env区跨了4K边界擦写的时候把相邻的uboot分区头给蹭了。这件事之后我把手头几个项目的u-boot flash子系统从头到尾捋了一遍从MTD分层到SPI-NOR驱动、从env持久化到jffs2挂载踩过的坑整理成这篇东西。如果你正在做嵌入式Linux的BSP、搞过光猫或者路由器的固件、或者单纯被mtdparts和mtdblock绕晕过这篇应该能帮你省下几个通宵。核心关键词就几个u-boot、flash子系统、持久化、MTD、SPI-NOR我会围绕这几个点把整个链路讲透包括那些文档里不会写、但产线上一定会遇到的细节。先说清楚这篇适合谁看一是刚接手u-boot移植、对drivers/mtd目录一脸懵的嵌入式新人二是做过一段时间BSP、但env持久化和分区挂载总是出玄学问题的老手三是做光猫、CPE、工业网关这类需要掉电保存配置的产品关心jffs2/ubifs到底该挂哪个mtd分区的工程师。全文基于我实际用过的几款主流SoC和SPI-NOR颗粒参数和命令都可以直接抄但具体到你的板子还是要以datasheet为准。2. u-boot flash子系统的整体设计与分层思路2.1 为什么u-boot要搞一套自己的MTD而不是直接用内核的很多人第一次看u-boot源码会疑惑Linux内核里已经有一套成熟的MTD子系统了u-boot为什么还要在drivers/mtd下再实现一遍这不是重复造轮子吗答案在于运行环境和生命周期完全不同。内核的MTD是在系统起来之后、内存管理、中断、工作队列全都就绪的前提下工作的它可以慢慢枚举、可以睡眠等待、可以做复杂的坏块管理。而u-boot跑在裸机或者极简环境下栈很小、没有完整的中断框架、很多驱动是轮询模式它的首要目标是在几百毫秒内把内核从flash里读出来并跳过去。所以u-boot的flash子系统设计哲学是够用、够快、够简单能读能写能擦就行复杂的FTL、磨损均衡、后台GC统统不要。这就解释了为什么u-boot里你会看到mtdcore.c、mtdpart.c、nand/core.c、spi/sf.c这些文件它们其实是内核MTD的一个精简克隆版。分层结构大体是这样的最上层是命令层cmd/mtd.c、cmd/flash.c中间是MTD核心层mtdcore.c负责注册mtd_infomtdpart.c负责分区解析下面是具体驱动spi/sf.c对应SPI-NORnand/对应NANDonenand/、cfi/对应并行NOR。这个分层和内核几乎一一对应所以你在内核里学到的MTD概念在u-boot里基本能直接迁移。提示u-boot的MTD没有内核那么完整的坏块管理和ECC自动纠正NAND场景下很多ECC是要驱动自己处理的这点后面会专门讲。2.2 MTD、mtd_info、mtd_part这三者的关系理解u-boot flash子系统的关键是把mtd_info这个结构体吃透。它就是一个块设备抽象里面最核心的字段是erasesize、writesize、size、type以及一组函数指针_read、_write、_erase。SPI-NOR驱动在probe的时候会填一个mtd_infoNAND驱动也会填一个上层命令和文件系统根本不关心底层是NOR还是NAND只认这个结构体。分区是怎么来的mtdpart.c里的add_mtd_partitions会拿一个父mtd_info然后根据你传进来的分区表为每个分区克隆出一个子mtd_info把offset和size调整好函数指针还是指向父设备的。所以你在u-boot里看到的mtd0、mtd1本质上是同一个物理flash的不同视图。这里有个很容易踩的坑子分区的读写最终会落到父设备的_read/_write上但父设备驱动如果没做边界检查你越界访问它也不会拦你。我那次量产翻车就是env分区写越界蹭到了uboot分区因为sf.c的写函数只检查了是否超过整个flash的size没检查分区边界。分区表怎么传给u-boot常见三种方式一是编译时通过CONFIG_MTDPARTS_DEFAULT写死二是通过设备树partition节点三是通过u-boot命令行mtdparts环境变量动态设置。工业产品我强烈建议用设备树或者写死别依赖env里的mtdparts因为env本身就在flash里一旦env坏了分区表就没了直接死锁。2.3 SPI-NOR在u-boot里的驱动模型SPI-NOR是u-boot flash子系统里最常打交道的对象光猫、路由器、工业网关基本都是它。它的驱动模型分两层drivers/spi/下面是SPI控制器驱动比如spi-quad.c、designware_spi.cdrivers/mtd/spi/sf.c是SPI-NOR通用层。sf.c通过spi_flash结构体跟控制器通信发0x03读、0x02页编程、0x20扇区擦除、0x06写使能这些标准命令。这里要重点说的是SPI模式。SPI-NOR支持单线、双线、四线Quad甚至八线Octal模式u-boot里通过spi_flash_read_*系列函数切换。四线模式下读速度能到单线的四倍但前提是控制器和颗粒都支持而且上电后必须先发0x35读状态寄存器确认QE位Quad Enable已经置位否则四线命令发出去颗粒根本不响应。我见过太多板子因为QE位没置u-boot里读flash慢得像蜗牛内核启动要十几秒。另一个关键点是地址模式。容量大于16MB的SPI-NOR需要4字节地址命令是0x134字节读而不是0x03。u-boot里通过CONFIG_SPI_FLASH_BAR或者spi_flash_set_4byte来切换。如果你的颗粒是32MB但u-boot只认16MB八成就是4字节地址没使能。3. 核心细节解析env持久化、分区表与文件系统挂载3.1 env持久化到底存在哪怎么存env环境变量的持久化是u-boot flash子系统里最实用也最容易出问题的部分。saveenv命令最终会调用env_flash.c里的saveenv函数把内存里的env数组写到flash的env分区。存储格式有两种老式的ENV_CRC单副本和新式的ENV_REDUNDANT双副本redundant。工业产品我强烈建议开CONFIG_ENV_OFFSET_REDUND双副本互为备份一个写坏了还能从另一个恢复。env区的擦写有个硬性约束擦除单位是扇区通常4KB写入单位是页通常256字节。env数据一般也就几KB但每次saveenv都要擦掉整个扇区再写回去。SPI-NOR的擦写寿命是10万次左右如果你在应用里频繁调用fw_setenv几年就能把env扇区写坏。我见过一个客户在应用里每秒写一次env记录运行状态半年后设备集体变砖。正确做法是env只存启动配置运行状态存到文件系统或者单独的日志分区。还有一个细节是env区的对齐。CONFIG_ENV_OFFSET必须是CONFIG_ENV_SECT_SIZE的整数倍否则擦除的时候会跨扇区把相邻分区的内容一起擦掉。我那次翻车就是CONFIG_ENV_OFFSET设成了0x3F000而扇区是4KB0x3F000不是4KB对齐的擦除时从0x3E000开始擦正好覆盖了uboot分区尾部。3.2 mtdparts分区表怎么写才不出事mtdparts的语法是mtdpartsmtd-id:sizeoffset(name),...比如mtdpartsspi0.0:1M(uboot),256K(env),4M(kernel),8M(rootfs),-(data)这里有几个坑。第一size和offset的写法1M、256K、-表示剩余全部都支持但offset如果省略就默认接在上一个分区后面。第二分区必须按offset升序排列乱序会导致mtdpart.c解析出错。第三最后一个分区用-但如果你前面算错了size-分到的空间可能为负u-boot会直接报错。更隐蔽的坑是分区名和文件系统的对应关系。光猫里经常看到有人问jffs2挂载哪个mtd分区答案取决于你的分区表。jffs2需要挂在一个独立的、可擦写的分区上通常是rootfs或者data分区。挂载命令是mount -t jffs2 /dev/mtdblock3 /mnt注意是mtdblock不是mtdmtd是字符设备用于擦写mtdblock是块设备用于文件系统。如果你挂错了分区比如挂到了uboot分区jffs2会尝试擦写uboot直接变砖。3.3 SPI-NOR的读写擦时序与参数计算SPI-NOR的时序参数直接决定稳定性尤其是低温或高频场景。几个关键参数read命令的dummy cycles、page program的tPP页编程时间、sector erase的tSE扇区擦除时间。以常见的W25Q128为例tSE典型值45ms、最大400mstPP典型0.7ms、最大3ms。u-boot里sf.c的spi_flash_wait_till_ready会轮询状态寄存器的BUSY位超时时间要设够否则低温下擦除没完成就发下一条命令直接报错。读时序的dummy cycles在高速模式下很关键。比如0xEBQuad IO Fast Read需要6个dummy cycles0x0BFast Read需要8个。如果控制器配的dummy数和颗粒要求不一致读出来的数据就是乱的。我那次低温翻车就是0x0B命令的dummy配成了6常温下勉强能读低温下时序裕量不够就挂了。改回8之后问题消失。频率方面SPI-NOR通常支持到50MHz、80MHz、104MHz甚至133MHz但频率越高对PCB走线和颗粒质量要求越高。工业产品我一般保守用50MHz消费类可以上80MHz。如果你发现u-boot读flash偶尔CRC错先把频率降下来试试八成是信号完整性问题。4. 实操过程从零配置一个SPI-NOR的flash子系统4.1 设备树里怎么描述SPI-NOR和分区以一款常见的ARM SoC为例设备树里SPI控制器的节点大概长这样spi0 { status okay; flash0 { compatible jedec,spi-nor; reg 0; spi-max-frequency 50000000; partitions { compatible fixed-partitions; #address-cells 1; #size-cells 1; partition0 { label uboot; reg 0x0 0x100000; }; partition100000 { label env; reg 0x100000 0x40000; }; partition140000 { label kernel; reg 0x140000 0x400000; }; partition540000 { label rootfs; reg 0x540000 0x800000; }; }; }; };注意reg的第二个值是size单位是字节。0x100000是1MB0x40000是256KB。分区必须连续且不重叠否则u-boot解析会报partition overlap。spi-max-frequency设50MHz如果你的板子信号好可以往上调但建议先用50MHz跑通再优化。4.2 u-boot配置项怎么开make menuconfig里几个关键项CONFIG_MTDy打开MTD核心CONFIG_CMD_MTDy打开mtd命令CONFIG_CMD_FLASHy打开flash命令CONFIG_SPI_FLASHy打开SPI-NOR通用层CONFIG_SPI_FLASH_WINBONDy打开对应厂商支持CONFIG_MTD_PARTITIONSy打开分区支持CONFIG_MTDPARTS_DEFAULT...默认分区表CONFIG_ENV_IS_IN_SPI_FLASHyenv存SPI-NORCONFIG_ENV_OFFSET0x100000env偏移CONFIG_ENV_SIZE0x10000env大小CONFIG_ENV_SECT_SIZE0x1000扇区大小CONFIG_ENV_OFFSET必须和分区表里env分区的offset一致CONFIG_ENV_SECT_SIZE必须和颗粒实际扇区大小一致SPI-NOR通常是4KB但有些老颗粒是64KB。这两个对不上saveenv必挂。4.3 上电后的验证流程u-boot起来之后先敲sf probe确认颗粒被识别sf probe SF: Detected w25q128 with page size 256 Bytes, erase size 4 KiB, total 16 MiB看到这行说明SPI-NOR驱动和颗粒通信正常。然后mtd list看分区mtd list List of MTD devices: * spi0.0 - device: spi-flash - parent: spi0 - driver: jedec_spi_nor - type: NOR flash - block size: 0x1000 - min I/O: 0x1 - size: 0x1000000 - offset: 0x0 - partitions: - 0x000000000000-0x000000100000 : uboot - 0x000000100000-0x000000140000 : env - 0x000000140000-0x000000540000 : kernel - 0x000000540000-0x000000d40000 : rootfs分区offset和size都对上了说明设备树解析正常。接着测env持久化setenv test_var hello saveenv断电重启printenv test_var如果还能看到helloenv持久化就通了。如果报Saving Environment to SPI Flash... Failed八成是CONFIG_ENV_OFFSET和分区表对不上或者扇区大小配错了。4.4 内核里怎么对接这些分区u-boot把分区表通过bootargs里的mtdparts传给内核内核的mtdpart.c会解析出同样的分区。内核起来后cat /proc/mtd应该能看到dev: size erasesize name mtd0: 00100000 00001000 uboot mtd1: 00040000 00001000 env mtd2: 00400000 00001000 kernel mtd3: 00800000 00001000 rootfs挂载jffs2就是mount -t jffs2 /dev/mtdblock3 /mnt。注意mtdblock3对应mtd3也就是rootfs分区。如果你要挂的是data分区就找对应的mtd编号。这里有个经验jffs2适合小容量NOR32MB大容量或者NAND建议用ubifs因为jffs2挂载时要扫描整个分区分区越大挂载越慢16MB的jffs2挂载可能要好几秒。5. 常见问题与排查技巧实录5.1 问题速查表现象可能原因排查方法解决sf probe报Failed to read page时序参数不对、QE位没置、频率过高降频到25MHz试读状态寄存器调整dummy cycles置QE位saveenv报Failedenv offset和分区表不一致、扇区大小错对比CONFIG_ENV_OFFSET和mtd list改成一致扇区设4KB分区表解析报overlap分区offset/size算错手算每个分区起止地址重新排布确保连续不重叠内核挂载jffs2失败挂错mtd分区、分区没格式化cat /proc/mtd确认编号挂到正确分区先flash_eraseall低温启动失败时序裕量不够、擦除超时低温箱里复现看log停在哪降频、加大超时、换工业级颗粒读flash CRC错信号完整性差、dummy cycles错示波器看SPI波形降频、改dummy、加匹配电阻5.2 几个文档里不会写的避坑经验第一env区别和uboot区挨着。我习惯在uboot和env之间留一个扇区的gap比如uboot占0x0-0x100000env从0x101000开始而不是0x100000。这样即使env擦除时越界一个扇区也不会蹭到uboot。这一个扇区的代价换来的稳定性在工业产品上非常值。第二SPI-NOR的4字节地址模式要在驱动probe时就设好。有些颗粒上电默认是3字节地址容量大于16MB时访问高地址会回绕到低地址。u-boot里spi_flash_scan会读SFDP表判断容量但有些老颗粒不支持SFDP需要手动在板级文件里调spi_flash_set_4byte。如果你发现32MB颗粒只能读写前16MB就是这个原因。第三jffs2分区第一次挂载前必须擦干净。新出厂的flash里可能是全0xFFjffs2能识别但如果之前写过别的数据残留的旧jffs2节点头会导致挂载失败。量产时在u-boot里加一句mtd erase rootfs或者在内核启动脚本里flash_eraseall /dev/mtd3能省掉大量售后。第四saveenv不要放在中断或者高频循环里。前面说过SPI-NOR擦写寿命10万次而且每次擦写要几十毫秒放在高频路径里既伤flash又拖慢系统。运行状态要持久化用文件系统或者单独的日志分区别折腾env。第五双副本env的CONFIG_ENV_OFFSET_REDUND要和CONFIG_ENV_OFFSET隔开至少一个扇区。两个副本如果挨着一个擦除时越界就把另一个也擦了双副本就失去意义了。我一般让两个副本间隔CONFIG_ENV_SIZE向上取整到扇区。5.3 一个真实的低温翻车复盘回到开头那次量产翻车。低温-40度下SPI-NOR的擦除时间从常温的45ms涨到接近300ms而u-boot里spi_flash_wait_till_ready的超时设的是100ms擦除还没完成就返回了后续写入自然失败。同时0x0B读命令的dummy cycles配成了6常温下时序裕量够低温下颗粒响应变慢读出来的数据错位CRC校验不过。修复方案三步一是把超时从100ms加到500ms覆盖最坏情况二是dummy cycles从6改回8严格按datasheet三是把SPI频率从80MHz降到50MHz给低温留裕量。改完之后三百台全部通过低温老化。这件事的教训是datasheet上的典型值只能用于常温评估工业产品的时序参数必须按最大值留裕量而且要在温度极限下实测。6. 关于持久化方案选型的一点个人看法env、jffs2、ubifs这三种持久化方式我在不同产品上都用过选型逻辑其实很清晰。env适合存几十个字节的启动配置读写频率极低双副本冗余就够。jffs2适合NOR上几MB到十几MB的配置数据掉电安全、自带磨损均衡但挂载慢、大分区不划算。ubifs适合NAND或者大容量NOR挂载快、压缩率高但需要内核支持且配置稍复杂。光猫这类产品配置数据通常就几百KBjffs2挂在data分区完全够用没必要上ubifs。工业网关如果配置多、还要存日志可以考虑单独划一个ubifs分区。至于env永远只存启动参数别拿它当配置数据库用。最后分享一个我常用的调试技巧在u-boot里用mtd read和mtd write直接读写分区配合md/mw看内存能快速定位是驱动问题还是分区问题。比如mtd read rootfs 0x80000000 0 0x1000把rootfs分区头读到内存再md 0x80000000看内容如果读出来全是0xFF说明分区是空的如果读出乱码说明读时序有问题。这个手法比反复烧录快得多建议你加到自己的调试工具箱里。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑