0 说明
嵌入式Bootloader,基本各大厂都在使用uboot,想要自己实现bootloader,那么对uboot启动内核前做了哪些工作认清后,自己实现bootloader将更有针对性和简单性。
本文主要是从打印上分析证明uboot启动内核前做的必要工作。uboot对启动影响不大的内容,本文不关注,因为uboot做与不做,内核都将启动。
1、uboot启动内核命令
uboot启动内核的命令在bootcmd中存储。可以在bootcmd中选择从哪里加载内核,然后启动。这里调试方便,将uboot设置为从网络加载文件然后启动,因此bootcmd可能这么设置:
bootcmd:
setenv bootargs 'console=ttymxc3,115200 imx2-wdt.timeout=120 root=/dev/mmcblk1p2 rootwait rw';dhcp zImage;dhcp 0x83000000 myd-y6ull-emmc.dtb;bootz 0x80800000 - 0x83000000
首先将内核、dtb拷贝到ddr,然后通过bootz启动内核
2、bootz需要什么
bootz命令执行分析将转门有一篇文章,这里直接分析到do_bootz结果,do_bootz根据几个阶段传入不同宏给do_bootm_states函数走不同流程,do_bootm_states有下面一段:
/* Now run the OS! We hope this doesn't return */
if (!ret && (states & BOOTM_STATE_OS_GO))
ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_GO,
images, boot_fn);
到此,uboot将不希望返回,boot_selected_os,调用注册的boot_fn进入启动内核阶段。boot_fn在bootm_os_get_boot_func中被赋值为do_bootm_linux。
do_bootm_linux中主要是boot_prep_linux、boot_jump_linux。boot_jump_linux为跳转内核。
boot_jump_linux中,关键的一行代码:
kernel_entry = (void (*)(int, int, uint))images->ep;
r2 = (unsigned long)images->ft_addr;
kernel_entry(0, machid, r2);
打印一下:
machid=0 r2=83000000 kernel_entry=80800000
因此跳转前需要:
两个地址:内核链接地址和dtb所在地址。r2=83000000就是dtb在前面bootcmd中的加载地址,80800000是内核编译后可以知道的连接地址。然后一句函数指针kernel_entry(0, machid, r2)内核就起来了。
接下来的问题是,这俩地址一个放内核,一个放设备树,然后跳转就起来了,那么bootargs怎么没体现用在哪里?bootargs中包含了内核启动的串口,文件系统等信息,是uboot和内核传递的一个关键信息。
早期uboot没有使用设备树的时候,r2传递的是一个地址,那个地址中放了很多TAG,其中有一个TAG就是存放bootargs信息的,内核从r2地址中找到bootargs。显然现在这个工作给到设备树去处理了。
因此,uboot对设备树进行了修改,在flash上的设备树基础上,增加了一些内容,其中就包含bootargs信息。我们可以从启动信息上得知uboot对设备树做了操作。
uboot中设备树加载:
TFTP from server 192.168.1.108; our IP address is 192.168.1.187
Filename 'myd-y6ull-emmc.dtb'.
Load address: 0x83000000
Loading: ###
3 MiB/s
done
Bytes transferred = 34414 (866e hex)
uboot启动前的打印
## Flattened Device Tree blob at 83000000
Booting using the fdt blob at 0x83000000
Using Device Tree in place at 83000000, end 8300b66d
可以看到,加载时候dtb是0x866e,启动的时候,设备树使用空间是b66d。
3、uboot对设备树的修改
到底uboot对设备树做了啥呢,分析代码流程太多,不如直接打印出来前后的设备树直接对比。
先确认,uboot加载的设备树在flash上的文件myd-y6ull-emmc.dtb,这个文件就是编译内核后的设备树文件。dtc -I dtb -O dts -o myd-y6ull-emmc.dts myd-y6ull-emmc.dtb改为dts。
另一个uboot启动内核调用kernel_entry,加入打印,将设备树打印出来:
将其存到txt中,先转二进制,然后再转dts
root@xxzh:~/src# xxd -r -p uboot-dtb.txt uboot-dtb.dtb
root@xxzh:~/src# dtc -I dtb -O dts -o uboot-dtb.dts uboot-dtb.dtb
对比myd-y6ull-emmc.dts(加载进ddr前)、uboot-dtb.dts(启动前)
可见将bootargs放了进来,同时增加了一段保留区域给内核,让内核不要占用设备树这块内存。
不过这些信息,完全可以在编译内核的时候定下来,这样自己实现bootloader的时候就可以不对设备树进行修改,毕竟是希望实现一个最小的boot程序。
总结
分析起uboot代码总是各种流程,各种初始化,各种跳转,层层函数迭代,抓取其主要目的是启动内核,从最根源打印,其实uboot并不需要做太多必须的工作,这里必要的工作指不存在将无法启动内核的操作。必要工作主要包含:
uboot启动内核所做必须工作
- 必要初始化:时钟、flash驱动初始化(emmc、sd、nand等),可从flash读取内核、设备树到ddr
- 初始化内核启动参数(bootargs压入设备树,可选)
- 跳转启动内核
对于uboot其他的初始化,设置,菜单等,都是可选的和无关紧要的。因此自己实现Bootloader的时候抓住以上几点即可。