3. U-Boot 启动第一阶段:汇编级初始化(CPU、DDR、时钟)
好,咱们直接切入正题。U-Boot 的第一阶段,说白了就是那段汇编代码。很多同学觉得汇编难,不愿意看。但我跟你说,这部分恰恰是调试启动问题的核心。你想想看,CPU 一上电,C 环境都还没建立,能跑什么?只能跑汇编。
这一阶段的目标很纯粹:把 CPU 初始化好,把时钟配好,把 DDR 搞起来,然后跳转到 C 代码。就这么四件事。但每一件都藏着坑。
3.1 入口点与异常向量表
CPU 上电后,第一件事是去固定的地址取指令。对于 ARM 芯片,通常是 0x00000000 或者 0xFFFF0000。U-Boot 的入口点就在 arch/arm/cpu/armv7/start.S 里。
核心要点: 入口点必须与链接脚本(.lds)中指定的地址一致。否则,你烧进去的代码根本跑不到。
异常向量表紧跟在入口点后面。我见过一个项目,工程师改了中断处理函数,但忘了更新向量表里的跳转地址。结果一触发中断,系统直接飞了。嗯,查了两天才找到原因。
// 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,是相对跳转。其他向量用的是 ldr pc, ...,是绝对跳转。为什么?因为复位后 MMU 还没开,地址空间是平坦的,相对跳转没问题。但其他异常可能发生在任何模式下,用绝对跳转更安全。
3.2 CPU 模式与状态初始化
进入 reset 标号后,第一件事是设置 CPU 为 SVC(超级用户)模式。为什么要这么做?因为其他模式(比如 IRQ、FIQ)的栈还没配,万一进来个中断,连压栈的地方都没有。
reset:
/* 设置 CPU 为 SVC 模式,关闭 IRQ 和 FIQ */
mrs r0, cpsr
bic r0, r0, #0x1f // 清除模式位
orr r0, r0, #0xd3 // SVC 模式 + IRQ/FIQ 禁止
msr cpsr, r0
这里有个细节:0xd3 这个值,二进制是 1101 0011。低 5 位 10011 是 SVC 模式,第 6、7 位是 1,表示关中断。我刚开始学的时候,总觉得这些数字是魔法数。后来习惯了,其实就那么几种模式,背下来就好。
| 模式 | CPSR 模式位 | 用途 |
|---|---|---|
| USR | 10000 | 用户模式,权限最低 |
| FIQ | 10001 | 快速中断,有专用寄存器 |
| IRQ | 10010 | 普通中断 |
| SVC | 10011 | 超级用户,启动时使用 |
| MON | 10110 | 安全监控模式 |
我的习惯: 在设置完 CPU 模式后,立刻把各个模式的栈指针(SP)初始化。虽然有些模式暂时用不到,但万一调试时误入了,不至于立刻崩溃。
3.3 关闭看门狗与中断
这一步很多人会忽略。看门狗(WDT)在芯片出厂时默认是开启的。如果你不关它,代码跑着跑着,看门狗超时了,啪,复位了。你还在那纳闷:怎么代码跑一会就重启?
/* 关闭看门狗 —— 以 S5PV210 为例 */
ldr r0, =0xE2700000 // WDT 控制寄存器地址
mov r1, #0x0
str r1, [r0] // 写 0 关闭
不同芯片的看门狗地址不一样。我建议你拿到开发板后,第一件事就是查芯片手册,找到 WDT 的基地址。我曾经在一个项目里,因为看门狗没关,调试了整整一个下午。后来发现,嗯,就是少了一行代码。
中断也要全部屏蔽。虽然前面已经关了 IRQ 和 FIQ,但有些芯片还有别的中断源。保险起见,把中断控制器里的所有中断都 mask 掉。
3.4 时钟系统初始化
时钟是芯片的心脏。CPU 能跑多快,DDR 能跑多快,全靠时钟。U-Boot 第一阶段通常会把时钟配到一个较高的频率,这样后面的代码执行更快。
时钟配置的流程一般是:
- 选择时钟源:外部晶振还是内部振荡器?
- 配置 PLL:锁相环,把低频时钟倍频到高频。
- 设置分频器:给 CPU、总线、外设分配不同的时钟。
- 切换时钟源:从低速时钟切换到 PLL 输出的高速时钟。
注意: 切换时钟源时,一定要先确认 PLL 已经锁定(Locked)。PLL 锁定需要时间,通常是微秒级。如果没锁定就切换,CPU 可能直接跑飞。
/* 以 i.MX6UL 为例的时钟初始化片段 */
ldr r0, =CCM_BASE_ADDR
/* 1. 使能 PLL1 (ARM PLL) */
ldr r1, [r0, #CCM_ANALOG_PLL_ARM]
orr r1, r1, #(1 << 13) // 使能 PLL
str r1, [r0, #CCM_ANALOG_PLL_ARM]
/* 2. 等待 PLL 锁定 */
1: ldr r1, [r0, #CCM_ANALOG_PLL_ARM]
tst r1, #(1 << 31) // 检查锁定位
beq 1b // 没锁定就继续等
/* 3. 切换 CPU 时钟到 PLL1 */
ldr r1, [r0, #CCM_CACRR]
bic r1, r1, #0x7 // 清除分频位
str r1, [r0, #CCM_CACRR]
这段代码里,那个 1: 和 beq 1b 是个循环等待。我见过有人忘了加这个等待,结果芯片工作不稳定,时好时坏。这种问题最难查,因为不是必现的。
3.5 DDR 初始化
DDR 初始化是整个第一阶段里最复杂、最容易出问题的部分。DDR 的时序参数非常多,而且每个芯片都不一样。U-Boot 里通常会把 DDR 的配置参数放在一个结构体里,然后调用一个通用的初始化函数。
DDR 初始化的关键步骤:
- 配置 DDR 控制器:设置地址映射、突发长度、CAS 延迟等。
- 配置 DDR PHY:物理层,负责信号的电气特性。
- 发送 ZQ 校准命令:DDR3/DDR4 需要校准输出阻抗。
- 发送 MRS 命令:模式寄存器设置,配置 DDR 芯片的工作模式。
- 执行训练:读写训练,确保数据线、地址线、控制线的时序正确。
避坑指南: 我曾经在一个项目里,DDR 初始化总是失败。查了三天,最后发现是 DDR 的 VREF(参考电压)没配好。DDR3 的 VREF 一般是 VDDQ 的一半,但有些板子因为电源纹波大,需要软件调整。嗯,从那以后,我每次调 DDR 都会先量一下 VREF。
/* DDR 初始化伪代码 */
void ddr_init(struct ddr_params *params)
{
/* 1. 复位 DDR 芯片 */
ddr_reset(params->reset_pin);
/* 2. 配置控制器 */
ddr_ctrl_config(params->ctrl_cfg);
/* 3. 配置 PHY */
ddr_phy_config(params->phy_cfg);
/* 4. 等待 ZQ 校准完成 */
while (!(readl(params->base + DDR_STATUS) & ZQ_DONE));
/* 5. 执行训练 */
ddr_training(params->base);
}
这里要注意,DDR 初始化必须在时钟稳定之后进行。而且,DDR 的供电顺序也有要求:VDDQ 要先于 VDD 上电。这些在芯片手册里都有,但很多人不看。
3.6 重定位与栈设置
DDR 初始化完成后,内存就有了。这时候,U-Boot 会把自身从 Flash(或 ROM)复制到 DDR 中。这个过程叫重定位(Relocation)。
为什么要重定位?因为 Flash 的读取速度太慢了。把代码放到 DDR 里,执行速度能快几十倍。
/* 重定位代码示例 */
relocate:
ldr r0, =_start // Flash 中的起始地址
ldr r1, =_TEXT_BASE // DDR 中的目标地址
ldr r2, =_bss_start // BSS 段起始地址
sub r2, r2, r0 // 计算代码长度
/* 复制代码到 DDR */
bl copy_loop
/* 设置栈指针 */
ldr sp, =_stack_end
/* 跳转到 DDR 中的 C 代码入口 */
ldr pc, =board_init_f
重定位之后,栈也设置好了。这时候,C 环境才算真正建立。注意,跳转到 C 代码时,要用 ldr pc, =... 这种绝对跳转,因为代码已经在 DDR 里了,地址是固定的。
3.7 第一阶段流程图
下面这张图,把整个第一阶段的流程串起来了。你可以对照着看,每一步都做了什么。
3.8 调试要点总结
第一阶段全是汇编,没法用 printk 调试。那怎么调?我常用的方法有几种:
- 点灯大法:在关键步骤后点亮不同的 LED。比如,时钟初始化成功后亮红灯,DDR 初始化成功后亮绿灯。这是最原始但最有效的方法。
- 示波器量波形:量时钟输出引脚,看频率对不对。量 DDR 的 DQS 信号,看有没有读写动作。
- JTAG 单步调试:用调试器(比如 J-Link)单步执行,看寄存器值对不对。这是最彻底的方法,但需要硬件支持。
我的建议: 在写第一阶段代码时,就预留好调试接口。比如,在某个 GPIO 上输出脉冲,用示波器看脉冲宽度就能知道代码执行到哪里了。别等到出了问题再想办法,那时候就晚了。
好了,第一阶段的内容就这些。说白了就是:配 CPU、配时钟、配 DDR、搬代码、设栈、跳转。每一步都有坑,但只要你理解了原理,调试起来就不难。