Bootstrap

从零开始的UBOOT的学习7--启动内核


参考朱有鹏UBOOT全集

1、UBOOT是一个裸机程序

UBOOT的本质就是一个复杂点的裸机程序。
和我们在ARM裸机全集中学习的每一个裸机程序并没有本质区别。

2、ARM裸机第十六部分写了一个简单的shell,这东西其实就是一个mini型的UBOOT。

(1)操作系统内核本身就是一个裸机程序,和UBOOT、和其他的裸机程序并没有本质区别。
(2)区别就是操作系统运行起来后在软件上分为内核层和应用层,分层后两层的权限不同。内存访问和设备操作的管理上更加精细,内核可以随便访问各种硬件,而应用程序只能限制的访问硬件和内存地址。

3、烧写程序的时候部署在特定的地方

(1)一个完整的软件+硬件的嵌入式系统,静止时(未上电)BootLoader、kernel、rootft等必须的软件都以镜像的形式存储到启动介质中。

x210中是iNAND/SD 卡上,运行时都是在DDR内存中运行的,存储介质无关,上面的两个状态都是稳定状态,第三个状态是动态过程。,从静止态到运行态的过程,也就是启动过程。

(2)动态启动过程就是一个从SD卡逐步搬移到DDR内存中,并且运行启动代码进行相关的硬件初始化和软件架构的建立,最终达到运行时的稳定状态。

(3)静止时uboot.bin,zImage rootft都在SD卡中,他们不可能随意存在SD卡的任意位置,因此需要对SD卡进行一个分区,然后将各种镜像各自存在各自的分区中,这样在启动过程中UBOOT、内核等就知道去哪里去找谁。

UBOOT和kernel的分区表必须一致,不然的话,kernel会找不到。
同时和SD卡的实际使用的分区要一样。

4、运行时必须先加载到DDR中链接地址处

(1)uboot在第一阶段中进行重定位时将第二阶段(整个UBOOT的镜像)
加载到DDR中的0xc3e00000地址处,这个地址就是UBOOT的链接地址。

(2)内核也有类似的要求,UBOOT启动内核将内存从SD卡读取放到DDR中。
其实就是重定位的过程,不能随意放置,必须放在内核的链接地址处,否则启动不起来,比如我们使用的内核链接地址是0x30008000

(3)内核启动需要必要的启动参数
1、UBOOT的是无条件启动的,从零开始启动的
2、内核是不能开机自动完全从零开始启动的,内核启动要别人帮忙,UBOOT要帮助内核实现重定位。从SD卡到DDR中,UBOOT还要给内核提供启动参数。

5、启动内核的过程

5.1、启动内核第一步:加载内核到DDR中。
UBOOT要启动内核,分为2个步奏,第一步是将内核镜像从启动介质中加载到DDR中的链接地址处,所以内核直接就是从链接地址开始运行的。

内核根本就不需要考虑重定位,因为内核知道会有UBOOT之类的把自己加载到DDR中的链接地址处的,所以内核直接就是从链接地址开始运行的。

5.2、静态内核镜像在哪里?
(1)SD卡、INAND、NAND、NORFLASH等RAM的分区。
常规启动时各种镜像都在SD卡中,因此UBOOT只需要从SD卡的kernel分区去读取内核镜像到DDR中即可,读取要使用UBOOT的命令来读取,比如X210的INAND版本是movi命令。X210的nand版本就是NAND命令。

(2)这种启动方式来加载DDR,使用命令movi read kernel 30008000
其中kernel指的是UBOOT中的kernel分区,就是UBOOT中规定的SD卡中的一个区域范围,这个区域范围被设计来存放kernel镜像,就是所谓的kernel分区。

(3)tftp/nfs等网络下载方式从远端服务器获取镜像。
UBOOT还支持远程启动,也就是从内核镜像不烧录到开发板的SD卡中,而是放在主机的服务器中,然后需要启动的时候,UBOOT通过网络从服务器中下载镜像到开发板的DDR中。

分析总结:
最终结果要的是内核镜像到DDR中特定地址即可,不管内核镜像是怎么到DDR中的,以上2种方式各有优劣,产品出厂会设置为从SD卡中启动,客户不会还要搭建tftp服务器才能用。。,tftp下载远程启动这种方式一般用来开发。

6、镜像要放在DDR中的什么地址?

内核一定要放在链接地址处,链接地址去内核源代码的链接脚本或者Makefile中去查找。X210中是0x30008000

7、do_bootm_linux函数

找到do_bootm_linux函数。
(1)函数在UBOOT/lib_arm/bootm.c中。
(2)SI找不到(是黑色的)不代表就没有,要搜索一下才能确定。
(3)

