U-Boot启动过程分析及移植

630阅读 0评论2013-10-26 wenliang1387
分类:LINUX

 

BootLoader的总目标是正确调用内核的执行,由于大部分的BoorLoader都依赖于CPU的体系结构。因此大部分的BootLoader都分为两个步骤启动。依赖于CPU体系结构(如设备初始化等)的代码都放在stage1。而stage2一般使用C语言实现,能够实现更加复杂的功能,代码的可移植性也提高。

stage1包含以下步骤:

1. 硬件设备初始化

2. 为加载stage2准备RAM空间

3. 拷贝stage2的代码到RAM空间

4. 设置好堆栈

5. 跳转到stage2C语言入口点

stage2一般包括以下步骤:

1. 初始化本阶段要使用的硬件设备

2. 检测系统内存映射

3. kernel映射和根文件系统映射从Flash读到RAM空间中

4. 为内核设置启动参数

5. 调用内核

1. Stage1cpu/arm920t/start.S

这个汇编程序是U-Boot的入口程序,开头就是复位向量的代码

U-Boot启动代码流程图

start.S代码分析:

1)主要实现复位向量,设置异常向量表。

_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 //中断向量

2)复位启动子程序,将CPU设置到SVC模式

/* the actual reset code */
reset: //复位启动子程序
       /* 设置CPUSVC32模式 */
       mrs r0,cpsr
       bic r0,r0,#0x1f ;;位清除,将某些位的值置0r0 = r0 AND ( !0x1f)
       orr r0,r0,#0xd3 ;;逻辑或,将r0与立即数进行逻辑或,放在r0中(第一个)
       msr cpsr,r0

3)关闭看门狗

/* 关闭看门狗 */
 /* turn off the watchdog */
#if defined(CONFIG_S3C2400)
# define pWTCON 0x15300000
# define INTMSK 0x14400008 /* Interupt-Controller base addresses */
# define CLKDIVN 0x14800014 /* clock divisor register */
#elif defined(CONFIG_S3C2410)
# define pWTCON 0x53000000
# define INTMSK 0x4A000008 /* Interupt-Controller base addresses */
# define INTSUBMSK 0x4A00001C
# define CLKDIVN 0x4C000014 /* clock divisor register */
#endif

#if defined(CONFIG_S3C2400) || defined(CONFIG_S3C2410)
    ldr r0, =pWTCON
    mov r1, #0x0
    str r1, [r0]

4)禁止所有中断,设置CPU频率

/* 禁止所有中断和设置CPU频率 */
    /*
     * mask all IRQs by setting all bits in the INTMR - default
     */
    mov r1, #0xffffffff
    ldr r0, =INTMSK
    str r1, [r0]
# if defined(CONFIG_S3C2410)
    ldr r1, =0x3ff
    ldr r0, =INTSUBMSK
    str r1, [r0]
# endif

    /* FCLK:HCLK:PCLK = 1:2:4 */ ;;FCLK用于CPUHCLK用于AHBPCLK用于APB
    /* default FCLK is 120 MHz ! */
    ldr r0, =CLKDIVN ;;根据硬件手册来设置CLKDIVN寄存器
    mov r1, #3 ;;用户手册的推荐值
    str r1, [r0]
#endif /* CONFIG_S3C2400 || CONFIG_S3C2410 */

5)系统重启的时候执行的初始化代码,而不是系统热复位(从RAM中执行)的时候

/*
* we do sys-critical inits only at reboot,
* not when booting from 
*/
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
    bl cpu_init_crit ;;跳转去初始化CPU
#endif
;;#ifdef CONFIG_INIT_CRITICAL 原文中的,估计是1.1.16版本的
;; bl cpu_init_crit
;;#endif

6CPURAM两个关键的初始化子程序

