嵌入式之uboot源码分析-启动第一阶段学习笔记

注: 以下的内容来自朱老师物联网大讲堂uboot部分课件

Uboot启动第一阶段start.S执行步骤

1.头文件包含 (x210的各种宏定义) (u-boot的版本信息) (用于存储用户、管理员各自数据的,类似于商场分为顾客、商贩)
x210寄存器的相关宏定义
2.16K的校验头,SD卡启动/Nand启动等整个镜像开头需要16字节的校验头。
3.代码开始,先reset进入SVC管理模式
4.构建异常向量表
5.读取链接地址和物理地址
6.Cpu初始化(关、设置、打开l2cache)
7.刷新L1 cache的icache和dcache,关MMU
8.识别启动方式(x210有多种启动方式)
9.第一次设置栈(因为要进行函数调用,需要保存返回地址在LR)
10.调用函数lowlevel_init(设置时钟和DDR,并使用串口打印debug字符“OK”)
11.开发板供电锁存
12.第二次设置栈(因为现在DDR已经可以使用了,我们就将栈迁移至DDR)
13.判断是否需要重定位
14.重定位,(热启动,即:睡眠唤醒等,已经重定位过,就不需要重定位)
15.虚拟地址映射(目的提高效率)
16.第三次设置栈(栈放在比较合适(安全,紧凑而不浪费内存)的地方)
17.清理bss
18.调用start_armboot,进入uboot的第二阶段

具体步骤及对应代码分析

1. start.S引入

根据之前的学习经验我们知道,程序的运行肯定都要有一个入口,uboot的程序运行也是一样的,我们在裸机的部分知道了程序执行开始必须执行一段汇编代码进行相关的设定后(关看门狗、设置SVC栈、开/关icache、重定位等),才能调用C语言程序,所以这里我们要先找到汇编代码的入口。

1.1 u-boot.lds中找到start.S入口

(1)在C语言中整个项目的入口就是main函数(这是C语言规定的),所以譬如说一个有10000个.c文件的项目,第一个要分析的文件就是包含了main函数的那个文件。
(2)在uboot中因为有汇编阶段参与,因此不能直接找main.c。整个程序的入口取决于链接脚本中ENTRY声明的地方。ENTRY(_start)因此_start符号所在的文件就是整个程序的起始文件,_start所在处的代码就是整个程序的起始代码。

1.2 利用SI软件中找到文件

(1)利用SI工具搜索到一共7个_start,然后分析搜索出来的7处,发现有2个是api_example,2个是onenand相关的,都不是我们要找的。剩下3个都在uboot/cpu/s5pc11x/start.S文件中。
(2)然后进入start.S文件中,发现57行中就是_start标号的定义处,于是乎我们就找到了整个uboot的入口代码,就是第57行。

1.3 SI中找文件技巧

(1)以上,找到了start.S文件,下面我们就从start.S文件开始分析uboot第一阶段。
(2)在SI中,如果我们知道我们要找的文件的名字,但是我们又不知道他在哪个目录下,我们要怎样找到并打开这个文件?方法是在SI中先打开右边的工程项目管理栏目,然后点击最左边那个(这个是以文件为单位来浏览的),然后在上面输入栏中输入要找的文件的名字。我们在输入的时候,SI在不断帮我们进行匹配,即使你不记得文件的全名只是大概记得名字,也能帮助你找到你要找的文件(更多的使用需要自己去实践)。

至此我们就找到了start.S文件也就是程序入口,下面将进行解析

1.4 start.S解析

1.4.1 _start之前的头文件包含(28~33)

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

(1)#include 。config.h是在include目录下的,这个文件不是源码中本身存在的文件,而是配置过程中自动生成的文件。(详见mkconfig脚本)。这个文件的内容其实是包含了一个头文件:#include ".

