ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

U-Boot USB链路全解析:从控制器初始化到fatload加载内核

U-Boot USB链路全解析:从控制器初始化到fatload加载内核 前段时间给一块新板子移植 u-boot折腾到凌晨两点卡住的不是 DDR 频率也不是网络驱动反而是最不起眼的 USB 口——插上 U 盘执行usb start控制台一片寂静仿佛这个接口根本不存在。后来排到根因其实很简单defconfig 里压根没开 EHCI 的配置控制器根本没工作。那一刻我突然意识到很多人包括当时的我对 u-boot 的 USB 链路理解是断层的知道fatload usb 0:1能读 U 盘但中间到底经过控制器初始化、协议枚举、存储扫描、文件系统解析这几道关卡每一层都可能让整个流程静默失败。这篇文章我准备把这整条链路拆开讲清楚。从 USB 控制器怎么被初始化到usb start命令内部做了什么再到fatload从 U 盘里把内核读进内存时的文件系统操作最后附上我这几年排查U 盘在 u-boot 里不识别的完整思路。适合正在做板卡移植、u-boot 定制或者被 U 盘启动问题折磨的嵌入式开发同学参考。1. u-boot 里为什么要折腾 USBU 盘是最朴素的救急通道很多做应用层开发的同学可能不理解引导加载阶段为什么非要跟 USB 较劲。大家更熟悉的是在 PC 上用 Rufus、Ventoy、软碟通这些工具制作 Windows 启动盘那是 UEFI/BIOS 时代的故事。而在嵌入式世界里u-boot 本身就是一套微型引导系统U 盘则是它的可移动启动介质——这个组合在几个场景里几乎是不可替代的。第一是烧写系统。板子出厂时 Flash 里可能是空的也可能是旧版本。传统做法是通过网络 tftp 下载镜像再写进 NAND/eMMC但现场往往没有网线环境或者网络驱动还没来得及适配。一个 Fat32 格式的 U 盘拷好几个镜像文件插上板子一条fatload就能把镜像搬进内存再配合mmc write或nand write就能完成整机烧写。量产工厂里我见过的大多数方案都是这个套路简单粗暴而且可靠。第二是内核/设备树临时加载。开发阶段内核镜像频繁改动每次编译完都烧进 Flash 太浪费时间。U 盘加载只需把新内核拷进去重启后从 U 盘引导调试循环从烧写 重启降级成插拔 U 盘效率提升不是一点半点。第三是急救。系统启动到一半挂了网络也没配好u-boot 是最后一道防线。此时只要 u-boot 的 USB 是好的就能从 U 盘把备用内核拉起来把机器救回来。所以 USB 在 u-boot 里不是锦上添花的功能它是一整套可靠运行机制的底座。理解了这一点你就明白为什么值得花力气把这条链路调通。2. USB 子系统完整链路拆解一次 fatload 请求到底经过了几层先画个宏观图景。当你敲下fatload usb 0:1 0x80800000 uImage这行命令时数据从 U 盘的 Flash 颗粒到板子的 DDR 内存中间经历了四层控制器Controller、USB 协议栈Protocol Stack、存储子系统Storage、文件系统Filesystem。每一层都有自己的初始化入口和数据结构任何一层出了问题表现都是读不到文件。2.1 控制器、HCD 与协议栈的分工控制器就是 SoC 内部那个 USB 硬件 IP常见的有 EHCIUSB 2.0 高速480Mbps、OHCIUSB 1.1 全速/低速12Mbps/1.5Mbps、xHCIUSB 3.0 超速5Gbps以及各类 OTG 控制器如 Synopsys DWC2/DWC3。不同控制器的寄存器布局、描述符格式、传输机制差别很大u-boot 通过 HCDHost Controller Driver层把它们统一抽象成一套接口。举个例子i.MX6 系列常用的是 Chipidea 的 OTG 控制器u-boot 里走CONFIG_USB_EHCI_MX6这个驱动它在 EHCI 框架下实现了平台相关的 PHY 初始化和时钟使能。而全志 H3 用的是ehci-sunxi树莓派和 STM32MP1 则是 DWC2。控制器驱动要做的事包括使能时钟和电源、复位控制器、配置 PHY 类型、初始化 EHCI/xHCI 的能力寄存器结构以及在 root hub 上注册端口。协议栈这层是 u-boot 自己实现的精简版 USB 核心代码主要在drivers/usb/core/。它负责总线枚举、设备地址分配、配置描述符解析、端点管理。这套核心不依赖具体控制器而是通过struct usb_device这个核心数据结构跟 HCD 交互。你可以把它理解为USB 设备接入时的接待处——不管你是 U 盘、键鼠还是 USB 网卡都得先在这里完成身份登记才能进入后续的类驱动处理。2.2 存储子系统的伪装U 盘在 u-boot 眼中是什么U 盘在 USB 协议里的正式身份是 Mass Storage 类设备传输采用 Bulk-Only TransportBOT协议。什么意思呢控制传输用来发命令数据则通过 Bulk 端点成块搬运。u-boot 的usb_stor驱动会向 U 盘发送 SCSI 命令u-boot 里实际用的是精简过的 UFI 命令集比如INQUIRY询问设备信息READ CAPACITY获取扇区总数和扇区大小READ(10)读取指定 LBA 的数据。这里有个嵌入式开发者容易忽略的点U 盘在 u-boot 里并不直接挂载文件系统它先被抽象成一个块设备。usb_stor_scan扫描到设备后会注册一个struct blk_desc这个结构体描述了设备号、扇区大小、总扇区数以及读写回调函数。后续fatload读取文件时实际上是先通过这个块设备接口按扇区读取数据再交给文件系统层去解析。所以你在usb storage命令的输出里看到Device 0: Vendor: Samsung Rev: 1.00 Prod: Flash Drive这类信息其实主机还没有读取任何文件只是完成了块设备注册。文件系统是块设备之上的另一层东西。2.3 FAT 层fatload 最终是怎么找到文件的u-boot 的文件系统层通过fs/fs.c里的统一接口对外提供服务。fatload命令实际调用路径是解析参数后调用file_fat_read最终进入fs/fat/fat.c的fat_read_file。FAT 文件系统的读取逻辑可以分为三步。第一步读引导扇区DBR从 BPB 参数里拿到每扇区字节数、每簇扇区数、保留扇区数、FAT 表个数和位置、根目录起始簇这些关键元数据。第二步从根目录开始逐级查找目录项——每个 32 字节的目录项里记录了文件名、属性、首簇号、文件大小长文件名LFN则通过 0x0F 属性的特殊目录项拼接出来。第三步沿着 FAT 表找下一个簇号按簇为单位读数据直到 EOF。这一步有一个性能上的冷知识u-boot 的 FAT 实现是一个簇一个簇往下读的每读一个簇都要向 U 盘发一次READ(10)命令读取几十 KB 的文件可能涉及几十次 USB 传输。所以 Flash 速度慢的 U 盘在 u-boot 里加载大内核镜像会很吃力这是正常现象不是代码有问题。3. 控制器初始化实战配置项、设备树、上电时序一个都不能少移植 u-boot 的 USB 支持最容易踩的坑就是只开了协议栈没开控制器驱动。配置项之间不是自动关联的你必须在编译前把整条链路的开关全部打开。3.1 defconfig 里的配置项组合以 i.MX6 平台为例最小可用的 USB Host fatload配置组合大致是这样# USB 核心与协议栈 CONFIG_USBy CONFIG_USB_STORAGEy CONFIG_CMD_USBy # EHCI 控制器框架与平台驱动 CONFIG_USB_EHCI_HCDy CONFIG_USB_EHCI_MX6y # FAT 文件系统命令 CONFIG_CMD_FATy CONFIG_FAT_WRITEyDWC2 平台的典型组合则是CONFIG_USB_DWC2y配合CONFIG_USB_STORAGEy。如果你用的是较新的 DMDriver Model框架还需要确认CONFIG_DM_USBy并且设备树里要有对应的 usb 节点老式 non-DM 代码则依赖 board 文件里的board_ehci_hcd_init这类回调函数来完成平台初始化。我踩过的坑是只配了CONFIG_CMD_USBy而漏掉CONFIG_USB_EHCI_HCDy结果usb start执行后命令存在内部却因为找不到控制器驱动而直接返回错误。所以每次改完配置建议先执行make menuconfig检查依赖关系再编译烧录。3.2 设备树里的 USB 节点怎么填DM 模式下设备树对 USB 的影响非常直接。常见 usb 节点长这样usbotg1 { status okay; dr_mode host; vbus-supply reg_usb_otg1_vbus; };dr_mode这里特别关键。如果 SoC 的 OTG 控制器没有正确配置为 host 模式控制器会工作在 peripheral 状态U 盘插上去毫无反应。有些平台还需要在节点里显式引用 PHY比如phy usbphy1并保证 PHY 的时钟和供电节点都已经打开。这里多说一句u-boot 使用的设备树跟 Linux 内核的设备树是两份虽然通常同源但 u-boot 只取自己需要的节点。修改完设备树后要确保重新编译了 dtb 并烧写到了 u-boot 实际加载的位置否则你改的设备树根本没生效——这种配置了但没生效的假象非常容易迷惑人。3.3 时钟与供电时序硬件上最容易踩的坑软件配置齐全但 U 盘就是不识别问题往往出在时序和供电。USB PHY 需要稳定的参考时钟通常是 24MHz而且从时钟使能到 PHY 正常工作有建立时间部分 SoC 的 USB 控制器在复位后需要等待一段时间才能开始枚举。VBUS 供电也是大坑。U 盘正常工作需要 5V 电源如果板子上的 VBUS 由 GPIO 控制必须在枚举前把 GPIO 拉高并等待 VBUS 稳定。我在一块板子上遇到的现象是usb start有时成功有时失败后来发现是 GPIO 控制 VBUS 后立即枚举电压还没爬升到 5VU 盘主控根本没上电自然枚举不到。解决方式是延时 100ms 左右再执行usb start或者在 u-boot 的 board 初始化函数里提前把 VBUS 拉高。4. usb start 命令背后的完整枚举流程U 盘是怎么被认出的当你执行usb start时u-boot 做的事情远比表面上看起来复杂。它先遍历所有控制器并复位总线然后扫描 root hub 的每个端口检测是否有设备连接。检测到设备后进入usb_new_device这个核心枚举函数。4.1 枚举核心步骤与 u-boot 代码路径标准 USB 枚举流程在 u-boot 里的实现大致是以下几步对端口执行复位信号usb_reset_port让设备回到默认地址 0。通过控制传输读取设备描述符的前 8 字节拿到bMaxPacketSize0这是端点 0 的最大包长度后续传输依赖这个参数。发送SET_ADDRESS控制请求给设备分配一个非 0 的总线地址。用新地址重新读取完整的设备描述符18 字节获得idVendor、idProduct、USB 协议版本等关键信息。读取配置描述符包括配置总数、接口数、端点描述符并选择合适的配置。发送SET_CONFIGURATION激活设备然后判断设备类型如果是 hub 就递归扫描子端口如果是 mass storage 就调用usb_stor_scan注册块设备。对应到代码这些逻辑集中在drivers/usb/core/usb.c核心函数是usb_new_device。枚举过程中任何一步出错u-boot 会打印错误信息并跳过该设备。这也是为什么很多 U 盘usb tree 能看到但 storage 扫描不到多半是GET_MAX_LUN或INQUIRY命令被 U 盘返回了错误。4.2 速度协商与端点参数的影响USB 2.0 高速设备在连接时会通过 chirp 序列跟主机握手确定以 480Mbps 还是 12Mbps 工作。u-boot 的 EHCI 驱动支持高速、全速设备混插。但这里有一个兼容性细节一些老旧的 U 盘主控对 chirp 时序要求严格如果 PHY 配置不对握手会失败设备会退化成全速模式读取速度大幅下降。如果你发现同样一个 U 盘在 Linux 下读写 30MB/s在 u-boot 里却慢得离谱就应该怀疑速度协商出了问题。端点的MaxPacketSize也影响传输效率。高速 Bulk 端点最大包是 512 字节全速只有 64 字节。U 盘主控参数里如果把这个值设成了 64主机就只能按 64 字节的包跟它通信fatload读大文件时性能会差一个数量级。4.3 调试命令 usb tree / usb storage 的读法枚举完成后的调试命令非常有用。usb tree会显示当前总线上的设备拓扑比如USB device tree: 1 Hub (480 Mb/s, 0mA) | u-boot root hub | -2 Mass Storage (480 Mb/s, 200mA) Samsung Flash Drive 1.00这个输出意味着枚举链路是通的。接着执行usb storage查看块设备注册情况输出里会显示 Vendor、Prod、Rev、容量信息。如果这两条命令都有输出说明 USB 子系统和存储子系统都正常问题就集中到文件系统层了。5. fatload 实战加载内核、设备树与根文件系统的完整流程fatload命令的完整形式是fatload interface dev[:part] addr filename [bytes]最常见的是fatload usb 0:1 0x80800000 uImage。其中usb是接口名0:1的语义是USB 控制器 0 上找到的第 0 个存储设备第 1 个分区。这个编号规则经常被误解——0并不是盘符它是usb_stor_scan注册设备时的顺序号1是分区号从 1 开始计数。如果你只有一个 U 盘且只有一个分区那无论插在哪台机器上在 u-boot 里通常都是0:1。5.1 参数逐项拆解与实践注意事项如果 U 盘有多个分区0:2表示同一块 U 盘的第二个分区如果插了两个 U 盘第二个就是1:1具体编号取决于控制器注册顺序。这个顺序不是由 USB 物理端口决定的而是由枚举先后决定的所以多 U 盘环境里最好固定插拔顺序或者用usb storage确认编号后再执行加载。地址参数0x80800000要落在 DDR 的有效地址范围内并且要避开 u-boot 自身代码、堆栈以及设备树 blob 的占用区域。我见过有人把加载地址设得跟 u-boot 自身重叠加载没问题但启动时内核一解压就把 u-boot 覆盖了系统直接崩溃。做法是查阅对应 SoC 的 memory map选一个安全的加载地址比如 i.MX6 常用0x12000000放内核、0x18000000放设备树。5.2 一套可用的从 U 盘启动脚本实际项目里不会手动敲命令而是通过环境变量设置自动启动脚本。我这里给出一套比较通用的流程setenv bootargs consolettymxc0,115200 root/dev/ram0 rw setenv loadkern usb start; fatload usb 0:1 0x12000000 uImage setenv loaddtb fatload usb 0:1 0x18000000 imx6q-board.dtb setenv bootcmd run loadkern; run loaddtb; bootm 0x12000000 - 0x18000000 saveenv这套脚本的核心思想是把准备 U 盘和加载文件拆成两个环境变量便于单独调试。加电后 u-boot 会自动执行bootcmdusb start会重新枚举 U 盘然后依次把内核和设备树读进内存最后bootm启动内核。注意bootm中间的-表示跳过 initrd直接用设备树启动。如果需要加载 initramfs命令变成bootm 0x12000000 0x13000000 0x18000000其中第二个地址是 initramfs 镜像的内存位置第三个是设备树位置。5.3 扩展fatls / fatinfo / fatwrite 与常见失败除了fatloadu-boot 的 FAT 命令还提供几个相当实用的工具。fatls usb 0:1 /可以列出根目录文件调试时验证文件是否真的在 U 盘上、文件名是否大小写匹配。fatinfo usb 0:1显示文件系统类型和容量信息用于确认分区格式是不是 FAT32。fatwrite usb 0:1 0x80800000 app.bin 0x10000则可以把内存数据写回 U 盘这在需要把系统运行日志导出到 U 盘的场合非常有用前提是开启CONFIG_FAT_WRITE。常见失败场景里最坑的是 FAT32 的兼容性边界。FAT32 分区在 Windows 下格式化时如果分配单元大小选得过大或者分区超过 32GB 之后某些格式化工具创建的参数异常u-boot 的 FAT 解析可能直接报Unable to read file或Invalid FAT entry。遇到这种情况最简单有效的办法是重新用默认参数格式化 U 盘或者改用小容量 U 盘。另一个常见问题是文件名超过 8.3 格式但没开启长文件名支持或文件名大小写不符——FAT 在 Linux 下是大小写不敏感的但 u-boot 的匹配实现比较死板建议务必将文件名与脚本中的大小写保持一致。6. 排查usb start 找不到存储设备的完整思路这个章节可能是最有实战价值的。遇到问题不要慌按照链路一层层切能把定位时间从瞎试一天压缩到半小时内。6.1 先通过输出日志判断断点在哪u-boot 的 USB 驱动在枚举过程中会打印关键信息但默认 log 级别可能不显示细节。第一步是观察usb start本身的输出连设备信息都不打印大概率掉在控制器初始化或枚举非常早的阶段优先查配置项、设备树、时钟供电。打印了设备信息但usb storage为空枚举成功了但 mass storage 类驱动注册失败查CONFIG_USB_STORAGE是否开启以及 U 盘的 SCSI 命令响应是否异常。usb storage有设备但fatload说找不到文件问题在文件系统层查分区格式、文件路径、文件名大小写。这一步能把范围缩小一大半。6.2 物理层排查线缆、供电、延长线一个都不能小看USB 在嵌入式板子上对电气参数极其敏感。我实测过很多次同一块板子用 USB 3.0 蓝色线材插 U 盘引导失败换成一根短粗的 USB 2.0 线立刻就正常。原因通常是线缆压降——U 盘启动瞬间电流可达 100mA 以上劣质线材的电阻导致 VBUS 跌落到 4.5V 以下U 盘主控进入欠压保护。还有一个高频坑是 USB Hub。如果 U 盘接在 Hub 后面Hub 的供电能力不足或者 Hub 自身枚举失败都会导致后面设备消失。排查时建议先直连板子的 USB Host 口排除 Hub 因素。嵌入式板子的 USB 口通常供电能力有限对于机械移动硬盘这类大功耗设备最好使用带外部供电的 Hub 或者单独供电的 U 盘。6.3 兼容性层U 盘主控与量产工具同一块板子U 盘 A 识别不了U 盘 B 一切正常这种案例非常常见。原因在于不同品牌 U 盘使用的主控芯片千差万别某些主控对嵌入式主机控制器的兼容性不好。这里有个实用思路用 ChipGenius芯片无忧在 PC 上查看 U 盘主控信息然后选择主控兼容性更好的品牌作为量产工具盘。量产工具不仅能查看主控信息还能重刷 U 盘固件。我遇到过一块 U 盘在 u-boot 下枚举一直超时用主控对应的量产工具重新格式化并修复固件后问题消失。顺带一提如果 U 盘在 PC 上出现容量从 8G 变成 300M这类异常也往往是主控固件损坏用量产工具恢复出厂状态通常能救回来。嵌入式现场多备几块不同主控的 U 盘是基本功。我一般随身带三块一个三星主控的、一个慧荣主控的、一个群联主控的交叉验证问题到底出在板子还是出在 U 盘。6.4 日志开启位置与最终检查清单如果物理和配置都排查完了还是不行就该打开 u-boot 的 USB 调试日志了。涉及的关键宏有USB_EHCI_HCD框架下的CONFIG_USB_EHCI_HCD配合DEBUG定义以及CONFIG_USB_GADGET_DEBUG如果涉及 device 模式。你可以在对应驱动文件的头部临时加#define DEBUG编译后用串口工具观察详细输出。调试嵌入式 u-boot 有一根 USB 转 TTL 串口线是必需品——注意电平匹配3.3V 的板子用 3.3V 的模块别直接拿 5V 的 TTL 去怼。我把最终的检查顺序整理成一张表遇到问题按这个顺序过一遍层面检查要点常见症状配置CONFIG_CMD_USB / CONFIG_USB_EHCI_HCD / CONFIG_USB_STORAGE命令存在但执行报错或无输出设备树usb 节点 status、dr_mode、phy、clockusb start 后端口无设备供电时序VBUS 电压、GPIO 时序、PHY 建立时间时好时坏、高温下必失败物理层线材、Hub、接口氧化、公母头换根线就好插拔后恢复协议层U 盘主控兼容性、枚举时序、MaxPacketSize特定 U 盘失败、速度异常低文件系统FAT32 分区参数、文件名大小写、目录路径fatload 报文件不存在7. 最后分享一点个人经验我实际工作中调 u-boot USB 最大的体会是慢一点、稳一点。每次改完配置先做最小验证usb startusb tree确认枚举通了再写脚本每次换 U 盘先看主控型号每次怀疑硬件之前先怀疑线材。这套方法论帮我避免了无数次无效调试。另外一个小技巧做 u-boot 启动盘时U 盘容量不是越大越好。16GB 以下的旧款 U 盘在 FAT32 兼容性和枚举稳定性上往往优于新的大容量盘尤其是那些 USB 2.0 的老盘在 u-boot 里反而最可靠。如果你在维护多条产品线建议把测试通过的 U 盘型号清单作为文档沉淀下来这比任何优化代码都省时间。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进