u-boot启动流程分析
以smdk2410为例,分析u-boot的启动流程。u-boot的启动流程是指从cpu上电开机执行u-boot到u-boot成功加载完操作系统的过程。这一过程可以分为两个阶段,各个阶段的功能如下:
第一阶段的功能:
第二阶段的功能:
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 ⑽设置栈 栈是执行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启动的第二阶段流程如下图: 首先,介绍一些重要的数据结构 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结构体用于存放板级相关的全局数据,是gd_t中结构体指针成员bd的结构体类型,其在include/asm-arm/u-boot.h文件中定义如下: u-boot启动内核时要给内核传递参数,这时需要使用gd_t、bd_t结构体中的信息来设置标记列表。 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()函数在lib_arm/board.c中定义,具体代码及注释如下: 从上述代码的注释中可知,start_armboot()在进行一系列的初始化(包括对全局数据指针gd、堆、各种设备和环境变量的初始化)后,最终调用main_loop()进入u-boot主循环中。 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无法启动。该功能适用于商业产品中。 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),终端将输出“