认识 uboot 和 内核 之间不可不说的关系

uboot 镜像为 uboot.bin,Linux 镜像为 zImage

嵌入式设备中的分区表是自己定义的,uboot 和内核中的分区表应一致

内核运行前必须加载到 ddr 中指定的地址处

uboot 需要提供内核必要的参数

内核启动的方式

uboot 启动内核有两种方式,一种是等待倒计时结束后直接启动内核,一种是在 uboot 命令行中使用 boot 命令启动内核

其代码分别如下

其中 parse_string_outer 的作用是解析 boot 参数并执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*------------------倒计时----------------------*/
s = getenv ("bootcmd");

if (bootdelay >= 0 && s && !abortboot (bootdelay)) {
...
parse_string_outer(s, FLAG_PARSE_SEMICOLON |
FLAG_EXIT_FROM_LOOP);
...
}
/*------------------命令行----------------------*/
int do_bootd (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
...
if (parse_string_outer (getenv ("bootcmd"),
FLAG_PARSE_SEMICOLON | FLAG_EXIT_FROM_LOOP) != 0)
rcode = 1;
...
}

U_BOOT_CMD(
boot, 1, 1, do_bootd,
"boot - boot default, i.e., run 'bootcmd'\n",
NULL
);
/*-----------------相关宏定义----------------------*/
#ifdef CONFIG_BOOTARGS
"bootargs=" CONFIG_BOOTARGS "\0"
#endif
#ifdef CONFIG_BOOTCOMMAND
"bootcmd=" CONFIG_BOOTCOMMAND "\0"
#endif

#define CONFIG_BOOTARGS "console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3"
#define CONFIG_BOOTCOMMAND "movi read kernel 30008000; movi read rootfs 30B00000 300000; bootm 30008000 30B00000"

加载内核到DDR中

uboot 启动内核的步骤

  • 内核镜像从启动介质中加载到DDR中
  • 去DDR中启动内核镜像

本文使用的开发板 x210 将镜像存放在 SD 卡中,要加载到 ddr 中需要使用到 movi 指令

movi 提供了对 iNand/SD 卡的操作,movi read 用来读取 iNand/SD 卡中的内容到DDR中;movi write 用来将DDR中的内容写入到 iNand/SD 卡中

上面的代码中 bootcmd 中的命令就是用来加载 kernel rootfs 到 ddr

除了从 SD 卡加载,还可以通过 tftp nfs 等网络下载方式加载镜像

通过 movi read kernel 30008000 可以知道,内核加载到了 0x30008000 的位置

内核的镜像生成

Linux 直接编译得到 elf 文件,叫 vmlinux 或 vmlinuz。这种文件会比较大,为了烧录方便,会使用 objcopy 工具制作成镜像文件,叫 Image(从78M精简成了7.5M)

早期使用的软盘比较小,Image 对与软盘来说还是太大了,放不下。Linux 对 Image 做进一步的压缩,并在压缩文件前端附加了一部分解压缩代码,形成 zImage

uboot 可以使用 mkimage 工具,在 zImage 前面加上64字节的uImage的头信息,形成 uImage

加载启动内核

内核的加载启动是通过 do_bootm 完成的

前面介绍过,镜像文件分为两个部分,头部以及真正的内核

所以 do_bootm 会先对镜像进行头部信息的校验,然后再进行内核的启动

头部信息的结构体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;

在 do_bootm 中就是通过 ih_os 判断镜像的类型,然后使用相应的方法启动内核

这里的镜像是 Linux 镜像,所以使用的是 do_bootm_linux, do_bootm_linux 的参数大部分是通过 do_bootm 传递的

启动的参数 bootm 30008000,告诉 uboot 去 30008000 这个地址去找镜像文件

内核启动

镜像的程序入口叫做 entrypoint ,在 do_bootm_linux 中使用 ep 保存,镜像的程序入口在头信息的 ih_ep 中,可以通过读取头信息得到

得到 ep 后,通过 theKernel = (void (*)(int, int, uint))ep; 将 ep 格式化后传递给 theKernel ,这样 theKernel 函数就指向了内存中加载的OS镜像的真正入口地址

前面也提到了,每个开发板在 uboot 中都有唯一的机器码,这个编码用来验证开发板与 uboot 是否匹配,这个机器码还会传到内核中再次验证。这个机器码获取的第一顺序备选是环境变量machid,第二顺序备选是gd->bd->bi_arch_num(x210_sd.h 中的 #define MACH_TYPE 2456)

接下来就是传参的过程。先看看 Linux 的 Documentation/arm/Booting 中对 CPU 寄存器设置的描述

1
2
3
4
5
- CPU register settings
r0 = 0,
r1 = machine type number discovered in (3) above.
r2 = physical address of tagged list in system RAM, or
physical address of device tree block (dtb) in system RAM

通过读取 r0 r1 r2 这三个寄存器的值来设置 CPU ,r0 固定为0,r1 为前面提到的机器码,r2 为存放启动参数 tag 结构体的首地址

所以在 do_bootm_linux 通过 theKernel (0, machid, bd->bi_boot_params); 完成传参的过程

传参是通过 struct tag 这个结构体完成的,获取参数就是获取一个个 tag 的过程。这些 tag 也有着规定的格式,do_bootm_linux 中通过 setup_start_tag 和 setup_end_tag 函数设置 tag 的开始和结束,这个函数的作用就是设置当前 tag 的类型为 ATAG_CORE 和 ATAG_NONE ,用作 tag 起始终止位置的判别

需要注意的是,传参是一个很重要的过程,内核启动不成功与传参错误有很大关系

uboot 启动4步骤总结

第一步:将内核搬移到DDR中

第二步:校验内核格式、CRC等

第三步:准备传参

第四步:跳转执行内核

参考资料