简介:围绕芯片从上电复位到操作系统启动的完整链路,这份 docx 文档以 ROM Code、SPL、U-Boot、Kernel 四个阶段为主线,梳理各阶段的职责边界与衔接关系,面向从事嵌入式驱动开发、Bootloader 移植和系统启动调试的初中级工程师,也适合准备面试或补强底层知识的学生。文档结合 SD 卡、UART、USB、eMMC 等常见启动方式,说明镜像如何从存储介质搬运到片内 SRAM 与 DDR 中运行,并对比 ptset 测试模式与下载模式的处理差异;其中对 ROM Code 初始化、SPL 分批搬运、U-Boot 加载内核到 DDR、Kernel 进入用户空间的顺序描述较完整,可用于排查启动卡死、镜像加载失败、内存搬运异常等问题。压缩包内为 1 个 docx 文件,约 1.41MB,内容集中、便于通读与整理笔记。已有 923 人学习下载,可作为理解芯片启动流程的入门到进阶参考。
1. 上电之后最先跑的是什么:从复位向量到 ROM Code 的第一条指令
芯片启动 boot 这件事,很多人调板子时只盯着串口有没有打印,真正翻车的地方往往在最前面那几百毫秒。上电、复位、CPU 从固化地址取第一条指令,到屏幕上出现第一个字符,中间要穿过 ROM Code、SPL、U-Boot、Kernel 四道门,而且每道门失败的样子完全不同:有的连串口都不通,有的打印到一半就断,有的明明显示成功却在 DDR 里跑飞。这份《芯片的 boot 过程》文档把四段链路连同 USB/UART 下载、SD/eMMC 启动、ptset 测试模式几条分支一起拆开了,适合正在做 bring-up 的底层软件工程师、需要定位启动失败的系统工程师,以及刚接触 SoC 启动流程想建立整体图的人。后面我不复述文档目录,而是沿着它给的骨架,把每一段能动手的部分补成可执行的步骤和判断依据。
2. ROM Code 阶段:CPU 初始化、系统初始化与启动介质判定
ROM Code 是整条链路上唯一改不了的一段,它被烧在芯片内部的掩膜里,出厂即定稿。正因为它改不了,它的设计取向非常明确:只做“不做后面就完全动不了”的事,凡是以后可能变的东西全部甩给 SPL。理解这一点,后面所有奇怪现象都好解释了——ROM 阶段能打印的字符极少、能读的介质有限、能用的内存只有片内那一小块,出了问题的表现自然就是“安静得像没上电”。
2.1 复位向量落在哪:handler 模式与 CPU 初始化的实际动作
上电复位后,CPU 从芯片内部固化的复位向量取指,进入特权模式(不少架构里叫 handler 模式,也有叫 supervisor 或 machine 模式的),这条 reset 异常是所有事情的起点。此刻 MMU 没开、cache 状态未知、DDR 控制器没初始化、PLL 还在默认频点,整个系统里唯一能确定可用的内存就是片内 SRAM,容量通常几十到几百 KB。ROM Code 的 cpu init 就在这个环境里展开:先把栈指针指到片内 SRAM 的一段保留区,再清掉可能残留的中断挂起状态,把异常向量表准备好,然后把主 PLL 拉到能跑的目标频率,同时打开后面要用的那几个外设时钟门控。
这套动作之所以要做得这么“抠”,是因为 ROM 的容量是按 KB 计的,多一个功能就意味着多一份掩膜成本,而且一旦流片就再没有反悔的余地。所以 ROM 里通常不会做 DDR 训练、不会解析文件系统、不会做复杂的容错重试,它只保证“能把下一段代码搬进 SRAM 并跳过去”。这也解释了一个常见疑惑:为什么有些板子换一颗 DDR 颗粒就必须改 SPL,而 ROM 完全不用动。
判断 ROM 有没有跑起来,最直接的办法是抓串口。ROM 阶段的打印量很少,但通常会有介质类型和搬运长度这两条关键信息:
# 冷启动时串口抓到的 ROM 阶段日志(115200 8N1,无硬件流控) [ROM] boot media: SD [ROM] load spl 0x00000000 -> 0x0000A000, len 0x11800 [ROM] jump 0x0000A000这段日志的价值在于三个参数:boot media告诉你 strap 判定成了哪种介质,如果这里显示的不是你预期的那一项,后面所有分析都白做;len是 ROM 决定的搬运长度,它一般由镜像头部字段给出,也不是随便定的;最后一个jump地址就是 SPL 的入口,和你链接脚本里的ORIGIN必须一致。抓不到任何字符时,先别怀疑芯片坏了,把波特率、电平标准、TX 引脚复用这三件事各核一遍,血泪经验里至少一半的“芯片不上电”最后都是这三个原因。
2.2 System Init:键盘、UART 这些“小模块”为什么必须先配
文档里那句“主要完成键盘/UART 等需要的对应模块的初始化配置”容易被一眼扫过去,实际上它是 ROM 阶段最实的一块工作。这里的“键盘”一般指按键扫描控制器,用途是检测强制进入下载模式的按键组合——板子上电时按住某个键,ROM 读到这个电平就直接跳过正常的介质启动,转去等上位机下发镜像,这是救砖通道的硬件入口。UART 则要配引脚复用、时钟源、波特率分频,很多 SoC 在 ROM 里用的是内部 RC 时钟,精度有限,所以早期串口出现少量乱码属于正常现象,等 SPL 把 PLL 切过去之后就干净了。
下表是我整理这类阶段时惯用的记录格式,把“配了什么”和“为什么必须现在配”写在一起,后面排查时对着看比翻手册快得多:
| 模块 | 典型配置项 | 为什么必须在 ROM 阶段完成 |
|---|---|---|
| 引脚复用 | UART TX/RX、SD 卡 CMD/CLK/DAT0-3 | 复用不配好,后面的控制器配了也出不了信号 |
| 时钟 | 主 PLL 倍频、外设门控、UART 分频 | SPL 的代码要靠时钟跑起来,串口打印也依赖它 |
| 按键扫描 | 扫描周期、去抖、组合键判定 | 决定是否强制进入 UART/USB 下载模式 |
| SD/eMMC 控制器 | 总线宽度、初始时钟(低速)、电压 | ROM 只做单块读取,高速模式留给 SPL |
| 看门狗 | 关闭或延长超时 | 否则 SPL 稍慢一点就被复位,表现为反复重启 |
这张表里最容易被忽略的是看门狗那一行。不少板子第一次上电时表现为“串口打印两行然后重来”,看日志像是 SPL 崩了,实际上是 ROM 打开的看门狗没被 SPL 及时喂上,到点直接复位。碰到这种节律性很强的重启,先怀疑看门狗,比一行行看代码快得多。
2.3 启动介质判定:SD/eMMC、USB/UART 与 AON SRAM 搬运窗口
介质判定通常由 strap 引脚电平、eFuse 里的一次性配置、以及前面说的按键组合共同决定,优先级各家不同,但常见顺序是 SD 卡优先、其次 eMMC、然后才是 UART/USB 下载。文档给的 SD 卡路径最典型:ROM 配好 SD 控制器后,按固定偏移读取卡上的头部扇区,校验 magic 和长度字段,再把指定长度的内容通过 DMA 搬到 AON SRAM 的固定基址,最后关掉自己的异常,跳到 SRAM 入口。
AON 是 always-on 域,这块 SRAM 在深睡时也不掉电,ROM 选它做落脚点是因为它在最早期就可用、不需要 DDR 参与。代价是容量小,所以 ROM 只会搬有限长度——文档里给的是 70KB 这个量级。这个数字不是随便定的,它等于“片内可用 SRAM 减去 ROM 自己占用的栈和变量区”之后剩下的窗口,改链接脚本时这个上限就是硬约束。
把这一段拆成可执行的检查步骤,顺序如下:
- 确认 strap 或按键把介质固定成 SD,避免板子跑到别的分支上。
- 用读卡器把卡的前若干个扇区 dump 出来,确认头部 magic 和长度字段符合 SoC 手册的定义。
- 用示波器或逻辑分析仪看 CMD 线上有没有初始化时序,确认 ROM 真的去读了卡。
- 对照串口日志里的跳转地址和链接脚本的 ORIGIN,两者必须一致,差一个字节都会跑飞。
# 把卡的前 512 字节 dump 出来,核对镜像头(偏移按具体 SoC 手册改) dd if=/dev/sdX of=card_head.bin bs=512 count=1 hexdump -C card_head.bin | head -n 8if=换成你实际的块设备名,count=1只读一个扇区,不要图省事整卡 dump。头部里通常有 magic、镜像长度、校验和、入口地址这几个字段,magic 对不上说明烧录偏移错了,长度对不上说明打包脚本算错了大小,这两种错在现场占的比例最高。
2.4 ptset 测试模式:一条容易被误触发的旁路分支
ptset 是产线测试模式,进入后不走正常的启动判定流程,而是等上位机下测试指令,做引脚连通性、内存读写、功耗这些项目。它的存在让整个 boot 流程复杂了一档,因为 strap 组合和正常启动很接近,产线上用的治具和开发板上的电阻配置稍有出入就可能误进。误进之后的现象特别有迷惑性:串口有反应,甚至能看到提示符,但就是不启动系统,新手很容易往 kernel 或文件系统方向去找,越找越偏。
我一般的判断顺序是:先看串口那一行提示的具体内容,测试模式的提示和正常 ROM 日志措辞完全不同;再用万用表量 strap 电阻,确认和原理图标注一致;最后检查板上有没有预留的测试点被短接或焊锡粘连。第三点在返修板上出现过不止一次,量一遍五分钟,比翻一下午代码划算。文档把 ptset 单独列成一条分支是有道理的,它不属于启动链路,但它是启动失败的常见干扰源,脑子里有这根弦,遇到“能打印但不启动”时会先想到它。
3. SPL 阶段:70KB 片内 SRAM 里怎么装下一个加载器
ROM 跳过去之后,控制权就到了 SPL。这一段的本质是一道容量数学题:片内 SRAM 只有 70KB 级别的可用窗口,完整的 U-Boot 动辄几百 KB 到 1MB,kernel 更是几 MB 起,谁都不可能塞进去。于是只能分两段走——先用 70KB 把 DDR 点起来,再把剩下的东西搬进 DDR 跑。所有 SPL 的代码组织、链接脚本、裁剪策略,都是围着这道数学题转的。
3.1 为什么必须有 SPL:容量鸿沟与两段式搬运
片内 SRAM 快、常开、不需要训练,但小且贵;DDR 大、便宜,但要初始化、要训练时序、跑起来之前不能直接用。SPL 的定位就是把“能操作 DDR 的最小代码集”塞进片内 SRAM,跑起来之后立刻初始化 DDR,然后把更大的镜像搬到 DDR,自己功成身退。文档里那句“先搬运 SD 卡前 70KB 的程序过来进行运行,然后运行配置完毕后再搬运剩下的 sd 内容(kernel)到 ddr 中进行运行”,说的就是这个两段式结构。
这里有个容易混淆的点:第二段搬的到底是 U-Boot 还是 kernel,取决于你的镜像组织方式。常见做法是 SPL 搬完整的 U-Boot 到 DDR,U-Boot 再搬 kernel;也有把 U-Boot 的后续阶段和 kernel 打包在一起、由 SPL 一次搬完的省事做法。不管哪种,判断依据都是 SPL 结尾那一次跳转的目标地址落在哪:落在 DDR 区间说明搬的是后续阶段,落在 SRAM 说明还在片内。查这个比读文档快。
两段式带来的直接后果是启动时间被拉长了一截,尤其是 SD 卡在低速模式下的单块读取,每一块都是几十微秒起。所以量产版本通常会把 SPL 里的 SD 时钟尽快切到高速模式,同时把镜像做紧凑,去掉用不上的驱动。这块的优化空间比很多人想的大,我见过把 SPL 阶段的读卡时间从 200ms 压到 40ms 的改法,动作无非是提高时钟、加大单次读取块数、减少重复校验。
3.2 70KB 是怎么切出来的:链接脚本与段布局
70KB 不是目标,是上限。SPL 编译出来的所有段——代码、只读数据、已初始化数据、BSS、栈——全部要落在这个窗口里,溢出一点,链接时不一定报错,跑起来就在别人的地盘上踩踏,表现为随机死机或打印乱码,属于最难查的一类问题。我的习惯是在链接脚本里直接写 ASSERT,让它在编译期就把问题暴露出来:
/* SPL 链接脚本的关键约束:所有段必须落在片内 SRAM 窗口内 */ MEMORY { sram (rwx) : ORIGIN = 0x0000A000, LENGTH = 0x11800 /* 70KB 搬运窗口 */ } SECTIONS { .text : { *(.text.entry) *(.text*) } > sram .rodata : { *(.rodata*) } > sram .data : { *(.data*) } > sram .bss (NOLOAD) : { *(.bss*) *(COMMON) } > sram _spl_end = .; ASSERT(_spl_end - ORIGIN(sram) <= LENGTH(sram), "SPL exceeds SRAM window") }ORIGIN必须和 ROM 日志里那个跳转地址完全一致,LENGTH必须和 ROM 实际搬运的长度一致,两个数任何一个对不上,第一跳就飞了。*(.text.entry)单独拎出来放最前面,是为了保证入口函数一定在窗口起始位置,不依赖链接顺序的运气。BSS 段标NOLOAD表示它不占镜像体积,但运行时必须由启动代码手动清零——这一步漏了,全局变量初值就是随机的,表现出来是“同一份固件有时候能起有时候不能起”的玄学问题。
SPL 里还有几条硬规矩值得写进注释:不要用malloc,不要开大数组,不要在栈上放缓冲区,不要在中断里做耗时操作。这几条不是风格问题,是 70KB 窗口逼出来的生存法则。真需要缓冲区,就借用 DDR 初始化之后的那一段空间,并且明确它不属于 SPL 的链接范围。
3.3 搬运链路:从 SD 卡到 AON SRAM 再到 DDR
把整条搬运链路按时间顺序摊开,大致是这么几步:ROM 读卡头部,按长度搬前 70KB 到 SRAM;SPL 起来后先做最简的串口和时钟初始化,接着初始化 DDR 控制器并训练时序;训练通过后,按镜像头里记录的剩余长度,把后续内容分块读到 DDR 的目标地址;全部读完做一次校验,通过后跳到 DDR 入口。每一步失败,现象都不一样,这也是排查能分段的原因。
打包环节经常被低估。手工拼镜像容易出现“切出来的长度和头部写的长度不一致”,ROM 校验不过就直接不启动,串口一片安静。我一般用脚本切,切完立刻核对大小:
# 从完整镜像里切出前 70KB 单独烧到卡的裸偏移上 dd if=firmware.img of=spl.bin bs=1024 count=70 ls -l spl.bin # 确认切出来的头部 magic 与长度字段 hexdump -C spl.bin | head -n 4bs=1024 count=70就是 70KB,改窗口大小时这两个数要一起改,只改一个必然出错。切完用ls -l看一眼实际字节数,如果不到 71680,说明源镜像本身就不够长,这时候该查的是打包脚本而不是烧录工具。烧录偏移也要对齐到块边界,多数 ROM 只认按块对齐的读取,偏移错一个字节,读到的头部就是垃圾。
3.4 SPL 的验证点:打印、时钟与 DDR 训练
SPL 是整条链路上第一个“你能改”的阶段,所以它也是最好的观测点。我在 SPL 里通常会埋三类探针:第一类是早期串口打印,只输出几个字符,用来确认代码真的跑起来了,注意这时候 PLL 可能还没切,波特率不一定准,字符乱码也算成功;第二类是 GPIO 翻转,在关键节点拉高拉低,用示波器看时序,比串口更可靠,因为它不依赖任何通信协议;第三类是 DDR 训练结果的回读,把训练得到的延迟参数打出来,后面调稳定性时这就是基线。
DDR 训练通过不代表 DDR 稳。SPL 说“初始化成功”,只能说明那一次读写校验过了,温度变化、电压波动、批次差异都可能让边缘参数失效。所以量产版本一般会把训练参数写进保留区,下次启动直接复用,省时间也省得每次都在边缘试探。这块我踩过的坑是:只做一次训练、不做回读校验,结果一批板子里有百分之几在高温下启动失败,返修时才发现训练参数根本没落盘。
另一个常被忽略的验证点是搬运完整性。SPL 把内容读到 DDR 之后,最好对整段做一次 CRC 或哈希比对,和镜像头里存的校验值对齐。多花的那几十毫秒,换来的是“DDR 里的内容和卡上的内容一致”这个确定性,后面 U-Boot 跑飞时可以直接排除搬运错误这个方向。没有这一步,排查会变成两头猜。
4. U-Boot 与 Kernel 交接:DDR 里的接力棒怎么递
到 U-Boot 这一层,环境宽松多了:DDR 可用、驱动齐全、命令行可交互,出问题也能停下来看。但宽松不等于简单,U-Boot 到 kernel 这一段是地址和参数最容易出错的地方,因为两边对内存布局的假设必须严格对齐,任何一方挪了位置,另一方就得跟着改。
4.1 U-Boot 的重定位:为什么它要把自己搬一次
U-Boot 分两个阶段跑:board_init_f阶段还在片内 SRAM 或低地址区域,只做最必要的初始化,并把内存布局规划好;board_init_r阶段则把自己整体重定位到 DDR 的高地址端,然后在那里跑完整功能。为什么要多此一举?因为 kernel 通常要求加载到 DDR 的低地址连续区域,如果 U-Boot 一直占着低地址,就得在启动前把自己挪开,否则跳过去之后互相踩。重定位的另一个好处是把片内 SRAM 完全让出来,减少和后续阶段的资源冲突。
这段机制带来的典型故障是“U-Boot 前半段正常、重定位之后就没输出了”。原因多半是重定位目标地址算错,或者 DDR 那一段实际不可用——训练参数看着通过了,但高地址区域有坏块或时序边缘。查法是打印重定位前后的地址和大小,再对目标区间做一次读写测试,能读能写再往下走。
4.2 bootargs 与镜像地址:kernel、DTB、rootfs 的三方对齐
U-Boot 到 kernel 的交接靠三样东西:镜像本身、设备树、启动参数。三者里任何一个地址或内容不对,现象都是“Starting kernel ...”之后没下文,而且没有任何报错——这是最让人抓狂的一种失败,因为串口什么都不说。
| 参数 | 常见取值示例 | 写错后的典型现象 |
|---|---|---|
console | ttyS0,115200 | kernel 起来了但看不到打印,误判为没启动 |
root | /dev/mmcblk0p2 | 挂载根文件系统失败,panic 后停在提示 |
rootfstype | ext4/squashfs | 同上,但报错信息里能看到类型不匹配 |
mem | 512M | 内核识别到的内存小于实际,高地址申请失败 |
earlycon | uart8250,mmio32,0x... | 早期打印缺失,前面几百毫秒是黑匣子 |
earlycon那一行值得单独说。kernel 自己的串口驱动加载之前,日志是打不出来的,中间这段空窗期恰恰是问题最集中的地方。加上earlycon就能把这段补上,等于给启动过程装了个黑匣子。我现在的习惯是调试版本一律带earlycon,量产再去掉或者改成静默。
# 在 U-Boot 命令行里核对地址,别背参数,直接看环境变量 printenv bootargs printenv loadaddr # 手动走一遍流程,定位到底断在哪一步 mmc dev 0 fatload mmc 0:1 ${loadaddr} Image fatload mmc 0:1 ${fdt_addr} board.dtb booti ${loadaddr} - ${fdt_addr}loadaddr是 kernel 加载地址,fdt_addr是设备树地址,booti的第二个参数传-表示没有 initrd。手动走一遍的好处是每一步都有返回值,哪一步失败一目了然,比直接敲boot然后等黑屏强得多。设备树地址尤其要注意:它不能被 kernel 的解压过程覆盖,所以通常放在 kernel 镜像上方留出足够间隔的位置。
4.3 UART/USB 下载模式:量产与救砖通道
文档里提到“如果是 uart/usb 下载模式则进行 download 下载”,这是 ROM 里保留的另一条路径。它绕过所有存储介质,直接通过串口或 USB 把镜像灌进片内 SRAM 再跳转,用途有两个:产线首次烧录(板上还没有任何可启动内容),以及救砖(存储介质里的镜像坏了,正常启动进不去)。
两种通道的差别主要在速度。UART 简单、接线少,但受波特率限制,灌一个几百 KB 的 SPL 要等十几秒;USB 快得多,但需要主机端驱动配合,协议握手也更复杂。常见做法是产线用 USB 提效率,研发调试用 UART 图省事。不管哪种,主机端工具都必须严格匹配 SoC 内置的 ROM 协议——命令码、握手序列、校验方式,任何一项不一致都会卡在第一步。
判断下载模式有没有进对,看的是主机端工具能不能完成握手。能读到芯片返回的版本或状态字,说明 ROM 在正常响应;一直超时,先查线(USB 的 D+/D- 有没有接反、UART 的 TX/RX 有没有交叉),再查按键或 strap 有没有正确进入下载分支。这一步不需要看代码,纯硬件排查。
4.4 跳进 kernel 前的最后一公里
bootm或booti执行的那一瞬间,U-Boot 做的事情比看上去多:校验镜像头部、按需解压、关掉自己的中断和外设、设置好 CPU 寄存器里的三个关键入参(机器类型或设备树地址、启动参数地址、入口地址)、刷 cache、最后跳转。这中间任何一步出问题,都会表现为“跳过去就没动静”。
最常见的两个错误,一个是镜像加载地址和 kernel 编译时假设的物理偏移冲突,解压过程中把自己覆盖了;另一个是设备树地址和 kernel 的保留区重叠,早期内存分配把 DTB 写坏了。前者的现象是解压到一半死掉,后者是启动过程中随机 panic。两者都不报错,只能靠提前规划内存布局来避免。我一般的做法是在 U-Boot 里把整段内存布局打印出来——kernel、DTB、initrd、保留区各占哪一段——贴在调试记录里,后面每次改配置都对着这张图核一遍。
5. 避坑与排查:启动失败的五个典型现场
启动失败和普通驱动问题最大的区别是:可观测信息极少。串口可能一个字都没有,JTAG 不一定接得上,日志可能停在半句话。所以排查的核心不是“读代码”,而是“按阶段切分,先确定死在哪一段”,把搜索空间从一个系统压缩到一个函数。下面五条是我在不同板子上反复遇到的现场,按出现频率排。
5.1 上电后串口一片安静
现象:按下复位,串口工具里一个字符都没有,示波器看 TX 引脚也没有任何翻转。
原因:这类情况绝大多数不在软件。可能是 ROM 阶段就没跑起来(供电、复位时序、晶振不起振),也可能跑了但串口这条路不通(引脚复用没配对、波特率不匹配、电平标准不对,比如板上是 1.8V 而转换芯片按 3.3V 判逻辑)。还有一种不太常见但确实存在的:板子进了 ptset 测试模式,走的不是正常打印路径。
解决:按供电→复位→晶振→引脚→波特率的顺序量一遍,别跳步。晶振那一项尤其要看起振波形,频率偏了 ROM 会卡在时钟初始化。如果怀疑是测试模式,把 strap 电阻量一遍,再看串口有没有极短的提示字符——测试模式的提示往往只有几个字节。
5.2 SPL 打印到一半就断了
现象:串口能看到 SPL 的头几行,然后停住,隔一会儿可能自己重启,也可能彻底不动。
原因:最常见的是 SRAM 溢出。加了点调试代码,.text或.bss撑破了 70KB 窗口,链接脚本没写 ASSERT 就不会报错,跑起来直接踩到栈或者别的数据区。其次是看门狗:SPL 里的 DDR 训练耗时较长,如果没在关键节点喂狗,到点就复位,表现出来的节律性重启很容易被误判成代码崩溃。第三种是栈开得太小,深一层的函数调用就溢出。
解决:先看 map 文件里各段的实际大小和窗口上限的差值,余量不足 4KB 就要警惕;再确认看门狗在 SPL 入口有没有关掉或延长;最后把栈指针初始值和栈大小打出来核对。这三步做完,绝大多数“打印到一半”都能定位。
5.3 SPL 说 DDR 好了,U-Boot 却在 DDR 里跑飞
现象:SPL 日志显示 DDR 初始化成功、搬运完成、跳转地址正确,U-Boot 一进 DDR 就异常,可能是乱码、可能是死循环、也可能直接复位。
原因:DDR 训练只验证了某几个地址的读写,不代表整个区间可用。高地址区域时序边缘、容量识别错误(把 512MB 认成 1GB)、或者训练参数在温度变化后失效,都会造成“局部能跑、整体跑飞”。另一个原因是搬运长度算错,镜像只搬了一半就跳过去。
解决:在 SPL 里对 DDR 的目标区间做一次全段读写测试,别只测头尾;把训练参数打出来和厂商给的参考值对比;核对搬运长度和镜像头里记录的长度是否一致,最好加一次整段 CRC。这一步花的时间,远比后面在 U-Boot 里瞎猜省。
5.4 USB 下载模式识别不到设备
现象:按流程进了下载模式,主机端工具一直超时,设备管理器里看不到任何新设备。
原因:分三层。物理层是线序和供电,USB 的 D+/D- 接反、VBUS 没供上、地没共;协议层是主机工具和 ROM 的命令码不匹配,版本对不上就握不上手;模式层是根本没进下载分支,strap 或按键判定失败,ROM 还在老老实实找 SD 卡。
解决:先换一台主机、换一根已知良好的线,排除物理层;再用总线分析仪看有没有枚举过程,有枚举说明物理层通了,问题在协议;没有枚举就回到模式层,量按键电平和 strap。这三层按顺序排,比乱试工具版本有效。
5.5 U-Boot 打印 Starting kernel 之后没了下文
现象:U-Boot 一切正常,最后一行Starting kernel ...打完,串口彻底安静,既不报错也不重启。
原因:这段是典型的黑匣子区间。可能是 kernel 镜像本身有问题(解压失败、入口地址不对),可能是设备树地址被覆盖(早期内存分配写坏 DTB),可能是console参数写错导致 kernel 起来了但打印不出来,也可能是 kernel 的内存布局假设和实际 DDR 容量不符,一进页表初始化就崩。
解决:先加earlycon,把这段空窗期的日志捞出来,这一招能解决一半以上的此类问题;再看bootargs里的console和mem两个参数;最后核对 kernel 加载地址、DTB 地址、initrd 地址三者有没有重叠。三者都不重叠还不行,就上 JTAG 单步,看第一条指令执行到哪停的。
6. 把 boot 流程固化成清单:我每次 bring-up 必走的三步
上面这些内容散着看容易记混,我最后收在一个动作上:把整条链路压成一张可执行的检查清单,每次拿到新板子或者改完配置,从头走一遍,不跳步。这个习惯是被坑出来的——有次改完 DDR 参数直接烧板子,结果卡在 SPL,查了两天才发现是链接脚本里窗口上限改了但搬运长度没跟着改,本来三分钟能发现的事花了两天。
第一步是静态核对。链接脚本的ORIGIN、LENGTH、镜像头里的长度字段、烧录脚本里的偏移,这四个数必须两两一致,我一般在打包脚本末尾加一句断言,让它在生成镜像时就报错,而不是等上板。第二步是分段观测。ROM、SPL、U-Boot、kernel 四段各设一个最小的可观测点:ROM 看串口那两三行,SPL 看 GPIO 翻转,U-Boot 看命令行能不能进,kernel 看earlycon的第一行打印。哪个观测点没有输出,问题就在它和前一个观测点之间,搜索范围立刻收敛。第三步是记录基线。每次成功启动,把 DDR 训练参数、内存布局、bootargs原文、镜像大小这几个值抄进调试记录,后面出问题时第一件事是拿当前值和基线比,差异项就是嫌疑项。
# 打包脚本末尾的自检:长度、偏移、窗口三者一致性检查 IMG_LEN=$(stat -c %s firmware.img) SPL_LEN=71680 if [ "$IMG_LEN" -lt "$SPL_LEN" ]; then echo "fatal: firmware shorter than SPL window"; exit 1 fi # 头部声明的长度必须等于实际切出来的长度,否则 ROM 校验不过 echo "spl window=$SPL_LEN image=$IMG_LEN"这段脚本里的SPL_LEN要和链接脚本的LENGTH用同一个来源,最好是同一个变量传两处,别手工抄。stat -c %s取实际字节数,比人眼估算可靠。断言失败就中断打包,不让有问题的镜像流到烧录环节——这个后悔药成本极低,收益极高。
从那以后我每次改内存布局或者搬运参数,都强制走一遍这三步,哪怕只是改了一个数字。启动链路的特点是环环相扣,任何一环的数字变了,上下游都得跟着复核,凭记忆跳步的代价通常是半天以上的排查。希望帮到你。
本文还有配套的精品资源,点击获取