ARM U-Boot SPL过程浅析

时间:2021-08-22 04:46:41

原文地址:http://bbs.chinaunix.net/thread-4248378-1-1.html


【1】SPL简介

SPL(Secondary programloader)是uboot第一阶段执行的代码。主要负责搬移uboot第二阶段的代码到系统内存(System Ram,也叫片外内存)中运行。SPL是由固化在芯片内部的ROM引导的。我们知道很多芯片厂商固化的ROM支持从nandflash、SDCARD等外部介质启动。所谓启动,就是从这些外部介质中搬移一段固定大小(4K/8K/16K等)的代码到内部RAM中运行。这里搬移的就是SPL。在最新版本的uboot中,可以看到SPL也支持nandflash,SDCARD等多种启动方式。当SPL本身被搬移到内部RAM中运行时,它会从nandflash、SDCARD等外部介质中搬移uboot第二阶段的代码到系统内存中。
SPL复用的是uboot里面的代码.


【2】SPL功能

1.BasicArm Initialization
2.UART console initialization
3.Clocks and DPLL Locking(minimal)
4.SDRAM initialization
5.Mux(minimal)
6.Boot Device Initialization, based on where we are booting from MMC1, or MMC2,or Nand, or Onenand
7.Bootloading real u-boot from the Boot Device and passing control to it.


【3】SPL配置选项CONFIG_SPL_BUILD

上文中说道“SPL复用的是uboot里面的代码”,那要生成我们所需要的SPL目标文件,我们又该如何下手呢?很容易想到,通过编译选项便可以将SPL和uboot代码分离、复用。这里所说的编译选项便是CONFIG_SPL_BUILD,在make Kconfig的时候使能。最终编译生成的SPL二进制文件有u-boot-spl,u-boot-spl.bin以及u-boot-spl.map。


【4】SPL链接文件U-boot-spl.lds

链接文件决定一个可执行程序的各个段的存储(加载)地址,以及运行(链接)地址。下面来看看SPL的链接文件U-boot-spl.lds:

SECTIONS
{
    . = 0x00000000;

    . = ALIGN(4);

    .text :
    {
        __image_copy_start = .;
        *(.vectors)
        CPUDIR/start.o (.text*)
        *(.text*)
    }
    . = ALIGN(4);

    .rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }

    . = ALIGN(4);

    .data : {
        *(.data*)
    }
    . = ALIGN(4);

    __image_copy_end = .;

    .rel.dyn : {
        __rel_dyn_start = .;
        *(.rel*)
        __rel_dyn_end = .;
    }

    .end :
    {
        *(.__end)
    }

    _image_binary_end = .;

    .bss __rel_dyn_start (OVERLAY) : {
        __bss_start = .;
        *(.bss*)
         . = ALIGN(4);
        __bss_end = .;
    }
    __bss_size = __bss_end - __bss_start;
    .dynsym _image_binary_end : { *(.dynsym) }
    .dynbss : { *(.dynbss) }
    .dynstr : { *(.dynstr*) }
    .dynamic : { *(.dynamic*) }
    .hash : { *(.hash*) }
    .plt : { *(.plt*) }
    .interp : { *(.interp*) }
    .gnu : { *(.gnu*) }
    .ARM.exidx : { *(.ARM.exidx*) }
}

#if defined(CONFIG_SPL_MAX_SIZE)
ASSERT(__image_copy_end - __image_copy_start < (CONFIG_SPL_MAX_SIZE), \
    "SPL image too big");
#endif

#if defined(CONFIG_SPL_BSS_MAX_SIZE)
ASSERT(__bss_end - __bss_start < (CONFIG_SPL_BSS_MAX_SIZE), \
    "SPL image BSS too big");
#endif

#if defined(CONFIG_SPL_MAX_FOOTPRINT)
ASSERT(__bss_end - _start < (CONFIG_SPL_MAX_FOOTPRINT), \
    "SPL image plus BSS too big");
#endif


【5】.vector段

可以看出,正真的.text段从.vector段开始,从字面上就能看出.vector段是中断向量表的存放处。下面U-boot源码所示是根据CPU的硬件特性所定义的中断向量表,_undefined_instruction、_software_interrupt、_prefetch_abort、_data_abort、_not_used、_irq、_fiq中分别存放着相应中断类型的中断处理程序的入口地址undefined_instruction、software_interrupt、prefetch_abort、data_abort、not_used、irq、fiq。U-boot/arch/arm/lib/vectors.S:

_start:

#ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
    .word    CONFIG_SYS_DV_NOR_BOOT_CFG
#endif

    b    reset /* system will auto jump here where power on */
    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

/*
*************************************************************************
*
* Indirect vectors table
*
* Symbols referenced here must be defined somewhere else
*
*************************************************************************
*/

    .globl    _undefined_instruction
    .globl    _software_interrupt
    .globl    _prefetch_abort
    .globl    _data_abort
    .globl    _not_used
    .globl    _irq
    .globl    _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

    .balignl 16,0xdeadbeef /* word align the following code to alignment byte boundary */

但是,SPL的中断处理过程是这样的:
#ifdef CONFIG_SPL_BUILD

        .align        5
undefined_instruction:
software_interrupt:
prefetch_abort:
data_abort:
not_used:
irq:
fiq:

1:
        bl        1b                        /* hang and never return */
也就是说,如果SPL出现中断,那就只能重启机器了,当然,reset中断得除外,因为那是机器上电后的入口地址。


【6】start.o

回到最初的.lds中的.text段,存放完vectors后,从.text段的0x20偏移处开始就是start.o了,与star.o相对应的start.S源码位于/u-boot/Arch/Arm/Cpu/Arm926ejs/目录下,其执行流程见下图。在流程图中,关于CONFIG_SKIP_LOWLEVEL_INIT以及后面可能还会接触到的CONFIG_SKIP_RELOCATE_UBOOT编译选项需要做下解释:在他们被定义了的情况下,相应的底层(low level)初始化就会被忽略,同时U-boot不会将自身(说的应该是第二阶段U-boot代码)重新加载到RAM中。通常情况下,这些变量不应被定义,除非U-boot已经被其他bootloader或debugger加载到RAM中了。
ARM U-Boot SPL过程浅析 

在调用lowlevel_init之前,LPC3250工作在直接运行模式(direct run mode),所有时钟运行在13MHz,而lowlevel_init调用返回时,ARM clock=208MHz,HCLK=104MHz,PCLK=13MHz。更改时钟的操作必须在SRAM中运行,否则势必会引起执行异常。


【7】_main过程

从start.S主流程可知,程序最终会进入到_main函数中运行。下面开始分析_main过程。_main过程定义在u_boot/arch/arm/lib/crt0.S中。对于SPL,_main过程执行流程:

1.为调用board_init_f()函数构建初始化环境,该初始化环境仅提供了一个堆栈和一个存储GD(global data)结构的存储空间,所述堆栈和GD的存储空间均分配在已经可用的RAM中(SRAM等)。此时,全局变量,BSS均不可用,仅常量可用。另外,在调用board_init_f()之前,GD需要被全部清零。

2.调用board_init_f()。board_init_f()为从系统RAM(DRAM,DDR等)中执行程序准备硬件环境。由于系统RAM当前可能还不能使用,board_init_f()必须采用GD将那些需要传递到下一级处理阶段的数据保存起来,这些数据包括重载地址(relocation destination,在新版u-boot中用于将u-boot重新加载到系统RAM的另一个地方,在“u-boot过程”中在做分析),将来在系统RAM中重新分配的堆栈、GD的地址。

3.构建中间环境(intermediate environment),此时的stack与GD均为board_init_f()在系统RAM中所分配,但是这个时候的BSS段和已经初始化的非常量还是不能使用。

4.为调用board_init_r()函数构建环境,This environment has BSS (initialized to 0), initialized non-constdata (initialized to their intended value), and stack in system RAM (for SPLmoving the stack and GD into RAM is optional – see CONFIG_SPL_STACK_R)。GD保持在board_init_f()中的设置值不变。

5.跳转到board_init_r()运行,复制u-boot第二阶段代码到系统RAM中,并跳到第二阶段代码在系统RAM的起始地址处开始运行。

从crt0.S中整理出来的SPL main源码:

ENTRY(_main)

#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
    ldr sp, =(CONFIG_SPL_STACK)
#else
    ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)
#endif
    bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
   
    mov r0, sp
    bl board_init_f_alloc_reserve
    mov sp, r0

    /* set up gd here,outside any c code */
    mov r9, r0
    bl board_init_f_init_reserve

    mov r0, #0
    bl board_init_f

#if !defined(CONFIG_SPL_BUILD) || defined(CONFIG_SPL_FRAMEWORK)
#ifdef CONFIG_SPL_BUILD
    /*use a DRAM stack for the rest of SPL, if requested */
    bl spl_relocate_stack_gd
    cmp r0, #0
    movne sp, r0
    movne r9, r0
