3. U-Boot启动流程分析:第一阶段(汇编)与第二阶段(C语言)的启动流程、SPL与TPL机制
大家好,我是你们的嵌入式系统讲师。今天我们来聊聊U-Boot的启动流程。说实话,这个主题我讲了不下几十遍,但每次讲都有新感悟。为什么?因为不同芯片、不同板子,启动流程的细节差异太大了。我当年第一次移植U-Boot时,就被这个启动流程折腾得够呛——板子死活不跑,最后发现是SPL阶段的一个跳转地址写错了。
好,咱们不扯远了。今天重点讲三块:第一阶段(汇编)干了什么、第二阶段(C语言)怎么接力,以及SPL和TPL这两个“小不点”到底有啥用。
3.1 第一阶段:汇编代码在忙什么?
U-Boot启动的第一阶段,说白了就是汇编代码。为什么非要用汇编?因为这时候C语言的环境还没准备好——栈指针没设、BSS段没清、内存控制器可能都没初始化。你想想看,C语言函数调用需要栈,没有栈怎么玩?
第一阶段的核心任务,我总结为四个字:“铺路搭桥”。具体来说:
- 设置异常向量表:CPU复位后第一条指令从哪取?就是向量表。我习惯把向量表放在0x00000000或0xFFFF0000,具体看芯片。
- 初始化CPU模式:比如ARM芯片,要切到SVC模式(超级用户模式),关中断。
- 初始化关键硬件:比如关闭看门狗、设置时钟、初始化串口(方便打印调试信息)。
- 设置栈指针:为C语言函数调用做准备。嗯,这一步特别容易忘,我当年就吃过这个亏。
- 清BSS段:把未初始化的全局变量清零。
- 跳转到C语言入口:通常是
board_init_f()或main()。
来看一段典型的ARM汇编启动代码(简化版):
reset:
/* 设置SVC模式,关中断 */
mrs r0, cpsr
bic r0, r0, #0x1f
orr r0, r0, #0xd3
msr cpsr, r0
/* 关看门狗(不同芯片寄存器不同) */
ldr r0, =WDT_BASE
mov r1, #0
str r1, [r0]
/* 初始化栈指针 */
ldr sp, =STACK_BASE
/* 清BSS段 */
ldr r0, =__bss_start
ldr r1, =__bss_end
mov r2, #0
clear_bss:
str r2, [r0], #4
cmp r0, r1
bne clear_bss
/* 跳转到C语言入口 */
bl board_init_f
这段代码看着简单,但每个芯片的细节都不一样。比如关看门狗,不同厂家的寄存器地址天差地别。我建议你移植时,先确认芯片手册里看门狗的基地址。
3.2 第二阶段:C语言开始接管
汇编把“路”铺好了,C语言就登场了。第二阶段的核心是board_init_f()和board_init_r()这两个函数。名字里的_f和_r,分别代表“前”和“后”——_f在重定位前执行,_r在重定位后执行。
第二阶段主要干这些事:
- 初始化板级硬件:比如定时器、GPIO、串口(方便打印)。
- 初始化内存控制器:让DDR/SDRAM能用。这一步很关键,因为后续代码要搬到内存里跑。
- 重定位U-Boot自身:把U-Boot从Flash/ROM复制到RAM中。为什么要搬?因为RAM里跑得快,而且可以写。
- 初始化设备驱动:比如网卡、MMC、USB等。
- 进入命令行:最后调用
main_loop(),等待用户输入命令。
这里有个坑:重定位时地址别算错。我曾经在移植时,把U-Boot搬到了错误的RAM地址,结果一运行就死机。排查了两天才发现是链接脚本里的地址定义错了。
核心要点:第一阶段是“铺路”,第二阶段是“盖楼”。汇编负责让CPU能跑C代码,C代码负责让板子能跑U-Boot命令。
3.3 SPL与TPL:为什么需要“双重加载”?
好,接下来聊SPL和TPL。这两个东西,说白了就是U-Boot的“迷你版”。为什么需要它们?因为有些芯片的片上SRAM太小了,装不下完整的U-Boot。比如某些单片机,SRAM只有4KB或8KB,而U-Boot编译出来动不动几百KB。怎么办?分步加载。
SPL(Secondary Program Loader):这是第一级加载器。它体积小,能放进SRAM里。它的任务很简单:初始化内存控制器,然后把完整的U-Boot从Flash搬到DDR里。
TPL(Tertiary Program Loader):这是第二级加载器。有些芯片更变态,连SPL都放不下,于是又加了一层TPL。TPL比SPL还小,只做最基础的事情——比如初始化时钟和内存,然后加载SPL,SPL再加载U-Boot。
我画了个流程图,帮你理解这个“套娃”过程:
你看这个图,从ROM到TPL到SPL再到U-Boot,像不像俄罗斯套娃?每一层都比上一层大,但功能也更完整。
我的经验:如果你在移植时发现SPL太大了放不进SRAM,可以考虑启用TPL。把最基础的初始化(比如时钟和内存控制器)放到TPL里,SPL只负责加载U-Boot。这样SPL的体积能再砍掉一半。
3.4 避坑指南:启动流程中的常见问题
最后,我分享几个实战中遇到的坑,希望能帮你少走弯路。
| 问题 | 现象 | 解决方案 |
|---|---|---|
| SPL加载U-Boot后死机 | 串口打印“Starting U-Boot...”后无响应 | 检查U-Boot的链接地址是否与SPL中设置的加载地址一致 |
| TPL无法初始化DDR | 板子反复重启 | 确认DDR时序参数是否正确,我建议用芯片厂商提供的DDR初始化代码 |
| 汇编阶段串口无输出 | 完全黑屏 | 检查串口引脚配置和波特率设置,有时是GPIO复用没配好 |
| 重定位后函数调用崩溃 | 执行到某个函数时跳飞 | 检查链接脚本中的重定位表(.rel.dyn段)是否正确生成 |
警告:千万不要在SPL或TPL里使用复杂的C库函数(比如printf的浮点支持)。这些函数体积大,而且依赖完整的C运行时环境。我见过有人把printf的浮点支持编译进SPL,结果SPL体积直接超了SRAM限制。
好了,关于U-Boot启动流程的第一阶段、第二阶段以及SPL/TPL机制,今天就讲到这里。记住:启动流程是移植的基础,这块搞不明白,后面寸步难行。下次遇到板子不启动,先按这个流程排查一遍,八成能找到问题。
可可精品资料库