3. U-Boot启动流程第一阶段:从reset向量到lowlevel_init的汇编代码分析

好,咱们今天来啃一块硬骨头——U-Boot的第一阶段启动。说白了,就是从CPU上电复位开始,到执行lowlevel_init之前的那段汇编代码。

我刚开始接触U-Boot时,总觉得这部分很神秘。CPU一上电,它怎么就知道该干嘛?代码在哪?谁把它加载到内存里的?其实说白了,第一阶段就是帮C语言运行环境“铺路”的。你想想看,C语言需要栈、需要BSS段清零、需要全局变量能访问……这些在汇编阶段都得搞定。

核心认知:U-Boot第一阶段(汇编阶段)的目标只有一个——为C语言执行准备好最基本的硬件环境。具体包括:设置CPU为SVC32模式、关闭中断、初始化时钟和串口、设置栈指针、搬移代码、清零BSS段。

3.1 入口点:reset向量

CPU复位后,会从0x00000000或0xFFFF0000(取决于具体架构)取第一条指令。这个地址对应的就是reset向量。在U-Boot源码中,入口定义通常在arch/arm/cpu/armv7/start.S(以ARMv7为例)。

/*
 * 异常向量表 —— 硬件强制要求放在起始位置
 */
_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,这是一个相对跳转。为什么不用ldr pc, =reset?因为此时CPU刚上电,内存控制器可能还没初始化,绝对地址访问可能会出问题。相对跳转只依赖当前PC值,安全得多。

我的经验:有一次我在移植U-Boot到一块新板子时,发现复位后总是跑飞。查了半天,原来是链接脚本里把入口地址设错了,导致b reset跳到了一个未初始化的内存区域。嗯,从那以后我每次都会先确认链接脚本的.text段起始地址是否正确。

3.2 设置CPU模式与关闭中断

进入reset标号后,第一件事就是设置CPU工作模式。ARM处理器有7种工作模式,U-Boot需要运行在SVC32模式(超级管理员模式),因为它拥有最高的权限,可以访问所有系统资源。

reset:
    /* 设置SVC32模式,关闭IRQ和FIQ */
    mrs r0, cpsr
    bic r0, r0, #0x1f       @ 清除模式位
    orr r0, r0, #0xd3       @ 设置SVC32模式 + 关IRQ/FIQ
    msr cpsr, r0

这段代码做了两件事:

  • 清除模式位:把CPSR的低5位清零
  • 设置新模式:写入0xD3,其中bit[4:0]=10011表示SVC模式,bit[7]=1关IRQ,bit[6]=1关FIQ

为什么要关中断?因为此时中断向量表可能还没准备好,栈也没设置,一旦来了中断,CPU根本不知道去哪处理,系统直接崩溃。我见过一个新手在调试时忘了关中断,结果每次单步执行到某条指令就莫名其妙跳走,查了一下午才发现是中断没关。

3.3 CPU初始化与SoC早期配置

接下来是cpu_init_cp15,这部分负责初始化CP15协处理器。CP15是ARM架构中用来控制MMU、Cache、TLB等核心功能的协处理器。

cpu_init_cp15:
    /* 关闭MMU和Cache */
    mrc p15, 0, r0, c1, c0, 0
    bic r0, r0, #0x00000005    @ 清除M位(MMU)和C位(DCache)
    bic r0, r0, #0x00001000    @ 清除I位(ICache)
    mcr p15, 0, r0, c1, c0, 0

    /* 使能分支预测 */
    mrc p15, 0, r0, c1, c0, 0
    orr r0, r0, #0x00000800    @ 设置Z位
    mcr p15, 0, r0, c1, c0, 0

    /* 设置域访问控制为Manager模式 */
    mov r0, #0xffffffff
    mcr p15, 0, r0, c3, c0, 0

这里有个关键点:刚上电时MMU和Cache都是关闭的,但U-Boot还是显式地再关一次,确保万无一失。为什么?因为有些BootROM可能会提前开启Cache,或者某些仿真器会修改CP15的状态。我习惯在初始化时先“重置”所有关键寄存器到已知状态,再按需开启。

