I.MX6ULL_Linux_系统篇(16) uboot分析-启动流程

news2024/12/23 3:42:59

原文链接:I.MX6ULL_系统篇(16) uboot分析-启动流程 – WSY Personal Blog (cpolar.cn)

前面我们详细的分析了 uboot 的顶层 Makefile,了解了 uboot 的编译流程。本章我们来详细的分析一下 uboot 的启动流程,理清 uboot 是如何启动的。通过对 uboot 启动流程的梳理,我们就可以掌握一些外设是在哪里被初始化的,这样当我们需要修改这些外设驱动的时候就会心里有数。另外,通过分析 uboot 的启动流程可以了解 Linux 内核是如何被启动的。

链接脚本 u-boot.lds

要分析 uboot 的启动流程,首先要找到入口,找到第一行程序在哪里。程序的链接是由链接脚本来决定的,所以通过链接脚本可以找到程序的入口。如果没有编译过 uboot 的话链接脚本为 arch/arm/cpu/u-boot.lds。但是这个不是最终使用的链接脚本,最终的链接脚本是在这个链接脚本的基础上生成的。编译一下 uboot,编译完成以后就会在 uboot 根目录下生成 u-boot.lds文件,如图所示:

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")

OUTPUT_ARCH(arm)

ENTRY(_start) #代码入口

SECTIONS

{

 . = 0x00000000;

 . = ALIGN(4);

 .text :

 {

  *(.__image_copy_start)

  *(.vectors)

  arch/arm/cpu/armv7/start.o (.text*)

  *(.text*)

 }

 . = ALIGN(4);

 .rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }

 . = ALIGN(4);

 .data : {

  *(.data*)

 }

 . = ALIGN(4);

 . = .;

 . = ALIGN(4);

 .u_boot_list : {

  KEEP(*(SORT(.u_boot_list*)));

 }

 . = ALIGN(4);

 .image_copy_end :

 {

  *(.__image_copy_end)

 }

 .rel_dyn_start :

 {

  *(.__rel_dyn_start)

 }

 .rel.dyn : {

  *(.rel*)

 }

 .rel_dyn_end :

 {

  *(.__rel_dyn_end)

 }

 .end :

 {

  *(.__end)

 }

 _image_binary_end = .;

 . = ALIGN(4096);

 .mmutable : {

  *(.mmutable)

 }

 .bss_start __rel_dyn_start (OVERLAY) : {

  KEEP(*(.__bss_start));

  __bss_base = .;

 }

 .bss __bss_base (OVERLAY) : {

  *(.bss*)

   . = ALIGN(4);

   __bss_limit = .;

 }

 .bss_end __bss_limit (OVERLAY) : {

  KEEP(*(.__bss_end));

 }

 .dynsym _image_binary_end : { *(.dynsym) }

 .dynbss : { *(.dynbss) }

 .dynstr : { *(.dynstr*) }

 .dynamic : { *(.dynamic*) }

 .plt : { *(.plt*) }

 .interp : { *(.interp*) }

 .gnu.hash : { *(.gnu.hash) }

 .gnu : { *(.gnu*) }

 .ARM.exidx : { *(.ARM.exidx*) }

 .gnu.linkonce.armexidx : { *(.gnu.linkonce.armexidx.*) }

}

3 行为代码当前入口点: _start _start 在文件 arch/arm/lib/vectors.S 中有定义,如图所示:

从图中的代码可以看出, _start 后面就是中断向量表,从图中的“.section “.vectors”,”ax”可以得到,此代码存放在.vectors 段里面。使用如下命令在 uboot 中查找“__image_copy_start”

grep -nR "__image_copy_start"

打开 u-boot.map,找到如上图所示位置:

u-boot.map uboot 的映射文件,可以从此文件看到某个文件或者函数链接到了哪个地址,从上图的 932 行可以看到__image_copy_start 0X87800000,而.text 的起始地址也是0X87800000

继续回到链接脚本中, 11 行是 vectors 段, vectors 段保存中断向量表,我们知道了 vectors.S 的代码是存在 vectors 段中的。从上图中可以看出, vectors 段的起始地址也是 0X87800000,说明整个 uboot 的起始地址就是 0X87800000
12 行将 arch/arm/cpu/armv7/start.s 编译出来的代码放到中断向量表后面。
13 行为 text 段,其他的代码段就放到这里
u-boot.lds 中有一些跟地址有关的变量需要我们注意一下,后面分析 u-boot 源码的时候会用到,这些变量要最终编译完成才能确定的!!!比如我编译完成以后这些变量的值如表所示:

变量

数值

描述

__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 段结束地址

表中的变量值可以在 u-boot.map 文件中查找,其中除了__image_copy_start以外,其他的变量值每次编译的时候可能会变化,如果修改了 uboot 代码、修改了 uboot 配置、选用不同的优化等级等等都会影响到这些值。所以,一切以实际值为准!

启动流程详解

reset 函数

u-boot.lds 中我们已经知道了入口点是 arch/arm/lib/vectors.S 文件中的_start,代码如下:

48 _start 开始的是中断向量表,其中 54~61 行就是中断向量表,和我们裸机例程里面一样。54 行跳转到 reset 函数里面, reset 函数在 arch/arm/cpu/armv7/start.S 里面,代码如下:

35 行就是 reset 函数。
37 行从 reset 函数跳转到了 save_boot_params 函数,而 save_boot_params 函数同样定义在 start.S 里面,定义如下:

save_boot_params 函数也是只有一句跳转语句,跳转到 save_boot_params_ret 函数,save_boot_params_ret 函数代码如下:

43 行,读取寄存器 cpsr 中的值,并保存到 r0 寄存器中。

44 行,将寄存器 r0 中的值与 0X1F 进行与运算,结果保存到 r1 寄存器中,目的就是提取 cpsr bit0~bit4 5 位,这 5 位为 M4 M3 M2 M1 M0 M[4:0]这五位用来设置处理器的工作模式,如表所示:

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)

45 行,判断 r1 寄存器的值是否等于 0X1A(0b11010),也就是判断当前处理器模式是否处于 Hyp 模式。
46 行,如果 r1 0X1A 不相等,也就是 CPU 不处于 Hyp 模式的话就将 r0 寄存器的bit0~5 进行清零,其实就是清除模式位
47 行,如果处理器不处于 Hyp 模式的话就将 r0 的寄存器的值与 0x13 进行或运算,0x13=0b10011,也就是设置处理器进入 SVC 模式。
48 行, r0 寄存器的值再与 0xC0 进行或运算,那么 r0 寄存器此时的值就是 0xD3 cpsr I 位和 F 位分别控制 IRQ FIQ 这两个中断的开关,设置为 1 就关闭了 FIQ IRQ!第 49 行,将 r0 寄存器写回到 cpsr 寄存器中。完成设置 CPU 处于 SVC 模式,并且关闭FIQ IRQ 这两个中断。
继续执行执行下面的代码!
56 行,如果没有定义 CONFIG_OMAP44XX CONFIG_SPL_BUILD 的话条件成立,此处条件成立。
58 行读取 CP15 c1 寄存器的值到 r0 寄存器中,这里是读取SCTLR 寄存器的值。
59 行, CR_V arch/arm/include/asm/system.h 中有如下所示定义:

#define CR_V (1 << 13) /* Vectors relocated to 0xffff0000 */

