uboot启动流程和架构

  • Post author:
  • Post category:其他


概述:

本文将从两个方面来阐述uboot:

1、启动流程

2、架构


一、uboot流程图:


这里写图片描述

从上图中看到红色1,2,3,4,5,7,8,9的标号,下面分别说明每个过程:


1、启动入口:

  • (1)确定链接脚本文件:

    在根目录下Makefile下LDSCRIPT宏值,就是指定链接脚本(如:arch/arm/cpu/u-boot.lsds)路径的

  • (2)从脚本文件找入口:

    在链接脚本中可以看到ENTRY()指定的入口,如:ENTRY(_start),_start就是入口

  • (3)链接脚本简要分析:

    ...
    ...
    ...
    ENTRY(_start)
    SECTIONS
    {
        .=0x00000000;
        .=ALIGN(4);
        .text:
        {
            *(.__image_copy_start)
            *(.vectors)
            CPUDIR/start.o (.text*)
            *(.text*)
        }
    }
    ...
    ...
    ...

像*(.__image_copy_start)这种,指定将标记为这个标记的代码段编译链接放入这个地方,上面的事例表示0x00000000地址(在编译完,生成的System.map文件中可以看到),在汇编中可以用如下方式将一段代码做标记(直到遇到下一个section为止,要是没有就到文件结束):

.section ".__image_copy_start", "ax"

在C文件中可以用如下方式,把一个变量或者函数标记到指定段:

char __image_copy_start[0] __attribute__((section(".__image_copy_start")));

在本人手中的例子,被编译的文件中没有__image_copy_start这样标记的代码段,只能找.vectors,在arch/arm/lib/vectors.S开头地方,找到被.vectors标记的代码段,并且_start也是位于有效代码第一行,最终启动入口位置确定了(假设_start没有位于vectors.S有效代码第一行,更或者,在start.o中,而ENTRY()又指定ENTRY(_start),会是什么结果?)。


2、初始化:


(1)启动过程中memory变化:

这里写图片描述

如上图:

a、上电,首先在CPU片上的bootrom空间里运行(CPU出厂就固化的code),检查外部flash(nand , emmc , nor , sdmmc等,看CPU支持情况):

——检测到,就会读取flash中前2K code到片上Sram,然后由这2K code负责将flash中全部code加载到外部RAM(EXT RAM),最终跳转到外部ram运行,然后继续初始化环境,加载系统等。

——检测不到外部flash,或者在外部flash检测不到有效bank,则会初始化USB(视具体板子),启动下载,等待img数据。

(验证:本人,验证结果是,在uboot前2k,里面取出的PC值,始终是外部RAM所在的地址空间,但是各种资料都描述有片上2K RAM)

(2)初始化过程,分board_init_f前,board_init_f,relocate, board_init_r这四个过程描述:

  • a、board_init_f前,做的都是关于CPU体系结构相关的初始化,对于做电子产品开发的,基本都不会去修改,就简单描述,首先,8个异常入口:
b   reset   
b   pc, _undefined_instuction
b   pc, _software_interrupt
b   pc, _prefetch_abort
b   pc, _data_abort
b   pc, _not_used
b   pc, _irq
b   pc, _fiq

从上面字面意思大概也知道,重点关注下reset(定位reset时,结合链接脚本文件很容易找到),reset首先做的是将FIQ,IRQ关闭,将CPU(arm)模式设置成SVC32(保护模式),然后配置CP15协处理器,CP15作用:

  • 第一,配置异常入口,以前都认为CPU发生reset异常后,都是PC为0x0地址,

    然而,通过配置CP15后,可以修改:
ldr     r0, =_start
mcr     p15,0,r0,c12,c0,0

如上,之后,再出现reset异常后,PC就是跳到_start处,不再是0x0地址

  • 第二,cpu_init_cp15在这个函数里面,配置(CP15配置)cache,MMU,TLBS,I-cache,L2 cache。

CP15完成配置,cpu_init_crit会调用lowlevel_init,这个一般定义板级初始化,需要很早初始化的,主要初始化pll,mux,memory(ddr)。

cpu_init_crit返回后,进入_main,这个_main入口有点难找,搜索会出来一大堆,我的是arm体系的,在arch/arm/lib/crt0.S里面找到的,从_main开始到board_init_f都是对ram空间的划分,有个重点宏CONFIG_SYS_INIT_SP_ADDR需要根据具体硬件RAM空间划分配置,例如,我的板子2G RAM空间映射到0x0地址开始2G地址空间,厂商将128M空间到0x0地址作为系统用,因此CONFIG_SYS_INIT_SP_ADD=0x0+SZ128M。

