zoukankan      html  css  js  c++  java
  • u-boot源码分析之C语言段

    题外话:

      最近一直在学习u-boot的源代码,从代码量到代码风格,都让我认识到什么才是真正的程序。以往我所学到的C语言知识和u-boot的源代码相比,实在不值一提。说到底,机器都是0和1控制的。感觉这很像我们中国《易经》里的一句话:“太极生两仪,两仪生四象。”两仪指的就是阴阳、天地,对立而又相互依存的一切,它们生成了天地万物。简单的0和1就构成了我们现在所用的操作系统,各种软件。硬件也是由高低电平控制,0和1就是万物。

      刚刚在读一本科幻小说,里面提到一种叫做“脑域”的新兴技术,意思是类比于物联网,将人类的大脑互相联网形成脑联网,俗称“脑域”。研究脑域的专家们住在最好的实验室大楼里研究技术。实验室大楼十分豪华,一切设备都是最先进的,一切生活用品、娱乐用品一应俱全。这些专家们就在这最好的环境下研究脑域技术。但是一个研究水稻的人说:“你们自以为研究脑域是在帮助这个社会,实际上这种技术也不过昙花一现罢了。我知道你们每年的产值高达几十个亿,但这又有什么用呢?你们以为自己在为社会造福,把脖子伸出窗外看看吧,在你们豪华的大楼下就是普通的市井生活,他们的生活环境脏乱差,人们的生活水平没有得到提高。人们的素质水平也没有得到提高。你们的研究就像阳春白雪,似乎很有用,但不能拯救这个社会。真正能帮这个社会的,只有那些最基本的东西,看似最无用的东西。人人都能够得到,人人都能够受益,才会真正让人们进步。”

      我思考了一下刚刚那句话,想我是不是正在研究的也是和人们生活完全不相关的东西。在宿舍里,在办公室里,研究着新兴技术,自以为是在造福社会,实际上做的事情却丝毫不会影响人们的生活?

      我想为开源世界贡献自己的力量,开发嵌入式系统,更好的控制硬件,制造出人人都能买的起的可穿戴设备,而不是什么黑科技。所以我觉得是有意义的,从这门技术的发展方向来看。我大胆预测一下吧,未来的可穿戴设备可能会发展成类似于一枚戒指的东东,一定是便于携带的,便于使用的形式存在。在尊重版权的情况下开源代码减少软件开发成本,用材料和结构减少硬件成本。让人人都买的起可穿戴设备,就像每个人都买的起手表,这才是真正的让人们受益。让大家都感到方便,感受到新科技的美好。

      科技在进步,这是必然趋势。不要害怕这种进步,而是要提高自身的道德素养。道德必须凌驾于科技之上。

    1.从我们的start_armboot开始讲起

      u-boot整体由汇编段和C语言段外加连接脚本组成。关于汇编段请看我之前的博客《u-boot源码汇编段简要分析》,好,让我们进入start_armboot函数。

      这里首先涉及到的是一个叫做gd_t的结构体指针。给它分配了一块内存。位于CFG_GBL_DATA_SIZE。我们把它打开来看:

      从作者的注释我们可以知道,这个结构体的作用是针对某些初始化较早的内存空间。在设置存储控制器之前,系统初始化的时候进行一些全局变量的定义。看里面的内容,涉及到了环境地址变量、环境检测变量、帧缓冲区的基地址、展示模式、CPU时钟、总线时钟、RAM大小、复位状态寄存器等等。回到start_armboot函数,我们接着往下看:

     

      在这里涉及到了init_sequence结构体,打开来看:

      这个结构体里面包含了cpu初始化、单板初始化、中断初始化、环境初始化、波特率初始化、串行通信、CPUINFO、BOARDINFO、RAM块等等一系列初始化函数。回到start_armboot函数接着往下看:

      在这里红框圈起来的是对Flash的初始化,flash_init()是对Nor Flash初始化,nand_init()是对Nand Flash初始化。中间是对LCD或者VFD进行初始化,并分配内存。接下来是一个 env_relocate ()函数,这个函数是对环境变量进行初始化。所谓的环境变量就是我们在用u-boot的时候从哪里读出内核,从哪里启动内核等一系列信息,如下图:

      其中bootcmd就表示你把内核读到哪里。这里明显是把内核读到0x30007FC0地址上,然后bootm 0x0007FC0,也就是启动该地址上的内核。这里还有波特率、IP地址、bootargs等u-boot环境变量。

      最后我们进入了一个循环:main_loop();这是最重要的一个函数。

    2.main_loop()函数

      

    char *s;
    
    s = getenv ("bootcmd");
    
    # ifndef CFG_HUSH_PARSER
            {
                printf("Booting Linux ...
    ");            
                run_command (s, 0);
            }
    
    
    len = readline (CFG_PROMPT);
    rc = run_command (lastcommand, flag);

      u-boot代码量很大,对于各种硬件初始化函数特别多,所以这里博主把最关键的语句摘出来。方便大家分析。这里的getenv是获取环境变量,u-boot在启动时会首先获取环境变量,所谓的环境变量我们上面通过u-boot打印出的信息就可以知道。这里的环境变量有两种,如果u-boot获取到了FLASH上的环境变量则应用此环境变量启动,否则使用默认的环境变量启动。如果在u-boot启动期间我们没有按下空格键,则在串口打印出“Booting Linux...”,并执行函数run_command启动内核。如果我们按下了空格键,则进入下面的分支语句,进入u-boot界面,readline函数获取我们输入的命令,run_command执行命令。所以run_command这个函数作用很大。

      

    if ((argc = parse_line (finaltoken, argv)) == 0) {
        rc = -1;    /* no command at all */
        continue;
    }
    /* Look up command in command table */
    if ((cmdtp = find_cmd(argv[0])) == NULL) {
        printf ("Unknown command '%s' - try 'help'
    ", argv[0]);
        rc = -1;    /* give up after bad command */
        continue;
    }
    
    /* found - check max args */
    if (argc > cmdtp->maxargs) {
        printf ("Usage:
    %s
    ", cmdtp->usage);
        rc = -1;
        continue;
    }

      在run_command函数中,我们能够看到,首先,parse_line函数是对我们输入的命令进行解析。一般我们在u-boot输入的命令大概是这样的:“命令,参数0,参数1,...”,所以由parse_line函数进行解析:argv[0]="命令字符串",argv[1]="参数1",argv[2]="参数2",...等等。而find_cmd则相当于一个switch,它把我们输入的命令与在u-boot中定义的各种命令相比对,若吻合则执行相应的函数操作。若遍历所有的命令名均没有我们输入的命令,则报错。在u-boot中,我们不是通过简单的switch进行命令的选择和函数的执行,而是将每个命令定义成结构体。我们进入find_cmd进行分析:

        /*
         * Some commands allow length modifiers (like "cp.b");
         * compare command name only until first dot.
         */
        len = ((p = strchr(cmd, '.')) == NULL) ? strlen (cmd) : (p - cmd);
    
        for (cmdtp = &__u_boot_cmd_start;
             cmdtp != &__u_boot_cmd_end;
             cmdtp++) {
            if (strncmp (cmd, cmdtp->name, len) == 0) {
                if (len == strlen (cmdtp->name))
                    return cmdtp;    /* full match */
    
                cmdtp_temp = cmdtp;    /* abbreviated command ? */
                n_found++;
            }
        }
        if (n_found == 1) {            /* exactly one match */
            return cmdtp_temp;
        }

      这是find_cmd函数的主要代码,这里根据作者的注释我们可以知道,和我们刚刚讲的一样,就是进行命令名的比对。strncmp大家都很熟悉这个字符串比较函数,若比对成功则返回这个命令的结构体指针,否则继续遍历,若最终没有找到则报错。这里的__u_boot_cmd_start和__u_boot_cmd_end是在u-boot.lds连接脚本里定义。 

    __u_boot_cmd_start = .;
     .u_boot_cmd : { *(.u_boot_cmd) }
     __u_boot_cmd_end = .;

      对于这里的u_boot_cmd大家可能不知道它是干什么的,在哪里定义的。通过查找,我们可以发现在Command.h中有一段相关的定义,是一个结构体:

    #define Struct_Section  __attribute__ ((unused,section (".u_boot_cmd")))

      现在我们还是不知道它是干嘛的,在此搜索bootm,在Cmd_bootm.c中,我们看到了这样的一个定义:

     

    U_BOOT_CMD(
         bootm,    CFG_MAXARGS,    1,    do_bootm,
         "bootm   - boot application image from memory
    ",
         "[addr [arg ...]]
        - boot application image stored in memory
    "
         "	passing arguments 'arg ...'; when booting a Linux kernel,
    "
         "	'arg' can be the address of an initrd image
    "
    #ifdef CONFIG_OF_FLAT_TREE
        "	When booting a Linux kernel which requires a flat device-tree
    "
        "	a third argument is required which is the address of the of the
    "
        "	device-tree blob. To boot that kernel without an initrd image,
    "
        "	use a '-' for the second argument. If you do not pass a third
    "
        "	a bd_info struct will be passed instead
    "
    #endif
    );

      这里其实就定义了u_boot_cmd的这条指令的具体参数。不过此刻我们还是不明白这些参数的具体含义,再次查找u_boot_cmd,在Command.h中我们终于找到了对于bootm这条指令的宏定义:

      #define U_BOOT_CMD(name,maxargs,rep,cmd,usage,help)
      cmd_tbl_t __u_boot_cmd_##name Struct_Section = {#name, maxargs, rep, cmd, usage, help}

      现在开始,我们解析bootm这条命令:

      name:命令名,这里将##name替换成bootm。

      Struct_Section:结构体段,根据上面我们找到的宏定义,替换成__attribute__ ((unused,section (".u_boot_cmd")))。

      #name:命令名,替换成"bootm"。

      maxargs:最大参数,替换成CFG_MAXARGS。

      rep:是否可重复,替换成1(可重复)。

      cmd:命令,替换成do_bootm。

      这里的usage表示短命令,help表长命令。对应U_BOOT_CMD中的字符串。经过一系列替换,这条语句最后变成了:

      #define U_BOOT_CMD(name,maxargs,rep,cmd,usage,help)

      cmd_tbl_t __u_boot_cmd_bootm __attribute__ ((unused,section (".u_boot_cmd"))) = {"bootm", CFG_MAXARGS, 1, do_bootm, usage, help}

      其实,经过刚才的分析,现在我们可以自己写u-boot命令了。这里是博主自己写的u-boot命令:

      命令名是"CCJ",执行这条命令显示的信息为:"Hi! I am CrazyCatJack!"并对输入的命令及参数进行输出。还有长信息和短信息如图所示。

      

    #include <common.h>
    #include <watchdog.h>
    #include <command.h>
    #include <image.h>
    #include <malloc.h>
    #include <zlib.h>
    #include <bzlib.h>
    #include <environment.h>
    #include <asm/byteorder.h>
    
    int do_CCJ (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
    {
            int i;
            printf (" Hi!I am CrazyCatJack!  %d
    ",argc);
            for(i=0;i<argc;i++)
                {
                    printf("%s
    ",argv[i]);
                }
            return 0;
    }
    
    U_BOOT_CMD(
         CCJ,    CFG_MAXARGS,    1,    do_CCJ,
         "CCJ   -CCJ short information.
    ",
         "CCJ,long information...........................................................
    "
    );

      这里是博主写的源代码,大家感兴趣的可以自己分析,自己写一个u-boot命令试试。

    3.读出内核启动内核

      经过上述一系列准备,现在终于可以读出内核,并启动它了。真是相当的不容易。真的感觉佩服u-boot的作者。还记得我们读出内核和启动内核的命令吗?

    bootcmd=nand read.jffs2 0x30007FC0 kernel; 
    
    bootm 0x30007FC0

      类比于PC上的硬盘,我们硬盘上都有各种各样的分区,而我们的FLASH则是将这种分区写到了代码里,换言之,FLASH里的分区是不能动态更改的。通过u-boot打印的信息,我们了解到kernel的分区,我的kernel位于0x00060000,而且有2M的大小。

      

    #define MTDPARTS_DEFAULT "mtdparts=nandflash0:256k@0(bootloader)," 
                                "128k(params)," 
                                "2m(kernel)," 
                                "-(root)"

      这是在具体我们板子型号的头文件中对FLASH的分区。256K装bootloader,128K装各种环境变量参数,2M的kernel,最后是root。在Cmd_nand.c中的do_nand函数将会对nand进行一系列细致的初始化。现在我们已经分析完第一条指令,读出内核,下一步是启动内核。我们的内核实际上是由一个头部+内核组成。这里的头部主要包含了内核的加载地址和入口地址:

    typedef struct image_header {
        uint32_t    ih_magic;    /* Image Header Magic Number    */
        uint32_t    ih_hcrc;    /* Image Header CRC Checksum    */
        uint32_t    ih_time;    /* Image Creation Timestamp    */
        uint32_t    ih_size;    /* Image Data Size        */
        uint32_t    ih_load;    /* Data     Load  Address        */
        uint32_t    ih_ep;        /* Entry Point Address        */
        uint32_t    ih_dcrc;    /* Image Data CRC Checksum    */
        uint8_t        ih_os;        /* Operating System        */
        uint8_t        ih_arch;    /* CPU architecture        */
        uint8_t        ih_type;    /* Image Type            */
        uint8_t        ih_comp;    /* Compression Type        */
        uint8_t        ih_name[IH_NMLEN];    /* Image Name        */
    } image_header_t;

      这就是我们内核的头部,其中最重要的就是加载地址ih_load:Image文件存放地址。还有入口地址ih_ep:内核真正开始执行的地址。

      在我们启动内核的时候,如果内核被放到了入口地址上,则无需移动内核文件,否则需执行搬运函数memmove (&header, (char *)addr, sizeof(image_header_t));将内核转移到入口地址上。

      接下来是执行do_bootm_linux函数,它主要的作用就是初始化TAG并启动内核。所谓的TAG就是u-boot需要将一些参数传递给内核,但由于二者文件格式不同,所以不能够直接进行通信。最直接的方法就是创建一个二者都能够读取的文件格式,约定好读取规则。在TAG中u-boot向即将启动的内核传递了初始化相关的信息。

    #if defined (CONFIG_SETUP_MEMORY_TAGS) || 
        defined (CONFIG_CMDLINE_TAG) || 
        defined (CONFIG_INITRD_TAG) || 
        defined (CONFIG_SERIAL_TAG) || 
        defined (CONFIG_REVISION_TAG) || 
        defined (CONFIG_LCD) || 
        defined (CONFIG_VFD)
        setup_start_tag (bd);
    #ifdef CONFIG_SERIAL_TAG
        setup_serial_tag (&params);
    #endif
    #ifdef CONFIG_REVISION_TAG
        setup_revision_tag (&params);
    #endif
    #ifdef CONFIG_SETUP_MEMORY_TAGS
        setup_memory_tags (bd);
    #endif
    #ifdef CONFIG_CMDLINE_TAG
        setup_commandline_tag (bd, commandline);
    #endif
    #ifdef CONFIG_INITRD_TAG
        if (initrd_start && initrd_end)
            setup_initrd_tag (bd, initrd_start, initrd_end);
    #endif
    #if defined (CONFIG_VFD) || defined (CONFIG_LCD)
        setup_videolfb_tag ((gd_t *) gd);
    #endif
        setup_end_tag (bd);
    #endif
    
        /* we assume that the kernel is in place */
        printf ("
    Starting kernel ...
    
    ");
    
    #ifdef CONFIG_USB_DEVICE
        {
            extern void udc_disconnect (void);
                    //udc_disconnect (); // cancled by www.100ask.net
        }
    #endif
    
        cleanup_before_linux ();
    
        theKernel (0, bd->bi_arch_number, bd->bi_boot_params);

      其中的TAG就是u-boot传递的各项参数,最后执行theKernel (0, bd->bi_arch_number, bd->bi_boot_params);内核启动完成!u-boot到此结束!接下来就是内核相关的操作了。

      u-boot源码分析到此结束。^_^

       

      版权声明:本博客为CrazyCatJack原创,未经允许禁止转载。

           博主尊重u-boot的作者DENX Software Engineering的版权,感谢他们制作出u-boot,并开源给全世界的人们学习、使用。

           本博客旨在分享学习成果,帮助大家理解u-boot源代码,让u-boot被更多的人理解并使用。若能受到启发,创造出更好的事物博主就没白写了^_^

    CCJ

    2016-11-25  17:53:21

  • 相关阅读:
    第二十九课 循环链表的实现
    第二十八课 再论智能指针(下)
    第二十七课 再论智能指针(上)
    第二十六课 典型问题分析(Bugfix)
    普通new和placement new的重载
    leetcode 581. Shortest Unsorted Continuous Subarray
    leetcode 605. Can Place Flowers
    leetcode 219. Contains Duplicate II
    leetcode 283. Move Zeroes
    leetcode 217. Contains Duplicate
  • 原文地址:https://www.cnblogs.com/CrazyCatJack/p/6100964.html
Copyright © 2011-2022 走看看