第四讲:U-Boot启动流程详解(第一阶段)——CPU初始化、关闭中断、设置时钟、初始化DDR、重定位
好,咱们今天正式进入U-Boot的启动流程。说实话,很多同学学完Linux内核,回头再看U-Boot,总觉得它是个“小角色”。其实不然。U-Boot的第一阶段,说白了就是帮CPU“穿衣服、穿鞋、站起来”。没有这一步,内核连个落脚的地方都没有。
我个人习惯把第一阶段拆成五个关键动作:CPU初始化、关中断、设时钟、初始化DDR、重定位。这五个动作,一个都不能少。顺序也不能乱。你想想看,如果DDR还没初始化,你就把代码搬过去,那不就等于把数据扔进黑洞吗?
1. CPU初始化:从复位向量开始
CPU上电后,第一件事是去复位向量取指令。对于ARM Cortex-A系列,复位向量通常是0x00000000或者0xFFFF0000。U-Boot的入口点_start就挂在这里。
这段代码通常写在arch/arm/cpu/armv7/start.S里。我刚开始看的时候,觉得汇编代码密密麻麻的,后来发现其实就干了几件事:
- 设置CPU为SVC模式(超级用户模式)
- 关闭MMU和Cache(刚上电,地址映射还没建立)
- 设置栈指针(SP),为C语言环境做准备
嗯,这里要注意:栈指针必须指向一段可用的内存。在DDR还没初始化之前,只能用CPU内部的SRAM或者片内RAM。我在项目中遇到过,有人把栈设到了DDR地址上,结果DDR还没初始化,程序直接跑飞了。
核心要点:CPU初始化阶段,所有操作都必须在“安全区”内完成。这个安全区通常是CPU内部的SRAM,或者Boot ROM。
2. 关闭中断:别让意外打断初始化
为什么要关中断?道理很简单:初始化过程是线性的、顺序的。如果中途来了个中断,中断服务程序可能访问还没初始化的外设,那不就乱套了吗?
U-Boot的做法是:在_start入口处,直接写一条cpsid if指令,把IRQ和FIQ都关了。
/*
* 关闭中断的典型写法
* 在 start.S 中
*/
mrs r0, cpsr
bic r0, r0, #0x1f @ 清除模式位
orr r0, r0, #0xd3 @ 设置为SVC模式,关IRQ和FIQ
msr cpsr, r0
我曾经见过一个案例:某款芯片的Boot ROM里默认开了某个外设的中断,U-Boot启动时没关干净,结果在初始化DDR的过程中,中断触发,访问了非法地址,系统直接挂掉。排查了两天才找到原因。所以,关中断这件事,宁可多关,不能少关。
避坑指南:有些SoC有多个中断控制器(比如GIC),除了关CPU的中断位,还要把中断控制器里的所有中断源都屏蔽掉。别问我怎么知道的——我曾经在这个坑里待过一整天。
3. 设置时钟:给系统一个心跳
时钟是SoC的“心跳”。没有时钟,CPU不跑,外设不转。U-Boot第一阶段要做的,是把系统时钟从默认的低频(比如24MHz晶振)倍频到目标频率。
不同的SoC,时钟树差异很大。但大体思路是一样的:
- 先配置PLL(锁相环),把低频倍频到高频
- 再配置分频器,给各个总线分配合适的时钟
- 最后切换时钟源,让CPU跑在新时钟上
以我常用的i.MX6为例,它的时钟设置代码在board/freescale/mx6q_sabresd/mx6q_sabresd.c里。核心函数是clock_init()。
/*
* i.MX6 时钟初始化示例(简化版)
*/
void clock_init(void)
{
/* 1. 配置PLL1为996MHz */
writel(0x0001C000, &ccm->analog_pll_arm);
/* 2. 等待PLL锁定 */
while (!(readl(&ccm->analog_pll_arm) & (1 << 31)));
/* 3. 设置分频,CPU主频=996MHz */
writel(0x00000000, &ccm->cacrr);
}
这里有个细节:PLL锁定需要时间。不同芯片的锁定时间不一样,有的几十微秒,有的几百微秒。我习惯在代码里加一个超时判断,防止硬件异常导致死循环。
个人经验:时钟设置完成后,建议读一下寄存器确认实际频率。我有一次发现PLL没锁定,代码却继续往下跑了,结果DDR初始化失败,查了半天才发现是时钟没起来。
4. 初始化DDR:给程序找个家
DDR初始化,是第一阶段里最复杂、也最容易出问题的一步。为什么?因为DDR的时序参数非常多,而且每个板子都不一样。
U-Boot里,DDR初始化通常由board_init_f()调用。它会做以下几件事:
- 配置DDR控制器:设置行列地址宽度、Burst长度、CAS延迟等
- 配置PHY:调整DQS信号延迟、ODT(片上端接)等
- 发送初始化序列:比如MRS(模式寄存器设置)、ZQ校准等
- 做读写测试:验证DDR是否正常工作
我见过最典型的错误是:DDR的时序参数直接从参考设计里抄过来,没做板级适配。结果板子跑起来,偶尔死机,偶尔正常。后来用示波器一量,发现DQS和时钟的相位关系不对。
/*
* DDR初始化中的读写测试(伪代码)
*/
int ddr_test(void)
{
volatile uint32_t *addr = (uint32_t *)0x80000000;
uint32_t pattern = 0xA5A5A5A5;
/* 写一个pattern */
*addr = pattern;
/* 读回来对比 */
if (*addr != pattern) {
printf("DDR test failed at 0x%08x\n", (uint32_t)addr);
return -1;
}
return 0;
}
关键提醒:DDR初始化成功后,一定要做全地址范围的读写测试。不要只测一个地址。我习惯写一个“走步”测试,从0地址开始,每隔1MB写一个pattern,然后读回来验证。这样能发现地址线短路或虚焊的问题。
5. 重定位:把U-Boot搬个家
重定位,说白了就是把U-Boot从Flash(或ROM)里,复制到DDR中运行。为什么要这么做?因为Flash的读取速度太慢了,而DDR可以跑几百兆赫兹。
U-Boot的重定位过程大致如下:
- 计算U-Boot镜像的大小
- 在DDR中分配一块连续内存
- 用
memcpy把代码从Flash搬到DDR - 修正代码中的绝对地址引用(relocation)
- 跳转到DDR中的新地址继续执行
这里有个坑:重定位之后,原来的代码还在Flash里,但PC指针已经指向DDR了。如果代码里有绝对地址引用(比如函数指针、全局变量地址),必须做修正。否则,访问的还是Flash里的旧数据。
U-Boot用了一个叫relocate_code()的函数来处理这件事。它会在重定位后,遍历一个“重定位表”,把每个需要修正的地址都加上一个偏移量。
/*
* 重定位的核心逻辑(简化版)
*/
void relocate_code(ulong addr)
{
ulong size = __bss_end - _start;
/* 把代码从Flash复制到DDR */
memcpy((void *)addr, (void *)_start, size);
/* 修正GOT表(全局偏移表) */
relocate_got(addr);
/* 跳转到DDR中的新地址 */
((void (*)(void))addr)();
}
我的习惯:重定位完成后,我会在DDR的新地址处打印一条信息,确认代码确实跑在DDR里了。比如打印当前PC指针的值。如果PC还在Flash地址范围,说明重定位没成功。
总结一下
U-Boot的第一阶段,就像给一个刚出生的婴儿穿衣服、喂奶、放到床上。每一步都有它的道理,顺序也不能乱。我整理了一个简单的对照表,方便你记忆:
| 步骤 | 核心任务 | 常见坑点 |
|---|---|---|
| CPU初始化 | 设置模式、关MMU、设栈 | 栈指针指向未初始化的DDR |
| 关闭中断 | 屏蔽IRQ/FIQ | 中断控制器里的中断没关干净 |
| 设置时钟 | 配置PLL和分频器 | PLL未锁定就继续执行 |
| 初始化DDR | 配置控制器和PHY | 时序参数未做板级适配 |
| 重定位 | 复制代码到DDR | 绝对地址未修正 |
好了,第一阶段的内容就讲到这里。下一讲,我们会进入第二阶段:板级初始化、设备树解析、加载内核。到时候你会发现,第一阶段的这些“脏活累活”,其实都是在为后面的“大戏”铺路。
记住一句话:Bootloader不嫌小,每一行汇编都有它的使命。