因此这一行的目的就是清除 SCTLR 寄存器中的 bit13 SCTLR 寄存器结构如图所示:

从图中可以看出, bit13 V 位,此位是向量表控制位,当为 0 的时候向量表基地址为 0X00000000,软件可以重定位向量表。为 1 的时候向量表基地址为 0XFFFF0000,软件不能重定位向量表。这里将 V 清零,目的就是为了接下来的向量表重定位。
60 行将 r0 寄存器的值重写写入到寄存器 SCTLR 中。
63行设置r0寄存器的值为_start _start就是整个uboot的入口地址,其值为0X87800000,相当于 uboot 的起始地址,因此 0x87800000 也是向量表的起始地址。
64 行将 r0 寄存器的值(向量表值)写入到 CP15 c12 寄存器中,也就是 VBAR 寄存器。因此第 58~64 行就是设置向量表重定位的。
代码继续往下执行!
68 行如果没有定义 CONFIG_SKIP_LOWLEVEL_INIT 的话条件成立。我们没有定义CONFIG_SKIP_LOWLEVEL_INIT,因此条件成立,执行下面的语句。就是分别调用函数 cpu_init_cp15 cpu_init_crit _main
函数 cpu_init_cp15 用来设置 CP15 相关的内容,比如关闭 MMU 啥的,此函数同样在 start.S文件中定义的,代码如下:

函数 cpu_init_crit 也在是定义在 start.S 文件中,函数内容如下:

可以看出函数 cpu_init_crit 内部仅仅是调用了函数 lowlevel_init,接下来就是详细的分析一下 lowlevel_init _main 这两个函数。

lowlevel_init 函数

函数 lowlevel_init 在文件 arch/arm/cpu/armv7/lowlevel_init.S 中定义,内容如下:

22 行设置 sp 指向 CONFIG_SYS_INIT_SP_ADDR CONFIG_SYS_INIT_SP_ADDR include/configs/mx6ullevk.h 文件中,在 mx6ullevk.h 中有如下所示定义:

上图中 IRAM_BASE_ADDR IRAM_SIZE arch/arm/include/asm/arch-mx6/imx-regs.h 中有定义,如下所示,其实就是 IMX6UL/IM6ULL 内部 ocram 的首地址和大小。

71 #define IRAM_BASE_ADDR 0x00900000

......

408 #if !(defined(CONFIG_MX6SX) || defined(CONFIG_MX6UL) || \

409 defined(CONFIG_MX6SLL) || defined(CONFIG_MX6SL))

410 #define IRAM_SIZE 0x00040000

411 #else

412 #define IRAM_SIZE 0x00020000

413 #endif

如果 408 行的条件成立的话 IRAM_SIZE=0X40000,当定义了 CONFIG_MX6SXCONFIG_MX6U CONFIG_MX6SLL CONFIG_MX6SL 中的任意一个的话条件就不成立,在.config 中定义了 CONFIG_MX6UL,所以条件不成立,因此 IRAM_SIZE=0X20000=128KB。可以得到如下值:

CONFIG_SYS_INIT_RAM_ADDR = IRAM_BASE_ADDR = 0x00900000。

CONFIG_SYS_INIT_RAM_SIZE = 0x00020000 =128KB。

还需要知道GENERATED_GBL_DATA_SIZE的值,在文件include/generated/generic-asm-offsets.h中有定义,如下:

1 #ifndef __GENERIC_ASM_OFFSETS_H__

2 #define __GENERIC_ASM_OFFSETS_H__

3 /*

4 * DO NOT MODIFY.

5 *

6 * This file was generated by Kbuild

7 */

8 9

#define GENERATED_GBL_DATA_SIZE 256

10 #define GENERATED_BD_INFO_SIZE 80

11 #define GD_SIZE 248

12 #define GD_BD 0

13 #define GD_MALLOC_BASE 192

14 #define GD_RELOCADDR 48

15 #define GD_RELOC_OFF 68

16 #define GD_START_ADDR_SP 64

17

18 #endif

GENERATED_GBL_DATA_SIZE=256 GENERATED_GBL_DATA_SIZE 的含义为(sizeof(struct global_data) + 15) & ~15 。综上所述, CONFIG_SYS_INIT_SP_ADDR 值如下:

CONFIG_SYS_INIT_SP_OFFSET = 0x00020000 – 256 = 0x1FF00。

CONFIG_SYS_INIT_SP_ADDR = 0x00900000 + 0X1FF00 = 0X0091FF00,

结果如下图所示:

此时 sp 指向 0X91FF00,这属于 IMX6UL/IMX6ULL 的内部 ram
继续回到文件 lowlevel_init.S,第 23 行对 sp 指针做 8 字节对齐处理!
34 行, sp 指针减去 GD_SIZE GD_SIZE 同样在 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

s_init 函数详解

在上一小节中,我们知道 lowlevel_init 函数后面会调用 s_init 函数, s_init 函数定义在文件
arch/arm/cpu/armv7/mx6/soc.c 中,如下所示:

在第 816 行会判断当前 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,函数调用路径如图所示:

从图中可知,接下来要执行的是 save_boot_params_ret 中的_main 函数,接下来分析_main 函数。

_main 函数

_main 函数定义在文件 arch/arm/lib/crt0.S 中,函数内容如下:

76 行,设置 sp 指针为 CONFIG_SYS_INIT_SP_ADDR,也就是 sp 指向 0X0091FF00
83 行, sp 8 字节对齐。
85 行,读取 sp 到寄存器 r0 里面,此时 r0=0X0091FF00
86 行,调用函数 board_init_f_alloc_reserve,此函数有一个参数,参数为 r0 中的值,也就是 0X0091FF00,此函数定义在文件 common/init/board_init.c 中,内容如下:

函数 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
继续回到代码中,第 87 行,将 r0 写入到 sp 里面, r0 保存着函数board_init_f_alloc_reserve 的返回值,所以这一句也就是设置 sp=0X0091FA00
89 行,将 r0 寄存器的值写到寄存器 r9 里面,因为 r9 寄存器存放着全局变量 gd 的地址,在文件 arch/arm/include/asm/global_data.h 中有如图所示宏定义:

uboot 中定义了一个指向 gd_t 的指针 gd gd 存放在寄存器 r9 里面,因此 gd 是个全局变量。 gd_t 是个结构体,在 include/asm-generic/global_data.h 里面有定义。因此这一行代码就是设置 gd 所指向的位置,也就是 gd 指向 0X0091FA00
继续回到代码中,第 90 行调用函数 board_init_f_init_reserve,此函数在文件common/init/board_init.c 中有定义,函数内容如下:

可以看出,此函数用于初始化 gd,其实就是清零处理。另外,此函数还设置了gd->malloc_base gd 基地址+gd 大小=0X0091FA00+248=0X0091FAF8,在做 16 字节对齐,最终 gd->malloc_base等于0X0091FB00,这个也就是 early malloc 的起始地址。


继续回到代码中,第 92 行设置 R0 0
93 行,调用 board_init_f 函数,此函数定义在文件 common/board_f.c 中!主要用来初始
DDR,定时器,完成代码拷贝等等,此函数我们后面在详细的分析。

