最近在调一块基于ARM64平台的核心板,遇到的最直接的一个需求就是把几MB的升级固件通过U盘灌进板子。板子的网络接口还没完全调通,TF卡驱动处于“能识别但不稳定”的状态,剩下最可靠的路径就是USB。u-boot下把U盘里的文件读出来,表面上就是一条usb start; fatload usb 0:1 0x82000000 upgrade.bin的命令,但从USB控制器初始化到U盘枚举、再到FAT文件系统解析,背后其实是一条完整的软件链路。我花了两天时间把这条链路完整跑通,过程中踩了不少坑,也把u-boot的USB驱动模型、存储协议、文件系统兼容性这些细节都翻了一遍。这篇文章就把从控制器初始化到fatload读U盘的完整过程和排障经验写出来,给准备在u-boot阶段用U盘做烧录、做产品升级的朋友一个参考。
1. 为什么要在u-boot里折腾U盘:场景、需求与整体方案
1.1 什么场景下必须在u-boot阶段读U盘
很多人觉得,u-boot里读U盘是个冷门需求,毕竟进了Linux之后挂载U盘太简单了,一句mount /dev/sda1 /mnt就完事。但在下面几个场景里,u-boot阶段的USB支持是绕不过去的:
第一种是开发板bring-up阶段。网络驱动、SD/MMC驱动可能都还没稳定,但你想先验证kernel镜像、设备树、rootfs能不能正常启动。这时候用U盘在u-boot里把文件load到DDR里,再bootm引导,是成本最低的方案。
第二种是产线批量烧录。很多设备没有网口或者不允许接网口,产线工人拿着一个量产母盘,插上设备、上电、自动烧录,这个流程必须在u-boot阶段完成,因为烧录动作就是要往emmc/NAND里写固件,还没到Linux那一层。
第三种是变砖恢复。emmc分区表损坏、kernel起不来,但u-boot还在,可以用U盘从外部加载一个恢复镜像,把flash重新刷一遍。
这三种场景有一个共同点:u-boot环境没有完整的驱动模块机制,也没有udev帮你自动创建设备节点。你要什么设备,就得先把对应的驱动编译进u-boot里。读U盘本质上是“控制器初始化 + 设备枚举 + 块设备读取 + FAT解析”四个环节缺一不可,任何一个环节出问题,最终表现都是“fatload失败”。
1.2 u-boot读取U盘的完整链路
我们可以把这条链路按层次拆开看:
- USB控制器驱动层:负责把EHCI/xHCI/DWC2这些控制器硬件初始化起来,包括时钟、复位、VBUS供电、PHY配置。
- USB core枚举层:负责端口复位、读取设备描述符、分配地址、读取配置描述符,把U盘识别成一个USB设备。
- 设备类匹配层:u-boot根据接口描述符里的设备类(bInterfaceClass = 0x08 Mass Storage Class),把设备绑定到usb_storage驱动。
- 传输协议层:u-boot的usb_storage驱动使用BOT(Bulk-Only Transport)协议,通过CBW/CSW命令与U盘交互,底层是基于SCSI命令集的READ/WRITE。
- 块设备层:usb_storage注册为blk device,提供
blk_dread()/blk_dwrite()接口,上层不管U盘内部是什么主控、什么闪存,统一按块读写。 - 文件系统层:u-boot的FAT文件系统驱动通过blk_dread读取U盘扇区,解析FAT表,最终提供
fatload/fatls/fatinfo这些命令。
如果只是执行一条fatload就完事,你可能感受不到这个层次。但一旦出问题,你必须能判断出问题到底出在哪一层——是硬件没起来、枚举失败、存储协议不兼容,还是文件系统解析不了。后面第5章我会专门用一个实际案例演示怎么逐层定位。
2. u-boot USB驱动架构拆解:从host控制器到文件系统层
2.1 驱动模型视角下的u-boot USB
新版u-boot(比如2022.04,我这次用的版本)已经全面切到driver model(DM)架构。USB相关的uclass主要涉及这几个:
UCLASS_USB:USB控制器主机端设备,例如EHCI、xHCI、DWC3。每个控制器在设备树里对应一个节点。UCLASS_USB_HUB:USB Hub设备,包括根Hub(root hub)和外部Hub。UCLASS_USB_DEVICE:实际插入的USB设备,U盘就属于这一类。
设备树里的USB节点长这样(以我手头板卡的OTG口为例):
&usbotg1 { status = "okay"; dr_mode = "host"; vbus-supply = <®_vbus_5v>; phy_type = "utmi"; };u-boot的USB子系统在DM模型下会自动把设备树里的控制器节点和host驱动绑定起来。这里的dr_mode = "host"很关键——如果控制器同时支持OTG,你必须在u-boot阶段明确指定host模式,否则初始化方向可能不对。vbus-supply会告诉驱动哪路电源控制5V输出,如果缺少这个属性,驱动可能不会去拉高VBUS,U盘压根就没上电。
老版本u-boot没有DM,直接在板级文件里用usb_lowlevel_init()注册控制器,宏定义也散落在头文件里。虽然我现在用的新版本,但排障思路和驱动层次基本一样,老项目的经验也能复用。
2.2 必须打开的配置项
u-boot里读U盘,第一个坑就是配置项缺失。不同的配置缺失,失败的现象完全不同:
| 配置项 | 作用 | 缺失时的现象 |
|---|---|---|
CONFIG_CMD_USB | 提供usb命令 | 命令行里敲usb start直接报“未知命令” |
CONFIG_USB_EHCI_HCD/CONFIG_USB_XHCI_HCD | 控制器主机驱动 | usb start看不到bus初始化信息 |
CONFIG_USB_STORAGE | Mass Storage设备支持 | usb tree能看到设备,但usb storage列表为空 |
CONFIG_DM_USB | 启用USB驱动模型 | 设备树里的USB节点不生效 |
CONFIG_FS_FAT/CONFIG_CMD_FAT | FAT文件系统支持 | 没有fatload/fatls命令 |
CONFIG_USB_HOST_ETHER | USB网卡支持 | 与读U盘无关,但排查时容易误开 |
基本上,读U盘的最低配置组合是:CONFIG_CMD_USB + CONFIG_USB_EHCI_HCD + CONFIG_USB_STORAGE + CONFIG_FS_FAT + CONFIG_CMD_FAT。如果你的控制器是xHCI或者DWC3,再把对应的HCD配置打开。我之前就在一份配置里漏掉了CONFIG_USB_STORAGE,结果usb start和usb tree全都正常,就是usb storage空荡荡,一度误以为U盘坏了。
2.3 存储协议:BOT与UASP的兼容性
这个点是最容易忽略的。u-boot对USB存储设备的支持主要集中在BOT协议(Bulk-Only Transport),这是USB 2.0时代定义的Mass Storage传输协议。而很多USB 3.0移动固态盘、U盘,默认使用的是UASP(USB Attached SCSI Protocol),或者自动在UASP和BOT之间切换。
当u-boot扫描到U盘时,它期望看到的是BOT接口的Mass Storage设备。如果U盘固件选择只暴露UASP接口,u-boot的usb_storage驱动就绑定不上,表现就是枚举成功了但拿不到块设备。我手里有几块不同主控的U盘,实测下来,SMI、Phison这类常见主控基本都保留了BOT兼容;但一些新出的NVMe桥接方案、高端移动固态盘,在u-boot里经常读不到。这不是u-boot有问题,是协议层对不上。
遇到这种情况,最直接的测试方法是换一块普通的USB 2.0 U盘验证。如果普通U盘能读,问题基本锁定在UASP/主控兼容性,而不是板卡驱动。
3. 控制器初始化到设备枚举:核心流程与驱动适配细节
3.1usb start背后到底发生了什么
在u-boot命令行敲usb start,实际执行动作是:
starting USB... Bus usb@1c14000: USB EHCI 1.00 scanning bus for devices... 2 USB Device(s) found这个输出里隐藏了几个关键步骤:
usb_init()遍历系统中所有的USB控制器(uclass),逐个调用控制器的usb_lowlevel_init()。usb_lowlevel_init()负责控制器硬件初始化——开时钟、解复位、配置PHY、拉起VBUS。- 控制器起来之后,
usb_host_scan()对根Hub执行端口扫描,也就是枚举流程。 - 枚举成功后再对每个新发现的USB设备调用
usb_scan_device(),读取描述符、分配地址,并尝试绑定驱动。
如果你在多个控制器(比如一个native EHCI加一个PCIe转USB的xHCI),可以敲usb start auto,u-boot会自动遍历所有控制器并逐个扫描设备。不过我个人习惯用默认usb start,因为日志少、定位更清楚。
3.2 控制器初始化最容易出问题的三个细节
控制器硬件初始化在u-boot里可比Linux简单粗暴得多,没有完整的runtime PM,没有延迟探测,也没有中断驱动。我这次在控制器初始化阶段就遇到了三个典型问题:
第一,时钟和复位。很多SoC的USB控制器时钟默认是关闭的,或者USB模块还处在复位状态。u-boot的设备树里如果clk属性不全,驱动起不来,现象就是usb start之后卡住或者直接显示找不到bus。解决办法是把设备树里USB节点的clocks、resets属性补全,去对照SoC的TRM确认USB控制器属于哪个时钟域。u-boot对clk的处理比Linux精简,有些平台需要自己加clock-notifier,这点在移植时尤其要注意。
第二,VBUS上电逻辑。主机模式下U盘需要VBUS提供5V电。u-boot不会像Linux那样调用一个完整的regulator框架,有的控制器驱动会去读设备树里的vbus-supply,有的板子需要你手动把VBUS的GPIO拉高。如果设备枚举总是超时,先用万用表量板子USB座的VBUS引脚有没有5V,这个判断最快。我这次就是靠量电压定位到VBUS使能GPIO配置错了。
第三,PHY配置。控制器和USB PHY是两回事,很多SoC把USB PHY独立成一个IP。u-boot里PHY的初始化往往不如ATF/SPL阶段完整,如果u-boot是从SPL或者ATF之后接管系统的,而前级启动代码没初始化PHY,USB就起不来。这种情况下不要在u-boot里死磕,先确认前级阶段有没有对PHY做配置,或者干脆在板级初始化函数里补上PHY的寄存器配置。
3.3 枚举成功的标志与日志解读
U盘枚举成功,u-boot通常会显示:
scanning bus for devices... 2 USB Device(s) found然后usb tree能看到层次结构:
USB device tree: 1 Hub (480 Mb/s, 0mA) |_ 2 Mass Storage (480 Mb/s, 200mA)看到Mass Storage这一行,说明设备类匹配成功了,u-boot已经认定这是一个存储设备。如果这里显示的是一堆Vendor Specific Class,那说明U盘主控暴露的接口描述符不是标准的Mass Storage类,存储驱动很可能绑不上。
枚举失败的报错也很有讲究。最典型的是:
unable to get device descriptor (error=8)error=8对应USB传输层的STALL错误,常见原因是U盘上电瞬间电流过大、VBUS电压跌落,或者DP/DM线上的上拉电阻有问题。如果出现timeout polling on port,一般是端口复位或者高速握手超时,这时候要检查USB PHY的终端阻抗配置和时钟稳定性。
一个非常容易被忽略的排查顺序:先看usb tree,再看usb storage。这两条命令之间的差别,能直接告诉你问题是在枚举层还是存储驱动层。我在下面第5章的排障案例里会重点用到这个对比。
4. fatload命令的实际使用:地址规划、分区号与文件系统坑
4.1 fatload参数解析
等U盘被识别成块设备之后,才能执行fatload。命令格式是:
fatload <interface> <dev[:part]> <addr> <filename> [bytes]实际使用中:
fatload usb 0:1 0x82000000 upgrade.bin这里的usb 0:1中间有个冒号,含义是“USB控制器0、第1分区”。冒号前的0对应usb start时注册的控制器编号,不是U盘编号;冒号后的1对应U盘存储设备上的第一个分区。如果U盘上有多个分区,读取第二个分区就是usb 0:2。
配套的命令还有:
fatls usb 0:1:列出U盘根目录文件。fatinfo usb 0:1:显示文件系统信息,包括卷标、FAT类型、簇大小。fatwrite usb 0:1 0x82000000 readme.txt 0x100:把内存数据写进U盘文件,写操作在量产测试里很常用。
有个小技巧:拿不准分区编号时,先用usb storage看设备编号,再用fatls usb 0:1试探,大多数U盘只有一个FAT分区,0:1就够了。
4.2 内存地址规划:别把固件load到u-boot自己身上
很多人以为fatload地址随便填一个就行,其实不是。我必须强调:加载地址一定要落在有效的DDR范围内,并且避开u-boot自身、环境变量、设备树blob这些区域。
先跑bdinfo查看DDR布局:
U-Boot> bdinfo DRAM bank 0: start 0x80000000, size 0x40000000这表明DDR从0x80000000开始,大小1GB。那么0x82000000就是合理地址,离起始地址有32MB偏移,足够避开u-boot自身(通常占用底部几MB)。如果板子的DDR起始地址是0x20000000,你往0x82000000写,地址根本不在DDR范围内,内核会瞬间崩溃或者直接hang死。
一个常见的内存规划方案:
| 用途 | 地址 | 大小 |
|---|---|---|
| u-boot自身 | 0x80000000 ~ 0x81000000 | 16MB |
| kernel镜像 load地址 | 0x82000000 | 32MB |
| dtb load地址 | 0x83000000 | 8MB |
| initrd/rootfs | 0x84000000 | 128MB |
黄色部分是我习惯留出的安全区,宁可多留一点,也不要让kernel镜像和dtb离得太近,很多奇怪的启动问题其实是地址重叠导致的。
另外一个坑是DMA buffer对齐。u-boot的FAT驱动每次读取一个簇,底层块设备DMA要求地址满足cacheline对齐。如果你用了一个奇奇怪怪的地址,可能会遇到:
FAT: Misaligned buffer address解决办法也很简单:地址按64字节对齐,0x82000000这种本身就是对齐的,不要自作聪明用0x82000100之类带偏移的地址。
4.3 FAT文件系统兼容性:为什么你的U盘fatload总是失败
这是另一个高频雷区。u-boot的Fat驱动对FAT12/16/32支持得好,对exFAT的支持要看版本和配置。新版u-boot可以打开CONFIG_FS_EXFAT来支持exFAT,但默认配置里经常没开。而NTFS在u-boot里至今没有像样的支持,不要指望它。
现在出厂的64GB、128GB U盘,Windows默认格式化出来的往往就是exFAT。你拿到u-boot里执行fatls usb 0:1,很可能直接报:
Failed to mount. Please check the device.这时候不是U盘坏了,是文件系统不认。解决办法是把U盘重新格式化成FAT32。Windows自带的格式化工具对超过32GB的分区比较固执,通常不给FAT32选项,可以用第三方分区工具或量产工具强制格式化为FAT32。我实测过用Rufus做启动盘时把文件系统改成FAT32,也能解决u-boot认不出的问题。
还有一个细节是分区表格式。MBR分区表是u-boot兼容性最好的,GPT分区在部分老版本u-boot上可能识别不了。如果你U盘之前用某个工具写成了GPT+多个分区,usb 0:1可能会指向一个不存在的东西,报** Bad device usb 0 : 1 **。最稳妥的做法是:用磁盘管理把U盘的所有分区删掉,新建一个主分区,格式FAT32,再拷文件。
5. “能枚举但读不到盘”的完整排障实录
5.1 一个典型问题案例
这次调试过程中最有代表性的问题出现在第2天:U盘插上后usb start正常,枚举也正常,但就是读不到文件。完整日志如下:
U-Boot> usb start starting USB... Bus usb@1c14000: USB EHCI 1.00 scanning bus for devices... 2 USB Device(s) found U-Boot> usb tree USB device tree: 1 Hub (480 Mb/s, 0mA) |_ 2 Mass Storage (480 Mb/s, 200mA) U-Boot> usb storage No USB device found注意到没有:usb tree里明明显示Mass Storage设备存在,但usb storage列表为空。这说明枚举层已经把一个USB设备识别成了存储类设备,但usb_storage驱动没有成功拿到块设备接口。问题出在设备类匹配之后、块设备注册之前。
5.2 逐步排查链路
我按照下面的顺序一层层排查,每一步都有明确结果:
| 步骤 | 操作 | 结果 |
|---|---|---|
| 1 | usb info查看设备描述符 | 能显示PID/VID,说明描述符读取正常 |
| 2 | 换普通USB 2.0 U盘 | 还是不行,排除U盘主控UASP问题 |
| 3 | 用万用表量VBUS电压 | 5V稳定,排除供电问题 |
| 4 | 检查CONFIG_USB_STORAGE配置 | 配置已打开,排除编译选项问题 |
| 5 | 直接插板卡USB口,不经过Hub | 依然不行,排除外置Hub兼容性 |
| 6 | 开DEBUG级别日志重新u-boot | 发现usb_stor_get_info()在获取MaxLUN阶段卡住 |
最后定位到问题根源:这块U盘实际是读卡器加TF卡,读卡器主控支持Multi-LUN,上报的MaxLUN大于0,而u-boot在某个版本对Multi-LUN设备的处理有兼容性缺陷,导致后续的INQUIRY命令没能正常完成,块设备注册失败。
5.3 解决方案与验证
当时我没有去改u-boot源码(涉及工作量不小),先用一块普通U盘替代,确认整条链路是通的。后面为了根治,我把u-boot里usb_stor_get_info()中关于MaxLUN部分的逻辑做了小改动:如果MaxLUN读取失败或异常,强制按单LUN处理。修改之后重新编译,同一块读卡器TF组合也能正常fatload了。
这个案例给我的启发是:排障时一定要分清“枚举成功”和“存储设备注册成功”是两件事。usb tree是最快的信息分层工具,它告诉你USB设备层的状态;usb storage告诉你块设备层的状态。两个输出对不上,直接定位到存储协议和驱动的交界处,不要反复去折腾硬件。
如果遇到更诡异的情况,比如枚举都过不去,我建议你用USB抓包工具(逻辑分析仪、USB协议分析仪)看看枚举过程中的数据传输,重点看SET_ADDRESS和GET_DESCRIPTOR有没有STALL。有时候一根看似正常的USB线,在高速模式下就是枚举不过去,换一根短线就好了,这种玄学问题用示波器反而更快。
6. 让U盘加载固件成为默认动作:bootcmd编排与扩展玩法
6.1 bootcmd里编排usb和fatload
u-boot读U盘不是只为了手动测试。产品量产、现场升级,都需要上电自动从U盘加载固件。我的习惯是把自动升级流程写进bootcmd:
setenv bootcmd 'usb start; fatload usb 0:1 0x82000000 boot.img; bootm 0x82000000' saveenv这样板子上电后自动执行usb start,如果U盘里有boot.img,就load到内存并引导。有个细节要注意:usb start的执行时间取决于USB扫描流程,如果U盘没插,扫描会超时,耗时会明显拉长。量产场景下,我一般这样处理:
setenv bootdelay 1 setenv bootcmd 'if usb start; then if fatload usb 0:1 0x82000000 boot.img; then bootm 0x82000000; fi; fi; run distro_bootcmd'用两个if嵌套,USB初始化失败或U盘文件缺失时,自动回退到其他启动设备,不会让产线或现场因为U盘没插而卡死在u-boot。
6.2 热插拔与量产场景的注意事项
u-boot没有中断驱动的热插拔机制,它的USB状态只在命令执行时更新。也就是说,你先执行了usb start,然后插上U盘,再执行fatload,大概率失败。正确的做法是每次插入U盘后重新执行一次usb reset或者完整usb start。
量产烧录的经验,我也分享一下:
- U盘质量参差不齐,主控兼容性是最大的变量。建议备几块不同主控的U盘做验证,不要只测一款。
- 线材越短越好。U盘插在板卡USB口上用短延长线或直接插,不要通过前置面板的长线转接,高速信号对线缆质量很敏感。
- 供电要稳。如果板卡从USB口给外设供电,后级USB Hub或者大电流U盘可能倒灌拉低VBUS,导致枚举失败。量产工装建议用独立供电的Hub。
- 固件文件命名固定。比如统一叫
firmware.img,脚本里写死,减少工人手动操作出错。
其实在这个场景里,u-boot的开发体验很像在做一个“极简操作系统”——没有中断、没有守护进程、没有高可用机制,一切靠命令触发。理解了这一点,很多奇怪的毛病就能从根上想明白。
6.3 从host读U盘到gadget设备的扩展思路
读完U盘只是u-boot USB能力的一半。如果你想让PC把板子识别成一个U盘,往里面拷文件,走的是完全相反的方向——USB gadget。u-boot里对应的命令是ums:
ums 0 mmc 1这条命令把板子的eMMC作为一个U盘挂给PC,在调试和生产加载数据时非常有用。但要记住:一个USB控制器同一时刻只能工作在host或gadget一种模式,设备树里的dr_mode需要在启动前定好。如果你在调host模式读U盘,就别同时期待这个口还能当gadget用,除非控制器和OTG检测都支持动态切换。
我觉得u-boot读U盘这件事,掌握了之后最大的收益不是“能load文件了”,而是你会真正理解一条存储链路从硬件到文件系统要走多少环节。后面再遇到USB打印机、USB网卡、USB摄像头这些设备的驱动移植,本质上都是同一套枚举和设备绑定逻辑,只是中间的类驱动不同。
最后分享一个我调节了无数次之后的心得:u-boot里调试USB,先看线,再量电压,然后用usb tree分层次,最后才怀疑代码。这四步走完,绝大多数问题都能定位到具体某一层,剩下的就是改配置或者换设备的问题了。