(2)经过分析后,发现start.S中包含的第一个头文件就是:include/configs/x210_sd.h,这个文件是整个uboot移植时的配置文件。这里面是好多宏。因此这个头文件包含将include/configs/x210_sd.h文件和start.S文件关联了起来。因此之后在分析start.S文件时,主要要考虑的就是x210_sd.h文件。

#include

#include 。include/version.h中包含了include/version_autogenerated.h,这个头文件就是配置过程中自动生成的。里面就一行内容:

#define U_BOOT_VERSION "U-Boot 1.3.4"

这里面定义的宏U_BOOT_VERSION的值是一个字符串,字符串中的版本号信息来自于Makefile中的配置值。这个宏在程序中会被调用,在uboot启动过程中会串口打印出uboot的版本号,那个版本号信息就是从这来的。

#include

(1)#include 。asm目录不是uboot中的原生目录,uboot中本来是没有这个目录的。asm目录是配置时创建的一个符号链接,实际指向的是就是asm-arm(详解上一章节分析mkconfig脚本时).
(2)经过分析后发现,实际文件是:include/asm-arm/proc-armv/domain.h
(3)从这里可以看出之前配置时创建的符号链接的作用,如果没有这些符号链接则编译时根本通不过,因为找不到头文件。(所以uboot不能在windows的共享文件夹下配置编译,因为windows中没有符号链接)

思考:为什么start.S不直接包含asm-arm/proc-armv/domain.h,而要用asm/proc/domain.h。
:这样的设计主要是为了可移植性。因为如果直接包含,则start
.S文件和CPU架构(和硬件)有关了,可移植性就差了。譬如我要把uboot移植到mips架构下,则start.S源代码中所有的头文件包含全部要修改。我们用了符号链接之后,则start.S中源代码不用改,只需要在具体的硬件移植时配置不同,创建的符号链接指向的不同,则可以具有可移植性。

#include

regs.h 里面是一些寄存器的宏定义

1.4.2 启动代码的16字节头部(49~54)

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

.word 0x2000就是在当前位置放一个word型的值,这个值就是0x2000,占4个字节。

(1)裸机中讲过,在SD卡启动/Nand启动等整个镜像开头需要16字节的校验头。(mkv210image.c中就是为了计算这个校验头)。我们以前做裸机程序时根本没考虑这16字节校验头,
因为:

  1. 如果我们是usb启动直接下载的方式启动的则不需要16字节校验头(irom application note);
  2. 如果是SD卡启动mkv210image.c中会给原镜像前加16字节的校验头。

(2)uboot这里start.S中在开头位置放了16字节的填充占位,这个占位的16字节只是保证正式的image的头部确实有16字节,但是这16字节的内容是不对的,还是需要后面去计算校验和然后重新填充的。

1.4.3 异常向量表的构建(57~85)

_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

(1)异常向量表是硬件决定的,软件只是参照硬件的设计来实现它。
(2)异常向量表中每种异常都应该被处理,否则真遇到了这种异常就跑飞了。但是我们在uboot中并未非常细致的处理各种异常。
(3)复位异常处的代码是:b reset,因此在CPU复位后真正去执行的有效代码是reset处的代码,因此reset符号处才是真正的有意义的代码开始的地方。

有点意思的deadbeef

(1).balignl 16,0xdeadbeef. 这一句指令是让当前地址对齐排布,如果当前地址不对齐则自动向后走地址直到对齐,并且向后走的那些内存要用0xdeadbeef来填充。
(2)0xdeadbeef这是一个十六进制的数字,这个数字很有意思,组成这个数字的十六进制数全是abcdef之中的字母,而且这8个字母刚好组成了英文的dead beef这两个单词,字面意思是坏牛肉。
(3)为什么要对齐访问?有时候是效率的要求,有时候是硬件的特殊要求。

1.4.4 TEXT_BASE等(99~100)

_TEXT_BASE:
	.word	TEXT_BASE

(1)第100行这个TEXT_BASE就是上个课程中分析Makefile时讲到的那个配置阶段的TEXT_BASE,其实就是我们链接时指定的uboot的链接地址。(值就是c3e00000)
(2)源代码中和配置Makefile中很多变量是可以互相运送的。简单来说有些符号的值可以从Makefile中传递到源代码中。