103 行,重新设置环境(sp gd)、获取 gd->start_addr_sp 的值赋给 sp,在函数 board_init_f中会初始化 gd 的所有成员变量,其中 gd->start_addr_sp=0X9EF44E90 所以这里相当于设置
sp=gd->start_addr_sp=0X9EF44E90 0X9EF44E90 DDR 中的地址,说明新的 sp gd 将会存放到 DDR 中,而不是内部的 RAM 了。 GD_START_ADDR_SP=64
109 行, sp 8 字节对齐。
111 行,获取 gd->bd 的地址赋给 r9,此时 r9 存放的是老的 gd,这里通过获取 gd->bd
地址来计算出新的 gd 的位置。 GD_BD=0
112 行,新的 gd bd 下面,所以 r9 减去 gd 的大小就是新的 gd 的位置,获取到新的 gd的位置以后赋值给 r9
114 行,设置 lr 寄存器为 here,这样后面执行其他函数返回的时候就返回到了第 122 行的 here 位置处。
115,读取 gd->reloc_off 的值复制给 r0 寄存器, GD_RELOC_OFF=68
116 行, lr 寄存器的值加上 r0 寄存器的值,重新赋值给 lr 寄存器。因为接下来要重定位代码,也就是把代码拷贝到新的地方去(现在的 uboot 存放的起始地址为 0X87800000,下面要将 uboot 拷贝到 DDR 最后面的地址空间,将 0X87800000 开始的内存空出来),其中就包括here,因此 lr 中的 here 要使用重定位后的位置。
120 行,读取 gd->relocaddr 的值赋给 r0 寄存器,此时 r0 寄存器就保存着 uboot 要拷贝的目的地址,为 0X9FF47000 GD_RELOCADDR=48
121 行,调用函数 relocate_code,也就是代码重定位函数,此函数负责将 uboot 拷贝到新的地方去,此函数定义在文件 arch/arm/lib/relocate.S 中稍后会详细分析此函数。
继续回到代码第 127 行,调用函数 relocate_vectors,对中断向量表做重定位,此函数定义在文件 arch/arm/lib/relocate.S 中,稍后会详细分析此函数。
继续回到代码第 131 行,调用函数 c_runtime_cpu_setup,此函数定义在文件arch/arm/cpu/armv7/start.S 中,函数内容如下:

关闭I-cache

141~159 行,清除 BSS 段。
167 行,设置函数 board_init_r 的两个参数,函数 board_init_r 声明如下:

board_init_r(gd_t *id, ulong dest_addr)

第一个参数是 gd,因此读取 r9 保存到 r0 里面。
168 行,设置函数 board_init_r 的第二个参数是目的地址,因此 r1= gd->relocaddr
174 行、调用函数 board_init_r,此函数定义在文件 common/board_r.c 中,稍后会详细的分析此函数。
这个就是_main 函数的运行流程,在_main 函数里面调用了 board_init_f relocate_coderelocate_vectors board_init_r 4 个函数,接下来依次看一下这 4 个函数都是干啥的。

board_init_f 函数

main 中会调用 board_init_f 函数, board_init_f 函数主要有两个工作:
、初始化一系列外设,比如串口、定时器,或者打印一些消息等。
、初始化 gd 的各个成员变量, uboot 会将自己重定位到 DRAM 最后面的地址区域,也就是将自己拷贝到 DRAM 最后面的内存区域中。这么做的目的是给 Linux 腾出空间,防止 Linux kernel 覆盖掉 uboot,将 DRAM 前面的区域完整的空出来。在拷贝之前肯定要给 uboot 各部分分配好内存位置和大小,比如 gd 应该存放到哪个位置, malloc 内存池应该存放到哪个位置等等。这些信息都保存在 gd 的成员变量中,因此要对 gd 的这些成员变量做初始化。最终形成一个完整的内存分配图,在后面重定位 uboot 的时候就会用到这个内存分配图
此函数定义在文件 common/board_f.c 中定义,代码如下:

因为没有定义CONFIG_SYS_GENERIC_GLOBAL_DATA,所以第1037~1054行代码无效。
1056 行,初始化 gd->flags=boot_flags=0
1057 行,设置 gd->have_console=0
重点在第 1059 行!通过函数 initcall_run_list 来运行初始化序列 init_sequence_f 里面的一些列函数, init_sequence_f 里面包含了一系列的初始化函数, init_sequence_f 也是定义在文件common/board_f.c 中, init_sequence_f 定义如下:

833行, setup_mon_len 函数设置 gd mon_len 成员变量,此处为__bss_end -_start,也就是整个代码的长度。 0X878A8E74-0x87800000=0XA8E74,这个就是代码长度。
840 行, initf_malloc 函数初始化 gd 中跟 malloc 有关的成员变量,比如 malloc_limit,此函数会设置 gd->malloc_limit = CONFIG_SYS_MALLOC_F_LEN=0X400 malloc_limit 表示 malloc内存池大小。
841 initf_console_record CONFIG_CONSOLE_RECORD
CONFIG_SYS_MALLOC_F_LEN 的话此函数就会调用函数 console_record_init,但是 IMX6ULL uboot 没有定义宏 CONFIG_CONSOLE_RECORD,所以此函数直接返回 0
849 行, arch_cpu_init 函数。
850 行, initf_dm 函数,驱动模型的一些初始化。
851 行, arch_cpu_init_dm 函数未实现。
852 行, mark_bootstage 函数应该是和啥标记有关的
854 行, board_early_init_f 函数,板子相关的早期的一些初始化设置, I.MX6ULL 用来初始化串口的 IO 配置。

869 行, timer_init,初始化定时器, Cortex-A7 内核有一个定时器,这里初始化的就是 CortexA 内核的那个定时器。通过这个定时器来为 uboot 提供时间。就跟 Cortex-M 内核 Systick 定时器一样。关于 Cortex-A 内部定时器的详细内容,请参考文档《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf “Chapter B8 The Generic Timer”章节。
877 行, board_postclk_init,对于 I.MX6ULL 来说是设置 VDDSOC 电压。
880 行, get_clocks 函数用于获取一些时钟值, I.MX6ULL 获取的是 sdhc_clk 时钟,也就是 SD 卡外设的时钟。
882 行, env_init 函数是和环境变量有关的,设置 gd 的成员变量 env_addr,也就是环境变量的保存地址。
890 行, init_baud_rate 函数用于初始化波特率,根据环境变量 baudrate 来初始化 gd->baudrate
891 行, serial_init,初始化串口。
892 行, console_init_f,设置 gd->have_console 1,表示有个控制台,此函数也将前面暂存在缓冲区中的数据通过控制台打印出来。
899 行、 display_options,通过串口输出一些信息,如图所示:

900 行, display_text_info,打印一些文本信息,如果开启 UBOOT DEBUG 功能的话就会输出 text_base bss_start bss_end,形式如下:

debug("U-Boot code: %08lX -> %08lX BSS: -> %08lX\n",text_base, bss_start, bss_end);

继续初始化序列:

911 行, print_cpuinfo 函数用于打印 CPU 信息,结果如图所示:

916 行, show_board_info 函数用于打印板子信息,会调用 checkboard 函数,结果如图所示:

918 行, INIT_FUNC_WATCHDOG_INIT,初始化看门狗,对于 I.MX6ULL 来说是空函数
922 行, INIT_FUNC_WATCHDOG_RESET,复位看门狗,对于 I.MX6ULL 来说是空函数
924 行, init_func_i2c 函数用于初始化 I2C,初始化完成以后会输出如图所示信息:

