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
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 */
.align 11 强制对齐。
3.3 系统时钟初始化——让 SoC 跑起来
时钟是 SoC 的心脏。没有时钟,CPU 不跑、外设不动、DDR 不工作。U-Boot 的时钟初始化通常分三步:
- 配置 PLL——从外部晶振倍频到目标频率
- 设置分频器——给 CPU、总线、外设分配不同时钟
- 切换时钟源——从低速时钟切换到 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 启动时这三步是严格顺序执行的。
3.5 常见问题与避坑指南
- 模式切换后 SP 没配:我见过有人切到 SVC 模式后忘了设栈指针,结果调用第一个函数就 crash。记住:
eret之后第一件事就是配 SP。 - 向量表地址写错:VBAR_EL1 写的是物理地址,如果你开了 MMU,记得用虚拟地址。我早期移植时在这上面栽过跟头。
- PLL 锁定超时:有些 SoC 的 PLL 锁定需要几百微秒,别在循环里死等,加个超时机制。我习惯设 100ms 超时,超时后打印错误并 fallback 到低速时钟。
调试小技巧:在时钟初始化前后各放一个 GPIO 翻转,用示波器量一下时间差,就能知道 PLL 锁定花了多久。这个方法比读寄存器直观多了。