ARM板移植Linux系统启动(七)uBoot中的启动器

前面说了那么多,系统已经烧写完毕,按理说没什么问题了,但是我的系统依然跑不起来!通过debug发现,我的kernel镜像按照debian文件系统的要求重命名成了”vmlinuz-XXX”,但我的烧写进去的uboot却在打印信息中明确表示:我找的是zImage!在BeagleBoneBlack的TF卡烧写启动模式中,它的kernel也是修改了命名,但却能顺利引导,是因为把kernel的文件名信息写入了uEnv.txt文件,但是为什么在我的板子上就会失败呢?

你看到的是非授权版本!爬虫凶猛,请尊重知识产权!

转载请注明出处:http://conanwhf.github.io/2017/06/14/bootup-7-loader/

访问原文「ARM板移植Linux系统启动(七)uBoot中的启动器」获取最佳阅读体验并参与讨论


又是一通debug……原来,启动BeagleBoneBlack的TF卡上的uboot跟我编译出来的不一样,它会事先尝试读取uEnv.txt文件,如果成功的话,就用文件中配置的环境变量覆盖原有的,从而达到根据不同配置来引导的效果。这就牵涉到了uboot中的启动器,而这个步骤,在我拿到的release里面是没有的😳。这个问题卡住了我不少时间,很奇怪为什么和烧写相对应的源码,在TI的release里面竟然没有放出来,竟然需要自己修改,也不知道是不是我什么地方搞错了。
还是老规矩,我们通过阅读源码一步一步地解决问题。

启动器的调用流程

选择从某设备启动

上次在讲uboot启动过程的时候,提到如何修改autoboot的尝试启动顺序:include/configs/am335x_evm.h文件中定义的

define BOOT_TARGET_DEVICES(func) \
    func(MMC, mmc, 0) \
    func(LEGACY_MMC, legacy_mmc, 0) \
    func(MMC, mmc, 1) \
    func(LEGACY_MMC, legacy_mmc, 1) \
    func(NAND, nand, 0) \
    func(PXE, pxe, na) \
    func(DHCP, dhcp, na)
endif

这里的每一行代表了一种启动方式,以LEGACY_MMC为例,可以看到他们表面上是一个很长的字符串,实际上是一个函数的执行列表,或者说执行他们就等于执行一个函数队列。

define BOOTENV_DEV_LEGACY_MMC(devtypeu, devtypel, instance) \
"bootcmd_" #devtypel #instance "=" \
"setenv mmcdev " #instance"; "\
"setenv bootpart " #instance":1 ; "\
"run mmcboot\0"

这些setenv, run有没有很熟悉?是的,他们就是uboot中的命令,在运行的时候uboot也可以直接调用他们以及所有的环境变量。

对应的启动函数

上面步骤的核心是函数(为了方便,权当它就是函数了)mmcboot,它定义在include/configs/ti_armv7_common.h。打开文件,你会看到满屏的巨长的一个字符串,不要害怕,就当它是一个个完整的函数,遵守格式修改就OK。注意:这部分代码因为本身全部是字符串的原因,对格式要求特别严格,修改的时候尽量不要从网页直接copy/paste,以避免很多格式问题。编译之前多检查空格和tab,以及每行末尾多余的空格问题,不然会造成编译失败。
可以看到,这里的函数中,有一个loadbootenv,便是用来从uEnv.txt中读取环境变量的:
"loadbootenv=load ${devtype} ${bootpart} ${loadaddr} ${bootdir}/${bootenvfile}\0"\
但mmcboot中并没有用到它,尝试在其中添加loadbootenv(格式已被注释破坏,请自行修改后再用):

"mmcboot=mmc dev ${mmcdev}; " \首先设置mmc的硬件switch
   "setenv devnum ${mmcdev}; " \
   "setenv devtype mmc; " \然后是设备号等设置
   "if mmc rescan; then " \如果mmc硬件初始化成功,
       "echo SD/MMC found on device ${mmcdev};" \
       则读取env数据,并更新环境变量
       "if run loadbootenv; then " \
           "echo Loaded env from ${bootenvfile};" \
           "run importbootenv;" \
       "fi;" \
       "echo debug: Get info=[${uname_r} ${bootfile} ${fdtfile}] ... ; " \
       尝试加载各种img并启动
       "if run loadimage; then " \
           "if test ${boot_fit} -eq 1; then " \
               "run loadfit; " \
           "else " \
               "run mmcloados;" \
           "fi;" \
       "fi;" \
   "fi;\0" \

环境变量和函数的修改

