u-boot启动流程分析
以smdk2410为例,分析u-boot的启动流程。u-boot的启动流程是指从cpu上电开机执行u-boot到u-boot成功加载完操作系统的过程。这一过程可以分为两个阶段,各个阶段的功能如下:
第一阶段的功能:
- 硬件设备初始化;
- 加载u-boot第二阶段的代码到RAM空间;
- 设置好栈;
- 跳转到u-boot第二阶段代码的入口处;
第二阶段的功能:
- 初始化本阶段使用的硬件设备;
- 检测系统内存映射;
- 将Linux内核从Flash读取到RAM中;
- 为Linux内核设置启动参数;
- 调用Linux内核。
1、u-boot启动流程的第一阶段代码分析
根据连接器脚本cpu/arm920t/u-boot.lds中指定的链接方式可知,u-boot代码段第一个链接的是cpu/arm920t/start.o,入口标号是_start,因此u-boot的入口代码在对应源文件cpu/arm920t/start.S中。下面分析cpu/arm920t/start.S的执行。
⑴设置异常向量
当一个异常或中断发生时,cpu会把pc指针设置为一个特定的存储器地址,这一地址存放在一个被称为向量表(vector table)的特定地址范围内。向量表的入口是一些跳转指令,跳转到专门处理某个特定异常或中断的子程序处。ARM的异常向量表及其各个异常向量的介绍如下表所示。
地址 | 异常类型 | 进入模式 | 说明 |
0x00000000 | 复位 | 管理模式 | 复位电平有效时产生 |
0x00000004 | 未定义指令 | 未定义指令模式 | 遇到ARM处理器无法识别的指令时产生 |
0x00000008 | 软件中断 | 管理模式 | SWI指令产生 |
0x0000000c | 预取指令 | 中止模式 | 当获取的指令不存在时产生 |
0x00000010 | 数据访问 | 中止模式 | 当获取的数据不存在时产生 |
0x00000014 | 保留 | 保留 | 保留 |
0x00000018 | IRQ | IRQ模式 | 中断请求有效,并且CRSR中的I位为0 |
0x0000001c | FIQ | FIQ模式 | 快速中断请求有效,并且CRSR中的F位为0 |
cpu/arm920t/start.S开头有如下代码用于设置上述异常向量表。
上述代码表明,当一个异常产生时,cpu根据异常号,在异常向量表中找到对应的异常向量,然后执行异常向量处的跳转指令,cpu跳转到对应的异常处理程序执行。例如,复位异常向量的指令“b start_code”决定u-boot启动后将自动跳转到标号“start_code”处执行。
⑵CPU进入管理模式(SVC)
在cpu/arm920t/start.S文件中有如下代码:
通过上述代码,u-boot将cpu的工作模式设置为管理模式,并将普通中断IRQ和快速中断FIQ的禁止位置1,从而屏蔽IRQ和FIQ中断。
⑶设置控制寄存器地址
在cpu/arm920t/start.S文件中有如下代码:
对于smdk2410开发板,上述代码完成WATCHDOG、INTMSK、INTSUBMSK、CLKDIVN这4个寄存器地址的设置。相关寄存器地址需要查看smdk2410的datasheet。
⑷关闭看门狗
在cpu/arm920t/start.S文件中有如下代码:
上述代码向看门狗控制寄存器写入0,从而关闭看门狗。否则,在u-boot启动过程中,cpu可能因为看门狗定时器超时而不断重启。
⑸屏蔽中断
在cpu/arm920t/start.S文件中有如下代码:
上述代码向主中断屏蔽寄存器INTMSK写入0xffffffff,即将INTMSK寄存器全部置1,从而屏蔽对应的中断。INTMSK的每一位对应SRCPND(源中断未决寄存器)中的一位,表明SRCPND相应位代表的中断请求是否会被cpu处理。
在cpu/arm920t/start.S文件中有如下代码:
上述代码屏蔽了所有SUBSRCPND中对应的中断。因为INTSUBMSK中的每一位对应SUBSRCPND中的一位,表明SUBSRCPND相应位代表的中断请求是否会被cpu所处理。
具体的中断和相关寄存器位需要查阅芯片的datasheet。
⑹设置MPLLCON、UPLLCON和CLKDIVN
cpu上电几毫秒后,晶振输出稳定,主频FCLK=Fin(晶振频率),cpu开始执行指令。但实际上,FCLK可以高于Fin。为了提高系统时钟,需要用软件来启动PLL(锁相环)。这就需要设置MPLLCON、UPLLCON和CLKDIVN这3个寄存器。
CLKDIVN寄存器用于设置FCLK、HCLK、PCLK三者之间的比例,可以根据下表设置(其实应该根据smdk2410的datasheet来设置)。
CLKDIVN | 位 | 说明 | 初始化 |
HCLK | [2:1] | 00:HCLK=FCLK/1 01:HCLK=FCLK/2 | 00 |
PCLK | [0] | 0: PCLK=HCLK/1 1:PCLK=HCLK/2 | 0 |
在cpu/arm920t/start.S文件中有如下代码:
上述代码将CLKDIVN寄存器设置为3,也就是将HCLK和PCLK分别设置为01和1,因此HCLK=FCLK/2,PCLK=HCLK/2。由此得出FCLK、HCLK、PCLK三者之间的比例关系为4:2:1。
Smdk2410中的MPLLCON寄存器用于设置FCLK与Fin的倍数,MPLLCON的位[19:12]称为MDIV,位[9:4]称为PDIV,位[1:0]称为SDIV。smdk2410的FCLK与Fin满足下面的公式:
Mpll=(m*Fin)/(p*2s),其中,m=(MDIV+8),p=(PDIV+2),s=SDIV。
MPLLCON与UPLLCON的寄存器的值要参考smdk2410的datasheet。
输入频率 | 输出频率 | MDIV | PDIV | SDIV |
12.0000MHz | 48.00MHz | 120(0x78) | 2 | 3 |
12.0000MHz | 202.80MHz | 161(0xa1) | 3 | 1 |
例如,将smdk2410时钟频率设置为48MHz时,系统可以稳定运行,因此设置MPLLCON与UPLLCON为:
MPLLCON=(0xA1<<12)|(0x03<<4)|(0x01)
UPLLCON=(0x78<<12)|(0x02<<4)|(0x03)
提示:MPLLCON与UPLLCON的初始化代码实际上位于board/samsung/smdk2410/smdk2410.c中,该文件存放u-boot第二阶段的初始化代码。
⑺关闭内部指令集(MMU)和cache
在cpu/arm920t/start.S文件中有如下代码:
cpu_init_crit代码段在u-boot正常启动时才需要执行,若将u-boot从内存中启动,则应该注释掉这段代码。
下面分析cpu_init_crit代码段到底做了什么。在cpu/arm920t/start.S文件中有如下代码:
在上述代码中,c0,c1,c7,c8都是ARM920t的协处理器CP15的寄存器。其中c7是cache控制寄存器,c8是TLB控制寄存器。第240~242行代码将0写入c7,c8,使cache和TLB内容无效。第247~252行代码关闭MMU,这是通过修改协处理器CP15的c1寄存器来实现的,CP15的c1寄存器格式如下表(可参考arm920t的数据手册)。可见,第247~252行代码是通过将c1寄存器的M位置0来关闭MMU的。
15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
. | . | V | I | . | . | R | S | B | . | . | . | . | C | A | M |
※V表示异常向量表所在的位置,0表示异常向量在0x00000000;1表示异常向量在0xFFFF0000;
※I为0时表示关闭Icaches;为1时表示开启Icaches;
※R、S用来与页表中的描述符一起确定内存的访问权限;
※B为0表示CPU为小字节序;为1表示CPU为大字节序;
※C为0表示关闭Dcaches;为1表示开启Dcaches;
※A为0表示数据访问时不进行地质对齐检查;为1表示数据访问时进行地址对齐检查;
※M为0表示关闭MMU;为1表示开启MMU。
⑻初始化存储控制器
内存初始化依赖于开发板,因此其初始化代码一般在board目录下对应的目录中,针对smdk2410,其内存初始化是在board/samsung/smdk2410/lowlevel_init.S中完成的。在board/samsung/smdk2410/lowlevel_init.S中有如下一段代码:
在上述代码中,lowlevel_init设置了13个寄存器来对存储控制器时序进行初始化。当u-boot从NOR Flash启动时,由于u-boot连接时确定的地址是内存中的地址,而此时u-boot还在NOR Flash中,因此最终需要将之从NOR Flash中复制到RAM中运行。
由于NOR Flash的起始地址为0,而u-boot加载到内存的起始地址是TEXT_BASE,所以SMRDATA标号在Flash中的地址就是SMRDATA减去TEXT_BASE。
因此,lowlevel_init的作用是将SMRDATA开始的13个值复制给开始地址[BWSCON]的13个寄存器,从而完成存储控制器的设置。
⑼复制u-boot第二阶段代码到RAM中
在cpu/arm920t/start.S文件中有如下代码:
在上述代码中,
第179行:当CONFIG_SKIP_RELOCATE_UBOOT宏被定义时,将跳过上述代码;
第181行:adr是相对寻址指令,adr r0,_start表示将利用PC寄存器的当前值减去该处代码连接地址与_start标号地址的偏移得到的相对地址传递给r0,也就是r0表示u-boot的运行地址;
第182行:将u-boot连接地址TEXT_BASE赋值给r1;
第183行:比较u-boot当前运行地址与连接地址是否一致;
第184行:如果一致,则无需赋值代码,跳至stack_setup标号处;
第186行:将_armboot_start表示的u-boot代码段地址赋值给r2;
第187行:将_bss_start表示的BSS段起始地址赋值给r3;
第188行:从连接器脚本可以看出,BSS段之前是代码段和数据段,因此r3-r2得到是代码段和数据段的总长度。这里将其赋值给r2;
第189行:再将r2+r0赋值给r2,这时r2表示需要复制的u-boot的结束地址;
第192~193行:利用多寄存器寻址提高复制效率,r0和r1自动生长;
第194~195行:判断r0是否等于r2,即是否到达结束地址,若r0<r2,则跳回copy_loop继续复制。
⑽设置栈
栈是执行C程序的必要条件,因此,在进入C语言实现的初始化代码之前,需要通过汇编进行栈的初始化。在cpu/arm920t/start.S文件中有如下代码:
根据上述代码,ARM处理器栈指针sp默认向下生长,因此只需要将sp指针指向一段低地址没有被使用的内存,即可完成栈的设置。
⑾清楚BSS段
在调用C代码之前,还需要由u-boot将BSS段中存放的未初始化全局变量、静态变量清零。在cpu/arm920t/start.S文件中有如下代码:
第209~211行:执行完成后,r0、r1将分别存放BSS段的起始地址和结束地址,r2被清零;
第213~216行:执行循环,将r0、r1之间的区域清零。
⑿跳转到第二阶段代码入口
经过上述初始化过程后,u-boot已经具备了在内存中执行C语言的能力,现在只需执行一条ldr指令就可以跳转至内存中的第二阶段初始化入口start_armboot,在cpu/arm920t/start.S文件中有如下代码:
2、u-boot启动流程的第二阶段代码分析
u-boot启动流程的第二阶段初始化代码的入口start_armboot在lib_arm/board.c中定义,u-boot启动的第二阶段流程如下图:
首先,介绍一些重要的数据结构
- gd_t结构体
u-boot使用gd_t结构体来存储全局数据区的数据,这个结构体在include/asm-arm/global_data.h中定义:
u-boot使用了一个存储在寄存器中的指针gd来记录全局数据区的地址,arm/global_data.h中有如下代码:
DECLARE_GLOBAL_DATA_PTR定义一个gd_t全局数据结构的指针,这个指针存放在指定的寄存器r8中。该声明避免了编译器把r8分配给其他变量。对于任何想要访问全局数据区的代码,只要在其开头加入“DECLARE_GLOBAL_DATA_PTR”一行代码,就可以使用gd指针访问全局数据区了。
根据u-boot内存分配图可以计算gd的值,
gd=TEXT_BASE-CONFIG_SYS_MALLOC_LEN-sizeof(gd_t)
这与lib_arm/board.c文件中start_armboot()函数中对gd指针的初始化一致,lib_arm/board.c文件中的相关代码如下:
上述代码中,全局标号_armboot_start在cpu/arm920t/start.S文件中有定义,
在这里,_armboot_start在此处取_start标号的值,也就是u-boot镜像的起始地址TEXT_BASE。
- bd_t结构体
bd_t结构体用于存放板级相关的全局数据,是gd_t中结构体指针成员bd的结构体类型,其在include/asm-arm/u-boot.h文件中定义如下:
u-boot启动内核时要给内核传递参数,这时需要使用gd_t、bd_t结构体中的信息来设置标记列表。
- init_sequence数组
u-boot使用一个数组init_sequence来存储大多数开发板都要执行的初始化函数的函数指针。init_sequence数组中有较多的编译选项。init_sequence数组在lib_arm/board.c文件中定义,相关代码如下:
在上述代码中,board_init函数在board/samsung/smdk2410/smdk2410.c中定义,该函数配置了MPLLCON、UPLLCON和部分GPIO寄存器的值,还设置了u-boot机器码和内核启动参数地址。具体代码如下:
dram_init函数在board/samsung/smdk2410/smdk2410.c中定义,具体代码如下:
由于smdk2410使用两片32MB的SDRAM组成了64MB的内存,这两片内存连接在存储控制器的BANK6上,地址空间是0x30000000~0x34000000。在include/configs/smdk2410.h中,PHYS_SDRAM_1和PHYS_SDRAM_1_SIZE分别被定义为0x30000000和0x04000000。
- 对start_armboot()函数的分析
start_armboot()函数在lib_arm/board.c中定义,具体代码及注释如下:
从上述代码的注释中可知,start_armboot()在进行一系列的初始化(包括对全局数据指针gd、堆、各种设备和环境变量的初始化)后,最终调用main_loop()进入u-boot主循环中。
- main_loop()函数的分析
main_loop()函数在common/main.c文件中定义,做的都是与具体平台无关的工作,主要包括初始化启动次数限制机制、设置软件版本号、打印启动信息、解析命令等。
-
- 设置启动次数有关参数
u-boot进入main_loop()函数后,首先根据配置加载已经保留的启动次数,并且根据配置判断是否超过启动次数。common/main.c文件中的相关代码如下:
第313~315行:加载保存的启动次数至变量bootcount,并将启动次数加1后重新保存;
第316~317行:打印启动次数,然后设置启动次数到环境变量“bootcount”;
第318~319行:从环境变量“bootlimit”中读出启动变量限制至bootlimit;
第312~320行:实现启动次数限制功能。启动次数限制可以设置为一个启动次数,然后保存在Flash存储器的特定位置,当到达启动次数后,u-boot无法启动。该功能适用于商业产品中。
-
- 启用Modem功能
common/main.c文件中的第322~331行实现Modem功能。如果系统中有Modem,打开该功能可以接受其他用户通过电话网络的拨号请求。Modem功能通常供一些远程控制的系统使用。
-
- 设置版本号、初始化命令自动完成功能
common/main.c文件中如下代码用来设置版本号、初始化命令自动完成功能。
第333~339行:支持动态版本号,version_string变量是在其他文件中定义的一个字符串变量,当用户改变u-boot版本时会更新该变量。在打开动态版本支持功能后,u-boot在启动时会显示最新版本号。
第350行:设置命令行自动完成功能,该功能与Linux的Shell类似,当用户输入一部分命令后,可以按Tab键补全命令的剩余部分;
第353~370行:判断是否支持预启动命令,若果支持,则从环境变量中取出相关命令执行。
注意:在嵌入式系统中,不同系统的Flash存储空间不同。对于一些Flash空间比较紧张的设备来说,通过宏开关关闭一些不必要的功能,如命令自动完成,可以减小u-boot编译后的文件大小。u-boot正是基于这种思想才在代码中添加了大量的宏开关,以便于工程师根据自己的需要进行裁剪。
-
- 启动延时和启动菜单
在u-boot进入主循环之前,如果配置了启动延时功能,需要等待用户从串口或网络接口输入。如果用户按下任意键打断启动流程,则向终端打印出一个启动菜单。common/main.c文件中有如下代码:
第372~376行:获得表示启动延时的环境变量“bootdelay”,如果不存在,则从配置宏CONFIG_BOOTDELAY获取,并保持在变量bootdelay中。
第388~395行:判断启动次数是否超过限制,如果是,则执行环境变量“altbootcmd”表示替代启动命令,替代启动命令可以用来为未授权的商业产品保留一些功能;
第396~415行:从环境变量“bootcmd”获取启动命令,在变量bootdelay表示的启动延时大于等于0且启动流程未被中止(abortboot()返回1)的情况下,执行启动命令。
第417~429行:检查是否是因为CONFIG_MENUKEY宏指定的按键被按下而中止了启动,如果是,则从环境变量“menucmd”中取出菜单命令并执行,以调出启动菜单。
第432~437行:如果配置了视频设备,则显示启动图标。
-
- 执行命令循环
common/main.c文件中有如下代码:
U-boot的各个功能设置完毕后,main_loop函数在第477行进入一个for死循环,该循环不断使用readline()函数(第456行)从控制台(一般是串口)读取用户的输入,再通过run_command()函数(第480行)执行命令。
第461~462行:如果用户直接按回车键(此时命令长度len等于0),就会在第480行重复执行上一次的命令;
第477~478行:如果用户按Ctrl+C组合键(此时命令长度等于-1),终端将输出“<INTERRUPT>”,表示上次命令执行被中断了。