uboot运行有两个阶段,一个阶段是运行在SRAM中的汇编阶段,另一个是运行在DDR中的C语言阶段。我们先进行第一阶段的分析。
start.S文件
start.S文件组成了uboot的第一阶段运行内容,在链接脚本中的代码段中第一句链接的start.o就说明了这一点,我们知道C语言中main函数就是整个程序的入口,在uboot汇编阶段中,程序的入口取决链接脚本中ENTRY声明的部分,我们知道uboot中ENTRY声明的入口为_start,所以_start符号所在处的代码就是程序的起始代码,而这段代码位于cpu\s5pc11x\中的start.S文件中。
头文件包含
start.S包含了很多头文件:
#include <config.h>
#include <version.h>
#if defined(CONFIG_ENABLE_MMU)
#include <asm/proc/domain.h>
#endif
#include <regs.h>
#ifndef CONFIG_ENABLE_MMU
#ifndef CFG_PHY_UBOOT_BASE
#define CFG_PHY_UBOOT_BASE CFG_UBOOT_BASE
#endif
#endif
config.h在配置阶段出现,在include目录下,文件内容是其实包含了x210_sd.h头文件,所以包含的其实是configs/x210_sd.h,这个文件是uboot移植时的配置文件,是由很多宏组成的。start.S后面的很多宏判断就是来自于该文件。
version.h中的内容为:
#ifndef __VERSION_H__
#define __VERSION_H__
#ifndef DO_DEPS_ONLY
#include "version_autogenerated.h"
#endif
#endif /* __VERSION_H__ */
包含了version_autogenerated.h,这个文件我们在Makefile开头就已经见过,里面是自动生成的版本号信息。在uboot启动过程中会从串口打印出版本号。
如果定义了CONFIG_ENABLE_MMU这个宏,就会包含asm/proc/domain.h头文件,这个宏是否定义在x210_sd.h中可以找到,通过确认是定义了的,所以这个头文件会被包含进来。asm目录不是uboot原生目录,是配置时创建的符号链接,实际指向的是asm-arm,同理,proc也是一个符号链接,指向proc-armv。domain.h文件的内容为一些宏文定义。
启动校验头
#if defined(CONFIG_EVT1) && !defined(CONFIG_FUSED)
.word 0x2000
.word 0x0
.word 0x0
.word 0x0
#endif
该判断语句成立,.word是gun汇编的伪指令相当于定义了一个4字节的变量,值为后面对应的值,地址为当前地址,所以这句相当于定义了一个int类型的数据,数组里面有4个元素。其长度总共16个字节。
使用SD卡或者Nand启动镜像是需要16个字节的校验头,这16个字节就是该校验头,usb下载方式启动则不需要校验头。uboot这里放了16字节填充占位,其中的内容是不能作准的,只是保证有16字节的空间,后面需要计算校验和后重新填充(sd_fusing中)。
异常向量表
_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
_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 */
异常向量表是硬件决定的,软件只是参照硬件设计定义了发生异常的时候跳转到哪个函数去执行,每种异常都应该被处理,防止发生异常跑飞,uboot由于程序简单并未细致处理各种异常,reset就是系统启动时执行的代码,其实每一次系统启动都是一次复位,reset的代码在下面:
/*
* the actual reset code
*/
reset:
/*
* set the cpu to SVC32 mode and IRQ & FIQ disable
*/
@;mrs r0,cpsr
@;bic r0,r0,#0x1f
@;orr r0,r0,#0xd3
@;msr cpsr,r0
msr cpsr_c, #0xd3 @ I & F disable, Mode: 0x13 - SVC
设置CPU为svc32模式并关闭FIQ和IRQ。
坏牛肉
.global _end_vect
_end_vect:
.balignl 16,0xdeadbeef
balignl 是一个伪指令,使内存按照16字节对齐,如果没对齐自动向后直到对齐,并依次使用0xdeadbeef来填充,可以看到填充物没有规定,使用了deadbeef这样的类单词语句。
为什么要对齐访问?有时候是效率要求,有时候是硬件的要求。
TEXT_BASE
_TEXT_BASE:
.word TEXT_BASE
TEXT_BASE就是Makefile配置阶段指定的TEXT_BASE,值为0xc3e00000,由此可见在源代码中和Makefile配置中很多变量是互相共用的,这个值就是从Makefile配置中传递过来的。
TEXT_PHY_BASE
/*
* Below variable is very important because we use MMU in U-Boot.
* Without it, we cannot run code correctly before MMU is ON.
* by scsuh.
*/
_TEXT_PHY_BASE:
.word CFG_PHY_UBOOT_BASE
代表uboot早DDR内存中物理地址的基址,CFG_PHY_UBOOT_BASE是一个x210.h里面的宏,其值为”MEMORY_BASE_ADDRESS + 0x3e00000”,其中MEMORY_BASE_ADDRESS的值为0x30000000,即DDR内存的起始地址,所以CFG_PHY_UBOOT_BASE值为0x33e00000。
设置CPU为SVC模式
/*
* the actual reset code
*/
reset:
/*
* set the cpu to SVC32 mode and IRQ & FIQ disable
*/
@;mrs r0,cpsr
@;bic r0,r0,#0x1f
@;orr r0,r0,#0xd3
@;msr cpsr,r0
msr cpsr_c, #0xd3 @ I & F disable, Mode: 0x13 - SVC
前4局代码被注释掉了,我们只关心后一句代码,这一句向cpsr的c位写入0xd3,我们知道cpsr是程序状态寄存器,c位是CPU的模式位,写入0xd3之后状态位的值为10011,也就是0x13,该值代表SVC模式。第5位是0,代表为arm模式6,7位是1,代表禁止IRQ和FIQ。其实arm的cpu在复位的时候,默认就会进入SVC模式,而且整个uboot程序运行时CPU都出于SVC模式,这里使用代码设置,多一层保险。
设置L1,L2 Cache和MMU
bl disable_l2cache
bl set_l2cache_auxctrl_cycle
bl enable_l2cache
/*
* 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
/*
* 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 12 (Z---) BTB
mcr p15, 0, r0, c1, c0, 0
- 禁止了L2 Cache,设置L2 Cache参数,再启动L2 Cache
- 刷新L1 Cache的icache指令缓存和dcache数据缓存
- 关闭MMU和相关cache
选择启动介质
/* Read booting information */
ldr r0, =PRO_ID_BASE
ldr r1, [r0,#OMR_OFFSET]
bic r2, r1, #0xffffffc1
读取启动信息,从PRO_ID_BASE(0xE0000000)中读取数据到r0,在加上OMR_OFFSET(0x04)的偏移量,所以值为0xE0000004,这个寄存器的值是根据OM引脚自动设置的,指定了从什么地方启动,我们在代码中可以读取该值来判断来选择什么启动介质来启动。最终r2就存储了启动信息。
/* NAND BOOT */
cmp r2, #0x0 @ 512B 4-cycle
moveq r3, #BOOT_NAND
cmp r2, #0x2 @ 2KB 5-cycle
moveq r3, #BOOT_NAND
cmp r2, #0x4 @ 4KB 5-cycle 8-bit ECC
moveq r3, #BOOT_NAND
cmp r2, #0x6 @ 4KB 5-cycle 16-bit ECC
moveq r3, #BOOT_NAND
cmp r2, #0x8 @ OneNAND Mux
moveq r3, #BOOT_ONENAND
/* SD/MMC BOOT */
cmp r2, #0xc
moveq r3, #BOOT_MMCSD
/* NOR BOOT */
cmp r2, #0x14
moveq r3, #BOOT_NOR
判断r2的值:
- 0代表512字节4cycle的nand
- 2代表2KB,5cycle的nand
- 4代表4KB,5cycle,8bitECC的nand
- 6代表4KB,5cycle,16bitECC的nand
- 8代表one nand
- c代表从SD/MMC启动
- 14代表从NOR Flash启动
这个启动信息将会放置到r3寄存器中。
设置栈
/*
* Go setup Memory and board specific bits prior to relocation.
*/
ldr sp, =0xd0036000 /* end of sram dedicated to u-boot */
sub sp, sp, #12 /* set stack */
mov fp, #0
设置sp寄存器值为0xd0036000,这个寄存器的位置是在sram中用于放置栈的,为调用函数创建环境。因为BL调用只会将返回地址存储到lr中,但是我们只有一个lr,所以第二层函数调用前要先将lr入栈,否则函数返回时第一层函数的返回地址就丢了。之所以不在DDR中设置栈,因为当前代码还在sram中运行,DDR还没有被初始化,不能使用。
低层初始化
bl lowlevel_init /* go setup pll,mux,memory */
lowlevel_init这个函数位于board/samsung/x210/lowlevel_init .S文件中。
压栈
使用语句push {lr}
将lr寄存器压栈,以便于后面再进行bl调用。
检查复位状态
/* check reset status */
ldr r0, =(ELFIN_CLOCK_POWER_BASE+RST_STAT_OFFSET)
ldr r1, [r0]
bic r1, r1, #0xfff6ffff
cmp r1, #0x10000
beq wakeup_reset_pre
cmp r1, #0x80000
beq wakeup_reset_from_didle
/* IO Retention release */
ldr r0, =(ELFIN_CLOCK_POWER_BASE + OTHERS_OFFSET)
ldr r1, [r0]
ldr r2, =IO_RET_REL
orr r1, r1, r2
str r1, [r0]
现代CPU在复位的时候有好几种情况,冷启动,热启动,睡眠唤醒,低功耗唤醒等,这里检测复位状态来判断属于哪种情况,例如冷上电时DDR需要初始化再使用,而其他一些情况则不需要。IO状态恢复检查也是一样的道理。
关闭看门狗
/* Disable Watchdog */
ldr r0, =ELFIN_WATCHDOG_BASE /* 0xE2700000 */
mov r1, #0
str r1, [r0]
SRAM,SROM的GPIO设置
/* SRAM(2MB) init for SMDKC110 */
/* GPJ1 SROM_ADDR_16to21 */
ldr r0, =ELFIN_GPIO_BASE
ldr r1, [r0, #GPJ1CON_OFFSET]
bic r1, r1, #0xFFFFFF
ldr r2, =0x444444
orr r1, r1, r2
str r1, [r0, #GPJ1CON_OFFSET]
ldr r1, [r0, #GPJ1PUD_OFFSET]
ldr r2, =0x3ff
bic r1, r1, r2
str r1, [r0, #GPJ1PUD_OFFSET]
/* GPJ4 SROM_ADDR_16to21 */
ldr r1, [r0, #GPJ4CON_OFFSET]
bic r1, r1, #(0xf<<16)
ldr r2, =(0x4<<16)
orr r1, r1, r2
str r1, [r0, #GPJ4CON_OFFSET]
ldr r1, [r0, #GPJ4PUD_OFFSET]
ldr r2, =(0x3<<8)
bic r1, r1, r2
str r1, [r0, #GPJ4PUD_OFFSET]
这里指的是使用SROM_BANK进行外部扩展的SRAM以及SROM。
供电锁存
/* PS_HOLD pin(GPH0_0) set to high */
ldr r0, =(ELFIN_CLOCK_POWER_BASE + PS_HOLD_CONTROL_OFFSET)
ldr r1, [r0]
orr r1, r1, #0x300
orr r1, r1, #0x1
str r1, [r0]
判断当前代码位置
/* when we already run in ram, we don't need to relocate U-Boot.
* and actually, memory controller must be configured before U-Boot
* is running in ram.
*/
ldr r0, =0xff000fff
bic r1, pc, r0 /* r0 <- current base addr of code */
ldr r2, _TEXT_BASE /* r1 <- original base addr in ram */
bic r2, r2, r0 /* r0 <- current base addr of code */
cmp r1, r2 /* compare r0, r1 */
beq 1f /* r0 == r1 then skip sdram init */
判断当前的代码是在哪里运行的,在SRAM中还是DDR中。由于uboot分成两部分,前一部分BL1在SRAM中,整个UBOOT在DDR中,所以这段代码有可能在SRAM中或者是DDR中运行,低功耗或者热启动时也会在DDR中,获取当前代码运行的地址也可以指导后面代码的运行,用于确定要不要初始化DDR以及初始化时钟。
- 向r0中加载一个常数0xff000fff
- bic r1, pc, r0,将pc的值中在r0中为1的bit清零,剩下的bit位赋值给r1。
- 将DRAM连接地址的基地址TEXT_BASE(0xc3e00000)加载到r2。
- bic r2, r2, r0,将r2的想赢bit也清零,剩下特定的bit位。
- 比较r1和r2,如果一致代表运行在DDR,不一致就是运行在SRAM中。
初始化系统时钟
/* init system clock */
bl system_clock_init
system_clock_init函数位于当前文件的205行,内容如下:
/*
* system_clock_init: Initialize core clock and bus clock.
* void system_clock_init(void)
*/
system_clock_init:
ldr r0, =ELFIN_CLOCK_POWER_BASE @0xe0100000
/* Set Mux to FIN */
ldr r1, =0x0
str r1, [r0, #CLK_SRC0_OFFSET]
ldr r1, =APLL_LOCKTIME_VAL
str r1, [r0, #APLL_LOCK_OFFSET]
/********lxg added*********************/
ldr r0, =ELFIN_CLOCK_POWER_BASE @0xe0100000
ldr r1, =MPLL_LOCKTIME_VAL
str r1, [r0, #MPLL_LOCK_OFFSET]
/********end*********************/
/* Disable PLL */
#if defined(CONFIG_CHECK_MPLL_LOCK)
retryloop:
#endif
ldr r1, =0x0
str r1, [r0, #APLL_CON0_OFFSET]
ldr r1, =0x0
str r1, [r0, #MPLL_CON_OFFSET]
ldr r1, =0x0
str r1, [r0, #MPLL_CON_OFFSET]
ldr r1, [r0, #CLK_DIV0_OFFSET]
ldr r2, =CLK_DIV0_MASK
bic r1, r1, r2
ldr r2, =CLK_DIV0_VAL
orr r1, r1, r2
str r1, [r0, #CLK_DIV0_OFFSET]
ldr r1, =APLL_VAL
str r1, [r0, #APLL_CON0_OFFSET]
ldr r1, =MPLL_VAL
str r1, [r0, #MPLL_CON_OFFSET]
ldr r1, =VPLL_VAL
str r1, [r0, #VPLL_CON_OFFSET]
/*******lxg added***********************/
ldr r1, =EPLL_VAL
str r1, [r0, #EPLL_CON_OFFSET]
/*******lxg added***********************/
ldr r1, [r0, #CLK_DIV1_OFFSET]
ldr r2, =CLK_DIV1_MASK
bic r1, r1, r2
ldr r2, =CLK_DIV1_VAL
orr r1, r1, r2
str r1, [r0, #CLK_DIV1_OFFSET]
ldr r1, [r0, #CLK_DIV2_OFFSET]
ldr r2, =CLK_DIV2_MASK
bic r1, r1, r2
ldr r2, =CLK_DIV2_VAL
orr r1, r1, r2
str r1, [r0, #CLK_DIV2_OFFSET]
ldr r1, [r0, #CLK_DIV4_OFFSET]
ldr r2, =CLK_DIV4_MASK
bic r1, r1, r2
ldr r2, =CLK_DIV4_VAL
orr r1, r1, r2
str r1, [r0, #CLK_DIV4_OFFSET]
ldr r1, [r0, #CLK_DIV6_OFFSET]
ldr r2, =CLK_DIV6_MASK
bic r1, r1, r2
ldr r2, =CLK_DIV6_VAL
orr r1, r1, r2
str r1, [r0, #CLK_DIV6_OFFSET]
/*******end*****************/
/*******end*****************/
#if defined(CONFIG_EVT1)
ldr r1, =AFC_ON
str r1, [r0, #APLL_CON1_OFFSET]
#endif
mov r1, #0x10000
1: subs r1, r1, #1
bne 1b
#if defined(CONFIG_CHECK_MPLL_LOCK)
/* MPLL software workaround */
ldr r1, [r0, #MPLL_CON_OFFSET]
orr r1, r1, #(1<<28)
str r1, [r0, #MPLL_CON_OFFSET]
mov r1, #0x100
1: subs r1, r1, #1
bne 1b
ldr r1, [r0, #MPLL_CON_OFFSET]
and r1, r1, #(1<<29)
cmp r1, #(1<<29)
bne retryloop
/* H/W lock detect disable */
ldr r1, [r0, #MPLL_CON_OFFSET]
bic r1, r1, #(1<<28)
str r1, [r0, #MPLL_CON_OFFSET]
#endif
ldr r1, [r0, #CLK_SRC0_OFFSET]
//ldr r2, =0x10001111 //lxg changed.
ldr r2, =0x00000111
orr r1, r1, r2
str r1, [r0, #CLK_SRC0_OFFSET]
// added by terry 2012.12.4 for camera
ldr r1, [r0, #CLK_SRC1_OFFSET]
bic r1, r1, #(0xf<<12)
orr r1, r1, #(0x1<<12) //0001 XusbXTI
str r1, [r0, #CLK_SRC1_OFFSET]
#if defined(CONFIG_MCP_AC)
/* CLK_SRC6[25:24] -> OneDRAM clock sel = MPLL */
ldr r1, [r0, #CLK_SRC6_OFFSET]
bic r1, r1, #(0x3<<24)
orr r1, r1, #0x01000000
str r1, [r0, #CLK_SRC6_OFFSET]
/* CLK_DIV6[31:28] -> 4=1/5, 3=1/4(166MHZ@667MHz), 2=1/3 */
ldr r1, [r0, #CLK_DIV6_OFFSET]
bic r1, r1, #(0xF<<28)
bic r1, r1, #(0x7<<12) @; ONENAND_RATIO: 0
orr r1, r1, #0x30000000
str r1, [r0, #CLK_DIV6_OFFSET]
#elif defined (CONFIG_MCP_H)
/* CLK_SRC6[25:24] -> OneDRAM clock sel = 00:SCLKA2M, 01:SCLKMPLL */
ldr r1, [r0, #CLK_SRC6_OFFSET]
bic r1, r1, #(0x3<<24)
orr r1, r1, #0x00000000
str r1, [r0, #CLK_SRC6_OFFSET]
/* CLK_DIV6[31:28] -> 4=1/5, 3=1/4(166MHZ@667MHz), 2=1/3 */
ldr r1, [r0, #CLK_DIV6_OFFSET]
bic r1, r1, #(0xF<<28)
bic r1, r1, #(0x7<<12) @; ONENAND_RATIO: 0
orr r1, r1, #0x00000000
str r1, [r0, #CLK_DIV6_OFFSET]
#elif defined (CONFIG_MCP_B) || defined (CONFIG_MCP_D)
/* CLK_SRC6[25:24] -> OneDRAM clock sel = 00:SCLKA2M, 01:SCLKMPLL */
ldr r1, [r0, #CLK_SRC6_OFFSET]
bic r1, r1, #(0x3<<24)
orr r1, r1, #0x01000000
str r1, [r0, #CLK_SRC6_OFFSET]
/* CLK_DIV6[31:28] -> 4=1/5, 3=1/4(166MHZ@667MHz), 2=1/3 */
ldr r1, [r0, #CLK_DIV6_OFFSET]
bic r1, r1, #(0xF<<28)
bic r1, r1, #(0x7<<12) @; ONENAND_RATIO: 0
orr r1, r1, #0x30000000
str r1, [r0, #CLK_DIV6_OFFSET]
#elif defined (CONFIG_MCP_SINGLE)
/* CLK_DIV6 */
/*ldr r1, [r0, #CLK_DIV6_OFFSET]
bic r1, r1, #(0x7<<12) @; ONENAND_RATIO: 0
str r1, [r0, #CLK_DIV6_OFFSET]*/ //lxg mask
#endif
mov pc, lr
时钟初始化的过程和裸机程序中初始化是一样的,最后使用mov pc lr
代码返回到调用函数,完成时钟的初始化。
初始化DRAM
/* Memory initialize */
bl mem_ctrl_asm_init
mem_ctrl_asm_init函数中初始化DDR的操作和裸机程序初始化DDR的操作是一样的。
串口初始化
/* for UART */
bl uart_asm_init
这个函数初始化串口的方式和裸机程序中是一样的。串口初始化完毕后通过串口发送了一个大写的“O”,用于检测初始化是否成功。
初始化TrustZone
bl tzpc_init
初始化TrustZone,以便使用TEE环境。
至此,lowlevel_init执行完毕,最后使用pop {pc}
进行函数的返回,如果初始化成功,则会打印“OK”字样。
栈设置
/* get ready to call C functions */
ldr sp, _TEXT_PHY_BASE /* setup temp stack pointer */
sub sp, sp, #12
mov fp, #0 /* no previous frame, so fp=0 */
之前已经设置过栈了,这里再次设置栈,是因为之前程序的执行都是在SRAM中,SRAM空间有限,不能过多的使用进行函数调用,否则会造成内存溢出,但是在这里DDR已经被初始化了,有大片的内存空间可用,因此要把栈移到DDR中,所以在这里要重新设置栈。
判断运行地址决定是否重定位
/* when we already run in ram, we don't need to relocate U-Boot.
* and actually, memory controller must be configured before U-Boot
* is running in ram.
*/
ldr r0, =0xff000fff
bic r1, pc, r0 /* r0 <- current base addr of code */
ldr r2, _TEXT_BASE /* r1 <- original base addr in ram */
bic r2, r2, r0 /* r0 <- current base addr of code */
cmp r1, r2 /* compare r0, r1 */
beq after_copy /* r0 == r1 then skip flash copy */
这里的判断方法和之前是一样的,用于决定是否进行relocate,uboot第第一部分即将结束之前,要把第二部分加载到DDR的链接地址上(0x33e00000),这个过程就是重定位,这里判断如果相等则跳过下面的代码直接运行after_copy函数。如果不相等就执行下面的重定位代码。
重定位
#if defined(CONFIG_EVT1)
/* If BL1 was copied from SD/MMC CH2 */
ldr r0, =0xD0037488
ldr r1, [r0]
ldr r2, =0xEB200000
cmp r1, r2
beq mmcsd_boot
#endif
ldr r0, =INF_REG_BASE
ldr r1, [r0, #INF_REG3_OFFSET]
cmp r1, #BOOT_NAND /* 0x0 => boot device is nand */
beq nand_boot
cmp r1, #BOOT_ONENAND /* 0x1 => boot device is onenand */
beq onenand_boot
cmp r1, #BOOT_MMCSD
beq mmcsd_boot
cmp r1, #BOOT_NOR
beq nor_boot
cmp r1, #BOOT_SEC_DEV
beq mmcsd_boot
0xD0037488的值是硬件自动设置的,硬件根据实际电路中SD卡在哪个通道中,会将它的值设置为相应数据。用于决定是从外部的SD卡启动还是从内部SD卡(iNand)启动。
接下来的代码判断是从哪个nand中进行启动,因为我们是从外部SD卡启动则会执行beq mmcsd_boot
,调用mmcsd_boot函数。函数内容如下:
mmcsd_boot:
#if DELETE
ldr sp, _TEXT_PHY_BASE
sub sp, sp, #12
mov fp, #0
#endif
bl movi_bl2_copy
b after_copy
可以看到主要调用的是movi_bl2_copy这个函数,用于将第二部分拷贝到DDR中,该函数是一个C语言函数:
void movi_bl2_copy(void)
{
ulong ch;
#if defined(CONFIG_EVT1)
ch = *(volatile u32 *)(0xD0037488);
copy_sd_mmc_to_mem copy_bl2 =
(copy_sd_mmc_to_mem) (*(u32 *) (0xD0037F98));
#if defined(CONFIG_SECURE_BOOT)
ulong rv;
#endif
#else
ch = *(volatile u32 *)(0xD003A508);
copy_sd_mmc_to_mem copy_bl2 =
(copy_sd_mmc_to_mem) (*(u32 *) (0xD003E008));
#endif
u32 ret;
if (ch == 0xEB000000) {
ret = copy_bl2(0, MOVI_BL2_POS, MOVI_BL2_BLKCNT,
CFG_PHY_UBOOT_BASE, 0);
#if defined(CONFIG_SECURE_BOOT)
/* do security check */
rv = Check_Signature( (SecureBoot_CTX *)SECURE_BOOT_CONTEXT_ADDR,
(unsigned char *)CFG_PHY_UBOOT_BASE, (1024*512-128),
(unsigned char *)(CFG_PHY_UBOOT_BASE+(1024*512-128)), 128 );
if (rv != 0){
while(1);
}
#endif
}
else if (ch == 0xEB200000) {
ret = copy_bl2(2, MOVI_BL2_POS, MOVI_BL2_BLKCNT,
CFG_PHY_UBOOT_BASE, 0);
#if defined(CONFIG_SECURE_BOOT)
/* do security check */
rv = Check_Signature( (SecureBoot_CTX *)SECURE_BOOT_CONTEXT_ADDR,
(unsigned char *)CFG_PHY_UBOOT_BASE, (1024*512-128),
(unsigned char *)(CFG_PHY_UBOOT_BASE+(1024*512-128)), 128 );
if (rv != 0) {
while(1);
}
#endif
}
else
return;
if (ret == 0)
while (1)
;
else
return;
}
关键部分在于
copy_bl2(2, MOVI_BL2_POS, MOVI_BL2_BLKCNT,
CFG_PHY_UBOOT_BASE, 0);
这里的调用
- 2:表示通道二
- MOVI_BL2_POS:代表uboot第二部分在SD卡中的开始的扇区号,必须和uboot烧录时的烧录位置相同。
- MOVI_BL2_BLKCNT:代表uboot占用的扇区数
- CFG_PHY_UBOOT_BASE:代表重定位时将uboot的第二部分复制到DDR中的起始位置
虚拟地址映射
#if defined(CONFIG_ENABLE_MMU)
enable_mmu:
/* enable domain access */
ldr r5, =0x0000ffff
mcr p15, 0, r5, c3, c0, 0 @load domain access register
/* Set the TTB register */
ldr r0, _mmu_table_base
ldr r1, =CFG_PHY_UBOOT_BASE
ldr r2, =0xfff00000
bic r0, r0, r2
orr r1, r0, r1
mcr p15, 0, r1, c2, c0, 0
/* Enable the MMU */
mmu_on:
mrc p15, 0, r0, c1, c0, 0
orr r0, r0, #1
mcr p15, 0, r0, c1, c0, 0
nop
nop
nop
nop
#endif
虚拟地址和物理地址
我们常接触到的是物理地址,物理地址就是设备设计生产时赋予的地址,例如裸机程序中的寄存器的地址,物理地址在设计和生产时确定好的,一旦确定好就不能修改了。
虚拟地址是在软件的操作和被操作硬件之间,增加一个虚拟地址映射层,负责将虚拟地址映射为物理地址,映射层有一张虚拟地址到物理地址的转换表,软件开发的时候使用的地址都是虚拟地址,不需要对硬件的物理地址有任何的了解,这些对虚拟地址的操作,都会通过映射层映射为物理地址,从而操纵硬件工作。
MMU(Memory Management Unit)内存管理单元,是Soc中的一个内部外设,主要功能就是实现虚拟地址到物理地址的映射,操作MMU主要是通过CP15协处理器进行,将虚拟地址映射成物理地址,主要是操作对CP15的寄存器进行编程控制。
虚拟地址映射可以实现访问控制,方法是我们在管理上对内存进行分块,每个块进行独立的虚拟地址映射,在每个块的映射关系上添加读,写,是否可以访问等权限,这样就建立起来了基于内存块的访问控制机制。
虚拟地址映射还可以实现cache功能,cache主要是在CPU和DDR内存之间实现一个缓冲区域,平衡CPU和DDR的读写速度差异,根据cache的读取差别,有的CPU给Cahce分成L1,L2等不同的等级。
使能域访问
/* enable domain access */
ldr r5, =0x0000ffff
mcr p15, 0, r5, c3, c0, 0 @load domain access register
mcr是用来写CP15协处理器的寄存器指令,c3寄存器就是CP15中处理域访问的寄存器,域访问用于管理MMU的访问控制权限。
TTB设置
/* Set the TTB register */
ldr r0, _mmu_table_base
ldr r1, =CFG_PHY_UBOOT_BASE
ldr r2, =0xfff00000
bic r0, r0, r2
orr r1, r0, r1
mcr p15, 0, r1, c2, c0, 0
TTB,转换表基地址,转换表就是虚拟地址映射表,转换表有表索引和表项,表索引对应虚拟地址,表项对应物理地址。转换表中的每个单元都由一对表索引和表项组成,对应一个内存块,若干个单元组成了一整个转换表,完成所有内存的转换。内存的映射和管理是以块为单位的,在ARM中支持三种块大小,分别为1KB,4KB,1MB。转换表位于内存空间中,转换表的起始地址要求对齐,一般是4字节或者8字节对齐。转换表使用时不需要软件干涉,只需要将基地址TTB赋值到CP15的c2寄存器中,MMU工作时自动会根据c2寄存器中的值找到转换表,自动查表完成地址转换。
开启MMU映射
/* Enable the MMU */
mmu_on:
mrc p15, 0, r0, c1, c0, 0
orr r0, r0, #1
mcr p15, 0, r0, c1, c0, 0
nop
nop
nop
nop
将c1寄存器的bit0值设置为1,表示使能MMU单元。MMU开启之后,软件执行时操作的地址都会经过TTB的转换,得到物理地址之后再去执行。
TTB转换表
TTB转换表中其实是由很多个数字组成的数组,这些都是表项,索引就是这些数组的下标,具体有多少个数组,是由所使用的映射块大小模式所决定,uboot中使用段式映射,大小为1MB,32bit的CPU内存范围为4GB,所以数组的大小为4096。理论上要依次设置这4096个数组的值,但在实际上是将这4096个数组分成几部分,每部分使用循环做相同的处理:
mmu_table:
.set __base,0
// Access for iRAM
.rept 0x100
FL_SECTION_ENTRY __base,3,0,0,0
.set __base,__base+1
.endr
// Not Allowed
.rept 0x200 - 0x100
.word 0x00000000
.endr
.set __base,0x200
// should be accessed
.rept 0x600 - 0x200
FL_SECTION_ENTRY __base,3,0,1,1
.set __base,__base+1
.endr
.rept 0x800 - 0x600
.word 0x00000000
.endr
.set __base,0x800
// should be accessed
.rept 0xb00 - 0x800
FL_SECTION_ENTRY __base,3,0,0,0
.set __base,__base+1
.endr
/* .rept 0xc00 - 0xb00
.word 0x00000000
.endr */
.set __base,0xB00
.rept 0xc00 - 0xb00
FL_SECTION_ENTRY __base,3,0,0,0
.set __base,__base+1
.endr
// 0xC000_0000映射到0x2000_0000
.set __base,0x300
//.set __base,0x200
// 256MB for SDRAM with cacheable
.rept 0xD00 - 0xC00
FL_SECTION_ENTRY __base,3,0,1,1
.set __base,__base+1
.endr
// access is not allowed.
@.rept 0xD00 - 0xC80
@.word 0x00000000
@.endr
.set __base,0xD00
// 1:1 mapping for debugging with non-cacheable
.rept 0x1000 - 0xD00
FL_SECTION_ENTRY __base,3,0,0,0
.set __base,__base+1
.endr
最终映射出来的效果如下:
虚拟地址 | 物理地址 | 映射大小 | 虚拟地址分布范围 |
---|---|---|---|
00000000~10000000 | 00000000~10000000 | 256MB | 0~256MB |
10000000~20000000 | 0 | 256MB | 256MB~512MB |
20000000~60000000 | 20000000~60000000 | 1GB | 512MB~1.5GB |
60000000~80000000 | 0 | 512MB | 1.5GB~2GB |
80000000~B0000000 | 80000000~B0000000 | 768MB | 2GB~2.75GB |
B0000000~C0000000 | B0000000~C0000000 | 256MB | 2.75GB~3GB |
C0000000~D0000000 | 30000000~40000000 | 256MB | 3GB~3.25GB |
D0000000~END | D0000000~END | 768MB | 3.25GB~4GB |
再来对比一下我们开发板上的DMC0和DMC1的有效地址范围:
DMC0:0x30000000~0x3FFFFFFF
DMC1:0x40000000~0x4FFFFFFF
可以看出,虚拟地址C0000000~D0000000映射到了0x30000000~0x3FFFFFFF,其他的虚拟地址和物理地址还是没动的。这也可以理解在uboot配置的时候为什么将链接地址设置为0xC3E00000,因为这个地址将会被映射到物理地址的0x33E00000上。
栈设置
/* Set up the stack */
stack_setup:
#if defined(CONFIG_MEMORY_UPPER_CODE)
ldr sp, =(CFG_UBOOT_BASE + CFG_UBOOT_SIZE - 0x1000)
#else
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 */
#if defined(CONFIG_USE_IRQ)
sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ)
#endif
sub sp, r0, #12 /* leave 3 words for abort-stack */
#endif
第一个宏满足,所以会执行ldr sp, =(CFG_UBOOT_BASE + CFG_UBOOT_SIZE - 0x1000)
,CFG_UBOOT_BASE的值为0x33E00000,CFG_UBOOT_SIZE的值为2MB,再减去0x1000,最终值为2MB-200K-0x1000(4KB)左右,之所以再次设置栈,是为了将栈放在一个比较合理的位置上,这个位置比较安全,不会被随意覆写,位置紧凑并且不浪费内存。
清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
ldr pc, _start_armboot
其中_bss_start和_bss_end的值来自于链接脚本。
开启uboot第二阶段
_start_armboot:
.word start_armboot
start_armboot函数用于开启uboot第二阶段的运行,清bss的时候将这个函数的地址赋值到pc寄存器,实际上就是使用一个长跳转让程序从这个函数开始执行,从DDR中运行这个函数,ldr pc, _start_armboot
加载的地址和当前的运行地址无关,使用的是链接地址,可以实现从SRAM跳转执行DDR中函数,也就实现了第二阶段的运行。