UBOOT1.3.4 学习笔记(四) start.S文件详解--uboot第一阶段

整个项目的入口:start.S文件,由链接脚本的ENTRY声明确定

第一部分:头文件包含:

#include 
#include 
#if defined(CONFIG_ENABLE_MMU)
#include 
#endif
#include 

#ifndef CONFIG_ENABLE_MMU
#ifndef CFG_PHY_UBOOT_BASE
#define CFG_PHY_UBOOT_BASE	CFG_UBOOT_BASE
#endif
#endif

包含了四个文件:

  1. config.h文件。config.h是在include目录下的,这个文件不是源码中本身存在的文件,而是配置过程中自动生成的文件。(详见mkconfig脚本)。这个文件的内容其实是包含了一个头文件:#include ",所以实际包含的是include/configs/x210_sd.h文件,该文件内容是uboot配置文件,内含各种宏
  2. version.h文件,这个文件包含uboot的版本信息
  3. 第三个是asm/proc/domain.h文件,asm和proc是符号链接(第三部分讲过了),所以实际文件是:include/asm-arm/proc-armv/domain.h
  4. regs.h文件,也是个符号链接,指向include/s5pc110.h

启动代码的16字节头部:

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

先用四个字(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 */
  1. uboot只对异常进行了简单处理,因为uboot只是一段裸机代码
  2. 复位异常处的代码是:b reset,因此在CPU复位后真正去执行的有效代码是reset处的代码,因此reset符号处才是真正的有意义的代码开始的地方
.global _end_vect
_end_vect:

	.balignl 16,0xdeadbeef

上述代码是让当前地址对齐排布,空的内存用0xdeadbeef(只是个16进制数,没什么特殊含义)填充

TEXT_BASE等变量:

_TEXT_BASE:
	.word	TEXT_BASE
  1. 链接时指定的uboot的链接地址,值就是c3e00000
  2. 源代码中和配置Makefile中很多变量是可以互相传递的。即有些符号的值可以从Makefile中传递到源代码中
_TEXT_PHY_BASE:
	.word	CFG_PHY_UBOOT_BASE

表示uboot在DDR中的物理地址 CFG_PHY_UBOOT_BASE 值为33e00000

开始真正的reset代码:

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
  1. msr cpsr_c, #0xd3,表示设置cpsr寄存器,关快速中断和中断模式,设置mode为0x13(SVC模式)
  2. 整个uboot工作时都是SVC模式

接下来是CPU的一些初始化:

	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
  1. 禁止L2 cache
  2. L2 cache相关初始化
  3. 使能L2 cache
  4. 刷新L1 cache 的icache 和dcache
  5. 关闭MMU
  6. 上述五步都和cpu的cache 和mmu有关,了解即可

识别并暂存启动介质的选择:

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

上述代码识别并暂存启动介质的选择,r2寄存器中存储了一个数字,这个数字等于某个特定值时就表示SD启动,等于另一个特定值时表示从Nand启动,我们采用sd启动

	/* SD/MMC BOOT */
	cmp     r2, #0xc
	moveq   r3, #BOOT_MMCSD	

SD启动时执行上述代码,r3中赋值#BOOT_MMCSD(0x03),以备后用

设置栈:

	/*
	 * 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
  1. 第一次设置栈,这是在SRAM中设置的,因为当前代码还在SRAM中运行,DDR还没初始化。栈地址自己指定
  2. 在调用函数前初始化栈,主要原因是在被调用的函数内还有再次调用函数,而BL只会将返回地址存储到LR中,但是我们只有一个LR,所以在第二层调用函数前要先将LR入栈,否则函数返回时第一层的返回地址就丢了

接下来跳转到lowlevel_init函数:

bl	lowlevel_init	/* go setup pll,mux,memory */

该函数前105行做了下面操作:

  1. 检查复位状态,复杂CPU允许多种复位情况。譬如直接冷上电、热启动、睡眠(低功耗)状态下的唤醒等,这些情况都属于复位。所以我们在复位代码中要去检测复位状态,来判断到底是哪种情况。
    判断哪种复位的意义在于:冷上电时DDR是需要初始化才能用的;而热启动或者低功耗状态下的复位则不需要再次初始化DDR(只要确定自己的启动状态,没有这段也行)

  2. IO状态复位(与主线启动代码关系不大)

  3. 关WDT

  4. SRAM,SROM相关的GPIO设置(与主线启动代码关系不大)

  5. 供电锁存

接下来:判断当前代码位置

	/* 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   */
	
	/* init system clock */
	bl system_clock_init
	/* Memory initialize */
	bl mem_ctrl_asm_init
1:
	/* for UART */
	bl uart_asm_init

	bl tzpc_init

上述代码作用是判定当前代码的执行位置是在SRAM还是DDR:

  1. 如果是冷启动,那么当前代码在SRAM中,则下面需要执行bl system_clock_init和bl mem_ctrl_asm_init来初始化时钟和DDR
  2. 如果是低功耗复位启动,那么BL2(全部的uboot,包括BL1)已经被复制到DDR中了,此时代码在DDR中运行,则不需要再初始化时钟和DDR,直接跳转到1:处执行下面的代码
  3. 这一段代码是通过读取当前运行地址和链接地址,然后处理两个地址后对比是否相等,来判定当前运行是在SRAM中(不相等)还是DDR中(相等)。从而决定是否跳过下面的时钟和DDR初始化
  4. r1中存放某些位被清零的运行地址,r2存放某些位被清零的连接地址。通过比较固定位来确定两个地址是否一样
  5. ldr r0, =0xff000fff
    bic r1, pc, r0 相当于 r1 = pc & ~(ff000fff)

如果是冷启动,则需要进行初始化时钟和DDR:

bl system_clock_init

该函数在lowlevel_init.S文件的205—385行:
初始化过程和裸机差不多,但使用汇编编写(另做分析)

bl mem_ctrl_asm_init

该函数在uboot/cpu/s5pc11x/s5pc110/cpu_init.S文件中,初始化了DCM0和DCM1

接下来跳转到1:处进行串口初始化和tzpc初始化:

1:
	/* for UART */
	bl uart_asm_init

	bl tzpc_init    #作用不明

接下来返回:

pop	{pc}

执行完lowlevel_init.S后成功返回前会通过串口打印“ok”字样

小结:lowlevel_init.S中总共做了以下事情,重要的加粗:

  1. IO状态复位
  2. 关WDT
  3. SRAM,SROM相关的GPIO设置
  4. 供电锁存
  5. 初始化时钟
  6. 初始化DDR
  7. 初始化串口并打印“ok”
  8. tzpc初始化

接下来返回start.S文件继续:

再次设置栈:

/* 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 */
  1. 这次设置栈是在DDR中设置的(已经初始化了)
  2. SRAM中空间很小,栈容易溢出

再次判断当前地址位置以决定是否重定位:

/* 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   */
  1. 第一次是为了确定是否需要初始化时钟和DDR,这次是为了确定是否进行uboot的relocate(是否可以根据第一次的位置决定第二次是否需要重定位,用个flag)
  2. 冷启动时当前情况是uboot的前一部分(16kb或者8kb)开机自动从SD卡加载到SRAM中正在运行,uboot的第二部分(其实第二部分是整个uboot)还躺在SD卡的某个扇区开头的N个扇区中。此时uboot的第一阶段已经即将结束了(第一阶段该做的事基本做完了),结束之前要把第二部分加载到DDR中链接地址处(33e00000),这个加载过程就叫重定位。

重定位详解:

#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

D0037488这个内存地址在SRAM中,这个地址中的值是被硬件自动设置的。硬件根据我们实际电路中SD卡在哪个通道中,会将这个地址中的值设置为相应的数字。譬如我们从SD0通道启动时,这个值为EB000000;从SD2通道启动时,这个值为EB200000

跳转到真正的重定位函数movi_bl2_copy(c函数):

mmcsd_boot:
bl      movi_bl2_copy
b       after_copy

movi_bl2_copy函数中最重要的是下面这个函数(全部太长不贴了):

copy_bl2(2, MOVI_BL2_POS, MOVI_BL2_BLKCNT,
			CFG_PHY_UBOOT_BASE, 0);
  1. 2表示通道2;
  2. MOVI_BL2_POS是uboot的第二部分在SD卡中的开始扇区,这个扇区数字必须和烧录uboot时烧录的位置相同;
  3. MOVI_BL2_BLKCNT是uboot的长度占用的扇区数;
  4. CFG_PHY_UBOOT_BASE是重定位时将uboot的第二部分复制到DDR中的起始地址(33E00000)

接下来到after_copy:处:

after_copy:

#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

做了三件事:

  1. 使能域访问(cp15的c3寄存器)
    (1)cp15协处理器内部有c0到c15共16个寄存器,这些寄存器每一个都有自己的作用。我们通过mrc和mcr指令来访问这些寄存器。所谓的操作cp协处理器其实就是操作cp15的这些寄存器。
    (2)c3寄存器在mmu中的作用是控制域访问。域访问是和MMU的访问控制有关的。

  2. 设置TTB(cp15的c2寄存器)
    (1)TTB就是translation table base,转换表基地址。
    (2)转换表是建立一套虚拟地址映射的关键。转换表分2部分,表索引和表项。表索引对应虚拟地址,表项对应物理地址。一对表索引和表项构成一个转换表单元,能够对一个内存块进行虚拟地址转换。(映射中基本规定中规定了内存映射和管理是以块为单位的,至于块有多大,要看你的MMU的支持和你自己的选择。在ARM中支持3种块大小,细表1KB、粗表4KB、段1MB)。真正的转换表就是由若干个转换表单元构成的,每个单元负责1个内存块,总体的转换表负责整个内存空间(0-4G)的映射。
    (3)整个建立虚拟地址映射的主要工作就是建立这张转换表
    (4)转换表放置在内存中的,放置时要求起始地址在内存中要xx位对齐。转换表不需要软件去干涉使用,而是将基地址TTB设置到cp15的c2寄存器中,然后MMU工作时会自动去查转换表。

  3. 使能MMU单元(cp15的c1寄存器)
    (1)MMU是内存管理单元(Soc中的硬件),负责实现虚拟地址到物理地址的映射
    (2)cp15的c1寄存器的bit0控制MMU的开关。只要将这一个bit置1即可开启MMU。开启MMU之后上层软件层的地址就必须经过TT的转换才能发给下层物理层去执行。

转换表mmu_table在lowlevel_init.S文件593行,整个转换表可以看作是一个int类型的数组,数组中的一个元素就是一个表索引和表项的单元。数组中的元素值就是表项,这个元素的数组下标就是表索引。
ARM的段式映射中长度为1MB,因此一个映射单元只能管1MB内存,那我们整个4G范围内需要4G/1MB=4096个映射单元,也就是说这个数组的元素个数是4096.实际上我们做的时候并没有依次单个处理这4096个单元,而是把4096个分成几部分,然后每部分用for循环做相同的处理。
如下:

.set __base,0
	// Access for iRAM
	.rept 0x100
	FL_SECTION_ENTRY __base,3,0,0,0
	.set __base,__base+1
	.endr

FL_SECTION_ENTRY宏定义(并不懂):

.macro FL_SECTION_ENTRY base,ap,d,c,b
	.word (\base << 20) | (\ap << 10) | \
	      (\d << 5) | (1<<4) | (\c << 3) | (\b << 2) | (1<<1)

接下来回到start.S继续:

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

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   /*第一阶段和第二阶段的分界,长跳转到第二阶段*/
	_start_armboot:
	.word start_armboot
  1. 再次设置栈:
    (1)还是在DDR中,本次设置栈的目的是将栈放在比较合适(安全,紧凑而不浪费内存)的地方。
    (2)我们实际将栈设置在uboot起始地址上方2MB处,这样安全的栈空间是:2MB-uboot大小-0x1000=1.8MB左右。这个空间既没有太浪费内存,又足够安全。

  2. 清理bss段:bss段的开头和结尾地址的符号是从链接脚本u-boot.lds得来的

  3. ldr pc, _start_armboot
    (1)start_armboot是uboot/lib_arm/board.c中,这是一个C语言实现的函数。这个函数就是uboot的第二阶段。这句代码的作用就是将uboot第二阶段执行的函数的地址传给pc,实际上就是使用一个远跳转直接跳转到DDR中的第二阶段开始地址处。
    (2)远跳转的含义就是这句话加载的地址和当前运行地址无关,而和链接地址有关。因此这个远跳转可以实现从SRAM中的第一阶段跳转到DDR中的第二阶段。
    (3)这里这个远跳转就是uboot第一阶段和第二阶段的分界线

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

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

你可能感兴趣的:(YT的学习笔记)