LV05-01-uboot-04-uboot启动流程
本文主要是uboot启动流程的相关笔记。若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows版本 | windows11 |
Ubuntu版本 | Ubuntu16.04的64位版本 |
VMware® Workstation 16 Pro | 16.2.3 build-19376536 |
终端软件 | MobaXterm(Professional Edition v23.0 Build 5042 (license)) |
Linux开发板 | 正点原子 i.MX6ULL Linux 阿尔法开发板 |
uboot | NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03) |
linux内核 | linux-4.15(NXP官方提供) |
Win32DiskImager | Win32DiskImager v1.0 |
点击查看本文参考资料
分类 | 网址 | 说明 |
官方网站 | https://www.arm.com/ | ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档 |
https://www.nxp.com.cn/ | NXP官方网站 | |
https://www.nxpic.org.cn/ | NXP 官方社区 | |
https://u-boot.readthedocs.io/en/latest/ | u-boot官网 | |
https://www.kernel.org/ | linux内核官网 |
点击查看相关文件下载
分类 | 网址 | 说明 |
NXP | https://github.com/nxp-imx | NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库 |
https://elixir.bootlin.com/linux/latest/source | 在线阅读linux kernel源码 | |
nxp-imx/linux-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga | NXP linux内核仓库tags中的rel_imx_4.1.15_2.1.0_ga | |
nxp-imx/uboot-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga | NXP u-boot仓库tags中的rel_imx_4.1.15_2.1.0_ga | |
I.MX6ULL | i.MX 6ULL Applications Processors for Industrial Products | I.MX6ULL 芯片手册(datasheet,可以在线查看) |
i.MX 6ULL Applications ProcessorReference Manual | I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网) |
一、链接脚本u-boot.lds
1. 程序入口
我们要分析uboot的启动流程,就要找到整个程序的“入口”,找到第一行程序在哪里。程序的链接是由链接脚本来决定的,所以通过链接脚本可以找到程序的入口。如果没有编译过 uboot 的话,链接脚本为uboot源码目录中的这个文件:
1 | arch/arm/cpu/u-boot.lds |
但是这个不是最终使用的链接脚本,最终的链接脚本是在这个链接脚本的基础上生成的。编译一下 uboot,编译完成以后就会在 uboot 根目录下生成 u-boot.lds文件:
【注意】只有编译 u-boot 以后才会在根目录下出现 u-boot.lds 文件 。
我们打开这个文件,会看到如下内容:
1 | OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") |
其中第 3 行为代码当前入口点: _start,关于 _start 在uboot源码中的这个文件中有定义:
1 | arch/arm/lib/vectors.S |
我们打开着这个文件,有如下内容:
1 | .globl _start |
可以看出, _start 后面就是中断向量表,从图中的“ .section “ .vectors “,” ax ” 可以得到,此代码存放在 .vectors 段里面。
2. uboot起始地址
我们使用以下命令在 uboot 源码中查找 __image_copy_start :
1 | grep -nR "__image_copy_start" |
我们打开 u-boot.map 文件,搜索 __image_copy_start 可以找到如下内容:
1 | // 上边部分省略 ... ... |
u-boot.map 是 uboot 的映射文件,编译 uboot 后存在于源码顶层目录中,可以从此文件看到某个文件或者函数链接到了哪个地址。可以看到 __image_copy_start 为 0X87800000,而 .text 的起始地址也是 0X87800000 。
我们再打开uboot源码目录中的链接文件 u-boot.lds ,可以看到有如下语句:
1 | OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") |
可以看到第 11 行是 vectors 段, vectors 段保存中断向量表 ,我们从 arch/arm/lib/vectors.S 文件中知道 vectors.S 的代码是存在 vectors 段中的。 vectors 段的起始地址也是 0X87800000,说明整个 uboot 的起始地址就是 0X87800000,这也是为什么我们裸机可执行程序的链接起始地址选择 0X87800000 了,目的就是为了和 uboot 一致。
u-boot.lds 的第12行是将 arch/arm/cpu/armv7/start.s 编译出来的代码放到中断向量表后面。
u-boot.lds 的第13行是 text 段,其他的代码段就放到这里 。
3. 与地址相关的变量
在 u-boot.lds 中有一些跟地址有关的“变量”需要我们注意一下,这些变量要最终编译完成才能确定的:
变量 | 数值 | 描述 |
---|---|---|
__image_copy_start | 0x87800000 | uboot 拷贝的首地址 |
__image_copy_end | 0x8785dd54 | uboot 拷贝的结束地址 |
__rel_dyn_start | 0x8785dd54 | .rel.dyn 段起始地址 |
__rel_dyn_end | 0x878668f4 | .rel.dyn 段结束地址 |
_image_binary_end | 0x878668f4 | 镜像结束地址 |
__bss_start | 0x8785dd54 | .bss 段起始地址 |
__bss_end | 0x878a8e74 | .bss 段结束地址 |
表一-3-1
这些“变量”值可以在 u-boot.map 文件中查找,上表中除了__image_copy_start以外,其他的变量值每次编译的时候可能会变化,如果修改了 uboot 代码、修改了 uboot 配置、选用不同的优化等级等等都会影响到这些值。所以,一切以实际值为准。
二、uboot启动流程
1. reset 函数
1.1 reset 函数在哪?
从 u-boot.lds 中我们已经知道了入口点是 arch/arm/lib/vectors.S 文件中的_start,相关代码如下:
1 | .globl _start |
_start后边为中断向量表,程序启动后,会首先跳转到 reset 函数里边,而 reset 函数则是定义在uboot源码的这个文件中:
1 | arch/arm/cpu/armv7/start.S |
我们打开这个文件,找到reset,会有如下内容:
1 | reset: |
可以看到 reset 中又从 reset 函数跳转到了 save_boot_params 函数,而 save_boot_params 函数同样定义在 start.S 里面 :
1 | /************************************************************************* |
发现 save_boot_params 函数也是只有一句跳转语句,跳转到 save_boot_params_ret 函数,save_boot_params_ret 也是定义在 start.S 文件中:
1 | save_boot_params_ret: |
1.2 最终做了什么?
1.2.1 设置工作状态
上边我们通过reset找到了 save_boot_params_ret 函数,reset函数经过多次跳转,最终执行的是 save_boot_params_ret 函数中的内容,如下所示:
1 | save_boot_params_ret: |
第 6 行:读取寄存器 cpsr 中的值,并保存到 r0 寄存器中。
第 7 行:将寄存器 r0 中的值与 0X1F 进行与运算,结果保存到 r1 寄存器中,目的就是提取 cpsr 寄存器的 bit[4:0] 这 5 位,这 5 位表示M4 M3 M2 M1 M0,而 M[4:0] 这五位用来设置处理器的工作模式。
点击查看 Cotex-A7 工作模式
M[4:0] | 模式 |
---|---|
10000 | User(usr) |
10001 | FIQ(fiq) |
10010 | IRQ(irq) |
10011 | Supervisor(svc) |
10110 | Monitor(mon) |
10111 | Abort(abt) |
11010 | Hyp(hyp) |
11011 | Undefined(und) |
11111 | System(sys) |
第 8 行:判断 r1 寄存器的值是否等于 0X1A(0b11010),也就是判断当前处理器模式是否处于 Hyp 模式。
第 9 行:如果 r1 和 0X1A 不相等,也就是 CPU 不处于 Hyp 模式的话就将 r0 寄存器的 bit[4:0] 进行清零,其实就是清除模式位
第 10 行:如果处理器不处于 Hyp 模式的话就将 r0 的寄存器的值与 0x13 进行或运算,0x13=0b10011,也就是设置处理器进入 SVC 模式。
第 11 行, r0 寄存器的值再与 0xC0 进行或运算,那么 r0 寄存器此时的值就是 0xD3, cpsr 寄存器的 I 位和 F 位分别控制 IRQ 和 FIQ 两个中断的开关,设置为 1 就关闭了 FIQ 和 IRQ。
第 12 行,将 r0 寄存器写回到 cpsr 寄存器中。完成设置 CPU 处于 SVC32 模式,并且关闭 FIQ 和 IRQ 这两个中断。
1.2.2 设置向量表
接下来,还是 save_boot_params_ret 函数,会继续执行以下代码:
1 | /* |
第 6 行:如果没有定义 CONFIG_OMAP44XX 和 CONFIG_SPL_BUILD 的话条件成立,此处条件成立。
第 8 行:读取 CP15 中 c1 寄存器的值到 r0 寄存器中,根据协处理器相关知识,这里是读取 SCTLR 寄存器的值。
第 9 行:CR_V 在 arch/arm/include/asm/system.h 中有如下所示定义:
1 |
因此这一行的目的就是清除 SCTLR 寄存器中的 bit[13]。bit[13] 为 V 位,此位是向量表控制位,当为 0 的时候向量表基地址为 0X00000000 ,软件可以重定位向量表。为 1 的时候向量表基地址为 0XFFFF0000,软件不能重定位向量表。这里将 V 清零,目的就是为了接下来的向量表重定位。
第 10 行:将 r0 寄存器的值重新写入到寄存器 SCTLR 中。
第 13 行:设置 r0 寄存器的值为 _start, _start就是整个 uboot 的入口地址,其值为0X87800000,相当于 uboot 的起始地址,因此 0x87800000 也是向量表的起始地址。
第 14 行:将 r0 寄存器的值(向量表值)写入到 CP15 的 c12 寄存器中,也就是 VBAR 寄存器。因此第 8~14 行就是设置向量表重定位的。
1.2.3 最终跳转到 main
接下来就是:
1 | /* the mask ROM code should have PLL and others stable */ |
第 2 行:如果没有定义 CONFIG_SKIP_LOWLEVEL_INIT 的话条件成立。我们没有定义: CONFIG_SKIP_LOWLEVEL_INIT,因此条件成立,执行下面的语句。
第 3 行:调用 cpu_init_cp15 函数,用来设置 CP15 相关的内容,比如关闭 MMU 等,此函数同样在 start.S文件中定义的,这里就暂时不关心这个函数了。
第 4 行:调用 cpu_init_crit函数,而该函数内部仅仅是调用了函数 lowlevel_init (这个函数后边会再分析)。
第 7 行:跳转到 main 函数。
2. lowlevel_init 函数
上边我们分析过,到执行 main 函数之前,还会调用lowlevel_init 函数,接下来我们看一下这个函数是干嘛的。
2.1 lowlevel_init 函数在哪?
lowlevel_init 这个函数在uboot源码的这个文件中定义 :
1 | arch/arm/cpu/armv7/lowlevel_init.S |
我们打开这个文件会看到有如下内容:
1 | /* |
2.2 最终做了什么?
2.1 设置堆栈指针 sp
第 22 行代码内容如下:
1 | ldr sp, =CONFIG_SYS_INIT_SP_ADDR |
该语句设置 sp 指向 CONFIG_SYS_INIT_SP_ADDR, CONFIG_SYS_INIT_SP_ADDR 在 include/configs/mx6ullevk.h 文件中有如下所示定义:
1 |
点击查看 IRAM_BASE_ADDR 和 IRAM_SIZE 的值
IRAM_BASE_ADDR 和 IRAM_SIZE 在 文 件 arch/arm/include/asm/arch-mx6/imx-regs.h 中有定义,如下所示,其实就是IMX6UL/IM6ULL 内部 ocram 的首地址和大小。
1 |
|
如果第 3 行的条件成立的话 IRAM_SIZE=0X40000,当定义了 CONFIG_MX6SX、CONFIG_MX6U、 CONFIG_MX6SLL 和CONFIG_MX6SL 中的任意一个的话,条件就不成立,在 .config 中定义了 CONFIG_MX6UL,所以条件不成立,所以最终IRAM_SIZE = 0X20000 = 128KB。所以我们可以得到如下值:
1 | CONFIG_SYS_INIT_RAM_ADDR = IRAM_BASE_ADDR = 0x00900000 |
点击查看 GENERATED_GBL_DATA_SIZE 的值
GENERATED_GBL_DATA_SIZE 在文件 include/generated/generic-asm-offsets.h 中有定义:
1 |
|
第 9 行:GENERATED_GBL_DATA_SIZE=256, GENERATED_GBL_DATA_SIZE 的含义为 (sizeof(struct global_data) + 15) & ~15 。所以我们可以得到如下值:
1 | GENERATED_GBL_DATA_SIZE = 256 |
所以,所需要的变量有如下三个:
1 | CONFIG_SYS_INIT_RAM_ADDR = IRAM_BASE_ADDR = 0x00900000 |
故,CONFIG_SYS_INIT_SP_ADDR 的值如下:
1 | CONFIG_SYS_INIT_SP_OFFSET = CONFIG_SYS_INIT_RAM_SIZE - GENERATED_GBL_DATA_SIZE = 0x00020000 - 256 = 0x1FF00 |
此时的 sp 指向 0X91FF00,这属于 IMX6UL/IMX6ULL 的内部 ram。
2.2 sp指针8字节对齐
第 23 行代码:
1 | bic sp, sp, #7 /* 8-byte alignment for ABI compliance */ |
2.3 sp指针其他操作
第 34 行:sp 指针减去 GD_SIZE, GD_SIZE 同样在 include/generated/generic-asm-offsets.h 中定了,大小为 248。
第 35 行:对 sp 做 8 字节对齐,此时 sp 的地址为 0X0091FF00 - 248 = 0X0091FE08。此时的 sp 指针如下图所示:
第 36 行:将 sp 地址保存在 r9 寄存器中。
第 42 行:将 ip 和 lr 压栈
第 57 行:调用函数 s_init,这个函数我们后边再说。
第 58 行:将第 36 行入栈的 ip 和 lr 进行出栈,并将 lr 赋给 pc。
3. s_init 函数
3.1 s_init 函数在哪?
前边我们分析 lowlevel_init 函数的时候,发现这个函数在最后调用了 s_init 函数,s_init 函数定义在uboot源码的这个文件中:
1 | arch/arm/cpu/armv7/mx6/soc.c |
我们打开这个文件,会看到此函数定义如下:
1 | void s_init(void) |
3.2 最终做了什么?
在此函数中会判断CPU的类型,如果 CPU 为 MX6SX、 MX6UL、 MX6ULL 或 MX6SLL中的任意一种,那么就会直接返回,相当于s_init 函数什么都没做。所以对于 I.MX6UL/I.MX6ULL 来说, s_init 就是个空函数。从 s_init 函数退出以后进入函数 lowlevel_init,但是 lowlevel_init 函数也执行完成了,返回到了函数 cpu_init_crit,函数 cpu_init_crit 也执行完成了,最终返回到 save_boot_params_ret。
4. _main 函数
4.1 _main 函数在哪里?
经过前边的分析,save_boot_params_ret 函数最后会跳转到 _main 函数,这个函数定义在 uboot 源码的这个文件中:
1 | arch/arm/lib/crt0.S |
我们打开这个文件,会看到如下内容:
1 | ENTRY(_main) |
4.2 做了什么?
在 arch/arm/lib/crt0.S 文件开头,其实已经说明了 _main 函数主要做了那些事情。
- (1)设置调用 board_init_f() 的初始环境。
- (2)调用board_init_f()。
- (3)设置中间环境,其中堆栈和GD是由系统RAM中的board_init_f()分配的,但BSS和初始化的非const数据仍然不可用。
- (4)对于U-Boot(不是SPL),调用relocate_code()。对于SPL, board_init_f()只返回(到crt0)。
- (5)设置调用board_init_r()的最终环境。
- (6)对于U-Boot本身(不是SPL),一些cpu在内存方面还有一些工作要做,因此调用 c_runtime_cpu_setup 。
- (7)调用board_init_r()。
4.2.1 设置初始C运行时环境
以下部分为 arch/arm/lib/crt0.S 文件的 69 - 93 行代码
1 | /* |
第 8 行(第 76 行):设置 sp 指针为 CONFIG_SYS_INIT_SP_ADDR,也就是 sp 指向 0X0091FF00。
第 15 行(第 83 行):sp 做 8 字节对齐。
第 17 行(第 85 行):读取 sp 到寄存器 r0 里面,此时 r0=0X0091FF00。
第 18 行(第 86 行):调用函数 board_init_f_alloc_reserve,此函数有一个参数,参数为 r0 中的值,也就是 0X0091FF00 ,该函数主要是留出早期的 malloc 内存区域和 gd 内存区域。
点击查看 board_init_f_alloc_reserve 函数
此函数定义在 common/init/board_init.c 文件中:
1 | ulong board_init_f_alloc_reserve(ulong top) |
函数 board_init_f_alloc_reserve 主要是留出早期的 malloc 内存区域和 gd 内存区域,其中 CONFIG_SYS_MALLOC_F_LEN=0X400( 在文件 include/generated/autoconf.h 中定义 ) ,sizeof(struct global_data)=248(GD_SIZE 值),完成以后的内存分布如下图:
函数 board_init_f_alloc_reserve 是有返回值的,返回值为新的 top 值,此时 top=0X0091FA00。
第 19 行(第 87 行):将 r0 写入到 sp 里面,r0 保存着函数 board_init_f_alloc_reserve 的返回值,所以这一句也就是设置 sp = 0X0091FA00。
第 21 行(第 89 行):将 r0 寄存器的值写到寄存器 r9 里面,因为 r9 寄存器存放着全局变量 gd 的地址。所以这一句其实就是让 gd 指向 0X0091FA00 。
点击查看 gd 地址
在文件 arch/arm/include/asm/global_data.h 中有如下宏定义:
1 |
可以看出, uboot 中定义了一个指向 gd_t 的指针 gd, gd 是存放在寄存器 r9 里面的,因此 gd 是个全局变量。 另外 gd_t 是个结构体, 这个结构体定义在 uboot 源码的这个文件中:
1 | include/asm-generic/global_data.h |
我们打开这个文件,可以看到这个结构体定义如下:
1 | typedef struct global_data { |
由此可知,这一行代码就是设置 gd 所指向的位置,也就是 gd 指向 0X0091FA00。
第 22 行(第 90 行):调用函数 board_init_f_init_reserve ,该函数主要是用于初始化 gd,其实就是清 0 处理。
点击查看 board_init_f_init_reserve
此函数在文件 common/init/board_init.c 中有定义 :
1 | void board_init_f_init_reserve(ulong base) |
可以看出,此函数用于初始化 gd,其实就是清零处理。另外,此函数还设置了 gd->malloc_base 为 gd 基地址+gd 大小 = 0X0091FA00 + 248 = 0X0091FAF8,再做 16 字节对齐,最终 gd->malloc_base = 0X0091FB00,这个也就是 early malloc 的起始地址。
第 25 行(第 93 行):调用 board_init_f 函数,这个函数定义在文件 common/board_f.c 中,主要用来初始化 DDR,定时器,完成代码拷贝等,后边我们会再详细分析这个函数。
4.2.2 设置中间环境
以下部分为 arch/arm/lib/crt0.S 文件的 97 - 127 行代码
1 | /* |
第 13 行(第 109 行):sp 做 8 字节对齐。
第 15 行(第 111 行):获取 gd->bd 的地址赋给 r9,此时 r9 存放的是之前的 gd,这里通过获取 gd->bd 的地址来计算出新的 gd 的位置。
点击查看 GD_BD 的值
GD_BD 定义在文件 include/generated/generic-asm-offsets.h 中:
1 |
|
所以 ,其实 GD_BD = 0。
第 16 行(第 112 行):新的 gd 在 bd 下面,所以 r9 减去 gd 的大小就是新的 gd 的位置,获取到新的 gd的位置以后赋值给 r9。
第 18 行(第 114 行):设置 lr 寄存器为 here,这样后面执行其他函数返回的时候就返回到了第 26 行(第 122 行)的 here 位置处。
第 19 行(第 115 行):读取 gd->reloc_off 的值复制给 r0 寄存器, GD_RELOC_OFF=68。
点击查看 GD_RELOC_OFF 的值
GD_RELOC_OFF 定义在文件 include/generated/generic-asm-offsets.h 中:
1 |
|
所以 ,其实 GD_RELOC_OFF = 68。
第 20 行(第 116 行): lr 寄存器的值加上 r0 寄存器的值,重新赋值给 lr 寄存器。因为接下来要重定位代码,也就是把代码拷贝到新的地方去(现在的 uboot 存放的起始地址为 0X87800000,下面要将 uboot 拷贝到 DDR 最后面的地址空间出,将 0X87800000 开始的内存空出来),其中就包括here,因此 lr 中的 here 要使用重定位后的位置。、
第 24 行(第 120 行):读取 gd->relocaddr 的值赋给 r0 寄存器,此时 r0 寄存器就保存着 uboot 要拷贝的目的地址,为 0X9FF47000。 GD_RELOCADDR=48。
点击查看 GD_RELOCADDR 的值
GD_RELOCADDR 定义在文件 include/generated/generic-asm-offsets.h 中:
1 |
|
所以 ,其实 GD_RELOCADDR = 48。
第 25 行(第 121 行):调用函数 relocate_code,也就是代码重定位函数,此函数负责将 uboot 拷贝到新的地方去,此函数定义在 arch/arm/lib/relocate.S 中,后边会再详细分析这个函数。
第 31 行(第 127 行):调用函数 relocate_vectors,对中断向量表做重定位,此函数定义在文件 arch/arm/lib/relocate.S 中,后边我们会再分析这个函数。
4.2.3 设置最终环境
以下部分为 arch/arm/lib/crt0.S 文件的 129 - 177 行代码,这部分其实是跟上边的 here 是一起的,只是这部分设置的是最终环境,就拆分开来分析了:
1 | /* Set up final (full) environment */ |
第 3 行(第 131 行):调用函数 c_runtime_cpu_setup。
点击查看 c_runtime_cpu_setup 函数
此函数定义在文件arch/arm/cpu/armv7/start.S 中 :
1 | ENTRY(c_runtime_cpu_setup) |
第 13 ~ 31 行(第 141~159 行):清除 BSS 段。
第 39 行(第 167 行):设置函数 board_init_r 的两个参数,board_init_r 函数第一个参数是 gd,因此读取 r9 保存到 r0 里面。
点击查看 board_init_r 函数
函数 board_init_r 声明如下 :
1 | board_init_r(gd_t *id, ulong dest_addr) |
第一个参数是 gd,因此读取 r9 保存到 r0 里面。
第 40 行(第 168 行):函数 board_init_r 的第二个参数是目的地址,因此 r1= gd->relocaddr。
第 46 行(第 174 行):调用函数 board_init_r,此函数定义在文件 common/board_r.c 中,后边会再详细分析这个函数。
这个就是_main 函数的运行流程,在_main 函数里面调用了 board_init_f、 relocate_code、relocate_vectors 和 board_init_r 这 4 个函数,
5. board_init_f 函数
5.1 board_init_f 函数在哪里?
上边我们分析 _main 函数的时候,它调用这个函数,board_init_f 函数主要有两个工作 :
(1)初始化一系列外设,比如串口、定时器,或者打印一些消息等。
(2)初始化 gd 的各个成员变量, uboot 会将自己重定位到 DRAM 最后面的地址区域,也就是将自己拷贝到 DRAM 最后面的内存区域中。这么做的目的是给 Linux 腾出空间,防止 Linux kernel 覆盖掉 uboot,将 DRAM 前面的区域完整的空出来。在拷贝之前肯定要给 uboot 各部分分配好内存位置和大小,比如 gd 应该存放到哪个位置, malloc 内存池应该存放到哪个位置等等。这些信息都保存在 gd 的成员变量中,因此要对 gd 的这些成员变量做初始化。最终形成一个完整的内存“分配图”,在后面重定位 uboot 的时候就会用到这个内存“分配图”。
这个函数定义在 uboot 源码中的这个文件中:
1 | common/board_f.c |
我们打开这个文件,可以看到函数如下:
1 | void board_init_f(ulong boot_flags) |
5.2 做了什么?
函数实现如下:
1 | void board_init_f(ulong boot_flags) |
因为没有定义 CONFIG_SYS_GENERIC_GLOBAL_DATA,所以第 3 ~ 20 行(第 1037 ~ 1054 行)代码无效。
第 22 行(第 1056 行):初始化 gd->flags = boot_flags = 0。
第 23 行(第 1057 行):设置 gd->have_console=0。
第 25 行(第 1059 行):通过函数 initcall_run_list 来运行初始化序列 init_sequence_f 里面的一些列函数, init_sequence_f 里面包含了一系列的初始化函数。这才是最重要的一个函数,由它完成的功能我们在 5.3 进行详细分析。
5.3 init_sequence_f
5.3.1 数组成员
点击查看 init_sequence_f
init_sequence_f 也是定义在文件 common/board_f.c 中,由于 init_sequence_f 的内容比较长,里面有大量的条件编译代码,将条件编译部分删除掉了,去掉条件编译以后的 init_sequence_f 定义如下:
1 | static init_fnc_t init_sequence_f[] = { |
5.3.2 重要函数说明
点击查看重要函数功能分析
接下来分析以上函数执行完以后的结果:
第 2 行:setup_mon_len 函数设置 gd 的 mon_len 成员变量,此处为__bss_end -_start,也就是整个代码的长度。 0X878A8E74 - 0x87800000 = 0XA8E74,这个就是代码长度。
第 3 行: initf_malloc 函数初始化 gd 中跟 malloc 有关的成员变量,比如 malloc_limit,此函数会设置 gd->malloc_limit = CONFIG_SYS_MALLOC_F_LEN=0X400。 malloc_limit 表示 malloc 内存池大小。
第 4 行: initf_console_record , 如 果 定 义 了 宏 CONFIG_CONSOLE_RECORD 和 宏 CONFIG_SYS_MALLOC_F_LEN 的话此函数就会调用函数 console_record_init,但是 IMX6ULL 的 uboot 没有定义宏 CONFIG_CONSOLE_RECORD,所以此函数直接返回 0。
第 5 行:arch_cpu_init 函数。
第 6 行:initf_dm 函数,驱动模型的一些初始化。
第 7 行:arch_cpu_init_dm 函数未实现。
第 8 行:mark_bootstage 函数应该是和什么标记有关的,具体没有深究过。
第 9 行: board_early_init_f 函数,板子相关的早期的一些初始化设置, I.MX6ULL 用来初始化串口的 IO 配置。
第 10 行: timer_init,初始化定时器, Cortex-A7 内核有一个定时器,这里初始化的就是 CortexA 内核的那个定时器。通过这个定时器来为 uboot 提供时间。就跟 Cortex-M 内核 Systick 定时器一样。关于 Cortex-A 内部定时器的详细内容,请参考文档《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition》 的 Chapter B8 The Generic Timer 章节。
第 11 行: board_postclk_init,对于 I.MX6ULL 来说是设置 VDDSOC 电压。
第 12 行:get_clocks 函数用于获取一些时钟值, I.MX6ULL 获取的是 sdhc_clk 时钟,也就是 SD 卡外设的时钟。
第 13 行:env_init 函数是和环境变量有关的,设置 gd 的成员变量 env_addr,也就是环境变量的保存地址。
第 14 行:init_baud_rate 函数用于初始化波特率,根据环境变量 baudrate 来初始化 gd->baudrate。
第 15 行:serial_init,初始化串口。
第 16 行:console_init_f,设置 gd->have_console 为 1,表示有个控制台,此函数也将前面暂存在缓冲区中的数据通过控制台打印出来。
第 17 行: display_options,通过串口输出一些信息 ,如:
第 18 行: display_text_info,打印一些文本信息,如果开启 UBOOT 的 DEBUG 功能的话就会输出 text_base、 bss_start、 bss_end,形式如下:
1 | debug("U-Boot code: %08lX -> %08lX BSS: -> %08lX\n",text_base, bss_start, bss_end); |
第 19 行: print_cpuinfo 函数用于打印 CPU 信息,如:
第 20 行: show_board_info 函数用于打印板子信息,会调用 checkboard 函数,如:
第 21 行:INIT_FUNC_WATCHDOG_INIT,初始化看门狗,对于 I.MX6ULL 来说是空函数。
第 22 行:INIT_FUNC_WATCHDOG_RESET,复位看门狗,对于 I.MX6ULL 来说是空函数。
第 23 行:init_func_i2c 函数用于初始化 I2C,初始化完成以后会输出如下信息:
第 24 行:announce_dram_init,这个函数很简单,就是输出字符串“DRAM:”
第 26 行:dram_init,并非真正的初始化 DDR,只是设置 gd->ram_size 的值,对于正点原子 I.MX6ULL 开发板 EMMC 版本核心板来说就是 512MB。
第 27 行: post_init_f,此函数用来完成一些测试,初始化 gd->post_init_f_time。
第 29 行:testdram,测试 DRAM,空函数。
第 44 行:setup_dest_addr 函数,设置目的地址,设置 gd->ram_size, gd->ram_top, gd->relocaddr 这三个的值。
点击查看 这三个值的打印方法
接下来我们会遇到很多跟数值有关的设置,如果直接看代码分析的话就太费时间了,我们可以修改 uboot 代码,直接将这些值通过串口打印出来,比如这里我们修改文件 common/board_f.c,因为 setup_dest_addr 函数定义在文件 common/board_f.c 中,在setup_dest_addr函数输入如下图内容:
设置好以后重新编译 uboot,然后烧写到 SD 卡中,选择 SD 卡启动,重启开发板,我们就会得到以下输出信息:
从图中可以看出:
1 | gd->ram_size = 0X20000000 //ram 大小为 0X20000000=512MB |
第 45 行 :reserve_round_4k 函 数 用 于 对 gd->relocaddr 做 4KB 对 齐 , 因 为 gd->relocaddr=0XA0000000,已经是 4K 对齐了,所以调整后不变。
第 46 行:reserve_mmu,留出 MMU 的 TLB 表的位置,分配 MMU 的 TLB 表内存以后会对 gd->relocaddr 做 64K 字节对齐。对齐之后有:
1 | gd->arch.tlb_size= 0X4000 //MMU 的 TLB 表大小 |
第 47 行:reserve_trace 函数,留出跟踪调试的内存, I.MX6ULL 没有用到。
第 48 行: reserve_uboot, 留出重定位后的 uboot 所占用的内存区域, uboot 所占用大小由 gd->mon_len 所指定,留出 uboot 的空间以后还要对 gd->relocaddr 做 4K 字节对齐,并且重新设置 gd->start_addr_sp 。完成之后吗,相关的变量值为:
1 | gd->mon_len = 0XA8EF4 |
第 49 行:reserve_malloc,留出 malloc 区域,调整 gd->start_addr_sp 位置, malloc 区域由宏TOTAL_MALLOC_LEN 定义。调整之后,相关变量的值如下:
1 | TOTAL_MALLOC_LEN=0X1002000 |
点击查看 TOTAL_MALLOC_LEN
这个宏,定义在 include/common.h 中:
1 |
在 include/configs/mx6ull_alientek_emmc.h 文件中定义宏 CONFIG_SYS_MALLOC_LEN 和 CONFIG_ENV_SIZE:
1 |
|
所以宏 CONFIG_SYS_MALLOC_LEN 为 16MB = 0X1000000,宏 CONFIG_ENV_SIZE = 8KB = 0X2000,因此TOTAL_MALLOC_LEN = 0X1002000。
第 50 行:reserve_board 函数,留出板子 bd 所占的内存区, bd 是结构体 bd_t, bd_t 大小为 80 字节 。此时相关变量的值如下:
1 | gd->start_addr_sp=0X9EF44FB0 |
第 51 行: setup_machine,设置机器 ID, linux 启动的时候会和这个机器 ID 匹配,如果匹配的话 linux 就会启动正常。但是 I.MX6ULL 不用这种方式了,这是以前老版本的 uboot 和 linux 使用的,新版本使用设备树了,因此此函数无效。
第 52 行:reserve_global_data 函数,保留出 gd_t 的内存区域, gd_t 结构体大小为 248B。相关变量的值如下:
1 | gd->start_addr_sp=0X9EF44EB8 //0X9EF44FB0-248=0X9EF44EB8 |
第 53 行:reserve_fdt,留出设备树相关的内存区域, I.MX6ULL 的 uboot 没有用到,因此此函数无效。
第 54 行:reserve_arch 是个空函数。
第 55 行: reserve_stacks,留出栈空间,先对 gd->start_addr_sp 减去 16,然后做 16 字节对齐。如果使能 IRQ 的话还要留出 IRQ 相应的内存,具体工作是由 arch/arm/lib/stack.c 文件中的函数 arch_reserve_stacks 完成。在我们使用的这个 uboot 中并没有使用到 IRQ,所以不会留出 IRQ 相应的内存区域,此时gd->start_addr_sp 的值如下:
1 | gd->start_addr_sp=0X9EF44E90 |
第 56 行: setup_dram_config 函数设置 dram 信息,就是设置 gd->bd->bi_dram[0].start 和 gd->bd->bi_dram[0].size,后面会传递给 linux 内核,告诉 linux DRAM 的起始地址和大小。我们可以在该函数中打印一下这几个变量,结果应该是下边这样:
1 | gd->bd->bi_dram[0].start = 0x80000000 |
第 57 行, show_dram_config 函数,用于显示 DRAM 的配置 ,如:
第 58 行:display_new_sp 函数,显示新的 sp 位置,也就是 gd->start_addr_sp,不过要定义宏 DEBUG 。
第 60 行:reloc_fdt 函数用于重定位 fdt,没有用到。
第 61 行: setup_reloc,设置 gd 的其他一些成员变量,供后面重定位的时候使用,并且将以前的 gd 拷贝到 gd->new_gd 处。需要使能 DEBUG 才能看到相应的信息输出 。这里我们要是打印这些信息的话,会发现uboot 重定位后的偏移为 0X18747000,重定位后的新地址为0X9FF4700,新的 gd 首地址为 0X9EF44EB8,最终的 sp 为 0X9EF44E90。
5.3.3 最终内存分配
经过上边的一系列函数的执行,最终的内存分配如下图所示:
6. relocate_code 函数
6.1 relocate_code 函数在哪里?
上边我们分析 _main 函数的时候,它调用这个函数,relocate_code 函数是用于代码拷贝的,它定义在 uboot 源码中的这个文件中:
1 | arch/arm/lib/relocate.S |
我们打开这个文件,可以看到如下代码:
1 | /* |
6.2 做了什么?
我们的分析按下边的行号进行:
1 | ENTRY(relocate_code) |
第 2 行(第 80 行):r1=__image_copy_start,也就是 r1 寄存器保存源地址,从第一节中的表一-3-1可知 __image_copy_start = 0X87800000。
第 3 行(第 81 行):r0=0X9FF47000,这个地址就是 uboot 拷贝的目标首地址。 r4 = r0-r1 = 0X9FF47000 - 0X87800000 = 0X18747000,因此 r4 保存偏移量。
第 4 行(第 82 行):如果在第 3 行(第 81 行)中, r0-r1 等于 0,说明 r0 和 r1 相等,也就是源地址和目的地址是一样的,那肯定就不需要拷贝了,直接执行 relocate_done 函数。
第 5 行(第 83 行):r2=__image_copy_end, r2 中保存拷贝之前的代码结束地址,由表一-3-1可知,__image_copy_end = 0x8785dd54。
第 6 行(第 84 行):函数 copy_loop 完成代码拷贝工作!从 r1,也就是__image_copy_start 开始,读取 uboot 代码保存到 r10 和 r11 中,一次就只拷贝这 2 个 32 位的数据。拷贝完成以后 r1 的值会更新,保存下一个要拷贝的数据地址。
第 9 行(第 87 行):将 r10 和 r11 的数据写到 r0 开始的地方,也就是目的地址。写完以后 r0 的值会更新,更新为下一个要写入的数据地址。
第 10 行(第 88 行):比较 r1 是否和 r2 相等,也就是检查是否拷贝完成,如果不相等的话说明没有拷贝完成, 没有拷贝完成的话就跳转到 copy_loop 接着拷贝,直至拷贝完成。
第 16 ~ 31 行(第 94 ~ 109 行):是重定位 .rel.dyn 段, .rel.dyn 段是存放 .text 段中需要重定位地址的集合。重定位就是 uboot 将自身拷贝到 DRAM 的另一个地方去继续运行(DRAM 的高地址处)。 我们知道,一个可执行的 bin 文件,其链接地址和运行地址要相等,也就是链接到哪个地址,在运行之前就要拷贝到哪个地址去。现在我们重定位以后,运行地址就和链接地址不同了,这样寻址的时候不会出问题吗?这个问题这里我就没有继续深入追究了,有兴趣的话可以看正点原子的驱动开发手册,里边有很详细的说明。
7. relocate_vectors函数
7.1 relocate_vectors 函数在哪里?
上边我们分析 _main 函数的时候,它调用这个函数,relocate_vectors 函数用于重定位向量表,它定义在 uboot 源码中的这个文件中:
1 | arch/arm/lib/relocate.S |
我们打开这个文件,可以看到如下代码:
1 | ENTRY(relocate_vectors) |
7.2 做了什么?
我们的分析按下边的行号进行:
1 | ENTRY(relocate_vectors) |
第 3 行(第 29 行):如果定义了 CONFIG_CPU_V7M 的话就执行第 4 ~ 10 行(第 30 ~ 36 行)的代码,这是 Cortex-M 内核单片机执行的语句,因此对于 I.MX6ULL 来说是无效的。
第 12 行(第 38 行):如果定义了 CONFIG_HAS_VBAR 的话就执行此语句,这个是向量表偏移, CortexA7 是支持向量表偏移的。而且,在.config 里面定义了 CONFIG_HAS_VBAR,因此会执行这个分支。
第 17 行(第 43 行):r0=gd->relocaddr,也就是重定位后 uboot 的首地址,向量表肯定是从这个地址开始存放的。
第 18 行(第 44 行):将 r0 的值写入到 CP15 的 VBAR 寄存器中,也就是将新的向量表首地址写入到寄存器 VBAR 中,设置向量表偏移。
8. board_init_r 函数
8.1 board_init_r 函数在哪里?
上边我们分析 _main 函数的时候,它调用这个函数,前边我们了解了 board_init_f 函数,在此函数里面会调用一系列的函数来初始化一些外设和 gd 的成员变量。但是 board_init_f 并没有初始化所有的外设,还需要做一些后续工作,这些后续工作就是由函数 board_init_r 来完成的 它定义在 uboot 源码中的这个文件中:
1 | common/board_r.c |
我们打开这个文件,可以看到如下代码:
1 | void board_init_r(gd_t *new_gd, ulong dest_addr) |
8.2 做了什么?
我们的分析按下边的行号进行:
1 | void board_init_r(gd_t *new_gd, ulong dest_addr) |
第 20 行(第 1010 行):调用 initcall_run_list 函数来执行初始化序列 init_sequence_r, init_sequence_r 是一个函数集合,init_sequence_r 也定义在文件 common/board_r.c 中,我们下一小节详细学习。
8.3 init_sequence_r
8.3.1 数组成员
点击查看 init_sequence_r
init_sequence_r 也定义在文件 common/board_r.c 中,由于 init_sequence_f 的内容比较长,里面有大量的条件编译代码,这里将条件编译部分删除掉了,去掉条件编译以后的 init_sequence_r 定义如下:
1 | init_fnc_t init_sequence_r[] = { |
8.3.2 重要函数说明
点击查看重要函数功能分析
第 2 行:initr_trace 函数,如果定义了宏 CONFIG_TRACE 的话就会调用函数 trace_init,初始化和调试跟踪有关的内容。
第 3 行:initr_reloc 函数用于设置 gd->flags,标记重定位完成。
第 4 行:initr_caches 函数用于初始化 cache,使能 cache。
第 5 行:initr_reloc_global_data 函数,初始化重定位后 gd 的一些成员变量。
第 6 行:initr_barrier 函数, I.MX6ULL 未用到。
第 7 行:initr_malloc 函数,初始化 malloc。
第 8 行:initr_console_record 函数,初始化控制台相关的内容, I.MX6ULL 未用到,空函数。
第 9 行: bootstage_relocate 函数,启动状态重定位。
第 10 行:initr_bootstage 函数,初始化 bootstage 什么的。
第 11 行:board_init 函数,板级初始化,包括 74XX 芯片, I2C、 FEC、 USB 和 QSPI 等。这里执行的是 mx6ull_alientek_emmc.c 文件中的 board_init 函数。
第 12 行:stdio_init_tables 函数, stdio 相关初始化。
第 13 行:initr_serial 函数,初始化串口。
第 14 行:initr_announce 函数,与调试有关,通知已经在 RAM 中运行。
第 18 行:power_init_board 函数,初始化电源芯片,正点原子的 I.MX6ULL 开发板没有用到。
第 19 行:initr_flash 函数,对于 I.MX6ULL 而言,没有定义宏 CONFIG_SYS_NO_FLASH的话函数 initr_flash 才有效。但是 mx6_common.h 中定义了宏 CONFIG_SYS_NO_FLASH,所以此函数无效。
第 21 行:initr_nand 函数,初始化 NAND,如果使用 NAND 版本核心板的话就会初始化NAND。
第 22 行:initr_mmc 函数,初始化 EMMC,如果使用 EMMC 版本核心板的话就会初始化EMMC,串口输出如下图所示信息:
第 23 行:initr_env 函数,初始化环境变量。
第 25 行:initr_secondary_cpu 函数,初始化其他 CPU 核, I.MX6ULL 只有一个核,所以此函数没用。
第 27 行:stdio_add_devices 函数,各种输入输出设备的初始化,如 LCD driver, I.MX6ULL 使用 drv_video_init 函数初始化 LCD。会输出如图所示信息:
第 28 行:initr_jumptable 函数,初始化跳转表。
第 29 行:console_init_r 函数,控制台初始化,初始化完成以后此函数会调用 stdio_print_current_devices 函数来打印出当前的控制台设备,如图所示:
第 31 行:interrupt_init 函数,初始化中断。
第 32 行:initr_enable_interrupts 函数,使能中断。
第 33 行:initr_ethaddr 函数,初始化网络地址,也就是获取 MAC 地址。读取环境变量“ethaddr”的值。
第 34 行, board_late_init 函数,板子后续初始化,此函数定义在文件 mx6ull_alientek_emmc.c 中,如果环境变量存储在 EMMC 或者 SD 卡中的话,此函数会调用 board_late_mmc_env_init 函数初始化 EMMC/SD。会切换到正在时候用的 emmc 设备。
点击查看 board_late_mmc_env_init
这个函数出现在uboot源码的这个文件中:
1 | board/freescale/mx6ull_alientek_emmc/mx6ull_alientek_emmc.c # 在该文件被调用 |
函数定义如下:
1 | void board_late_mmc_env_init(void) |
第 17 ~ 18 行(第 46 ~ 47 行)就是运行“mmc dev xx”命令,用于切换到正在使用的EMMC 设备,串口输出信息如图所示:
第 38 行:initr_net 函数,用于初始化网络设备,函数的调用顺序为:initr_net→eth_initialize→board_eth_init() ,最终在串口上会输出如下信息:
第 40 行, run_main_loop 函数(后边再详细说明),主循环,处理命令。
9. run_main_loop 函数
前边我们分析到了 board_init_r 函数,在此函数中又会调用到 run_main_loop 函数,接下来我们来了解一下这个函数的功能。
uboot 启动以后会进入 bootdelay 秒倒计时,如果在 bootdelay 秒倒计时结束之前按下按下回车键,那么就会进入 uboot 的命令模式,如果倒计时结束以后都没有按下回车键,那么就会自动启动 Linux 内核,这个功能就是由 run_main_loop 函数来完成的。
9.1 run_main_loop 函数在哪?
run_main_loop 函数定义在uboot 源码的这个文件中:
1 | common/board_r.c |
函数内容如下:
1 | static int run_main_loop(void) |
9.2 做了什么?
9.2.1 run_main_loop函数定义
我们按照下边的行号分析:
1 | static int run_main_loop(void) |
第 7 行 和第 8 行(第 759 行和第 760 行)是个死循环,“for(;;)”和“while(1)”功能一样,死循环里面就一个 main_loop 函数 。
9.2.2 main_loop 函数
这个函数定义在 common/main.c 中,函数内容如下:
1 | /* We come here after U-Boot is initialised and ready to process commands */ |
第 6 行(第 48 行):调用 bootstage_mark_name 函数,打印出启动进度。
第 15 行(第 57 行):如果定义了宏 CONFIG_VERSION_VARIABLE 的话就会执行函数 setenv,将变量 ver 的值设置为 version_string,也就是设置版本号环境变量。
点击查看 version_string
version_string 定义在文件 cmd/version.c 中,定义如下:
1 | const char __weak version_string[] = U_BOOT_VERSION_STRING; |
其中U_BOOT_VERSION_STRING 是个宏, 定义在文件 include/version.h:
1 |
U_BOOT_VERSION 定义在文件 include/generated/version_autogenerated.h 中:
1 |
可以看出, U_BOOT_VERSION 为“U-boot 2016.03”,U_BOOT_DATE 、 U_BOOT_TIME 和 U_BOOT_TZ 定义在 include/generated/timestamp_autogenerated.h 文件中,内容如下:
1 |
宏 CONFIG_IDENT_STRING 为空,所以 U_BOOT_VERSION_STRING 为“U-Boot 2016.03(Oct 15 2022 - 17:13:29 +0800)”,进入 uboot 命令模式,输入命令“version”查看版本号,如图:
第 18 行(第 60 行):cli_init 函数,跟命令初始化有关,初始化 hush shell 相关的变量。
第 20 行(第 62 行):run_preboot_environment_command 函数,获取环境变量 perboot 的内容, perboot 是一些预启动命令,一般不使用这个环境变量。
第 26 行(第 68 行):bootdelay_process 函数,此函数会读取环境变量 bootdelay 和 bootcmd 的内容,然后将 bootdelay 的值赋值给全局变量 stored_bootdelay,返回值为环境变量 bootcmd 的值。
第 27 行(第 69 行):如果定义了 CONFIG_OF_CONTROL 的话函数 cli_process_fdt 就会实现,如果没有定义 CONFIG_OF_CONTROL 的话函数 cli_process_fdt 直接返回一个 false。在我们使用的 uboot 中没有定义 CONFIG_OF_CONTROL,因此 cli_process_fdt 函数返回值为 false。
第 30 行(第 72 行):autoboot_command 函数,此函数就是检查倒计时是否结束,倒计时结束之前有没有被打断。
点击查看 autoboot_command
此函数定义在文件 common/autoboot.c 中,内容如下:
1 | void autoboot_command(const char *s) |
autoboot_command 函数里面有很多条件编译,宏CONFIG_AUTOBOOT_KEYED 、 CONFIG_AUTOBOOT_KEYED_CTRLC 和
CONFIG_MENUKEY 这三个宏在 I.MX6ULL 里面没有定义,去掉条件编译得到如下代码:
1 | void autoboot_command(const char *s) |
可以看到,当以下三条全部成立的话,就会执行函数 run_command_list :
(1)stored_bootdelay 不等于 -1 。
(2)s 不为空。
(3)函数 abortboot 返回值为 0。
stored_bootdelay 等于环境变量 bootdelay 的值; s 是环境变量 bootcmd 的值,一般不为空,因此前两个成立,就剩下了函数 abortboot 的返回值 ,如果倒计时自然结束那么就执行函数 run_command_list,此函数会执行参数 s 指定的一系列命令,也就是环境变量 bootcmd 的命令,bootcmd 里面保存着默认的启动命令,因此 linux 内核启动!这个就是 uboot 中倒计时结束以后自动启动 linux 内核的原理。如果倒计时结束之前按下了键盘上的按键,那么 run_command_list 函数就不会执行,相当于 autoboot_command 是个空函数。
点击查看 abortboot 函数
abortboot 函数 也 定 义 在 文 件 common/autoboot.c 中:
1 | static int abortboot(int bootdelay) |
因为宏 CONFIG_AUTOBOOT_KEYE 未定义,因此执行函数 abortboot_normal ,此函数也定义在文件 common/autoboot.c 中,内容
如下:
1 | static int abortboot_normal(int bootdelay) |
函数 abortboot_normal 同样很多条件编译,删除掉条件编译相关代码后 abortboot_normal 函数内容如下:
1 | static int abortboot_normal(int bootdelay) |
第 3 行:变量 abort 是函数 abortboot_normal 的返回值,默认值为 0。
第 7 行:通过串口输出“Hit any key to stop autoboot”字样,如图:
第 9 ~ 25 行:是倒计时的具体实现。其中的第 16 行判断键盘是否有按下,也就是是否打断了倒计时,如果键盘按下的话就执行相应的
分支。比如设置 abort 为 1,设置 bootdelay 为 0 等,最后跳出倒计时循环。
第 30 行,返回 abort 的值,如果倒计时自然结束,没有被打断 abort 就为 0,否则的话 abort的值就为 1。
第 32 行(第 74 行):如果倒计时结束之前按下按键,那么就会执行cli_loop 函数,这个就是命令处理函数,负责接收好处理输入的命令。
10. cli_loop 函数
前边我们分析到,当执行run_main_loop 函数,倒计时结束前按下按键,会执行 cli_loop,接下来就来了解一下这个函数。
10.1 cli_loop 函数在哪?
cli_loop 函数是 uboot 的命令行处理函数,最终是调用到 cmd_process 函数来处理命令。我们在 uboot 中输入各种命令,进行各种操作就是 cli_loop 来处理的,此函数定义在uboot源码这个文件中 :
1 | common/cli.c |
我们打开这个文件,可以看到函数内容如下:
1 | void cli_loop(void) |
10.2 做了什么?
10.2.1 cli_loop
我们按照下边的行号分析:
1 | void cli_loop(void) |
在文件 include/configs/mx6_common.h 中有定义宏 CONFIG_SYS_HUSH_PARSER,而正点原子的 I.MX6ULL 开发板配置头文件 mx6ullevk.h 里面会引用 mx_common.h 这个头文件,因此宏 CONFIG_SYS_HUSH_PARSER 有定义。
第 4 行(第 205 行):调用函数 parse_file_outer(10.2.2中分析)。
第 6 行(第 207 行)是个死循环,永远不会执行到这里。
10.2.2 parse_file_outer 函数
函数 parse_file_outer 定义在文件 common/cli_hush.c 中,去掉条件编译内容以后的函数内容如下:
1 | int parse_file_outer(void) |
第 5 行:调用函数 setup_file_in_str 初始化变量 input 的成员变量。
第 6 行:调用函数 parse_stream_outer(10.2.3中分析),这个函数就是 hush shell 的命令解释器,负责接收命令行输入,然后解析并执行相应的命令。
10.2.3 parse_stream_outer函数
函数 parse_stream_outer 定义在文件 common/cli_hush.c中,精简版的函数内容如下:
1 | static int parse_stream_outer(struct in_str *inp, int flag) |
第 7 ~ 21 行:这几行中的do-while 循环就是处理输入命令的。
第 10 行:调用函数 parse_stream 进行命令解析。
第 16 行:调用 run_list 函数来执行解析出来的命令 。
10.2.4 run_list 函数
该函数定义在 common/cli_hush.c 文件中,函数会经过一系列的函数调用,最终通过调用 cmd_process 函数来处理命令 :
1 | /* Select which version we will use */ |
第 8 行:run_list 调用 run_list_real 函数。
- run_list_real 函数
1 | // common/cli_hush.c |
第 8 行:run_list_real 函数调用 run_pipe_real 函数。
- run_pipe_real 函数
1 | // common/cli_hush.c |
第 19 行:run_pipe_real 函数调用 cmd_process 函数。
11. cmd_process 函数
我们在uboot中使用的命令,是通过该函数进行解析和执行的。
11.1 命令的定义与执行
11.1.1 相关宏定义
uboot使用宏U_BOOT_CMD来定义命令,宏 U_BOOT_CMD 定义在文件 include/command.h 中,定义如下:
1 |
可以看出 U_BOOT_CMD 是 U_BOOT_CMD_COMPLETE 的 特 例 , 我们将U_BOOT_CMD_COMPLETE 的最后一个参数设置成NULL 就是 U_BOOT_CMD。U_BOOT_CMD_COMPLETE 定义在文件 include/command.h 中,定义如下:
1 |
宏 U_BOOT_CMD_COMPLETE 又 用 到 了 ll_entry_declare 和 U_BOOT_CMD_MKENT_COMPLETE。 ll_entry_declar 定义在文件include/linker_lists.h 中,定义如下:
1 |
其中_type 为 cmd_tbl_t,因此 ll_entry_declare 就是定义了一个 cmd_tbl_t 变量,这里用到了 C 语言中的“##”连接符。其中的“##_list”表示用_list 的值来替换,“##_name”就是用_name 的值来替换。
宏 U_BOOT_CMD_MKENT_COMPLETE 定义在文件 include/command.h 中,内容如下:
1 |
“ # ” 表 示 将 _name 传递过来的值字符串化 ,U_BOOT_CMD_MKENT_COMPLETE 又用到了宏_CMD_HELP 和_CMD_COMPLETE,这两个宏的定义(include/command.h)如下:
1 |
可以看出,如果定义了宏 CONFIG_AUTO_COMPLETE 和 CONFIG_SYS_LONGHELP 的话 , _CMD_COMPLETE 和 _CMD_HELP 就是取自身的值,然后在加上一个 ‘ , ’。 CONFIG_AUTO_COMPLETE 和 CONFIG_SYS_LONGHELP 这两个宏有定义在文件 mx6_common.h 中。
11.1.2 命令实例
U_BOOT_CMD宏的流程大概我们已经清楚了,我们就以一个具体的命令为例,来看一下 U_BOOT_CMD 经过展开以后究竟是个什么模样的。以命令 dhcp 为例, dhcp 命令定义在 cmd/net.c 文件中:
1 | U_BOOT_CMD( |
我们将这个命令进行一个展开:
- 将 U_BOOT_CMD 展开
1 | U_BOOT_CMD_COMPLETE(dhcp, 3, 1, do_dhcp, |
- 将 U_BOOT_CMD_COMPLETE 展开
1 | ll_entry_declare(cmd_tbl_t, dhcp, cmd) = \ |
- 将 ll_entry_declare 和 U_BOOT_CMD_MKENT_COMPLETE 展开
1 | cmd_tbl_t _u_boot_list_2_cmd_2_dhcp __aligned(4) \ |
所以,dhcp 命令展开后,最终形式为:
1 | cmd_tbl_t _u_boot_list_2_cmd_2_dhcp __aligned(4) \ |
第 1 行:定义了一个 cmd_tbl_t 类型的变量,变量名为_u_boot_list_2_cmd_2_dhcp,此变量 4 字节对齐。
第 2 行 :使 用 __attribute__ 关键字设置变量 _u_boot_list_2_cmd_2_dhcp 存储在 .u_boot_list_2_cmd_2_dhcp 段中。u-boot.lds 链接脚本中有一个名为“.u_boot_list”的段,所有.u_boot_list 开头的段都存放到.u_boot.list 中,所以第 2 行 就是设置变量_u_boot_list_2_cmd_2_dhcp 的存储位置。
第 3~6 行: cmd_tbl_t 是个结构体,因此第 3 ~ 6 行是初始化 cmd_tbl_t 这个结构体的各个成员变量。
点击查看 cmd_tbl_t 结构体成员
cmd_tbl_t 结构体定义在文件 include/command.h 中 :
1 | struct cmd_tbl_s |
结合 dhcp 命令展开后的最终形式,可以得出变量_u_boot_list_2_cmd_2_dhcp 的各个成员的值如下所示:
1 | _u_boot_list_2_cmd_2_dhcp.name = "dhcp" |
11.2.3 命令执行
前边我们已经分析了在uboot中是如何定义一个命令的,那当我们在 uboot 的命令行中输入“dhcp”这个命令的时候,是怎么执行的呢?其实最终执行的是 do_dhcp 这个函数。
1 | static int do_dhcp(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[]) |
在这个函数中实际上是调用了 netboot_common 函数,这个函数定义在 cmd/net.c 中,具体怎么执行的,这里我就没有去了解了。
总的来说,uboot 中使用 U_BOOT_CMD 来定义一个命令,最终的目的就是为了定义一个 cmd_tbl_t 类型的变量,并初始化这个变量的各个成员。 uboot 中的每个命令都存储在.u_boot_list 段中,每个命令都有一个名为 do_xxx(xxx 为具体的命令名)的函数,这个 do_xxx 函数就是具体的命令处理函数。
11.2 cmd_process 函数在哪?
cmd_process 函数定义在文件 common/command.c 中,函数内容如下:
1 | enum command_ret_t cmd_process(int flag, int argc, char * const argv[], |
11.3 做了什么?
我们按照下边的行号分析:
1 | enum command_ret_t cmd_process(int flag, int argc, char * const argv[], |
第 8 行(第 507 行):调用函数 find_cmd 在命令表中找到指定的命令。
点击查看 find_cmd 函数
函数定义在 common/command.c 文件中:
1 | cmd_tbl_t *find_cmd(const char *cmd) |
参数 cmd 就是所查找的命令名字, uboot 中的命令表其实就是 cmd_tbl_t 结构体数组,通过函数 ll_entry_start 得到数组的第一个元素,也就是命令表起始地址。通过函数 ll_entry_count 得到数组长度,也就是命令表的长度。最终通过函数 find_cmd_tbl 在命令表中找到所需的命令,每个命令都有一个 name 成员,所以将参数 cmd 与命令表中每个成员的 name 字段都对比一下,如果相等的话就说明找到了这个命令,找到以后就返回这个命令。
第 34 行(第 533 行):当 find_cmd 找到相应命令后,就会调用函数 cmd_call 来执行具体的命令 。
点击查看 cmd_call 函数
这个函数定义在 common/command.c 中:
1 | /** |
在前面的分析中我们知道, cmd_tbl_t 的 cmd 成员就是具体的命令处理函数,所以第 16 行(第 494 行)调用 cmdtp 的 cmd 成员来处理具体的命令,返回值为命令的执行结果。
第 39 行(第 538 行):cmd_process 中会检测 cmd_tbl 的返回值,如果返回值为 CMD_RET_USAGE 的话就会调用 cmd_usage 函数输出命令的用法,其实就是输出 cmd_tbl_t 的 usage 成员变量。