U-Boot,全称 Universal Boot Loader,即通用引导程序,是遵循GPL条款的开放源码项目。它的源码目录、编译形式与Linux内核很相似,事实上,不少U-Boot源码就是相应的Linux内核源程序的简化,尤其是一些设备的驱动程序,这从U-Boot源码的注释中能体现这一点。U-Boot不仅仅支持嵌入式Linux系统的引导,它还支持NetBSD,
VxWorks, QNX, RTEMS, ARTOS, LynxOS嵌入式操作系统。
U-Boot的工作模式有启动加载模式和下载模式。启动加载模式是Bootloader的正常工作模式,嵌入式产品发布时,Bootloader必须工作在这种模式下,Bootloader将嵌入式操作系统从FLASH中加载到SDRAM中运行,整个过程是自动的。下载模式就是Bootloader通过某些通信手段将内核映像或根文件系统映像等从PC机中下载到目标板的FLASH中。用户可以利用Bootloader提供的一些命令接口来完成自己想要的操作。
Android体系中U-Boot的大致引导流程:
U-Boot的源码目录结构
1、board:开发板相关的配置文件,一个子文件对应一个开发板配置;
2、common:通用的多功能函数实现,比如环境,命令,控制台相关函数等;
3、cpu:存放特定CPU结构相关的目录;
4、disk:硬盘接口程序;
5、doc:
6、drivers:支持的各种设备的驱动程序;
7、dtt:数字温度测量器或传感器的驱动;
8、examples:这个不解析;
9、fs:文件系统,最常用的是JFFS2文件系统;
10、include:头文件和开发板配置文件,开发板的配置文件放在include/configs目录下;
11、lib_generic:通用函数库,比如printf函数等;
12、lib_*:某一具体平台架构下的通用函数库,lib_i386就对应i386系统架构的通用函数库;
13、nand_spl:
14、net:与网络协议相关的代码,bootp协议、TFTP协议、NFS文件系统的实现;
15、post:上电自检程序;
16、rtc:实时时钟的驱动;
17、temp:
18、tools:制作s-record、u-boot格式映像的工具,比如mkimage。
U-Boot启动流程(简约)
大多数BootLoader都分为stage1和stage2两大部分,U-boot也不例外。依赖于cpu体系结构的代码(如设备初始化代码等)通常都放在stage1且可以用汇编语言来实现,而stage2则通常用C语言来实现,这样可以实现复杂的功能,而且有更好的可读性和移植性。
1、 stage1(start.s代码结构)
U-boot的stage1代码通常放在start.s文件中,它用汇编语言写成,其主要代码部分如下:
(1) 定义入口。由于一个可执行的image必须有一个入口点,并且只能有一个全局入口,通常这个入口放在rom(Flash)的0x0地址,因此,必须通知编译器以使其知道这个入口,该工作可通过修改连接器脚本来完成。
(2)设置异常向量(exception vector)。
(4)初始化内存控制器 。
(5)将rom中的程序复制到ram中。
(6)初始化堆栈 。
(7)转到ram中执行,该工作可使用指令ldrpc来完成。
2、 stage2(C语言代码部分)
lib_arm/board.c中的start armboot是C语言开始的函数,也是整个启动代码中C语言的主函数,同时还是整个u-boot(armboot)的主函数,该函数主要完成如下操作:
(1)调用一系列的初始化函数。
(2)初始化flash设备。
(3)初始化系统内存分配函数。
(4)如果目标系统拥有nand设备,则初始化nand设备。
(5)如果目标系统有显示设备,则初始化该类设备。
(6)初始化相关网络设备,填写ip,c地址等。
(7)进入命令循环(即整个boot的工作循环),接受用户从串口输入的命令,然后进行相应的工作。
下面结合SMDK2410(ARM920T)的U-Boot的源码来分析下U-Boot的整个引导流程。
Stage I过程分析
从文件层面上看主要流程是在这几个文件中进行的:cpu/xxx/start.S、board/xxx/lowlevel_init.S、lib_arm_board.c。
设置中断向量表
cpu/arm920t/start.S开头有如下代码:
/* ************************************************************************* * * Jump vector table as in table 3.1 in [1] * ************************************************************************* */ .globl _start _start: b reset /* 复位,CPU复位,b是不带返回的跳转,即无条件直接跳转至reset处执行 */ ldr pc, _undefined_instruction /* 未定义指令向量 */ ldr pc, _software_interrupt /* 软件中断向量 */ ldr pc, _prefetch_abort /* 预取指令异常向量 */ ldr pc, _data_abort /* 数据操作异常向量 */ ldr pc, _not_used /* 未使用 */ ldr pc, _irq /* irq中断向量 */ ldr pc, _fiq /* fiq中断向量 */ _undefined_instruction: .word undefined_instruction _software_interrupt: .word software_interrupt _prefetch_abort: .word prefetch_abort _data_abort: .word data_abort _not_used: .word not_used _irq: .word irq _fiq: .word fiq .balignl 16,0xdeadbeef /* 16字节对齐,不足之处,用0xdeadbeef填充(搞笑,用“死牛”填充) */
红色标记的"undefined_instruction"是一个标号,即地址值,对应的就是发生"未定义指令"中断时,系统处理该中断的代码的地址。
/* * exception handlers */ .align 5 undefined_instruction: get_bad_stack bad_save_user_regs bl do_undefined_instruction .align 5 software_interrupt: get_bad_stack bad_save_user_regs bl do_software_interrupt .align 5 prefetch_abort: get_bad_stack bad_save_user_regs bl do_prefetch_abort .align 5 data_abort: get_bad_stack bad_save_user_regs bl do_data_abort .align 5 not_used: get_bad_stack bad_save_user_regs bl do_not_used #ifdef CONFIG_USE_IRQ .align 5 irq: get_irq_stack irq_save_user_regs bl do_irq irq_restore_user_regs .align 5 fiq: get_fiq_stack /* someone ought to write a more effiction fiq_save_user_regs */ irq_save_user_regs bl do_fiq irq_restore_user_regs #else .align 5 irq: get_bad_stack bad_save_user_regs bl do_irq .align 5 fiq: get_bad_stack bad_save_user_regs bl do_fiq #endif
在cpu/arm920t/interrupts.c中有对应的do_undefined_instruction方法。
void do_undefined_instruction (struct pt_regs *pt_regs) { printf ("undefined instruction\n"); show_regs (pt_regs); bad_mode (); } void do_software_interrupt (struct pt_regs *pt_regs) { printf ("software interrupt\n"); show_regs (pt_regs); bad_mode (); } void do_prefetch_abort (struct pt_regs *pt_regs) { printf ("prefetch abort\n"); show_regs (pt_regs); bad_mode (); } void do_data_abort (struct pt_regs *pt_regs) { printf ("data abort\n"); show_regs (pt_regs); bad_mode (); } void do_not_used (struct pt_regs *pt_regs) { printf ("not used\n"); show_regs (pt_regs); bad_mode (); } void do_fiq (struct pt_regs *pt_regs) { printf ("fast interrupt request\n"); show_regs (pt_regs); bad_mode (); } void do_irq (struct pt_regs *pt_regs) { #if defined (CONFIG_USE_IRQ) && defined (CONFIG_ARCH_INTEGRATOR) /* ASSUMED to be a timer interrupt */ /* Just clear it - count handled in */ /* integratorap.c */ *(volatile ulong *)(CFG_TIMERBASE + 0x0C) = 0; #else printf ("interrupt request\n"); show_regs (pt_regs); bad_mode (); #endif }
当一个中断发生,CPU首先会在中断向量表中查找对应的中断向量,然后跳转到对应的中断处理程序处执行。
CPU复位操作,会使CPU进入SVC模式。
设置CPU进入SVC模式
ARM处理器有7中模式,由CPSR的[4:0]决定。
用户模式USR:正常程序运行的工作模式。只能读CPSR。
系统模式SYS:与用户模式共用一套寄存器。用于支持操作系统的特权任务模式,它可以直接切换到其他模式。
管理模式SVC:操作系统的特权任务模式。系统复位和软件中断时才能进入该模式。
除了用户模式外,其他模式都是特权模式。只有在特权模式下,才允许对当前的程序状态寄存器的控制位直接进行读写。特权模式中除了系统模式外,都是异常模式。特权模式可以访问所有系统资源。一般,进入特权模式是为了处理中断、异常或者访问被保护的资源。
ARM处理器的工作模式:
处理器模式 | 特权模式 | 说明 | CPSR[4:0] |
用户模式USR | 否 | 用户程序运行模式 | 10000 |
系统模式SYS | 是 | 运行特权级的操作系统级任务 | |
管理模式SVC | 是 | 提供操作系统使用的保护模式 | 10011 |
异常终止模式ABT | 是 | 用于虚拟存储及存储保护 | 10111 |
未定义模式UND | 是 | 用于支持通过软件仿真硬件的协处理器 | 11011 |
一般中断模式IRQ | 是 | 用户通常的中断使用 | 10010 |
快速中断模式FIQ | 是 | 用于高速数据传输和通道处理 | 10001 |
CPSR,Current Program Status Register,当前程序状态寄存器。
/* * the actual reset code */ reset: /* * set the cpu to SVC32 mode */ mrs r0,cpsr bic r0,r0,#0x1f /* 位清零,工作模式为清零 */ orr r0,r0,#0xd3 /* 逻辑或,工作模式为设为'(110)10011',即设置CPU为SVC模式,并将中断禁止位和快中断禁止位置1(屏蔽)*/ msr cpsr,r0
设置控制寄存器,关看门狗,关中断
看门狗是一个硬件模块,当系统出现死机现象,看门狗就会自动重启系统。看门狗的硬件逻辑:其硬件有一个记录超时功能,它要求用户每隔一段时间(根据需求配置)对某个寄存器置位(简称“喂狗”),若超时为进行“喂狗”动作,系统就会重启。
/* turn off the watchdog */ #if defined(CONFIG_S3C2400) # define pWTCON 0x15300000 /* 看门狗寄存器 */ # define INTMSK 0x14400008 /* Interupt-Controller base addresses 中断屏蔽寄存器 */ # define CLKDIVN 0x14800014 /* clock divisor register 时钟分频寄存器 */ #elif defined(CONFIG_S3C2410) # define pWTCON 0x53000000 # define INTMSK 0x4A000008 /* Interupt-Controller base addresses */ # define INTSUBMSK 0x4A00001C # define CLKDIVN 0x4C000014 /* clock divisor register */ #endif #if defined(CONFIG_S3C2400) || defined(CONFIG_S3C2410) ldr r0, =pWTCON mov r1, #0x0 /* 向看门狗寄存器写入0 */ str r1, [r0]
/* * mask all IRQs by setting all bits in the INTMR - default */ mov r1, #0xffffffff ldr r0, =INTMSK /* 屏蔽所有中断 */ str r1, [r0]
INTMSK寄存器是一个32位寄存器,每一位对应一个中断,向其中写入0xffffffff,即将INTMSK寄存器全部置1,从而屏蔽对应的中断。
INTSUBMSK寄存器也是一个32位寄存器,但是只使用了低15位,向其中写入0x7fffffff,即可屏蔽对应的中断。
以上代码完成了对pWTCON、INTMSK、INTSUBMSK、CLKDIVN四个寄存器的地址设置,并且通过向看门狗寄存器写入0,禁止看门狗的复位功能(即重启功能),否则,在U-Boot启动过程中,CPU将不断重启。
初始化CPU时钟
关闭MMU和Cache
MMU,Memory Management Unit,即内存管理单元。MMU是CPU中用来管理虚拟存储器、物理存储器的控制线路,同时负责将虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权。
/* * we do sys-critical inits only at reboot, * not when booting from ram! */ #ifndef CONFIG_SKIP_LOWLEVEL_INIT bl cpu_init_crit #endif
cpu_init_crit到底做了哪些操作呢?
#ifndef CONFIG_SKIP_LOWLEVEL_INIT cpu_init_crit: /* * flush v4 I/D caches */ mov r0, #0 mcr p15, 0, r0, c7, c7, 0 /* flush v3/v4 cache 向c7写入0 */ mcr p15, 0, r0, c8, c7, 0 /* flush v4 TLB 向c8写入0 */ /* * disable MMU stuff and caches */ mrc p15, 0, r0, c1, c0, 0 /* 读出控制寄存器到r0中 */ bic r0, r0, #0x00002300 @ clear bits 13, 9:8 (--V- --RS) bic r0, r0, #0x00000087 @ clear bits 7, 2:0 (B--- -CAM) orr r0, r0, #0x00000002 @ set bit 2 (A) Align orr r0, r0, #0x00001000 @ set bit 12 (I) I-Cache mcr p15, 0, r0, c1, c0, 0 /* 保存r0到控制寄存器中 */ /* * before relocating, we have to setup RAM timing * because memory timing is board-dependend, you will * find a lowlevel_init.S in your board directory. */ mov ip, lr bl lowlevel_init /* 跳转到lowlevel_init,执行完lowlevel_init后返回 */ mov lr, ip mov pc, lr #endif /* CONFIG_SKIP_LOWLEVEL_INIT */
注:TLB,Translation Lookaside Buffer,即旁路缓冲,它的作用是在处理器访问内存数据的时候做地址转换。TLB中存放了一些页表文件,文件中记录了虚拟地址和物理地址的映射关系。当程序访问呢一个虚拟地址,会从TLB中查询出对应的物理地址,然后访问物理地址。
代码中的c0、c1、c7、c8是ARM920T的协处理器CP15的寄存器。其中,c7是cache控制寄存器,c8是TLB控制寄存器。
关闭MMU是通过修改CP15协处理器的c1寄存器来实现的。CP15的c1寄存器格式如下:
15 |
14 |
13 |
12 |
11 |
10 |
9 |
8 |
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
· |
· |
V |
I |
· |
· |
R |
S |
B |
· |
· |
· |
· |
C |
A |
M |
对应位的意义如下:
位 |
置0 |
置1 |
|
V |
异常向量在0x00000000 |
异常向量在0xFFFF0000 |
|
I |
关闭ICaches |
开启ICaches |
|
R、S |
用来与页表中的描述符一起确定内存的访问权限 |
||
B |
CPU为小字节序 |
CPU为大字节序 |
|
C |
关闭DCaches |
开启DCaches |
|
A |
数据访问时不进行地址对齐检查 |
数据访问时进行地址对齐检查 |
|
M |
关闭MMU |
开启MMU |
初始化内存
lowlevel_init完成了内存初始化的任务,由于内存初始化时依赖开发板的,所以lowlevel_init的代码一般放在board下面相应的目录。对于SMDK2410,lowlevel_init的代码在board/smdk2410/lowlevel_init.S中。
_TEXT_BASE: .word TEXT_BASE .globl lowlevel_init lowlevel_init: /* memory control configuration */ /* make r0 relative the current location so that it */ /* reads SMRDATA out of FLASH rather than memory ! */ ldr r0, =SMRDATA ldr r1, _TEXT_BASE sub r0, r0, r1 ldr r1, =BWSCON /* Bus Width Status Controller */ add r2, r0, #13*4 0: ldr r3, [r0], #4 str r3, [r1], #4 cmp r2, r0 bne 0b /* everything is fine now */ mov pc, lr .ltorg /* the literal pools origin */
SMRDATA: /* 13个寄存器的值 */ .word (0+(B1_BWSCON<<4)+(B2_BWSCON<<8)+(B3_BWSCON<<12)+(B4_BWSCON<<16)+(B5_BWSCON<<20)+(B6_BWSCON<<24)+(B7_BWSCON<<28)) .word ((B0_Tacs<<13)+(B0_Tcos<<11)+(B0_Tacc<<8)+(B0_Tcoh<<6)+(B0_Tah<<4)+(B0_Tacp<<2)+(B0_PMC)) .word ((B1_Tacs<<13)+(B1_Tcos<<11)+(B1_Tacc<<8)+(B1_Tcoh<<6)+(B1_Tah<<4)+(B1_Tacp<<2)+(B1_PMC)) .word ((B2_Tacs<<13)+(B2_Tcos<<11)+(B2_Tacc<<8)+(B2_Tcoh<<6)+(B2_Tah<<4)+(B2_Tacp<<2)+(B2_PMC)) .word ((B3_Tacs<<13)+(B3_Tcos<<11)+(B3_Tacc<<8)+(B3_Tcoh<<6)+(B3_Tah<<4)+(B3_Tacp<<2)+(B3_PMC)) .word ((B4_Tacs<<13)+(B4_Tcos<<11)+(B4_Tacc<<8)+(B4_Tcoh<<6)+(B4_Tah<<4)+(B4_Tacp<<2)+(B4_PMC)) .word ((B5_Tacs<<13)+(B5_Tcos<<11)+(B5_Tacc<<8)+(B5_Tcoh<<6)+(B5_Tah<<4)+(B5_Tacp<<2)+(B5_PMC)) .word ((B6_MT<<15)+(B6_Trcd<<2)+(B6_SCAN)) .word ((B7_MT<<15)+(B7_Trcd<<2)+(B7_SCAN)) .word ((REFEN<<23)+(TREFMD<<22)+(Trp<<20)+(Trc<<18)+(Tchr<<16)+REFCNT) .word 0x32 .word 0x30 .word 0x30
lowlevel_init的作用就是将SMRDATA开始的13个值复制给开始地址[BWSCON]的13个寄存器,从而完成了存储控制器的设置。
复制StageII的代码到RAM
上面初始化玩内存之后会跳转回start.S继续执行。
#ifndef CONFIG_SKIP_RELOCATE_UBOOT
relocate: /* relocate U-Boot to RAM */
adr r0, _start /* r0 <- current position of code */
ldr r1, _TEXT_BASE /* test if we run from flash or RAM */
cmp r0, r1 /* don't reloc during debug */
beq stack_setup
ldr r2, _armboot_start
ldr r3, _bss_start
sub r2, r3, r2 /* r2 <- size of armboot */
add r2, r0, r2 /* r2 <- source end address */
copy_loop:
ldmia r0!, {r3-r10} /* copy from source address [r0] */
stmia r1!, {r3-r10} /* copy to target address [r1] */
cmp r0, r2 /* until source end addreee [r2] */
ble copy_loop
#endif /* CONFIG_SKIP_RELOCATE_UBOOT */
relocate处首先比较_start和_TEXT_BASE的地址,如果相同则说明程序以及在内存中,无须加载。copy_loop处则实现了循环复制Flash的数据到内存中,每次复制8个字长的数据。
设置堆栈
只要将SP指针指向一段未使用的内存就算是完成了对堆栈的设置了。
/* Set up the stack */ stack_setup: ldr r0, _TEXT_BASE /* upper 128 KiB: relocated uboot */ sub r0, r0, #CFG_MALLOC_LEN /* malloc area */ sub r0, r0, #CFG_GBL_DATA_SIZE /* bdinfo */ #ifdef CONFIG_USE_IRQ sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ) #endif sub sp, r0, #12 /* leave 3 words for abort-stack */
根据上面的代码知道U-Boot内存使用情况。
清除BSS段
clear_bss: ldr r0, _bss_start /* find start of bss segment */ ldr r1, _bss_end /* stop here */ mov r2, #0x00000000 /* clear */ clbss_l:str r2, [r0] /* clear loop... */ add r0, r0, #4 cmp r0, r1 ble clbss_l
BSS段中存放有初始值为0得变量、无初始值的全局变量和一些静态变量。清除BSS段,就是将这些变量初始化赋值0,否则这些变量的初始值将是一个随机的值,若有程序直接使用这些值将会引起未知的后果。
跳转到Stage II代码入口
#if 0 /* try doing this stuff after the relocation */ ldr r0, =pWTCON mov r1, #0x0 str r1, [r0] /* * mask all IRQs by setting all bits in the INTMR - default */ mov r1, #0xffffffff ldr r0, =INTMR str r1, [r0] /* FCLK:HCLK:PCLK = 1:2:4 */ /* default FCLK is 120 MHz ! */ ldr r0, =CLKDIVN mov r1, #3 str r1, [r0] /* END stuff after relocation */ #endif ldr pc, _start_armboot _start_armboot: .word start_armboot
由代码可以看出,Stage II代码的入口处在start_armboot。
U-boot引导流程分析一