在_main第一条指令:

ldr     sp, =(CONFIG_SYS_INIT_SP_ADD)

然后,对sp按照8字节对齐处理后,将SP-sizeof(gd_t):

sub     sp, sp, #GD_SIZE

GD_SIZE定义在include/generated/generic-asm-offsets.h里面,从这里面看:

#define     GD_SIZE     232

gd_t结构体size与结构体系有关,这里写死,估计是厂商根据自己板子cpu体系算过。

这里sp是按照向下生长的,这条指令后sp,再通过:

mov     r9, sp

这样r9就指向gd全局变量起始地址了(空间就是从gd到CONFIG_SYS_INI_SP_ADD),至于为何这么说,且看下面:

gd_t(通用)定义在include/asm-generic/global_data.h,然后在gd_t结构体重嵌入了与体系有关的struct arch_global_data,我的是arm的,所以在arch/arm/include/asm/global_data.h中定义了arch_global_data,在这个头文件中还发现了:

#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm("r9")

结合上面r9的值,可以确定gd=CONFIG_SYS_INIT_SP_ADD-sizeof(gd_t)。

然后_main完成r9赋值后,要对gd这段空间清零处理,清零完成后就进入board_init_f函数。

  • b、board_init_f中,主要是做了gd成员值的初始化,这些成员值,最后会为copy code提供地址和size。

    最终划分的空间大致情况(根据firefly-rk3288,2G的ram空间,图子格子大小不代表ram空间大小),整理如下图:

    这里写图片描述

更详细的分析图(relocaddr表示的是gd->relocaddr,基于firefly-rk3288开发板分析的):

这里写图片描述

图看不清楚的可以

百度盘

下载看。

各分区说明:


tlb

: 一种cach,百度吧,我没很明白


globa buff

:firefly-rk3288用作flash管理buff(nand/emmc坏块等)


boot /fastboot buff




fastboot log buff

:


uboot code data&bss

:relocate后uboot code会copy到此处运行


malloc

: malloc()分配的内存空间


bd_t

: DDR信息,即几个bank,每个bank多大(bank信息的获取,就要看具体的板子 了,例如,我的firefly-rk3288,做得够隐秘了,代码(函数dram_init_banksize())分析看,将信息放在了0x0+32M的地方,然后用这个函数解析存入gd->bd->rk_dram里面,bd_t是一个与CPU体系结构相关的,也是可以根据在基础上增加成员)


gd_t

:new gd所在空间,原来在r9指定的gd空间,会copy到此处后,在board_init_f调用完成后由汇编把r9修改指向这块空间

在board_init_f中也有对serial,console初始化:

serial_init()在这里的初始化,做两件事,

首先,初始化gd->flags|=GD_FLG_SERIAL_READY表示serial以就绪

然后,调用具体serial初始化函数,将所用的serial端口和波特率等设置到就绪状态。

console_init_f()之中了一件事情,就是将gd->have_console=1,表示有控制台。

board_init_f最后调用setup_reloc()两个很重要的动作:

gd->reloc_off = gd->relocaddr - CONFIG_SYS_TEXT_BASE;
memcpy(gd->newd_gd,(char *)gd,sizeof(gd_t))

重要的宏CONFIG_SYS_TEXT_BASE,可以说虚拟地址空间,编译的时候,uboot会根据这值来链接。实际真实的硬件RAM起始地址和CONFIG_SYS_TEXT_BASE差,就是虚拟地址和物理地址偏移量。在这里,relocate后uboot在实际的物理地址gd->relocaddr(起始地址),CONFIG_SYS_TEXT_BASE是虚拟地址的起始,差值自然就是relocate后虚拟地址和物理地址的偏移量。

然后将重要的gd数据从(CONFIG_SYS_INIT_SP_ADD-SZ128M,CONFIG_SYS_INIT_SP_ADD)区间,copy到新的new gd区间

c、relocate:

这部分就是将,当前uboot运行空间的code,copy到上面“uboot code data&bss”空间里,即gd->relocaddr地址处,接着上面的运行。具体可以看这个博客地址:

uboot的relocation原理详细分析