CP15控制位 功能 U-Boot初始值
M (bit0) MMU使能 0(关闭)
C (bit2) 数据Cache使能 0(关闭)
I (bit12) 指令Cache使能 0(关闭)
Z (bit11) 分支预测使能 1(开启)

注意:分支预测可以提前打开,因为它不依赖内存映射。但MMU和Cache必须在确定物理内存可用后才能开启,否则CPU会尝试访问不存在的页表,导致数据中止异常。

3.4 lowlevel_init之前的最后准备

在跳转到lowlevel_init之前,还有几件小事要处理:

  1. 设置栈指针:为C函数调用准备栈空间。通常指向IRQ/FIQ栈之后的安全区域。
  2. 清零BSS段:全局变量和静态变量的初始值必须为0,否则C代码无法正常工作。
  3. 搬移代码:如果U-Boot当前运行在Flash或ROM中,需要把自己搬到RAM里执行(这一步有时在第二阶段做)。
    /* 设置栈指针 —— 指向内部SRAM的顶部 */
    ldr sp, =CONFIG_SYS_INIT_SP_ADDR

    /* 清零BSS段 */
    ldr r0, =__bss_start
    ldr r1, =__bss_end
    mov r2, #0
clear_bss:
    cmp r0, r1
    strlo r2, [r0], #4
    blo clear_bss

    /* 跳转到lowlevel_init */
    bl lowlevel_init

这里有个细节:CONFIG_SYS_INIT_SP_ADDR通常指向内部SRAM的顶部。为什么不用外部DDR?因为此时DDR控制器可能还没初始化,外部内存根本不可用。内部SRAM是CPU自带的,上电就能用,虽然容量小(通常只有4KB~64KB),但足够放栈了。

避坑指南:我曾经在一个项目中,把栈指针设到了外部DDR的地址,结果bl lowlevel_init一执行就死机。因为bl指令会把返回地址压栈,而栈在不可用的内存上……嗯,那次调试花了我整整一天。记住:在DDR初始化完成之前,只能用内部SRAM。

3.5 第一阶段流程图

下面这张图把整个流程串起来了,你可以对照着看:

U-Boot第一阶段启动流程 CPU复位 → reset向量 设置SVC32模式 + 关中断 cpu_init_cp15 设置栈指针 (内部SRAM) 清零BSS段 bl lowlevel_init 说明: lowlevel_init 负责初始化时钟、串口、DDR控制器等硬件。 完成后会返回 start.S,继续执行 board_init_f 等C语言函数。

3.6 关键数据结构与配置项

第一阶段涉及的配置项通常在include/configs/目录下的头文件中定义。我整理了一个表格,方便你对照:

配置宏 含义 典型值
CONFIG_SYS_INIT_SP_ADDR 初始栈指针地址 内部SRAM顶部地址,如0x1000FFFF
CONFIG_SYS_TEXT_BASE U-Boot代码链接基址 0x87800000(DDR地址)
CONFIG_SYS_SDRAM_BASE DDR控制器基地址 0x80000000
CONFIG_SYS_INIT_RAM_SIZE 内部SRAM大小 0x8000(32KB)

重要提醒:CONFIG_SYS_INIT_SP_ADDR必须指向可读写的内存区域,且不能与代码段重叠。我见过有人把栈指针设到了代码段中间,结果压栈时把指令覆盖了,程序直接跑飞。调试这种问题,用JTAG单步跟踪是最快的。

3.7 第一阶段总结

好了,咱们把第一阶段的核心逻辑捋一遍:

  • 入口:CPU复位后从reset向量开始执行
  • 模式设置:切换到SVC32模式,关闭所有中断
  • CP15初始化:关闭MMU和Cache,开启分支预测
  • 栈与BSS:在内部SRAM上设置栈指针,清零BSS段
  • 跳转:调用lowlevel_init,进入硬件初始化阶段

我个人觉得,第一阶段最难理解的地方在于“为什么要在汇编里做这些事”。其实答案很简单:C语言需要栈才能调用函数,需要BSS清零才能使用全局变量,需要关中断才能保证执行流不被干扰。这些基础条件不满足,C代码根本跑不起来。

下一阶段,lowlevel_init会初始化时钟、串口和DDR,然后U-Boot才能把自身搬到DDR里继续执行。不过那是后话了,咱们先把第一阶段吃透再说。