4、U-Boot启动流程分析(上):从reset向量到lowlevel_init,解析第一阶段汇编代码

大家好,我是你们的嵌入式系统讲师。今天我们来啃一块硬骨头——U-Boot的第一阶段汇编启动代码。

说实话,我刚开始接触U-Boot时,看到arch/arm/cpu/armv7/start.S这个文件,第一反应是「这都什么鬼?」满屏的汇编宏、条件编译、跳转标签……但后来我意识到,这部分恰恰是理解整个系统启动的关键。你想想看,CPU上电后第一条指令从哪里来?就是这里。

核心认知:U-Boot第一阶段汇编代码,负责完成从CPU复位到C语言环境初始化的全部底层工作。说白了,就是给后续的C代码铺好路。

4.1 复位向量:一切从这里开始

CPU上电或复位后,会从预设的复位向量地址取指执行。对于ARM芯片,这个地址通常是0x00000000或0xFFFF0000。但实际中,很多SoC会把复位向量映射到内部ROM或SRAM的起始位置。

我们来看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标签。为什么不用ldr?因为b指令是位置无关的,在ROM中也能正确执行。我曾在某个项目中遇到过,有人把b改成了ldr,结果芯片在ROM里启动时直接跑飞……嗯,这个坑我踩过。

4.2 reset入口:CPU模式的切换

跳转到reset后,第一件事是切换CPU模式。ARM芯片复位后默认进入SVC模式(超级管理员模式),但有些SoC会进入其他模式,我们需要显式设置:

reset:
    /* 设置SVC模式,关闭IRQ和FIQ */
    mrs r0, cpsr
    bic r0, r0, #0x1f
    orr r0, r0, #0xd3
    msr cpsr, r0

这段代码把CPSR寄存器的模式位设为0b10011(SVC模式),同时设置I位和F位来关闭中断。为什么要关中断?因为此时栈还没准备好,中断来了会直接崩溃。我个人习惯在进入任何底层初始化前,先把所有中断屏蔽掉,这是保命操作。

4.3 CPU初始化:缓存与MMU的早期配置

接下来是CPU级别的初始化。不同架构差异很大,我们以ARMv7为例:

cpu_init_crit:
    /* 使能L1缓存 */
    mrc p15, 0, r0, c1, c0, 0
    orr r0, r0, #0x00001000     /* I-cache */
    orr r0, r0, #0x00000004     /* D-cache */
    mcr p15, 0, r0, c1, c0, 0

    /* 关闭MMU */
    mrc p15, 0, r0, c1, c0, 0
    bic r0, r0, #0x00000001
    mcr p15, 0, r0, c1, c0, 0

这里有个关键点:为什么先开缓存再关MMU?因为开缓存能加速后续的代码执行,而关MMU是因为此时页表还没建立,开了MMU会直接触发异常。我记得有一次调试,同事把顺序搞反了,结果板子怎么都起不来,查了两天才发现是MMU没关……

经验之谈:在关闭MMU之前,务必确保代码运行在物理地址空间。如果之前已经使能了MMU,需要先跳转到物理地址映射的代码段,再执行关闭操作。否则你会看到「优雅」的异常。

4.4 板级初始化:lowlevel_init的调用

做完CPU级别的初始化,就该轮到板级了。这里会调用lowlevel_init函数:

    /* 调用板级底层初始化 */
    bl  lowlevel_init

lowlevel_init通常定义在board/你的板子目录下,负责初始化DDR、时钟、GPIO等外设。为什么叫「lowlevel」?因为它操作的都是寄存器,没有操作系统,没有驱动框架,就是裸奔。

我们来看一个典型的lowlevel_init实现:

ENTRY(lowlevel_init)
    /* 初始化系统时钟 */
    bl  system_clock_init

    /* 初始化DDR控制器 */
    bl  ddr_init

    /* 初始化串口(用于调试输出) */
    bl  uart_early_init

    /* 返回 */
    mov pc, lr
ENDPROC(lowlevel_init)

这里每个函数都是汇编或极简C实现的。为什么不用完整的C?因为此时栈指针sp还没设置,C语言的函数调用栈用不了。你想想看,没有栈,局部变量放哪里?全局变量又在哪里?所以只能用汇编或者特殊编译的「无栈C函数」。

