以下内容源于朱有鹏嵌入式课程的学习与整理,如有侵权请告知删除。
目录
二、分析start.S文件(位于/cpu/s5pc11x/目录)
9、调用lowlevel_init函数(位于/board/samsung/x210\lowlevel_init.S文件)
(6)第一次判断当前代码执行位置(以决定是否进行时钟与DDR的初始化)
11、第二次判断当前代码执行位置(以决定是否进行uboot的重定位)
12、配置内存管理单元MMU(实现虚拟地址到物理地址的映射)
一、总结
1、关于阶段的定义
第一阶段,即在内部SRAM运行的阶段,简单地理解为汇编阶段。此阶段主要涉及start.S文件,在/cpu/s5pc11x/目录下。第一阶段以ldr pc _start_armboot为结束。
第二阶段,即在DDR中运行的阶段,简单地理解为C语言阶段。此阶段主要涉及start_armboot函数,在/lib_arm/board.c文件的444~908行。
2、第一阶段完成的任务
- 异常向量表的实现;
- 设置CPU进入特权模式,即SVC模式;
- 检查恢复状态;
- IO状态恢复;
- 关看门狗;
- 一些与SRAM、SROM相关的GPIO设置;
- 开发板的供电锁存;
- 时钟的初始化;
- DDR的初始化;
- 串口的初始化并打印“OK”;
- 设置栈空间;
- uboot的重定位;
- 建立映射表并开启MMU。
可见uboot的第一阶段初始化了SoC内部的一些部件,初始化DDR并且重定位。
uboot被分割成两部分,即前8kb和整个ubooot,则前8k内容肯定包括第一阶段的操作任务,其中很重要的操作有重定位。
二、分析start.S文件(位于/cpu/s5pc11x/目录)
在C语言中,整个项目的入口就是main函数(这是C语言规定的),所以如果有一个包含几万个.c文件的项目,第一个要分析的文件就是包含了main函数的那个文件。
在uboot中因为有汇编阶段参与,因此不能直接找包含了main函数的那个文件。整个程序的入口取决于链接脚本中ENTRY声明的地方。uboot的链接脚本见博文uboot的链接脚本u-boot.lds分析,里面有一句代码“ ENTRY(_start)”,因此_start符号所在的文件就是起始文件,所处的位置就是起始位置。通过使用SI工具查找符号“_start”,发现其所在文件是/cpu/s5pc11x/start.S文件。
//start.S文件内容
//省略其他代码
.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
//省略其他代码
1、头文件包含
#include <config.h>
#include <version.h>
#if defined(CONFIG_ENABLE_MMU)
#include <asm/proc/domain.h>
#endif
#include <regs.h>
(1)#include <config.h>
文件config.h位于/include目录,该文件不是源码中本身存在的文件,而是配置过程中自动生成的文件(详见mkconfig脚本)。
此文件的全部内容如下:
/* Automatically generated - do not edit */
#include <configs/x210_sd.h>
则/include/config.h文件的内容就等同于/include/configs/x210_sd.h文件的内容。
x210_sd.h文件是整个uboot移植时的配置文件,里面有好多宏。因此这个头文件包含将/include/configs/x210_sd.h文件和start.S文件关联了起来,之后在分析start.S文件时,主要要考虑的就是x210_sd.h文件。
(2)#include <version.h>
文件version.h位于/include目录,文件内容是“ #include "version_autogenerated.h" ”。
version_autogenerated.h这个文件是在配置过程中自动生成的,它里面就一行内容:
#define U_BOOT_VERSION "U-Boot 1.3.4"
由此可知,version.h文件的内容就相当于“ #define U_BOOT_VERSION "U-Boot 1.3.4" ”。
这里面定义的宏U_BOOT_VERSION的值是一个字符串,字符串中的版本号信息来自于Makefile中的配置值,如下所示:
$(VERSION_FILE): @( printf '#define U_BOOT_VERSION "U-Boot %s%s"\n' "$(U_BOOT_VERSION)" \ '$(shell $(CONFIG_SHELL) $(TOPDIR)/tools/setlocalversion $(TOPDIR))' \ ) > $@.tmp @cmp -s $@ $@.tmp && rm -f $@.tmp || mv -f $@.tmp $@
这个宏U_BOOT_VERSION在程序中会被调用,在uboot启动过程中串口会打印出uboot的版本号,那个版本号信息就是从这来的。
(3)#include <asm/proc/domain.h>
asm目录不是uboot的原生目录,它是配置时创建的一个符号链接,指向了asm-arm(见uboot的配置与编译——分析/mkconfig文件)。
经过分析后发现,#include <asm/proc/domain.h>,实际等同于#include<include/asm-arm/proc-armv/domain.h>。
从这里可以看出/mkconfig文件中创建的符号链接的作用。如果没有这些符号链接,则编译时根本通不过,因为找不到头文件。另外start.S文件的绝对路径是/cpu/s5pc11x/start.S,而asm的绝对路径是/include/asm,在start.S中包含头文件时,为何不用列出asm的绝对路径呢?因为在uboot的配置与编译——分析/config.mk文件中,已经设置将/include作为头文件的搜寻路径。
这也说明uboot不能在windows的共享文件夹下配置编译,因为windows中没有符号链接。
为什么start.S不直接包含asm-arm/proc-armv/domain.h,而要用asm/proc/domain.h?这样设计主要是为了可移植性。因为如果直接包含,则start.S文件和CPU架构就有关了,可移植性就差了。比如把uboot移植到mips架构下,则start.S源代码中所有的头文件包含全部要修改。这里我们用了符号链接之后,则start.S中源代码不用改,只需要在具体的硬件移植时配置不同(传入mkconfig文件的那6个参数),则创建的符号链接指向就会不同,从而具有可移植性。
2、启动代码的16字节头部
#if defined(CONFIG_EVT1) && !defined(CONFIG_FUSED)
.word 0x2000
.word 0x0
.word 0x0
.word 0x0
#endif
(1)裸机中讲过,在SD卡启动或者iNand启动时,整个镜像开头需要16字节的校验头。我们以前做裸机程序时,如果是以usb启动直接下载的方式启动的,则不需要16字节校验头;如果是SD卡启动,则mkv210image.c中会给原镜像前加16字节的校验头。
(2)arm7和arm9的arm指令集,一个字类型是32bit,即4个字节。uboot的start.S文件中在开头位置放了16字节的填充占位(这里的数字可以任意),这个占位的16字节只是保证正式的image的头部确实有16字节,但是这16字节的内容是不对的,还是需要后面去计算校验和然后重新填充的。
(3).word是arm汇编的伪指令,表示“当前地址对应的存储单元里存储的数据是xxx”。
- “.word 0x2000”表示当前地址对应的存储单元里存储的数据是0x2000。
- “.word _start”表示当前地址对应的存储单元里存储的数据是标号_start所在的地址。
3、构建异常向量表
.globl _start
_start:
b reset //刚上电肯定执行这行语句。b是分支指令,表示跳转
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)最后一句是让内存16字节对齐,如果不对齐,则自动向后移动地址直到对齐,并且向后移动地址所对应的那些内存要用0xdeadbeef来填充。至于为何用这个数字填充,是因为a~f这几个字母所能组成的有意义的单词有限,deadbeef是其中一个。至于为什么要对齐访问?有时候是效率的要求,有时候是硬件的特殊要求。
(4)复位异常处理代码是“b reset”。开发板刚上电属于复位,会执行这条语句。
4、uboot的链接地址与物理地址
_TEXT_BASE: //链接地址
.word TEXT_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
.globl _armboot_start
_armboot_start:
.word _start
/*
* These are defined in the board-specific linker script.
*/
.globl _bss_start
_bss_start:
.word __bss_start
.globl _bss_end
_bss_end:
.word _end
(1)标签或者说标号,它表示一个内存地址。而.word后面的内容,表示此内存地址对应的存储单元中存储的数值(4字节)。比如_bss_end: .word _end,它表示地址_bss_end所对应的存储单元中存放的内容是_end这个标号所在的地址。
(2)_TEXT_BASE 这个内存地址存放的内容为TEXT_BASE。
- 由链接脚本u-boot.lds分析、分析/config.mk文件可知,TEXT_BASE其实就是uboot的链接地址,为0xc3e0_0000。
(3)_TEXT_PHY_BASE 这个内存地址存放的内容为CFG_PHY_UBOOT_BASE。
- 这个宏定义在/include/configs/x210_sd.h文件中(第556行),如下所示:
//第89行
//#define MEMORY_BASE_ADDRESS 0x20000000
#define MEMORY_BASE_ADDRESS 0x30000000 //移植过的
#define MEMORY_BASE_ADDRESS2 0x40000000
//第556
#define CFG_PHY_UBOOT_BASE MEMORY_BASE_ADDRESS + 0x3e00000
//0x30000000 + 0x3e00000 =0x33e0_0000
- CFG_PHY_UBOOT_BASE其实就是uboot在内存中的物理地址,为0x33e00000。
下面第12点在配置MMU时,将uboot的链接地址设置为0xc3e00000,因为它将被映射成0x33e00000这个物理地址。
5、设置CPU为SVC模式
复位异常处理代码是“b reset”,开发板刚上电属于复位而会执行这条语句。
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 - 0b1101 0011 - SVC
//还有其他代码,这里不写
该函数对cpsr寄存器赋值为0b1101_0011,意思是把CPU的工作模式设置为超级用户模式、arm状态,并将普通中断IRQ和快速中断FIQ的禁止位置1,从而屏蔽IRQ与FIQ中断。关于cpsr寄存器的位含义,见博客ARM通识——ARM的7种基本工作模式、37个通用寄存器。
6、设置L2、L1cache和关闭MMU
cpu_init_crit:
//此处为条件编译语句,删除了部分代码
bl disable_l2cache // 禁止L2 cache
bl set_l2cache_auxctrl_cycle // L2 cache相关初始化
bl enable_l2cache // 使能L2 cache
//刷新L1 cache的icache和dcache。
/*
* 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 //注释1
//关闭MMU
/*
* disable MMU stuff and caches
*/
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #0x00002000 @ clear bits 13 (--V-) 0010 0000 0000 0000
bic r0, r0, #0x00000007 @ clear bits 2:0 (-CAM) 0000 0000 0000 0111
orr r0, r0, #0x00000002 @ set bit 1 (--A-) Align 0000 0000 0000 0010
orr r0, r0, #0x00000800 @ set bit 12 (Z---) BTB 0000 1000 0000 0000
@ 这里明明是bit 11,怎么说是bit 12呢?
mcr p15, 0, r0, c1, c0, 0 //注释2
(1)bic和orr指令的用法见博客汇编指令——bic(位清除)、orr(位或)、eor (异或)。
(2)代码中的c0,c1,c7,c8都是协处理器CP15的寄存器,其中c7是cache控制寄存器,c8是TLB控制寄存器,注释1将0写入c7、c8,使得cache和TLB内容无效。
(3)注释2关闭了MMU。这是通过修改协处理器CP15的c1寄存器来实现的。通过将c1寄存器的M位置为0来关闭MMU。关于CP15的c1寄存器的位含义,可以查看其数据手册。
(4)这段代码都是和CPU的cache和mmu有关的,不用去细看,大概知道即可。
7、识别并暂存启动介质选择
(1)启动介质由SoC的OM0:OM5这6个引脚的电平情况来决定。具体来说,某个寄存器根据这6个引脚的电平情况,自动设置寄存器的值。通过读取这个寄存器的值,就可以判断启动介质。
/* Read booting information */
ldr r0, =PRO_ID_BASE
ldr r1, [r0,#OMR_OFFSET]
bic r2, r1, #0xffffffc1
在/include/s5pc110.h文件中第267行有如下代码,得知寄存器的地址为0xE000_0004。
#define PRO_ID_BASE 0xE0000000
#define OMR_OFFSET 0x04
(2)上面三行代码后,r2就存储了一个数字。这个数字等于某个特定值时就表示Nand启动,等于另一个特定值时表示从SD卡启动,下面通过该数字进行启动介质的判断。
//条件编译,省略部分代码
/* 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 */ //sd卡启动 ,执行这个
cmp r2, #0xc
moveq r3, #BOOT_MMCSD
/* NOR BOOT */
cmp r2, #0x14
moveq r3, #BOOT_NOR
//条件编译,省略部分代码
/* Uart BOOTONG failed */
cmp r2, #(0x1<<4)
moveq r3, #BOOT_SEC_DEV
ldr r0, =INF_REG_BASE
str r3, [r0, #INF_REG3_OFFSET]
如果r2中的值为0xc,则表示从SD卡启动,然后把#BOOT_MMCSD(值为0x3)赋给r3(注意moveq是否执行,取决于上一行cmp比较的结果,相同则执行,不同则moveq不会执行)。
最后两行中,因为有#define INF_REG_BASE 0xE010F000、#define INF_REG3_OFFSET 0x0c,则合成寄存器的地址为0xE010F00C,然后把r3寄存器中的值放入INF_REG3寄存器中。
8、第一次设置栈(在SRAM中设置栈)
/*
* 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 */ //sp=sp-12
mov fp, #0
bl lowlevel_init /* go setup pll,mux,memory */
(1)这次设置栈是在SRAM中设置的,因为当前整个代码还在SRAM中运行,此时DDR还未被初始化还不能用。
(2)之所以要初始化栈,是因为接下来调用的lowlevel_init函数中还要调用其他函数。bl跳转时会自动将返回地址存储到LR中,但是CPU特权模式下只有一个LR,在调用lowlevel_init函数时,LR就已经自动保存着该函数的返回地址了。如果lowlevel_init函数中还调用其他函数,则LR又会自动保存其他函数的返回地址,这样就会把lowlevel_init函数的返回地址覆盖掉。我们初始化栈后,就可以将LR中所保存的函数返回地址入栈,当函数退出时就可以一层层地出栈,这样就不怕返回地址被覆盖掉了。
(3)栈地址0xD0036000是自己指定的,指定的原则就是这块空间只给栈用,不会被别人占用。
(4)减法指令sub的用法,见博客ARM官方汇编指令。
(5)“mov fp, #0”的含义,见博客FP、SP、LR寄存器。
9、调用lowlevel_init函数(位于/board/samsung/x210\lowlevel_init.S文件)
上面设置好栈之后,就跳转到了lowlevel_init函数。
该函数位于/board/samsung/x210\lowlevel_init.S文件中,主要完成以下工作。
(1)检查复位状态
复杂CPU支持多种复位情况,包括冷上电、热启动、休眠状态下的唤醒等,因此在复位代码中判别复位状态。然后根据复位状态来判断是否需要进行DDR的初始化:当冷上电时DDR需要初始化,而热启动或者低功耗状态下的复位则不需要再次初始化DDR。
_TEXT_BASE:
.word TEXT_BASE
.globl lowlevel_init
lowlevel_init:
push {lr}
/* 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
(2)IO状态恢复
本段代码和主线启动代码都无关,因此不用去管它。
/* IO Retention release */ //IO状态恢复
ldr r0, =(ELFIN_CLOCK_POWER_BASE + OTHERS_OFFSET)
ldr r1, [r0]
ldr r2, =IO_RET_REL
orr r1, r1, r2
str r1, [r0]
(3)关看门狗
/* Disable Watchdog */ //关闭看门狗
ldr r0, =ELFIN_WATCHDOG_BASE /* 0xE2700000 */
mov r1, #0
str r1, [r0]
- ELFIN_WATCHDOG_BASE是定义在/include/s5pc110.h文件中的一个宏,值为0xE2700000,它是WTCON寄存器的地址(见博客S5PV210定时器的理论与操作第五点的内容)。
- 该寄存器主要用来设置是否使能看门狗定时器、选择预分频的分频系数、是否启用中断、是否产生复位信号等内容。
(4)与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]
/* CS0 - 16bit sram, enable nBE, Byte base address */
ldr r0, =ELFIN_SROM_BASE /* 0xE8000000 */
mov r1, #0x1
str r1, [r0]
(5) 开发板的供电置锁
/* 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]
- 关于供电置锁的内容,见博客开发板——X210开发板的软开关(供电置锁)。
- 简单地说,就是把某个寄存器的bit0、8、9置为1。
(6)第一次判断当前代码执行位置(以决定是否进行时钟与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 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 //初始化串口打印“O”
bl tzpc_init //trust zone的初始化
这几行代码的作用,就是判定当前代码执行的位置在SRAM中还是在DDR中。
为什么要做这个判定?BL1在SRAM中有一份,在DDR中也有一份。因此如果是冷启动,那么当前代码应该是在SRAM中运行的BL1;如果是低功耗状态的复位,那么当前代码应该是在DDR中运行的BL1。我们判定当前运行代码的地址是有用的,可以指导后面代码的运行。比如在lowlevel_init.S中判定当前代码的运行地址,就是为了确定要不要执行时钟初始化和初始化DDR的代码。如果当前代码是在SRAM中,说明是冷启动,那么时钟和DDR都需要初始化;如果当前代码是在DDR中,则说明是热启动(低功耗状态的复位),那么时钟和DDR都不用再次初始化。
“ bic r1, pc, r0 ” 这句代码的意义是:将pc的值中的某些bit位清0(r0中为1的那些位清零),剩下一些特殊的bit位赋值给r1,就相当于“ r1 = pc & ~(ff000fff)”;
“ldr r2, _TEXT_BASE ”,“ bic r2, r2, r0 ”这两句代码意思是:将链接地址赋值给r2,然后将r2的相应位清0剩下特定位。
最后比较r1和r2。
总结:这一段代码是通过读取当前运行地址和链接地址,然后将两个地址处理一下后进行对比,看是否相等,来判定当前运行是在SRAM中(不相等)还是DDR中(相等),从而决定是否跳过下面的时钟和DDR初始化。
(7)时钟初始化函数:system_clock_init
这个初始化时钟的过程和裸机中的初始化的过程一样的,只是更加完整而且是用汇编代码写的。
在/include/configs/x210_sd.h文件中300行到428行,都是和时钟相关的配置值。这些宏定义就决定了S5PV210的时钟配置是多少。换言之,代码在lowlevel_init.S中都写好了,但是代码的设置值都被宏定义在x210_sd.h文件中了。因此如果移植时需要更改CPU的时钟设置,不需要改动代码,只需要在x210_sd.h文件中更改配置值即可。
(8)内存的初始化:mem_ctrl_asm_init
该函数在/cpu/s5pc11x/s5pc110/cpu_init.S文件中,裸机课程中初始化DDR的代码就是参考这函数写的。
由X210核心板原理图可知,S5PV210共有2个内存端口,即DRAM0和DRAM1,其范围如下所示,可知最多支持1.5GB的内存。
- DRAM0端口允许的内存地址范围:0x20000000~0x3FFFFFFF(512MB)。
- DRAM1端口允许的内存地址范围:0x40000000~0x7FFFFFFF(1GB)。
但是开发板实际上不需要这么大的内存,比如X210开发板只有512MB内存,分布情况如下所示。
- 在DRAM0端口分布256MB。SoC允许我们在DRAM0端口给这256MB挑选地址范围。
- 在DRAM1端口分布256MB。其地址范围是0x40000000~0x4FFFFFFF。
- 这些地址是可以重新设置的,见博客移植三星官方的uboot到x210开发板第二点的第6部分。
在X210开发板的uboot中,我们(在x210_sd.h文件中通过设置DMC0_MEMCONFIG_0这个宏的值)设置的内存物理地址范围是 0x30000000~0x4FFFFFFF(512MB),其中DRAM0端口选择0x30000000~0x3FFFFFFF,DRAM1端口是0x40000000~0x4FFFFFFF。(另外注意条件编译,x210_sd.h配置头文件中考虑了不同时钟配置下的内存配置值,目的是让不同时钟需求的客户都能找到适当的内存配置值。)
//第439行
#define DMC0_MEMCONFIG_0 0x30F01313 // MemConfig0 256MB config, 8 banks,Mapping Method[12:1 5]0:linear, 1:linterleaved, 2:Mixed
#define DMC0_MEMCONFIG_1 0x40F01313 // MemConfig1
//其他代码
与X210开发板的uboot不同,裸机中设置DMC0_MEMCONFIG_0这个宏的值为0x20E01323D,因此DRAM0端口选择的是0x20000000~0x2FFFFFFF。
(9)串口的初始化(并通过串口发送了一个“O”)
关于串口的初始化,见博客串口通信——S5PV210串口通信的简单案例。
下面代码将串口初始化后,通过串口发送了一个字符'O'。
/*
* uart_asm_init: Initialize UART in asm mode, 115200bps fixed.
* void uart_asm_init(void)
*/
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
(10)tzpc初始化
bl tzpc_init
trust zone初始化,没搞过,不管。
(11)pop {pc}(并在返回前通过串口打印“K”)
由关于ASCII字符的那些事儿可知,字母K的ASCII码用十六进制表示是4B。
/* check reset status */
ldr r0, =(ELFIN_CLOCK_POWER_BASE+RST_STAT_OFFSET)
ldr r1, [r0]
bic r1, r1, #0xfffeffff
cmp r1, #0x10000
beq wakeup_reset_pre
/* ABB disable */
ldr r0, =0xE010C300
orr r1, r1, #(0x1<<23)
str r1, [r0]
/* Print 'K' */ //打印一个字符K
ldr r0, =ELFIN_UART_CONSOLE_BASE
ldr r1, =0x4b4b4b4b //大写字母K的ASCII码,用十六进制就是4b
str r1, [r0, #UTXH_OFFSET]
pop {pc}
总结:lowlevel_init.S执行完如果没出错,则会通过串口打印出"OK"字样,这应该是我们uboot中看到的最早的输出信息。
10、第二次设置栈(在DDR中设置栈)
//第一次设置栈(在SRAM中)
/*
* 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 */
//电源锁存,这个设置在lowlevel_init中已经完成,再次设置也没事
/* To hold max8698 output before releasing power on switch,
* set PS_HOLD signal to high
*/
ldr r0, =0xE010E81C /* PS_HOLD_CONTROL register */
ldr r1, =0x00005301 /* PS_HOLD output high */
str r1, [r0]
//第二次设置栈(在DDR中)
/* 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 */
上面代码中再次开发板供电锁存,为何?第一,做2次是不会错的;第二,做2次则第2次无意义。做代码移植时有一个古怪谨慎保守策略,就是尽量添加代码而不要删除代码。
(1)为什么要再次设置栈?
第一次设置栈,是在调用lowlevel_init函数前,那时程序在SRAM中执行,所以在SRAM中分配一部分内存作为栈。但是SRAM中内存空间有限,栈放在那里要注意不能使用过多,否则栈会溢出。
第二次设置栈,是因为DDR已经被初始化,有大片内存可以使用,没必要再把栈放在SRAM那可怜巴巴的有限空间中。我们及时将栈迁移到DDR中也是为了尽可能避免使用栈时的诸多不便。
(2)第二次设置的栈地址是多少?
第二次设置的栈的地址是_TEXT_PHY_BASE(由上面第4大点可知为0x33E00000)。注意到这个地址刚好是uboot在内存中开始存放的地址,uboot存放是向上增长的;而这里的栈是满减栈,所以栈向下增长,两者不冲突的。
11、第二次判断当前代码执行位置(以决定是否进行uboot的重定位)
(1)为什么再次判断运行地址?
在上面的lowlevel_init函数中,判断运行地址是在SRAM中还是在DDR中,是为了决定是否要执行初始化时钟和DDR的代码。本次判断运行地址,是为了决定是否进行uboot的重定位。
下面这段代码,和第一次判断运行地址的代码是一样的。
//判断当前位置是否在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 */
//如果相同则说明当前位置在DDR中,不需要重定位,直接跳转到标号after_copy处执行
beq after_copy /* r0 == r1 then skip flash copy */
//如果不同则说明当前位置在SRAM中,需要重定位,即执行下面的代码
//这里省略重定位代码,见下面(3)的描述
after_copy:
//省略代码
(2)什么叫uboot的重定位?
冷启动时,uboot的第一部分BL1会自动从SD卡加载到SRAM中运行,而uboot的第二部分(即完整的uboot)还存储在SD卡(以某个扇区为开始的)N个扇区中。此时uboot的第一阶段即将结束,需要把第二部分从SD卡加载到DDR中链接地址处(0x33e00000),这个加载过程就叫重定位。
(3)如何实现uboot的重定位?
#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 //如果从SD卡启动,则直接跳到标号mmcsd_boot
#endif
///
//如果是sd卡,这段区域代码不会执行;如果是iNand则会执行,但最终还是跳转到标号mmcsd_boot
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 //iNand符合这个
beq mmcsd_boot
cmp r1, #BOOT_NOR
beq nor_boot
cmp r1, #BOOT_SEC_DEV
beq mmcsd_boot
nand_boot:
mov r0, #0x1000
bl copy_from_nand
b after_copy
onenand_boot:
bl onenand_bl2_copy
b after_copy
mmcsd_boot://执行这个标号处的代码
#if DELETE //无定义
ldr sp, _TEXT_PHY_BASE
sub sp, sp, #12
mov fp, #0
#endif
bl movi_bl2_copy //实际进行重定位的函数
b after_copy //直接跳到标号after_copy
nor_boot://这个标号处的代码不会被执行
bl read_hword
b after_copy
after_copy:
//忽略该处代码
上面代码中,D0037488这个内存地址在SRAM中,这个地址中的值会被硬件自动设置。硬件根据实际电路中SD卡在哪个通道,自动将这个地址中的值设置为相应的数字(注意和二7中自动识别启动介质的那个寄存器地址区别开来。这里是已经知道从SD或iNand启动后,再进一步确定是SD卡还是iNand)。比如从SD0通道(即板载的iNand)启动时,这个值为EB000000;从SD2通道(即SD卡,接近复位键的哪个卡槽)启动时,这个值为EB200000。
因为我们从SD卡启动,或者从iNand启动,都会跳转到mmcsd_boot函数。该函数通过调用movi_bl2_copy函数完成重定位操作。movi_bl2_copy函数位于/cpu/s5pc11x/movi.c文件中,内容如下:
typedef u32(*copy_sd_mmc_to_mem)(u32 channel, \
u32 start_block, u16 block_size, u32 *trg, u32 init);
void movi_bl2_copy(void)
{
ulong ch;
#if defined(CONFIG_EVT1) //执行这个
ch = *(volatile u32 *)(0xD0037488);
//定义了一个函数指针copy_bl2,其指向0xD0037F98这个地址的内容(拷贝函数)
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) { //从通道0,即iNand中拷贝
ret = copy_bl2(0, MOVI_BL2_POS, MOVI_BL2_BLKCNT,
CFG_PHY_UBOOT_BASE, 0);
}
else if (ch == 0xEB200000) {//从通道2,即sd卡中拷贝
ret = copy_bl2(2, MOVI_BL2_POS, MOVI_BL2_BLKCNT,
CFG_PHY_UBOOT_BASE, 0);
}
else
return;
if (ret == 0)
while (1)
;
else
return;
}
根据开发板——X210开发板的SD卡启动方式,拷贝函数的入口地址0xD0037F98(我们只知道该函数的入口地址,以及它的参数含义、返回值含义,从而知道如何调用这个函数,但拷贝函数内部是如何拷贝的,或者说函数的实体,我们并不知道)。
我们关注copy_bl2(2, MOVI_BL2_POS, MOVI_BL2_BLKCNT,CFG_PHY_UBOOT_BASE, 0)函数的参数含义。
- 参数1表示什么通道。这里是通道2,也就是SD2通道,即SD卡。
- 参数2表示从SD卡的第几个扇区开始复制。MOVI_BL2_POS是uboot的第二部分(其实就是整个uboot)在SD卡中的开始扇区,这个扇区的编号必须与烧录uboot时的位置一致。
- 参数3表示一共复制多少个扇区的内容。MOVI_BL2_BLKCNT是uboot的第二部分(其实就是整个uboot)所占用的扇区数。
- 参数4表示将内容复制到哪个内存地址。我们要将内容复制到链接地址处,因此CFG_PHY_UBOOT_BASE这个宏的值为0x33E00000(目前为止还没有开启MMU,因此使用实际的物理地址)。
这里计算一下MOVI_BL2_POS、MOVI_BL2_BLKCNT的值(以扇区为单位)。
在/include/movi.h文件有如下代码:
/* Change writing block position at fused chip */
#if defined(CONFIG_EVT1)
#if defined(CONFIG_SECURE) || defined(CONFIG_FUSED)
#define MOVI_BL2_POS ((eFUSE_SIZE / MOVI_BLKSIZE) + (FWBL1_SIZE / MOVI_BLKSIZE) + MOVI_BL1_BLKCNT + MOVI_ENV_BLKCNT)
#else //选这条路
#define MOVI_BL2_POS ((eFUSE_SIZE / MOVI_BLKSIZE) + MOVI_BL1_BLKCNT + MOVI_ENV_BLKCNT)
#endif
#else
#define MOVI_BL2_POS (MOVI_LAST_BLKPOS - MOVI_BL1_BLKCNT - MOVI_BL2_BLKCNT - MOVI_ENV_BLKCNT)
#endif
这表明,MOVI_ENV_BLKCNT共16KB,折合32个扇区;MOVI_BL1_BLKCNT共8KB,折合16个扇区;而(eFUSE_SIZE / MOVI_BLKSIZE)由下面代码可知,其值为1个扇区。
#if defined(CONFIG_EVT1)//选择这个
#define eFUSE_SIZE (1 * 512) // 512 Byte eFuse, 512 Byte reserved
#else
#define eFUSE_SIZE (1 * 1024) // 1 kB eFuse, 1 KB reserved
#endif /* CONFIG_EVT1 */
则MOVI_BL2_POS表示SD卡的第49扇区(注意SD卡扇区编号以0为开始,像数组下标一样)。
12、配置内存管理单元MMU(实现虚拟地址到物理地址的映射)
【1】知识充能1:什么是虚拟地址、物理地址?
(1)物理地址,就是物理设备设计生产时赋予的地址。像裸机中使用的寄存器的地址就是CPU设计时指定的,这个就是物理地址。物理地址是硬件编码的,是设计生产时确定好的,一旦确定了就不能改了。一个事实就是:寄存器的物理地址是无法通过编程修改的,是多少就是多少,只能通过查询数据手册获得并操作。坏处就是不够灵活。一个解决方案就是使用虚拟地址。
(2)虚拟地址,意思就是在我们软件操作和硬件操作之间增加一个层次,叫做虚拟地址映射层。有了虚拟地址映射后,软件操作只需要给虚拟地址,硬件操作还是使用原来的物理地址,映射层建立一个虚拟地址到物理地址的映射表。当我们软件运行的时候,软件中使用的虚拟地址在映射表中查询得到对应的物理地址再发给硬件去执行。
【2】知识充能2:MMU单元的作用。
(1)内存管理单元(MMU,memory management unit),其本质是SOC中的一个硬件单元,主要功能是实现虚拟地址到物理地址的映射。
(2)在uboot阶段并不是必须要开启MMU,在没开启MMU前使用的是物理地址,开启MMU后使用的是虚拟地址。
(3)MMU就是在物理内存和应用程序之间添加了一个层次,专门用来管理内存,这样写应用程序的人就不用关心物理内存的细节。
(4)比如32位的机器理论上最大内存为4G,运行的程序以进程为单位,每个进程都认为自己拥有4G的内存,其实分配给该进程的物理内存肯定是没有4G的,进程的虚拟内存有4G。虚拟内存和物理内存都按照相同大小的划分页,虚拟内存的页和物理内存的页建立映射关系,当程序要访问的页不在内存时就会发生缺页异常,然后将磁盘里的页加载到内存中,如果此时物理内存没有空闲的页则还涉及页面置换。使用虚拟内存的好处:访问控制、内存分配和释放提供方便(比如malloc实际可能不是连续的)、运行超过物理内存大小的程序等。
【3】知识充能3:虚拟地址映射的额外收益。
(1)实现访问控制。访问控制,就是在管理上对物理内存进行分块,然后对每块物理内存进行独立的虚拟地址映射,并且在每一块内存的映射关系中同时实现访问控制(对该块可读、可写、只读、只写、不可访问等控制)。在C语言中编程中经常会出现一个错误Segmentation fault,这个段错误就和MMU实现的访问控制有关。当前程序只能操作自己有权限操作的地址范围(若干个内存块),如果当前程序指针访问了不该访问的内存块则就会触发段错误。
(2)cache。cache的工作也和虚拟地址映射有关系。cache是快速缓存,意思就是比CPU慢但是比DDR块。CPU嫌DDR太慢了,于是把一些DDR中常用的内容事先读取缓存在cache中,然后CPU每次需要找东西时先在cache中找。如果cache中有就直接用cache中的;如果cache中没有才会去DDR中寻找。
【4】虚拟地址和物理地址的转换
虚拟地址和物理地址的转换是依靠转换表。
转换表(TT,translation table)是建立一套虚拟地址映射的关键。转换表分2部分,表索引(对应虚拟地址)和表项(对应物理地址)。一对表索引和表项构成一个转换表单元,能够对一个内存块进行虚拟地址转换(内存映射与管理是以块为单位的。至于每个块有多大,要看MMU的支持和你自己的选择。ARM支持3种大小的块:细表1KB、粗表4KB、段1MB)。真正的转换表由若干个转换表单元构成,每个转换表单元负责1个内存块;总体的转换表负责整个内存空间(0~4G)的映射。
映射分为段映射和页映射,这里是按照段映射(1M)进行讲解的,就是把内存按照1M为单位(上面提到的的内存块,这里为1M)进行划分,4G的内存就分为4096个段,每个段对应一个页表项,每个页表项占4字节,页表总共占16KB。只需要构建好页表然后设置TTB(转换表的基地址),硬件会自动去查询页表进行转换。
可以这样宏观上理解转换表:整个转换表可以看作是一个int类型的数组,数组中的一个元素就是一个表索引和表项的单元。数组中的元素值就是表项,这个元素的数组下标就是表索引。把虚拟地址按照一定规则进行查表,根据表的内容就可以知道对应的物理地址。
建立虚拟地址映射的主要工作,其实就是建立这张转换表。
【5】知识充能4:如何开启MMU?
MMU是通过CP15协处理器进行控制的,也就是说要操控MMU进行虚拟地址映射,需要对CP15协处理器的寄存器进行编程。CP15协处理器内部有c0到c15共16个寄存器,这些寄存器每一个都有自己的作用。我们可以通过mrc和mcr指令来访问这些寄存器。
(1)步骤1:使能域访问(通过设置CP15的c3寄存器)
c3寄存器在MMU中的作用是控制域访问,而域访问是和MMU的访问控制有关的。
(2)步骤2:设置TTB(通过设置CP15的c2寄存器)
TTB就是translation table base,即“转换表的基地址”。
转换表放置在内存中的,放置时要求起始地址在内存中要xx位对齐。转换表不需要软件去干涉使用,而是将基地址TTB设置到cp15的c2寄存器中,然后MMU工作时会自动去查转换表。
(3)步骤3:开启MMU(通过设置CP15的c1寄存器)
CP15的c1寄存器的bit0控制着MMU的开关,只要将这个bit置1即可开启MMU。
开启MMU之后上层软件层的地址就必须经过转换表的转换才能发给下层物理层去执行。
上面二6是关闭MMU,这里是开启MMU,可以对照理解。
after_copy:
#if defined(CONFIG_ENABLE_MMU)
enable_mmu:
/* 使能域访问,将cp15的c3寄存器设置成0x0000ffff*/
ldr r5, =0x0000ffff
mcr p15, 0, r5, c3, c0, 0 @load domain access register
/* 设置转换表基地址,_mmu_table_base就是转换表的基地址*/
ldr r0, _mmu_table_base
ldr r1, =CFG_PHY_UBOOT_BASE //CFG_PHY_UBOOT_BASE是uboot的链接地址
ldr r2, =0xfff00000
bic r0, r0, r2 //将r0的bit20-bit31清零
orr r1, r0, r1 //将r0的值与r1进行或操作,并将结果保存在r1中
mcr p15, 0, r1, c2, c0, 0 //将r1的值写到cp15协处理器的c2寄存器中
mmu_on:
/* 开启MMU:cp15的c1寄存器的bit0控制MMU的开关。只要将这一个bit置1即可开启MMU。*/
mrc p15, 0, r0, c1, c0, 0
orr r0, r0, #1
mcr p15, 0, r0, c1, c0, 0
#endif
【6】知识充能5: 虚拟映射表分析
首先找到建立虚拟映射表的代码。
由上面的分析可知,c2寄存器与虚拟映射表的基地址有关。上面代码中涉及c2的可疑的符号是_mmu_table_base,我们在SI中一路跟踪,得到:
#if defined(CONFIG_ENABLE_MMU)
_mmu_table_base:
.word mmu_table //mmu_table就是构建虚拟地址映射表
#endif
继续跟踪mmu_table,发现它在lowlevel_init.S文件的593行,发现构建虚拟映射表的代码如下:
/* form a first-level section entry */
//.macro与.endm 之间定义了一个宏
.macro FL_SECTION_ENTRY base,ap,d,c,b
.word (\base << 20) | (\ap << 10) | \
(\d << 5) | (1<<4) | (\c << 3) | (\b << 2) | (1<<1)
.endm
//这行代码解释,见//https://blog.csdn.net/tianxiawuzhei/article/details/7674313
.section .mmudata, "a"
.align 14 //因为MMU的table需4K对齐
// the following alignment creates the mmu table at address 0x4000.
.globl mmu_table
mmu_table:
.set __base,0 //.set的语法作用:将后者(0)赋值给前者(__base)
// Access for iRAM
// .rept与.endr配合使用
.rept 0x100 //重复0x100次(即256次),对应256MB
FL_SECTION_ENTRY __base,3,0,0,0 //定义一个字
.set __base,__base+1 //__base的值+1
.endr
// Not Allowed
.rept 0x200 - 0x100 //(512-256=256),所以重复256次,对应256MB
.word 0x00000000
.endr
.set __base,0x200
// should be accessed
.rept 0x600 - 0x200 //重复1024次,对应1024MB=1GB
FL_SECTION_ENTRY __base,3,0,1,1
.set __base,__base+1
.endr
.rept 0x800 - 0x600//重复512次,对应512MB
.word 0x00000000
.endr
.set __base,0x800
// should be accessed
.rept 0xb00 - 0x800 //重复768次,对应768MB
FL_SECTION_ENTRY __base,3,0,0,0
.set __base,__base+1
.endr
.set __base,0xB00
.rept 0xc00 - 0xb00//重复256次,对应256MB
FL_SECTION_ENTRY __base,3,0,0,0
.set __base,__base+1
.endr
.set __base,0x300
.rept 0xD00 - 0xC00//重复256次,对应256MB
FL_SECTION_ENTRY __base,3,0,1,1
.set __base,__base+1
.endr
.set __base,0xD00
.rept 0x1000 - 0xD00//重复768次,对应768MB
FL_SECTION_ENTRY __base,3,0,0,0
.set __base,__base+1
.endr
然后分析建立虚拟映射表的代码:
(1)ARM的段式映射中长度为1MB,因此一个映射单元只能管1MB内存,那我们整个4G范围内需要4G/1MB=4096个映射单元,也就是说这个数组的元素个数是4096。实际上我们做的时候并没有依次单个去处理这4096个单元,而是把4096个分成几部分,然后每部分用for循环做相同的处理。
(2).rept和.end构成循环语句,比如.rept 0xD00 - 0xC00和.endr,就是循环(0xD00-0xC00)次。
(3)构建页表就是通过多个循环,构建满足特定规则的int型数,因为一个页表项就刚好是4个字节,页表项的每个bit都有特定含义。
(4)FL_SECTION_ENTRY:这个宏就是用来构建页表项的,具体含义参考一级页表的页表项含义,实际效果就是定义一个特定的word类型数。
1】bit20~bit31(段基址):在设计地址映射时,之所以从bit20开始,是因为映射的物理地址要 1MB 对齐。段基址就是这段1MB 物理地址起始地址的高[31:20]位,每个条目中的描述符的段基址都不一样(以段来说,相差 1MB)。
2】bit10~bit11(AP):AP 是用来设置权限的,与 C1 的 R/S 位结合使用。
3】bit5~bit8(Domain域):不管是段模式还是页模式,系统都把 4GB 空间分为 16 个域,每个域有相同的权限检查(在 C3 设置 ),这里的 Domain 是用来标识本段所在的域。
4】bit2~bit3(C/B):C/B 位是控制位,与本条目(描述符)所在域的 Cache 和 Buffer 有关(是否允许本域开启 Cache 和 Buffer)。
5】bit0~bit1:=0b10,表示本描述符是表示段模式(段描述符标识)。
(5)最终得到的虚拟地址映射表如下:
转换得到的虚拟地址映射表:
VA PA length
0-10000000 0-10000000 256MB
10000000-20000000 0 256MB
20000000-60000000 20000000-60000000 1GB 512-1.5G
60000000-80000000 0 512MB 1.5G-2G
80000000-b0000000 80000000-b0000000 768MB 2G-2.75G
b0000000-c0000000 b0000000-c0000000 256MB 2.75G-3G
c0000000-d0000000 30000000-40000000 256MB 3G-3.25G
d-完 d-完 768MB 3.25G-4G
由此可知虚拟地址映射,只是把虚拟地址的c0000000开头的256MB映射到了DMC0的30000000开头的256MB物理内存上去了。其他的虚拟地址空间根本没动,还是原样映射的。
为什么配置时将uboot的链接地址设置为c3e00000?因为这个虚拟地址将来会被映射到33e00000这个物理地址(这里把33e00000叫做虚拟地址也行,因为虚拟地址33e00000会被原样映射为物理地址33e00000)。
13、第三次设置栈(在DDR中设置更合适的栈)
(1)为何再次设置栈?
本次设置栈,是为了将栈放在更合适(安全,紧凑而不浪费内存)的地方。
(2)将栈设置在哪个地方?
我们实际将栈设置在uboot起始地址上方的2MB处,则这个栈空间大小为:
2MB-uboot大小(约384KB)-0x1000(安全隔离?4KB)≈ 1.6MB。
这样设置栈空间,既没有浪费内存,也足够安全。
但其实根据uboot启动流程——C阶段的start_armboot函数-CSDN博客中“二2 2.2(3)”的描述,这大小为1.6MB的地址空间不全是栈空间,还有堆空间。
skip_hw_init:
/* Set up the stack */
stack_setup:
#if defined(CONFIG_MEMORY_UPPER_CODE) //执行这分支
ldr sp, =(CFG_UBOOT_BASE + CFG_UBOOT_SIZE - 0x1000)
//0x33e00000+2M-0x10000
#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
14、清理bss并跳转到start_armboot函数
clear_bss:
ldr r0, _bss_start /* find start of bss segment */
ldr r1, _bss_end /* find stop of bss segment */
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)清理bss段的代码和裸机中讲的一样(见内存与重定位——重定位的理论与代码实践)。
(2)bss段的开头_bss_start、结尾_bss_end,这两个符号是从链接脚本u-boot.lds得来的。
(3)start_armboot这个C语言函数在/lib_arm/board.c文件中中,它是uboot的第二阶段。
(4)“ldr pc _start_armboot”,将uboot第二阶段执行的函数的地址传给pc。实际上就是通过远跳转直接跳转到第二阶段的开始地址。远跳转,就是这句代码所加载的地址和当前运行地址无关,而与链接地址有关,因此可以实现从SRAM中的第一阶段跳转到DDR中的第二阶段。这个远跳转就是uboot第一阶段和第二阶段的分界线。