929 行, announce_dram_init,此函数很简单,就是输出字符串“DRAM:”
933 行, dram_init,并非真正的初始化 DDR,只是设置 gd->ram_size 的值,对于正点原子 I.MX6ULL 开发板 EMMC 版本核心板来说就是 512MB

继续初始化序列:

939 行, post_init_f,此函数用来完成一些测试,初始化 gd->post_init_f_time
943 行, testdram,测试 DRAM,空函数。
963 行, 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 卡启动,重启开发板,打开SecureCRT uboot 会输出如图所示信息:

继续:

977 reserve_round_4k gd->relocaddr 4KB
gd->relocaddr=0XA0000000,已经是 4K 对齐了,所以调整后不变。
980 行, reserve_mmu,留出 MMU TLB 表的位置,分配 MMU TLB 表内存以后会对 gd->relocaddr 64K 字节对齐。完成以后 gd->arch.tlb_size gd->arch.tlb_addr gd->relocaddr如图所示:

995 行, reserve_trace 函数,留出跟踪调试的内存, I.MX6ULL 没有用到!
997 行, reserve_uboot 留出重定位后的 uboot 所占用的内存区域, uboot 所占用大小由
gd->mon_len 所指定,留出 uboot 的空间以后还要对 gd->relocaddr 4K 字节对齐,并且重新设
gd->start_addr_sp,结果如图所示:

1000 行, reserve_malloc,留出 malloc 区域,调整 gd->start_addr_sp 位置, malloc 区域由宏
TOTAL_MALLOC_LEN 定义,宏定义如下:

#define TOTAL_MALLOC_LEN (CONFIG_SYS_MALLOC_LEN +CONFIG_ENV_SIZE)

mx6ull_alientek_emmc.h 文件中定义宏 CONFIG_SYS_MALLOC_LEN 16MB=0X1000000
CONFIG_ENV_SIZE=8KB=0X2000,因此 TOTAL_MALLOC_LEN=0X1002000。调整以后
gd->start_addr_sp 如图所示:

1001 行, reserve_board 函数,留出板子 bd 所占的内存区, bd 是结构体 bd_t bd_t 大小为80 字节,结果如图所示:

1003 行, setup_machine,设置机器 ID linux 启动的时候会和这个机器 ID 匹配,如果匹配的话 linux 就会启动正常。但是 I.MX6ULL 不用这种方式了,这是以前老版本的 uboot linux 使用的,新版本使用设备树了,因此此函数无效。
1004 行, reserve_global_data 函数,保留出 gd_t 的内存区域, gd_t 结构体大小为 248B,结果如图所示:

1005 行, reserve_fdt,留出设备树相关的内存区域, I.MX6ULL uboot 没有用到,因此此函数无效。
1006 行, reserve_arch 是个空函数。
1007 行, reserve_stacks,留出栈空间,先对 gd->start_addr_sp 减去 16,然后做 16 字节对齐。如果使能 IRQ 的话还要留出 IRQ 相应的内存,具体工作是由 arch/arm/lib/stack.c 文件中的函数 arch_reserve_stacks 完成。结果如图所示:

在本 uboot 中并没有使用到 IRQ,所以不会留出 IRQ 相应的内存区域。
1008 行, setup_dram_config 函数设置 dram 信息,就是设置 gd->bd->bi_dram[0].start
gd->bd->bi_dram[0].size,后面会传递给 linux 内核,告诉 linux DRAM 的起始地址和大小。结果如图所示:

1009 行, show_dram_config 函数,用于显示 DRAM 的配置,如图所示:

继续:

1017 行, display_new_sp 函数,显示新的 sp 位置,也就是 gd->start_addr_sp,不过要定义宏 DEBUG,结果如图所示:

图中的 gd->start_addr_sp 值和我们前面分析的最后一次修改的值一致。
1022 行, reloc_fdt 函数用于重定位 fdt,没有用到。
1023 行, setup_reloc,设置 gd 的其他一些成员变量,供后面重定位的时候使用,并且将以前的 gd 拷贝到 gd->new_gd 处。需要使能 DEBUG 才能看到相应的信息输出,如图所示:

从图中可以看出, uboot 重定位后的偏移为 0X18747000,重定位后的新地址为0X9FF4700,新的 gd 首地址为 0X9EF44EB8,最终的 sp 0X9EF44E90
至此, board_init_f 函数就执行完成了,最终的内存分配如图所示:

relocate_code 函数

relocate_code 函数是用于代码拷贝的,此函数定义在文件 arch/arm/lib/relocate.S 中,代码如下:

80 行, r1=__image_copy_start,也就是 r1 寄存器保存源地址,__image_copy_start=0X87800000
81 行, r0=0X9FF47000,这个地址就是 uboot 拷贝的目标首地址。 r4=r0-r1=0X9FF47000-0X87800000=0X18747000,因此 r4 保存偏移量。
82 行,如果在第 81 中, r0-r1 等于 0,说明 r0 r1 相等,也就是源地址和目的地址是
一样的,那肯定就不需要拷贝了!执行 relocate_done 函数
83 行, r2=__image_copy_end r2 中保存拷贝之前的代码结束地址__image_copy_end =0x8785dd54
84 行,函数 copy_loop 完成代码拷贝工作!从 r1,也就是__image_copy_start 开始,读取 uboot 代码保存到 r10 r11 中,一次就只拷贝这 2 32 位的数据。拷贝完成以后 r1 的值会更新,保存下一个要拷贝的数据地址。
87 行,将 r10 r11 的数据写到 r0 开始的地方,也就是目的地址。写完以后 r0 的值会更新,更新为下一个要写入的数据地址。
88 行,比较 r1 是否和 r2 相等,也就是检查是否拷贝完成,如果不相等的话说明没有拷贝完成, 没有拷贝完成的话就跳转到 copy_loop 接着拷贝,直至拷贝完成。
接下来的第 94 ~109 行是重定位.rel.dyn 段, .rel.dyn 段是存放.text 段中需要重定位地址的集合。重定位就是 uboot 将自身拷贝到 DRAM 的另一个地放去继续运行(DRAM 的高地址处)。我们知道,一个可执行的 bin 文件,其链接地址和运行地址要相等,也就是链接到哪个地址,在运行之前就要拷贝到哪个地址去。现在我们重定位以后,运行地址就和链接地址不同了,这样寻址的时候不会出问题吗?

uboot 对于重定位后链接地址和运行地址不一致的解决方法就是采用位置无关码,在使用 ld 进行链接的时候使用选项“ -pie”生成位置无关的可执行文件。在文件arch/arm/config.mk 下有如下代码:

# needed for relocation

LDFLAGS_u-boot += -pie

使用“-pie”选项以后会生成一个.rel.dyn 段, uboot 就是靠这个.rel.dyn 来解决重定位问题的,在 u-bot.dis .rel.dyn 段中有如下所示内容:

Disassembly of section .rel.dyn:

8785da44 <__rel_dyn_end-0x8ba0>:

