3. U-Boot源码结构与构建系统

U-Boot 启动的第一行代码,说白了就是跟硬件打交道。我刚开始接触这块时,总觉得不就是设置几个寄存器嘛,后来才发现——顺序错一步,板子就变砖。这一章咱们把最底层的三件事讲透:CPU 模式怎么切、异常向量表怎么摆、时钟怎么配。

3.1 CPU 模式设置——从 EL3 到 SVC 的切换

ARM 处理器上电后,默认在最高特权级。以 ARMv8 为例,启动时处于 EL3(Monitor 模式)。但 U-Boot 大部分代码跑在 SVC 模式(Supervisor),所以第一步就是降权。

我习惯把这段代码放在 start.S 的最前面,紧挨着 reset 入口。为什么?因为异常向量表还没配好,任何中断进来都会 crash。

/* 示例:ARMv8 从 EL3 切换到 SVC 模式 */
reset:
    /* 读取当前异常级别 */
    mrs     x0, CurrentEL
    and     x0, x0, #0x0C
    cmp     x0, #0x0C      /* EL3? */
    b.eq    el3_entry

el3_entry:
    /* 设置 SCR_EL3,允许 lower EL 使用 SIMD 等 */
    mov     x0, #0x0
    msr     scr_el3, x0

    /* 设置 SPSR_EL3,目标 EL1(SVC模式) */
    mov     x0, #0x1C5      /* SPSR: EL1h, 所有中断屏蔽 */
    msr     spsr_el3, x0

    /* 设置返回地址,跳转到 el1_entry */
    adr     x0, el1_entry
    msr     elr_el3, x0
    eret                        /* 执行切换 */

el1_entry:
    /* 现在处于 EL1 SVC 模式 */
    msr     spsel, #1       /* 使用 SP_EL1 */
    ldr     x0, =__stack_start
    mov     sp, x0
我的习惯:切换模式前,先把所有中断屏蔽掉。我在项目中遇到过,某款 SoC 在切换过程中因为未屏蔽 FIQ,直接跳到了未初始化的向量表,板子死得透透的。

3.2 异常向量表——CPU 的“紧急电话本”

异常向量表,你可以把它理解成 CPU 的紧急电话本。每个异常类型对应一个入口地址,CPU 拿到异常后直接查表跳转。

ARMv8 的向量表有 4 个基地址,每个基地址对应不同的异常类型。U-Boot 通常只配一个基地址,放在 __vectors 段。

偏移 异常类型 说明
0x000 Current EL, SP0, Sync 同步异常(如数据中止)
0x080 Current EL, SP0, IRQ 普通中断
0x100 Current EL, SP0, FIQ 快速中断
0x180 Current EL, SP0, SError 系统错误
0x200 Current EL, SPx, Sync 使用 SP_ELx 时的同步异常
0x280 Current EL, SPx, IRQ 使用 SP_ELx 时的 IRQ
0x300 Current EL, SPx, FIQ 使用 SP_ELx 时的 FIQ
0x380 Current EL, SPx, SError 使用 SP_ELx 时的 SError

每个入口只放一条跳转指令,因为向量表必须 128 字节对齐。我见过有人把整个异常处理函数直接塞进去,结果编译出来地址不对齐,CPU 直接跑飞。

/* 向量表示例 */
.align 11                     /* 2^11 = 2048 字节对齐 */
.globl __vectors
__vectors:
    b    _sync_el1_sp0        /* 0x000 */
    .align 7                  /* 128 字节对齐 */
    b    _irq_el1_sp0         /* 0x080 */
    .align 7
    b    _fiq_el1_sp0         /* 0x100 */
    .align 7
    b    _serror_el1_sp0      /* 0x180 */
    .align 7
    b    _sync_el1_spx        /* 0x200 */
    .align 7
    b    _irq_el1_spx         /* 0x280 */
    .align 7
    b    _fiq_el1_spx         /* 0x300 */
    .align 7
    b    _serror_el1_spx      /* 0x380 */
我曾经踩过的坑:某次移植,向量表地址没对齐到 2KB 边界,VBAR_EL1 写进去后,CPU 触发异常时跳到了错误地址。调试了整整两天,最后用 JTAG 单步才发现是对齐问题。嗯,从那以后我每次配向量表都会用 .align 11 强制对齐。

3.3 系统时钟初始化——让 SoC 跑起来

时钟是 SoC 的心脏。没有时钟,CPU 不跑、外设不动、DDR 不工作。U-Boot 的时钟初始化通常分三步:

  1. 配置 PLL——从外部晶振倍频到目标频率
  2. 设置分频器——给 CPU、总线、外设分配不同时钟
  3. 切换时钟源——从低速时钟切换到 PLL 输出

我习惯先读芯片手册的时钟树,搞清楚每个 PLL 的输入范围。举个例子,某款 SoC 的 CPU PLL 输入只能接受 24MHz 晶振,你如果接了 25MHz,PLL 根本锁不住。

/* 时钟初始化伪代码 */
void clock_init(void)
{
    /* 1. 等待 PLL 稳定 */
    while (!(readl(PLL_STATUS) & PLL_LOCK_MASK))
        ;

    /* 2. 设置 CPU 分频 */
    writel(CPU_DIV_VAL, CPU_CLK_DIV);

    /* 3. 切换时钟源到 PLL */
    writel(CLK_SEL_PLL, CLK_SELECT);

    /* 4. 等待切换完成 */
    while (!(readl(CLK_SEL_STATUS) & CLK_SEL_DONE))
        ;
}

关键点:时钟切换时,一定要先配好目标频率,再切源。顺序反了,中间会出现一段“无时钟”的空窗期,CPU 直接挂掉。

3.4 本章知识结构图

下面这张图把 CPU 模式、异常向量表、时钟初始化三者的关系串起来了。你可以看到,U-Boot 启动时这三步是严格顺序执行的。

U-Boot 硬件初始化流程(第3章核心) 上电复位 CPU模式设置(EL3→SVC) 异常向量表配置 系统时钟初始化 进入U-Boot主流程 注意事项 • 模式切换前必须关中断 • 向量表必须2KB对齐 • 时钟切换先配频率再切源 • 每一步都要确认状态寄存器 硬件初始化阶段

3.5 常见问题与避坑指南

  • 模式切换后 SP 没配:我见过有人切到 SVC 模式后忘了设栈指针,结果调用第一个函数就 crash。记住:eret 之后第一件事就是配 SP。
  • 向量表地址写错:VBAR_EL1 写的是物理地址,如果你开了 MMU,记得用虚拟地址。我早期移植时在这上面栽过跟头。
  • PLL 锁定超时:有些 SoC 的 PLL 锁定需要几百微秒,别在循环里死等,加个超时机制。我习惯设 100ms 超时,超时后打印错误并 fallback 到低速时钟。

调试小技巧:在时钟初始化前后各放一个 GPIO 翻转,用示波器量一下时间差,就能知道 PLL 锁定花了多久。这个方法比读寄存器直观多了。