4. U-Boot编译系统与配置机制

好,咱们正式开始啃U-Boot启动流程这块硬骨头。这一章我先讲第一阶段,也就是汇编部分。很多初学者一看到start.S就头大,满屏的汇编宏和条件编译,其实没那么可怕。你把它拆成几个功能块来看,就清晰了。

我个人习惯把第一阶段分成三个核心任务:异常向量表设置、CPU初始化、以及为C运行环境铺路。说白了,就是让CPU先认识自己,再认识内存,最后能跑C代码。

4.1 入口点与异常向量表

U-Boot的入口点通常是 _start,定义在 arch/arm/lib/vectors.S 或 arch/arm/cpu/armv7/start.S 里。不同架构路径不同,但思路一致。

为什么一上来就要搞异常向量表?因为CPU上电后,第一条指令就是从复位向量地址取的。ARM架构规定,0x00000000或0xFFFF0000处放一张向量表,每个表项占4字节,存一条跳转指令。

核心要点:异常向量表是CPU的“紧急联络簿”。复位、中断、数据中止等异常发生时,CPU自动跳转到对应表项执行。

来看一段典型的向量表代码:

_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                    @ IRQ中断
    ldr     pc, _fiq                    @ FIQ中断

_undefined_instruction: .word undefined_instruction
_software_interrupt:    .word software_interrupt
_prefetch_abort:        .word prefetch_abort
_data_abort:            .word data_abort
_not_used:              .word not_used
_irq:                   .word irq
_fiq:                   .word fiq

注意看,复位向量用的是 b reset,其他用的是 ldr pc, xxx。为什么?因为复位后CPU处于SVC模式,且MMU还没开,地址空间是平坦的。用 b 跳转范围有限(±32MB),但复位后通常够用。其他异常用 ldr pc 是因为它们可能发生在任意阶段,需要能跳到任意地址。

我的经验:有一次移植到一颗Cortex-A7芯片上,板子死活不进中断。查了两天,发现是向量表基地址没重定位。ARM默认从0x0取向量表,但我把代码链接到了0x87800000。后来在start.S里加了 mcr p15, 0, r0, c12, c0, 0 设置VBAR,问题解决。嗯,这个坑我印象很深。

4.2 CPU初始化:从复位到SVC模式

CPU复位后处于什么模式?ARM架构下通常是SVC模式(超级管理员模式),但有些芯片会进入其他模式。所以第一步,我们要确保CPU进入SVC模式,并且关掉所有中断。

这段代码通常在 reset 标号后面:

reset:
    /* 设置CPU为SVC模式,关中断 */
    mrs     r0, cpsr
    bic     r0, r0, #0x1f          @ 清除模式位
    orr     r0, r0, #0xd3          @ SVC模式 + 关IRQ/FIQ
    msr     cpsr, r0

这里 0xd3 的含义:bit[4:0] = 10011(SVC模式),bit[7] = 1(关IRQ),bit[6] = 1(关FIQ)。你想想看,在初始化阶段,我们绝对不希望被任何中断打断,否则CPU状态就乱套了。

CPSR位域 值 含义
M[4:0] 10011 SVC模式
I (bit7) 1 禁止IRQ
F (bit6) 1 禁止FIQ

4.3 CPU关键寄存器初始化

接下来要初始化一些CPU控制寄存器。不同架构差异很大,我以ARMv7为例讲几个关键的。

4.3.1 SCTLR:系统控制寄存器

SCTLR控制着MMU、Cache、对齐检查等开关。刚复位时,这些功能都是关闭的。我们要确保MMU和Cache是关的,因为还没初始化页表。

    mrc     p15, 0, r0, c1, c0, 0   @ 读取SCTLR
    bic     r0, r0, #0x0005         @ 关MMU (bit0) 和 关对齐检查 (bit2)
    bic     r0, r0, #0x1000         @ 关I-Cache (bit12)
    bic     r0, r0, #0x0004         @ 关D-Cache (bit2) — 注意不同架构位不同
    mcr     p15, 0, r0, c1, c0, 0   @ 写回SCTLR

注意:不同ARM版本SCTLR的位定义有差异。比如Cortex-A8和Cortex-A7的Cache控制位就不一样。移植时一定要查对应TRM(技术参考手册)。我曾经因为照搬了A8的代码到A7上,Cache死活刷不干净,折腾了一下午。

4.3.2 设置中断向量基地址(VBAR)

前面提到过,ARM允许把异常向量表放在任意地址。通过CP15的c12寄存器设置:

    ldr     r0, =_start             @ 向量表基地址
    mcr     p15, 0, r0, c12, c0, 0  @ 写入VBAR

这一步很关键。如果你的U-Boot链接地址不是0x0,就必须设置VBAR,否则异常来了CPU会去0x0取指令,而那里可能什么都没有。

4.4 整体启动流程图

我把第一阶段的核心流程画了张图,方便你对照理解:

U-Boot第一阶段启动流程图 U-Boot第一阶段启动流程(start.S) 上电复位 → _start 设置异常向量表(b / ldr pc) 设置SVC模式,关中断 初始化SCTLR、VBAR等寄存器 关闭MMU、I/D-Cache 跳转到C代码入口(bl _main) 说明:以上流程在 start.S 中顺序执行,每个步骤都依赖上一步的正确配置

4.5 一些容易被忽略的细节

讲几个我实际移植中遇到的坑:

  • 栈指针设置时机:有些移植代码在设置SVC模式后立刻设SP,但此时内存控制器可能还没初始化。我建议等DDR初始化完成后再设SP,否则写栈会写飞。
  • WDT(看门狗)处理:很多SoC默认看门狗是开的。如果不在第一阶段关掉,板子会在初始化过程中反复复位。我习惯在 reset 开头就加一段关WDT的代码。
  • CPU版本检测:有些芯片有多个CPU核,或者有大小核架构。start.S里通常要读MIDR寄存器判断当前是哪个核,主核继续初始化,从核进WFI等待。

我的调试技巧:如果板子卡在start.S里出不来,我会在关键步骤后加GPIO翻转。比如设置完SVC模式后拉高一个GPIO,初始化完SCTLR后再拉低。用示波器看GPIO波形,就知道卡在哪一步了。这比瞎猜高效得多。

4.6 第一阶段完成后CPU的状态

当start.S执行完,即将跳转到C代码入口(通常是 _main)时,CPU应该处于以下状态:

资源 状态 说明
CPU模式 SVC 超级管理员模式,权限最高
IRQ/FIQ 关闭 防止中断干扰初始化
MMU 关闭 使用物理地址
I-Cache 关闭 避免取指不一致
D-Cache 关闭 避免数据不一致
异常向量表 已设置 指向U-Boot的向量表
栈指针 未设置 等C代码入口设置

嗯,第一阶段的核心内容就这些。说白了就是让CPU从“混沌”状态进入一个“可控”状态,为后续的C代码执行铺平道路。下一阶段我们会进入C代码,开始板级初始化和设备驱动配置。但那是后话了,先把这一阶段的代码吃透再说。