void do_bootm_linux (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[],
             bootm_headers_t *images)
{
    ulong    initrd_start, initrd_end;
    ulong    ep = 0;
    bd_t    *bd = gd->bd;
    char    *s;
    int    machid = bd->bi_arch_number; //这个是机器码
    void    (*theKernel)(int zero, int arch, uint params);
        //这个是函数指针的方式
    int    ret;

#ifdef CONFIG_CMDLINE_TAG
    //命令行,是通过bootargs这个环境变量
    char *commandline = getenv ("bootargs");
#endif

    /* 找到内核的进入入口 */
    if (images->legacy_hdr_valid) 
    {
        ep = image_get_ep (&images->legacy_hdr_os_copy);
#if defined(CONFIG_FIT)
    } 
    else if (images->fit_uname_os)
    {
        ret = fit_image_get_entry (images->fit_hdr_os,
                    images->fit_noffset_os, &ep);
        if (ret) 
        {
            puts ("Can't get entry point property!\n");
            goto error;
        }
#endif
    } 
    else 
    {
        puts ("Could not find kernel entry point!\n");
        goto error;
    }
    //强制类型转换,把EP的指针传递给theKernel
    theKernel = (void (*)(int, int, uint))ep;

    //得到环境变量machid这个字符串的值。
    s = getenv ("machid");

    if(s) 
    {
        machid = simple_strtoul (s, NULL, 16);
        printf ("Using machid 0x%x from environment\n", machid);
    }
    ret = boot_get_ramdisk (argc, argv, images, IH_ARCH_ARM,
            &initrd_start, &initrd_end);
    if (ret)
        goto error;

    show_boot_progress (15);

    debug ("## Transferring control to Linux (at address %08lx) ...\n",
           (ulong) theKernel);

#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) || \
    defined (CONFIG_MTDPARTITION)
    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

#ifdef CONFIG_MTDPARTITION
    setup_mtdpartition_tag();
#endif

    setup_end_tag (bd);
#endif

    /* we assume that the kernel is in place */
    printf ("\nStarting kernel ...\n\n");

#ifdef CONFIG_USB_DEVICE
    {
        extern void udc_disconnect (void);
        udc_disconnect ();
    }
#endif

    cleanup_before_linux ();

    //内核的函数进行传参
    theKernel (0, machid, bd->bi_boot_params);
    /* does not return */
    return;

error:
    do_reset (cmdtp, flag, argc, argv);
    return;
}

8、镜像的entrypoint

(1)ep就是entrypoint的缩写,就是程序入口,一个镜像文件的起始执行部分不是在镜像的开头,真正的镜像文件执行时第一句代码在镜像的中部的某个字节处,相当于头是有一定的偏移量的,这个偏移量记录在头信息中。

(2)一般执行一个镜像都是,第一步先读取头信息,然后在头信息的特定地址中找MAGIC_NUM,由此来确定镜像种类,第二步对镜像进行校验,第三步再次读取头信息。--由特定地址知道这个镜像的各种信息(镜像长度、镜像种类,入口地址)。
第四种就去我们的entrypoint处开始执行镜像。

以后我们会有很多类似的动作,比如我们在读取图片信息的时候,有可能它不是一个JPG的文件。

(3)theKernel = (void (*)(int, int, uint))ep;将ep指针赋予给theKernel,则这个函数指向了内存中加载的OS镜像的真正入口地址,就是操作系统第一句执行的代码。
theKernel (0, machid, bd->bi_boot_params);
这个函数是给内核传参的函数,第一个参数是0,第二个参数是machid:就是机器码,第三个参数就是bi_boot_params:UBOOT启动的第三个参数)UBOOT的最后的一句代码。

(4)uboot在启动内核时候,机器码要传给内核,UBOOT传给内核的机器码是怎么确定的?第一顺序备选是环境变量machid,第二顺序备选是gd->bd->bi_arch_num(X210_sd.h中硬编码配置的)

(5)要启动内核,必须进行传参,110行到144行就是UBOOT在给Linux内核准备传递的参数处理。

(6)starting kernel...这个是UBOOT最后一句打印出来的东西,这句如果能够出现的,说明UBOOT的整个是成功的,也可以成功的加载了内核镜像,也校验通过了,也找到入口地址了,也试图去执行了,如果后面串口没有输出了,那么说明内核没有被成功的执行,原因一般是传参,内核在DDR中的加载地址有问题。。。。

9、传参详解

tag方式传参:
(1)struct tag,tag是一个数据结构,在UBOOT和Linux kernel中都有定义tag数据结构,而且定义是一样的。

(2)tag_header和tag_xxx,tag_header中这个tag的size和类型编码,kernel拿到一个tag中剩余的部分当作一个tag_xxx来处理。

(3)tag_start与tag_end,kernel接收到的传参是诺干个tag构成的,这些tag由tag_start起始,到tag_end结束。

(4)tag传参的方式是由Linux kernel发明的,kernel定义了这种向我传参的方式,UBOOT只是实现了这种传参方式从而可以执行给kernel传参。

(5)如何进行配置传参宏
CONFIG_SETUP_MEMORY_TAGS,tag_mem,传参内容是内存配置信息。
CONFIG_CMDLINE_TAG,tag_cmdline,传参内容是启动命令行参数,也就是UBOOT的环境变量的bootargs.
CONFIG_MTDPARTTION,传参内容是iNAND/SD卡的分区表

思考:内核是如何拿到这些tag的?
UBOOT最终是调用theKernel函数来执行Linux内核的,UBOOT调用这个函数,其实就是Linux内核时传递了三个参数,这三个参数就是UBOOT直接传递给Linux内核的三个参数。通过寄存器来传参。
第一个参数放在r0中,第二个参数放在r1中,第三个参数放在r2中。
第一个参数固定为0,第2个参数是机器码,第三个参数传递的就是大片传参tag的首地址。

移植时需要注意事项。
(1)UBOOT移植时一般只需要配置相应的宏即可。
(2)kernel启动不成功,注意传参是否成功,传参不成功首先看UBOOT中bootargs设置是否正确,其次看UBOOT是否开启了相应宏以支持传参。

2.7.8、UBOOT启动内核的总结
启动的4个步奏:

第一步:将内核搬移到DDR中
第二步:校准内核格式,CRC等
第三步:准备传参
第四步:跳转执行内核

涉及的函数:
1、do_bootm和do_bootm_linux
2、UBOOT能启动的内核格式:zImage uImage fdt方式
3、跳转与函数指针的方式运行内核。

;