最近开始接触uboot,现在需要将2014.4版本uboot移植到公司armv7开发板。
在网上搜索讲uboot启动过程的文章,大多都是比较老版本的uboot,于是决定将新版uboot启动过程记录下来,和大家共享。
对于uboot,我写了一个专栏来记录我的一些理解,感兴趣的朋友可以点击以下链接:
u-boot学习笔记
辛苦之作,大家共享,转载还请注明出处!
Author : kerneler
Email :karse0104@163.com
# # (C) Copyright 2000-2013 # Wolfgang Denk, DENX Software Engineering, wd@denx.de. # # SPDX-License-Identifier: GPL-2.0+ # VERSION = 2014 PATCHLEVEL = 04 SUBLEVEL = EXTRAVERSION = NAME =
到我写这篇文章之时,这个版本的uboot是最新版本。
2014.4版本uboot启动至命令行几个重要函数为:_start,_main,board_init_f,relocate_code,board_init_r。
一 _start
对于任何程序,入口函数是在链接时决定的,uboot的入口是由链接脚本决定的。uboot下armv7链接脚本默认目录为arch/arm/cpu/u-boot.lds。这个可以在配置文件中与CONFIG_SYS_LDSCRIPT来指定。
入口地址也是由连接器决定的,在配置文件中可以由CONFIG_SYS_TEXT_BASE指定。这个会在编译时加在ld连接器的选项-Ttext中
uboot的配置编译原理也非常值得学习,我想在另外写一篇文章来记录,这里不详细说了。
查看u-boot.lds
OUTPUT_ARCH(arm) ENTRY(_start) SECTIONS { . = 0x00000000; . = ALIGN(4); .text : { *(.__image_copy_start) CPUDIR/start.o (.text*) *(.text*) } . = ALIGN(4); .rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) } . = ALIGN(4); .data : { *(.data*) }
_start在arch/arm/cpu/armv7/start.S中,一段一段的分析,如下:
.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 #ifdef CONFIG_SPL_BUILD _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 _pad: .word 0x12345678 /* now 16*4=64 */ #else .globl _undefined_instruction _undefined_instruction: .word undefined_instruction .globl _software_interrupt _software_interrupt: .word software_interrupt .globl _prefetch_abort _prefetch_abort: .word prefetch_abort .globl _data_abort _data_abort: .word data_abort .globl _not_used _not_used: .word not_used .globl _irq _irq: .word irq .globl _fiq _fiq: .word fiq _pad: .word 0x12345678 /* now 16*4=64 */</span> <span style="font-size:14px;">#endif /* CONFIG_SPL_BUILD */ .global _end_vect _end_vect: .balignl 16,0xdeadbeef
以上代码是设置arm的异常向量表,arm异常向量表如下:
地址 |
异常 |
进入模式 |
描述 |
0x00000000 |
复位 |
管理模式 |
复位电平有效时,产生复位异常,程序跳转到复位处理程序处执行 |
0x00000004 |
未定义指令 |
未定义模式 |
遇到不能处理的指令时,产生未定义指令异常 |
0x00000008 |
软件中断 |
管理模式 |
执行SWI指令产生,用于用户模式下的程序调用特权操作指令 |
0x0000000c |
预存指令 |
中止模式 |
处理器预取指令的地址不存在,或该地址不允许当前指令访问,产生指令预取中止异常 |
0x00000010 |
数据操作 |
中止模式 |
处理器数据访问指令的地址不存在,或该地址不允许当前指令访问时,产生数据中止异常 |
0x00000014 |
未使用 |
未使用 |
未使用 |
0x00000018 |
IRQ |
IRQ |
外部中断请求有效,且CPSR中的I位为0时,产生IRQ异常 |
0x0000001c |
FIQ |
FIQ |
快速中断请求引脚有效,且CPSR中的F位为0时,产生FIQ异常 |
后面汇编是定义了7种异常的入口函数,这里没有定义CONFIG_SPL_BUILD,所以走后面一个。
接下来定义的_end_vect中用.balignl来指定接下来的代码要16字节对齐,空缺的用0xdeadbeef,方便更加高效的访问内存。接着分析下面一段代码
#ifdef CONFIG_USE_IRQ /* IRQ stack memory (calculated at run-time) */ .globl IRQ_STACK_START IRQ_STACK_START: .word 0x0badc0de /* IRQ stack memory (calculated at run-time) */ .globl FIQ_STACK_START FIQ_STACK_START: .word 0x0badc0de #endif /* IRQ stack memory (calculated at run-time) + 8 bytes */ .globl IRQ_STACK_START_IN IRQ_STACK_START_IN: .word 0x0badc0de
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在上电或者重启后,处理器取得第一条指令就是b reset,所以会直接跳转到reset函数处。reset首先是跳转到save_boot_params中,如下:
/************************************************************************* * * void save_boot_params(u32 r0, u32 r1, u32 r2, u32 r3) * __attribute__((weak)); * * Stack pointer is not yet initialized at this moment * Don't save anything to stack even if compiled with -O0 * *************************************************************************/ ENTRY(save_boot_params) bx lr @ back to my caller ENDPROC(save_boot_params) .weak save_boot_params这里save_boot_params函数中没做什么直接跳回,注释也说明了,栈没有初始化,最好不要再函数中做操作。
这里值得注意的是.weak关键字,在网上找了到的解释,我的理解是.weak相当于声明一个函数,如果该函数在其他地方没有定义,则为空函数,有定义则调用该定义的函数。
具体解释可以看这位大神的详解:
http://blog.csdn.net/norains/article/details/5954459
接下来reset执行7条指令,修改cpsr寄存器,设置处理器进入svc模式,并且关掉irq和fiq。
/* * 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
前面6条汇编指令是对协处理器cp15进行操作,设置了处理器的异常向量入口地址为_start,
这里需要注意,ARM默认的异常向量表入口在0x0地址,uboot的运行介质(norflash nandflash sram等)映射地址可能不在0x0起始的地址,所以需要修改异常向量表入口。
但是我在网上没有找到cp15协处理器的c12寄存器的说明,可能是armv7新添加的
协处理器cp15的说明可以看我转的一篇文章:
http://blog.csdn.net/skyflying2012/article/details/25823967
接下来如果没有定义宏CONFIG_SKIP_LOWLEVEL_INIT,则会分别跳转执行cpu_init_cp15以及cpu_init_crit。
在分析这2个函数之前先总结一下上面分析的这一段_start中汇编的作用:
1 初始化异常向量表 2 设置cpu svc模式,关中断 3 配置cp15,设置异常向量入口
都是跟异常有关的部分。
接下来先分析cpu_init_cp15
/************************************************************************* * * cpu_init_cp15 * * Setup CP15 registers (cache, MMU, TLBs). The I-cache is turned on unless * CONFIG_SYS_ICACHE_OFF is defined. * *************************************************************************/ 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_SYS_ICACHE_OFF bic r0, r0, #0x00001000 @ clear bit 12 (I) I-cache #else orr r0, r0, #0x00001000 @ set bit 12 (I) I-cache #endif mcr p15, 0, r0, c1, c0, 0 #ifdef CONFIG_ARM_ERRATA_716044 mrc p15, 0, r0, c1, c0, 0 @ read system control register orr r0, r0, #1 << 11 @ set bit #11 mcr p15, 0, r0, c1, c0, 0 @ write system control register #endif #if (defined(CONFIG_ARM_ERRATA_742230) || defined(CONFIG_ARM_ERRATA_794072)) 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 #ifdef CONFIG_ARM_ERRATA_743622 mrc p15, 0, r0, c15, c0, 1 @ read diagnostic register orr r0, r0, #1 << 6 @ set bit #6 mcr p15, 0, r0, c15, c0, 1 @ write diagnostic register #endif #ifdef CONFIG_ARM_ERRATA_751472 mrc p15, 0, r0, c15, c0, 1 @ read diagnostic register orr r0, r0, #1 << 11 @ set bit #11 mcr p15, 0, r0, c15, c0, 1 @ write diagnostic register #endif #ifdef CONFIG_ARM_ERRATA_761320 mrc p15, 0, r0, c15, c0, 1 @ read diagnostic register orr r0, r0, #1 << 21 @ set bit #21 mcr p15, 0, r0, c15, c0, 1 @ write diagnostic register #endif mov pc, lr @ back to my caller ENDPROC(cpu_init_cp15)
接下来看cpu_init_crit
/************************************************************************* * * CPU_init_critical registers * * setup important registers * setup memory timing * *************************************************************************/ 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)
lowlevel_init函数则是需要移植来实现,做clk初始化以及ddr初始化
从cpu_init_crit返回后,_start的工作就完成了,接下来就要调用_main,总结一下_start工作:
1 前面总结过的部分,初始化异常向量表,设置svc模式,关中断
2 配置cp15,初始化mmu cache tlb
3 板级初始化,pll memory初始化
二 _main
_main函数在arch/arm/lib/crt0.S中,mian函数的作用在注释中有详细的说明,我们分段来分析一下
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 bic sp, sp, #7 /* 8-byte alignment for ABI compliance */ sub sp, sp, #GD_SIZE /* allocate one GD above SP */ bic sp, sp, #7 /* 8-byte alignment for ABI compliance */ mov r9, sp /* GD is above SP */ mov r0, #0 bl board_init_f
首先将CONFIG_SYS_INIT_SP_ADDR定义的值加载到栈指针sp中,这个宏定义在配置头文件中指定。
这段代码是为board_init_f C函数调用提供环境,也就是栈指针sp初始化
8字节对齐,然后减掉GD_SIZE,这个宏定义是指的全局结构体gd的大小,是160字节在此处,这个结构体用来保存uboot一些全局信息,需要一块单独的内存。
最后将sp保存在r9寄存器中。因此r9寄存器中的地址就是gd结构体的首地址。
在后面所有code中如果要使用gd结构体,必须在文件中加入DECLARE_GLOBAL_DATA_PTR宏定义,定义如下:
#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r9")
gd结构体首地址就是r9中的值。
C语言函数栈是向下生长,这里sp为malloc空间顶端减去gd bd空间开始的,起初很纳闷,sp设在这里,以后的C函数调用不都会在malloc空间了吗,堆栈空间不就重合了嘛,不用急,看完board_init_f就明白了。
接着说_main上面一段代码,接着r0赋为0,也就是参数0为0,调用board_init_f
三 board_init_f
移植uboot先做一个最精简版本,很多配置选项都没有打开,比如fb mmc等硬件都默认不打开,只配置基本的ddr serial,这样先保证uboot能正常启动进入命令行,然后再去添加其他。
我们这里分析就是按最精简版本来,这样可以更加简洁的说明uboot的启动流程。
board_init_f函数主要是根据配置对全局信息结构体gd进行初始化。
gd结构体中有个别成员意义我也不是很理解,这里我只说我理解并且在后面起到作用的成员。
gd->mon_len = (ulong)&__bss_end - (ulong)_start;初始化mon_len,代表uboot code的大小。
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) { if ((*init_fnc_ptr)() != 0) { hang (); } }遍历调用init_sequence所有函数,init_sequence定义如下:
init_fnc_t *init_sequence[] = { arch_cpu_init, /* basic arch cpu dependent setup */ mark_bootstage, #ifdef CONFIG_OF_CONTROL fdtdec_check_fdt, #endif #if defined(CONFIG_BOARD_EARLY_INIT_F) board_early_init_f, #endif timer_init, /* initialize timer */ #ifdef CONFIG_BOARD_POSTCLK_INIT board_postclk_init, #endif #ifdef CONFIG_FSL_ESDHC get_clocks, #endif env_init, /* initialize environment */ init_baudrate, /* initialze baudrate settings */ serial_init, /* serial communications setup */ console_init_f, /* stage 1 init of console */ display_banner, /* say that we are here */ print_cpuinfo, /* display cpu info (and speed) */ #if defined(CONFIG_DISPLAY_BOARDINFO) checkboard, /* display board info */ #endif #if defined(CONFIG_HARD_I2C) || defined(CONFIG_SYS_I2C) init_func_i2c, #endif dram_init, /* configure available RAM banks */ NULL, };arch_cpu_init需要实现,要先启动uboot这里可以先写一个空函数。
timer_init在lib/time.c中有实现,也是空函数,但是有__WEAK关键字,如果自己实现,则会调用自己实现的这个函数
对最精简uboot,需要做好就是ddr和serial,所以我们最关心是serial_init,console_init_f以及dram_init.
先看serial_init
int serial_init(void) { return get_current()->start(); } static struct serial_device *get_current(void) { struct serial_device *dev; if (!(gd->flags & GD_FLG_RELOC)) dev = default_serial_console(); else if (!serial_current) dev = default_serial_console(); else dev = serial_current; /* We must have a console device */ if (!dev) { #ifdef CONFIG_SPL_BUILD puts("Cannot find console\n"); hang(); #else panic("Cannot find console\n"); #endif } return dev; }
serial_device结构体代表了一个串口设备,其中的成员都需要在自己的serial驱动中实现。
这样在serial_init中get_current获取就是串口驱动中给出的默认调试串口结构体,执行start,做一些特定串口初始化。
console_init_f将gd中have_console置1,这个函数不详细说了。
display_banner,print_cpuinfo利用现在的调试串口打印了uboot的信息。
接下来就是dram_init。
dram_init对gd->ram_size初始化,以便board_init_f后面代码对dram空间进行规划。
dram_init实现可以通过配置文件定义宏定义来实现,也可以通过对ddrc控制器读获取dram信息。
继续分析board_init_f,剩余代码将会对sdram空间进行规划!
#if defined(CONFIG_SYS_MEM_TOP_HIDE) /* * Subtract specified amount of memory to hide so that it won't * get "touched" at all by U-Boot. By fixing up gd->ram_size * the Linux kernel should now get passed the now "corrected" * memory size and won't touch it either. This should work * for arch/ppc and arch/powerpc. Only Linux board ports in * arch/powerpc with bootwrapper support, that recalculate the * memory size from the SDRAM controller setup will have to * get fixed. */ gd->ram_size -= CONFIG_SYS_MEM_TOP_HIDE; #endif addr = CONFIG_SYS_SDRAM_BASE + get_effective_memsize();CONFIG_SYS_MEM_TOP_HIDE宏定义是将一部分内存空间隐藏,注释说明对于ppc处理器在内核中有接口来实现使用uboot提供的值,这里咱们不考虑。
addr的值由CONFIG_SYS_SDRAM_BASE加上ram_size。也就是到了可用sdram的顶端。
#if !(defined(CONFIG_SYS_ICACHE_OFF) && defined(CONFIG_SYS_DCACHE_OFF)) /* reserve TLB table */ gd->arch.tlb_size = PGTABLE_SIZE; addr -= gd->arch.tlb_size; /* round down to next 64 kB limit */ addr &= ~(0x10000 - 1); gd->arch.tlb_addr = addr; debug("TLB table from %08lx to %08lx\n", addr, addr + gd->arch.tlb_size); #endif /* round down to next 4 kB limit */ addr &= ~(4096 - 1); debug("Top of RAM usable for U-Boot at: %08lx\n", addr);如果打开了icache以及dcache,则预留出PATABLE_SIZE大小的tlb空间,tlb存放首地址赋值给gd->arch.tlb_addr。
最后addr此时值就是tlb的地址,4kB对齐。
#ifdef CONFIG_LCD #ifdef CONFIG_FB_ADDR gd->fb_base = CONFIG_FB_ADDR; #else /* reserve memory for LCD display (always full pages) */ addr = lcd_setmem(addr); gd->fb_base = addr; #endif /* CONFIG_FB_ADDR */ #endif /* CONFIG_LCD */ /* * reserve memory for U-Boot code, data & bss * round down to next 4 kB limit */ addr -= gd->mon_len; addr &= ~(4096 - 1); debug("Reserving %ldk for U-Boot at: %08lx\n", gd->mon_len >> 10, addr);如果需要使用frambuffer,使用配置fb首地址CONFIG_FB_ADDR或者调用lcd_setmem获取fb大小,这里面有板级相关函数需要实现,不过为了先能启动uboot,没有打开fb选项。addr值就是fb首地址。
gd->fb_base保存fb首地址。
接着-gd->mon_len为uboot的code留出空间,到这里addr的值就确定,addr作为uboot relocate的目标addr。
到这里,可以看出uboot现在空间划分是从顶端往下进行的。
先总结一下addr之上sdram空间的划分:
由高到低 : top-->hide mem-->tlb space(16K)-->framebuffer space-->uboot code space-->addr
接下来要确定addr_sp的值。
#ifndef CONFIG_SPL_BUILD /* * reserve memory for malloc() arena */ addr_sp = addr - TOTAL_MALLOC_LEN; debug("Reserving %dk for malloc() at: %08lx\n", TOTAL_MALLOC_LEN >> 10, addr_sp); /* * (permanently) allocate a Board Info struct * and a permanent copy of the "global" data */ addr_sp -= sizeof (bd_t); bd = (bd_t *) addr_sp; gd->bd = bd; debug("Reserving %zu Bytes for Board Info at: %08lx\n", sizeof (bd_t), addr_sp); #ifdef CONFIG_MACH_TYPE gd->bd->bi_arch_number = CONFIG_MACH_TYPE; /* board id for Linux */ #endif addr_sp -= sizeof (gd_t); id = (gd_t *) addr_sp; debug("Reserving %zu Bytes for Global Data at: %08lx\n", sizeof (gd_t), addr_sp); #ifndef CONFIG_ARM64 /* setup stackpointer for exeptions */ gd->irq_sp = addr_sp; #ifdef CONFIG_USE_IRQ addr_sp -= (CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ); debug("Reserving %zu Bytes for IRQ stack at: %08lx\n", CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ, addr_sp); #endif /* leave 3 words for abort-stack */ addr_sp -= 12; /* 8-byte alignment for ABI compliance */ addr_sp &= ~0x07; #else /* CONFIG_ARM64 */ /* 16-byte alignment for ABI compliance */ addr_sp &= ~0x0f; #endif /* CONFIG_ARM64 */
首先预留malloc len,这里我定义的是0x400000.
注释中说明,为bd,gd做一个永久的copy。
留出了全局信息bd_t结构体的空间,首地址存在gd->bd。
留出gd_t结构体的空间。首地址存在id中。
将此地址保存在gd->irq_sp中作为异常栈指针。uboot中我们没有用到中断。
最后留出12字节,for abort stack,这个没看懂。
到这里addr_sp值确定,总结一下addr_sp之上空间分配。
由高到低 : addr-->malloc len(0x400000)-->bd len-->gd len-->12 byte-->addr_sp(栈往下增长,addr_sp之下空间作为栈空间)
gd->bd->bi_baudrate = gd->baudrate; /* Ram ist board specific, so move it to board code ... */ dram_init_banksize(); display_dram_config(); /* and display it */ gd->relocaddr = addr; gd->start_addr_sp = addr_sp; gd->reloc_off = addr - (ulong)&_start; debug("relocation Offset is: %08lx\n", gd->reloc_off); if (new_fdt) { memcpy(new_fdt, gd->fdt_blob, fdt_size); gd->fdt_blob = new_fdt; } memcpy(id, (void *)gd, sizeof(gd_t));给bd->bi_baudrate赋值gd->baudrate,gd->baudrate是在前面baudrate_init中初始化。
dram_init_banksize()是需要实现的板级函数。根据板上ddrc获取ddr的bank信息。填充在gd->bd->bi_dram[CONFIG_NR_DRAM_BANKS]。
gd->relocaaddr为目标addr,gd->start_addr_sp为目标addr_sp,gd->reloc_off为目标addr和现在实际code起始地址的偏移。reloc_off非常重要,会作为后面relocate_code函数的参数,来实现code的拷贝。
最后将gd结构体的数据拷贝到新的地址id上。
board_init_f函数将sdram空间重新进行了划分,可以看出栈空间和堆空间是分开的,就不存在_main调用board_init_f之前的那个问题啦。
并且在重新规划空间完成之前并没有出现初始化堆,以及使用堆空间的问题,比如malloc函数,所以之前的堆栈空间重合的问题是过虑了。
至此,board_init_f结束,回到_main
四 _main
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 */ bic sp, sp, #7 /* 8-byte alignment for ABI compliance */ ldr r9, [r9, #GD_BD] /* r9 = gd->bd */ sub r9, r9, #GD_SIZE /* new GD is below bd */ adr lr, here ldr r0, [r9, #GD_RELOC_OFF] /* r0 = gd->reloc_off */ add lr, lr, r0 ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */ b relocate_code here:这段汇编很有意思,前4条汇编实现了新gd结构体的更新。
首先更新sp,并且将sp 8字节对齐,方便后面函数开辟栈能对齐,
然后获取gd->bd地址到r9中,需要注意,在board_init_f中gd->bd已经更新为新分配的bd了,下一条汇编将r9减掉bd的size,这样就获取到了board_init_f中新分配的gd了!
后面汇编则是为relocate_code做准备,首先加载here地址,然后加上新地址偏移量给lr,则是code relocate后的新here了,relocate_code返回条转到lr,则是新位置的here!
最后在r0中保存code的新地址,跳转到relocate_code
五 relocate_code
relocate_code函数在arch/arm/lib/relocate.S中,这个函数实现了将uboot code拷贝到relocaddr。
这部分算是整个uboot中最核心也是最难理解的代码,我单独写了一篇文章来介绍这一部分的工作原理,感兴趣的朋友可以看下面这个链接
http://blog.csdn.net/skyflying2012/article/details/37660265
这里就不再详说了。
到这里需要总结一下,经过上面的分析可以看出,
新版uboot在sdram空间分配上,是自顶向下,
不管uboot是从哪里启动,spiflash,nandflash,sram等跑到这里code都会被从新定位到sdram上部的一个位置,继续运行。
我找了一个2010.6版本的uboot大体看了一下启动代码,是通过判断_start和TEXT_BASE(链接地址)是否相等来确定是否需要relocate。如果uboot是从sdram启动则不需要relocate。
新版uboot在这方面还是有较大变动。
这样变动我考虑好处可能有二,一是不用考虑启动方式,all relocate code。二是不用考虑uboot链接地址,因为都要重新relocate。
uboot sdram空间规划图:
六 _main
从relocate_code回到_main中,接下来是main最后一段代码,如下:
/* 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, r9 /* gd_t */ ldr r1, [r9, #GD_RELOCADDR] /* dest_addr */ /* call board_init_r */ ldr pc, =board_init_r /* this is auto-relocated! */ /* we should not return here. */首先跳转到c_runtime_cpu_setup,如下:
ENTRY(c_runtime_cpu_setup) /* * If I-cache is enabled invalidate it */ #ifndef CONFIG_SYS_ICACHE_OFF mcr p15, 0, r0, c7, c5, 0 @ invalidate icache mcr p15, 0, r0, c7, c10, 4 @ DSB mcr p15, 0, r0, c7, c5, 4 @ ISB #endif /* * Move vector table */ /* Set vector address in CP15 VBAR register */ ldr r0, =_start mcr p15, 0, r0, c12, c0, 0 @Set VBAR bx lr ENDPROC(c_runtime_cpu_setup)如果icache是enable,则无效掉icache,保证从sdram中更新指令到cache中。
接着更新异常向量表首地址,因为code被relocate,所以异常向量表也被relocate。
从c_runtime_cpu_setup返回,下面一段汇编是将bss段清空。
接下来分别调用了coloured_LED_init以及red_led_on,很多开发板都会有led指示灯,这里可以实现上电指示灯亮,有调试作用。
最后r0赋值gd指针,r1赋值relocaddr,进入最后的board_init_r !
七 board_init_r
参数1是新gd指针,参数2是relocate addr,也就是新code地址
gd->flags |= GD_FLG_RELOC; /* tell others: relocation done */ bootstage_mark_name(BOOTSTAGE_ID_START_UBOOT_R, "board_init_r"); monitor_flash_len = (ulong)&__rel_dyn_end - (ulong)_start; /* Enable caches */ enable_caches(); debug("monitor flash len: %08lX\n", monitor_flash_len); board_init(); /* Setup chipselects */
#ifdef CONFIG_CLOCKS set_cpu_clk_info(); /* Setup clock information */ #endif serial_initialize(); debug("Now running in RAM - U-Boot at: %08lx\n", dest_addr);如果打开CONFIG_CLOCKS,set_cpu_clk_info也是需要实现的板级支持函数。
重点来说一些serial_initialize,对于最精简能正常启动的uboot,serial和ddr是必须正常工作的。
实现在drivers/serial/serial.c中,如下:
void serial_initialize(void) { mpc8xx_serial_initialize(); ns16550_serial_initialize(); pxa_serial_initialize(); s3c24xx_serial_initialize(); s5p_serial_initialize(); mpc512x_serial_initialize();。。。。 mxs_auart_initialize(); arc_serial_initialize(); vc0718_serial_initialize(); serial_assign(default_serial_console()->name); }所有串口驱动都会实现一个xxxx_serial_initialize函数,并且添加到serial_initialize中,
xxxx_serial_initialize函数中是将所有需要的串口(用结构体struct serial_device表示,其中实现了基本的收 发 配置)调用serial_register注册,serial_register如下:
void serial_register(struct serial_device *dev) { #ifdef CONFIG_NEEDS_MANUAL_RELOC if (dev->start) dev->start += gd->reloc_off; if (dev->stop) dev->stop += gd->reloc_off; if (dev->setbrg) dev->setbrg += gd->reloc_off; if (dev->getc) dev->getc += gd->reloc_off; if (dev->tstc) dev->tstc += gd->reloc_off; if (dev->putc) dev->putc += gd->reloc_off; if (dev->puts) dev->puts += gd->reloc_off; #endif dev->next = serial_devices; serial_devices = dev; }就是将你的serial_dev加到全局链表serial_devices中。
int serial_assign(const char *name) { struct serial_device *s; for (s = serial_devices; s; s = s->next) { if (strcmp(s->name, name)) continue; serial_current = s; return 0; } return -EINVAL; }serial_assign就是从serial_devices链表中找到指定的默认调试串口,条件就是串口的name,最后serial_current就是当前的默认串口了。
/* The Malloc area is immediately below the monitor copy in DRAM */ malloc_start = dest_addr - TOTAL_MALLOC_LEN; mem_malloc_init (malloc_start, TOTAL_MALLOC_LEN);
void mem_malloc_init(ulong start, ulong size) { mem_malloc_start = start; mem_malloc_end = start + size; mem_malloc_brk = start; memset((void *)mem_malloc_start, 0, size); malloc_bin_reloc(); }mem_malloc_init中就是对malloc预留的空间初始化,起始地址,结束地址,清空。咱们已经relocate,malloc_bin_reloc中无操作了。
void serial_stdio_init(void) { struct stdio_dev dev; struct serial_device *s = serial_devices; while (s) { memset(&dev, 0, sizeof(dev)); strcpy(dev.name, s->name); dev.flags = DEV_FLAGS_OUTPUT | DEV_FLAGS_INPUT; dev.start = s->start; dev.stop = s->stop; dev.putc = s->putc; dev.puts = s->puts; dev.getc = s->getc; dev.tstc = s->tstc; stdio_register(&dev); s = s->next; } }将serial_devices链表上所有serial device同样初始化一个stdio_dev,flag为output input,调用stdio-register,将stdio_dev添加到全局devs链表中。
int console_init_r(void) { struct stdio_dev *inputdev = NULL, *outputdev = NULL; int i; struct list_head *list = stdio_get_list(); struct list_head *pos; struct stdio_dev *dev; /* Scan devices looking for input and output devices */ list_for_each(pos, list) { dev = list_entry(pos, struct stdio_dev, list); if ((dev->flags & DEV_FLAGS_INPUT) && (inputdev == NULL)) { inputdev = dev; } if ((dev->flags & DEV_FLAGS_OUTPUT) && (outputdev == NULL)) { outputdev = dev; } if(inputdev && outputdev) break; } if (outputdev != NULL) { console_setfile(stdout, outputdev); console_setfile(stderr, outputdev); } /* Initializes input console */ if (inputdev != NULL) { console_setfile(stdin, inputdev); } #ifndef CONFIG_SYS_CONSOLE_INFO_QUIET stdio_print_current_devices(); #endif /* CONFIG_SYS_CONSOLE_INFO_QUIET */ /* Setting environment variables */ for (i = 0; i < 3; i++) { setenv(stdio_names[i], stdio_devices[i]->name); } gd->flags |= GD_FLG_DEVINIT; /* device initialization completed */ return 0; }console_init_r前半部分很清楚了,从devs.list链表中查找flag为output或者input的dev,如果只有serial之前注册了stdio_dev,则outputdev inputdev都是咱们注册的第一个serial。
static int console_setfile(int file, struct stdio_dev * dev) { int error = 0; if (dev == NULL) return -1; switch (file) { case stdin: case stdout: case stderr: /* Start new device */ if (dev->start) { error = dev->start(); /* If it's not started dont use it */ if (error < 0) break; } /* Assign the new device (leaving the existing one started) */ stdio_devices[file] = dev; /* * Update monitor functions * (to use the console stuff by other applications) */ switch (file) { case stdin: gd->jt[XF_getc] = dev->getc; gd->jt[XF_tstc] = dev->tstc; break; case stdout: gd->jt[XF_putc] = dev->putc; gd->jt[XF_puts] = dev->puts; gd->jt[XF_printf] = printf; break; } break; default: /* Invalid file ID */ error = -1; } return error; }首先运行设备的start,就是特定serial实现的start函数。然后将stdio_device放到stdio_devices全局数组中,这个数组3个成员,stdout,stderr,stdin。最后还会在gd中设一下操作函数。
int printf(const char *fmt, ...) { va_list args; uint i; char printbuffer[CONFIG_SYS_PBSIZE]; #if !defined(CONFIG_SANDBOX) && !defined(CONFIG_PRE_CONSOLE_BUFFER) if (!gd->have_console) return 0; #endif va_start(args, fmt); /* For this to work, printbuffer must be larger than * anything we ever want to print. */ i = vscnprintf(printbuffer, sizeof(printbuffer), fmt, args); va_end(args); /* Print the string */ puts(printbuffer); return i; }字符串的拼接跟一般printf实现一样,最后调用puts,puts实现也在console.c中,如下:
void puts(const char *s) { #ifdef CONFIG_SANDBOX if (!gd) { os_puts(s); return; } #endif #ifdef CONFIG_SILENT_CONSOLE if (gd->flags & GD_FLG_SILENT) return; #endif #ifdef CONFIG_DISABLE_CONSOLE if (gd->flags & GD_FLG_DISABLE_CONSOLE) return; #endif if (!gd->have_console) return pre_console_puts(s); if (gd->flags & GD_FLG_DEVINIT) { /* Send to the standard output */ fputs(stdout, s); } else { /* Send directly to the handler */ serial_puts(s); } }
gd->have_console在board_init_f的console_init_f中置位,flag的GD_FLG_DEVINIT则是在刚才board_init_r中console_init_r最后置位。
如果GD_FLG_DEVINIT没有置位,表明console没有注册,是在board_init_f之后,board_init_r执行完成之前,这时调用serial_puts,如下:
void serial_puts(const char *s) { get_current()->puts(s); }
void fputs(int file, const char *s) { if (file < MAX_FILES) console_puts(file, s); } static inline void console_puts(int file, const char *s) { stdio_devices[file]->puts(s); }fputs调console_puts从全局stdio_devices中找到对应stdout对应的成员stdio_device,调用puts,最终也是会调用到特定serial的puts函数。