注意:lowlevel_init中千万不要使用全局变量!因为此时BSS段还没清零,全局变量的初始值是不确定的。我曾经见过一个案例,有人在lowlevel_init里用了一个全局标志位来控制初始化流程,结果每次启动行为都不一样——因为BSS段里的值完全是随机的。

4.5 设置栈指针:为C语言铺路

lowlevel_init返回后,第一件事就是设置栈指针。这是从汇编世界进入C语言世界的「桥梁」:

    /* 设置栈指针,指向内部SRAM的顶部 */
    ldr sp, =CONFIG_SYS_INIT_SP_ADDR

CONFIG_SYS_INIT_SP_ADDR这个宏在配置文件中定义,通常指向内部SRAM的最高地址。为什么用内部SRAM?因为此时DDR可能还没完全初始化好,或者DDR的时序还不稳定。内部SRAM是CPU内部集成的,上电即可用,安全可靠。

栈的大小一般设置4KB到16KB不等。我个人建议留大一点,因为后续的board_init_f函数会做很多临时计算,栈太小容易溢出。我见过一个项目,栈设了2KB,结果board_init_f里有个大数组,直接栈溢出,系统反复重启……排查起来特别痛苦。

4.6 跳转到C代码:board_init_f

栈设置完毕,就可以愉快地跳转到C语言了:

    /* 跳转到C语言的初始化函数 */
    bl  board_init_f

board_init_f是U-Boot第二阶段初始化的入口,它运行在「临时栈」上,主要任务是:

  • 初始化串口、定时器等基础外设
  • 检测DDR大小并初始化
  • 计算U-Boot的重定位地址
  • 准备全局数据结构gd_t

这部分我们下一节再详细展开。这里你只需要记住:从reset到board_init_f,U-Boot完成了从「裸金属」到「有栈C环境」的跨越。

4.7 第一阶段汇编代码的完整流程

为了让你看得更清楚,我画了一张流程图:

U-Boot第一阶段汇编启动流程 CPU上电/复位 从复位向量取指 异常向量表 b reset → 跳转到reset入口 reset入口 设置SVC模式,关中断 cpu_init_crit 初始化缓存,关闭MMU lowlevel_init 初始化时钟、DDR、串口 设置栈指针 ldr sp, =CONFIG_SYS_INIT_SP_ADDR board_init_f 进入C语言初始化阶段 关键说明 1. 所有代码位置无关 2. 不能使用全局变量 3. 栈在内部SRAM中 4. DDR初始化在lowlevel 5. 关MMU前确保物理地址 6. 异常向量表必须对齐 7. 栈大小建议4KB+ 8. 调试串口尽早初始化

4.8 常见问题与避坑指南

这部分我总结几个实际项目中遇到过的问题:

问题现象 根本原因 解决方案
板子上电后无任何输出 串口初始化失败,或时钟配置错误 检查lowlevel_init中的时钟和UART寄存器配置
系统反复重启 栈溢出或看门狗未关闭 增大栈空间,或在reset入口关闭看门狗
跳转到board_init_f后崩溃 栈指针设置错误,或DDR未初始化 确认CONFIG_SYS_INIT_SP_ADDR指向有效RAM区域
异常向量表未对齐 链接脚本中_start地址不是32字节对齐 在链接脚本中添加 . = ALIGN(32);

调试技巧:如果第一阶段汇编代码出了问题,你连串口输出都看不到。这时候怎么办?我的做法是在关键位置用GPIO点灯。比如在reset入口点亮LED1,在lowlevel_init返回后点亮LED2。通过观察LED的状态,就能判断代码执行到了哪一步。这招在没有任何调试工具的情况下特别管用。

好了,第一阶段汇编代码的核心内容就这些。从复位向量到lowlevel_init,再到设置栈指针跳转到C代码,每一步都有它的道理。你可能会觉得汇编代码看起来繁琐,但正是这些「底层细节」决定了系统的稳定性和启动速度。

下一节我们会进入C语言的世界,看看board_init_f里到底做了什么。不过在那之前,我建议你打开自己板子的start.S文件,对照着今天讲的内容,一行一行地读一遍。相信我,读完之后你会对U-Boot有全新的认识。

可可精品资料库