以下内容源于朱有鹏嵌入式课程的学习与整理,如有侵权请告知删除。
一、总结
1、关于阶段的定义
第一阶段,即在内部SRAM运行的阶段,简单地理解为汇编阶段。此阶段主要涉及start.S文件,在cpu/s5pc11x/目录下。第一阶段以ldr pc _start_armboot为结束。
第二阶段,即在DDR中运行的阶段,简单地理解为C语言阶段。此阶段主要涉及start_armboot函数,在uboot/lib_arm/board.c文件的444~908行。
2、第一阶段完成的任务
- 异常向量表的实现;
- 设置进入特权模式,即SVC模式;
- 检查恢复状态;
- IO状态恢复;
- 关看门狗;
- 一些与SRAM、SROM相关的GPIO设置;
- 开发板的供电锁存;
- 时钟的初始化;
- DDR的初始化;
- 串口的初始化;
- 重新设置栈空间;
- uboot的重定位;
- 转换表的建立;
- 使能MMU。
可见uboot的第一阶段初始化了SoC内部的一些部件,初始化DDR并且重定位。
uboot被分割成两部分,即前8kb和整个ubooot,则前8k内容肯定包括第一阶段的操作任务,其中很重要的操作有重定位。
二、start.S文件的解析
uboot的链接脚本的分析见博文uboot的链接脚本u-boot.lds分析。
uboot中整个程序的入口取决于链接脚本中ENTRY声明的地方。uboot的链接脚本中有一句代码“ ENTRY(_start)”,因此_start符号所在的文件就是起始文件,所处的位置就是起始位置。通过使用SI工具查找符号“_start”,发现其所在文件是cpu\s5pc11x\start.S文件。
//省略其他代码 #if defined(CONFIG_EVT1) && !defined(CONFIG_FUSED).word 0x2000.word 0x0.word 0x0.word 0x0 #endif.globl _start //入口在这里 _start: b resetldr pc, _undefined_instructionldr pc, _software_interruptldr pc, _prefetch_abortldr pc, _data_abortldr pc, _not_usedldr pc, _irqldr pc, _fiq //省略其他代码
1、start.S所包含的头文件
#include <config.h> #include <version.h> #if defined(CONFIG_ENABLE_MMU) #include <asm/proc/domain.h> #endif #include <regs.h>
(1)config.h文件在mkconfig脚本中生成,此文件内容为“#include <configs/x210.h>”。
(2)version.h文件内容是“ #include "version_autogenerated.h" ”。
version_autogenerated.h文件内容为“ #define U_BOOT_VERSION "U-Boot 1.3.4" ”。
version_autogenerated.h这个文件是在主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 $@
(3)由于定义了宏CONFIG_ENABLE_MMU,因此包含asm/proc/domain.h,实际包含include\asm-arm\proc-arm\domain.h文件。
2、启动代码的16字节头部
#if defined(CONFIG_EVT1) && !defined(CONFIG_FUSED).word 0x2000.word 0x0.word 0x0.word 0x0 #endif
(1)arm7和arm9的arm指令集,一个字类型是32bit,即4个字节。
(2)此段代码用16个字节填充占位(这些数字貌似可以任意?字节内容后续计算重新填充?这里填充的原因是SD卡需要16字节的校验?)
(3).word是arm汇编的伪指令,表示“当前地址(某个内存地址)的值(这个内存地址所对应的存储单元所存储的数值)为XX”。比如,.word 0x2000表示当前地址的值为0x2000(注意,不是说当前地址编号是0x2000,而是说当前地址所对应的存储单元存储的数值是0x2000),.word _start 表示当前地址的值为_start。
3、构建异常向量表
.globl _start _start: b resetldr pc, _undefined_instructionldr pc, _software_interruptldr pc, _prefetch_abortldr pc, _data_abortldr pc, _not_usedldr pc, _irqldr 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)但此向量表只是虚有其表,并未做非常细致的异常处理。
(3)复位异常处理代码是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,r0msr cpsr_c, #0xd3 @ I & F disable, Mode: 0x13 - SVC 0b1101 0011
注意里面对cpsr这个寄存器的赋值为0b1101_0011。关于cpsr寄存器的位含义,见博客:ARM通识——ARM的7种基本工作模式、37个通用寄存器
(4)上图中的最后一句是让内存16字节对齐,如果不对齐,用0xdeadbeef这个数字填充。
4、一些地址值
_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)_TEXT_BASE(4字节)这个内存地址存放的内容为TEXT_BASE(即0xc3e00000)。由uboot源码——链接脚本u-boot.lds分析可知,这个TEXT_BASE就是链接地址。
(2)_TEXT_PHY_BASE(4字节)这个内存地址存放的内容为CFG_PHY_UBOOT_BASE。CFG_PHY_UBOOT_BASE定义在x210_sd.h中,它的值为 MEMORY_BASE_ADDRESS + 0x3e00000,而MEMORY_BASE_ADDRESS的值为0x30000000,因此CFG_PHY_UBOOT_BASE为0x33e00000。其实它就是uboot在DDR中的物理地址。
(3)结合后续的第12点配置MMU,可以明白为什么配置时将uboot的链接地址设置为0xc3e00000,因为它被映射成0x33e00000这个物理地址,而这个物理地址就是uboot在内存中开始存放的地方。参考uboot中的虚拟地址映射_天糊土的博客。
(4)总结,即标签表示地址,.word后面的表示此内存地址中存储的值。
比如_bss_end: .word _end表示地址_bss_end上存放的值是_end。
5、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,r0msr cpsr_c, #0xd3 @ I & F disable, Mode: 0x13 - SVC 0b1101 0011
(1)因为0xd3=1101 0011,则cpu设置为SVC模式、arm状态,禁止FIQ、IRQ中断。
(2)整个uboot工作时,cpu一直处于SVC模式。
6、CPU的初始化(设置l1,l2cache和MMU等内容)
cpu_init_crit://此处为条件编译语句,删除了部分代码bl disable_l2cachebl set_l2cache_auxctrl_cyclebl enable_l2cache/** Invalidate L1 I/D*/mov r0, #0 @ set up for MCRmcr p15, 0, r0, c8, c7, 0 @ invalidate TLBsmcr p15, 0, r0, c7, c5, 0 @ invalidate icache/** disable MMU stuff and caches*/mrc p15, 0, r0, c1, c0, 0bic r0, r0, #0x00002000 @ clear bits 13 (--V-) 0010 0000 0000 0000bic r0, r0, #0x00000007 @ clear bits 2:0 (-CAM) 0000 0000 0000 0111orr r0, r0, #0x00000002 @ set bit 1 (--A-) Align 0000 0000 0000 0010orr r0, r0, #0x00000800 @ set bit 12 (Z---) BTB 0000 1000 0000 0000@ 这里明明是bit 11,怎么说是bit 12呢?mcr p15, 0, r0, c1, c0, 0
(1)关于bic和orr指令的介绍,见博客汇编指令——bic(位清除)、orr(位或)。
(2)关于代码的细节内容,待写。
7、识别并暂存启动介质选择
/* Read booting information */ldr r0, =PRO_ID_BASEldr r1, [r0,#OMR_OFFSET]bic r2, r1, #0xffffffc1
(1)启动介质由SoC的OM0:OM5这6个引脚的高低电平决定。
(2) 由#define PRO_ID_BASE 0xE0000000、#define OMR_OFFSET 0x04得到寄存器的地址为0xE000 0004。这个寄存器会根据OM引脚状况,硬件自动设置值。
(3)上面三行代码后,r2存储了一个数字,下面通过该数字进行启动介质的判断。
//条件编译,省略部分代码/* NAND BOOT */cmp r2, #0x0 @ 512B 4-cyclemoveq r3, #BOOT_NANDcmp r2, #0x2 @ 2KB 5-cyclemoveq r3, #BOOT_NANDcmp r2, #0x4 @ 4KB 5-cycle 8-bit ECCmoveq r3, #BOOT_NANDcmp r2, #0x6 @ 4KB 5-cycle 16-bit ECCmoveq r3, #BOOT_NANDcmp r2, #0x8 @ OneNAND Muxmoveq r3, #BOOT_ONENAND/* SD/MMC BOOT */ //sd卡启动cmp r2, #0xcmoveq r3, #BOOT_MMCSD /* NOR BOOT */cmp r2, #0x14moveq r3, #BOOT_NOR//条件编译,省略部分代码/* Uart BOOTONG failed */cmp r2, #(0x1<<4)moveq r3, #BOOT_SEC_DEV ldr r0, =INF_REG_BASEstr r3, [r0, #INF_REG3_OFFSET]
(4)通过判断r2中的值,来确定是从哪里启动的。
- 如果r2中的值为0xc,则从SD卡启动,然后把#BOOT_MMCSD(#define BOOT_MMCSD 0x3)赋给r3。
- 注意moveq是否执行,得看前一句的cmp比较的结果是否相同,相同则执行,不同则moveq不会执行。
(5)最后两行中,因为有#define INF_REG_BASE 0xE010F000、#define INF_REG3_OFFSET 0x0c,则合成寄存器的地址为0xE010F00C,然后把r3中的值放入这个寄存器中。
8、第一次设置栈
/** 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-12mov fp, #0bl lowlevel_init /* go setup pll,mux,memory */
(1)这次设置栈是在SRAM中设置的,因为当前整个代码还在SRAM中运行,此时DDR还未被初始化还不能用。
(2)之所以要初始化栈,是因为接下来调用的lowlevel_init函数中还要调用其他函数。bl跳转时只会将返回地址存储到LR中,但是只有一个LR,所以在lowlevel_init函数中调用其他函数之前要先将LR入栈,否则函数返回时第一层的返回地址(从lowlevel_init函数退出时)就被覆盖掉了。
(3)栈地址0xd0036000是自己指定的,指定的原则就是这块空间只给栈用,不会被别人占用。
(4)关于sub指令的解释,见ARM的汇编指令【各种指令的解释】。
9、lowlevel_init函数
lowlevel_init函数在uboot\board\samsung\x210\lowlevel_init.S文件中。
(1)检查复位状态
/* check reset status */ldr r0, =(ELFIN_CLOCK_POWER_BASE+RST_STAT_OFFSET)ldr r1, [r0]bic r1, r1, #0xfff6ffffcmp r1, #0x10000beq wakeup_reset_precmp r1, #0x80000beq wakeup_reset_from_didle
因为复杂CPU支持多种复位状态(冷上电、休眠复位等),因此在复位代码中检查复位状态,判断到底是哪一种。冷上电时DDR需要初始化,而休眠状态下复位不需要再次初始化DDR。
(2)IO状态恢复
/* IO Retention release */ //IO状态恢复ldr r0, =(ELFIN_CLOCK_POWER_BASE + OTHERS_OFFSET)ldr r1, [r0]ldr r2, =IO_RET_RELorr r1, r1, r2str r1, [r0]
(3)关看门狗
/* Disable Watchdog */ //关闭看门狗ldr r0, =ELFIN_WATCHDOG_BASE /* 0xE2700000 */mov r1, #0str r1, [r0]
(4)SRAM、SROM相关的GPIO设置
/* SRAM(2MB) init for SMDKC110 *//* GPJ1 SROM_ADDR_16to21 */ldr r0, =ELFIN_GPIO_BASEldr r1, [r0, #GPJ1CON_OFFSET]bic r1, r1, #0xFFFFFFldr r2, =0x444444orr r1, r1, r2str r1, [r0, #GPJ1CON_OFFSET]ldr r1, [r0, #GPJ1PUD_OFFSET]ldr r2, =0x3ffbic r1, r1, r2str 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, r2str r1, [r0, #GPJ4CON_OFFSET]ldr r1, [r0, #GPJ4PUD_OFFSET]ldr r2, =(0x3<<8)bic r1, r1, r2str r1, [r0, #GPJ4PUD_OFFSET]/* CS0 - 16bit sram, enable nBE, Byte base address */ldr r0, =ELFIN_SROM_BASE /* 0xE8000000 */mov r1, #0x1str 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]
(6)判断当前代码执行位置(判断在SRAM还是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, =0xff000fffbic 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_init1: //如果是休眠恢复,则忽略上面两个操作,进入这里/* for UART */bl uart_asm_init //初始化串口打印“O”bl tzpc_init //trust zone的初始化
为什么要判断?因为BL1在SRAM中有一份,在DDR中也有一份。如果是冷启动,则当前代码是在SRAM中运行的BL1;如果是低功耗状态的复位,则当前代码是在DDR中运行的。判断的目的是指导后面代码的运行。比如在lowlevel_init.S中判定当前代码的运行地址,就是为了确定是否执行时钟初始化和初始化DDR的代码。如果当前代码是在SRAM中,说明是冷启动,则需要初始化时钟和DDR;如果当前代码是在DDR中,说明是热启动,则时钟和DDR都不用再次初始化。
(7)时钟初始化函数:system_clock_init
代码见博客:uboot中系统时钟初始化函数:system_clock_init
(8)内存的初始化:mem_ctrl_asm_init
- 该函数在uboot/cpu/s5pc11x/s5pc110文件中。该函数和裸机中初始化DDR代码是一样的,实际裸机中初始化DDR的代码就是从这里抄的。见博客SDRAM——X210的SDRAM的初始化_天糊土的博客
- uboot配置值中有一个和裸机中讲的不一样,即DMC0_MEMCONFIG_0。它在裸机中配置值为0x20E01323,在uboot中配置为0x30F01323。
- 在裸机中DMC0的256 MB内存地址范围是0x20000000-0x2FFFFFFF;在uboot中DMC0的256MB内存地址范围为0x30000000-0x3FFFFFFF。
- DMC0上允许的地址范围是20000000-3FFFFFFF(一共是512MB),而实际只接了256MB物理内存,SoC允许我们给这256MB挑选地址范围。
- 在uboot中,可用的物理地址范围为:0x30000000-0x4FFFFFFF,一共512MB,其中30000000-3FFFFFFF为DMC0,40000000-4FFFFFFF为DMC1。
- 注意条件编译的条件,配置头文件中考虑了不同时钟配置下的内存配置值,主要目的是让不同时钟需求的客户都能找到合适自己的内存配置值。
(9)串口初始化:打印一个“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 UARTldr r0, =ELFIN_GPIO_BASEldr r1, =0x22222222str r1, [r0, #GPA0CON_OFFSET]ldr r1, =0x2222str r1, [r0, #GPA1CON_OFFSET]// HP V210 use. SMDK not use. #if defined(CONFIG_VOGUES)ldr r1, =0x100str r1, [r0, #GPC0CON_OFFSET]ldr r1, =0x4str r1, [r0, #GPC0DAT_OFFSET] #endifldr r0, =ELFIN_UART_CONSOLE_BASE @0xEC000000mov r1, #0x0str r1, [r0, #UFCON_OFFSET]str r1, [r0, #UMCON_OFFSET]mov r1, #0x3str r1, [r0, #ULCON_OFFSET]ldr r1, =0x3c5str r1, [r0, #UCON_OFFSET]ldr r1, =UART_UBRDIV_VALstr r1, [r0, #UBRDIV_OFFSET]ldr r1, =UART_UDIVSLOT_VALstr r1, [r0, #UDIVSLOT_OFFSET]ldr r1, =0x4f4f4f4fstr r1, [r0, #UTXH_OFFSET] @'O'mov pc, lr
(10)pop {pc}返回前通过串口打印“K”
/* check reset status */ldr r0, =(ELFIN_CLOCK_POWER_BASE+RST_STAT_OFFSET)ldr r1, [r0]bic r1, r1, #0xfffeffffcmp r1, #0x10000beq wakeup_reset_pre/* ABB disable */ldr r0, =0xE010C300orr r1, r1, #(0x1<<23)str r1, [r0]/* Print 'K' */ //打印一个字符Kldr r0, =ELFIN_UART_CONSOLE_BASEldr r1, =0x4b4b4b4bstr r1, [r0, #UTXH_OFFSET]pop {pc}
至此,lowlevel_init函数结束。
10、第二次设置栈
/** Go setup Memory and board specific bits prior to relocation.*/ //第一次设置栈(在SRAM中)ldr sp, =0xd0036000 /* end of sram dedicated to u-boot */sub sp, sp, #12 /* set stack */mov fp, #0bl lowlevel_init /* go setup pll,mux,memory *//* 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, #12mov fp, #0 /* no previous frame, so fp=0 *//* 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.*/ //判断当前位置是否在DDR中,不在的话需要重定位ldr r0, =0xff000fffbic 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)第一次设置栈,是在调用lowlevel_init函数前。那时程序在SRAM中执行,所以在SRAM中分配了一部分内存作为栈。
(2)第二次因为DDR已经被初始化,因此要把栈挪移到DDR中,所以要重新设置栈。实际设置的栈的地址是33E00000,刚好在uboot的代码段的下面紧挨着。因为是满减栈,所以栈向下增长。注意uboot基地址在0x33e00000,向上增长。
(3)为什么要再次设置栈?因为DDR已经初始化了,已经有大片内存可以用了,没必要再把栈放在SRAM中;原来SRAM中内存大小空间有限,栈放在那里要注意不能使用过多,否则栈会溢出。我们及时将栈迁移到DDR中也是为了尽可能避免使用栈时的诸多不便。
(4)奇怪,这里为何还有一个电源锁存,不是在lowlevel_init函数中完成了么?
11、再次判断运行地址是在SRAM中还是DDR中
(1)上次判断运行地址是在SRAM中还是在DDR中,是为了决定是否要执行初始化时钟和DDR的代码,本次判断是为了决定是否进行uboot的重定位。
- 冷启动时,uboot的前一部分(16kb或者8kb)开机自动从SD卡加载到SRAM中运行,uboot的第二部分(其实第二部分是整个uboot)还躺在SD卡的某个扇区开头的N个扇区中。此时uboot的第一阶段即将结束(第一阶段该做的事基本做完了),结束之前要把第二部分加载到DDR中链接地址处(0x33e00000),这个加载过程就叫重定位。
(2)如下图。
- D0037488这个内存地址在SRAM中,这个地址中的值是被硬件自动设置的。硬件根据我们实际电路中SD卡在哪个通道中,会将这个地址中的值设置为相应的数字。譬如我们从SD0通道启动时,这个值为EB000000;从SD2通道启动时,这个值为EB200000。
- 我们确定是从MMCSD启动,因此最终跳转到mmcsd_boot函数中去执行重定位动作,即把SD卡中相应的内容复制到内存中。
- 真正的重定位是通过调用movi_bl2_copy函数完成的,在uboot/cpu/s5pc11x/movi.c中。此函数包含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)。
12、配置MMU
- MMU(memory management unit),内存管理单元。它实际上是SOC中一个硬件单元,它的主要功能就是实现虚拟地址到物理地址的映射。
- MMU单片在CP15协处理器中进行控制,也就是说要操控MMU进行虚拟地址映射,方法就是对cp15协处理器的寄存器进行编程。
13、第三次设置栈
(1)这次设置栈还是在DDR中,之前虽然已经在DDR中设置过一次栈了,但是本次设置栈的目的是将栈放在比较合适(安全,紧凑而不浪费内存)的地方。
(2)我们实际将栈设置在uboot起始地址上方2MB处,这样安全的栈空间大小是:2MB-uboot大小-0x1000=1.8MB左右。这个空间既没有太浪费内存,又足够安全。
14、清理bss
注意表示bss段的开头和结尾地址的符号是从链接脚本u-boot.lds得来的。
15、跳转到start_armboot函数
- start_armboot函数在uboot/lib_arm/board.c中,这是一个C语言实现的函数。这个函数就是uboot的第二阶段。
- “ldr pc _start_armboot”这句代码的作用就是将uboot第二阶段执行的函数的地址传给pc,实际上就是使用一个远跳转直接跳转到DDR中的第二阶段开始地址处。
- 远跳转的含义就是这句话加载的地址和当前运行地址无关,而和链接地址有关。因此这个远跳转可以实现从SRAM中的第一阶段跳转到DDR中的第二阶段。
- 这里这个远跳转就是uboot第一阶段和第二阶段的分界线。