1.4.5 CFG_PHY_UBOOT_BASE (107~108)

_TEXT_PHY_BASE:
	.word	CFG_PHY_UBOOT_BASE

嵌入式之uboot源码分析-启动第一阶段学习笔记_第1张图片

CFG_PHY_UBOOT_BASE 33e00000 uboot在DDR中的物理地址

1.4.6 设置CPU为SVC模式(141~149),实际执行在(57)

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

mrs: 用来读cpsr
msr: 用来写cpsr

CPSR程序状态寄存器
嵌入式之uboot源码分析-启动第一阶段学习笔记_第2张图片

嵌入式之uboot源码分析-启动第一阶段学习笔记_第3张图片
0xd3 = b11010011
(1)msr cpsr_c, #0xd3 将CPU设置为禁止FIQ IRQ,ARM状态,SVC模式。
(2)其实ARM CPU在复位时默认就会进入SVC模式,但是这里还是使用软件将其置为SVC模式。整个uboot工作时CPU一直处于SVC模式。

1.4.7 设置L2、L1cache和MMU(200~221)

	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

TLB是一个内存管理单元用于改进虚拟地址到物理地址转换速度的缓存.

(1)bl disable_l2cache // 禁止L2 cache
(2)bl set_l2cache_auxctrl_cycle // l2 cache相关初始化
(3)bl enable_l2cache // 使能l2 cache
(4)刷新L1 cache的icache和dcache。
(5)关闭MMU

总结:上面这5步都是和CPU的cache和mmu有关的,不用去细看,大概知道即可。

1.4.8 识别并暂存启动介质选择(224~227)


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

(1)从哪里启动是由SoC的OM5:OM0这6个引脚的高低电平决定的。
(2)实际上在210内部有一个寄存器(地址是0xE0000004),这个寄存器中的值是硬件根据OM引脚的设置而自动设置值的。这个值反映的就是OM引脚的接法(电平高低),也就是真正的启动介质是谁。
(3)我们代码中可以通过读取这个寄存器的值然后判断其值来确定当前选中的启动介质是Nand还是SD还是其他的。
(4)start.S的225-227行执行完后,在r2寄存器中存储了一个数字,这个数字等于某个特定值时就表示SD启动,等于另一个特定值时表示从Nand启动····
(5)260行中给r3中赋值#BOOT_MMCSD(0x03),这个在SD启动时实际会被执行,因此执行完这一段代码后r3中存储了0x03,以后备用。

1.4.9 设置栈(SRAM中的栈)(284-286)

	ldr	sp, =0xd0036000 /* end of sram dedicated to u-boot */
	sub	sp, sp, #12	/* set stack */
	mov	fp, #0

(1)284-286行第一次设置栈。这次设置栈是在SRAM中设置的,因为当前整个代码还在SRAM中运行,此时DDR还未被初始化还不能用。栈地址0xd0036000是自己指定的,指定的原则就是这块空间只给栈用,不会被别人占用。
(2)为什么要在调用函数前初始化栈
主要原因是在被调用的函数内还有再次调用函数,而BL只会将返回地址存储到LR中,但是我们只有一个LR,所以在第二层调用函数前要先将LR入栈,否则函数返回时第一层的返回地址就丢了。

1.4.10 调用lowlevel_init(288)

bl	lowlevel_init	/* go setup pll,mux,memory */
lowlevel_init解析:
1)先入栈
push	{lr}
2)检查reset状态(判断属于那种reset)

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

3)IO状态恢复
4)关看门狗
5)SRAM和SROM初始化(外接的)
6)供电锁存
7)判断当前代码执行位置,以确定是否需要初始化时钟和初始化DDR