d、board_init_r做的事情主要概括如下:

  • 标记uboot启动已经到了board_init_r阶段
  • 设置machine id & 启动参数存放位置
  • 最后硬件的初始化
  • Flash接口初始化
  • 环境变量最后的初始化
  • console最后初始化
  • 检查key是否进入下载模式
  • 进入main loop或者直接启动系统

    检查key是否进入下载模式、进入main loop或者直接启动系统放到流程图中3、4、5、6、7、8、9降


标记uboot启动已经到了board_init_r阶段



看下面代码很好理解:

static int initr_reloc(void)
{
    gd->flags |= GD_FLG_RELOC;
    bootstage_mark_name(BOOTSTAGE_ID_START_UBOOT_R,"board_init_r");
}


设置machine id & 启动参数存放位置


如下就是对gd两个重要成员的初始化:

int board_init(void)
{
    gd->bd->bi_arch_number = MACH_TYPE_XX;
    gd->bd->bi_boot_params = PHYS_SDRAM;
}

gd->bd->bi_arch_number作为machine id通过R1传递到linux kernel,也就是linux kernel会将这个值与宏MACHINE_START定义的nr比较,要相同,才能通过linux kernel的检查,否则会导致系统不能启动,这是旧版linux方式,新版的用宏DT_MACHINE_START,从宏定义也可以看出nr已无作用,这两个宏都定义在linux根目录下arch/arm/include/asm/mach/arch.h:

#define MACHINE_START(_type,_name)          \
static const struct machine_desc __mach_desc_##_type    \
 __used                         \
 __attribute__((__section__(".arch.info.init"))) = {    \
    .nr     = MACH_TYPE_##_type,        \
    .name       = _name,

#define MACHINE_END             \
};

#define DT_MACHINE_START(_name, _namestr)       \
static const struct machine_desc __mach_desc_##_name    \
 __used                         \
 __attribute__((__section__(".arch.info.init"))) = {    \
    .nr     = ~0,              \
    .name       = _namestr,

现在来看下linux是怎么来兼容新旧两种方式的,在linux根目录下init/main.c中start_kernel()调用setup_arch():

void __init setup_arch(char **cmdline_p)
{
    const struct machine_desc *mdesc;

    setup_processor();
    mdesc = setup_machine_fdt(__atags_pointer);
    if (!mdesc)
        mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);
    machine_desc = mdesc;
    .......
    .......
    .......
}

setup_machine_fdt()就是新版,参数__atags_pointer就是r2传过来的地址(下面会讲到uboot对r2的处理),匹配的方式就是r2地址开始的内存中,前4byte必须是0xd00dfeed:

#define FDT_MAGIC   0xd00dfeed

头部匹配通过后,开始匹配compatible属性,r2是dtb存放的内存起始地址,在dtb中有这么样值:

compatible = "rockchip,rk3288"

在r2中搜索这个属性,然后将属性值与linux中DT_MACHINE_START定义的dt_compat值对比,相同才最终匹配成功。DT_MACHINE_START定义变量都存储在.arch.info.init段中,搜索这段中的dt_compat就可以了。匹配成功后,会搜索dtb中如下节点:

chosen{
    bootargs = "console=ttyS2 init=/init initrd=0x62000000,0x00800000";
};

搜索到后,将bootargs的值copy到boot_command_line中,到此linux新版获取启动参数方式成功。

当linux新版获取启动参数方式失败,调用setup_machine_tags(__atags_pointer, __machine_arch_type)用旧版获取启动参数的方式,形参__atags_pointer当然就是r2的值,__machine_arch_type就是machine id,在哪里获取?

在linux根目录arch/arm/boot/compressed/misc.c中:

decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p,
        unsigned long free_mem_ptr_end_p,
        int arch_id)
{
    ...
    ...
    ...
    __machine_arch_type = arch_id;
    ...
    ...
    ...
}

参数arch_id又是哪里传入的:

在linux根目录下arch/arm/boot/compressed/head.S中:

/*
 * The C runtime environment should now be setup sufficiently.
 * Set up some pointers, and start decompressing.
 *   r4  = kernel execution address
 *   r7  = architecture ID
 *   r8  = atags pointer
 */
        mov r0, r4
        mov r1, sp          @ malloc space above stack
        add r2, sp, #0x10000    @ 64k max
        mov r3, r7
        bl  decompress_kernel
        bl  cache_clean_flush
        bl  cache_off
        mov r1, r7          @ restore architecture number
        mov r2, r8          @ restore atags pointer

