参考朱有鹏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 (¶ms);
#endif
#ifdef CONFIG_REVISION_TAG
setup_revision_tag (¶ms);
#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、跳转与函数指针的方式运行内核。