事实上,上面这些(其实是一个)字符串,就是我们在uboot命令行中使用printenv所看到的各种环境变量了。在uboot尝试读取各种文件的过程中,环境变量作为参数配置扮演了很重要的角色。从哪里读、读哪个目录、文件名是什么、读取到内存什么地址……还有引导的流程、需要传递给kernel的启动参数,无一不是靠这些环境变量来决定。读者认真阅读的话,一定能看懂uboot引导kernel的整个流程和参数的配置过程。
经过上面的修改,系统果然尝试读取uEnv.txt并且成功启动了kernel,但依然会在启动时崩溃,因为找不到可以挂载的文件系统。自己检查log,发现uboot给的启动参数为root=PARTUUID=${uuid} rw,这里的UUID又是通过finduuid=part uuid mmc ${bootpart} uuid\0来获取的。不知道为什么我编译出来的这一套系统在BeagleBoneBlack上可以正确得到uuid,在我工作的板子上却不行。由于时间关系,原因我没有深究下去,大概还是因为缺少EEPROM的缘故吧。我决定修改uboot来绕过这个问题。为了尽量保持代码原样,我添加了一个args_mmc_new来作为我的启动参数:

"args_mmc_new=setenv bootargs console=ttyS0,115200n8 ${optargs} "\
                "root=/dev/mmcblk${mmcdev}p1 rw " \
                "rootfstype=${mmcrootfstype} rootwait\0" \

然后在调用的地方替换原来的:

"mmcloados=run args_mmc_new; " \
               "if test ${boot_fdt} = yes || test ${boot_fdt} = try; then " \
                       "if run loadfdt; then " \
                               "echo debug: [${bootargs}] ... ; " \
                               "echo debug: [bootz ${loadaddr} - ${fdtaddr}] ... ; " \
                               "bootz ${loadaddr} - ${fdtaddr}; " \
                       "else " \
                               "if test ${boot_fdt} = try; then " \
                                       "bootz; " \
                               "else " \
                                       "echo WARN: Cannot load the DT; " \
                               "fi; " \
                       "fi; " \
               "else " \
                       "bootz; " \
               "fi;\0" \

至此,整个系统才是真正地能够顺利跑起来。

自己添加USB启动的支持

讲到这里,整个启动器的原理已经很清楚了。为了验证和练习,我自己照葫芦画瓢添加了一个USB启动的支持。这个support的本质跟emmc启动是一样的,只是系统的数据被烧写在USB上(不包括uboot),这样也相当于我变相做了一个不带bootloader的TF卡系统,能够更加方便地调试kernel了。毕竟可以在PC上对系统内容进行修改,万一把系统玩坏了,还能随时修复不用麻烦地重新烧写。
首先是在include/configs/am335x_evm.h文件中添加USB启动的选项:

#define BOOT_TARGET_DEVICES(func) \
    func(LEGACY_USB, legacy_usb, 0) \
    func(LEGACY_MMC, legacy_mmc, 1) \
    func(LEGACY_MMC, legacy_mmc, 0)

然后仿照EMMC定义USB的启动函数表:

#define BOOTENV_DEV_LEGACY_USB(devtypeu, devtypel, instance) \
    "bootcmd_" #devtypel #instance "=" \
    "setenv usbdev " #instance"; "\
    "setenv bootpart " #instance":1 ; "\
    "run usbboot\0"

最后在include/configs/ti_armv7_common.h添加USB启动的函数:

#define DEFAULT_USB_TI_ARGS \
    "usbdev=0\0"\
    "args_usb=setenv bootargs console=ttyS0,115200 ${optargs} " \
            "root=/dev/sda1 rw " \
            "rootfstype=ext4 rootwait\0" \
    "usbboot=usb reset; " \
            "setenv devnum ${usbdev}; " \
            "setenv devtype usb; " \
            "if usb start; then " \
                    "echo USB device found on device ${usbdev}:${bootpart};" \
                    "if run loadbootenv; then " \
                            "echo Loaded env from ${bootenvfile};" \
                            "run importbootenv;" \
                    "fi;" \
            "echo debug: Get info=[${uname_r} ${bootfile} ${fdtfile}] ... ; " \
            "if run loadimage; then " \
                    "run args_usb; " \
                    "echo debug: [${bootargs}] ... ; " \
                    "if run loadfdt; then " \
                            "echo debug: [bootz ${loadaddr} - ${fdtaddr}] ... ; " \
                            "bootz ${loadaddr} - ${fdtaddr}; " \
                    "else " \
                            "echo WARN: Cannot load the DT; " \
                    "fi; " \
            "fi;" \
    "fi;\0" \

大功告成!😊