函数一:/* 初始化CPU */
cpu_init_crit:
    /*
     * flush v4 I/D caches 设置CP15
     */
    mov r0, #0
    mcr p15, 0, r0, c7, c7, 0 /* flush v3/v4 cache */ ;;使I/D cache失效:将寄存器r0的数据传送到协处理器p15c7中。C7寄存器位对应cp15中的cache控制寄存器
    mcr p15, 0, r0, c8, c7, 0 /* flush v4 TLB */ ;;使TLB操作寄存器失效:将r0数据送到cp15c8c7中。C8对应TLB操作寄存器

    /*
     * disable MMU stuff and caches 禁止MMUcaches
     */
    mrc p15, 0, r0, c1, c0, 0 ;;先把c1c0寄存器的各位置0(r0 = 0)
    bic r0, r0, #0x00002300 @ clear bits 13, 9:8 (--V- --RS)
    bic r0, r0, #0x00000087 @ clear bits 7, 2:0 (B--- -CAM) ;;这里我本来有个疑问:为什么要分开设置。因为arm汇编要求的立即数格式所决定的
    orr r0, r0, #0x00000002 @ set bit 2(??) (A) Align ;;上一条已经设置bit10,这一条又设置为1??
    orr r0, r0, #0x00001000 @ set bit 12 (I) I-Cache
    mcr p15, 0, r0, c1, c0, 0 ;;用上面(见下面)设定的r0的值设置c1??(cache类型寄存器)c0(control字寄存器),以下为c0的位定义
;;bit8: 0 = Disable System protection
;;bit9: 0 = Disable ROM protection
;;bit0: 0 = MMU disabled
;;bit1: 0 = Fault checking disabled 禁止纠错
;;bit2: 0 = Data cache disabled
;;bit7: 0 = Little-endian operation
;;bit12: 1 = Instruction cache enabled

    /* 配置内存区控制寄存器 ??有待分析,是1.1.4版本的
     * before relocating, we have to setup RAM timing
     * because memory timing is board-dependend, you will
     * find a lowlevel_init.S in your board directory.
     */
mov ip, lr
bl lowlevel_init ;;位于board/smdk2410/lowlevel_init.S:用于完成芯片存储器的初始化,执行完成后返回
mov lr, ip
mov pc, lr

函数二:/* U-Boot重新定位到RAM */

relocate: 
       adr r0, _start /* r0是代码的当前位置 */ ;;adr伪指令,汇编器自动通过当前PC的值算出 如果执行到_startPC的值,放到r0中:
当此段在flash中执行时r0 = _start = 0;当此段在RAM中执行时_start = _TEXT_BASE(board/smdk2410/config.mk中指定的值为0x33F80000,即u-boot在把代码拷贝到RAM中去执行的代码段的开始)
       ldr r1, _TEXT_BASE /* 测试判断是从Flash启动,还是RAM */ ;;此句执行的结果r1始终是0x33FF80000,因为此值是又编译器指定的(ads中设置,或-D设置编译器参数)
       cmp r0, r1 /* 比较r0r1,调试的时候不要执行重定位 */
       beq stack_setup /* 如果r0等于r1,跳过重定位代码 */
       /* 准备重新定位代码 */ ;;以上确定了复位启动代码是在flash中执行的(是系统重启,而不是软复位),就需要把代码拷贝到RAM中去执行,以下为计算即将拷贝的代码的长度
       ldr r2, _armboot_start ;;前面定义了,就是_start
       ldr r3, _bss_start ;;所谓bss段,就是未被初始化的静态变量存放的地方,这个地址是如何的出来的?根据board/smsk2410/u-boot.lds内容?
       sub r2, r3, r2 /* r2 得到armboot的大小 */
       add r2, r0, r2 /* r2 得到要复制代码的末尾地址 */

7)重新定位代码,循环拷贝启动的代码到RAM

copy_loop:
       ldmia {r3-r10} /*从源地址[r0]复制 */ ;;r0指向_start(=0)
       stmia {r3-r10} /* 复制到目的地址[r1] */ ;;r1指向_TEXT_BASE(=0x33F80000)
       cmp r0, r2 /* 复制数据块直到源数据末尾地址[r2] */
       ble copy_loop