根据汇编和C互相调用时,传参数的规定,arch_id就等于r3的值,r3==r7,根据注释r7是architecture ID,至于r7什么时候得到,我就不继续追下去了,有兴趣的可以跟踪下代码。接下来就是用__machine_arch_type与MACHINE_START定义的nr比较,相等就匹配成功,获取linux中struct machine_desc结构体变量(描述cpu相关的):

    /*
     * locate machine in the list of supported machines.
     */
    for_each_machine_desc(p)
        if (machine_nr == p->nr) {
            pr_info("Machine: %s\n", p->name);
            mdesc = p;
            break;
        }

到此新旧两种获取启动参数的方式结束。我另外一篇博客:

linux之early_param()和__setup

对跟踪启动参数作用或许在理解上有一定帮助

回到uboot:

在arch/arm/lib/bootm.c中函数boot_jump_linux()如下代码,将bi_arch_number传到r1,然后传到linux:

static void boot_jump_linux(bootm_headers_t *images, int flag)
{
    unsigned long machid = gd->bd->bi_arch_number;
    unsigned long r2;
    void (*kernel_entry)(int zero, int arch, uint params);
    kernel_entry = (void (*)(int,int,uint))images->ep;
    if(IMAGE_ENABLE_OF_LIBFDT && images->ft_len)
        r2 = (unsigned long)images->ft_addr;
    else
        r2 = gd->bd->bi_boot_params;
    kernel_entry(0,machid,r2);
}

images->ep就是linux在ram里面起始地址,地址直接赋值给一个函数指针,然后就当一个函数调用。uboot和linux传参,规定函数的第一个参数为0(r0 = 0),第2个参数machine id(r1= machine id),第三个参数为命令行起始地址(r2 = params addr)。

(c语言中具体地址当作函数调用,参数一般都是第一个对应r0,第2个对应r1…依次类推,这也是c调用汇编的规定)

关于MACH_TYPE_XX生成,由linux kernel一个awk脚本arch/arm/tools/gen-mach-types在编译的时候,会根据在同目录下的mach-types(修改这个文件,就可以在mach-types.h下生成形如MACH_TYPE_XX宏)文件生成linux根目录下include/generated/mach-types.h。与uboot根目录下arch/arm/include/asm/mach-types.h很相似,但在uboot下面没找到生成的脚本(个人感觉从linux下面copy过去,哈哈)。

gd->bd->bi_arch_number,gd->bd->bi_boot_params这个启动参数的方式是linux老式传参方式了,但新版本linux都是兼容的,bi_boot_params地址一般都设置在靠近ram起始地址(fireflye-rk3288设置在0x0+0x88000)。

uboot对新版命令行传参方式的支持:

在arch/arm/lib/bootm.c中boot_prep_linux()中有调用到image_setup_linux(bootm_headers_t *images) ,函数参数struct boot_headers_t结构体中成员ft_addr,ft_len就是新版参数的关键。ft_addr是dtb装载到ram中的地址,dtb可以从两个地方resource.img和boot.img装载,

boot.img是android中用到的,在android中有mkbootimg工具,将zImage(Linux),ramdisk,second stage(可选,可以将dtb放到这段)。这样,uboot就可以从boot.img中解析出sencond stage加载到内存,并将地址赋给fd_addr。关于boot.img和工具mkbootimg理解,可以参考这篇博客:

通过分析mkbootimg源代码了解boot.img文件结构

dtb是linux根目录下scripts/dts/dtc(dtc工具编译的时候会产生)工具,将dts生成dtb二进制文件。dtc的工具使用如下:

dts编译成二进制文件dtb:

scripts/dtc/dtc -I dtb -O dts ./product1.dtb -o ./my.dts

dtb反编译成dts:

scripts/dtc/dtc -I dts -O dtb ./a.dts -o ./b.dtb

dts中命令行参数放在如下的节点中:

chosen{
    bootargs = "console=ttyS2 init=/init initrd=0x62000000,0x00800000";
};

从这个获取resource.img启动参数,在uboot根目录下common/resource.c,include/resource.h来看:

struct resource_content:这个结构体,是解析时的内存结构

struct resource_ptn_header:这个结构体,是存在于resource.img开始地方,即文件头

struct index_tbl_entry:存在于resource.img每个数据块的头

如下resource.img在nand/emmc结构示意图:

resource.img结构

捣事的CSDN,写完整的博客,却不见了,这是我写到一半时保存在自己电脑上的先放这,心好累,以后有时间在看着补全吧,心累啊



版权声明:本文为eZiMu原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。