cortex-a8 uboot系列: 第五章 uboot源码分析1-启动第一阶段
0赞一、 start.S文件引入
1. u-boot.lds中找到start.S入口
在uboot中因为有汇编阶段参与,程序的开头不再是main.c了。整个程序的入口就取决于链接脚本中ENTRY声明的地方。ENTRY(_start),因此_start符号所在的文件就是整个程序的起始文件,_start所在处的代码就是整个程序的起始代码。
之前分析,使用的u-boot.lds在board/Samsung/210目录下。
使用sourceinsight工具来查找。
得到,点击前面的左右箭头打开文件。
可以知道_start在cpu\s5pc11x目录下的start.S文件在。
2. start.S文件分析
#include <config.h> #include <version.h> |
#include <config.h> config.h是在include目录下的,这个文件不是源码中本身存在的,而是配置过程中自动生成的。(详见mkconfig脚本)。这个文件的内容其实是包含了一个头文件:#include <configs/x210_sd.h>
因此分析后,发现start.S中包含的第一个头文件就include/configs/x210_sd.h。这个文件时整个uboot移植时的配置文件。这里面是好多宏,因此这个头文件包含将include/configs/x210_sd.h文件和start.S文件关联了起来。因此之后在分析start.S文件时,主要是考虑x210_sd.h文件。因此后面在start.S中一旦遇到有#ifdef , #ifndef这样的判断宏,就去x210_sd.h中查找该宏是否有定义。
#include<version.h> , include/version.h中包含了include/version_autogenerated.h头文件,这个头文件是make后自动产生的。主要是定义了一个宏。
#ifndef __VERSION_H__ #define __VERSION_H__ #ifndef DO_DEPS_ONLY #include "version_autogenerated.h" #endif #endif /* __VERSION_H__ */ |
version_autogenerated.h头文件,这个头文件是make x210_sd_config后自动产生。
定义了一个宏,U_BOOT_VERSION,表明uboot的版本。后面的值是makefile中设置的值。这个宏会在程序中被调用,在uboot启动过程中会串口打印出uboot的版本号,那个版本号信息就是从这来的。
#if defined(CONFIG_ENABLE_MMU) #include <asm/proc/domain.h> #endif |
在x210_sd.h中有定义这个宏CONFIG_ENABLE_MMU,因此会包括后面的头文件。
但是对于asm/proc/domain.h头文件,asm目录不是uboot中的原有目录,uboot中本来是没有这个目录的。asm目录是配置时创建的一个符号链接,实际指向的是include/asm-arm/proc-armv这个目录。因为在makefile中将asm指向了asm-arm, proc指向了proc-armv。
从这里,可以看出之前配置时创建的符号链接的作用,如果没有这些符号链接则编译时根本通不过,因为找不到头文件。(所以uboot不能在window的共享文件夹下编译,因为windows中没有符号链接)
#include <regs.h> |
reg.h是s5pc110.h的符号链接,所以这里其实是
#include <s5pc110.h>
为什么start.S不直接包含asm-arm/proc-armv/domain.h,而要用asm/proc/domain.h。这样的设计主要是为了可移植性。因为如果直接包含,则start.S文件和CPU架构(和硬件)就有关了,可移植性就差了。如果要使用其他架构的,就start.S源代码中所有的头文件包含全部都要修改。如果使用符号链接之后,则start.S源代码不需要改,只需要在具体的硬件移植时,创建的符号链接指向的不同的文件就可以了。这样增加了可移植性。
#ifndef CONFIG_ENABLE_MMU #ifndef CFG_PHY_UBOOT_BASE #define CFG_PHY_UBOOT_BASE CFG_UBOOT_BASE #endif #endif |
有定义CONFIG_ENABLE_MMU和CFG_PHY_UBOOT_BASE这两个宏(在x210_sd.h中有定义)。
CONFIG_ENABLE_MMU这个宏,是表示是否要启动MMU。
CFG_PHY_UBOOT_BASE这个宏,表示将来uboot运行时,放置在DRAM中的物理地址。通过查找,这个地址为0x33e0_0000。也就是将来,uboot会放到DRAM的0x33e0_0000地址处去执行。
#if defined(CONFIG_EVT1) && !defined(CONFIG_FUSED) .word 0x2000 .word 0x0 .word 0x0 .word 0x0 #endif |
宏判断(在x210_sd.h中有定义)为真,定义了4个字。这4个字其实是SD卡启动时,210要校验的头。
对于SD卡/iNand启动,启动的镜像开头需要16字节的校验头。(mkv210image.c中就是为了计算这个校验头)。这个是三星的手册中规定的。
第一个字表示镜像大小,第三个字是校验和。其余两个为0。
uboot这里只是定义了4个字空间进行占位,但是这里数据是不对的,等之后uboot编译生成后,在进行计算,然后在来修改这里的内容。
1) _start
异常向量表构建。
.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 |
异常向量表构建,和硬件有关。
对于 ldr pc, _undefined_instruction, 意思是将_undefined_instruction地址处的值赋值给pc,也就是undefined_instruction。于是PC就跳转到undefined_instruction去执行指令。
.global _end_vect _end_vect: .balignl 16,0xdeadbeef |
伪指令 .balignl ,让当前地址对齐排布。如果当前地址没有对齐,则自动向后走直到地址对齐,并且后面走的地址数据使用0xdeadbeef填充。
_TEXT_BASE: .word TEXT_BASE |
定义的TEXT_BASE只是一个符号,代码中没有定义这个值。这个值是makefile中定义的,定义这个值是链接时指定的uboot的链接地址(值就是0xc3e00000)。
_TEXT_PHY_BASE: .word CFG_PHY_UBOOT_BASE |
定义PHY的物理首地址,其实是0x33e00000。
#define CFG_PHY_UBOOT_BASE MEMORY_BASE_ADDRESS + 0x3e00000 #define MEMORY_BASE_ADDRESS 0x30000000 |
.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 #if defined(CONFIG_USE_IRQ) /* IRQ stack memory (calculated at run-time) */ .globl IRQ_STACK_START IRQ_STACK_START: .word 0x0badc0de /* IRQ stack memory (calculated at run-time) */ .globl FIQ_STACK_START FIQ_STACK_START: .word 0x0badc0de #endif |
定义一些值,供后面使用。
还可以配置时用中断,不过uboot中不使用中断,所以这里配置没有用。
2) 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 |
复位后执行的代码。设置处理器模式为SVC,工作在ARM状态下,并关闭中断。
Uboot工作时CPU一直处于SVC模式。
3) cpu_init_crit
bl disable_l2cache bl set_l2cache_auxctrl_cycle bl enable_l2cache |
禁止L2 cache。
L2 cache相关初始化
使能L2 cache。
其实就是对L2 cache进行初始化并使能。
/** 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 |
invalide TLB和icache。因为CPU在刚上电后,MMU是默认为关闭的,cache默认为也是关闭的,因此这里invalide操作其实是可以不用做的。
TLB是缓存虚拟地址转物理地址的结果。
icache是缓存指令的cache。
/* 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, 0x |
关闭MMU以及cache,因为还没有建立页表,所以需要关闭。其实这个时候MMU和cache就是关闭的,因为CPU默认为上电后,MMU和cache是关闭的。
/* Read booting information */ ldr r0, =PRO_ID_BASE ldr r1, [r0,#OMR_OFFSET] bic r2, r1, #0xffffffc1 |
PRO_ID_BASE和OMR_OFFSET是在include/reg.h中定义的,而这个reg.h文件其实是s5pc110.h的符号链接。其实也就是在s5pc110.h中定义的,这里可以看到符号链接的好处了。
读取启动信息。从一个寄存器里面读取值。0xe0000004。这个寄存器里面保存了OMpin的6个引脚的信息。而OM5:OM0这6个引脚电平情况决定了210的启动方式。而在210中,内部的寄存器(0xe0000004)的值就保存了OM引脚的值。所以读取这个寄存器可以知道启动方式是什么。
R2寄存器就保存了OM的值,所以通过R2就可以知道启动方式。
s5pv210手册中,没有提到这个寄存器,但是另外一个寄存器OM_STAT也保存了OM的值。
对于OM的值,见下表。
这样,通过判断OM的值,也就是r2的值,就可以知道启动方式是什么。
/* 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 |
判断NAND启动。是NAND的话,设置R3的值。
/* SD/MMC BOOT */ cmp r2, #0xc moveq r3, #BOOT_MMCSD /* NOR BOOT */ cmp r2, #0x14 moveq r3, #BOOT_NOR |
SD卡启动和nor启动。因为是从SD卡启动,所以R3就为3。
/* Uart BOOTONG failed */ cmp r2, #(0x1<<4) moveq r3, #BOOT_SEC_DEV
ldr r0, =INF_REG_BASE str r3, [r0, #INF_REG3_OFFSET] |
UART启动。
判断启动完毕后,往用户自定义寄存器3写入特定的数据。写入的地址是0xe010_f00c。该数据就表示启动方式是什么。
s5pv210提供了7个用户自定义寄存器,用户自己自由的使用这些寄存器,来完成一些自己定义的功能。
ldr sp, =0xd0036000 /* end of sram dedicated to u-boot */ sub sp, sp, #12 /* set stack */ mov fp, #0 |
设置SVC的栈。栈顶值是0xd0036000。使用的是内部的SRAM。设置栈以便后面调用C程序。
因为当前整个代码还在sram中运行,此时DDR还未被初始化还不能用。因此此时只能用内部的SRAM。
s5pv210的irom程序中,已经将内部的SRAM的空间进行了分配,每一个部分的功能都已经确定,如对于SVC的栈,空间从0xd003_7780-0xd003_7d80。因此将栈设置在这边空间即可。
bl lowlevel_init /* go setup pll,mux,memory */ |
执行低层次的初始化,设置时钟,初始化DRAM。
4) lowlevel_init
lowlevel_init函数在lowlevel_init.S中。
#include <config.h> #include <version.h> #include <s5pc110.h> #include "smdkc110_val.h" _TEXT_BASE: .word TEXT_BASE |
首先包含了一些头文件,以及定义了一个变量,_TEST_BASE,这个变量的值就是TEXT_BASE。
而TEXT_BASE,是编译的时候,通过编译器的传参传递进来的。这个值表示uboot的代码段的起始地址。
.globl lowlevel_init lowlevel_init: push {lr} |
使用global申明lowlevel_init,表示这是个全局标号,其他程序可以调用这个标号。
将返回地址压栈。因为后面还会调用一些其他函数,就会把目前的lr给覆盖掉,所以需要提前保存。这样,以后才能返回到_start函数中。这里可以看出栈的作用了,保存寄存器值。
/* 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允许多种复位情况,如冷上电(直接上电),热启动(CPU从休眠状态启动)等,这些都属于复位。所以要在复位代码中要去检测复位状态,来判断到底是那种情况。
判断那种复位状态的意义在于,冷上电时DDR是需要初始化才能用的;而热启动复位不需要再次初始化DDR。
这里读取的是RST_STAT寄存器。地址是0xe010_a000。456页。
这个寄存器保存的是由什么引起的复位。程序中检查16位和19位。判断是睡眠模式下唤醒,还是从深层闲置模式下唤醒。
对于210,CPU有以下的工作模式。
/* IO Retention release */ ldr r0, =(ELFIN_CLOCK_POWER_BASE + OTHERS_OFFSET) ldr r1, [r0] ldr r2, =IO_RET_REL orr r1, r1, r2 str r1, [r0] |
在低功耗模式下,将IO进行处理,恢复到工作模式下,应该要将IO恢复回原来的功能。
将OTHERS寄存器的31位,29位,28位置1。将GPIO,MMC,USART的管脚功能恢复成原来的功能。
手册中说明,如果从低功耗状态唤醒,需要将对应位置1,以使原来的PAD工作。硬件会自动将该位清0。
/* Disable Watchdog */ ldr r0, =ELFIN_WATCHDOG_BASE /* 0xE2700000 */ mov r1, #0 str r1, [r0] |
关闭看门狗。
/* 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] |
外部的SRAM初始化。对于SMDKC110开发板,外部接有2M的SRAM,因此需要对SRAM进行初始化,但是对于x210开发板,外部没有接,因此这部分代码是没有用的。
/* 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] |
控制外部的供电模块的管脚。锁存开发板供电控制管脚。这样,就不用一直按着电源键了。
5) 时钟和DRAM初始化
/* 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: |
判断当前是在DRAM中运行还是在内部的SRAM运行。
判断原因:
l 如果从冷启动,那么这时候就会运行在内部SRAM中
l 如果是热启动,那么这时候就会运行在外部DRAM中
如果是在内部的SRAM中运行的话,就需要对时钟和DRAM进行初始化。如果是在DRAM中运行的话,就不需要对时钟和DRAM进行初始化。
读取当前PC值,将读取的值的某些位给清零。然后读取链接脚本的链接地址,也将某些位给清零,比较两个值,如果相等,说明是在DRAM中运行,就不需要进行时钟和DRAM初始化。如果不等,说明是在内部的SRAM中运行,就需要进行时钟和DRAM初始化。
时钟初始化里面很多的参数根据x210_sd.h定义的时钟宏定义来进行设置的。具体是300行到428行。移植时,只需要去更改x210_sd.h中的时钟宏定义参数。
210_sd.h中定义了7类时钟。1000_200_166_133指内核时钟1000M,DRAM为200M,HCLK_DSYS为166M,HCLK_PSYS为133M。后面根据这里定义的宏,设置的参数不一样。
DRAM初始化,代码是在cpu\s5pc11x\s5pc110下的cpu_init.S文件中。代码和裸机开发基本一样,但是有一个寄存器不一样,DMC0_MEMCONFIG_0,裸机中设置为0x20e01323,在uboot中配置为0x30f01313。这个配置不同就导致结果不同。
裸机中的DMC0的256MB内存地址范围是0x20000000-0x2fffffff。
Uboot中DMC0的256MB内存范围地址是0x30000000-0x3fffffff。
内存配置的代码在x210_sd.h的438行到468行。分析的时候要注意条件编译的条件,配置头文件中考虑了不同时钟配置下的内存配置值,这个主要目的是让不同时钟需求的使用都能找到合适自己的内存配置值。
从手册中知道,DMC0允许的地址范围是0x20000000-0x3fffffff。而DMC0的起始地址是可以更改的,通过DMC0_MEMCONFIG_0寄存器进行更改。九鼎开发板上只接了256MB物理内存,因此可以在程序中给这256MB挑选地址范围。
6) 串口初始化
1: /* for UART */ bl uart_asm_init |
串口初始化
/* * 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] |
首先是对UART的0-3的管脚都进行初始化,初始化成具有UART功能。
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 |
对UART进行设置。设置完毕后通过串口发送O。这里使用了ELFIN_UART_CONSOLE_BASE作为串口的首地址。这个地址是在s5pc110.h中定义的。
/* * UART */ #define ELFIN_UART_BASE 0XE2900000 #define ELFIN_UART0_OFFSET 0x0000 #define ELFIN_UART1_OFFSET 0x0400 #define ELFIN_UART2_OFFSET 0x0800 #define ELFIN_UART3_OFFSET 0x0c00 #if defined(CONFIG_SERIAL1) #define ELFIN_UART_CONSOLE_BASE (ELFIN_UART_BASE + ELFIN_UART0_OFFSET) #elif defined(CONFIG_SERIAL2) #define ELFIN_UART_CONSOLE_BASE (ELFIN_UART_BASE + ELFIN_UART1_OFFSET) #elif defined(CONFIG_SERIAL3) #define ELFIN_UART_CONSOLE_BASE (ELFIN_UART_BASE + ELFIN_UART2_OFFSET) #elif defined(CONFIG_SERIAL4) #define ELFIN_UART_CONSOLE_BASE (ELFIN_UART_BASE + ELFIN_UART3_OFFSET) #else #define ELFIN_UART_CONSOLE_BASE (ELFIN_UART_BASE + ELFIN_UART0_OFFSET) #endif |
这里判断使用串口的宏是哪一个,然后设置ELFIN_UART_CONSOLE_BASE的地址值,从而达到设置哪一个串口。
而这个宏是在x210_sd.h中定义的。需要使用哪一个串口,就将对应的宏设置为1。
/* * select serial console configuration */ //#define CONFIG_SERIAL1 1 /* we use UART0 on SMDKC110 */ #define CONFIG_SERIAL3 1 /* we use UART2 on SMDKC110 */ |
bl tzpc_init |
trust zone初始化。和secure有关系的。
#if defined(CONFIG_ONENAND) bl onenandcon_init #endif
#if defined(CONFIG_NAND) /* simple init for NAND */ bl nand_asm_init #endif |
两个宏在x210_sd.h中没有定义,所以不被执行。因为x210开发板上,没有onenand和nand。
/* 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] |
判断复位状态。如果是wakeup,那么要去执行wakeup_reset_pre。
0xe010c300这个寄存器,手册中没有说明。
/* Print 'K' */ ldr r0, =ELFIN_UART_CONSOLE_BASE ldr r1, =0x4b4b4b4b str r1, [r0, #UTXH_OFFSET] pop {pc} |
串口打印K,lowlevel_init.S执行完毕。启动uboot,如果串口打印OK,说明lowlevel_init.S执行成功。
pop {pc},从栈中弹出数据,赋值给PC,也就是之前保存的lr寄存器,所以返回到_start函数中继续执行。
7) 总结:lowlevel_init.S
l 检查复位状态、IO恢复、关看门狗、开发板供电锁存
l 判断程序在内部SRAM还是外部DRAM运行,从而决定是否要初始化时钟和DDR
l 串口初始化并打印’O’,并初始化tzpc
l 打印’K’
8) 回到start.S
/* 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] |
重新设置开发板供电锁存。在lowlevel_init.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 */ |
重新设置栈的位置,设置栈的位置在DRAM中。值为0x33e00000 - 12。也就是uboot代码地址的向下偏移12个字节的地址。。
之前调用lowlevel_init.S前设置过1次栈,那时候DDR尚未初始化,因此程序执行都是在SRAM中,所以在SRAM中分配了一部分内存作为栈。本次因为DDR已经被初始化了,因此要把栈设置在DDR中,所以需要重新设置栈。
再次设置栈的原因是因为DDR已经初始化了,已经有大片内存可以使用了,没必要再把栈放在IRAM中了。而IRAM中内存空间大小有限,栈放在那里要注意不能使用过多的栈,否则栈会溢出。将栈迁移到DDR中也是为了尽可能避免栈使用出现溢出。
_TEXT_PHY_BASE:代码段的物理起始地址。Uboot以后是被放在0x33e00000地址被执行的。
/* 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中。在SRAM中运行,需要将代码拷贝到DDR中。
在lowlevel_init.S也有上述操作,不过那时是判断是否要执行初始化时钟和DDR。而这次判断为了决定是否进行uboot的relocate。
冷启动是:uboot的前一部分(16kb或者8kb)开机自动从SD卡加载到SRAM中运行,uboot的第二部分(其实第二部分是整个uboot)还在SD卡的某个扇区中。此时uboot的第一阶段已经要完成了,在完成之前,要把第二部分加载到DDR中链接地址处(0x33e00000),这个加载过程就是重定位。
如果是热启动,程序已经是拷贝到DDR中,就不在需要拷贝代码,直接跳转到after_copy去执行。
9) 判断启动方式,拷贝代码
#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 |
从地址0xd0037488读取值,判断是否是0xeb20000,是的话,说明从SD卡启动。如果不相等的话,说明从内部iNand启动。
地址0xd0037488这个内存地址在SRAM中,这个地址中的值是IROM代码执行时设置的。IROM代码根据我们实际电路中SD卡在哪个通道中,会将这个地址中的值设置为相应的值。如从SD0通道启动时,这个值是0xEB000000,从SD2通道启动时,这个值是0xEB200000
这个地址保存的是SDMMC的通道号。
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 beq mmcsd_boot cmp r1, #BOOT_NOR beq nor_boot cmp r1, #BOOT_SEC_DEV beq mmcsd_boot |
从0xe010f00c寄存器读值。这个寄存器是210内部用户自定义寄存器。
在之前判断从什么地方启动时,会往这个寄存器写入不同的值(244行到279行)。把这个值读出来,判断从哪里启动,然后调用不同启动函数。
为1 ,说明从ONENAND启动。调用onenand_boot
为2,说明从NAND启动。调用nand_boot
为3,说明从MMCSD启动,调用mmcsd_boot
为4,说明从nor启动,调用nor_boot
为5,说明从uart启动,也调用mmcsd_boot
对于九鼎的开发板,是从MMC\SD启动,所以最终是调用mmcds_boot。
nand_boot: mov r0, #0x1000 bl copy_from_nand b after_copy
onenand_boot: bl onenand_bl2_copy b after_copy |
NAND启动和ONENAND启动的拷贝代码的函数
mmcsd_boot: #if DELETE ldr sp, _TEXT_PHY_BASE sub sp, sp, #12 mov fp, #0 #endif bl movi_bl2_copy b after_copy |
被引掉的代码是设置栈,因为之前设置过栈了,所以这里可以将这代码删掉,九鼎官方在这使用宏来对这段代码不编译。
执行movi_bl2_copy函数,实现代码从MMC\SD拷贝到DDR。这个函数在cpu\s5pc11x中的movi.c文件中。是一个用c写的函数。
typedef u32(*copy_sd_mmc_to_mem) (u32 channel, u32 start_block, u16 block_size, u32 *trg, u32 init); |
定义函数指针,以便使用IROM提供的MMC\SD拷贝函数。
void movi_bl2_copy(void) { ulong ch; #if defined(CONFIG_EVT1) ch = *(volatile u32 *)(0xD0037488); 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 |
从0xd0037488地址读出数据,这个地址是IROM代码执行时保存外部SD\MMC的全局变量的地址。表示外部用SD\MMC启动的通道号。然后定义一个函数指针变量copy_bl2,指向地址0xd0037f98的数据,地址0xd0037f98的数据保存的是IROM实现的SD\MMC数据拷贝函数的地址。
u32 ret; if (ch == 0xEB000000) { ret = copy_bl2(0, MOVI_BL2_POS, MOVI_BL2_BLKCNT, CFG_PHY_UBOOT_BASE, 0);
#if defined(CONFIG_SECURE_BOOT) /* do security check */ rv = Check_Signature( (SecureBoot_CTX *)SECURE_BOOT_CONTEXT_ADDR, (unsigned char *)CFG_PHY_UBOOT_BASE, (1024*512-128), (unsigned char *)(CFG_PHY_UBOOT_BASE+(1024*512-128)), 128 ); if (rv != 0){ while(1); } #endif } else if (ch == 0xEB200000) { ret = copy_bl2(2, MOVI_BL2_POS, MOVI_BL2_BLKCNT, CFG_PHY_UBOOT_BASE, 0); |
判断启动通道,对于九鼎开发板,如果从iNand启动,ch值为0xeb000000,那么执行的拷贝函数的通道号是0。如果从SD卡启动,ch值为0xeb200000,那么执行的拷贝函数的通道号是2。
对于MOVI_BL2_POS参数(起始地址的块数)。蓝色部分是宏条件真的部分,该值是蓝色部分后面定义的。
对于eFUSE_SIZE,为1024, 占用2个块,uboot保留该区域。
MOVI_BLKSIZE,为512。对于iNand、SD卡,每块的大小是512字节。
MOVI_BL1_BLKCNT为,8*1024 / 512 = 16。BL1的大小8K,占用16个块。
SS_SIZE为8*1024,MOVI_BLKSIZE为512
MOVI_ENV_BLKCNTT为, 0x4000 / 512 = 32。 环境变量的大小是16K,占用32个块。
CFG_ENV_SIZE为0x4000,在x210_sd.h中定义
所以最终MOVI_BL2_POS的值为1024/512 + 16 + 32 = 50。即拷贝SD\MMC的块地址是第50块。
MOVI_BL2_BLKCNT= 512*1024 / 512 = 1024。所以拷贝的块数目是1024块,总共拷贝的大小就是512KB。
CFG_PHY_UBOOT_BASE为拷贝到DDR的内存地址,为0x33e00000。最后一个参数0。
上述就实现了将SD\MMC的49块(0开始)开始的512K大小内容拷贝到DDR的0x33e00000去。
SD卡中的分布
起始块(0开始) | 空间 | 块大小 |
0 | eFUSE, | 2个块 |
2 | BL1, | 16个块 |
18 | ENV, 环境变量 | 32个块 |
50 | BL2, | 1024个块 |
但实际上,烧录uboot到SD卡时,只给eFUSE分配了一个块,因此BL1从块1开始放置,ENV从块17开始放置,BL2从块49开始放置。
uboot编译后,需要使用sd_fusing.sh脚本烧写uboot,该脚本中有定义BL1和BL2的块地址。也就是uboot_position这个变量的值。
nor_boot: bl read_hword b after_copy |
Nor启动
10) 页表建立
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 |
代码拷贝完毕后,需要设置页表,这样开始MMU后,才可以虚拟地址映射。
先设置访问域。将16个域都设置为0b11,不进行访问权限检查。
设置TTB,通过处理得到TTB的值,然后写入到cp15的c2寄存器。这里的处理,其实就是将页表项的内容,放置在uboot地址向下的一片区域中。
TTB(translation table base),转换表基地址。转换表是建立一套虚拟地址映射的关键。转换表分2部分,分索引和表项。表索引对应虚拟地址,表项对应物理地址。一对表索引和表项构成一个转换表单元,能够对一个内存块进行虚拟地址转换。(映射中规定了内存映射和管理是以块为单位的,置于块有多大,要看你的MMU的支持和程序的配置,ARM中支持3种块大小,细表1KB,粗表4KB,段1MB)。真正的转换表就是由若干个转换表单元构成的,每个单元负责1个内存块,总体的转换表负责整个内存空间(0-4G)的映射。
整个建立虚拟地址映射的主要工作就是建立这张转换表。
转换表放置在内存中的,放置时要求其实地址在内存中要xx位对齐。转换表不需要软件干涉使用,而是将基地址TTB设置到CP15的c2寄存器中,MMU工作时会自动去查转换表。
Uboot使用简单的虚拟地址映射,使用段映射,而且是一级映射。
映射转换表:在lowlevel_init.S的593行。
宏观上理解转换表:整个转换表可以看做是一个unsigned int类型的数组,数组中的一个元素就是一个表索引和表项的单元。数组中的元素值就是表项,数组的下标就是表索引。
ARM的段式映射长度为1MB,因此一个映射单元只能管1MB内存,那整个4GB范围内需要4G/1MB=4096个映射单元。也就说这个数组的元素个数是4096。实际上依次单个处理这4096个单元,而是把4096个分成了几部分。然后每部分用for循环做相同的处理。
Uboot中的映射了:
虚拟地址 | 物理地址 | 大小 | 地址空间 |
0x0-0x0FFF_FFFF | 0x0-0x0FFF_FFFF | 256MB | 0-256M |
0x1000_0000-0x1FFF_FFFF | 0 | 256MB | 256M-512M |
0x2000_0000-0x5FFF_FFFF | 0x2000_0000-0x5FFF_FFFF | 1GB | 512M-1.5G |
0x6000_0000-0x7FFF_FFFF | 0 | 512MB | 1.5G-2G |
0x8000_0000-0xAFFF_FFFF | 0x8000_0000-0xAFFF_FFFF | 768MB | 2G-2.75G |
0xB000_0000-0xBFFF_FFFF | 0xB000_0000-0xBFFF_FFFF | 256MB | 2.75G-3G |
0xC000_0000-0xCFFF_FFFF | 0x3000_0000-0x3FFF_FFFF | 256MB | 3G-3.25G 有映射 |
0xC000_0000-0XFFFF_FFFF | 0xC000_0000-0XFFFF_FFFF | 768MB | 3.25G-4G |
DRAM有效范围:
DMC0: 0x30000000-0x3FFFFFFF
DMC1: 0x40000000-0x4FFFFFFF
虚拟地址映射只是把虚拟地址的c0000000开头的256MB映射到了DMC0的30000000开头的256MB物理内存上去了。其他的虚拟地址空间根本没动,还是原样映射的。
为什么配置时将链接地址设置为c3e00000,因为这个地址将来会被映射到33e00000这个物理地址。
映射第一段:
.section .mmudata, "a" .align 14 // the following alignment creates the mmu table at address 0x4000. .globl mmu_table mmu_table: .set __base,0 // Access for iRAM .rept 0x100 FL_SECTION_ENTRY __base,3,0,0,0 .set __base,__base+1 .endr |
将页表,放置在.mmudata段,并且将该段的首地址mmu_table通过global定义为全局标号,这样,外部可以调用这个mmu_table,从而获取到页表的首地址。
汇编中使用 .rept 循环次数 .endr来当循环用,上面的代码,循环256次,中间的是循环代码。循环256次,建立了256个表项,每个表项1M空间,所以总共256M空间。
.set __base ,0 意思是设置变量__base为0。
建立第一个256M的映射。不开启cache,不使用write buffer。
0-10000000 0-10000000 256MB
循环中用的FL_SECTION_ENTTY 是一个宏,汇编中使用 .macro 定义。
/* form a first-level 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) .endm |
这个宏后面有5个参数。利用这5个参数来构造一个32位的数,该数就是虚拟地址对物理地址的转换项。
Base为转换物理的基地址(左移20位,刚好是1M,说明每一段是1MB空间),ap是属于的域,d为 ,c是表示是否开启cache, b是是否时能write buffer。
映射第二段:
// Not Allowed .rept 0x200 - 0x100 .word 0x00000000 .endr |
第二个定义256MB空间,值都为0。也就说这块地址不能使用。
虚拟地址范围:0x1000_0000-0x1fff_ffff。
映射第三段:
.set __base,0x200 // should be accessed .rept 0x600 - 0x200 FL_SECTION_ENTRY __base,3,0,1,1 .set __base,__base+1 .endr |
定义512M空间。循环512次,建立512个表项。
虚拟地址范围:0x 2000_0000-0x 5fff_ffff。
映射的物理地址 : 0x 2000_0000-0x 5fff_ffff,包括了DMC0和DMC1的地址空间,也就是为DMC0和DMC1的空间建立了映射表。
开启cache,使用write buffer。
映射第四段:
.rept 0x800 - 0x600 .word 0x00000000 .endr |
定义512MB空间,循环512次,建立512个表项,值都为0。也就说这块地址不能使用。
虚拟地址范围:0x6000_0000-0x7FFF_FFFF。
映射第5段:
.set __base,0x800 // should be accessed .rept 0xb00 - 0x800 FL_SECTION_ENTRY __base,3,0,0,0 .set __base,__base+1 .endr |
定义768M空间。循环768次,建立768个表项。
虚拟地址范围:0x8000_0000-0xAFFF_FFFF
映射的物理地址 : 0x8000_0000-0xAFFF_FFFF
不开启cache,不使用write buffer。
映射第6段:
.set __base,0xB00 .rept 0xc00 - 0xb00 FL_SECTION_ENTRY __base,3,0,0,0 .set __base,__base+1 .endr |
定义256M空间。循环256次,建立256个表项。
虚拟地址范围:0xB000_0000-0xBFFF_FFFF
映射的物理地址 : 0xB000_0000-0xBFFF_FFFF
不开启cache,不使用write buffer。
映射第七段:
.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 |
定义256M空间。循环256次,建立256个表项。
虚拟地址范围:0xC000_0000-0xCFFF_FFFF
映射的物理地址 : 0x3000_0000-0x3FFF_FFFF, (DMC0的有效区域,也就是外部DRAM1的区域)
开启cache,使用write buffer。
将内存的物理地址0x3000_0000-0x3FFF_FFFF映射到虚拟地址0xC000_0000-0xCFFF_FFFF
映射第八段
// access is not allowed. @.rept 0xD00 - 0xC80 @.word 0x00000000 @.endr |
定义 地址 0xc800_0000 – 0xd000_0000这片空间不能使用。
映射第九段
.set __base,0xD00 // 1:1 mapping for debugging with non-cacheable .rept 0x1000 - 0xD00 FL_SECTION_ENTRY __base,3,0,0,0 .set __base,__base+1 .endr |
定义768M空间。循环768次,建立768个表项。
虚拟地址范围:0xC000_0000-0XFFFF_FFFF
映射的物理地址 : 0xC000_0000-0XFFFF_FFFF
不开启cache,不使用write buffer。
注释也说明了地址映射的关系。
/* * MMU Table for SMDKC110 * 0x0000_0000 -- 0xBFFF_FFFF => Not Allowed * 0xB000_0000 -- 0xB7FF_FFFF => A:0xB000_0000 -- 0xB7FF_FFFF * 0xC000_0000 -- 0xC7FF_FFFF => A:0x3000_0000 -- 0x37FF_FFFF * 0xC800_0000 -- 0xDFFF_FFFF => Not Allowed * 0xE000_0000 -- 0xFFFF_FFFF => A:0xE000_0000 -- 0XFFFF_FFFF */ |
通过查看反汇编dis文件,知道MMU映射表的地址是0xc3e5c000,但是在dis文件中却找不到这个地址对应的值。
原来dis反汇编文件,没有包括全部的数据段。要查看完成的数据段内容,需要去看bin文件。
使用winhex工具查看bin文件的内容,因为链接地址是0xc3e00000。所以偏移地址是0x5c000。查看这个地址的值,发现这一块区域的值就是MMU映射表的内容。
对于第一个数据(0005c000) : 0x0000_0c12, 对应下面循环的第一次循环产生的数据。
mmu_table: .set __base,0 // Access for iRAM .rept 0x100 FL_SECTION_ENTRY __base,3,0,0,0 .set __base,__base+1 .endr |
其他的分析方法,是一样的。
11) 开启MMU
/* 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 |
使能MMU,将c1寄存器的bit-0置1,因为这一位控制mmu的开关。
之后使用的地址就全是虚拟地址了。
12) 设置栈
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中设置过一次栈了,但是本次设置栈的目的是将栈放在比较合适(安全,紧凑而不浪费内存)的地方。
将栈放在uboot的起始地址往上的2M-0x1000地址处。
13) 清bss
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 |
清bss段。_bss_start和_bss_end是start.S中定义的标号,而值是在链接脚本中定义。
链接脚本中
14) start_armboot
清理bss段后,就跳去执行_start_armboot,其实就是去执行start_armboot函数。这个函数在lib_arm/board.c中,这个是一个c语言实现的函数。这个就是uboot的第二阶段。
使用远跳转,跳转到DDR中的start_armboot函数执行。至此,第一阶段uboot执行完毕。第二阶段uboot开始执行。
ldr pc, _start_armboot _start_armboot: .word start_armboot |
二、 总结
总结:uboot的第一阶段所作工作
1. 构建异常向量表
2. 设置CPU为SVC模式
3. 执行lowlevel.S, 检查复位状态,恢复IO功能。关看门狗,开发板供电电路置锁,检检查运行地址,从而决定是否要时钟和DDR初始化,串口初始化打印OK
4. 重定位,代码拷贝
5. 建立映射表并开启MMU
6. 设置栈,跳转到第二阶段