为什么要做这个判定?
原因1:BL1(uboot的前一部分)在SRAM中有一份,在DDR中也有一份,因此如果是冷启动那么当前代码应该是在SRAM中运行的BL1,如果是低功耗状态的复位这时候应该就是在DDR中运行的。
原因2:我们判定当前运行代码的地址是有用的,可以指导后面代码的运行。譬如在lowlevel_init.S中判定当前代码的运行地址,就是为了确定要不要执行时钟初始化和初始化DDR的代码。如果当前代码是在SRAM中,说明冷启动,那么时钟和DDR都需要初始化;如果当前代码是在DDR中,那么说明是热启动则时钟和DDR都不用再次初始化。

lowlevel_init.S的110-115行。

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

Learning Tips : BIC{cond}{S} Rd,Rn,operand2 BIC指令将Rn 的值与操作数operand2
的反码按位逻辑”与”,结果存放到目的寄存器Rd 中。

(1)bic r1, pc, r0 这句代码的意义是:将pc的值中的某些bit位清0,剩下一些特殊的bit位赋值给r1(r0中为1的那些位清零)相等于:r1 = pc & ~(ff000fff)
(2)ldr r2, _TEXT_BASE 加载链接地址到r2,然后将r2的相应位清0剩下特定位。
(3)最后比较r1和r2.

总结: 这一段代码是通过读取当前运行地址和链接地址,然后处理两个地址后对比是否相等,来判定当前运行是在SRAM中(不相等)还是DDR中(相等)。从而决定是否跳过下面的时钟和DDR初始化。

8)初始化时钟(system_clock_init)

如果7)中的链接地址和执行地址不相等,执行system_clock_init

	/* init system clock */
	bl system_clock_init

回顾之前的裸机时钟初始化部分----->嵌入式S5PV210时钟系统学习笔记

(1)使用SI搜索功能,确定这个函数就在当前文件的205行,一直到第385行。这个初始化时钟的过程和裸机中初始化的过程一样的,只是更加完整而且是用汇编代码写的。
(2)在x210_sd.h中300行到428行,都是和时钟相关的配置值。这些宏定义就决定了210的时钟配置是多少。也就是说代码在lowlevel_init.S中都写好了,但是代码的设置值都被宏定义在x210_sd.h中了。因此,如果移植时需要更改CPU的时钟设置,根本不需要动代码,只需要在x210_sd.h中更改配置值即可。

9)初始化DDR(mem_ctrl_asm_init)
	/* Memory initialize */
	bl mem_ctrl_asm_init

(1)该函数用来初始化DDR
(2)函数位置在uboot/cpu/s5pc11x/s5pc110/cpu_init.S文件中。
(3)该函数和裸机中初始化DDR代码是一样的。实际上裸机中初始化DDR的代码就是从这里抄的。配置值也可以从这里抄,但是当时我自己根据理解+抄袭整出来的一份。
(4)配置值中其他配置值参考裸机中的解释即可明白,有一个和裸机中讲的不一样。DMC0_MEMCONFIG_0,在裸机中配置值为0x20E01323;在uboot中配置为0x30F01313.这个配置不同就导致结果不同。
在 裸机中DMC0的256MB内存地址范围是0x20000000-0x2FFFFFFF;
在uboot中DMC0的256MB内存地址范围为0x30000000-0x3FFFFFFF。
(5)之前在裸机中时配置为2开头的地址,当时并没有说可以配置为3开头。从分析九鼎移植的uboot可以看出:DMC0上允许的地址范围是20000000-3FFFFFFF(一共是512MB),而我们实际只接了256MB物理内存,SoC允许我们给这256MB挑选地址范围。
(6)总结一下:在uboot中,可用的物理地址范围为:0x30000000-0x4FFFFFFF。一共512MB,其中30000000-3FFFFFFF为DMC0,40000000-4FFFFFFF为DMC1。
(7)我们需要的内存配置值在x210_sd.h的438行到468行之间。分析的时候要注意条件编译的条件,配置头文件中考虑了不同时钟配置下的内存配置值,这个的主要目的是让不同时钟需求的客户都能找到合适自己的内存配置值。
(8)在uboot中DMC0和DMC1都工作了,所以在裸机中只要把uboot中的配置值和配置代码全部移植过去,应该是能够让DMC0和DMC1都工作的。

  • 这一部分自己不是特别理解,所以当下设置待办处理
