Cortax A9:其是芯片核心,也就是中央处理器——CPU。
Internal Rom:是一个只读存储器,里面存储了代码,总大小为64K。它的功能是用于读写pin脚,其作用是用来告诉系统从何处去读取uboot代码。uboot的代码可以从Nand、SD/MMC、eMMC、USB OTG等地方去启动。
也就说uboot的代码可以存储在外部的存储介质里面,通过拨动Opertaing Mode Pin(拨码开关),来选择从其中某个介质里去读取。而这些读取的驱动代码就集成在iROM之中。
其次iROM还有一个功能是读取外部的代码,将其读入内部的Internal SRAM之中,也就是BL1和BL2(未画)。BL代码就是我们自己写的BootLoader代码,然后程序计数器(PC指针)就会从iROM移动到Internal SRAM内部,执行我们的BootLoader代码。
Internal SRAM:集成在芯片内部的内存。执行SRAM的代码,也就是开始执行我们的BootLoader代码,它会完成一些硬件的初始化工作,如:初始化时钟和我们上面的Dram Controller控制器等,这样我们才能够使用DRAM内存。接着将OS加载到DRAM之中。
DRAM:uboot初始化结束之后,会将控制器交给DRAM之中的OS,这时候我们的可访问内存空间的大小就由256K扩展到外部的DRAM大小。
总结一下:
在进行uboot的链接启动流程分析之前,我们需要先看一下uboot的链接脚本。在C语言中,程序可以分为以下几个部分:
其中堆和栈属于动态区域,在程序运行时动态分配和释放。而代码段、数据段、只读数据段在链接之后产生。
一个C语言程序分为映象和运行时两种状态:
而链接脚本的作用就是:
如:uboot-2012.04.01的uboot的lds文件,其部分代码如下:
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
. = 0x00000000;
. = ALIGN(4);
.text :
{
__image_copy_start = .;
CPUDIR/start.o (.text)
board/samsung/smdk2440/libsmdk2440.o (.text)
*(.text)
}
. = ALIGN(4);
.rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
. = ALIGN(4);
.data : {
*(.data)
}
. = ALIGN(4);
. = .;
__u_boot_cmd_start = .;
.u_boot_cmd : { *(.u_boot_cmd) }
__u_boot_cmd_end = .;
...
}
这里简要说明一下:
从上面的代码我们可以看到有**.text段,其指定了所以uboot文件的.text段的存放位置**。往下是**.rodata段,其指定只读数据的存放位置**,紧跟着.text段,并且进行了4字节的对齐操作。再往下是数据段.data的存放位置。其中有一个__u_boot_cmd_start,它充当一个标号的作用,其指定了boot_cmd的起始地址,cmd也就是我们在boot的shell中使用的那些指令,而__u_boot_cmd_end指定的是结束地址。
uboot程序的入口地址,在第3行的ENTRY(_start)
处指定了。
我们可以使用arm-linux-nm u-boot
显示我们生成的uboot文件的符号信息。节选部分如下:
...
33f9964c T xyzModem_stream_open
33f993c0 T xyzModem_stream_read
33f9905c T xyzModem_stream_terminate
...
有了这个命令后,我们就可以很方便的查找到我们的uboot启动代码的地址,通过arm-linux-nm u-boot | grep _start
,我们找到我们的起始地址,其输出信息如下面:
33fada14 A __bss_start
33fad4bc A __u_boot_cmd_start
33f80044 T _armboot_start
33f80048 T _bss_start
33f80000 T _start
33f80184 t _start_armboot
33fb2cbc b bin_start_address
33fadaf8 b mem_malloc_start
33f9daf8 t setup_start_tag
(这个uboot用的是韦东山提供的uboot-1.1.6版本,打了补丁可能位置不一样,不过不影响)
知道了地址,我们怎么定位文件的位置呢?
可以通过arm-linux-addr2line
来定位,其可以将地址转成行号输出显示出来,其参数如下:
-b --target=<bfdname> Set the binary file format
-e --exe=<executable> Set the input file name (default is a.out)
-i --inlines Unwind inlined functions
-j --section=<name> Read section-relative offsets instead of addresses
-s --basenames Strip directory names
-f --functions Show function names
-C --demangle[=style] Demangle function names
-h --help Display this information
-v --version Display the program's version
我们可以通过arm-linux-addr2line -e u-boot 33f80000
找到我们起始地址所在的文件,以及其在文件中的位置,输出如下:
/home/binwatson/uboot/u-boot/u-boot-1.1.6/cpu/arm920t/start.S:56
上图为uboot的启动流程代码:
了解了整个uboot大致的启动流程后,我们就可以开始着手进行uboot的源代码分析了。我们从start.S文件开始分析,也就是uboot的入口地址_start,其节选代码如下:
...
.globl _start
_start: 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
ldr pc, _fiq
...
在第3行有一条跳转指令b reset
跳转到reset标号处去执行,在reset下面的那些ldr xxxx
指令是用于后面设置中断向量表使用的。reset标号处的代码如下:
reset:
bl save_boot_params
/*
* disable interrupts (FIQ and IRQ), also set the cpu to SVC32 mode,
* except if in HYP mode already
*/
mrs r0, cpsr
and r1, r0, #0x1f @ mask mode bits
teq r1, #0x1a @ test for HYP mode
bicne r0, r0, #0x1f @ clear all mode bits
orrne r0, r0, #0x13 @ set SVC mode
orr r0, r0, #0xc0 @ disable FIQ and IRQ
msr cpsr,r0
从注释中我们就可以看到,其作用就是关闭中断,将CPU设置为SVC32模式。
接着_start的代码往下走:
/*
* Setup vector:
* (OMAP4 spl TEXT_BASE is not 32 byte aligned.
* Continue to use ROM code vector only in OMAP4 spl)
*/
#if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD))
/* Set V=0 in CP15 SCTRL register - for VBAR to point to vector */
mrc p15, 0, r0, c1, c0, 0 @ Read CP15 SCTRL Register
bic r0, #CR_V @ V = 0
mcr p15, 0, r0, c1, c0, 0 @ Write CP15 SCTRL Register
/* Set vector address in CP15 VBAR register */
ldr r0, =_start
mcr p15, 0, r0, c12, c0, 0 @Set VBAR
#endif
/* the mask ROM code should have PLL and others stable */
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
bl cpu_init_cp15
bl cpu_init_crit
#endif
bl _main
从注释中我们可以看到,行2~14大致完成内容是设置SCTRL寄存器,也就是系统控制器以及设置中断向量表,这个中断向量表的内容就是我们前面在reset下面看到的那些ldr xxxx
指令,这个需要借助CPU的协处理器CP15进行设置,基体细节不深究。
完成上面操作后,会跳转到cpu_init_cp15和cpu_init_crit标号执行,最后转到_main函数去执行。
我们一步一步进行分析,先看cpu_init_cp15标号处的代码:
ENTRY(cpu_init_cp15)
/*
* Invalidate L1 I/D
*/
mov r0, #0 @ set up for MCR
mcr p15, 0, r0, c8, c7, 0 @ invalidate TLBs
mcr p15, 0, r0, c7, c5, 0 @ invalidate icache
mcr p15, 0, r0, c7, c5, 6 @ invalidate BP array
mcr p15, 0, r0, c7, c10, 4 @ DSB
mcr p15, 0, r0, c7, c5, 4 @ ISB
/*
* disable MMU stuff and caches
*/
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #0x00002000 @ clear bits 13 (--V-)
bic r0, r0, #0x00000007 @ clear bits 2:0 (-CAM)
orr r0, r0, #0x00000002 @ set bit 1 (--A-) Align
orr r0, r0, #0x00000800 @ set bit 11 (Z---) BTB
...
#ifdef CONFIG_ARM_ERRATA_742230
mrc p15, 0, r0, c15, c0, 1 @ read diagnostic register
orr r0, r0, #1 << 4 @ set bit #4
mcr p15, 0, r0, c15, c0, 1 @ write diagnostic register
#endif
...
ENDPROC(cpu_init_cp15)
从代码的注释我们可以看出,该标号处的代码主要完成关闭缓存、关闭虚拟内存MMU的作用,以及设置了diagnostic寄存器。
接着我们回到reset处的cpu_init_crit继续往下追踪:
ENTRY(cpu_init_crit)
/*
* Jump to board specific initialization...
* The Mask ROM will have already initialized
* basic memory. Go here to bump up clock rate and handle
* wake up conditions.
*/
b lowlevel_init @ go setup pll,mux,memory
ENDPROC(cpu_init_crit)
cpu_init_crit处的代码很简单,就是调用lowlevel_init进行初始化。
由流程图和注释我们可以知道,lowlevel_init主要完成板级相关的一些初始化工作,如:时钟、内存、网卡、串口的初始化。
接着我们回到reset处的_main继续往下追踪。_main标号定义在文件arch/arm/lib/crt0.S里,可以通过arm-linux-nm和arm-linux-address2line进行定位。
下面我们对_main进行追踪:
/*
* Set up initial C runtime environment and call board_init_f(0).
*/
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
ldr sp, =(CONFIG_SPL_STACK)
#else
ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)
#endif
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
sub sp, #GD_SIZE /* allocate one GD above SP */
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
mov r8, sp /* GD is above SP */
mov r0, #0
bl board_init_f
#if ! defined(CONFIG_SPL_BUILD)
从注释里我们可以看出,_main首先初始化了一下C的运行环境,接着调用board_init_f函数。除此以外进行sp寄存器的设置,也就是第10行处的代码,还有在栈空间里为GD
变量分配了内存。接着我们继续追踪board_init_f:
void board_init_f(ulong bootflag)
{
bd_t *bd;
init_fnc_t **init_fnc_ptr;
gd_t *id;
ulong addr, addr_sp;
...
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
if ((*init_fnc_ptr)() != 0) {
hang ();
}
}
...
}
其中有两个最重要的全局变量bd_t *bd
和gd_t *id
它们用于存储一些信息,这两个全局变量的存储空间就是在调用board_init_f之前已经在栈中进行分配了。
board_init_f主要完成两个工作,一是对gd_t *id
进行初始化,二是它会调用一些列的函数(在第10行处的代码)完成一些列工作。GD
(也就是gd_t *id
)主要是记录一些地址信息,用于后续的自搬移操作使用。
接着我们回到_main,接着board_init_f继续往下看:
bl board_init_f
#if ! defined(CONFIG_SPL_BUILD)
/*
* Set up intermediate environment (new sp and gd) and call
* relocate_code(addr_moni). Trick here is that we'll return
* 'here' but relocated.
*/
ldr sp, [r8, #GD_START_ADDR_SP] /* r8 = gd->start_addr_sp */
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
ldr r8, [r8, #GD_BD] /* r8 = gd->bd */
sub r8, r8, #GD_SIZE /* new GD is below bd */
adr lr, here
ldr r0, [r8, #GD_RELOC_OFF] /* lr = gd->start_addr_sp */
add lr, lr, r0
ldr r0, [r8, #GD_RELOCADDR] /* r0 = gd->relocaddr */
b relocate_code
在最后一行有一个b relocate_code
跳转指令,从名字我们就可以知道,其完成的工作就是对代码进行自搬移操作,接着我们继续往下看:
here:
/* Set up final (full) environment */
bl c_runtime_cpu_setup /* we still call old routine here */
ldr r0, =__bss_start /* this is auto-relocated! */
ldr r1, =__bss_end /* this is auto-relocated! */
mov r2, #0x00000000 /* prepare zero to clear BSS */
clbss_l:cmp r0, r1 /* while not at end of BSS */
strlo r2, [r0] /* clear 32-bit BSS word */
addlo r0, r0, #4 /* move to next */
blo clbss_l
bl coloured_LED_init
bl red_led_on
/* call board_init_r(gd_t *id, ulong dest_addr) */
mov r0, r8 /* gd_t */
ldr r1, [r8, #GD_RELOCADDR] /* dest_addr */
/* call board_init_r */
ldr pc, =board_init_r /* this is auto-relocated! */
/* we should not return here. */
#endif
自搬移结束后,就准备进入C语言环境运行了。
从注释中可以看到设置了bss段,也就是对bss段进行了清零,标号clbss_l
标号处的循环代码。
之后就会通过board_init_r标号,跳转到C语言函数,我们继续追踪board_init_r:
void board_init_r(gd_t *id, ulong dest_addr)
{
ulong malloc_start;
#if !defined(CONFIG_SYS_NO_FLASH)
ulong flash_size;
#endif
...
/* main_loop() can return to retry autoboot, if so just run it again. */
for (;;) {
main_loop();
}
...
}
board_init_r会打印一些列的信息到串口,然后会进入main_loop()循环,main_loop会进行倒计时,如果此时按下回车就会进入uboot的shell交互界面,否则就会自动引导启动OS系统。
uboot的启动如下,假设我们使用SD卡进行启动:
iROM会将SD卡内的BL1加载到iRAM中,然后将控制器交给BL1;
BL1会将BL2以及填充,一共16K的代码,加载到iRAM中;
uboot由两部分组成,一部分是SPL完成硬件相关操作,另一部分是uboot代码。
接着SPL部分会将uboot代码加载DRAM之中,此时uboot位于DRAM的内存地址0x43E00000处;
uboot加载需要加载Linux系统的uImage到内存,一般加载到0x4008000处,为了防止把uboot自己给覆盖了,所以uboot还需要进行一次自搬移操作;[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
图中的Global data就是我们前面看到的
GD
,它记录一些列地址信息,包括uboot自搬移后存储的地址,malloc的堆空间起始地址等;
然后CPU跳转新的uboot地址执行,这时我们就能看到串口打印一些列控制信息了。
至此uboot的启动流程大致分析就结束了。