8)初始化堆栈等

stack_setup:
       ldr r0, _TEXT_BASE /* 上面是128 KiB重定位的u-boot */
       sub r0, r0, #CFG_MALLOC_LEN /* 向下是内存分配空间 */    
       sub r0, r0, #CFG_GBL_DATA_SIZE /* 然后是bdinfo结构体地址空间 */
#ifdef CONFIG_USE_IRQ
       sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ)
#endif        ;;这些宏定义在/include/configs/smdk2410.h中:
#define CFG_MALLOC_LEN    (CFG_ENV_SIZE + 128*1024)        ;;64K+128K=0xC0
#define CFG_ENV_SIZE    0x10000        /* Total Size of Environment Sector 64k*/
#define CONFIG_STACKSIZE    (128*1024)    /* regular stack 128k */
#define CFG_GBL_DATA_SIZE     128    /* size in bytes reserved for initial data */
0x33F8000 – 0xC0 – 0x80得到_TEXT_BASE向下(低地址)的堆栈指针sp的起点地址
       sub sp, r0, #12 /* abort-stack预留3个字 */    ;;得到最终sp指针初始值
clear_bss:
       ldr r0, _bss_start /* 找到bss段起始地址 */
       ldr r1, _bss_end /* bss段末尾地址 */
       mov r2, #0x00000000 /* 清零 */
clbss_l:str r2, [r0] /* bss段地址空间清零循环... */
       add r0, r0, #4
       cmp r0, r1
       bne clbss_l

9)跳转到start_armboot函数入口,_start_armboot字保存函数的入口指针

       ldr pc, _start_armboot
_start_armboot: .word start_armboot ;;start_armboot函数在lib_arm/board.c中实现

2. Stage2lib_arm/board.c

这个文件是bootloaderstage2部分,这个文件中的start_armboot函数是U-Boot执行的第一个C语言函数,主要完成系统的初始化工作,然后进入主循环,等待并处理用户输入的命令。

在编译和链接BootLoader这样的程序的时候,不能使用glibc库中的任何支持函数,这就带来了一个问题:从何处跳入Main函数,最直接的想法是直接把Main函数的起始地址作为整个Stage2执行映像的入口。但是这样做有两个缺点:

a: 无法通过Main函数传递参数

b: 无法处理Main函数返回的情况

一种更好的解决方案是利用trampoline(弹簧床)的概念:用汇编写一段trampoline小程序,并将这段trampoline小程序作为Stage2可执行映像的入口点,然后就可以在trampoline小程序中用CPU跳转指令跳入Main函数去执行,当Main函数执行结束以后CPU执行路径显然再次回到trampoline程序。其核心思想就是用这段trampoline程序作为Main函数的外部包裹。

1. 初始化本阶段要使用到的硬件设备

一般包括:

a:点亮LED,表示已经进入main函数执行(可选)

b: 至少一个串口,以便和终端用户进行IO信息交换

c: 初始化定时器等

d: 输出一些打印信息,程序名称,版本号等

2. 检测系统的内存映射

所谓内存映射就是指在整个 4GB 物理地址空间中有哪些地址范围被分配用来寻址系统的 RAM 单元。比如,在 SA-1100 CPU 中,从 0xC000,0000 开始的 512M 地址空间被用作系统的 RAM 地址空间,而在 Samsung S3C44B0X CPU 中,从 0x0c00,0000 0x1000,0000 之间的 64M 地址空间被用作系统的 RAM 地址空间。虽然 CPU 通常预留出一大段足够的地址空间给系统 RAM,但是在搭建具体的嵌入式系统时却不一定会实现 CPU 预留的全部 RAM 地址空间。也就是说,具体的嵌入式系统往往只把 CPU 预留的全部 RAM 地址空间中的一部分映射到 RAM 单元上,而让剩下的那部分预留 RAM 地址空间处于未使用状态。  由于上述这个事实,因此 Boot Loader stage2 必须在它想干点什么 (比如,将存储在 flash 上的内核映像读到 RAM 空间中) 之前检测整个系统的内存映射情况,也即它必须知道 CPU 预留的全部 RAM 地址空间中的哪些被真正映射到 RAM 地址单元,哪些是处于 "unused" 状态的。