10)初始化串口(uart_asm_init)

可参考裸机部分进行学习-----> 嵌入式ARM裸机串口通信详解笔记

/* for UART */
	bl uart_asm_init
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

(1)这个函数用来初始化串口
(2)初始化完了后通过串口发送了一个’O’

11) tzpc_init(Setting TZPC[TrustZone Protection Controller])

trust zone初始化,暂时先不用管

12) 检查reset状态,返回
/* Print 'K' */
	ldr	r0, =ELFIN_UART_CONSOLE_BASE
	ldr	r1, =0x4b4b4b4b
	str	r1, [r0, #UTXH_OFFSET]

	pop	{pc}

返回前通过串口打印’K’

分析;lowlevel_init.S执行完如果没错那么就会串口打印出"OK"字样。这应该是我们uboot中看到的最早的输出信息。

总结回顾:lowlevel_init.S中总共做了哪些事情

检查复位状态、IO恢复、关看门狗、开发板供电锁存、时钟初始化、DDR初始化、串口初始化并打印’O’、tzpc初始化、打印’K’。我们在上面的章节已经用目录表示出来了。

其中值得关注的:关看门狗、开发板供电锁存、时钟初始化、DDR初始化、打印"OK"

1.4.11 返回到start.S ,再次开发板供电锁存

 * set PS_HOLD signal to high
	 */
	ldr	r0, =0xE010E81C  /* PS_HOLD_CONTROL register */
	ldr	r1, =0x00005301	 /* PS_HOLD output high	*/
	str	r1, [r0]

再次开发板供电锁存。第一,做2次是不会错的;第二,做2次则第2次无意义;做代码移植时有一个古怪谨慎保守策略就是尽量添加代码而不要删除代码。

1.4.12 再次设置栈(DDR中的栈)(297-299)

/* 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)之前在调用lowlevel_init程序前设置过1次栈(start.S 284-287行),那时候因为DDR尚未初始化,因此程序执行都是在SRAM中,所以在SRAM中分配了一部分内存作为栈。本次因为DDR已经被初始化了,因此要把栈挪移到DDR中,所以要重新设置栈,这是第二次(start.S 297-299行);这里实际设置的栈的地址是33E00000,刚好在uboot的代码段的下面紧挨着。
(3)为什么要再次设置栈?DDR已经初始化了,已经有大片内存可以用了,没必要再把栈放在SRAM中可怜兮兮的了;原来SRAM中内存大小空间有限,栈放在那里要注意不能使用过多的栈否则栈会溢出,我们及时将栈迁移到DDR中也是为了尽可能避免栈使用时候的小心翼翼。

感慨:uboot的启动阶段主要技巧就在于小范围内有限条件下的辗转腾挪。

1.4.13 再次判断当前地址以决定是否重定位(305~310)

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

1.4.14 uboot重定位

  • 需要先复习之前裸机时的重定位部分,然后再来整理

(1)D0037488这个内存地址在SRAM中,这个地址中的值是被硬件自动设置的。硬件根据我们实际电路中SD卡在哪个通道中,会将这个地址中的值设置为相应的数字。譬如我们从SD0通道启动时,这个值为EB000000;从SD2通道启动时,这个值为EB200000
(2)我们在start.S的260行确定了从MMCSD启动,然后又在278行将#BOOT_MMCSD写入了INF_REG3寄存器中存储着。然后又在322行读出来,再和#BOOT_MMCSD去比较,确定是从MMCSD启动。最终跳转到mmcsd_boot函数中去执行重定位动作。
(3)真正的重定位是通过调用movi_bl2_copy函数完成的,在uboot/cpu/s5pc11x/movi.c中。是一个C语言的函数
(4)copy_bl2(2, MOVI_BL2_POS, MOVI_BL2_BLKCNT,
CFG_PHY_UBOOT_BASE, 0);
分析参数:2表示通道2;MOVI_BL2_POS是uboot的第二部分在SD卡中的开始扇区,这个扇区数字必须和烧录uboot时烧录的位置相同;MOVI_BL2_BLKCNT是uboot的长度占用的扇区数;CFG_PHY_UBOOT_BASE是重定位时将uboot的第二部分复制到DDR中的起始地址(33E00000).

1.4.15 虚拟地址映射(357~382)

1.4.15.1 虚拟映射的基本概念
1)什么是虚拟地址、物理地址

(1)物理地址就是物理设备设计生产时赋予的地址。像裸机中使用的寄存器的地址就是CPU设计时指定的,这个就是物理地址。物理地址是硬件编码的,是设计生产时确定好的,一旦确定了就不能改了。
(2)一个事实就是:寄存器的物理地址是无法通过编程修改的,是多少就是多少,只能通过查询数据手册获得并操作。坏处就是不够灵活。一个解决方案就是使用虚拟地址。
(3)虚拟地址意思就是在我们软件操作和硬件被操作之间增加一个层次,叫做虚拟地址映射层。有了虚拟地址映射后,软件操作只需要给虚拟地址,硬件操作还是用原来的物理地址,映射层建立一个虚拟地址到物理地址的映射表。当我们软件运行的时候,软件中使用的虚拟地址在映射表中查询得到对应的物理地址再发给硬件去执行(虚拟地址到物理地址的映射是不可能通过软件来实现的)。

为什么要进行虚拟地址映射------>ARM MMU中虚拟地址到物理地址转换的研究

2)MMU单元的作用

(1)MMU就是memory management unit,内存管理单元。MMU实际上是SOC中一个硬件单元,它的主要功能就是实现虚拟地址到物理地址的映射。
(2)MMU单片在CP15协处理器中进行控制,也就是说要操控MMU进行虚拟地址映射,方法就是对cp15协处理器的寄存器进行编程。

3)地址映射的额外收益

访问控制
(1)访问控制就是:在管理上对内存进行分块,然后每块进行独立的虚拟地址映射,然后在每一块的映射关系中同时还实现了访问控制(对该块可读、可写、只读、只写、不可访问等控制)
(2)回想在C语言中编程中经常会出现一个错误:Segmentation fault。实际上这个段错误就和MMU实现的访问控制有关。当前程序只能操作自己有权操作的地址范围(若干个内存块),如果当前程序指针出错访问了不该访问的内存块则就会触发段错误。

cache
(1)cache的工作和虚拟地址映射有关系。
(2)cache是快速缓存,意思就是比CPU慢但是比DDR块。CPU嫌DDR太慢了,于是乎把一些DDR中常用的内容事先读取缓存在cache中,然后CPU每次需要找东西时先在cache中找。如果cache中有就直接用cache中的;如果cache中没有才会去DDR中寻找。

4)CP15寄存器

概述: 在基于ARM的嵌入式应用系统中,存储系统的操作通常是由协处理器CP15完成的。CP15包含16个32位的寄存器,其编号为0~15。

CP15 的寄存器 C3
作用:CP15 中的寄存器 C3 定义了 ARM 处理器的 16 个域的访问权限。
在 CP15的C3寄存器中,划分了 16个域,每个区域由两位构成,这两位说明了当前内存的检查权限:

  • 00:当前级别下,该内存区域不允许被访问,任何的访问都会引起一个domain fault,这时 AP位无效
  • 01:当前级别下,该内存区域的访问必须配合该内存区域的段描述符中AP位进行权检查
  • 10:保留状态(我们最好不要填写该值,以免引起不能确定的问题)
  • 11:当前级别下,对该内存区域的访问都不进行权限检查。 这时 AP位无效

所以只有当相应域的编码为 01 时,才会根据 AP位 和协处理器CP15中的C1寄存器的R,S位进行权限检查

而访问CP15寄存器的指令主要是MCR和MRC这两个指令。

Learning Tips

MRC:协处理器寄存器到ARM处理器寄存器的数据传送指令(读出协处理器寄存器)。
MCR:ARM处理器寄存器到协处理器寄存器的数据传送指令(写入协处理器寄存器)。

MRC/MCR指令读取CP15寄存器格式:

MRC{cond} p15,,,,,

MCR{cond} p15,,,,,

cond:为指令执行的条件码。当cond忽略时指令为无条件执行。 Opcode_1:协处理器的特定操作码.
对于CP15寄存器来说,opcode1=0
Rd:作为源寄存器的ARM寄存器,其值将被传送到协处理器寄存器中,或者将协处理器寄存器的值传送到该寄存器里面 ,通常为R0
CRn:作为目标寄存器的协处理器寄存器,其编号是C~C15。
CRm:协处理器中附加的目标寄存器或源操作数寄存器。如果不需要设置附加信息,将CRm设置为c0,否则结果未知
Opcode_2:可选的协处理器特定操作码。(用来区分同一个编号的不同物理寄存器,当不需要提供附加信息时,指定为0)

更多CP15的相关内容参考------->协处理器CP15介绍—MCR/MRC指令(6)

更多内存映射的内容需要参考----->【ARM】虚拟地址映射理解

1.4.15.2 start.S中的代码实现
1)使能域访问(cp15的c3寄存器)
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

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

2)设置TTB(cp15的c2寄存器)
	/* Set the TTB register */
	ldr	r0, _mmu_table_base       
	ldr	r1, =CFG_PHY_UBOOT_BASE   //0x33E00000
	ldr	r2, =0xfff00000
	bic	r0, r0, r2
	orr	r1, r0, r1
	mcr	p15, 0, r1, c2, c0, 0

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

3)使能MMU单元(cp15的c1寄存器)

