zoukankan      html  css  js  c++  java
  • vboot完全解读

              上半个月在学习bootloader,突然找到了一个非常好的vboot,vboot只有最基本的内核引导功能(基于s3c2440,从nand flash启动),对其深入研究后,发现对bootloader有了比较全面的理解,虽然没有像uboot那么多功能,但vboot已经实现了bootloader最核心的功能,其他像什么网络功能、烧写功能等等也只是一些裸机驱动而已。学习bootloader需要有汇编的基础,如果有单片机编程经验的话那更是“如鱼得水”了。

           先看vboot的整体架构,下面是vboot包含的所有文件:

    很简单是吧,其中核心的文件是head.S、main.c和nand.c,vboot.bin已经是编译出来的二进制文件,用于烧写在nand flash里。先看mem.lds文件,这是一个链接脚本,从那里可以找到程序的入口:

    1 SECTIONS { 
    2   . = 000000;
    3   .myhead ALIGN(0): {*(.text.FirstSector)}
    4   .text ALIGN(512): { *(.text) }
    5   .bss ALIGN(4)  : { *(.bss*)  *(COMMON) }
    6   .data ALIGN(4) : { *(.data*) *(.rodata*) }
    7 } 

    比较简单,程序入口位于text.FirstSector这个段里(因为程序是从nand flash的0地址开始执行的),它在head.S文件里定义:

     1     .section .text.FirstSector
     2     .globl first_sector
     3 
     4 first_sector:
     5 @ 0x00: Reset
     6     b    Reset
     7 
     8 @ 0x04: Undefined instruction exception
     9 UndefEntryPoint:
    10     b    UndefEntryPoint
    11 
    12 @ 0x08: Software interrupt exception
    13 SWIEntryPoint:
    14     b    SWIEntryPoint
    15 
    16 @ 0x0c: Prefetch Abort (Instruction Fetch Memory Abort)
    17 PrefetchAbortEnteryPoint:
    18     b    PrefetchAbortEnteryPoint
    19 
    20 @ 0x10: Data Access Memory Abort
    21 DataAbortEntryPoint:
    22     b    DataAbortEntryPoint
    23 
    24 @ 0x14: Not used
    25 NotUsedEntryPoint:
    26     b    NotUsedEntryPoint
    27 
    28 @ 0x18: IRQ(Interrupt Request) exception
    29 IRQEntryPoint:
    30     b    IRQHandle
    31 
    32 @ 0x1c: FIQ(Fast Interrupt Request) exception
    33 FIQEntryPoint:
    34     b    FIQEntryPoint
    35 
    36 @0x20: Fixed address global value. will be replaced by downloader.
    37 
    38     .long ZBOOT_MAGIC
    39     .byte OS_TYPE, HAS_NAND_BIOS, (LOGO_POS & 0xFF), ((LOGO_POS >>8) &0xFF)
    40     .long OS_START
    41     .long OS_LENGTH
    42     .long OS_RAM_START
    43     .string LINUX_CMD_LINE

    第5~34行的作用是安装异常向量表,在这里除了复位,其他异常都没有定义具体的执行代码。

     1 .section .text
     2 Reset:
     3     @ 关闭看门狗
     4     mov    r1, #0x53000000
     5     mov    r2, #0x0
     6     str    r2, [r1]
     7 
     8     @ 关闭中断
     9     mov    r1, #INT_CTL_BASE
    10     mov    r2, #0xffffffff
    11     str    r2, [r1, #oINTMSK]
    12     ldr    r2, =0x7ff
    13     str    r2, [r1, #oINTSUBMSK]    
    14 
    15     @ 初始化系统时钟
    16     mov    r1, #CLK_CTL_BASE
    17     mvn    r2, #0xff000000
    18     str    r2, [r1, #oLOCKTIME]   @设置LOCKTIME寄存器
    19     
    20     mov    r1, #CLK_CTL_BASE
    21     ldr    r2, clkdivn_value
    22     str    r2, [r1, #oCLKDIVN]    @设置分频寄存器
    23 
    24     mrc    p15, 0, r1, c1, c0, 0        @ read ctrl register 
    25     orr    r1, r1, #0xc0000000          @ Asynchronous 异步总线模式 
    26     mcr    p15, 0, r1, c1, c0, 0        @ write ctrl register
    27 
    28     mov    r1, #CLK_CTL_BASE
    29     ldr r2, =S3C2440_UPLL_48MHZ_Fin12MHz
    30     str r2, [r1, #oUPLLCON]
    31 
    32     nop
    33     nop
    34     nop
    35     nop
    36     nop
    37     nop
    38     nop
    39     nop
    40     nop
    41     
    42     ldr    sp, DW_STACK_START           @ setup stack pointer
    43 
    44     ldr     r2, mpll_value_USER         @ clock user set 12MHz
    45     str    r2, [r1, #oMPLLCON]
    46     bl    memsetup
    47 
    48     @ set GPIO for UART
    49     mov    r1, #GPIO_CTL_BASE
    50     add    r1, r1, #oGPIO_H
    51     ldr    r2, gpio_con_uart    
    52     str    r2, [r1, #oGPIO_CON]
    53     ldr    r2, gpio_up_uart
    54     str    r2, [r1, #oGPIO_UP]    
    55     bl    InitUART
    56 
    57 
    58     @ get read to call C functions
    59     mov    fp, #0            @ no previous frame, so fp=0
    60     mov    a2, #0            @ set argv to NULL 
    61 
    62     bl    Main            
    63 
    64 1:    b    1b @

    第4~6行,关闭看门狗,以免系统不断复位;第9~13行,关闭中断;第16~18行,设置系统时钟稳定(锁定)时间;第20~22行,设置时钟分频比为1:4:8(FCLK:HCLK:PCLK);第24~26行,设置为异步总线模式(因为FCLK已经不等于HCLK);第28~30,行,设置UPLL为48MHZ,用于USB通信;第42行,设置栈指针,为下面调用c程序做准备;第44~45行,设置FCLK为400MHZ,那么HCLK=100MHZ,PCLK=50MHZ;第46行,跳到内存初始化程序:

     1 memsetup:
     2     @ initialise the static memory 
     3 
     4     @ set memory control registers
     5     mov    r1, #MEM_CTL_BASE
     6     adrl    r2, mem_cfg_val
     7     add    r3, r1, #52       @13*4
     8 1:  ldr    r4, [r2], #4
     9     str    r4, [r1], #4
    10     cmp    r1, r3
    11     bne    1b
    12     mov    pc, lr

    2440总共有13个设置内存的寄存器,因此第7行的立即数是52(13*4);第8~11行,通过循环设置13个寄存器的值。返回到memsetup下面的代码:

     1 @ set GPIO for UART
     2     mov    r1, #GPIO_CTL_BASE
     3     add    r1, r1, #oGPIO_H
     4     ldr    r2, gpio_con_uart    
     5     str    r2, [r1, #oGPIO_CON]
     6     ldr    r2, gpio_up_uart
     7     str    r2, [r1, #oGPIO_UP]    
     8     bl    InitUART
     9 
    10 
    11     @ get read to call C functions
    12     mov    fp, #0            @ no previous frame, so fp=0
    13     mov    a2, #0            @ set argv to NULL 
    14 
    15     bl    Main            
    16 
    17 1:    b    1b @

    第2~8行,用于初始化串口(115200bps,8N1);第12~13行,设置两个arm寄存器;第15行,跳到Main函数执行。在main.c文件里:

     1 void Main(void)
     2 {
     3     MMU_EnableICache();
     4     MMU_EnableDCache();
     5 
     6     Port_Init();
     7     NandInit();
     8 
     9     if (g_page_type == PAGE_UNKNOWN) {
    10         Uart_SendString("\r\nunsupport NAND\r\n");
    11         for(;;);
    12     }
    13 
    14     GetParameters();
    15 
    16     Uart_SendString("loading Image of Linux from Nand Flash...\n\r");
    17     ReadImageFromNand();
    18 }

    第3~4行,使能Dcache和Icache:

    static inline void MMU_EnableICache(void)
    {
    
        asm (
            "mrc p15,0,r0,c1,c0,0\n"
            "orr r0,r0,#(1<<12)\n"
            "mcr p15,0,r0,c1,c0,0\n"
        );
    }
    
    static inline void MMU_EnableDCache(void)
    {
        asm (
            "mrc p15,0,r0,c1,c0,0\n"
            "orr r0,r0,#(1<<2)\n"
            "mcr p15,0,r0,c1,c0,0\n"
        );
    }

    第6行,初始化一些IO口(没用到);第7行,初始化nand flash控制器,在nand.c文件里定义:

    void NandInit(void)
    {
        NFCONF = (TACLS << 12) | (TWRPH0 << 8) | (TWRPH1 << 4) | (0 << 0);
        NFCONT =
            (0 << 13) | (0 << 12) | (0 << 10) | (0 << 9) | (0 << 8) | (0 << 6) |
            (0 << 5) | (1 << 4) | (1 << 1) | (1 << 0);
        NFSTAT = 0;
        NandReset();
        NandCheckId();
    }

    设置具体nand flash芯片的时序参数、页的大小和位宽等,初始化之后,就可以读写nand flash了。回到Main函数的第14行调用的GetParameters()函数的定义:

    static inline void GetParameters(void)
    {
        U32 Buf[2048];
        g_os_type = OS_LINUX;
        //内核在flash中的起始地址
        g_os_start = 0x50000;
        //内核映像的大小
        g_os_length = 0x300000;
        //内核被拷贝到内存的起始地址
        g_os_ram_start = 0x30008000;
    
        // vivi LINUX CMD LINE
        //从flash的参数分区中读命令行参数
        NandReadOneSector((U8 *)Buf, 0x40000);
        if (Buf[0] == 0x49564956 && Buf[1] == 0x4C444D43) {
            memcpy(g_linux_cmd_line, (char *)&(Buf[2]), sizeof g_linux_cmd_line);
        }
    }

    设置了内核映像在nand flash的起始地址和大小,还有设置内核映像被拷贝到ram的起始地址,命令行参数是通过BIOS(nor flash里的supervivi)写到nand flash的0x40000地址处,通过NandReadOneSector()把它读出来,其中Buf[0]、Buf[1]这两个值是“暗藏值”,是对应于具体的BIOS的,是由BIOS写进去的,位于命令行参数的第一和第二个字,因为BIOS的代码不不开源的,无法修改,所以移植vboot的时候只要是用这个BIOS来烧写vboot就不用修改两个值(不用太纠结,我曾纠结了很久)。从memcpy()函数也可以知道,Buf[0]和Buf[1]这两个值是用来识别具体的BIOS的,没用于命令行参数。现在看NandReadOneSector()函数:

     1 int NandReadOneSector(U8 * buffer, U32 addr)
     2 {
     3     int ret;
     4     
     5     switch(g_page_type) {
     6     case PAGE512:
     7         ret = NandReadOneSectorP512(buffer, addr);
     8         break;
     9     case PAGE2048:
    10         ret = NandReadOneSectorP2048(buffer, addr);
    11         break;
    12     default:
    13         for(;;);
    14     }
    15     return ret;
    16 }

    因为我板子(GT2440)上的nand flash是64M的,页的大小为512字节,所以看第7行的调用:

    static inline int NandReadOneSectorP512(U8 * buffer, U32 addr)
    {
        U32 sector;
        sector = addr >> 9;
    
        NandReset();
    #if 0
        NF_RSTECC();
        NF_MECC_UnLock();
    #endif
        NF_nFCE_L();
    
        NF_CLEAR_RB();
        NF_CMD(0x00);
    
        NF_ADDR(0x00);
        NF_ADDR(sector & 0xff);
        NF_ADDR((sector >> 8) & 0xff);
        NF_ADDR((sector >> 16) & 0xff);
    
        delay();
        NF_DETECT_RB();
    
        ReadPage512(buffer, &NFDATA);
    #if 0
        NF_MECC_Lock();
    #endif
        NF_nFCE_H();
    
        return 1;
    }

    该函数里前面那些是设置读操作,设置读起始地址,核心是调用ReadPage512()函数,它由汇编实现,在head.S里:

     1 .globl ReadPage512
     2 
     3 ReadPage512:
     4     stmfd    sp!, {r2-r7} @ 将r2~r7寄存器的值压栈
     5     mov    r2, #0x200   @ 512个字节
     6 
     7 1:
     8     ldr    r4, [r1]
     9     ldr    r5, [r1]
    10     ldr    r6, [r1]
    11     ldr    r7, [r1]
    12     stmia    r0!, {r4-r7}
    13     ldr    r4, [r1]
    14     ldr    r5, [r1]
    15     ldr    r6, [r1]
    16     ldr    r7, [r1]
    17     stmia    r0!, {r4-r7}
    18     ldr    r4, [r1]
    19     ldr    r5, [r1]
    20     ldr    r6, [r1]
    21     ldr    r7, [r1]
    22     stmia    r0!, {r4-r7}
    23     ldr    r4, [r1]
    24     ldr    r5, [r1]
    25     ldr    r6, [r1]
    26     ldr    r7, [r1]
    27     stmia    r0!, {r4-r7}
    28     subs    r2, r2, #64  @ 一次循环读64个字节
    29     bne    1b;
    30     ldmfd    sp!, {r2-r7} @ 恢复r2~r7寄存器的值
    31     mov    pc,lr @ 返回

    挺好懂的,不多解析。再回到Main()函数的17行(最后一个函数调用)调用ReadImageFromNand():

     1 void ReadImageFromNand(void)
     2 {
     3     unsigned int Length;
     4     U8 *RAM;
     5     unsigned BlockNum;
     6     unsigned pos;
     7 
     8     Length = g_os_length;
     9     //内核的大小(单位:块)
    10     Length = (Length + BLOCK_SIZE - 1) >> (BYTE_SECTOR_SHIFT + SECTOR_BLOCK_SHIFT) << (BYTE_SECTOR_SHIFT + SECTOR_BLOCK_SHIFT); // align to Block Size
    11     //内核在flash中的第几块
    12     BlockNum = g_os_start >> (BYTE_SECTOR_SHIFT + SECTOR_BLOCK_SHIFT);
    13     //要拷贝到的起始地址
    14     RAM = (U8 *) g_os_ram_start;
    15     for (pos = 0; pos < Length; pos += BLOCK_SIZE) {
    16         unsigned int i;
    17         // skip badblock
    18         //坏块检测
    19         for (;;) {
    20             if (NandIsGoodBlock
    21                 (BlockNum <<
    22                  (BYTE_SECTOR_SHIFT + SECTOR_BLOCK_SHIFT))) {
    23                 break;
    24             }
    25             BlockNum++;    //try next
    26         }
    27         for (i = 0; i < BLOCK_SIZE; i += SECTOR_SIZE) {
    28             int ret =
    29                 NandReadOneSector(RAM,
    30                           (BlockNum <<
    31                            (BYTE_SECTOR_SHIFT +
    32                         SECTOR_BLOCK_SHIFT)) + i);
    33             RAM += SECTOR_SIZE;
    34             ret = 0;
    35 
    36         }
    37 
    38         BlockNum++;
    39     }
    40 
    41     CallLinux();
    42 }

    主要是从nand flash里把内核映像一块一块地读到ram里,每读一块之前先进行坏块检测,如果是坏块就跳过,继续读下一块(这里的坏块检测是一个比较粗略的检测方法),直到把整个内核映像读到ram里面。这里内核映像的大小设置为3M(实际上不到3M),因此读也是读3M大小到ram里面。最后该函数的第41行调用CallLinux():

     1 static void CallLinux(void)
     2 {
     3     struct param_struct {
     4         union {
     5             struct {
     6                 unsigned long page_size;    /*  0 */
     7                 unsigned long nr_pages;    /*  4 */
     8                 unsigned long ramdisk_size;    /*  8 */
     9                 unsigned long flags;    /* 12 */
    10                 unsigned long rootdev;    /* 16 */
    11                 unsigned long video_num_cols;    /* 20 */
    12                 unsigned long video_num_rows;    /* 24 */
    13                 unsigned long video_x;    /* 28 */
    14                 unsigned long video_y;    /* 32 */
    15                 unsigned long memc_control_reg;    /* 36 */
    16                 unsigned char sounddefault;    /* 40 */
    17                 unsigned char adfsdrives;    /* 41 */
    18                 unsigned char bytes_per_char_h;    /* 42 */
    19                 unsigned char bytes_per_char_v;    /* 43 */
    20                 unsigned long pages_in_bank[4];    /* 44 */
    21                 unsigned long pages_in_vram;    /* 60 */
    22                 unsigned long initrd_start;    /* 64 */
    23                 unsigned long initrd_size;    /* 68 */
    24                 unsigned long rd_start;    /* 72 */
    25                 unsigned long system_rev;    /* 76 */
    26                 unsigned long system_serial_low;    /* 80 */
    27                 unsigned long system_serial_high;    /* 84 */
    28                 unsigned long mem_fclk_21285;    /* 88 */
    29             } s;
    30             char unused[256];
    31         } u1;
    32         union {
    33             char paths[8][128];
    34             struct {
    35                 unsigned long magic;
    36                 char n[1024 - sizeof(unsigned long)];
    37             } s;
    38         } u2;
    39         char commandline[1024];
    40     };
    41     //启动参数在内存的起始地址
    42     struct param_struct *p = (struct param_struct *)0x30000100;
    43     memset(p, 0, sizeof(*p));
    44     memcpy(p->commandline, g_linux_cmd_line, sizeof(g_linux_cmd_line));
    45     //内存页的大小4K
    46     p->u1.s.page_size = 4 * 1024;
    47     //内存总共有多少页
    48     p->u1.s.nr_pages = 64 * 1024 * 1024 / (4 * 1024);
    49 
    50     {
    51         unsigned int *pp = (unsigned int *)(0x30008024);
    52         if (pp[0] == 0x016f2818) {  //zImage的魔数,在内核中定义
    53             //Uart_SendString("\n\rOk\n\r");
    54         } else {
    55             Uart_SendString("\n\rWrong Linux Kernel\n\r");
    56             for (;;) ;
    57         }
    58 
    59     }
    60      asm (
    61         "mov    r5, %2\n"
    62         "mov    r0, %0\n"
    63         "mov    r1, %1\n"
    64         "mov    ip, #0\n"
    65         "mov    pc, r5\n"
    66         "nop\n" "nop\n":    /* no outpus */
    67         :"r"(0), "r"(782), "r"(g_os_ram_start)
    68     );
    69 }

    首先定义了一个struct param_struct结构体变量,从这里就可以看出,vboot用的是旧的方式(新的是用tag方式),struct param_struct与内核里定义的一样。第41~59行,看注释可以明白,第60~67行,是内核的一些约定:

    R0 = 0

    R1 = 机器ID

    .....

    最后第65行,设置pc为内核映像在内存中的起始地址,直接跳到内核映像的入口,从而开始内核代码的执行......

    总结:

          vboot是一个十分精简的bootloader,从nand flash启动,目前只支持2440 Linux,只有引导内核的功能,它的编译后的二进制文件不会超过4K(这是由2440从nand flash启动所限制的),编译vboot只需要在代码目录下执行make,便可生成vboot.bin文件,通过BIOS将它烧写到nand flash里。强烈推荐想学习ARM bootloader的同学从vboot开始入手。

  • 相关阅读:
    java 14 -7 Date
    java 14 -6 BigInteger和BigDecimal
    java 14 -5 System类
    java14-4 Pattern和Matcher类的使用
    java 14-3 正则表达式的分割
    转:StringBuilder与StringBuffer的区别(转)
    kafka之config/server.properties配置参数说明
    Kafka内核理解:消息的收集/消费机制
    kafka删除topic及其相关数据
    kafka使用问题解决
  • 原文地址:https://www.cnblogs.com/lknlfy/p/2655743.html
Copyright © 2011-2022 走看看