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

本文详细解析UBOOT1.3.4的start.S文件,涵盖头文件包含、异常向量表构建、TEXT_BASE变量、CPU初始化、启动介质识别、栈设置、lowlevel_init函数等内容,揭示UBOOT启动第一阶段的关键步骤,包括时钟和DDR初始化、重定位及MMU启用等过程。

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

第一部分:头文件包含:

#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

包含了四个文件:

  1. config.h文件。config.h是在include目录下的,这个文件不是源码中本身存在的文件,而是配置过程中自动生成的文件。(详见mkconfig脚本)。这个文件的内容其实是包含了一个头文件:#include <configs/x210_sd.h>",所以实际包含的是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. 跳转到第二阶段
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值