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。

我画了个流程图,帮你理解这个“套娃”过程:

U-Boot启动流程:SPL/TPL机制 ROM Bootloader (芯片固化代码) 加载 TPL (极简初始化:时钟+内存) 加载 SPL (初始化DDR,加载U-Boot) 加载 完整U-Boot (在DDR中运行) 注意: 1. 有些芯片没有TPL,直接从ROM加载SPL 2. SPL和TPL的代码量通常 < 32KB 3. 每一步都要检查地址对齐

你看这个图,从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机制,今天就讲到这里。记住:启动流程是移植的基础,这块搞不明白,后面寸步难行。下次遇到板子不启动,先按这个流程排查一遍,八成能找到问题。

可可精品资料库