3. 加载内核映像和根文件系统映像

a:规划内存占用的布局:主要包括基地址和映像大小两个方面。对于内核映像,一般将其拷贝到从(MEM_START0x8000) 这个基地址开始的大约 1MB大小的内存范围内(嵌入式 Linux 的内核一般都不操过 1MB)。为什么要把从 MEM_STARTMEM_START0x 8000 这段 32KB 大小的内存空出 来呢?这是因为 Linux 内核要在这段内存中放置一些全局数据结构,如:启动参数和内核页表等信息。而对于根文件系统映像,则一般将其拷贝到 MEM_START+0x0010,0000 开始的地方。如果用 Ramdisk 作为根文件系统映像,则其解压后的大小一般是 1MB

b:从Flash中拷贝映像

while(count) {         

    *dest++ = *src++;  /* they are all aligned with word boundary */         

    count -= 4; /* byte number */ 

};

4. 设置内核的启动参数

将内核映像拷贝到RAM中之后就可以启动了,但是一般都需要先设定Linux内核的启动参数。Linux2.4以后的内核都以标记列表(tagged list)的形式来传递启动参数。启动参数列表以标记ATAG_CORE开始,以标记ATAG_NONE结束。每个标记由标示被传递参数的tag_header结构以及随后的参数数据结构来组成。数据结构tagtag_header定义在Linux内核源码的include/asm/setup.h头文件中。在嵌入式Linux系统中,通常需要由BootLoader设定的参数有:ATAG_COREATAG_MEMATAG_CMDLINEATAG_RAMDISKATAG_INITRD

比如,设置 ATAG_CORE 的代码如下:  

params = (struct tag *)BOOT_PARAMS;         

params->hdr.tag = ATAG_CORE;         

params->hdr.size = tag_size(tag_core);         

params->u.core.flags = 0;         

params->u.core.pagesize = 0;         

params->u.core.rootdev = 0;         

params = tag_next(params);  

其中,BOOT_PARAMS 表示内核启动参数在内存中的起始基地址,指针 params 是一个 struct tag 类型的指针。宏 tag_next() 将以指向当前标记的指针为参数,计算紧临当前标记的下一个标记的起始地址。注意,内核的根文件系统所在的设备 ID 就是在这里设置的。

5. 调用内核

BootLoader调用内核的方法是直接跳转到内核的第一条指令处,即直接跳到MEM_START+0x8000处。在跳转的时候要满足下面的条件:

a: CPU寄存器的设置

R0 = 0;

R1 = 机器类型ID

b: CPU必须在SVC模式

c: CacheMMU的设置:

MMU必须关闭

指令Cache可以打开也可以关闭

数据Cache必须关闭

说明:如果用 C 语言,可以像下列示例代码这样来调用内核:  

void (*theKernel)(int zero, int arch, u32 params_addr) = (void  (*)(int, int, 

u32))KERNEL_RAM_BASE; 

…… 

theKernel(0, ARCH_NUMBER, (u32) kernel_params_start);  

 

注意:theKernel()函数调用应该永远不返回的。如果这个调用返回,则说明出错。

3. 关于BootLoader开发中串口显示乱码或没有显示的原因:

1. boot loader 对串口的初始化设置不正确。 

2. 运行在 host 端的终端仿真程序对串口的设置不正确, 这包括:波特率、奇偶校验、数据位和停止位等方面的设置。

关于BootLoader启动时串口能输出,但是启动内核后不能正确显示的原因:

1. 内核编译时缺少配置对串口驱动的支持,或配置正确的串口驱动

2. BootLoader的串口配置和内核的不一致

3. 内核没有正确启动

上一篇:/bin/sh: can
下一篇:对内存的理解