cp15的c1寄存器的bit0控制MMU的开关。只要将这一个bit置1即可开启MMU。开启MMU之后上层软件层的地址就必须经过TT的转换才能发给下层物理层去执行。
嵌入式之uboot源码分析-启动第一阶段学习笔记_第4张图片

	/* 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
4)映射表分析

(1)通过符号查找,确定转换表在lowlevel_init.S文件的593行,我们重点关注的是(634~641),虚拟地址映射只是把虚拟地址的c0000000开头的256MB映射到了DMC0的30000000开头的256MB物理内存上去了。其他的虚拟地址空间根本没动,还是原样映射的
嵌入式之uboot源码分析-启动第一阶段学习笔记_第5张图片

	// 0xC000_0000鏄犲皠鍒?x2000_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

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

1.4.16 再次设置栈(388)

//0xc3e00000+2*1024*1024-0x1000   
ldr	sp, =(CFG_UBOOT_BASE + CFG_UBOOT_SIZE - 0x1000) 

(1)第三次设置栈。这次设置栈还是在DDR中,之前虽然已经在DDR中设置过一次栈了,但是本次设置栈的目的是将栈放在比较合适(安全,紧凑而不浪费内存)的地方。
(2)我们实际将栈设置在uboot起始地址上方2MB处,这样安全的栈空间是:2MB-uboot大小-0x1000=1.8MB左右。这个空间既没有太浪费内存,又足够安全。

1.4.17 清理bss(400~409)

清理bss段代码和裸机中讲的一样。注意表示bss段的开头和结尾地址的符号是从链接脚本u-boot.lds得来的。

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

1.4.18 进入uboot第二阶段(414)

_start_armboot:
	.word start_armboot

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

2.遗留问题

  • uboot重定位对比裸机
  • 初始化DDR对比裸机
  • 初始化串口对比裸机
  • 初始化时钟对比裸机

你可能感兴趣的:(嵌入式之uboot源码分析-启动第一阶段学习笔记)