#endif
    ldr r0, =__bss_start /*this is auto-relocated */

#ifdef CONFIG_USE_ARCH_MEMSET
    ldr r3, =__bss_end /*this is auto-relocated */
    mov r1, #0x00000000 /*prepare zero to clear BSS */
   
    subs r2, r3, r0 /*r2=memset len */
    bl memset
#else
    ldr r1, =__bss_end /* this is auto-relocated */
    mov r2, #0x00000000 /*prepare zero to clear BSS */
clbss_1:     cmp r0, r1 /* while not at end of BSS */
   
    strlo r2, [r0] /*clear 32-bit BSS word */
    addlo r0, r0, #4 /* move to next */
    blo clbss_1
#endif
    /* call board_init_r(gd_t *id, ulong dest_addr) */
    mov     r0, r9                  /* gd_t */
    ldr    r1, [r9, #GD_RELOCADDR]    /* dest_addr */
    /* call board_init_r */
#if defined(CONFIG_SYS_THUMB_BUILD)
    ldr    lr, =board_init_r    /* this is auto-relocated! */
    bx    lr
#else
    ldr    pc, =board_init_r    /* this is auto-relocated! */
#endif
    /* we should not return here. */
#endif

ENDPROC(_main)


前面说到_main的最后一步是“跳转到board_init_r()运行,复制u-boot第二阶段代码到系统RAM中,并跳到第二阶段代码在系统RAM的起始地址处开始运行”。

这里有个“从SPL到U-Boot第二阶段代码的跳转执行”的过程:
__attribute__((noreturn)) void (*uboot)(void);

/* Jump to U_Boot image */
uboot = (void *) CONFIG_SYS_NAND_U_BOOT_START;
(* uboot)();

如果说翻译成我们所熟悉的汇编跳转的话应该是:ldr pc, =CONFIG_NAND_U_BOOT_START

接下来需要确认、深究的是,
1. 片内RAM和系统RAM是不是统一编址的,如果不是,怎么解决片内RAM和系统RAM的地址重叠问题;
2. 在程序开始在系统RAM中运行之后,片内RAM留作何用;
3. 何为C运行环境;


楼上一直没有具体说明的硬件平台:NXP LPC3220; 架构:ARM926EJS

关于“2楼”中提出的遗留问题1“片内RAM和系统RAM是不是统一编址的,如果不是,怎么解决片内RAM和系统RAM的地址重叠问题”

芯片手册的"memory map"可以给出答案:

on-chip memory ranges from 0x0000 0000 to 0x0fff ffff
off-chip memory ranges from 0x7fff ffff to 0xe3ff ffff

那么既然系统RAM和片内RAM的地址空间是不重叠的,在系统RAM初始化完成且加载完代码之后,程序将跳到系统RAM中运行,此时的片内RAM留作何用呢?这也是“2楼”提到的问题2。


一直觉得需要对1楼中的_main过程进行更加深入的了解,领会“c runtime environment”的具体含义。
这两天抽空再看了看这部分代码,下面对代码进行了简单注释,记录一下,方便以后回顾。

要点:
1. gd_t结构体,用于在系统初始化的过程中传递参数;定义在/u-boot/include/asm-generic/Global_data.h中
2. bss段的初始化;
3. SP的设置。

ENTRY(_main)

#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
        ldr sp, =(CONFIG_SPL_STACK) //sp=CONFIG_SPL_STACK
#else
        ldr sp, =(CONFIG_SYS_INIT_SP_ADDR) //sp=CONFIG_SYS_INIT_SP_ADDR
#endif
        bic sp, sp, #7/* sp寄存器的bit0~2置零,8-byte alignment for ABI compliance */
        
        mov r0, sp /* r0=sp,即将CONFIG_SPL_STACK/CONFIG_SYS_INIT_SP_ADDR做为参数传递给函数board_init_f_alloc_reserve */
        bl board_init_f_alloc_reserve 

/******************board_init_f_alloc_reserve函数定义********************************************/
ulong board_init_f_alloc_reserve(ulong top)
{
        /* Reserve early malloc arena */
#if defined(CONFIG_SYS_MALLOC_F)
        top -= CONFIG_SYS_MALLOC_F_LEN;/* 保存已被分配的存储空间,栈顶top移动到可用内存区的顶部*/
#endif
        /* LAST : reserve GD (rounded up to a multiple of 16 bytes) */
        top = rounddown(top-sizeof(struct global_data), 16);/* 16 bytes对齐,为global_data分配存储空间,top置为栈底 */

        return top;/* 返回栈底,此时,栈底同时也是global_data的地址 */
}
/******************board_init_f_alloc_reserve函数定义 end*****************************************/

        mov sp, r0 /* sp=r0=top,r0中存储的是board_init_f_alloc_reserve函数的返回值 */

        /* set up gd here,outside any c code */
        mov r9, r0 /* r9=r0=top ,接下来将r0中存储的栈底传递给board_init_f_init_reserve函数*/
        bl board_init_f_init_reserve 

/******************board_init_f_init_reserve函数定义*****************************************/
void board_init_f_init_reserve(ulong base)
{
        struct global_data *gd_ptr;
#ifndef _USE_MEMCPY
        int *ptr;
#endif

        /*
         * clear GD entirely and set it up.
         * Use gd_ptr, as gd may not be properly set yet.
         */

        gd_ptr = (struct global_data *)base;//gd_ptr赋值为栈底
        /* zero the area */
#ifdef _USE_MEMCPY
        memset(gd_ptr, '\0', sizeof(*gd));
#else
        for (ptr = (int *)gd_ptr; ptr < (int *)(gd_ptr + 1); )
                *ptr++ = 0;//将global_data结构体置零
#endif
        /* set GD unless architecture did it already */
#if !defined(CONFIG_ARM)
        arch_setup_gd(gd_ptr);
#endif
        /* next alloc will be higher by one GD plus 16-byte alignment */
        base += roundup(sizeof(struct global_data), 16);//修改base值为当前栈底值和global_data大小之和,同时base值需要16bytes对齐。即移动栈底位置,且要求16 bytes对齐。指定到下一块GD或malloc区域的base地址

        /*
         * record early malloc arena start.
         * Use gd as it is now properly set for all architectures.
         */

#if defined(CONFIG_SYS_MALLOC_F)
        /* go down one 'early malloc arena' */
        gd->malloc_base = base; 
        /* next alloc will be higher by one 'early malloc arena' size */
        base += CONFIG_SYS_MALLOC_F_LEN; //指定到下一块GD或malloc区域的base地址
#endif
}
/******************board_init_f_init_reserve函数定义 end*****************************************/


        mov r0, #0
        bl board_init_f

#if !defined(CONFIG_SPL_BUILD) || defined(CONFIG_SPL_FRAMEWORK)
#ifdef CONFIG_SPL_BUILD
        /*use a DRAM stack for the rest of SPL, if requested */
        bl spl_relocate_stack_gd //在定义了CONFIG_SPL_STACK_R等情况下,将片内SRAM中的GD复制到系统RAM中。
        cmp r0, #0 //如果spl_relocate_stack_gd返回为0,即未定义CONFIG_SPL_STACK_R,GD依旧在SRAM中
        movne sp, r0 //如果r0不等于0,则sp=r0,即sp等于SDRAM中GD的地址
        movne r9, r0 //如果r0不等于0,则r9=r0,即r9等于SDRAM中GD的地址
#endif
        ldr r0, =__bss_start /*this is auto-relocated */ //BSS段基址

#ifdef CONFIG_USE_ARCH_MEMSET
        ldr r3, =__bss_end /*this is auto-relocated */ //BSS段尾
        mov r1, #0x00000000 /*prepare zero to clear BSS */
        
        subs r2, r3, r0 /*r2=memset len */ //r2=r3-r0=BSS段长
        bl memset //clear BSS
#else
        ldr r1, =__bss_end /* this is auto-relocated */
        mov r2, #0x00000000 /*prepare zero to clear BSS */
clbss_1:         cmp r0, r1 /* while not at end of BSS */
        
        strlo r2, [r0] /*clear 32-bit BSS word */
        addlo r0, r0, #4 /* move to next */
        blo clbss_1 //clear BSS循环clbss_l
#endif
        /* call board_init_r(gd_t *id, ulong dest_addr) */
        mov     r0, r9                  /* gd_t */ //r0=r9=gd首地址
        ldr        r1, [r9, #GD_RELOCADDR]        /* dest_addr */ //r1=gd->GD_RELOCADDR
        /* call board_init_r */
#if defined(CONFIG_SYS_THUMB_BUILD)
        ldr        lr, =board_init_r        /* this is auto-relocated! */
        bx        lr
#else
        ldr        pc, =board_init_r        /* this is auto-relocated! */
#endif
        /* we should not return here. */
#endif

ENDPROC(_main)