• 热门专题

uboot源码分析之C语言段

作者:CrazyCatJack  发布日期:2016-11-28 21:01:55
Tag标签:源码  语言  
  • 题外话:

      最近一直在学习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

About IT165 - 广告服务 - 隐私声明 - 版权申明 - 免责条款 - 网站地图 - 网友投稿 - 联系方式
本站内容来自于互联网,仅供用于网络技术学习,学习中请遵循相关法律法规