4. U-Boot启动流程第一阶段:从reset向量到lowlevel_init的汇编阶段
好,我们正式开始啃U-Boot启动的第一块硬骨头——汇编阶段。很多同学觉得汇编难,其实你把它想成是给CPU下的一系列「死命令」就行了。这个阶段没有复杂的逻辑判断,就是按部就班地初始化硬件,为C语言运行环境铺路。
我个人习惯把第一阶段拆成三个关键节点:Reset向量 → 异常向量表 → lowlevel_init。搞懂这三个点,汇编阶段你就拿下了。
4.1 第一行代码:Reset向量
CPU上电后,PC指针会跳到一个固定地址。对于ARM芯片来说,通常是0x00000000或0xFFFF0000。这个地址存放的就是第一条指令——Reset向量。
我们来看看U-Boot的入口代码长什么样(以ARMv7为例):
/* arch/arm/cpu/armv7/start.S */
.globl _start
_start:
b reset
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq
ldr pc, _fiq
看到没?第一条指令就是b reset,无条件跳转到reset标签。后面的那些是异常向量表,每个表项占4字节,存放的是跳转到对应异常处理函数的指令。
b而不是ldr pc?因为b是相对跳转,不依赖绝对地址。在芯片刚上电、MMU还没开启的时候,你敢用绝对地址吗?我当年第一次移植U-Boot时就踩过这个坑——用了ldr pc,结果PC直接飞到外太空去了。
4.2 异常向量表:CPU的「紧急联络人」
异常向量表说白了就是一张「紧急情况处理清单」。CPU遇到中断、数据中止、未定义指令等情况时,会自动跳转到对应的表项去执行。
这张表的位置很讲究。ARM有两种选择:
| 模式 | 基地址 | 适用场景 |
|---|---|---|
| 低端向量 | 0x00000000 | 大多数嵌入式系统 |
| 高端向量 | 0xFFFF0000 | Linux内核、某些ARM芯片 |
U-Boot默认使用低端向量。但要注意,有些SoC会把BootROM映射到0x00000000,这时候U-Boot的向量表就得放在别处。我记得调试一块TI的AM335x板子时,就遇到过向量表被BootROM覆盖的问题,折腾了两天才找到原因。
4.3 reset标签:真正的初始化开始
跳转到reset后,U-Boot开始干正事了。我们看看核心代码:
reset:
/* 设置CPU为SVC32模式,关中断 */
mrs r0, cpsr
bic r0, r0, #0x1f
orr r0, r0, #0xd3
msr cpsr, r0
/* 关闭MMU、I/D-Cache */
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #0x00002000 /* 清除V位(向量表位置) */
bic r0, r0, #0x00000007 /* 清除M、A、C位 */
mcr p15, 0, r0, c1, c0, 0
这段代码做了三件要紧事:
- 设置CPU模式:强制进入SVC32模式,关掉所有中断。为什么?因为初始化阶段不允许被打断,你想想看,如果正在配置DDR时序时来了个中断,那内存数据不就乱套了吗?
- 关MMU:刚上电时MMU必须关掉,因为页表还没建立。你敢开MMU,CPU就敢给你报Data Abort。
- 关Cache:Cache里的数据是脏的,必须清掉。我见过一个案例,同事没关Cache就直接初始化DDR,结果读回来的数据全是错的。
4.4 lowlevel_init:板级硬件的「开胃菜」
做完CPU基本设置后,U-Boot会调用lowlevel_init。这个函数通常放在board/你的板子/lowlevel_init.S里,负责初始化板级最基础的硬件。
典型的工作包括:
- 设置系统时钟:配置PLL,让CPU跑在目标频率
- 初始化DDR控制器:配置时序参数,让内存可用
- 设置GPIO复用:把引脚功能配好
- 初始化串口:方便后面打印调试信息
调用方式很简单:
bl lowlevel_init
就这一条指令。但lowlevel_init内部可能极其复杂。我见过最夸张的一个实现,光DDR初始化就写了500多行汇编,因为要适配不同容量的内存颗粒。
4.5 第一阶段流程图
我把整个流程画了张图,方便你对照理解:
4.6 常见故障与排查方法
汇编阶段出问题,通常表现为「板子完全没反应」或「串口没有任何输出」。我总结了几种典型情况:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 上电后电流异常 | 时钟配置错误,CPU跑在超频状态 | 用示波器量晶振输出,确认频率 |
| 串口无输出 | GPIO复用没配好,或波特率不对 | 检查lowlevel_init里的串口初始化代码 |
| DDR初始化失败 | 时序参数不对,或DDR电源没起来 | 用逻辑分析仪抓DDR控制信号 |
| 跳转到C代码后崩溃 | 栈指针没设,或代码段没重定位 | 检查SP是否指向有效的RAM地址 |
我的经验: 调试汇编阶段最痛苦的是没有调试器可用。我的做法是在关键位置加「点灯代码」——让GPIO输出高低电平,用LED闪烁来指示程序执行到了哪里。虽然土,但真的管用。
4.7 小结
第一阶段的核心任务就三个字:铺环境。把CPU模式设好、把MMU和Cache关掉、把板级最基础的硬件初始化了。做完这些,U-Boot才能放心地跳转到C语言代码去干更复杂的事情。
嗯,这里要注意一点:不同架构的U-Boot入口代码差异很大。ARM、RISC-V、MIPS的启动流程完全不同。但思路是一样的——先处理CPU核心,再处理板级外设。你只要抓住这个主线,不管换什么芯片都能快速上手。