• 从0移植uboot (二) _uboot启动流程分析


    经过了上一篇的配置,我们已经执行make就可以编译出一个uboot.bin,但这还不够,首先,此时的uboot并不符合三星芯片对bootloader的格式要求,同时,此时的uboot.bin也没有结合我们的开发板进行配置,还无法使用。而要进行这样的个性化配置,前提条件就是对uboot开机流程和编译系统有所了解,本文主要讨论前者。在三星的SoC中, 启动流程可以分为三个阶段BL0, BL1, BL2, BL3, 三星自己的手册对BL1的解释也不尽相同, 一种是将在iRAM中运行的程序都归结为BL1; 一种是将iRAM中三星加密的代码bl1.bin作为BL1, iRAM中剩余的部分作为BL2, 本文采用后者, 他们的主要分工如下:

    • BL0: ARM的起始地址都是0地址, 三星的芯片一般将0地址映射到iROM中, BL0就是指iROM中固化的启动代码, 主要负责加载BL1
    • BL1: 三星对于bootloader的加密代码bl1.bin, 要放在外设中uboot.bin的头上, 和一部分uboot.bin一起加载到iRAM中运行.
    • BL2: 从(nand/sd/usb)中拷贝的uboot.bin头最大14K到iRAM中代码中除去bl1.bin后剩余的部分, 负责设置CPU为SVC模式, 关闭MMU, 关闭中断, 关闭iCache, 关闭看门狗, 初始化DRAM,初始化时钟, 初始化串口, 设置栈, 校验BL2并将其搬移到DRAM高位地址, 重定位到DRAM中执行BL3
    • BL3:是指在代码重定向后在内存中执行的uboot的完整代码, 负责初始化外设,更新向量表, 清BSS, 准备内核启动参数, 加载并运行OS内核

    可以借助下图理解这个流程

    我们常说的uboot是一个两阶段bootloader,就是指上述的BL2和BL3. BL2主要做硬件直接相关的初始化,使用汇编编写;BL3主要为操作系统的运行准备环境,主要用C编写,这里以ARM平台为例分析其启动流程。下面是启动过程中主要涉及的文件

    arch/arm/cpu/armv7/start.S
    board/samsung/myboard/lowlevel_init.S
    arch/arm/lib/crt0.S
    arch/arm/lib/board.c
    arch/samsung/myboard/myboard.c

    BL2

    BL2的主要文件和任务流程如下

    arch/arm/cpu/armv7/start.S
               1. 设置CPU为SVC模式
               2. 关闭MMU
               3. 关闭Cache
               4. 跳转到lowlevel_init.S low_level_init
    board/samsung/origen/lowlevel_init.S
               5. 初始化时钟
               6. 初始化内存
               7. 初始化串口
               8. 关闭看门狗
               9. 跳转到crt0.S _main
    arch/arm/lib/crt0.S
               10. 设置栈
               11. 初始化C运行环境
               12. 调用board_init_f()
    arch/arm/lib/board.c
               13. board_init_f对全局信息GD结构体进行填充
    arch/arm/lib/crt0.S
               14. 代码重定位------------BL2的最后的工作, 执行完就进入DRAM执行BL2

    start.S

     39 .globl _start
     40 _start: b       reset
     41         ldr     pc, _undefined_instruction
     42         ldr     pc, _software_interrupt
     43         ldr     pc, _prefetch_abort
     44         ldr     pc, _data_abort
     45         ldr     pc, _not_used
     46         ldr     pc, _irq
     47         ldr     pc, _fiq
    

    --40--> 异常向量表设置

    126 reset:
    127         bl      save_boot_params
    131         mrs     r0, cpsr
    132         bic     r0, r0, #0x1f
    133         orr     r0, r0, #0xd3
    134         msr     cpsr,r0
    

    --126-->设置CPU为SVC模式

    下面这三行代码非常重要,是BL2启动过程的交叉点

    154         bl      cpu_init_cp15
    155         bl      cpu_init_crit
    158         bl      _main
    

    --154-->跳转执行cpu_init_cp15,即初始化CP15协处理器
    --155-->跳转执行cpu_init_crit,
    --158-->跳转执行_main

    287 ENTRY(cpu_init_cp15)
    291         mov     r0, #0                  @ set up for MCR
    292         mcr     p15, 0, r0, c8, c7, 0   @ invalidate TLBs
    293         mcr     p15, 0, r0, c7, c5, 0   @ invalidate icache
    294         mcr     p15, 0, r0, c7, c5, 6   @ invalidate BP array
    295         mcr     p15, 0, r0, c7, c10, 4  @ DSB
    296         mcr     p15, 0, r0, c7, c5, 4   @ ISB
    297 
    301         mrc     p15, 0, r0, c1, c0, 0
    302         bic     r0, r0, #0x00002000     @ clear bits 13 (--V-)
    303         bic     r0, r0, #0x00000007     @ clear bits 2:0 (-CAM)
    304         orr     r0, r0, #0x00000002     @ set bit 1 (--A-) Align
    305         orr     r0, r0, #0x00000800     @ set bit 11 (Z---) BTB
    307         bic     r0, r0, #0x00001000     @ clear bit 12 (I) I-cache
    311         mcr     p15, 0, r0, c1, c0, 0
    312         mov     pc, lr                  @ back to my caller
    313 ENDPROC(cpu_init_cp15)
    

    --291-->关闭Cache
    --301-->关闭MMU

    324 ENTRY(cpu_init_crit)
    331         b       lowlevel_init           @ go setup pll,mux,memory
    332 ENDPROC(cpu_init_crit)
    

    --331-->跳转到lowlevel_init,位于"board/samsung/origen/lowlevel_init.S",进行板级相关的设置。

    lowlevel_init.S

    这是位于目录的初始化文件,主要完成特定开发板的初始化工作,包括时钟、内存和串口等。

     82         bl system_clock_init
     85         bl mem_ctrl_asm_init
     87 1:
     88         /* for UART */
     89         bl uart_asm_init
     90         bl tzpc_init
     91         pop     {pc}
    114 system_clock_init:
    329 uart_asm_init:
    357 tzpc_init:
    

    --82-->初始化系统时钟,即跳转到114行
    --85-->初始化系统内存
    --89-->初始化UART串口,即跳转到329行
    --90-->初始化TrustZoneProtectorController,即跳转到357行

    执行完lowlevel_init.S,依据上面那三行代码,执行流程就该回到start.S执行156行跳转到_main

    crt0.S

    首要任务就是设置栈, 准备C语言运行的环境:

     96 _main:
    102 #if defined(CONFIG_NAND_SPL)
    103         /* deprecated, use instead CONFIG_SPL_BUILD */
    104         ldr     sp, =(CONFIG_SYS_INIT_SP_ADDR)
    105 #elif defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
    106         ldr     sp, =(CONFIG_SPL_STACK)
    107 #else
    108         ldr     sp, =(CONFIG_SYS_INIT_SP_ADDR)
    109 #endif
    110         bic     sp, sp, #7      /* 8-byte alignment for ABI compliance */
    111         sub     sp, #GD_SIZE    /* allocate one GD above SP */
    112         bic     sp, sp, #7      /* 8-byte alignment for ABI compliance */
    113         mov     r8, sp          /* GD is above SP */
    114         mov     r0, #0
    115         bl      board_init_f
    

    _main
    --104-->初始化SP,为C语言做准备
    --110-->保存128B放GD结构体来存放全局信息,
    --111-->GD的地址放在r8中,
    --115-->跳转到board_init_f(),这个整个初始化过程中第一次执行的C代码

    board.c

    下面这个函数就是uboot初始化过程中执行的第一个C函数,可以看作这个文件的入口函数。其中最重要的就是准备全局信息GD结构体, 内核启动参数就是来自于这个结构体

    209 typedef int (init_fnc_t) (void);
    243 init_fnc_t *init_sequence[] = {
    244         arch_cpu_init,          /* basic arch cpu dependent setup */
    245         mark_bootstage,
    246 #ifdef CONFIG_OF_CONTROL
    247         fdtdec_check_fdt,
                ...
    277 void board_init_f(ulong bootflag)
    278 {
            ...
    291         gd->mon_len = _bss_end_ofs;
    292 #ifdef CONFIG_OF_EMBED
    293         /* Get a pointer to the FDT */
    294         gd->fdt_blob = _binary_dt_dtb_start;
    295 #elif defined CONFIG_OF_SEPARATE
    296         /* FDT is at end of image */
    297         gd->fdt_blob = (void *)(_end_ofs + _TEXT_BASE);
    298 #endif
    299         /* Allow the early environment to override the fdt address */
    300         gd->fdt_blob = (void *)getenv_ulong("fdtcontroladdr", 16,
    301                                                 (uintptr_t)gd->fdt_blob);
    302 
    303         for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
    304                 if ((*init_fnc_ptr)() != 0) {
    305                         hang ();
    306                 }
    307         }
            ...
    

    board_init_f()
    --243--> 全局的函数指针数组,每个指针都是int (*ptr)(void)型的。
    --291-->mon_len 通过链接脚本可以知道存放的是 uboot 代码大小;
    --294-->fdt_blob 存放设备数地址;
    --303--遍历函数指针数组init_sequence中的每一个成员,就是将数组中的每一个初始化函数都执行一次,这种写法可以借鉴

    crt0.S

    函数board_init_f()返回后,继续执行crt0.S中115行之后的部分,主要的工作是执行代码重定位,执行完这些之后,我们找到了最感兴趣的下面这几句

    163         /* call board_init_r(gd_t *id, ulong dest_addr) */
    164         mov     r0, r8                  /* gd_t */
    165         ldr     r1, [r8, #GD_RELOCADDR] /* dest_addr */
    166         /* call board_init_r */
    167         ldr     pc, =board_init_r       /* this is auto-relocated! */
    

    --167-->跳转到board_init_r函数执行,这次跳出去这个文件的语句就执行完毕了,不会再回来了, 开始执行BL3.

    BL3

    这一阶段涉及的文件及任务如下

    arch/arm/lib/crt0.S
    arch/arm/lib/board.c
               1. board_init_r()是进入定制板目录的入口
    common/main.c
               2. main_loop()中关闭中断,执行命令以及加载引导内核

    board.c

    这也是最后一次跳转到这个文件了,执行额函数如下

    519 void board_init_r(gd_t *id, ulong dest_addr)
    520 {
    521         ulong malloc_start;
    522 #if !defined(CONFIG_SYS_NO_FLASH)
    523         ulong flash_size;
    524 #endif
    525 
    526         gd->flags |= GD_FLG_RELOC;      /* tell others: relocation done */
    527         bootstage_mark_name(BOOTSTAGE_ID_START_UBOOT_R, "board_init_r");
    528 
    529         monitor_flash_len = _end_ofs;
    530 
    531         /* Enable caches */
    532         enable_caches();
    533 
    534         debug("monitor flash len: %08lX
    ", monitor_flash_len);
    535         board_init();   /* Setup chipselects */
            ...
    650          /* set up exceptions */
    651         interrupt_init();
    652         /* enable exceptions */
    653         enable_interrupts(); 
    667         eth_initialize(gd->bd);
            ...
    701         /* main_loop() can return to retry autoboot, if so just run it again. */
    702         for (;;) {
    703                 main_loop();
    704         }
    705 
    
    

    board_init_r()
    --532-->很多紧急工作都做完了,可以打开cache了
    --535-->关键!!!这个就是我们苦苦寻找的板级定制文件的xxx.c的入口函数!!!
    --651-->中断初始化
    --653-->使能中断
    --667-->网卡初始化,函数的实现在net/eth.c,会回调板级xxx.c中的board_eth_init()
    --703-->**执行main_loop(),实现在common/main.c,它的主要功能就是循环检测输入的命令并执行,其中一个环境变量bootdelay(自启动)的设置决定了是否启动内核,如果延时大于等于零,并且没有在延时过程中接收到按键,则引导内核

    main.c

    这个文件就是main_loop()所在的文件, 不论是启动内核, 打印信息还是输入命令, 都在这个函数中执行, 我们这里主要关心这个函数是如何启动内核的, 函数的调用关系如下:

    main_loop()
               ├──getenv
               │           └──getenv_f
               │                      ├──env_get_char
               │                      └──envmatch
               └──run_command_list
                          └──builtin_run_command_list
                                     └──builtin_run_command
                                                ├──process_macros
                                                ├──parse_line
                                                └──cmd_process
                                                           ├──find_cmd
                                                           ├──cmd_call
                                                           │           └──cmdtp->cmd//找到命令do_bootm
                                                           │                      └──bootm_start
                                                           │                                 └──boot_start_lmb
                                                           │                                            └──arch_lmb_reserve(struct lmb *lmb)
                                                           │                                                       └──cleanup_before_linux
                                                           │                                                                  └──disable_interrupts //关闭中断
                                                           └──cmd_usage
                                                                                 └──bootm_load_os

    上面就是uboot启动Linux前大体上做的最后的工作流程, uboot做了这么多, 其实都是为了能引导OS内核,针对ARM Linux, 它对启动前的环境要求在内核文档"/Documentation/kernel-parameters.txt""Documentation/arm/Booting"" 有叙述,其中就可以解释uboot为什么做了上面这些工作. 文档中对于在ARM平台中启动Linux内核, 作了如下6条要求, 我们逐条解释:

    Setup and initialise the RAM.

    这里的RAM指的是DRAM, 因为Linux内核需要在DRAM中运行, 而DRAM必须初始化, 这部分工作在BL2中完成了.

    Initialise one serial port.

    初始化一个串口, bootloader应该初始化一个串口, 这样内核串口驱动才可以自动探测到给控制台用的串口是哪个
    bootloader可以在taglist中使用console=来指定一个串口, 这部分工作在BL2中也做了

    Detect the machine type.

    探测板子类型, Linux内核需要知道自己运行的板子类型, 这部分工作也交给bootloader来完成, bootloader通过一些方式获得了板子的类型后, 按照内核源码的"arch/arm/tools/mach-types"中的描述, 将板子的type编号存储在r1寄存器

    Setup the kernel tagged list.

    建立tag列表, 也就是内核参数, 在内核源码和uboot源码中都使用一个struct tag来描述. 数据结构 tag 和 tag_header 定义在 Linux 内核源码的"include/asm/setup.h" 头文件中:

    1. 有效的tagged list 必须以ATAG_CORE开始并且以ATAG_NONE结束, 空的ATAG_CORE的size域是0x00000002, ATAG_NONE的size域是0。
    2. list中可以放置任意数目的tag, 名称相同的tag的后果未定义, 最终的取值可能是之前的, 也可能是之后的
    3. bootloader至少要将系统内存的位置, 内存的大小以及根文件系统的位置传递给内核
    4. taggedlist放置的位置不能和内核自解压区域或initrd的bootp程序相冲突, 防止被重写, 建议的地址是RAM的头16KB
    5. bootloader必须把dtb放置在64bit对齐的已经初始化过的内存中, dtb的格式在"Documentation/devicetree/booting-without-of.txt"中, 其中定义了设备树的大小
    struct boot_param_header {
        __be32 magic;                   //设备树魔数,固定为0xd00dfeed
        __be32 totalsize;               //整个设备树的大小
        __be32 off_dt_struct;           //保存结构块在整个设备树中的偏移
        __be32 off_dt_strings;          //保存的字符串块在设备树中的偏移
        __be32 off_mem_rsvmap;          //保留内存区,该区保留了不能被内核动态分配的内存空间
        __be32 version;                 //设备树版本
        __be32 last_comp_version;       //向下兼容版本号
        __be32 boot_cpuid_phys;         //为在多核处理器中用于启动的主cpu的物理id
        __be32 dt_strings_size;         //字符串块大小
        __be32 dt_struct_size;          //结构块大小
    };
    

    在内核关于启动参数的约定中, 它认为r2中的地址可能是设备树的地址, 也可能是tagged list的地址, 所以, 拿到这个地址后,内核首要的工作就是判断到底是什么,判断的依据就是判断其第一个32bit上存储的到底是设备树魔数0xd00dfeed还是ATAGS_CORE。参见内核"arch/arm/kernel/head-common.h"这里我有一个疑问, 既然r2可能会存放dtb的地址, 那此时内核是如何找到tagged list的呢???。实际开发中一般都是将tagged list的地址放到r2中。内核推荐将dtb放置在RAM开始的128MB处

    Load initramfs.

    加载ramfs,ramfs推荐正好放在设备树上面

    Calling the kernel image

    启动内核镜像,如果使用的flash中的zImage,bootloader可以直接将zImage加载到内存并执行,Linux内核对非zImage内核镜像的地址有更严格的要求————镜像必须加载到PAGE_OFFSET + TEXT_OFFSET处。PAGE_OFFSET定义了虚拟地址空间中内核空间的起始地址,32bit系统中就是3GB处。TEXT_OFFSET表示内核空间中开始的用来保存内核的页表(也就是进程0的PGD)、bootload和kernel传递参数的一块空间的大小,对于arm,TEXT_OFFSET是32kB,由于内核空间的前896MB(3GB~3GB+896MB)是一一映射的,所以将内核加载到物理地址0x4000 0000其实就是加载到虚拟地址空间的3GB处,考虑到上述的32KB用来保存页表、内核参数等,我们需要将内核解压到0x40008000处运行,即虚拟地址的(3GB+32KB),如果使用uImage,这个参数在制作uImage的时候被写入到了文件头中。我们在uboot的启动参数通常不指定加载到这个地址,因为内核自解压之后会自搬移到0x40008000处开始执行,如果我们指定的就是这个地址,那么内核首先会自搬移到别处,解压之后再搬回来执行,防止解压过程中将未解压的部分覆盖造成错误。

    无论是哪种启动方式,内核启动的时候都必须满足下面的条件:

    1. r0=0;r1=板子类型号;r2=内核中tagged list或设备树地址
    2. 所有的IRQ FIQ必须关闭
    3. 必须是ARM状态, SVC模式
    4. MMU必须关闭
    5. iCache可以关闭也可以不管
    6. dCache必须关闭
    7. DMA设备必须关闭

    我们已经分析了整个Uboot的启动框架,在细节上,Uboot必须完成上面这些工作以满足Linux的启动要求。

  • 相关阅读:
    Leetcode 1489找到最小生成树李关键边和伪关键边
    Leetcode 113 路径总和 II
    hdu 1223 还是畅通工程
    hdu 1087 Super Jumping! Jumping! Jumping!
    hdu 1008 Elevator
    hdu 1037 Keep on Truckin'
    湖工oj 1241 畅通工程
    湖工oj 1162 大武汉局域网
    hdu 2057 A + B Again
    poj 2236 Wireless Network
  • 原文地址:https://www.cnblogs.com/xiaojiang1025/p/6496704.html
Copyright © 2020-2023  润新知