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

这段代码做了三件要紧事:

  1. 设置CPU模式:强制进入SVC32模式,关掉所有中断。为什么?因为初始化阶段不允许被打断,你想想看,如果正在配置DDR时序时来了个中断,那内存数据不就乱套了吗?
  2. 关MMU:刚上电时MMU必须关掉,因为页表还没建立。你敢开MMU,CPU就敢给你报Data Abort。
  3. 关Cache:Cache里的数据是脏的,必须清掉。我见过一个案例,同事没关Cache就直接初始化DDR,结果读回来的数据全是错的。
避坑指南: 我曾经在移植U-Boot到新板子时,忘记关掉I-Cache。结果CPU从Cache里取指令,而DDR里的代码还没完全加载完,导致执行了半截指令——直接死机。排查了整整一天才发现是Cache没关。

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 第一阶段流程图

我把整个流程画了张图,方便你对照理解:

U-Boot第一阶段启动流程 U-Boot第一阶段启动流程(汇编阶段) CPU上电复位 跳转到Reset向量 设置SVC模式,关中断 关闭MMU、I/D-Cache bl lowlevel_init 时钟/DDR/GPIO/串口初始化 关键点 • 必须关中断 • MMU必须关 • Cache必须清空 lowlevel_init • 板级相关,需移植 • 出错直接死机

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核心,再处理板级外设。你只要抓住这个主线,不管换什么芯片都能快速上手。