8785da44: 87800020 strhi r0, [r0, r0, lsr #32]

8785da48: 00000017 andeq r0, r0, r7, lsl r0

......

8785dfb4: 87804198 ; <UNDEFINED> instruction: 0x87804198

8785dfb8: 00000017 andeq r0, r0, r7, lsl r0

先来看一下.rel.dyn 段的格式,类似第 7 行和第 8 行这样的是一组,也就是两个 4 字节数据为一组。高 4 字节是 Label 地址标识 0X17,低 4 字节就是 Label 的地址,首先判断 Label 地址标识是否正确,也就是判断高 4 字节是否为 0X17,如果是的话低 4 字节就是 Label 地址值。
7 行值为 0X87804198,第 8 行为 0X00000017,说明第 7 行的 0X87804198 是个 Label,根据前面的分析,只要将地址0X87804198+offset 处的值改为重定位后的地址即可。我们猜测的是否正确,看一下
uboot .rel.dyn 段的重定位即可 .rel.dyn 段的重定位代码如下:

94 行, r2=__rel_dyn_start,也就是.rel.dyn 段的起始地址。
95 行, r3=__rel_dyn_end,也就是.rel.dyn 段的终止地址。
97 行,从.rel.dyn 段起始地址开始,每次读取两个 4 字节的数据存放到 r0 r1 寄存器中, r0 存放低 4 字节的数据,也就是 Label 地址; r1 存放高 4 字节的数据,也就是 Label 标志。
98 行, r1 中给的值与 0xff 进行与运算,其实就是取 r1 的低 8 位。
99 行,判断 r1 中的值是否等于 23(0X17)
100 行,如果 r1 不等于 23 的话就说明不是描述 Label 的,执行函数 fixnext,否则的话继续执行下面的代码。
103 行, r0 保存着 Label 值, r4 保存着重定位后的地址偏移, r0+r4 就得到了重定位后的Label 值。此时 r0 保存着重定位后的 Label 值,相当于 0X87804198+0X18747000=0X9FF4B198
104,读取重定位后 Label 所保存的变量地址,此时这个变量地址还是重定位前的(相当于变量重定位前的地址 0X8785DA50),将得到的值放到 r1 寄存器中。
105 r1+r4 rel_a
0X8785DA50+0X18747000=0X9FFA4A50
106 行,重定位后的变量地址写入到重定位后的 Label 中,相等于设置地址 0X9FF4B198处的值为 0X9FFA4A50
108 行,比较 r2 r3,查看.rel.dyn 段重定位是否完成。
109 行,如果 r2 r3 不相等,说明.rel.dyn 重定位还未完成,因此跳到 fixloop 继续重定位.rel.dyn 段。
可以看出, uboot 中对.rel.dyn 段的重定位方法和我们猜想的一致。 .rel.dyn 段的重定位比较
复杂一点,有点绕,因为涉及到链接地址和运行地址的问题。

relocate_vectors 函数

函数 relocate_vectors 用于重定位向量表,此函数定义在文件 relocate.S 中, 函数源码如下:

29 行,如果定义了 CONFIG_CPU_V7M 的话就执行第 30~36 行的代码,这是 Cortex-M内核单片机执行的语句,因此对于 I.MX6ULL 来说是无效的。
38 行,如果定义了 CONFIG_HAS_VBAR 的话就执行此语句,这个是向量表偏移, CortexA7 是支持向量表偏移的。而且,在.config 里面定义了 CONFIG_HAS_VBAR,因此会执行这个分支。
43 行, r0=gd->relocaddr,也就是重定位后 uboot 的首地址,向量表肯定是从这个地址开始存放的。
44 行,将 r0 的值写入到 CP15 VBAR 寄存器中,也就是将新的向量表首地址写入到寄存器 VBAR 中,设置向量表偏移。

board_init_r 函数

前面讲解了 board_init_f 函数,在此函数里面会调用一系列的函数来初始化一些外设和 gd 的成员变量。但是 board_init_f 并没有初始化所有的外设,还需要做一些后续工作,这些后续工作就是由函数 board_init_r 来完成的, board_init_r 函数定义在文件 common/board_r.c中,代码如下:

1010 行调用 initcall_run_list 函数来执行初始化序列 init_sequence_r init_sequence_r 是一个函数集合, init_sequence_r 也定义在文件 common/board_r.c 中,init_sequence_r 定义如下:

774 行, initr_trace 函数,如果定义了宏 CONFIG_TRACE 的话就会调用函数 trace_init,初始化和调试跟踪有关的内容。
775 行, initr_reloc 函数用于设置 gd->flags,标记重定位完成。
778 行, initr_caches 函数用于初始化 cache,使能 cache
786 行, initr_reloc_global_data 函数,初始化重定位后 gd 的一些成员变量。
790 行, initr_barrier 函数, PPC架构使用。
791 行, initr_malloc 函数,初始化 malloc
792 行, initr_console_record 函数,初始化控制台相关的内容, 需要定义CONSOLE_RECORD
796 行, bootstage_relocate 函数,启动状态重定位。
800 行, initr_bootstage 函数,初始化 bootstage 什么的。
802 行, board_init 函数,板级初始化,该函数一般由soc原厂或用户编写。
这里执行的是 mx6ull_alientek_emmc.c 文件中的 board_init 函数。

813 行, stdio_init_tables 函数, stdio 相关初始化。
814 行, initr_serial 函数,初始化串口。
815 行, initr_announce 函数,与调试有关,通知已经在 RAM 中运行。

853 行, power_init_board 函数,初始化电源芯片,即PMIC
855 行, initr_flash 函数,初始化flash
867 行, initr_nand 函数,初始化 NAND,如果使用 NAND 版本核心板的话就会初始化NAND
873 行, initr_mmc 函数,初始化 EMMC,如果使用 EMMC 版本核心板的话就会初始化EMMC,串口输出如图所示信息:

从图中可以看出,此时有两个 EMCM 设备, FSL_SDHC:0 FSL_SDHC:1
878 行, initr_env 函数,初始化环境变量。
883 行, initr_secondary_cpu 函数,初始化其他 CPU 核。

894 行, stdio_add_devices 函数,各种输入输出设备的初始化,如 LCD driver I.MX6ULL使用 drv_video_init 函数初始化 LCD。会输出如图所示信息:

895 行, initr_jumptable 函数,初始化跳转表。
899 console_init_r
stdio_print_current_devices 函数来打印出当前的控制台设备,如图所示:

913 行, interrupt_init 函数,初始化中断。
915 行, initr_enable_interrupts 函数,使能中断。
925 行, initr_ethaddr 函数,初始化网络地址,也就是获取 MAC 地址。读取环境变量“ethaddr”的值。

928 行, board_late_init 函数,板子后续初始化,此函数定义在文件 mx6ull_alientek_emmc.c中,如果环境变量存储在 EMMC 或者 SD 卡中的话此函数会调用 board_late_mmc_env_init 函数初始化 EMMC/SD。会切换到正在时候用的 emmc 设备,代码如图所示:

图中的第 46 行和第 47 行就是运行“mmc dev xx”命令,用于切换到正在使用的EMMC 设备,串口输出信息如图所示:

952 行,initr_net函数,初始化网络,函数调用顺序为 initr_net->eth_initialize->board_eth_init() 串口输出如图所示信息:

988 行, run_main_loop 行,主循环,处理命令。

run_main_loop 函数

uboot 启动以后会进入 3 秒倒计时,如果在 3 秒倒计时结束之前按下按下回车键,那么就会进入 uboot 的命令模式,如果倒计时结束以后都没有按下回车键,那么就会自动启动 Linux 内核 这个功能就是由 run_main_loop run_main_loop common/board_r.c 中,函数内容如下:

759 行和第 760 行是个死循环,“for(;;)”“while(1)”功能一样,死循环里面就一个main_loop 函数, main_loop 函数定义在文件 common/main.c 里面,代码如下:

48 行,调用 bootstage_mark_name 函数,打印出启动进度。
57 行,如果定义了宏 CONFIG_VERSION_VARIABLE 的话就会执行函数 setenv,设置换将变量 ver 的值为 version_string,也就是设置版本号环境变量。 version_string 定义在文件cmd/version.c 中,定义如下:

const char __weak version_string[] = U_BOOT_VERSION_STRING;

U_BOOT_VERSION_STRING 是个宏, 定义在文件 include/version.h,如下:

U_BOOT_VERSION include/generated/version_autogenerated.h
version_autogenerated.h 内如如下:

#define PLAIN_VERSION "2016.03"

#define U_BOOT_VERSION "U-Boot " PLAIN_VERSION

#define CC_VERSION_STRING "arm-linux-gnueabihf-gcc (Linaro GCC 4.9-2017.01) 4.9.4"

#define LD_VERSION_STRING "GNU ld (Linaro_Binutils-2017.01)2.24.0.20141017 Linaro 2014_11-3-git"

可以看出, U_BOOT_VERSION “U-boot 2016.03”U_BOOT_DATE U_BOOT_TIME U_BOOT_TZ include/generated/timestamp_autogenerated.h 中,如下所示:

#define U_BOOT_DATE "Apr 25 2019"

#define U_BOOT_TIME "21:10:53"

#define U_BOOT_TZ "+0800"

#define U_BOOT_DMI_DATE "04/25/2019"

CONFIG_IDENT_STRING 为空,所以 U_BOOT_VERSION_STRING “U-Boot 2016.03(Apr 25 2019 – 21:10:53 +0800)”,进入 uboot 命令模式,输入命令“version”查看版本号,如图所示:

上图中的第一行就是 uboot 版本号,和我们分析的一致。
接着回到函数main_loop中,第 60 行, cli_init 函数,跟命令初始化有关,初始化 hush shell 相关的变量。
62 行, run_preboot_environment_command 函数,获取环境变量 perboot 的内容, preboot是一些预启动命令,一般不使用这个环境变量。
68 行, bootdelay_process 函数,此函数会读取环境变量 bootdelay bootcmd 的内容,然后将 bootdelay 的值赋值给全局变量 stored_bootdelay,返回值为环境变量 bootcmd 的值。
69 行,如果定义了 CONFIG_OF_CONTROL 的话函数 cli_process_fdt 就会实现,如果没有定义 CONFIG_OF_CONTROL 的话函数 cli_process_fdt 直接返回一个 false。在本 uboot 中没有定义 CONFIG_OF_CONTROL,因此 cli_process_fdt 函数返回值为 false
72 行, autoboot_command 函数,此函数就是检查倒计时是否结束,倒计时结束之前有没有被打断,此函数定义在文件 common/autoboot.c 中,内容如下:

当以下三条全部成立的话,就会执行函数 run_command_list
stored_bootdelay 不等于-1
s 不为空。
、函数 abortboot 返回值为 0
stored_bootdelay 等于环境变量 bootdelay 的值; s 是环境变量 bootcmd 的值,一般不为空,因此前两个成立,就剩下了函数abortboot 的返回值,abortboot函数也定义在文件common/autoboot.c ,内容如下:

因为宏 CONFIG_AUTOBOOT_KEYE 未定义,因此执行函数 abortboot_normal,接着来看函数abortboot_normal,此函数也定义在文件 common/autoboot.c 中,内容如下:

227 行的变量 abort 是函数 abortboot_normal 的返回值,默认值为 0
234 行通过串口输出“Hit any key to stop autoboot”字样,如图所示:

251-267 行就是倒计时的具体实现。

256 行判断键盘是否有按下,也就是是否打断了倒计时,如果键盘按下的话就执行相应的分支。比如设置 abort 1,设置 bootdelay 0 等,最后跳出倒计时循环。
279 行,返回abort的值,如果倒计时自然结束,没有被打断 abort 就为 0,否则的话 abort的值就为1
回到autoboot_command 函数中,如果倒计时自然结束那么就执行函数run_command_list,此函数会执行参数 s 指定的一系列命令,也就是环境变量 bootcmd 的命令,bootcmd 里面保存着默认的启动命令,因此 linux 内核启动!这个就是 uboot 中倒计时结束以后自动启动 linux 内核的原理。如果倒计时结束之前按下了键盘上的按键,那么 run_command_list函数就不会执行,相当于 autoboot_command 是个空函数。
回到遥远 main_loop 函数中,如果倒计时结束之前按下按键,那么就会执行第 74 行的 cli_loop 函数,这个就是命令处理函数,负责接收处理输入的命令。

cli_loop 函数

cli_loop 函数是 uboot 的命令行处理函数,我们在 uboot 中输入各种命令,进行各种操作就是有 cli_loop 来处理的,此函数定义在文件 common/cli.c 中,函数内容如下:

在文件 include/configs/mx6_common.h 中有定义宏 CONFIG_SYS_HUSH_PARSER,而开发板配置头文件 mx6ullevk.h 里面会引用 mx_common.h 这个头文件,因此宏 CONFIG_SYS_HUSH_PARSER 有定义。
205 行调用函数 parse_file_outer
207 行是个死循环,永远不会执行到这里。
函数 parse_file_outer 定义在文件 common/cli_hush.c 中,函数内容如下:

3296 行调用函数 setup_file_in_str 初始化变量 input 的成员变量。
3300 行调用函数 parse_stream_outer,这个函数就是 hush shell 的命令解释器,负责接收命令行输入,然后解析并执行相应的命令,函数parse_stream_outer 定义在文件 common/cli_hush.c中,函数内容如下:

1 static int parse_stream_outer(struct in_str *inp, int flag)
2 {
3 struct p_context ctx;
4 o_string temp=NULL_O_STRING;
5 int rcode;
6 int code = 1;
7 do {
8 ......
9 rcode = parse_stream(&temp, &ctx, inp,
10 flag & FLAG_CONT_ON_NEWLINE ? -1 : '\n');
11 ......
12 if (rcode != 1 && ctx.old_flag == 0) {
13 ......
14 run_list(ctx.list_head);
15 ......
16 } else {
17 ......
18 }
19 b_free(&temp);
20 /* loop on syntax errors, return on EOF */
21 } while (rcode != -1 && !(flag & FLAG_EXIT_FROM_LOOP) &&
22 (inp->peek != static_peek || b_peek(inp)));
23 return 0;
24 }

7~21 行中的 do-while 循环就是处理输入命令的。
9 行调用函数 parse_stream 进行命令解析。
14 行调用调用 run_list 函数来执行解析出来的命令。函数 run_list 会经过一系列的函数调用,最终通过调用 cmd_process 函数来处理命令,过程如下:

……

 

……

run_list 函数调用 run_list_real 函数。
run_list_real 函数调用 run_pipe_real 函数。
run_pipe_real 函数调用 cmd_process 函数。
最终通过函数 cmd_process 来处理命令,接下来就是分析 cmd_process 函数。

cmd_process 函数

在学习cmd_process 之前先看一下uboot中命令是如何定义的。uboot使用宏U_BOOT_CMD来定义命令,宏 U_BOOT_CMD 定义在文件 include/command.h 中,定义如下:

U_BOOT_CMD U_BOOT_CMD_COMPLETE U_BOOT_CMD_COMPLETE NULL U_BOOT_CMD U_BOOT_CMD_COMPLETE 如下:

U_BOOT_CMD_COMPLETE ll_entry_declare U_BOOT_CMD_MKENT_COMPLETE ll_entry_declar 定义在文件 include/linker_lists.h 中,定义如下:

_type cmd_tbl_t,因此 ll_entry_declare 就是定义了一个 cmd_tbl_t 变量,这里用到了 C 语言中的“##”连接符。其中的“##_list”表示用_list 的值来替换,“##_name”就是用_name 的值来替换。
U_BOOT_CMD_MKENT_COMPLETE 定义在文件 include/command.h 中,内容如下:

“ # ” _name U_BOOT_CMD_MKENT_COMPLETE 又用到了宏_CMD_HELP _CMD_COMPLETE,这两个宏的定义如下:

可以看出,如果定义了宏 CONFIG_AUTO_COMPLETE CONFIG_SYS_LONGHELP 的话 _CMD_COMPLETE _CMD_HELP ‘ , ’CONFIG_AUTO_COMPLETE CONFIG_SYS_LONGHELP mx6_common.h 中。
U_BOOT_CMD宏的流程我们已经清楚了,我们就以一个具体的命令为例,来看一下 U_BOOT_CMD 经过展开以后究竟是个什么模样的。以命令 dhcp 为例, dhcp 命令定义如下:

U_BOOT_CMD(

    dhcp, 3, 1, do_dhcp,

    "boot image via network using DHCP/TFTP protocol",

    "[loadAddress] [[hostIPaddr:]bootfilename]"

);

将其展开,结果如下:

U_BOOT_CMD(

    dhcp, 3, 1, do_dhcp,

    "boot image via network using DHCP/TFTP protocol",

    "[loadAddress] [[hostIPaddr:]bootfilename]"

);

1、将 U_BOOT_CMD 展开后为:

U_BOOT_CMD_COMPLETE(dhcp, 3, 1, do_dhcp,

    "boot image via network using DHCP/TFTP protocol",

    "[loadAddress] [[hostIPaddr:]bootfilename]",

  NULL)

2、将 U_BOOT_CMD_COMPLETE 展开后为:

ll_entry_declare(cmd_tbl_t, dhcp, cmd) = \

U_BOOT_CMD_MKENT_COMPLETE(dhcp, 3, 1, do_dhcp, \

    "boot image via network using DHCP/TFTP protocol", \

    "[loadAddress] [[hostIPaddr:]bootfilename]", \

  NULL);

3、将 ll_entry_declare 和 U_BOOT_CMD_MKENT_COMPLETE 展开后为:

cmd_tbl_t _u_boot_list_2_cmd_2_dhcp aligned(4) \ __attribute((unused,section(.u_boot_list_2_cmd_2_dhcp))) \

{ "dhcp", 3, 1, do_dhcp, \

    "boot image via network using DHCP/TFTP protocol", \

    "[loadAddress] [[hostIPaddr:]bootfilename]",\

NULL}

dhcp 命令最终展开结果为:

cmd_tbl_t _u_boot_list_2_cmd_2_dhcp __aligned(4) \

    __attribute__((unused,section(.u_boot_list_2_cmd_2_dhcp))) \

    { "dhcp", 3, 1, do_dhcp, \

    "boot image via network using DHCP/TFTP protocol", \

    "[loadAddress] [[hostIPaddr:]bootfilename]",\

  NULL}

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 结构体定义在文件 include/command.h 中,内容如下:

可以得出变量_u_boot_list_2_cmd_2_dhcp 的各个成员的值如下所示:

_u_boot_list_2_cmd_2_dhcp.name = "dhcp"

_u_boot_list_2_cmd_2_dhcp.maxargs = 3

_u_boot_list_2_cmd_2_dhcp.repeatable = 1

_u_boot_list_2_cmd_2_dhcp.cmd = do_dhcp

_u_boot_list_2_cmd_2_dhcp.usage = "boot image via network using DHCP/TFTP protocol"

_u_boot_list_2_cmd_2_dhcp.help = "[loadAddress] [[hostIPaddr:]bootfilename]"

_u_boot_list_2_cmd_2_dhcp.complete = NULL

当我们在 uboot 的命令行中输入“dhcp”这个命令的时候,最终执行的是 do_dhcp 这个函数。总结一下, uboot 中使用 U_BOOT_CMD 来定义一个命令,最终的目的就是为了定义一个cmd_tbl_t 类型的变量,并初始化这个变量的各个成员。 uboot 中的每个命令都存储在.u_boot_list段中,每个命令都有一个名为 do_xxx(xxx 为具体的命令名)的函数,这个 do_xxx 函数就是具体的命令处理函数。
了解了 uboot 中命令的组成以后,再来看一下 cmd_process 函数的处理过程, cmd_process函数定义在文件 common/command.c 中,函数内容如下:

507 行,调用函数 find_cmd 在命令表中找到指定的命令, find_cmd 函数内容如下:

参数 cmd 就是所查找的命令名字, uboot 中的命令表其实就是 cmd_tbl_t 结构体数组,通过函数 ll_entry_start 得到数组的第一个元素,也就是命令表起始地址。通过函数 ll_entry_count得到数组长度,也就是命令表的长度。最终通过函数 find_cmd_tbl 在命令表中找到所需的命令,每个命令都有一个 name 成员,所以将参数 cmd 与命令表中每个成员的 name 字段都对比一下,如果相等的话就说明找到了这个命令,找到以后就返回这个命令。
回到 cmd_process 函数中,找到命令以后肯定就要执行这个命令了,第 533 行调用函数 cmd_call 来执行具体的命令, cmd_call 函数内容如下:

在前面的分析中我们知道, cmd_tbl_t cmd 成员就是具体的命令处理函数,所以第 494 行调用 cmdtp cmd 成员来处理具体的命令,返回值为命令的执行结果。cmd_process 中会检测 cmd_tbl 的返回值,如果返回值为 CMD_RET_USAGE 的话就会调用cmd_usage 函数输出命令的用法,其实就是输出 cmd_tbl_t usage 成员变量。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/358415.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

虹科资讯| 虹科AR荣获汽车后市场“20佳”维修工具评委会提名奖!

2022 虹科荣获20佳维修工具 评委会提名奖 特大喜讯&#xff0c;在2月16日《汽车维修与保养》杂志主办的第十八届汽车后市场“20佳”评选活动中&#xff0c;虹科的产品“M400智能AR眼镜”凭借在AR领域的专业实力&#xff0c;通过层层筛选&#xff0c;在102款入围产品中脱颖而出…

GIT:【基础三】Git工作核心原理

目录 一、Git本地四个工作区域 二、Git提交文件流程 一、Git本地四个工作区域 工作目录(Working Directory)&#xff1a;电脑上存放开发代码的地方。暂存区(Stage/Index)&#xff1a;用于l临时存放改动的文件&#xff0c;本质上只是一个文件&#xff0c;保存即将提交到文件列…

[ 对比学习篇 ] 经典网络模型 —— Contrastive Learning

&#x1f935; Author &#xff1a;Horizon Max ✨ 编程技巧篇&#xff1a;各种操作小结 &#x1f3c6; 神经网络篇&#xff1a;经典网络模型 &#x1f4bb; 算法篇&#xff1a;再忙也别忘了 LeetCode [ 对比学习篇 ] 经典网络模型 —— Contrastive Learning&#x1f680; …

MongoDB介绍及使用教程

文章目录一、MongoDB介绍1. 什么是MongoDB2. 为什么要用MongoDB3. MongoDB的应用场景4. MongoDB基本概念二、MongoDB使用教程1.下载安装&#xff08;Windows&#xff09;2.MongoDB Conpass简单使用&#xff08;选学&#xff09;3.使用navicat连接MongoDB4.JAVA项目中使用MongoD…

JVM11 垃圾回收

1.1GC分类与性能指标 垃圾收集器没有在规范中进行过多的规定&#xff0c;可以由不同的厂商、不同版本的JVM来实现。 从不同角度分析垃圾收集器&#xff0c;可以将GC分为不同的类型。 Java不同版本新特性 语法层面&#xff1a;Lambda表达式、switch、自动拆箱装箱、enumAPI层面…

AI稳定生成图工业链路打造

前沿这篇文章会以比较轻松的方式&#xff0c;跟大家交流下如何控制文本生成图片的质量。要知道如何控制文本生成质量&#xff0c;那么我们首先需要知道我们有哪些可以控制的参数和模块。要知道我们有哪些控制的参数和模块&#xff0c;我们就得知道我们文本生成图片的这架机器或…

新手福利——x64逆向基础

一、x64程序的内存和通用寄存器 随着游戏行业的发展&#xff0c;x32位的程序已经很难满足一些新兴游戏的需求了&#xff0c;因为32位内存的最大值为0xFFFFFFFF&#xff0c;这个值看似足够&#xff0c;但是当游戏对资源需求非常大&#xff0c;那么真正可以分配的内存就显得捉襟…

测试人员如何运用好OKR

在软件测试工作中是不是还不知道OKR是什么?又或者每次都很害怕写OKR?或者总觉得很迷茫&#xff0c;不知道目标是什么? OKR 与 KPI 的区别 去年公司从KPI换OKR之后&#xff0c;我也有一段抓瞎的过程&#xff0c;然后自己找了两本书看&#xff0c;一本是《OKR工作法》&#xf…

WPF_ObservableCollection基本使用及其注意项

文章目录一、引言二、ObservableCollection三、结语一、引言 在GUI编程中经常会用到条目控件&#xff0c;常见的如ComboBox&#xff08;下拉列表框&#xff09;&#xff0c;它内部往往有多个项。 在使用一些图形框架&#xff08;Qt、WinForm&#xff09;上进行原始开发时&…

安卓mvvm

AndroidX的意思是android extension libraries, 也就是安卓扩展包 AndroidX其实是Jetpack类库的命名空间 (190条消息) AndroidX初识_Neda Wang的博客-CSDN博客https://blog.csdn.net/weixin_38261570/article/details/111500044 viewmodel ViewModel类旨在以注重生命周期的方…

【机器学习】决策树-C4.5算法

1.C4.5算法 C4.5算法与ID3相似&#xff0c;在ID3的基础上进行了改进&#xff0c;采用信息增益比来选择属性。ID3选择属性用的是子树的信息增益&#xff0c;ID3使用的是熵&#xff08;entropy&#xff0c; 熵是一种不纯度度量准则&#xff09;&#xff0c;也就是熵的变化值&…

回溯算法理论基础及组合问题

文章目录回溯算法理论基础什么是回溯法回溯法的效率回溯法解决的问题如何理解回溯法回溯法模板组合问题回溯算法理论基础 什么是回溯法 回溯法也可以叫做回溯搜索法&#xff0c;它是一种搜索的方式。 回溯是递归的副产品&#xff0c;只要有递归就会有回溯。 所以以下讲解中&…

LPWAN及高效弹性工业物联网核心技术方案

20多年前的一辆拖拉机就是一个纯机械的产品&#xff0c;里面可能并没有电子或者软件的构成&#xff1b;而随后随着软件的发展&#xff0c;拖拉机中嵌入了软件&#xff0c;它能控制发动机的功率及拖拉机防抱死系统&#xff1b;接下来&#xff0c;通过融入各种软件&#xff0c;拖…

js逆向基础篇-某房地产网站-登录

提示!本文章仅供学习交流,严禁用于任何商业和非法用途,如有侵权,可联系本文作者删除! 网站链接:aHR0cHM6Ly9tLmZhbmcuY29tL215Lz9jPW15Y2VudGVyJmE9aW5kZXgmY2l0eT1iag== 案例分析: 本篇文章分析的是登录逻辑。话不多说,先看看登录中有哪些加密参数,在登录页面随便输入…

K8S DNS解析过程和延迟问题

一、Linux DNS查询解析原理&#xff08;对于调用glibc库函数gethostbyname的程序&#xff09;我们在浏览器访问www.baidu.com这个域名&#xff0c;dns怎么查询到这台主机呢&#xff1f;  1、在浏览器中输入www.baidu.com域名&#xff0c;操作系统会先查找本地DNS解析器缓存&a…

实例2:树莓派GPIO控制外部LED灯闪烁

实例2&#xff1a;树莓派GPIO控制外部LED灯闪烁 实验目的 通过背景知识学习&#xff0c;了解四足机器人mini pupper搭载的微型控制计算机&#xff1a;树莓派。通过树莓派GPIO操作的学习&#xff0c;熟悉GPIO的读写控制。通过外部LED灯的亮灭控制&#xff0c;熟悉树莓派对外界…

vue3 + vite 使用 svg 可改变颜色

文章目录vue3 vite 使用 svg安装插件2、配置插件 vite.config.js3、根据vite配置的svg图标文件夹&#xff0c;建好文件夹&#xff0c;把svg图标放入4、在 src/main.js内引入注册脚本5、创建一个公共SvgIcon.vue组件6.1 全局注册SvgIcon.vue组件6.2、在想要引入svg的vue组件中引…

Boom 3D最新版本下载电脑音频增强应用工具

为了更好地感受音乐的魅力&#xff0c;Boom 3D 可以让你对音效进行个性化增强&#xff0c;并集成 3D 环绕立体声效果&#xff0c;可以让你在使用任何耳机时&#xff0c;都拥有纯正、优质的音乐体验。Boom 3D是一款充满神奇魅力的3D环绕音效升级版&#xff0c;BOOM 3D是一个全新…

MyBatis 之四(动态SQL之 if、trim、where、set、foreach 标签)

文章目录动态 SQL1. if 标签2. trim 标签3. where 标签4. set 标签5. foreach 标签回顾一下&#xff0c;在上一篇 MyBatis 之三&#xff08;查询操作 占位符#{} 与 ${}、like查询、resultMap、association、collection&#xff09;中&#xff0c;学习了针对查询操作的相关知识点…

【C++】map和set的封装

文章目录一、前情回顾二、简化源码三、仿函数四、迭代器五、set的实现六、map的实现七、红黑树代码一、前情回顾 set 参数只有 key&#xff0c;但是map除了key还有value。我们还是需要KV模型的红黑树的&#xff1a; #pragma once #include <iostream> #include <ass…