SPL在uboot的启动过程中是一个非常重要的概念,在uboot的启动相关代码中,可以看到很多与SPL相关判断和处理,了解SPL对于理解CPU以及整个系统的启动过程是很有帮助的。本文主要对以下内容进行介绍
什么是SPL,为什么需要SPL?
SPL在uboot编译层面是如何设计的?
启动过程中SPL主要做了什么?
为什么需要SPL?
SPL全程是Secondary Program Loader,叫做第二阶段引导程序,这个”Secondary”第二阶段是怎么来的呢?uboot目前将整个启动过程设计为4个阶段
ROM Boot
SPL
uboot
kernel
第一阶段ROM Boot是固化在SoC内部的一小段启动程序,一般由芯片厂商在出厂时固化好,一般不需要对这段代码关心,需要关心的是芯片的启动模式有哪些,如何设置启动模式,启动地址是多少。ROM Boot会引导SPL的启动,按照预先设置的启动模式,看是从SD卡、emmc还是Flash加载SPL到片上RAM中运行;SPL引导uboot的加载,将uboot拷贝到DDR中,重定向到uboot运行;uboot又加载kernel,重定向到kernel运行
以上这整个过程中,为什么不直接用ROM Boot引导uboot到RAM中,然后让uboot将kernel拷贝到DDR中再切换到kernel运行,而要多出来一个SPL多此一举呢?
关键点在于一方面uboot慢慢发展,代码量及功能越来越多,另一方面SoC厂商由于程本、面积等方面的考虑,一般不会将片内RAM设计的太大,一般不会超过100KB,而uboot编译下来一般都会超过200KB,因此在RAM空间上就产生了矛盾。uboot为了规避这种问题,引入了SPL的概念,即将原本的uboot功能一分为二,输出2个独立的程序,一个是SPL,另一个是真正的uboot,SPL程序只包含CPU底层相关的关键启动代码,体积较小,能够完全运行在片上RAM中,SPL负责对DDR进行初始化,并将uboot拷贝到DDR中,切换到uboot运行。下图显示了不同启动阶段及引导程序的运行介质
CPU上电后,首先是从片内ROM加载ROM Boot程序,进行片上系统初始化,然后将启动介质中的SPL加载到片内RAM运行,SPL对DDR进行初始化,然后将uboot拷贝到DDR中,uboot在DDR中运行,拷贝kernel到DDR中
从编译的层面理解SPL
SPL与真正的uboot在源代码上是同一套代码,通过编译选项CONFIG_SPL_BUILD
进行区分,在uboot代码的很多地方,通过以下形式区分了SPL处理还是uboot处理
1 |
|
在uboot的Makefile设计里,将最终uboot生成的二进制输出文件分离为了u-boot-spl.bin和u-boot.bin,u-boot-spl.bin就是SPL程序的二进制文件,而u-boot.bin是真正uboot的二进制文件。从u-boot-spl.lds和u-boot.lds链接文件可以看出,SPL和真正uboot的启动都是从_start符号开始,_start
->lowlevel_init
->_main
->board_init_f
的代码调用流程,SPL和uboot都会走一遍,只是其中执行的内容不同
SPL主要内容
这里以u-boot-2012.10版本源码为参考,主要分析ARMv7架构的SPL处理。下图是SPL整个运行过程调用时序
start.S
arch/arm/cpu/armv7/start.S,入口符号为_start
1 | .globl _start |
_start
地址为0x00000000,设置后续的几个地址为SPL的异常向量,跳转到reset
1 | reset: |
对armv7的cpsr寄存器进行设置,设置CPU的模式为超级模式
1 | #if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD)) |
通过armv7的CP15协处理器将异常向量设置到VBAR,关于ARMv7协处理器CP15以及VBAR的详细内容后续会出其他相关文章专门介绍。这里进行了CONFIG_SPL_BUILD
宏定义判断,说明设置这里的异常向量是SPL的操作,也就是说_start
后面的几个异常处理是针对SPL程序而言的,uboot应该会设置其他的异常向量
1 | #ifndef CONFIG_SKIP_LOWLEVEL_INIT |
这里是调用了cpu_init_cp15
和cpu_init_crit
两个函数
1 | ENTRY(cpu_init_cp15) |
cpu_init_cp15
的处理是通过设置CP15寄存器来禁用TLB、指令和数据cache,禁用MMU及cache
1 | ENTRY(cpu_init_crit) |
cpu_init_crit
的处理是直接跳转到lowlevel_init
,这定义在lowlevel_init.S中
lowlevel_init.S
arch/arm/cpu/armv7/lowlevel_init.S中只定义了lowlevel_init
一个函数
1 | ENTRY(lowlevel_init) |
这里有一个保存栈的操作,先将ip入栈,然后调用了s_init()
C函数,定义在xxx/board.c
中,主要是初始化系统时钟等,然后返回到start.S中这里
1 | /* Set stackpointer in internal RAM to call board_init_f */ |
调用lib/board.c中的board_init_f
函数
lib/board.c
arch/arm/lib/board.c中定义了board_init_f
1 | void board_init_f(ulong bootflag) |
维护了一个global data结构,计算uboot的地址及大小,初始化DDR,将uboot拷贝到DDR中,然后再调用relocate_code
将uboot拷贝到DDR中,并跳转到uboot