uboot中的start.S--uboot的第一阶段

分析uboot时,因为有汇编阶段的参与,因此不能直接找main.c,整个程序的入口取决于链接脚本ENTRY声明的地方。

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
/*OUTPUT_FORMAT("elf32-arm", "elf32-arm", "elf32-arm")*/
OUTPUT_ARCH(arm)
ENTRY(_start)

因此_start符号所在的文件(start.S)就是整个程序的起始文件,_start所在处的代码就是整个程序的起始代码。

头文件包含

#include <config.h>
#include <version.h>
#if defined(CONFIG_ENABLE_MMU)
#include <asm/proc/domain.h>
#endif
#include <regs.h>

config.h是在include目录下的,这个文件不是源码中本身存在的文件,而是配置过程中自动生成的文件(详见mkconfig脚本)。这个文件的内容其实是包含了一个头文件:#include<config/x210_sd.h>。亦即,start.Sz中包含的第一个头文件就是include/config/x210_sd.h,这个文件是整个uboot移植时的配置文件,里面包含很多宏。

include/version.h包含include/version_autogenerated.h,这个头文件就是配置过程中自动生成的,里面就一行内容:#define U_BOOT_VERSION "U_Boot 1.3.4"。这里面定义的宏U_BOOT_VERSION的值是一个字符串,字符串中的版本号信息来自于Makefile中的配置值。这个宏在程序中会被调用,在uboot启动过程中会串口打印出uboot的版本号。

在include/asm/proc/domain.h,asm目录不是uboot的原生目录,asm只是配置时创建的一个符号链接,实际指向的就是asm-arm,通过分析mkconfig脚本即可发现,实际文件为:include/asm-arm/proc-armv/domain.h。从这里可以看出之前配置时创建的符号链接的作用,如果没有这些符号链接则编译时根本通不过,因为找不到头文件。因此uboot不能在Windows的共享文件夹下编译,因为Windows没有符号链接。

那为什么start.S不直接包含asm-arm/proc-armv/domain.h,而选择用符号链接呢。这样的设计主要是为了可移植性。如果直接包含,则start.S文件就和CPU架构(硬件)有关了,若要移植到MIPS架构,则start.S的所有头文件包含都需要修改。如果用了符号链接后,start.S的源代码便无需改动,只需要在具体的硬件移植时更改配置就可以,使得创建的符号链接指向不同。

启动代码的16字节头部

#if defined(CONFIG_EVT1) && !defined(CONFIG_FUSED)
	.word 0x2000
	.word 0x0
	.word 0x0
	.word 0x0

在做裸机开发时,在SD卡启动/Nand启动等整个镜像开头需要16字节的校验头。uboot这里再开头放了16字节的填充位,这个占位的16字节只是保证正式的image的头部确实有16字节,但是这16字节的内容是不对的,需要后面去计算校验和然后重新填充。

异常向量表的构建

.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

_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 */
.global _end_vect
_end_vect:

	.balignl 16,0xdeadbeef

异常向量表是由硬件决定的,软件只是参照硬件的设计来实现它。异常向量表的每种异常都应该被处理,否则遇到未处理异常就会跑飞,因此reset符号处才是真正的有意义的代码开始的地方。

.balignl 16,0xdeadbeef 这一句指令是让当前地址对齐排布,如果当前地址不对齐则自动向后走地址直到对齐,并且向后走的那些内存要用0xdeadbeef 来填充。对齐访问有时候是硬件的要求,有时候是效率的要求。

_TEXT_BASE:
	.word	TEXT_BASE

这个TEXT_BASE就是Makefile中配置阶段的TEXT_BASE,其实就是 链接时指定的uboot的链接地址。源代码中和配置Makefile中很多变量是可以相互运送的,简单来说,有些符号的值可以从Makefile中传递到源代码中。

设置CPU

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设置为禁止FIQ,IRQ,ARM状态,SVC模式。其实ARM CPU在复位时默认就会进入SVC模式,这里使用软件将其设置为SVC模式,在整个uboot工作时CPU一直处于SVC模式。

	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和L1cache,同时关闭MMU,与CPU有关。

