本文简单的分析下u-boot2016 的初始化汇编代码,并且能从openjtag 加载启动。
代码从 arch/arm/lib/vectors.S
开始执行全局标号 _start:
_start:
#ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
.word CONFIG_SYS_DV_NOR_BOOT_CFG
#endif
b reset
ldr pc, _undefined_instruction
......
ldr pc, _fiq
关于 ldr 伪指令,可见前文分析,这里直接执行b指令跳转到reset位置处。关于b指令可见这里。
下面我们再来看看 reset 代码段,reset定义在:arch/arm/cpu/arm920t/start.S文件中,reset(英文复位),系统复位时也会从这里开始,处理硬件上的复位,软件复位命令时通过看门狗操作实现。reset开始的代码段实现切换到超级用户模式(SVC 模式)。为什么要在初始化的时候设置为svc模式,可见这里。
接下来的重定位异常向量表,因为宏没有定义,所以没编译进去,关于这一点可以从生成的u-boot.dis 文件看出来。
然后就是常说的几个例行操作:关看门狗、屏蔽中断、设置系统时钟,因为现有的系统代码没有对s3c2440设置的支持,所以这里需要加上(直接上代码,具体原理可以见s3c2440芯片手册):
#ifdef CONFIG_S3C24X0
/* turn off the watchdog */
# if defined(CONFIG_S3C2400)
# define pWTCON 0x15300000
# define INTMSK 0x14400008 /* Interrupt-Controller base addresses */
# define CLKDIVN 0x14800014 /* clock divisor register */
#else
# define pWTCON 0x53000000
# define INTMSK 0x4A000008 /* Interrupt-Controller base addresses */
# define INTSUBMSK 0x4A00001C
# define CLKDIVN 0x4C000014 /* clock divisor register */
# endif
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, =INTMSK
str r1, [r0]
# if defined(CONFIG_S3C2410)
ldr r1, =0x3ff
ldr r0, =INTSUBMSK
str r1, [r0]
# elif defined(CONFIG_S3C2440)
ldr r1, =0x7fff
ldr r0, =INTSUBMSK
str r1, [r0]
# endif
# if defined(CONFIG_S3C2440)
# define MPLLCON 0x4C000004 /* 系统主频配置寄存器 */
# define UPLLCON 0x4C000008 /* USB频率配置寄存器 */
# define CAMDIVN 0x4C000018 /* CAMERA时钟分频寄存器 */
# define MMDIV_405 (0x7f<<12)
# define MPSDIV_405 0x21
# define UMDIV_48 (0x38<<12)
# define UPSDIV_48 0X22
ldr r0, =CAMDIVN
mov r1, #0
str r1, [r0]
/* FCLK:HCLK:PCLK = 1:2:4 */
/* default FCLK is 120 MHz ! */
ldr r0, =CLKDIVN
mov r1, #0x05
str r1, [r0]
/* 如果HDIVN不等于0,CPU必须设置为异步总线模式 */
mrc p15, 0, r0, c1, c0, 0
orr r0, r0, #0xC0000000
mcr p15, 0, r0, c1, c0, 0
ldr r0, =UPLLCON
mov r1, #UMDIV_48 /* USB时钟48MHz */
add r1, r1, #UPSDIV_48
str r1, [r0]
/* * When you set MPLL&UPLL values, you have to set the UPLL * value first and then the MPLL value. (Needs intervals * approximately 7 NOP) */
nop
nop
nop
nop
nop
nop
nop
ldr r0, =MPLLCON
mov r1, #MMDIV_405 /* cpu时钟 400MHz */
add r1, r1, #MPSDIV_405
str r1, [r0]
# else
/* FCLK:HCLK:PCLK = 1:2:4 */
/* default FCLK is 120 MHz ! */
ldr r0, =CLKDIVN
mov r1, #3
str r1, [r0]
#endif
#endif /* CONFIG_S3C24X0 */
然后就是 cpu_init_crit ,这里主要完成dram初始化操作,因为现阶段目标是让u-boot用openjtag加载跑起来,因此先不考虑cpu_init_crit。
如果不执行 cpu_init_crit ,就会调用函数 _main。来到了arch/arm/lib/crt0.S 文件中。
ENTRY(_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
#if defined(CONFIG_CPU_V7M) /* v7M forbids using SP as BIC destination */
mov r3, sp
bic r3, r3, #7
mov sp, r3
#else
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
#endif
mov r0, sp
bl board_init_f_mem
mov sp, r0
mov r0, #0
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, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */
宏定义 CONFIG_SYS_INIT_SP_ADDR 在文件 mini2440.h 中。注意这两行:
mov r0, sp
bl board_init_f_mem
C函数 board_init_f_mem(ulong top) 的参数top,就是上面的寄存器r0,这个函数主要完成 global_data 的简单初始化,global_data 是一个很大的数据结构,放在栈中,这里先不讲解,等以后用到了再说。并且把栈减去一个合适的数值返回。
然后就来到了函数 void board_init_f(ulong boot_flags), 在文件 common/board_f.c ,这个函数主要功能就是完成 init_sequence_f 函数指针数组每个函数的调用。
1> 首先看 setup_mon_len :
gd->mon_len = (ulong)&__bss_end - (ulong)_start;
这行代码是获取u-boot代码的大小。
2> 然后就是 timer_init : arch/arm/cpu/arm920t/s3c24x0/timer.c 可以看到这个函数做了很多的初始化工作。
3> 接着是 serial_init : drivers/serial/serial.c
int serial_init(void)
{
gd->flags |= GD_FLG_SERIAL_READY;
return get_current()->start();
}
在函数 get_current() 中返回的 dev 是 default_serial_console();最后 get_current()->start(); 调用的函数是 serial_init_dev – drivers/serial/serial_s3c24x0.c,做一些特定串口初始化。
4> 接着是dram_init :board/samsung/mini2440/mini2440.c,简单的初始化 ram_size 为64M。
int dram_init(void)
{
/* dram_init must store complete ramsize in gd->ram_size */
gd->ram_size = PHYS_SDRAM_1_SIZE;
return 0;
}
dram_init实现可以通过配置文件定义宏定义来实现,也可以通过对ddrc控制器读获取dram信息。
5> setup_dest_addr 这个函数初始化 gd->ram_top(可用内存空间顶部)然后赋值
gd->relocaddr = gd->ram_top;
后面的一系列函数会对调整relocaddr 对 sdram 空间进行规划。
/*
* now that we have dram mapped and working, we can
* relocate the code and continue running from dram.
*
* reserve memory at end of ram for (top down in that order):
* - area that won't get touched by u-boot and linux (optional)
* - kernel log buffer
* - protected ram
* - lcd framebuffer
* - monitor code
* - board info struct
*/
setup_dest_addr,
static int setup_dest_addr(void)
{
gd->ram_top = CONFIG_SYS_SDRAM_BASE;
///#define CONFIG_SYS_SDRAM_BASE PHYS_SDRAM_1
///#define PHYS_SDRAM_1 0x30000000 /* SDRAM Bank #1 */
gd->ram_top += gd->ram_size;
///到了可用sdram的顶端
gd->relocaddr = gd->ram_top;
}
6 > 根据 reserve_mmu 的宏定义判断,当 icache 和 dcache 有任意一个打开的时候则预留出PATABLE_SIZE大小的tlb空间,tlb存放首地址赋值给gd->arch.tlb_addr。
#if !(defined(CONFIG_SYS_ICACHE_OFF) && defined(CONFIG_SYS_DCACHE_OFF)) && \
defined(CONFIG_ARM)
static int reserve_mmu(void)
{
/* reserve TLB table */
gd->arch.tlb_size = PGTABLE_SIZE;
gd->relocaddr -= gd->arch.tlb_size;
/* round down to next 64 kB limit */
gd->relocaddr &= ~(0x10000 - 1);
gd->arch.tlb_addr = gd->relocaddr;
debug("TLB table from %08lx to %08lx\n", gd->arch.tlb_addr,
gd->arch.tlb_addr + gd->arch.tlb_size);
return 0;
}
#endif
7 > reserve_uboot
static int reserve_uboot(void)
{
/*
* reserve memory for U-Boot code, data & bss
* round down to next 4 kB limit
*/
gd->relocaddr -= gd->mon_len;
gd->relocaddr &= ~(4096 - 1);
gd->start_addr_sp = gd->relocaddr;
}
这里 减去 gd->mon_len为 uboot 的code留出空间,到这里addr的值就确定,addr作为uboot relocate的目标addr。
先总结一下从setup_dest_addr之后到reserve_uboot 之间的函数实现了 addr 之上 sdram 空间的划分,由高到低 (根据宏定义可能还保留有其它的内存块):
top–>hide mem–>tlb space(16K)–>uboot code space–>addr
然后从 reserve_malloc 开始的函数就是调整 start_addr_sp 保留内存空间了。
8 > reserve_malloc 分配堆空间,这里是 TOTAL_MALLOC_LEN。
9 > reserve_board 为 bd_info 结构体预留空间。
10 > reserve_global_data 为 global_data 结构体预留空间。
11 > reserve_stacks() 首先栈指针16字节对齐
/* make stack pointer 16-byte aligned */
gd->start_addr_sp -= 16;
gd->start_addr_sp &= ~0xf;
然后调用 arch_reserve_stacks() 这个函数为abort stack分配空间,并且初始化irq_sp,作为异常栈指针。
12 > setup_dram_config() 调用 dram_init_banksize() 后者声明为 __weak 类型,我们可以有自己的实现。这个函数初始化内存的首地址和大小。
13> show_dram_config() 打印 ram 区域分配。
14> setup_reloc() 计算出要重定位的偏移大小:
gd->reloc_off = gd->relocaddr - CONFIG_SYS_TEXT_BASE;
gd->relocaddr 为目标addr,gd->start_addr_sp 为目标 addr_sp ,gd->reloc_off 为目标addr和现在实际code起始地址的偏移。reloc_off非常重要,会作为后面 relocate_code 函数的参数,来实现code的拷贝。
board_init_f 函数将 sdram 空间重新进行了划分,可以看出栈空间和堆空间是分开的。
board_init_f 初始化过程至此结束。
然后就是 代码重定位了,关于u-boot 的代码重定位涉及到的知识点很多,包括了编译器选项,反汇编分析,arm寻址方式 其中一位大牛已经写的很详细了,直接附上他讲解的地址吧。
这里总结几点:
1)对于函数调用用的是相对跳转,这个不受重定位影响。
2)对于全局变量,包括指针变量(指向函数,指向全局变量)在函数的尾部存放着这些变量的地址(叫做LABEL),当要访问这些变量(或者指针)的时候 用PC+相对偏移获取(全局变量,或者指针)的地址。
如果重定位,对于全局变量只要让函数尾部的(LABEL+相对偏移)就可以 就可以获取到全局变量了。
但如果这个全局变量是个指针,上面的一步完成之后,那么指针的值也要修改。这个也是通过 rel.dyn 段来完成的。也就是说对于全局指针变量 rel.dyn 有2项数据成员来完成它的重定位操作。
重定位代码之后就是 relocate_vectors 重定位异常向量,
/* * Copy the relocated exception vectors to the * correct address * CP15 c1 V bit gives us the location of the vectors: * 0x00000000 or 0xFFFF0000. */
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
mrc p15, 0, r2, c1, c0, 0 /* V bit (bit[13]) in CP15 c1 */
ands r2, r2, #(1 << 13)
ldreq r1, =0x00000000 /* If V=0 */
ldrne r1, =0xFFFF0000 /* If V=1 */
ldmia r0!, {r2-r8,r10}
stmia r1!, {r2-r8,r10}
ldmia r0!, {r2-r8,r10}
stmia r1!, {r2-r8,r10}
通过注释可以看出,这里通过读取协处理器,获取异常地址,这里正好要把异常向量放到0x0地址处,这个位置是SRAM空间。(如果是从norflash启动的时候,0x0 地址处 就无法写了)
然后就是 clbss_l 清除 bss 段,bss 段存放程序中未初始化的全局变量和静态变量。特点是可读写的,在程序执行之前BSS段应该清0。
接着是 coloured_LED_init ,这个可以适当控制LED灯,实现调试作用。
最后就是 board_init_r : dest_addr 是新code地址
void board_init_r(gd_t *new_gd, ulong dest_addr)