识别并暂存启动介质选择

        /* Read booting information */
        ldr	r0, =PRO_ID_BASE
        ldr	r1, [r0,#OMR_OFFSET]
        bic	r2, r1, #0xffffffc1

这段代码的意思是:在r2寄存器中存储了一个数字,这个数字等于某个特定值时表示sd卡启动,等于另一个特定值表示从Nand启动。事实上,在开发板内部有一个寄存器,这个寄存器中的值是硬件根据OM引脚的设置而自动设置值的。这个值反映的就是OM引脚的接法(电平高低),也就是真正的启动介质。

第一次设置栈并调用lowlevel_init

/*
	 * 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
	
	bl	lowlevel_init	/* go setup pll,mux,memory */

这是uboot第一次设置栈,是在SRAM中设置的,因为当前整个代码还在SRAM中运行,此时DDR还未被初始化还不能用。栈地址0xd0036000 是自己指定的,指定的原则就是这块空间只给栈用,不会被别人占用。

在调用函数前初始化栈,主要原因是在被调函数内还要再次调用函数,而BL函数只会将返回地址存储到LR中,但是CPU只有一个LR,所以在第二层调用函数前要先将LR入栈,否则函数返回时第一层的返回地址就丢了。

lowlevel_init.S

  • 检查复位状态
	/* 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

复杂CPU允许多种复位情况,譬如直接冷上电、热启动、睡眠(低功耗)状态下的唤醒等,这些都属于复位。所以需要在复位代码中检测复位状态,来判断到底是那种情况。判断复位类型的意义是:冷上电时DDR是需要初始化才能使用的,而热启动或低功耗状态下的复位则不需要再次初始化DDR。

接下来是 I/O状态恢复,关看门狗,供电锁存。

  • 判断代码执行位置
/* 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中。做这个判定的原因有下:

  1. BL1(uboot的前一部分)在SRAM中有一份,在DDR中也有一份。因此如果是冷启动,那么当前代码应该是在SRAM中运行的BL1,如果是低功耗状态的复位这时候应该是在DDR中运行的。
  2. 判断当前运行代码的运行位置可以对后续程序流程做出指导。如果当前代码是在SRAM中,说明系统是冷启动,那么时钟和DDR都需要初始化;如果当前代码是在DDR中,那么说明是热启动,则时钟和DDR都不闭再次初始化。

时钟和DDR初始化略过不表,接下来看串口的初始化。

uart_asm_init:

	/* set GPIO(GPA) to enable UART */
	@ GPIO setting for UART
	ldr	r0, =ELFIN_GPIO_BASE
	ldr	r1, =0x22222222
	str   	r1, [r0, #GPA0CON_OFFSET]

	ldr     r1, =0x2222
	str     r1, [r0, #GPA1CON_OFFSET]

	// HP V210 use. SMDK not use.
#if defined(CONFIG_VOGUES)
	ldr    r1, =0x100
	str    r1, [r0, #GPC0CON_OFFSET]

	ldr    r1, =0x4
	str    r1, [r0, #GPC0DAT_OFFSET]
#endif

	ldr	r0, =ELFIN_UART_CONSOLE_BASE		@0xEC000000
	mov	r1, #0x0
	str	r1, [r0, #UFCON_OFFSET]
	str	r1, [r0, #UMCON_OFFSET]

	mov	r1, #0x3
	str	r1, [r0, #ULCON_OFFSET]

	ldr	r1, =0x3c5
	str	r1, [r0, #UCON_OFFSET]

	ldr	r1, =UART_UBRDIV_VAL
	str	r1, [r0, #UBRDIV_OFFSET]

	ldr	r1, =UART_UDIVSLOT_VAL
	str	r1, [r0, #UDIVSLOT_OFFSET]

	ldr	r1, =0x4f4f4f4f
	str	r1, [r0, #UTXH_OFFSET]		@'O'

	mov	pc, lr

 初始化串口函数在执行完毕时会通过串口发送一个 'O' 字符。再之后,是lowlevel_init函数的返回:

	/* Print 'K' */
	ldr	r0, =ELFIN_UART_CONSOLE_BASE
	ldr	r1, =0x4b4b4b4b
	str	r1, [r0, #UTXH_OFFSET]

	pop	{pc}

 返回之时打印一个 'K' 字符。也就是说,lowlevel_init正确执行之后,就会通过串口打印出 "OK" 字样,这应该是uboot中看到的最早的调试信息。

总结一下,lowlevel_init.S总共做了以下事情:检查复位状态、I/O恢复、关看门狗、开发板供电锁存、时钟和DDR初始化(依复位状态而定)、串口初始化并打印 'O' 、tzpc初始化、返回之前打印 'K' 。

 第二次设置栈

	/* 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 */

在lowlevel_init.S返回之后,需要再次设置开发板供电锁存;这是因为:做代码移植有一个谨慎保守的策略:尽量添加代码而不是删除代码。

上面的代码是再次设置栈。原因如下:

之前在调用lowlevel_init程序前设置过一次栈,那时候因为DDR尚未初始化,因此程序执行都是在SRAM中,所以在SRAM中分配了一部分内存作为栈。本次因为DDR已经被初始化,因此要把栈挪移到DDR中,使得栈的容量得以扩大,方便下面程序的执行。可以体会到,uboot的启动阶段主要技巧就是小范围内有限条件下的辗转腾挪。

再次判断程序执行位置

	/* 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   */

再次用相同的代码判断代码运行位置是在SRAM中还是DDR中,本次判断的目的不同,这次判断是为了决定是否进行uboot的relocate。

若是冷启动,当前情况则是uboot的前一部分(16kb或者8kb)开机自动从SD卡加载到SRAM中运行,uboot的第二部分(即全部的uboot)还在SD卡的某一位置。此时uboot的第一阶段将要结束了,结束之前要把第二部分加载到DDR中链接地址处,这个加载过程就叫重定位。

虚拟地址与物理地址

物理地址就是物理设备设计生产时赋予的地址,像裸机中使用的寄存器的地址就是CPU设计指定的;物理地址是硬件编码,是设计时就确定好了。一个事实就是,寄存器的物理地址是无法通过编程修改的,是多少就是多少,只能通过查询数据手册获得并操作。坏处就是不够灵活,一个解决方案就是使用虚拟地址。

虚拟地址的产生依赖于软件操作与硬件操作之间增加一个层次,叫做虚拟地址映射层,有了虚拟地址映射后,软件操作只需要给虚拟地址,硬件操作还是用原来的物理地址,映射层建立一个虚拟地址到物理地址的映射表。这个映射不能通过软件来实现,因为软件只知道虚拟地址,现实中是通过MMU这个硬件来实现地址映射的。MMU在CP15协处理器中进行控制,若要操控MMU进行虚拟地址映射,方法就是对CP15协处理器的寄存器进行编程。使能MMU单元在于CP15协处理器的c1寄存器的bit0控制MMU的开关。只要将这一个bit置1即可开启MMU。开启MMU后,上层软件层的地址必须经过TT的转换后才能下发给下层物理层去执行。

地址映射的额外收益就是访问控制。简单来说,设置访问控制后,当前程序只能操作自己有权操作的地址范围,如果当前程序指针出错访问了不该访问的内存块就会出发段错误。

使能域访问

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

CP15协处理器内部有c0到c15共16个寄存器,这些寄存器每个都有其作用。我们通过mrc和mcr指令来访问这些寄存器。所谓的操作协处理器其实就是操作里面的寄存器。c3寄存器在MMU里的作用就是控制域访问,域访问是和MMU的访问控制有关的。

设置TTB

/* 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

TTB就是translation table base,转换表基地址。转换表是建立一套虚拟地址映射的关键。转换表分两部分,表索引和表项。表索引对应虚拟地址,表项对应物理地址。一对表索引和表项构成一个转换表单元,能够对一个内存块进行虚拟地址转换(内存映射和管理是以块为单位的)。真正的转换表就是由若干个转换表单元构成的,每个单元负责一个块,总体的转换表负责整个内存空间的映射。

转换表是放置在内存中的,放置时要求起始地址在内存中要xx位对齐。转换表不需要软件去干涉,只需要将基地址TTB设置到CP15的c2寄存器中,然后MMU工作时会自动去查转换表。

宏观上理解转换表:整个转换表可以看作是一个int类型的数组,数组中的元素就是一个表索引和表项的单元。数组中的元素值就是表项,这个元素的数组下标就是表索引。ARM的段式映射中长度为1MB,因此一个映射单元只能管1MB内存,那整个4G范围内需要4G/1MB = 4096个映射单元,也就是说,这个数组的元素个数是4096。

第三次设置栈

skip_hw_init:
	/* 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

这次设置栈还是在DDR中,之前虽然已经在DDR中设置过一次了,但是本次设置栈的目的是将栈放在比较合适(安全又不浪费内存)的地方。 

之后的清理BSS段不必多说,最后看start.S的收尾阶段:

	ldr	pc, _start_armboot

_start_armboot:
	.word start_armboot

start_armboot是uboot/lib_arm/board.c中,这是一个C语言实现的函数。这个函数就是uboot的第二阶段。上面代码的作用就是将uboot第二阶段执行函数的地址传给PC,实际上是使用一个远跳转直接跳转到DDR中的第二阶段开始地址处。

远跳转的含义就是这句话加载的地址和当前运行的地址无关,而和链接地址有关。因此这个远跳转可以实现从SRAM中的第一阶段跳转到DDR中的第二阶段。

总结:uboot第一阶段做的工作

  1. 构建异常像量表
  2. 设置CPU为SVC模式
  3. 关看门狗
  4. 开发板供电置锁
  5. 时钟初始化
  6. DDR初始化
  7. 串口初始化并打印“OK”
  8. 重定位
  9. 建立映射表并开启MMU
  10. 跳转到第二阶段

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值