U-Boot 启动流程详解

news2024/10/5 16:31:43

目录

  • 链接脚本u-boot.lds 详解
  • U-Boot 启动流程详解
    • reset 函数源码详解
    • lowlevel_init 函数详解
    • s_init 函数详解
    • _main 函数详解
    • board_init_f 函数详解
    • relocate_code 函数详解
    • relocate_vectors 函数详解
    • board_init_r 函数详解
    • run_main_loop 函数详解
    • cli_loop 函数详解
    • cmd_process 函数详解
  • bootz 启动Linux 内核过程
    • images 全局变量
    • do_bootz 函数
    • bootz_start 函数
    • do_bootm_states 函数
    • bootm_os_get_boot_func 函数
    • do_bootm_linux 函数

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

链接脚本u-boot.lds 详解

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

图32.1.1 链接脚本
只有编译u-boot 以后才会在根目录下出现u-boot.lds 文件!
打开u-boot.lds,内容如下:

示例代码32.1.1 u-boot.lds 文件代码
1 OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
2 OUTPUT_ARCH(arm)
3 ENTRY(_start)
4 SECTIONS
5 {
6 . = 0x00000000;
7 . = ALIGN(4);
8 .text :
9 {
10 *(.__image_copy_start)
11 *(.vectors)
12 arch/arm/cpu/armv7/start.o (.text*)
13 *(.text*)
14 }
15 . = ALIGN(4);
16 .rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
17 . = ALIGN(4);
18 .data : {
19 *(.data*)
20 }
21 . = ALIGN(4);
22 . = .;
23 . = ALIGN(4);
24 .u_boot_list : {
25 KEEP(*(SORT(.u_boot_list*)));
26 }
27 . = ALIGN(4);
28 .image_copy_end :
29 {
30 *(.__image_copy_end)
31 }
32 .rel_dyn_start :
33 {
34 *(.__rel_dyn_start)
35 }
36 .rel.dyn : {
37 *(.rel*)
38 }
39 .rel_dyn_end :
40 {
41 *(.__rel_dyn_end)
42 }
43 .end :
44 {
45 *(.__end)
46 }
47 _image_binary_end = .;
48 . = ALIGN(4096);
49 .mmutable : {
50 *(.mmutable)
51 }
52 .bss_start __rel_dyn_start (OVERLAY) : {
53 KEEP(*(.__bss_start));
54 __bss_base = .;
55 }
56 .bss __bss_base (OVERLAY) : {
57 *(.bss*)
58 . = ALIGN(4);
59 __bss_limit = .;
60 }
61 .bss_end __bss_limit (OVERLAY) : {
62 KEEP(*(.__bss_end));
63 }
64 .dynsym _image_binary_end : { *(.dynsym) }
65 .dynbss : { *(.dynbss) }
66 .dynstr : { *(.dynstr*) }
67 .dynamic : { *(.dynamic*) }
68 .plt : { *(.plt*) }
69 .interp : { *(.interp*) }
70 .gnu.hash : { *(.gnu.hash) }
71 .gnu : { *(.gnu*) }
72 .ARM.exidx : { *(.ARM.exidx*) }
73 .gnu.linkonce.armexidx : { *(.gnu.linkonce.armexidx.*) }
74 }

第3 行为代码当前入口点:_start,_start 在文件arch/arm/lib/vectors.S 中有定义,如图32.1.2所示:
在这里插入图片描述

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

grep -nR "__image_copy_start"

搜索结果如图32.1.3 所示:
在这里插入图片描述
图32.1.3 查找结果
打开u-boot.map,找到如图32.1.4 所示位置:
在这里插入图片描述

图32.1.4 u-boot.map
u-boot.map 是uboot 的映射文件,可以从此文件看到某个文件或者函数链接到了哪个地址,
从图32.1.4 的932 行可以看到__image_copy_start 为0X87800000,而.text 的起始地址也是
0X87800000。
继续回到示例代码32.1.1 中,第11 行是vectors 段,vectors 段保存中断向量表,从图32.1.2
中我们知道了vectors.S 的代码是存在vectors 段中的。从图32.1.4 可以看出,vectors 段的起始地址也是0X87800000,说明整个uboot 的起始地址就是0X87800000,这也是为什么我们裸机例程的链接起始地址选择0X87800000 了,目的就是为了和uboot 一致。
第12 行将arch/arm/cpu/armv7/start.s 编译出来的代码放到中断向量表后面。
第13 行为text 段,其他的代码段就放到这里
在u-boot.lds 中有一些跟地址有关的“变量”需要我们注意一下,后面分析u-boot 源码的时候会用到,这些变量要最终编译完成才能确定的!!!比如我编译完成以后这些“变量”的值如表32.1.1 所示:
在这里插入图片描述
在这里插入图片描述
表32.1.1 uboot 相关变量表
表32.1.1 中的“变量”值可以在u-boot.map 文件中查找,表32.1.1 中除了__image_copy_start
以外,其他的变量值每次编译的时候可能会变化,如果修改了uboot 代码、修改了uboot 配置、选用不同的优化等级等等都会影响到这些值。所以,一切以实际值为准!

U-Boot 启动流程详解

reset 函数源码详解

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

示例代码32.2.1.1 vectors.S 代码段
38 /*
39 *************************************************************
40 *
41 * Exception vectors as described in ARM reference manuals
42 *
43 * Uses indirect branch to allow reaching handlers anywhere in
44 * memory.
45 **************************************************************
46 */
47
48 _start:
49
50 #ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
51 .word CONFIG_SYS_DV_NOR_BOOT_CFG
52 #endif
53
54 b reset
55 ldr pc, _undefined_instruction
56 ldr pc, _software_interrupt
57 ldr pc, _prefetch_abort
58 ldr pc, _data_abort
59 ldr pc, _not_used
60 ldr pc, _irq
61 ldr pc, _fiq

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

示例代码32.2.1.2 start.S 代码段
22 /*****************************************************************
23 *
24 * Startup Code (reset vector)
25 *
26 * Do important init only if we don't start from memory!
27 * Setup memory and board specific bits prior to relocation.
28 * Relocate armboot to ram. Setup stack.
29 *
30 *****************************************************************/
31
32 .globl reset
33 .globl save_boot_params_ret
34
35 reset:
36 /* Allow the board to save important registers */
37 b save_boot_params

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

示例代码32.2.1.3 start.S 代码段
91 /******************************************************************
92 *
93 * void save_boot_params(u32 r0, u32 r1, u32 r2, u32 r3)
94 * __attribute__((weak));
95 *
96 * Stack pointer is not yet initialized at this moment
97 * Don't save anything to stack even if compiled with -O0
98 *
99 ******************************************************************/
100 ENTRY(save_boot_params)
101 b save_boot_params_ret @ back to my caller

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

示例代码32.2.1.4 start.S 代码段
38 save_boot_params_ret:
39 /*
40 * disable interrupts (FIQ and IRQ), also set the cpu to SVC32
41 * mode, except if in HYP mode already
42 */
43 mrs r0, cpsr
44 and r1, r0, #0x1f @ mask mode bits
45 teq r1, #0x1a @ test for HYP mode
46 bicne r0, r0, #0x1f @ clear all mode bits
47 orrne r0, r0, #0x13 @ set SVC mode
48 orr r0, r0, #0xc0 @ disable FIQ and IRQ
49 msr cpsr,r0

第43 行,读取寄存器cpsr 中的值,并保存到r0 寄存器中。
第44 行,将寄存器r0 中的值与0X1F 进行与运算,结果保存到r1 寄存器中,目的就是提取cpsr 的bit0~bit4 这5 位,这5 位为M4 M3 M2 M1 M0,M[4:0]这五位用来设置处理器的工作
模式,如表32.2.1.1 所示:
在这里插入图片描述
表32.2.1.1 Cortex-A7 工作模式
第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 处于SVC32 模式,并且关闭
FIQ 和IRQ 这两个中断。
继续执行执行下面的代码:

示例代码32.2.1.5 start.S 代码段
51 /*
52 * Setup vector:
53 * (OMAP4 spl TEXT_BASE is not 32 byte aligned.
54 * Continue to use ROM code vector only in OMAP4 spl)
55 */
56 #if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD))
57 /* Set V=0 in CP15 SCTLR register - for VBAR to point to vector */
58 mrc p15, 0, r0, c1, c0, 0 @ Read CP15 SCTLR Register
59 bic r0, #CR_V @ V = 0
60 mcr p15, 0, r0, c1, c0, 0 @ Write CP15 SCTLR Register
61
62 /* Set vector address in CP15 VBAR register */
63 ldr r0, =_start
64 mcr p15, 0, r0, c12, c0, 0 @Set VBAR
65 #endif

第56 行,如果没有定义CONFIG_OMAP44XX 和CONFIG_SPL_BUILD 的话条件成立,此处条件成立。
第58 行读取CP15 中c1 寄存器的值到r0 寄存器中,根据17.1.4 小节可知,这里是读取SCTLR 寄存器的值。
第59 行,CR_V 在arch/arm/include/asm/system.h 中有如下所示定义:

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

因此这一行的目的就是清除SCTLR 寄存器中的bit13,SCTLR 寄存器结构如图32.2.1.1 所示:
在这里插入图片描述

图32.2.1.1 SCTLR 寄存器结构图
从图32.2.1.1 可以看出,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 行就是设置向量表重定位的。
代码继续往下执行:

示例代码32.2.1.6 start.S 代码段
67 /* the mask ROM code should have PLL and others stable */
68 #ifndef CONFIG_SKIP_LOWLEVEL_INIT
69 bl cpu_init_cp15
70 bl cpu_init_crit
71 #endif
72
73 bl _main

第68 行如果没有定义CONFIG_SKIP_LOWLEVEL_INIT 的话条件成立。我们没有定义
CONFIG_SKIP_LOWLEVEL_INIT,因此条件成立,执行下面的语句。
示例代码32.2.1.6 中的内容比较简单,就是分别调用函数cpu_init_cp15、cpu_init_crit 和
_main。
函数cpu_init_cp15 用来设置CP15 相关的内容,比如关闭MMU 啥的,此函数同样在start.S文件中定义的,代码如下:

示例代码32.2.1.7 start.S 代码段
105 /*****************************************************************
106 *
107 * cpu_init_cp15
108 *
109 * Setup CP15 registers (cache, MMU, TLBs). The I-cache is turned on
110 * unless CONFIG_SYS_ICACHE_OFF is defined.
111 *
112 *****************************************************************/
113 ENTRY(cpu_init_cp15)
114 /*
115 * Invalidate L1 I/D
116 */
117 mov r0, #0 @ set up for MCR
118 mcr p15, 0, r0, c8, c7, 0 @ invalidate TLBs
119 mcr p15, 0, r0, c7, c5, 0 @ invalidate icache
120 mcr p15, 0, r0, c7, c5, 6 @ invalidate BP array
121 mcr p15, 0, r0, c7, c10,4 @ DSB
122 mcr p15, 0, r0, c7, c5, 4 @ ISB
123
124 /*
125 * disable MMU stuff and caches
126 */
127 mrc p15, 0, r0, c1, c0, 0
128 bic r0, r0, #0x00002000 @ clear bits 13 (--V-)
129 bic r0, r0, #0x00000007 @ clear bits 2:0 (-CAM)
130 orr r0, r0, #0x00000002 @ set bit 1 (--A-) Align
131 orr r0, r0, #0x00000800 @ set bit 11 (Z---) BTB
132 #ifdef CONFIG_SYS_ICACHE_OFF
133 bic r0, r0, #0x00001000 @ clear bit 12 (I) I-cache
134 #else
135 orr r0, r0, #0x00001000 @ set bit 12 (I) I-cache
136 #endif
137 mcr p15, 0, r0, c1, c0, 0
138
......
255
256 mov pc, r5 @ back to my caller
257 ENDPROC(cpu_init_cp15)

函数cpu_init_cp15 都是一些和CP15 有关的内容,我们不用关心,有兴趣的可以详细的看
一下。
函数cpu_init_crit 也在是定义在start.S 文件中,函数内容如下:

示例代码32.2.1.8 start.S 代码段
260 /*****************************************************************
261 *
262 * CPU_init_critical registers
263 *
264 * setup important registers
265 * setup memory timing
266 *
267 *****************************************************************/
268 ENTRY(cpu_init_crit)
269 /*
270 * Jump to board specific initialization...
271 * The Mask ROM will have already initialized
272 * basic memory. Go here to bump up clock rate and handle
273 * wake up conditions.
274 */
275 b lowlevel_init @ go setup pll,mux,memory
276 ENDPROC(cpu_init_crit)

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

lowlevel_init 函数详解

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

示例代码32.2.2.1 lowlevel_init.S 代码段
14 #include <asm-offsets.h>
15 #include <config.h>
16 #include <linux/linkage.h>
17
18 ENTRY(lowlevel_init)
19 /*
20 * Setup a temporary stack. Global data is not available yet.
21 */
22 ldr sp, =CONFIG_SYS_INIT_SP_ADDR
23 bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
24 #ifdef CONFIG_SPL_DM
25 mov r9, #0
26 #else
27 /*
28 * Set up global data for boards that still need it. This will be
29 * removed soon.
30 */
31 #ifdef CONFIG_SPL_BUILD
32 ldr r9, =gdata
33 #else
34 sub sp, sp, #GD_SIZE
35 bic sp, sp, #7
36 mov r9, sp
37 #endif
38 #endif
39 /*
40 * Save the old lr(passed in ip) and the current lr to stack
41 */
42 push {ip, lr}
43
44 /*
45 * Call the very early init function. This should do only the
46 * absolute bare minimum to get started. It should not:
47 *
48 * - set up DRAM
49 * - use global_data
50 * - clear BSS
51 * - try to start a console
52 *
53 * For boards with SPL this should be empty since SPL can do all
54 * of this init in the SPL board_init_f() function which is
55 * called immediately after this.
56 */
57 bl s_init
58 pop {ip, pc}
59 ENDPROC(lowlevel_init)

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

示例代码32.2.2.2 mx6ullevk.h 代码段
234 #define CONFIG_SYS_INIT_RAM_ADDR IRAM_BASE_ADDR
235 #define CONFIG_SYS_INIT_RAM_SIZE IRAM_SIZE
236
237 #define CONFIG_SYS_INIT_SP_OFFSET \
238 (CONFIG_SYS_INIT_RAM_SIZE - GENERATED_GBL_DATA_SIZE)
239 #define CONFIG_SYS_INIT_SP_ADDR \
240 (CONFIG_SYS_INIT_RAM_ADDR + CONFIG_SYS_INIT_SP_OFFSET)

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

示例代码32.2.2.3 imx-regs.h 代码段
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_MX6SX、
CONFIG_MX6U、CONFIG_MX6SLL 和CONFIG_MX6SL 中的任意一个的话条件就不成立,
在.config 中定义了CONFIG_MX6UL,所以条件不成立,因此IRAM_SIZE=0X20000=128KB。
结合示例代码32.2.2.2,可以得到如下值:

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中有定义,如下:

示例代码32.2.2.4 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 = 0x00020000256 = 0x1FF00。
CONFIG_SYS_INIT_SP_ADDR = 0x00900000 + 0X1FF00 = 0X0091FF00

结果如下图所示:
在这里插入图片描述
图32.2.2.1 sp 值
此时sp 指向0X91FF00,这属于IMX6UL/IMX6ULL 的内部ram。
继续回到文件lowlevel_init.S,第23 行对sp 指针做8 字节对齐处理!
第34 行,sp 指针减去GD_SIZE,GD_SIZE 同样在generic-asm-offsets.h 中定了,大小为248,见示例代码32.2.2.4 第11 行。
第35 行对sp 做8 字节对齐,此时sp 的地址为0X0091FF00-248=0X0091FE08,此时sp 位置如图32.2.2.2 所示:
在这里插入图片描述
图32.2.2.2 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 中,如下所示:

示例代码32.2.3.1 soc.c 代码段
808 void s_init(void)
809 {
810 struct anatop_regs *anatop = (struct anatop_regs *)ANATOP_BASE_ADDR;
811 struct mxc_ccm_reg *ccm = (struct mxc_ccm_reg *)CCM_BASE_ADDR;
812 u32 mask480;
813 u32 mask528;
814 u32 reg, periph1, periph2;
815
816 if (is_cpu_type(MXC_CPU_MX6SX) || is_cpu_type(MXC_CPU_MX6UL) ||
817 is_cpu_type(MXC_CPU_MX6ULL) || is_cpu_type(MXC_CPU_MX6SLL))
818 return;
819
820 /* Due to hardware limitation, on MX6Q we need to gate/ungate
821 * all PFDs to make sure PFD is working right, otherwise, PFDs
822 * may not output clock after reset, MX6DL and MX6SL have added
823 * 396M pfd workaround in ROM code, as bus clock need it
824 */
825
826 mask480 = ANATOP_PFD_CLKGATE_MASK(0) |
827 ANATOP_PFD_CLKGATE_MASK(1) |
828 ANATOP_PFD_CLKGATE_MASK(2) |
829 ANATOP_PFD_CLKGATE_MASK(3);
830 mask528 = ANATOP_PFD_CLKGATE_MASK(1) |
831 ANATOP_PFD_CLKGATE_MASK(3);
832
833 reg = readl(&ccm->cbcmr);
834 periph2 = ((reg & MXC_CCM_CBCMR_PRE_PERIPH2_CLK_SEL_MASK)
835 >> MXC_CCM_CBCMR_PRE_PERIPH2_CLK_SEL_OFFSET);
836 periph1 = ((reg & MXC_CCM_CBCMR_PRE_PERIPH_CLK_SEL_MASK)
837 >> MXC_CCM_CBCMR_PRE_PERIPH_CLK_SEL_OFFSET);
838
839 /* Checking if PLL2 PFD0 or PLL2 PFD2 is using for periph clock */
840 if ((periph2 != 0x2) && (periph1 != 0x2))
841 mask528 |= ANATOP_PFD_CLKGATE_MASK(0);
842
843 if ((periph2 != 0x1) && (periph1 != 0x1) &&
844 (periph2 != 0x3) && (periph1 != 0x3))
845 mask528 |= ANATOP_PFD_CLKGATE_MASK(2);
846
847 writel(mask480, &anatop->pfd_480_set);
848 writel(mask528, &anatop->pfd_528_set);
849 writel(mask480, &anatop->pfd_480_clr);
850 writel(mask528, &anatop->pfd_528_clr);
851 }

在第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,函数调用路径如图32.2.3.1 所示:
在这里插入图片描述
图32.2.3.1 uboot 函数调用路径
从图32.2.3.1 可知,接下来要执行的是save_boot_params_ret 中的_main 函数,接下来分析
_main 函数。

_main 函数详解

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

示例代码32.2.4.1 crt0.S 代码段
63 /*
64 * entry point of crt0 sequence
65 */
66
67 ENTRY(_main)
68
69 /*
70 * Set up initial C runtime environment and call board_init_f(0).
71 */
72
73 #if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
74 ldr sp, =(CONFIG_SPL_STACK)
75 #else
76 ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)
77 #endif
78 #if defined(CONFIG_CPU_V7M) /* v7M forbids using SP as BIC destination */
79 mov r3, sp
80 bic r3, r3, #7
81 mov sp, r3
82 #else
83 bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
84 #endif
85 mov r0, sp
86 bl board_init_f_alloc_reserve
87 mov sp, r0
88 /* set up gd here, outside any C code */
89 mov r9, r0
90 bl board_init_f_init_reserve
91
92 mov r0, #0
93 bl board_init_f
94
95 #if ! defined(CONFIG_SPL_BUILD)
96
97 /*
98 * Set up intermediate environment (new sp and gd) and call
99 * relocate_code(addr_moni). Trick here is that we'll return
100 * 'here' but relocated.
101 */
102
103 ldr sp, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */
104 #if defined(CONFIG_CPU_V7M) /* v7M forbids using SP as BIC destination */
105 mov r3, sp
106 bic r3, r3, #7
107 mov sp, r3
108 #else
109 bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
110 #endif
111 ldr r9, [r9, #GD_BD] /* r9 = gd->bd */
112 sub r9, r9, #GD_SIZE /* new GD is below bd */
113
114 adr lr, here
115 ldr r0, [r9, #GD_RELOC_OFF] /* r0 = gd->reloc_off */
116 add lr, lr, r0
117 #if defined(CONFIG_CPU_V7M)
118 orr lr, #1 /* As required by Thumb-only */
119 #endif
120 ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
121 b relocate_code
122 here:
123 /*
124 * now relocate vectors
125 */
126
127 bl relocate_vectors
128
129 /* Set up final (full) environment */
130
131 bl c_runtime_cpu_setup /* we still call old routine here */
132 #endif
133 #if !defined(CONFIG_SPL_BUILD) || defined(CONFIG_SPL_FRAMEWORK)
134 # ifdef CONFIG_SPL_BUILD
135 /* Use a DRAM stack for the rest of SPL, if requested */
136 bl spl_relocate_stack_gd
137 cmp r0, #0
138 movne sp, r0
139 movne r9, r0
140 # endif
141 ldr r0, =__bss_start /* this is auto-relocated! */
142
143 #ifdef CONFIG_USE_ARCH_MEMSET
144 ldr r3, =__bss_end /* this is auto-relocated! */
145 mov r1, #0x00000000 /* prepare zero to clear BSS */
146
147 subs r2, r3, r0 /* r2 = memset len */
148 bl memset
149 #else
150 ldr r1, =__bss_end /* this is auto-relocated! */
151 mov r2, #0x00000000 /* prepare zero to clear BSS */
152
153 clbss_l:cmp r0, r1 /* while not at end of BSS */
154 #if defined(CONFIG_CPU_V7M)
155 itt lo
156 #endif
157 strlo r2, [r0] /* clear 32-bit BSS word */
158 addlo r0, r0, #4 /* move to next */
159 blo clbss_l
160 #endif
161
162 #if ! defined(CONFIG_SPL_BUILD)
163 bl coloured_LED_init
164 bl red_led_on
165 #endif
166 /* call board_init_r(gd_t *id, ulong dest_addr) */
167 mov r0, r9 /* gd_t */
168 ldr r1, [r9, #GD_RELOCADDR] /* dest_addr */
169 /* call board_init_r */
170 #if defined(CONFIG_SYS_THUMB_BUILD)
171 ldr lr, =board_init_r /* this is auto-relocated! */
172 bx lr
173 #else
174 ldr pc, =board_init_r /* this is auto-relocated! */
175 #endif
176 /* we should not return here. */
177 #endif
178
179 ENDPROC(_main)

第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 中,内容如下:

示例代码32.2.4.2 board_init.c 代码段
56 ulong board_init_f_alloc_reserve(ulong top)
57 {
58 /* Reserve early malloc arena */
59 #if defined(CONFIG_SYS_MALLOC_F)
60 top -= CONFIG_SYS_MALLOC_F_LEN;
61 #endif
62 /* LAST : reserve GD (rounded up to a multiple of 16 bytes) */
63 top = rounddown(top-sizeof(struct global_data), 16);
64
65 return top;
66 }

函数board_init_f_alloc_reserve 主要是留出早期的malloc 内存区域和gd 内存区域,其中
CONFIG_SYS_MALLOC_F_LEN=0X400( 在文件include/generated/autoconf.h 中定义) ,
sizeof(struct global_data)=248(GD_SIZE 值),完成以后的内存分布如图32.2.4.1 所示:
在这里插入图片描述
图32.2.4.1 内存分布图
函数board_init_f_alloc_reserve 是有返回值的,返回值为新的top 值,从图32.2.4.1 可知,此时top=0X0091FA00。
继续回到示例代码32.2.4.1 中,第87 行,将r0 写入到sp 里面,r0 保存着函数board_init_f_alloc_reserve 的返回值,所以这一句也就是设置sp=0X0091FA00。
第89 行,将r0 寄存器的值写到寄存器r9 里面,因为r9 寄存器存放着全局变量gd 的地址,在文件arch/arm/include/asm/global_data.h 中有如图32.2.4.2 所示宏定义:
在这里插入图片描述

图32.2.4.2 DECLARE_GLOBAL_DATA_PTR 宏定义
从图32.2.4.2 可以看出,uboot 中定义了一个指向gd_t 的指针gd,gd 存放在寄存器r9 里面的,因此gd 是个全局变量。gd_t 是个结构体,在include/asm-generic/global_data.h 里面有定义,gd_定义如下:

示例代码32.2.4.3 global_data.h 代码段
27 typedef struct global_data {
28 bd_t *bd;
29 unsigned long flags;
30 unsigned int baudrate;
31 unsigned long cpu_clk; /* CPU clock in Hz! */
32 unsigned long bus_clk;
33 /* We cannot bracket this with CONFIG_PCI due to mpc5xxx */
34 unsigned long pci_clk;
35 unsigned long mem_clk;
36 #if defined(CONFIG_LCD) || defined(CONFIG_VIDEO)
37 unsigned long fb_base; /* Base address of framebuffer mem */
38 #endif
......
121 #ifdef CONFIG_DM_VIDEO
122 ulong video_top; /* Top of video frame buffer area */
123 ulong video_bottom; /* Bottom of video frame buffer area */
124 #endif
125 } gd_t;

因此这一行代码就是设置gd 所指向的位置,也就是gd 指向0X0091FA00。
继续回到示例代码32.2.4.1 中,第90 行调用函数board_init_f_init_reserve,此函数在文件
common/init/board_init.c 中有定义,函数内容如下:

示例代码32.2.4.4 board_init.c 代码段
110 void board_init_f_init_reserve(ulong base)
111 {
112 struct global_data *gd_ptr;
113 #ifndef _USE_MEMCPY
114 int *ptr;
115 #endif
116
117 /*
118 * clear GD entirely and set it up.
119 * Use gd_ptr, as gd may not be properly set yet.
120 */
121
122 gd_ptr = (struct global_data *)base;
123 /* zero the area */
124 #ifdef _USE_MEMCPY
125 memset(gd_ptr, '\0', sizeof(*gd));
126 #else
127 for (ptr = (int *)gd_ptr; ptr < (int *)(gd_ptr + 1); )
128 *ptr++ = 0;
129 #endif
130 /* set GD unless architecture did it already */
131 #if !defined(CONFIG_ARM)
132 arch_setup_gd(gd_ptr);
133 #endif
134 /* next alloc will be higher by one GD plus 16-byte alignment */
135 base += roundup(sizeof(struct global_data), 16);
136
137 /*
138 * record early malloc arena start.
139 * Use gd as it is now properly set for all architectures.
140 */
141
142 #if defined(CONFIG_SYS_MALLOC_F)
143 /* go down one 'early malloc arena' */
144 gd->malloc_base = base;
145 /* next alloc will be higher by one 'early malloc arena' size */
146 base += CONFIG_SYS_MALLOC_F_LEN;
147 #endif
148 }

可以看出,此函数用于初始化gd ,其实就是清零处理。另外,此函数还设置了gd->malloc_base 为gd 基地址+gd 大小=0X0091FA00+248=0X0091FAF8,在做16 字节对齐,最终gd->malloc_base=0X0091FB00,这个也就是early malloc 的起始地址。
继续回到示例代码32.2.4.1 中,第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,参考示例代码32.2.2.4。
第109 行,sp 做8 字节对齐。
第111 行,获取gd->bd 的地址赋给r9,此时r9 存放的是老的gd,这里通过获取gd->bd 的地址来计算出新的gd 的位置。GD_BD=0,参考示例代码32.2.2.4。
第112 行,新的gd 在bd 下面,所以r9 减去gd 的大小就是新的gd 的位置,获取到新的gd的位置以后赋值给r9。
第114 行,设置lr 寄存器为here,这样后面执行其他函数返回的时候就返回到了第122 行的here 位置处。
第115,读取gd->reloc_off 的值复制给r0 寄存器,GD_RELOC_OFF=68,参考示例代码
32.2.2.4。
第116 行,lr 寄存器的值加上r0 寄存器的值,重新赋值给lr 寄存器。因为接下来要重定位代码,也就是把代码拷贝到新的地方去(现在的uboot 存放的起始地址为0X87800000,下面要
将uboot 拷贝到DDR 最后面的地址空间出,将0X87800000 开始的内存空出来),其中就包括
here,因此lr 中的here 要使用重定位后的位置。
第120 行,读取gd->relocaddr 的值赋给r0 寄存器,此时r0 寄存器就保存着uboot 要拷贝的目的地址,为0X9FF47000。GD_RELOCADDR=48,参考示例代码32.2.2.4。
第121 行,调用函数relocate_code,也就是代码重定位函数,此函数负责将uboot 拷贝到新的地方去,此函数定义在文件arch/arm/lib/relocate.S 中稍后会详细分析此函数。
继续回到示例代码32.2.4.1 第127 行,调用函数relocate_vectors,对中断向量表做重定位,此函数定义在文件arch/arm/lib/relocate.S 中,稍后会详细分析此函数。
继续回到示例代码32.2.4.1 第131 行,调用函数c_runtime_cpu_setup,此函数定义在文件
arch/arm/cpu/armv7/start.S 中,函数内容如下:

示例代码32.2.4.5 start.S 代码段
77 ENTRY(c_runtime_cpu_setup)
78 /*
79 * If I-cache is enabled invalidate it
80 */
81 #ifndef CONFIG_SYS_ICACHE_OFF
82 mcr p15, 0, r0, c7, c5, 0 @ invalidate icache
83 mcr p15, 0, r0, c7, c10, 4 @ DSB
84 mcr p15, 0, r0, c7, c5, 4 @ ISB
85 #endif
86
87 bx lr
88
89 ENDPROC(c_runtime_cpu_setup)

第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_code、
relocate_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 中定义,代码如下:

示例代码32.2.5.1 board_f.c 代码段
1035 void board_init_f(ulong boot_flags)
1036 {
1037 #ifdef CONFIG_SYS_GENERIC_GLOBAL_DATA
1038 /*
1039 * For some archtectures, global data is initialized and used
1040 * before calling this function. The data should be preserved.
1041 * For others, CONFIG_SYS_GENERIC_GLOBAL_DATA should be defined
1042 * and use the stack here to host global data until relocation.
1043 */
1044 gd_t data;
1045
1046 gd = &data;
1047
1048 /*
1049 * Clear global data before it is accessed at debug print
1050 * in initcall_run_list. Otherwise the debug print probably
1051 * get the wrong vaule of gd->have_console.
1052 */
1053 zero_global_data();
1054 #endif
1055
1056 gd->flags = boot_flags;
1057 gd->have_console = 0;
1058
1059 if (initcall_run_list(init_sequence_f))
1060 hang();
1061
1062 #if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \
1063 !defined(CONFIG_EFI_APP)
1064 /* NOTREACHED - jump_to_copy() does not return */
1065 hang();
1066 #endif
1067 }

因为没有定义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 的内容比较长,里面有大量的条件编译代码,这里为了缩小篇幅,将条件编译部分删除掉了,去掉条件编译以后的init_sequence_f 定义如下:

示例代码32.2.5.1 board_f.c 代码段
/*****************去掉条件编译语句后的init_sequence_f***************/
1 static init_fnc_t init_sequence_f[] = {
2 setup_mon_len,
3 initf_malloc,
4 initf_console_record,
5 arch_cpu_init, /* basic arch cpu dependent setup */
6 initf_dm,
7 arch_cpu_init_dm,
8 mark_bootstage, /* need timer, go after init dm */
9 board_early_init_f,
10 timer_init, /* initialize timer */
11 board_postclk_init,
12 get_clocks,
13 env_init, /* initialize environment */
14 init_baud_rate, /* initialze baudrate settings */
15 serial_init, /* serial communications setup */
16 console_init_f, /* stage 1 init of console */
17 display_options, /* say that we are here */
18 display_text_info, /* show debugging info if required */
19 print_cpuinfo, /* display cpu info (and speed) */
20 show_board_info,
21 INIT_FUNC_WATCHDOG_INIT
22 INIT_FUNC_WATCHDOG_RESET
23 init_func_i2c,
24 announce_dram_init,
25 /* TODO: unify all these dram functions? */
26 dram_init, /* configure available RAM banks */
27 post_init_f,
28 INIT_FUNC_WATCHDOG_RESET
29 testdram,
30 INIT_FUNC_WATCHDOG_RESET
31 INIT_FUNC_WATCHDOG_RESET
32 /*
33 * Now that we have DRAM mapped and working, we can
34 * relocate the code and continue running from DRAM.
35 *
36 * Reserve memory at end of RAM for (top down in that order):
37 * - area that won't get touched by U-Boot and Linux (optional)
38 * - kernel log buffer
39 * - protected RAM
40 * - LCD framebuffer
41 * - monitor code
42 * - board info struct
43 */
44 setup_dest_addr,
45 reserve_round_4k,
46 reserve_mmu,
47 reserve_trace,
48 reserve_uboot,
49 reserve_malloc,
50 reserve_board,
51 setup_machine,
52 reserve_global_data,
53 reserve_fdt,
54 reserve_arch,
55 reserve_stacks,
56 setup_dram_config,
57 show_dram_config,
58 display_new_sp,
59 INIT_FUNC_WATCHDOG_RESET
60 reloc_fdt,
61 setup_reloc,
62 NULL,
63 };

接下来分析以上函数执行完以后的结果:
第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 内核有一个定时器,这里初始化的就是Cortex-A 内核的那个定时器。通过这个定时器来为uboot 提供时间。就跟Cortex-M 内核Systick 定时器一样。关于Cortex-A 内部定时器的详细内容,请参考文档《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf》的“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,通过串口输出一些信息,如图32.2.5.1 所示:
在这里插入图片描述

图32.2.5.1 串口信息输出
第18 行,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);

结果如图32.2.5.2 所示:
在这里插入图片描述

图32.2.5.2 文本信息
第19 行,print_cpuinfo 函数用于打印CPU 信息,结果如图32.2.5.3 所示:
在这里插入图片描述

图32.2.5.3 CPU 信息
第20 行,show_board_info 函数用于打印板子信息,会调用checkboard 函数,结果如图
32.2.5.4 所示:
在这里插入图片描述

图32.2.5.4 板子信息
第21 行,INIT_FUNC_WATCHDOG_INIT,初始化看门狗,对于I.MX6ULL 来说是空函数
第22 行,INIT_FUNC_WATCHDOG_RESET,复位看门狗,对于I.MX6ULL 来说是空函数
第23 行,init_func_i2c 函数用于初始化I2C,初始化完成以后会输出如图32.2.5.5 所示信息:
在这里插入图片描述

图32.2.5.5 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函数输入如图32.2.5.6 所示内容:
在这里插入图片描述
图32.2.5.6 添加print 函数打印成员变量值
设置好以后重新编译uboot,然后烧写到SD 卡中,选择SD 卡启动,重启开发板,打开
SecureCRT,uboot 会输出如图32.2.5.7 所示信息:
在这里插入图片描述

图32.2.5.7 信息输出
从图32.2.5.7 可以看出:

gd->ram_size = 0X20000000 //ram 大小为0X20000000=512MB
gd->ram_top = 0XA0000000 //ram 最高地址为0X80000000+0X20000000=0XA0000000
gd->relocaddr = 0XA0000000 //重定位后最高地址为0XA0000000

第45 行,reserve_round_4k 函数用于对gd->relocaddr 做4KB 对齐,因为gd->relocaddr=0XA0000000,已经是4K 对齐了,所以调整后不变。
第46 行,reserve_mmu,留出MMU 的TLB 表的位置,分配MMU 的TLB 表内存以后会对gd->relocaddr 做64K 字节对齐。完成以后gd->arch.tlb_size、gd->arch.tlb_addr 和gd->relocaddr
如图32.2.5.8 所示:
在这里插入图片描述

图32.2.5.8 信息输出
从图32.2.5.8 可以看出:

gd->arch.tlb_size= 0X4000 //MMU 的TLB 表大小
gd->arch.tlb_addr=0X9FFF0000 //MMU 的TLB 表起始地址,64KB 对齐以后
gd->relocaddr=0X9FFF0000 //relocaddr 地址

第47 行,reserve_trace 函数,留出跟踪调试的内存,I.MX6ULL 没有用到!
第48 行,reserve_uboot,留出重定位后的uboot 所占用的内存区域,uboot 所占用大小由
gd->mon_len 所指定,留出uboot 的空间以后还要对gd->relocaddr 做4K 字节对齐,并且重新设置gd->start_addr_sp,结果如图32.2.5.9 所示:
在这里插入图片描述
图32.2.5.9 信息输出
从图32.2.5.9 可以看出:

gd->mon_len = 0XA8EF4
gd->start_addr_sp = 0X9FF47000
gd->relocaddr = 0X9FF47000

第49 行,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 如图32.2.5.10 所示:
在这里插入图片描述

图32.2.5.10 信息输出
从图32.2.5.10 可以看出:

TOTAL_MALLOC_LEN=0X1002000
gd->start_addr_sp=0X9EF45000 //0X9FF47000-16MB-8KB=0X9EF45000

第50 行,reserve_board 函数,留出板子bd 所占的内存区,bd 是结构体bd_t,bd_t 大小为
80 字节,结果如图32.2.5.11 所示:
在这里插入图片描述

图32.2.5.10 信息输出
从图32.2.5.11 可以看出:

gd->start_addr_sp=0X9EF44FB0
gd->bd=0X9EF44FB0

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

图32.2.5.11 信息输出

gd->start_addr_sp=0X9EF44EB8 //0X9EF44FB0-248=0X9EF44EB8
gd->new_gd=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 完成。结果如图32.2.5.12 所示:
在这里插入图片描述

图32.2.5.12 信息输出
在本uboot 中并没有使用到IRQ,所以不会留出IRQ 相应的内存区域,此时:

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 的起始地址和大小。结果如图32.2.5.13 所示:
在这里插入图片描述

图32.2.5.13 信息输出
从图32.2.5.13 可以看出,DRAM 的起始地址为0X80000000,大小为0X20000000(512MB)。
第57 行,show_dram_config 函数,用于显示DRAM 的配置,如图32.2.5.14 所示:
在这里插入图片描述

图32.2.5.14 信息输出
第58 行,display_new_sp 函数,显示新的sp 位置,也就是gd->start_addr_sp,不过要定义
宏DEBUG,结果如图32.2.5.15 所示:
在这里插入图片描述

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

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

在这里插入图片描述
图32.2.5.16 最终的内存分配图

relocate_code 函数详解

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

示例代码32.2.6.1 relocate.S 代码段
/*
* void relocate_code(addr_moni)
*
* This function relocates the monitor code.
*
* NOTE:
* To prevent the code below from containing references with an
* R_ARM_ABS32 relocation record type, we never refer to linker-
* defined symbols directly. Instead, we declare literals which
* contain their relative location with respect to relocate_code,
* and at run time, add relocate_code back to them.
*/
79 ENTRY(relocate_code)
80 ldr r1, =__image_copy_start /* r1 <- SRC &__image_copy_start */
81 subs r4, r0, r1 /* r4 <- relocation offset */
82 beq relocate_done /* skip relocation */
83 ldr r2, =__image_copy_end /* r2 <- SRC &__image_copy_end */
84
85 copy_loop:
86 ldmia r1!, {r10-r11} /* copy from source address [r1] */
87 stmia r0!, {r10-r11} /* copy to target address [r0] */
88 cmp r1, r2 /* until source end address [r2] */
89 blo copy_loop
90
91 /*
92 * fix .rel.dyn relocations
93 */
94 ldr r2, =__rel_dyn_start /* r2 <- SRC &__rel_dyn_start */
95 ldr r3, =__rel_dyn_end /* r3 <- SRC &__rel_dyn_end */
96 fixloop:
97 ldmia r2!, {r0-r1} /* (r0,r1) <- (SRC location,fixup) */
98 and r1, r1, #0xff
99 cmp r1, #23 /* relative fixup? */
100 bne fixnext
101
102 /* relative fix: increase location by offset */
103 add r0, r0, r4
104 ldr r1, [r0]
105 add r1, r1, r4
106 str r1, [r0]
107 fixnext:
108 cmp r2, r3
109 blo fixloop
110
111 relocate_done:
112
113 #ifdef __XSCALE__
114 /*
115 * On xscale, icache must be invalidated and write buffers
116 * drained, even with cache disabled - 4.2.7 of xscale core
117 developer's manual */
118 mcr p15, 0, r0, c7, c7, 0 /* invalidate icache */
119 mcr p15, 0, r0, c7, c10, 4 /* drain write buffer */
120 #endif
121
122 /* ARMv4- don't know bx lr but the assembler fails to see that */
123
124 #ifdef __ARM_ARCH_4__
125 mov pc, lr
126 #else
127 bx lr
128 #endif
129
130 ENDPROC(relocate_code)

第80 行,r1=__image_copy_start,也就是r1 寄存器保存源地址,由表31.4.1.1 可知,
__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 中保存拷贝之前的代码结束地址,由表31.4.1.1 可知,
__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 文件,其链接地址和运行地址要相等,也就是链接到哪个地址,
在运行之前就要拷贝到哪个地址去。现在我们重定位以后,运行地址就和链接地址不同了,这样寻址的时候不会出问题吗?为了分析这个问题,我们需要在mx6ull_alientek_emmc.c 中输入如下所示内容:

示例代码32.2.6.2 mx6ull_alientek_emmc.c 新添代码段
1 static int rel_a = 0;
2
3 void rel_test(void)
4 {
5 rel_a = 100;
6 printf("rel_test\r\n");
7 }

最后还需要在mx6ullevk.c 文件中的board_init 函数里面调用rel_test 函数,否则rel_reset 不
会被编译进uboot。修改完成后的mx6ullevk.c 如图32.2.6.1 所示:
在这里插入图片描述

图32.2.6.1 加入rel 测试相关代码
board_init 函数会调用rel_test,rel_test 会调用全局变量rel_a,使用如下命令编译uboot:

./mx6ull_alientek_emmc.sh

编译完成以后,使用arm-linux-gnueabihf-objdump 将u-boot 进行反汇编,得到u-boot.dis 这
个汇编文件,命令如下:

arm-linux-gnueabihf-objdump -D -m arm u-boot > u-boot.dis

在u-boot.dis 文件中找到rel_a、rel_rest 和board_init,相关内容如下所示:

示例代码32.2.6.3 汇编文件代码段
1 87804184 <rel_test>:
2 87804184: e59f300c ldr r3, [pc, #12] ; 87804198 <rel_test+0x14>
3 87804188: e3a02064 mov r2, #100 ; 0x64
4 8780418c: e59f0008 ldr r0, [pc, #8] ; 8780419c <rel_test+0x18>
5 87804190: e5832000 str r2, [r3]
6 87804194: ea00d668 b 87839b3c <printf>
7 87804198: 8785da50 ; <UNDEFINED> instruction: 0x8785da50
8 8780419c: 878426a2 strhi r2, [r4, r2, lsr #13]
9
10 878041a0 <board_init>:
11 878041a0: e92d4010 push {r4, lr}
12 878041a4: ebfffff6 bl 87804184 <rel_test>
13
14 ......
15
16 8785da50 <rel_a>:

17 8785da50: 00000000 andeq r0, r0, r0
第12 行是borad_init 调用rel_test 函数,用到了bl 指令,而bl 指令是位置无关指令,bl 指令是相对寻址的(pc+offset),因此uboot 中函数调用是与绝对位置无关的。
再来看一下函数rel_test 对于全局变量rel_a 的调用,第2 行设置r3 的值为pc+12 地址处的值,因为ARM流水线的原因,pc寄存器的值为当前地址+8,因此pc=0X87804184+8=0X8780418C,
r3=0X8780418C+12=0X87804198,第7 行就是0X87804198 这个地址,0X87804198 处的值为0X8785DA50。根据第17 行可知,0X8785DA50 正是变量rel_a 的地址,最终r3=0X8785DA50。
第3 行,r2=100。
第5 行,将r2 内的值写到r3 地址处,也就是设置地址0X8785DA50 的值为100,这不就是示例代码代码32.2.6.2 中的第5 行:rel_a = 100。
总结一下rel_a=100 的汇编执行过程:
①、在函数rel_test 末尾处有一个地址为0X87804198 的内存空间(示例代码32.2.6.3 第7行),此内存空间保存着变量rel_a 的地址。
②、函数rel_test 要想访问变量rel_a,首先访问末尾的0X87804198 来获取变量rel_a 的地
址,而访问0X87804198 是通过偏移来访问的,很明显是个位置无关的操作。
③、通过0X87804198 获取到变量rel_a 的地址,对变量rel_a 进行操作。
④、可以看出,函数rel_test 对变量rel_a 的访问没有直接进行,而是使用了一个第三方偏
移地址0X87804198,专业术语叫做Label。这个第三方偏移地址就是实现重定位后运行不会出错的重要原因!
uboot 重定位后偏移为0X18747000 ,那么重定位后函数rel_test 的首地址就是
0X87804184+0X18747000=0X9FF4B184 。保存变量rel_a 地址的Label 就是
0X9FF4B184+8+12=0X9FF4B198( 既:0X87804198+0X18747000) ,变量rel_a 的地址就为0X8785DA50+0X18747000=0X9FFA4A50。重定位后函数rel_test 要想正常访问变量rel_a 就得设置0X9FF4B198(重定位后的Label)地址出的值为0X9FFA4A50(重定位后的变量rel_a 地址)。
这样就解决了重定位后链接地址和运行地址不一致的问题。
可以看出,uboot 对于重定位后链接地址和运行地址不一致的解决方法就是采用位置无关码,在使用ld 进行链接的时候使用选项“-pie”生成位置无关的可执行文件。在文件
arch/arm/config.mk 下有如下代码:

示例代码32.2.6.4 config.mk 文件代码段
82 # needed for relocation
83 LDFLAGS_u-boot += -pie

第83 行就是设置uboot 链接选项,加入了“-pie”选项,编译链接uboot 的时候就会使用到
“-pie”,如图32.2.6.2 所示:
在这里插入图片描述

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

示例代码32.2.6.5 .rel.dyn 段代码段
1 Disassembly of section .rel.dyn:
2
3 8785da44 <__rel_dyn_end-0x8ba0>:
4 8785da44: 87800020 strhi r0, [r0, r0, lsr #32]
5 8785da48: 00000017 andeq r0, r0, r7, lsl r0
6 ......
7 8785dfb4: 87804198 ; <UNDEFINED> instruction: 0x87804198
8 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,
这个正是示例代码32.2.6.3 中存放变量rel_a 地址的那个Label。根据前面的分析,只要将地址0X87804198+offset 处的值改为重定位后的变量rel_a 地址即可。我们猜测的是否正确,看一下uboot 对.rel.dyn 段的重定位即可(示例代码代码32.2.6.1 中的第94~109 行),.rel.dyn 段的重定位代码如下:

示例代码32.2.6.6 relocate.S 代码段
91 /*
92 * fix .rel.dyn relocations
93 */
94 ldr r2, =__rel_dyn_start /* r2 <- SRC &__rel_dyn_start */
95 ldr r3, =__rel_dyn_end /* r3 <- SRC &__rel_dyn_end */
96 fixloop:
97 ldmia r2!, {r0-r1} /* (r0,r1) <- (SRC location,fixup) */
98 and r1, r1, #0xff
99 cmp r1, #23 /* relative fixup? */
100 bne fixnext
101
102 /* relative fix: increase location by offset */
103 add r0, r0, r4
104 ldr r1, [r0]
105 add r1, r1, r4
106 str r1, [r0]
107 fixnext:
108 cmp r2, r3
109 blo fixloop

第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 所保存的变量地址,此时这个变量地址还是重定位前的(相当于rel_a 重定位前的地址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 中,函数源码如下:

示例代码32.2.7.1 relocate.S 代码段
27 ENTRY(relocate_vectors)
28
29 #ifdef CONFIG_CPU_V7M
30 /*
31 * On ARMv7-M we only have to write the new vector address
32 * to VTOR register.
33 */
34 ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
35 ldr r1, =V7M_SCB_BASE
36 str r0, [r1, V7M_SCB_VTOR]
37 #else
38 #ifdef CONFIG_HAS_VBAR
39 /*
40 * If the ARM processor has the security extensions,
41 * use VBAR to relocate the exception vectors.
42 */
43 ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
44 mcr p15, 0, r0, c12, c0, 0 /* Set VBAR */
45 #else
46 /*
47 * Copy the relocated exception vectors to the
48 * correct address
49 * CP15 c1 V bit gives us the location of the vectors:
50 * 0x00000000 or 0xFFFF0000.
51 */
52 ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
53 mrc p15, 0, r2, c1, c0, 0 /* V bit (bit[13]) in CP15 c1 */
54 ands r2, r2, #(1 << 13)
55 ldreq r1, =0x00000000 /* If V=0 */
56 ldrne r1, =0xFFFF0000 /* If V=1 */
57 ldmia r0!, {r2-r8,r10}
58 stmia r1!, {r2-r8,r10}
59 ldmia r0!, {r2-r8,r10}
60 stmia r1!, {r2-r8,r10}
61 #endif
62 #endif
63 bx lr
64
65 ENDPROC(relocate_vectors)

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

board_init_r 函数详解

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

示例代码32.2.8.1 board_r.c 代码段
991 void board_init_r(gd_t *new_gd, ulong dest_addr)
992 {
993 #ifdef CONFIG_NEEDS_MANUAL_RELOC
994 int i;
995 #endif
996
997 #ifdef CONFIG_AVR32
998 mmu_init_r(dest_addr);
999 #endif
1000
1001 #if !defined(CONFIG_X86) && !defined(CONFIG_ARM) && !defined(CONFIG_ARM64)
1002 gd = new_gd;
1003 #endif
1004
1005 #ifdef CONFIG_NEEDS_MANUAL_RELOC
1006 for (i = 0; i < ARRAY_SIZE(init_sequence_r); i++)
1007 init_sequence_r[i] += gd->reloc_off;
1008 #endif
1009
1010 if (initcall_run_list(init_sequence_r))
1011 hang();
1012
1013 /* NOTREACHED - run_main_loop() does not return */
1014 hang();
1015 }

第1010 行调用initcall_run_list 函数来执行初始化序列init_sequence_r,init_sequence_r 是
一个函数集合,init_sequence_r 也定义在文件common/board_r.c 中,由于init_sequence_f 的内容比较长,里面有大量的条件编译代码,这里为了缩小篇幅,将条件编译部分删除了,去掉条件编译以后的init_sequence_r 定义如下:

示例代码32.2.8.2 board_r.c 代码段
1 init_fnc_t init_sequence_r[] = {
2 initr_trace,
3 initr_reloc,
4 initr_caches,
5 initr_reloc_global_data,
6 initr_barrier,
7 initr_malloc,
8 initr_console_record,
9 bootstage_relocate,
10 initr_bootstage,
11 board_init, /* Setup chipselects */
12 stdio_init_tables,
13 initr_serial,
14 initr_announce,
15 INIT_FUNC_WATCHDOG_RESET
16 INIT_FUNC_WATCHDOG_RESET
17 INIT_FUNC_WATCHDOG_RESET
18 power_init_board,
19 initr_flash,
20 INIT_FUNC_WATCHDOG_RESET
21 initr_nand,
22 initr_mmc,
23 initr_env,
24 INIT_FUNC_WATCHDOG_RESET
25 initr_secondary_cpu,
26 INIT_FUNC_WATCHDOG_RESET
27 stdio_add_devices,
28 initr_jumptable,
29 console_init_r, /* fully init console as a device */
30 INIT_FUNC_WATCHDOG_RESET
31 interrupt_init,
32 initr_enable_interrupts,
33 initr_ethaddr,
34 board_late_init,
35 INIT_FUNC_WATCHDOG_RESET
36 INIT_FUNC_WATCHDOG_RESET
37 INIT_FUNC_WATCHDOG_RESET
38 initr_net,
39 INIT_FUNC_WATCHDOG_RESET
40 run_main_loop,
41 };

第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,串口输出如图32.2.8.1 所示信息:
在这里插入图片描述

图32.2.8.1 EMMC 信息输出
从图32.2.8.1 可以看出,此时有两个EMCM 设备,FSL_SDHC:0 和FSL_SDHC:1。
第23 行,initr_env 函数,初始化环境变量。
第25 行,initr_secondary_cpu 函数,初始化其他CPU 核,I.MX6ULL 只有一个核,因此此
函数没用。
第27 行,stdio_add_devices 函数,各种输入输出设备的初始化,如LCD driver,I.MX6ULL
使用drv_video_init 函数初始化LCD。会输出如图32.2.8.2 所示信息:
在这里插入图片描述

图32.2.8.2 LCD 信息
第28 行,initr_jumptable 函数,初始化跳转表。
第29 行,console_init_r 函数,控制台初始化,初始化完成以后此函数会调用
stdio_print_current_devices 函数来打印出当前的控制台设备,如图32.2.8.3 所示:
在这里插入图片描述

图32.2.8.3 控制台信息
第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 设备,代码如图32.2.8.4 所示:
在这里插入图片描述

图32.2.8.4 board_late_mmc_env_init 函数
图32.2.8.4 中的第46 行和第47 行就是运行“mmc dev xx”命令,用于切换到正在使用的
EMMC 设备,串口输出信息如图32.2.8.5 所示:
在这里插入图片描述

图32.2.8.5 切换mmc 设备
第38 行,initr_net 函数,初始化网络设备,函数调用顺序为:
initr_net->eth_initialize->board_eth_init(),串口输出如图32.2.8.6 所示信息:
在这里插入图片描述

图32.2.8.6 网络信息输出
第40 行,run_main_loop 行,主循环,处理命令。

run_main_loop 函数详解

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

示例代码32.2.9.1 board_r.c 文件代码段
753 static int run_main_loop(void)
754 {
755 #ifdef CONFIG_SANDBOX
756 sandbox_main_loop_init();
757 #endif
758 /* main_loop() can return to retry autoboot, if so just run it again */
759 for (;;)
760 main_loop();
761 return 0;
762 }

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

示例代码32.2.9.2 main.c 文件代码段
43 /* We come here after U-Boot is initialised and ready to process commands */
44 void main_loop(void)
45 {
46 const char *s;
47
48 bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");
49
50 #ifndef CONFIG_SYS_GENERIC_BOARD
51 puts("Warning: Your board does not use generic board. Please read\n");
52 puts("doc/README.generic-board and take action. Boards not\n");
53 puts("upgraded by the late 2014 may break or be removed.\n");
54 #endif
55
56 #ifdef CONFIG_VERSION_VARIABLE
57 setenv("ver", version_string); /* set version variable */
58 #endif /* CONFIG_VERSION_VARIABLE */
59
60 cli_init();
61
62 run_preboot_environment_command();
63
64 #if defined(CONFIG_UPDATE_TFTP)
65 update_tftp(0UL, NULL, NULL);
66 #endif /* CONFIG_UPDATE_TFTP */
67
68 s = bootdelay_process();
69 if (cli_process_fdt(&s))
70 cli_secure_boot_cmd(s);
71
72 autoboot_command(s);
73
74 cli_loop();
75 }

第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,如下:

#define U_BOOT_VERSION_STRING U_BOOT_VERSION " (" U_BOOT_DATE " - " \
U_BOOT_TIME " " U_BOOT_TZ ")" CONFIG_IDENT_STRING

U_BOOT_VERSION 定义在文件include/generated/version_autogenerated.h 中,文件
version_autogenerated.h 内如如下:

示例代码32.2.9.4 version_autogenerated.h 文件代码
1 #define PLAIN_VERSION "2016.03"
2 #define U_BOOT_VERSION "U-Boot " PLAIN_VERSION
3 #define CC_VERSION_STRING "arm-linux-gnueabihf-gcc (Linaro GCC 4.9-2017.01) 4.9.4"
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 中,如下所示:

示例代码32.2.9.5 timestamp_autogenerated.h 文件代码
1 #define U_BOOT_DATE "Apr 25 2019"
2 #define U_BOOT_TIME "21:10:53"
3 #define U_BOOT_TZ "+0800"
4 #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”查看版本号,如图32.2.9.1 所示:
在这里插入图片描述

图32.2.9.1 版本查询
图32.2.9.1 中的第一行就是uboot 版本号,和我们分析的一致。
接着回到示例代码32.2.9.2 中,第60 行,cli_init 函数,跟命令初始化有关,初始化hush shell 相关的变量。
第62 行,run_preboot_environment_command 函数,获取环境变量perboot 的内容,perboot是一些预启动命令,一般不使用这个环境变量。
第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 中,内容如下:

示例代码32.2.9.5 auboboot.c 文件代码段
380 void autoboot_command(const char *s)
381 {
382 debug("### main_loop: bootcmd=\"%s\"\n", s ? s : "<UNDEFINED>");
383
384 if (stored_bootdelay != -1 && s && !abortboot(stored_bootdelay)) {
385 #if defined(CONFIG_AUTOBOOT_KEYED) && !defined(CONFIG_AUTOBOOT_KEYED_CTRLC)
386 int prev = disable_ctrlc(1); /* disable Control C checking */
387 #endif
388
389 run_command_list(s, -1, 0);
390
391 #if defined(CONFIG_AUTOBOOT_KEYED) && !defined(CONFIG_AUTOBOOT_KEYED_CTRLC)
392 disable_ctrlc(prev); /* restore Control C checking */
393 #endif
394 }
395
396 #ifdef CONFIG_MENUKEY
397 if (menukey == CONFIG_MENUKEY) {
398 s = getenv("menucmd");
399 if (s)
400 run_command_list(s, -1, 0);
401 }
402 #endif /* CONFIG_MENUKEY */
403 }

可以看出,autoboot_command 函数里面有很多条件编译,条件编译一多就不利于我们阅读
程序(所以正点原子的例程基本是不用条件编译的,就是为了方便大家阅读源码)!宏
CONFIG_AUTOBOOT_KEYED 、CONFIG_AUTOBOOT_KEYED_CTRLC 和
CONFIG_MENUKEY 这三个宏在I.MX6ULL 里面没有定义,所以讲示例代码32.2.9.5 进行精
简,得到如下代码:

示例代码32.2.9.6 autoboot_command 函数精简版本
1 void autoboot_command(const char *s)
2 {
3 if (stored_bootdelay != -1 && s && !abortboot(stored_bootdelay)) {
4 run_command_list(s, -1, 0);
5 }
6 }

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

示例代码32.2.9.7 abortboot 函数
283 static int abortboot(int bootdelay)
284 {
285 #ifdef CONFIG_AUTOBOOT_KEYED
286 return abortboot_keyed(bootdelay);
287 #else
288 return abortboot_normal(bootdelay);
289 #endif
290 }

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

示例代码32.2.9.8 abortboot_normal 函数
225 static int abortboot_normal(int bootdelay)
226 {
227 int abort = 0;
228 unsigned long ts;
229
230 #ifdef CONFIG_MENUPROMPT
231 printf(CONFIG_MENUPROMPT);
232 #else
233 if (bootdelay >= 0)
234 printf("Hit any key to stop autoboot: %2d ", bootdelay);
235 #endif
236
237 #if defined CONFIG_ZERO_BOOTDELAY_CHECK
238 /*
239 * Check if key already pressed
240 * Don't check if bootdelay < 0
241 */
242 if (bootdelay >= 0) {
243 if (tstc()) { /* we got a key press */
244 (void) getc(); /* consume input */
245 puts("\b\b\b 0");
246 abort = 1; /* don't auto boot */
247 }
248 }
249 #endif
250
251 while ((bootdelay > 0) && (!abort)) {
252 --bootdelay;
253 /* delay 1000 ms */
254 ts = get_timer(0);
255 do {
256 if (tstc()) { /* we got a key press */
257 abort = 1; /* don't auto boot */
258 bootdelay = 0; /* no more delay */
259 # ifdef CONFIG_MENUKEY
260 menukey = getc();
261 # else
262 (void) getc(); /* consume input */
263 # endif
264 break;
265 }
266 udelay(10000);
267 } while (!abort && get_timer(ts) < 1000);
268
269 printf("\b\b\b%2d ", bootdelay);
270 }
271
272 putc('\n');
273
274 #ifdef CONFIG_SILENT_CONSOLE
275 if (abort)
276 gd->flags &= ~GD_FLG_SILENT;
277 #endif
278
279 return abort;
280 }

函数abortboot_normal 同样很多条件编译,删除掉条件编译相关代码后abortboot_normal 函
数内容如下:

示例代码32.2.9.9 abortboot_normal 函数精简
1 static int abortboot_normal(int bootdelay)
2 {
3 int abort = 0;
4 unsigned long ts;
5
6 if (bootdelay >= 0)
7 printf("Hit any key to stop autoboot: %2d ", bootdelay);
8
9 while ((bootdelay > 0) && (!abort)) {
10 --bootdelay;
11 /* delay 1000 ms */
12 ts = get_timer(0);
13 do {
14 if (tstc()) { /* we got a key press */
15 abort = 1; /* don't auto boot */
16 bootdelay = 0; /* no more delay */
17 (void) getc(); /* consume input */
18 break;
19 }
20 udelay(10000);
21 } while (!abort && get_timer(ts) < 1000);
22
23 printf("\b\b\b%2d ", bootdelay);
24 }
25 putc('\n');
26 return abort;
27 }

第3 行的变量abort 是函数abortboot_normal 的返回值,默认值为0。
第7 行通过串口输出“Hit any key to stop autoboot”字样,如图32.2.9.2 所示:
在这里插入图片描述

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

cli_loop 函数详解

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

示例代码32.2.10.1 cli.c 文件代码段
202 void cli_loop(void)
203 {
204 #ifdef CONFIG_SYS_HUSH_PARSER
205 parse_file_outer();
206 /* This point is never reached */
207 for (;;);
208 #else
209 cli_simple_loop();
210 #endif /*CONFIG_SYS_HUSH_PARSER*/
211 }

在文件include/configs/mx6_common.h 中有定义宏CONFIG_SYS_HUSH_PARSER,而正点
原子的I.MX6ULL 开发板配置头文件mx6ullevk.h 里面会引用mx_common.h 这个头文件,因此宏CONFIG_SYS_HUSH_PARSER 有定义。
第205 行调用函数parse_file_outer。
第207 行是个死循环,永远不会执行到这里。
函数parse_file_outer 定义在文件common/cli_hush.c 中,去掉条件编译内容以后的函数内容
如下:

示例代码32.2.10.2 parse_file_outer 函数精简
1 int parse_file_outer(void)
2 {
3 int rcode;
4 struct in_str input;
5
6 setup_file_in_str(&input);
7 rcode = parse_stream_outer(&input, FLAG_PARSE_SEMICOLON);
8 return rcode;
9 }

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

示例代码32.2.10.3 parse_stream_outer 函数精简
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 函数来处理命令,过程
如下:

示例代码32.2.10.4 run_list 执行流程
1 static int run_list(struct pipe *pi)
2 {
3 int rcode=0;
4
5 rcode = run_list_real(pi);
6 ......
7 return rcode;
8 }
9
10 static int run_list_real(struct pipe *pi)
11 {
12 char *save_name = NULL;
13 ......
14 int if_code=0, next_if_code=0;
15 ......
16 rcode = run_pipe_real(pi);
17 ......
18 return rcode;
19 }
20
21 static int run_pipe_real(struct pipe *pi)
22 {
23 int i;
24
25 int nextin;
26 int flag = do_repeat ? CMD_FLAG_REPEAT : 0;
27 struct child_prog *child;
28 char *p;
29 ......
30 if (pi->num_progs == 1) child = & (pi->progs[0]);
31 ......
32 return rcode;
33 } else if (pi->num_progs == 1 && pi->progs[0].argv != NULL) {
34 ......
35 /* Process the command */
36 return cmd_process(flag, child->argc, child->argv,
37 &flag_repeat, NULL);
38 }
39
40 return -1;
41 }

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

cmd_process 函数详解

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

示例代码32.2.11.1 U_BOOT_CMD 宏定义
#define U_BOOT_CMD(_name, _maxargs, _rep, _cmd, _usage, _help) \
U_BOOT_CMD_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, _help, NULL)

可以看出U_BOOT_CMD 是U_BOOT_CMD_COMPLETE 的特例,将
U_BOOT_CMD_COMPLETE 的最后一个参数设置成NULL 就是U_BOOT_CMD 。宏
U_BOOT_CMD_COMPLETE 如下:

示例代码32.2.11.2 U_BOOT_CMD_COMPLETE 宏定义
#define U_BOOT_CMD_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, _help, _comp) \
ll_entry_declare(cmd_tbl_t, _name, cmd) = \
U_BOOT_CMD_MKENT_COMPLETE(_name, _maxargs, _rep, _cmd, \
_usage, _help, _comp);

宏U_BOOT_CMD_COMPLETE 又用到了ll_entry_declare 和U_BOOT_CMD_MKENT_COMPLETE。ll_entry_declar 定义在文件include/linker_lists.h中,定义如下:

示例代码32.2.11.3 ll_entry_declare 宏定义
#define ll_entry_declare(_type, _name, _list) \
_type _u_boot_list_2_##_list##_2_##_name __aligned(4) \
__attribute__((unused, \

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

示例代码32.2.11.4 U_BOOT_CMD_MKENT_COMPLETE 宏定义
#define U_BOOT_CMD_MKENT_COMPLETE(_name, _maxargs, _rep, _cmd, \
_usage, _help, _comp) \
{ #_name, _maxargs, _rep, _cmd, _usage, \
_CMD_HELP(_help) _CMD_COMPLETE(_comp) }

上述代码中的“# ”表示将_name 传递过来的值字符串化,U_BOOT_CMD_MKENT_COMPLETE 又用到了宏_CMD_HELP 和_CMD_COMPLETE,这两个宏的定义如下:

示例代码32.2.11.5 _CMD_HELP 和_CMD_COMPLETE 宏定义
1 #ifdef CONFIG_AUTO_COMPLETE
2 # define _CMD_COMPLETE(x) x,
3 #else
4 # define _CMD_COMPLETE(x)
5 #endif
6 #ifdef CONFIG_SYS_LONGHELP
7 # define _CMD_HELP(x) x,
8 #else
9 # define _CMD_HELP(x)
10 #endif

可以看出,如果定义了宏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 宏就如此的绕来绕去的!),
我们就以一个具体的命令为例,来看一下U_BOOT_CMD 经过展开以后究竟是个什么模样的。以命令dhcp 为例,dhcp 命令定义如下:

示例代码32.2.11.6 dhcp 命令宏定义
U_BOOT_CMD(
dhcp, 3, 1, do_dhcp,
"boot image via network using DHCP/TFTP protocol",
"[loadAddress] [[hostIPaddr:]bootfilename]"
);

将其展开,结果如下:

示例代码32.2.11.7 dhcp 命令展开
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}

从示例代码32.2.11.7 可以看出,dhcp 命令最终展开结果为:

示例代码32.2.11.8 dhcp 命令最终结果
1 cmd_tbl_t _u_boot_list_2_cmd_2_dhcp __aligned(4) \
2 __attribute__((unused,section(.u_boot_list_2_cmd_2_dhcp))) \
3 { "dhcp", 3, 1, do_dhcp, \
4 "boot image via network using DHCP/TFTP protocol", \
5 "[loadAddress] [[hostIPaddr:]bootfilename]",\
6 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 中,如图32.2.11.1 所示:
在这里插入图片描述

图32.2.11.1 u-boot.lds 中的.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 中,内容如下:

示例代码32.2.11.9 cmd_tbl_t 结构体
30 struct cmd_tbl_s {
31 char *name; /* Command Name */
32 int maxargs; /* maximum number of arguments */
33 int repeatable; /* autorepeat allowed? */
34 /* Implementation function */
35 int (*cmd)(struct cmd_tbl_s *, int, int, char * const []);
36 char *usage; /* Usage message (short) */
37 #ifdef CONFIG_SYS_LONGHELP
38 char *help; /* Help message (long) */
39 #endif
40 #ifdef CONFIG_AUTO_COMPLETE
41 /* do auto completion on the arguments */
42 int (*complete)(int argc, char * const argv[], char last_char, int maxv, char *cmdv[]);
43 #endif
44 };
45
46 typedef struct cmd_tbl_s cmd_tbl_t;

结合实例代码32.2.11.8,可以得出变量_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 中,函数内容如下:

示例代码32.2.11.10 command.c 文件代码段
500 enum command_ret_t cmd_process(int flag, int argc,
501 char * const argv[],int *repeatable, ulong *ticks)
502 {
503 enum command_ret_t rc = CMD_RET_SUCCESS;
504 cmd_tbl_t *cmdtp;
505
506 /* Look up command in command table */
507 cmdtp = find_cmd(argv[0]);
508 if (cmdtp == NULL) {
509 printf("Unknown command '%s' - try 'help'\n", argv[0]);
510 return 1;
511 }
512
513 /* found - check max args */
514 if (argc > cmdtp->maxargs)
515 rc = CMD_RET_USAGE;
516
517 #if defined(CONFIG_CMD_BOOTD)
518 /* avoid "bootd" recursion */
519 else if (cmdtp->cmd == do_bootd) {
520 if (flag & CMD_FLAG_BOOTD) {
521 puts("'bootd' recursion detected\n");
522 rc = CMD_RET_FAILURE;
523 } else {
524 flag |= CMD_FLAG_BOOTD;
525 }
526 }
527 #endif
528
529 /* If OK so far, then do the command */
530 if (!rc) {
531 if (ticks)
532 *ticks = get_timer(0);
533 rc = cmd_call(cmdtp, flag, argc, argv);
534 if (ticks)
535 *ticks = get_timer(*ticks);
536 *repeatable &= cmdtp->repeatable;
537 }
538 if (rc == CMD_RET_USAGE)
539 rc = cmd_usage(cmdtp);
540 return rc;
541 }

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

示例代码32.2.11.11 command.c 文件代码段
118 cmd_tbl_t *find_cmd(const char *cmd)
119 {
120 cmd_tbl_t *start = ll_entry_start(cmd_tbl_t, cmd);
121 const int len = ll_entry_count(cmd_tbl_t, cmd);
122 return find_cmd_tbl(cmd, start, len);
123 }

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

示例代码32.2.11.12 command.c 文件代码段
490 static int cmd_call(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
491 {
492 int result;
493
494 result = (cmdtp->cmd)(cmdtp, flag, argc, argv);
495 if (result)
496 debug("Command failed, result=%d\n", result);
497 return result;
498 }

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

bootz 启动Linux 内核过程

images 全局变量

不管是bootz 还是bootm 命令,在启动Linux 内核的时候都会用到一个重要的全局变量:
images,images 在文件cmd/bootm.c 中有如下定义:

示例代码32.3.1.1 images 全局变量
43 bootm_headers_t images; /* pointers to os/initrd/fdt images */

images 是bootm_headers_t 类型的全局变量,bootm_headers_t 是个boot 头结构体,在文件include/image.h 中的定义如下(删除了一些条件编译代码):

示例代码32.3.1.2 bootm_headers_t 结构体
304 typedef struct bootm_headers {
305 /*
306 * Legacy os image header, if it is a multi component image
307 * then boot_get_ramdisk() and get_fdt() will attempt to get
308 * data from second and third component accordingly.
309 */
310 image_header_t *legacy_hdr_os; /* image header pointer */
311 image_header_t legacy_hdr_os_copy; /* header copy */
312 ulong legacy_hdr_valid;
313
......
333
334 #ifndef USE_HOSTCC
335 image_info_t os; /* OS镜像信息*/
336 ulong ep; /* OS入口点*/
337
338 ulong rd_start, rd_end; /* ramdisk开始和结束位置*/
339
340 char *ft_addr; /* 设备树地址*/
341 ulong ft_len; /* 设备树长度*/
342
343 ulong initrd_start; /* initrd开始位置*/
344 ulong initrd_end; /* initrd结束位置*/
345 ulong cmdline_start; /* cmdline开始位置*/
346 ulong cmdline_end; /* cmdline结束位置*/
347 bd_t *kbd;
348 #endif
349
350 int verify; /* getenv("verify")[0] != 'n' */
351
352 #define BOOTM_STATE_START (0x00000001)
353 #define BOOTM_STATE_FINDOS (0x00000002)
354 #define BOOTM_STATE_FINDOTHER (0x00000004)
355 #define BOOTM_STATE_LOADOS (0x00000008)
356 #define BOOTM_STATE_RAMDISK (0x00000010)
357 #define BOOTM_STATE_FDT (0x00000020)
358 #define BOOTM_STATE_OS_CMDLINE (0x00000040)
359 #define BOOTM_STATE_OS_BD_T (0x00000080)
360 #define BOOTM_STATE_OS_PREP (0x00000100)
361 #define BOOTM_STATE_OS_FAKE_GO (0x00000200)/*'Almost' run the OS*/
362 #define BOOTM_STATE_OS_GO (0x00000400)
363 int state;
364
365 #ifdef CONFIG_LMB
366 struct lmb lmb; /* 内存管理相关,不深入研究*/
367 #endif
368 } bootm_headers_t;

第335 行的os 成员变量是image_info_t 类型的,为系统镜像信息。
第352~362 行这11 个宏定义表示BOOT 的不同阶段。
接下来看一下结构体image_info_t,也就是系统镜像信息结构体,此结构体在文件
include/image.h 中的定义如下:

示例代码32.3.1.3 image_info_t 结构体
292 typedef struct image_info {
293 ulong start, end; /* blob开始和结束位置*/
294 ulong image_start, image_len; /* 镜像起始地址(包括blob)和长度*/
295 ulong load; /* 系统镜像加载地址*/
296 uint8_t comp, type, os; /* 镜像压缩、类型,OS类型*/
297 uint8_t arch; /* CPU架构*/
298 } image_info_t;

全局变量images 会在bootz 命令的执行中频繁使用到,相当于Linux 内核启动的“灵魂”。

do_bootz 函数

bootz 命令的执行函数为do_bootz,在文件cmd/bootm.c 中有如下定义:

示例代码32.3.2.1 do_bootz 函数
622 int do_bootz(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
623 {
624 int ret;
625
626 /* Consume 'bootz' */
627 argc--; argv++;
628
629 if (bootz_start(cmdtp, flag, argc, argv, &images))
630 return 1;
631
632 /*
633 * We are doing the BOOTM_STATE_LOADOS state ourselves, so must
634 * disable interrupts ourselves
635 */
636 bootm_disable_interrupts();
637
638 images.os.os = IH_OS_LINUX;
639 ret = do_bootm_states(cmdtp, flag, argc, argv,
640 BOOTM_STATE_OS_PREP | BOOTM_STATE_OS_FAKE_GO |
641 BOOTM_STATE_OS_GO,
642 &images, 1);
643
644 return ret;
645 }

第629 行,调用bootz_start 函数,bootz_start 函数执行过程参考32.3.3 小节。
第636 行,调用函数bootm_disable_interrupts 关闭中断。
第638 行,设置images.os.os 为IH_OS_LINUX,也就是设置系统镜像为Linux,表示我们
要启动的是Linux 系统!后面会用到images.os.os 来挑选具体的启动函数。
第639 行,调用函数do_bootm_states 来执行不同的BOOT 阶段,这里要执行的BOOT 阶段有:BOOTM_STATE_OS_PREP 、BOOTM_STATE_OS_FAKE_GO 和BOOTM_STATE_OS_GO。

bootz_start 函数

bootz_srart 函数也定义在文件cmd/bootm.c 中,函数内容如下:

示例代码32.3.3.1 bootz_start 函数
578 static int bootz_start(cmd_tbl_t *cmdtp, int flag, int argc,
579 char * const argv[], bootm_headers_t *images)
580 {
581 int ret;
582 ulong zi_start, zi_end;
583
584 ret = do_bootm_states(cmdtp, flag, argc, argv,
585 BOOTM_STATE_START, images, 1);
586
587 /* Setup Linux kernel zImage entry point */
588 if (!argc) {
589 images->ep = load_addr;
590 debug("* kernel: default image load address = 0x%08lx\n",
591 load_addr);
592 } else {
593 images->ep = simple_strtoul(argv[0], NULL, 16);
594 debug("* kernel: cmdline image address = 0x%08lx\n",
595 images->ep);
596 }
597
598 ret = bootz_setup(images->ep, &zi_start, &zi_end);
599 if (ret != 0)
600 return 1;
601
602 lmb_reserve(&images->lmb, images->ep, zi_end - zi_start);
603
604 /*
605 * Handle the BOOTM_STATE_FINDOTHER state ourselves as we do not
606 * have a header that provide this informaiton.
607 */
608 if (bootm_find_images(flag, argc, argv))
609 return 1;
610
......
619 return 0;
620 }

第584 行,调用函数do_bootm_states,执行BOOTM_STATE_START 阶段。
第593 行,设置images 的ep 成员变量,也就是系统镜像的入口点,使用bootz 命令启动系统的时候就会设置系统在DRAM 中的存储位置,这个存储位置就是系统镜像的入口点,因此images->ep=0X80800000。
第598 行,调用bootz_setup 函数,此函数会判断当前的系统镜像文件是否为Linux 的镜像文件,并且会打印出镜像相关信息,bootz_setup 函数稍后会讲解。
第608 行,调用函数bootm_find_images 查找ramdisk 和设备树(dtb)文件,但是我们没有用到ramdisk,因此此函数在这里仅仅用于查找设备树(dtb)文件,此函数稍后也会讲解。
先来看一下bootz_setup 函数,此函数定义在文件arch/arm/lib/bootm.c 中,函数内容如下:

示例代码32.3.3.2 bootz_setup 函数
370 #define LINUX_ARM_ZIMAGE_MAGIC 0x016f2818
371
372 int bootz_setup(ulong image, ulong *start, ulong *end)
373 {
374 struct zimage_header *zi;
375
376 zi = (struct zimage_header *)map_sysmem(image, 0);
377 if (zi->zi_magic != LINUX_ARM_ZIMAGE_MAGIC) {
378 puts("Bad Linux ARM zImage magic!\n");
379 return 1;
380 }
381
382 *start = zi->zi_start;
383 *end = zi->zi_end;
384
385 printf("Kernel image @ %#08lx [ %#08lx - %#08lx ]\n", image,
386 *start, *end);
387
388 return 0;
389 }

第370 行,宏LINUX_ARM_ZIMAGE_MAGIC 就是ARM Linux 系统魔术数。
第376 行,从传递进来的参数image(也就是系统镜像首地址)中获取zimage 头。zImage 头
结构体为zimage_header。
第377~380 行,判断image 是否为ARM 的Linux 系统镜像,如果不是的话就直接返回,并且打印出“Bad Linux ARM zImage magic!”,比如我们输入一个错误的启动命令:

bootz 80000000900000000

因为我们并没有在0X80000000 处存放Linux 镜像文件(zImage),因此上面的命令肯定会执行出错的,结果如图32.3.3.1 所示:
在这里插入图片描述

图32.3.3.1 启动出错
第382、383 行初始化函数bootz_setup 的参数start 和end。
第385 行,打印启动信息,如果Linux 系统镜像正常的话就会输出图32.3.3.2 所示的信息:
在这里插入图片描述

图32.3.3.3 Linux 镜像信息
接下来看一下函数bootm_find_images,此函数定义在文件common/bootm.c 中,函数内容如下:

示例代码32.3.3.3 bootm_find_images 函数
225 int bootm_find_images(int flag, int argc, char * const argv[])
226 {
227 int ret;
228
229 /* find ramdisk */
230 ret = boot_get_ramdisk(argc, argv, &images, IH_INITRD_ARCH,
231 &images.rd_start, &images.rd_end);
232 if (ret) {
233 puts("Ramdisk image is corrupt or invalid\n");
234 return 1;
235 }
236
237 #if defined(CONFIG_OF_LIBFDT)
238 /* find flattened device tree */
239 ret = boot_get_fdt(flag, argc, argv, IH_ARCH_DEFAULT, &images,
240 &images.ft_addr, &images.ft_len);
241 if (ret) {
242 puts("Could not find a valid device tree\n");
243 return 1;
244 }
245 set_working_fdt_addr((ulong)images.ft_addr);
246 #endif
......
258 return 0;
259 }

第230~235 行是跟查找ramdisk,但是我们没有用到ramdisk,因此这部分代码不用管。
第237~244 行是查找设备树(dtb)文件,找到以后就将设备树的起始地址和长度分别写到
images 的ft_addr 和ft_len 成员变量中。我们使用bootz 启动Linux 的时候已经指明了设备树在DRAM 中的存储地址,因此images.ft_addr=0X83000000,长度根据具体的设备树文件而定,比如我现在使用的设备树文件长度为0X8C81,因此images.ft_len=0X8C81。
bootz_start 函数就讲解到这里,bootz_start 主要用于初始化images 的相关成员变量。

do_bootm_states 函数

do_bootz 最后调用的就是函数do_bootm_states ,而且在bootz_start 中也调用了
do_bootm_states 函数,看来do_bootm_states 函数还是个香饽饽。此函数定义在文件
common/bootm.c 中,函数代码如下:

示例代码32.3.4.1 do_bootm_states 函数
591 int do_bootm_states(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[],
592 int states, bootm_headers_t *images, int boot_progress)
593 {
594 boot_os_fn *boot_fn;
595 ulong iflag = 0;
596 int ret = 0, need_boot_fn;
597
598 images->state |= states;
599
600 /*
601 * Work through the states and see how far we get. We stop on
602 * any error.
603 */
604 if (states & BOOTM_STATE_START)
605 ret = bootm_start(cmdtp, flag, argc, argv);
606
607 if (!ret && (states & BOOTM_STATE_FINDOS))
608 ret = bootm_find_os(cmdtp, flag, argc, argv);
609
610 if (!ret && (states & BOOTM_STATE_FINDOTHER)) {
611 ret = bootm_find_other(cmdtp, flag, argc, argv);
612 argc = 0; /* consume the args */
613 }
614
615 /* Load the OS */
616 if (!ret && (states & BOOTM_STATE_LOADOS)) {
617 ulong load_end;
618
619 iflag = bootm_disable_interrupts();
620 ret = bootm_load_os(images, &load_end, 0);
621 if (ret == 0)
622 lmb_reserve(&images->lmb, images->os.load,
623 (load_end - images->os.load));
624 else if (ret && ret != BOOTM_ERR_OVERLAP)
625 goto err;
626 else if (ret == BOOTM_ERR_OVERLAP)
627 ret = 0;
628 #if defined(CONFIG_SILENT_CONSOLE) && !defined(CONFIG_SILENT_U_BOOT_ONLY)
629 if (images->os.os == IH_OS_LINUX)
630 fixup_silent_linux();
631 #endif
632 }
633
634 /* Relocate the ramdisk */
635 #ifdef CONFIG_SYS_BOOT_RAMDISK_HIGH
636 if (!ret && (states & BOOTM_STATE_RAMDISK)) {
637 ulong rd_len = images->rd_end - images->rd_start;
638
639 ret = boot_ramdisk_high(&images->lmb, images->rd_start,
640 rd_len, &images->initrd_start, &images->initrd_end);
641 if (!ret) {
642 setenv_hex("initrd_start", images->initrd_start);
643 setenv_hex("initrd_end", images->initrd_end);
644 }
645 }
646 #endif
647 #if defined(CONFIG_OF_LIBFDT) && defined(CONFIG_LMB)
648 if (!ret && (states & BOOTM_STATE_FDT)) {
649 boot_fdt_add_mem_rsv_regions(&images->lmb, images->ft_addr);
650 ret = boot_relocate_fdt(&images->lmb, &images->ft_addr,
651 &images->ft_len);
652 }
653 #endif
654
655 /* From now on, we need the OS boot function */
656 if (ret)
657 return ret;
658 boot_fn = bootm_os_get_boot_func(images->os.os);
659 need_boot_fn = states & (BOOTM_STATE_OS_CMDLINE |
660 BOOTM_STATE_OS_BD_T | BOOTM_STATE_OS_PREP |
661 BOOTM_STATE_OS_FAKE_GO | BOOTM_STATE_OS_GO);
662 if (boot_fn == NULL && need_boot_fn) {
663 if (iflag)
664 enable_interrupts();
665 printf("ERROR: booting os '%s' (%d) is not supported\n",
666 genimg_get_os_name(images->os.os), images->os.os);
667 bootstage_error(BOOTSTAGE_ID_CHECK_BOOT_OS);
668 return 1;
669 }
670
671 /* Call various other states that are not generally used */
672 if (!ret && (states & BOOTM_STATE_OS_CMDLINE))
673 ret = boot_fn(BOOTM_STATE_OS_CMDLINE, argc, argv, images);
674 if (!ret && (states & BOOTM_STATE_OS_BD_T))
675 ret = boot_fn(BOOTM_STATE_OS_BD_T, argc, argv, images);
676 if (!ret && (states & BOOTM_STATE_OS_PREP))
677 ret = boot_fn(BOOTM_STATE_OS_PREP, argc, argv, images);
678
679 #ifdef CONFIG_TRACE
680 /* Pretend to run the OS, then run a user command */
681 if (!ret && (states & BOOTM_STATE_OS_FAKE_GO)) {
682 char *cmd_list = getenv("fakegocmd");
683
684 ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_FAKE_GO,
685 images, boot_fn);
686 if (!ret && cmd_list)
687 ret = run_command_list(cmd_list, -1, flag);
688 }
689 #endif
690
691 /* Check for unsupported subcommand. */
692 if (ret) {
693 puts("subcommand not supported\n");
694 return ret;
695 }
696
697 /* Now run the OS! We hope this doesn't return */
698 if (!ret && (states & BOOTM_STATE_OS_GO))
699 ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_GO,
700 images, boot_fn);
......
712 return ret;
713 }

函数do_bootm_states 根据不同的BOOT 状态执行不同的代码段,通过如下代码来判断BOOT 状态:

states & BOOTM_STATE_XXX

在do_bootz 函数中会用到BOOTM_STATE_OS_PREP 、BOOTM_STATE_OS_FAKE_GO 和BOOTM_STATE_OS_GO 这三个BOOT 状态,bootz_start 函数中会用到BOOTM_STATE_START这个BOOT 状态。为了精简代码,方便分析,因此我们将示例代码32.3.4.1 中的函数do_bootm_states 进行精简,只留下下面这4 个BOOT 状态对应的处理代码:

BOOTM_STATE_OS_PREP
BOOTM_STATE_OS_FAKE_GO
BOOTM_STATE_OS_GO
BOOTM_STATE_START

精简以后的do_bootm_states 函数如下所示:

示例代码32.3.4.2 精简后的do_bootm_states 函数
591 int do_bootm_states(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[],
592 int states, bootm_headers_t *images, int boot_progress)
593 {
594 boot_os_fn *boot_fn;
595 ulong iflag = 0;
596 int ret = 0, need_boot_fn;
597
598 images->state |= states;
599
600 /*
601 * Work through the states and see how far we get. We stop on
602 * any error.
603 */
604 if (states & BOOTM_STATE_START)
605 ret = bootm_start(cmdtp, flag, argc, argv);
......
654
655 /* From now on, we need the OS boot function */
656 if (ret)
657 return ret;
658 boot_fn = bootm_os_get_boot_func(images->os.os);
659 need_boot_fn = states & (BOOTM_STATE_OS_CMDLINE |
660 BOOTM_STATE_OS_BD_T | BOOTM_STATE_OS_PREP |
661 BOOTM_STATE_OS_FAKE_GO | BOOTM_STATE_OS_GO);
662 if (boot_fn == NULL && need_boot_fn) {
663 if (iflag)
664 enable_interrupts();
665 printf("ERROR: booting os '%s' (%d) is not supported\n",
666 genimg_get_os_name(images->os.os), images->os.os);
667 bootstage_error(BOOTSTAGE_ID_CHECK_BOOT_OS);
668 return 1;
669 }
670
......
676 if (!ret && (states & BOOTM_STATE_OS_PREP))
677 ret = boot_fn(BOOTM_STATE_OS_PREP, argc, argv, images);
678
679 #ifdef CONFIG_TRACE
680 /* Pretend to run the OS, then run a user command */
681 if (!ret && (states & BOOTM_STATE_OS_FAKE_GO)) {
682 char *cmd_list = getenv("fakegocmd");
683
684 ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_FAKE_GO,
685 images, boot_fn);
686 if (!ret && cmd_list)
687 ret = run_command_list(cmd_list, -1, flag);
688 }
689 #endif
690
691 /* Check for unsupported subcommand. */
692 if (ret) {
693 puts("subcommand not supported\n");
694 return ret;
695 }
696
697 /* Now run the OS! We hope this doesn't return */
698 if (!ret && (states & BOOTM_STATE_OS_GO))
699 ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_GO,
700 images, boot_fn);
......
712 return ret;
713 }

第604、605 行,处理BOOTM_STATE_START 阶段,bootz_start 会执行这一段代码,这里
调用函数bootm_start,此函数定义在文件common/bootm.c 中,函数内容如下:

示例代码32.3.4.2 bootm_start 函数
69 static int bootm_start(cmd_tbl_t *cmdtp, int flag, int argc,
70 char * const argv[])
71 {
72 memset((void *)&images, 0, sizeof(images)); /* 清空images */
73 images.verify = getenv_yesno("verify");/* 初始化verfify成员*/
74
75 boot_start_lmb(&images);
76
77 bootstage_mark_name(BOOTSTAGE_ID_BOOTM_START, "bootm_start");
78 images.state = BOOTM_STATE_START;/* 设置状态为BOOTM_STATE_START */
79
80 return 0;
81 }

接着回到示例代码32.3.4.2 中,继续分析函数do_bootm_states。第658 行非常重要!通过
函数bootm_os_get_boot_func 来查找系统启动函数,参数images->os.os 就是系统类型,根据这个系统类型来选择对应的启动函数,在do_bootz 中设置images.os.os= IH_OS_LINUX。函数返回值就是找到的系统启动函数,这里找到的Linux 系统启动函数为do_bootm_linux,关于此函数查找系统启动函数的过程请参考32.3.5 小节。因此boot_fn=do_bootm_linux,后面执行boot_fn函数的地方实际上是执行的do_bootm_linux 函数。
第676 行,处理BOOTM_STATE_OS_PREP 状态,调用函数do_bootm_linux,do_bootm_linux也是调用boot_prep_linux 来完成具体的处理过程。boot_prep_linux 主要用于处理环境变量bootargs,bootargs 保存着传递给Linux kernel 的参数。
第679~689 行是处理BOOTM_STATE_OS_FAKE_GO 状态的,但是要我们没用使能TRACE
功能,因此宏CONFIG_TRACE 也就没有定义,所以这段程序不会编译。
第699 行,调用函数boot_selected_os 启动Linux 内核,此函数第4 个参数为Linux 系统镜像头,第5 个参数就是Linux 系统启动函数do_bootm_linux。boot_selected_os 函数定义在文件
common/bootm_os.c 中,函数内容如下:

示例代码32.3.4.3 boot_selected_os 函数
476 int boot_selected_os(int argc, char * const argv[], int state,
477 bootm_headers_t *images, boot_os_fn *boot_fn)
478 {
479 arch_preboot_os();
480 boot_fn(state, argc, argv, images);
......
490 return BOOTM_ERR_RESET;
491 }

第480 行调用boot_fn 函数,也就是do_bootm_linux 函数来启动Linux 内核。

bootm_os_get_boot_func 函数

do_bootm_states 会调用bootm_os_get_boot_func 来查找对应系统的启动函数,此函数定义
在文件common/bootm_os.c 中,函数内容如下:

示例代码32.3.5.1 bootm_os_get_boot_func 函数
493 boot_os_fn *bootm_os_get_boot_func(int os)
494 {
495 #ifdef CONFIG_NEEDS_MANUAL_RELOC
496 static bool relocated;
497
498 if (!relocated) {
499 int i;
500
501 /* relocate boot function table */
502 for (i = 0; i < ARRAY_SIZE(boot_os); i++)
503 if (boot_os[i] != NULL)
504 boot_os[i] += gd->reloc_off;
505
506 relocated = true;
507 }
508 #endif
509 return boot_os[os];
510 }

第495~508 行是条件编译,在本uboot 中没有用到,因此这段代码无效,只有509 行有效。
在509 行中boot_os 是个数组,这个数组里面存放着不同的系统对应的启动函数。boot_os 也定义在文件common/bootm_os.c 中,如下所示:

示例代码32.3.5.2 boot_os 数组
435 static boot_os_fn *boot_os[] = {
436 [IH_OS_U_BOOT] = do_bootm_standalone,
437 #ifdef CONFIG_BOOTM_LINUX
438 [IH_OS_LINUX] = do_bootm_linux,
439 #endif
......
465 #ifdef CONFIG_BOOTM_OPENRTOS
466 [IH_OS_OPENRTOS] = do_bootm_openrtos,
467 #endif
468 };

第438 行就是Linux 系统对应的启动函数:do_bootm_linux。

do_bootm_linux 函数

经过前面的分析,我们知道了do_bootm_linux 就是最终启动Linux 内核的函数,此函数定义在文件arch/arm/lib/bootm.c,函数内容如下:

示例代码32.3.6.1 do_bootm_linux 函数
339 int do_bootm_linux(int flag, int argc, char * const argv[],
340 bootm_headers_t *images)
341 {
342 /* No need for those on ARM */
343 if (flag & BOOTM_STATE_OS_BD_T || flag & BOOTM_STATE_OS_CMDLINE)
344 return -1;
345
346 if (flag & BOOTM_STATE_OS_PREP) {
347 boot_prep_linux(images);
348 return 0;
349 }
350
351 if (flag & (BOOTM_STATE_OS_GO | BOOTM_STATE_OS_FAKE_GO)) {
352 boot_jump_linux(images, flag);
353 return 0;
354 }
355
356 boot_prep_linux(images);
357 boot_jump_linux(images, flag);
358 return 0;
359 }

第351 行,如果参数flag 等于BOOTM_STATE_OS_GO 或者BOOTM_STATE_OS_FAKE_GO的话就执行boot_jump_linux 函数。boot_selected_os 函数在调用do_bootm_linux 的时候会将flag设置为BOOTM_STATE_OS_GO。
第352 行,执行函数boot_jump_linux,此函数定义在文件arch/arm/lib/bootm.c 中,函数内容如下:

示例代码32.3.6.2 boot_jump_linux 函数
272 static void boot_jump_linux(bootm_headers_t *images, int flag)
273 {
274 #ifdef CONFIG_ARM64
......
292 #else
293 unsigned long machid = gd->bd->bi_arch_number;
294 char *s;
295 void (*kernel_entry)(int zero, int arch, uint params);
296 unsigned long r2;
297 int fake = (flag & BOOTM_STATE_OS_FAKE_GO);
298
299 kernel_entry = (void (*)(int, int, uint))images->ep;
300
301 s = getenv("machid");
302 if (s) {
303 if (strict_strtoul(s, 16, &machid) < 0) {
304 debug("strict_strtoul failed!\n");
305 return;
306 }
307 printf("Using machid 0x%lx from environment\n", machid);
308 }
309
310 debug("## Transferring control to Linux (at address %08lx)" \
311 "...\n", (ulong) kernel_entry);
312 bootstage_mark(BOOTSTAGE_ID_RUN_OS);
313 announce_and_cleanup(fake);
314
315 if (IMAGE_ENABLE_OF_LIBFDT && images->ft_len)
316 r2 = (unsigned long)images->ft_addr;
317 else
318 r2 = gd->bd->bi_boot_params;
319
......
328 kernel_entry(0, machid, r2);
329 }
330 #endif
331 }

第274~292 行是64 位ARM 芯片对应的代码,Cortex-A7 是32 位芯片,因此用不到。
第293 行,变量machid 保存机器ID,如果不使用设备树的话这个机器ID 会被传递给Linux
内核,Linux 内核会在自己的机器ID 列表里面查找是否存在与uboot 传递进来的machid 匹配的项目,如果存在就说明Linux 内核支持这个机器,那么Linux 就会启动!如果使用设备树的话这个machid 就无效了,设备树存有一个“兼容性”这个属性,Linux 内核会比较“兼容性”属性的值(字符串)来查看是否支持这个机器。
第295 行,函数kernel_entry,看名字“内核_进入”,说明此函数是进入Linux 内核的,也就是最终的大boos!!此函数有三个参数:zero,arch,params,第一个参数zero 同样为0;第二个参数为机器ID;第三个参数ATAGS 或者设备树(DTB)首地址,ATAGS 是传统的方法,用于传递一些命令行信息啥的,如果使用设备树的话就要传递设备树(DTB)。
第299 行,获取kernel_entry 函数,函数kernel_entry 并不是uboot 定义的,而是Linux 内
核定义的,Linux 内核镜像文件的第一行代码就是函数kernel_entry,而images->ep 保存着Linux内核镜像的起始地址,起始地址保存的正是Linux 内核第一行代码!
第313 行,调用函数announce_and_cleanup 来打印一些信息并做一些清理工作,此函数定义在文件arch/arm/lib/bootm.c 中,函数内容如下:

示例代码32.3.6.3 announce_and_cleanup 函数
72 static void announce_and_cleanup(int fake)
73 {
74 printf("\nStarting kernel ...%s\n\n", fake ?
75 "(fake run for tracing)" : "");
76 bootstage_mark_name(BOOTSTAGE_ID_BOOTM_HANDOFF, "start_kernel");
......
87 cleanup_before_linux();
88 }

第74 行,在启动Linux 之前输出“Starting kernel …”信息,如图32.3.6.1 所示:
在这里插入图片描述
图32.3.6.1 系统启动提示信息
第87 行调用cleanup_before_linux 函数做一些清理工作。
继续回到示例代码32.3.6.2 的函数boot_jump_linux,第315~318 行是设置寄存器r2 的值?
为什么要设置r2 的值呢?Linux 内核一开始是汇编代码,因此函数kernel_entry 就是个汇编函
数。向汇编函数传递参数要使用r0、r1 和r2(参数数量不超过3 个的时候),所以r2 寄存器就是
函数kernel_entry 的第三个参数。
第316 行,如果使用设备树的话,r2 应该是设备树的起始地址,而设备树地址保存在images
的ftd_addr 成员变量中。
第317 行,如果不使用设备树的话,r2 应该是uboot 传递给Linux 的参数起始地址,也就是环境变量bootargs 的值,
第328 行,调用kernel_entry 函数进入Linux 内核,此行将一去不复返,uboot 的使命也就完成了,它可以安息了!
总结一下bootz 命令的执行过程,如图32.3.6.2 所示:
在这里插入图片描述
图32.3.6.2 bootz 命令执行过程
到这里uboot 的启动流程我们就讲解完成了,加上uboot 顶层Makefile 的分析,洋洋洒洒
100 多页,还是不少的!这也仅仅是uboot 启动流程分析,当缕清了uboot 的启动流程以后,后面移植uboot 就会轻松很多。其实在工作中我们基本不需要这么详细的去了解uboot,半导体厂商提供给我们的uboot 一般是可以直接用的,只要能跑起来,可以使用就可以了。但是作为学习,我们是必须去详细的了解一下uboot 的启动流程,否则如果在工作中遇到问题我们连解决的方法都没有,都不知道该从哪里看起。但是呢,如果第一次就想弄懂uboot 的整个启动流程还是有点困难的,所以如果没有看懂的话,不要紧!不要气馁,大多数人第一次看uboot 启动流程基本都有各种各样的问题。
题外话:
相信大家看完本章以后基本都有一个感觉:长、复杂、绕!没错,当我第一次学习uboot 的
时候看到uboot 启动流程的时候也是这个感觉,当时我也一脸懵逼,怎么这么复杂,这么长呢?
尤其前面的汇编代码部分,还要涉及到ARM 处理器架构的内容,当时也怀疑过自己是不是搞
这一块的料。不过好在自己坚持下来了,uboot 的启动流程我至少分析过7,8 遍,各种版本的,零几年很古老的;12 年、14 年比较新的等等很多个版本的uboot。就I.MX6ULL 使用的这个2016.03 版本uboot 我至少详细的分析了2 遍,直至写完本章,大概花了1 个月的时间。这期间查阅了各种资料,看了不知道多少篇博客,在这里感谢那些无私奉献的网友们。
相信很多朋友看完本章可能会想:我什么时候也能这么厉害,能够这么详细的分析uboot 启
动流程。甚至可能会有挫败感,还是那句话:不要气馁!千里之行始于足下,所有你羡慕的人都曾经痛苦过,挫败过。脚踏实地,一步一个脚印,一点一滴的积累,最终你也会成为你所羡慕的人。在嵌入式Linux 这条道路上,有众多的学习者陪着你,大家相互搀扶,终能踏出一条康庄大道,祝所有的同学终有所获!

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

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

相关文章

【教3妹学编程-算法题】最长平衡子字符串

3妹&#xff1a;呜呜&#xff0c;烦死了&#xff0c; 脸上长了一个痘 2哥 : 不要在意这些细节嘛&#xff0c;不用管它&#xff0c;过两天自然不就好了。 3妹&#xff1a;切&#xff0c;你不懂&#xff0c;影响这两天的心情哇。 2哥 : 我看你是不急着找工作了啊&#xff0c; 工作…

OCR技术助力行驶证识别,引领未来智能交通新潮流

随着人工智能技术的发展&#xff0c;光学字符识别&#xff08;OCR&#xff09;技术在各个领域得到了广泛的应用&#xff0c;而行驶证OCR识别作为OCR技术的一个重要应用之一&#xff0c;在多个领域展现出了巨大的潜力。 首先&#xff0c;行驶证OCR识别在交通管理领域具有重要意…

【Android】Dagger2 框架设计理念和使用方式详解

文章目录 Dagger 框架作用基本使用方法引入依赖创建 Object创建 Module创建 Component向 Activity 注入对象 Component 内部单例全局单例自定义 Scope关于单例作用域的理解注入多种同类型对象Component 依赖Component 继承传递 Activity Dagger 框架作用 这里&#xff0c;我们…

Vue路由使用参数传递数据

一、使用query参数传递数据 &#xff08;一&#xff09;参数的传递 1. 携带参数进行传递 <router-link to"/路径?参数名1参数值1&参数名2参数值2">内容</router-link> 我们在下面的代码中传递每条消息的id和标题&#xff1a; 2. 配置对象进行传递…

计算/存储虚拟化高级特性

目录 计算虚拟化特性 HA高可用 虚拟机热迁移 虚拟机快照技术 存储虚拟化特性 链接克隆 存储热迁移 裸设备映射 计算虚拟化特性 HA高可用 通过HA&#xff08;High Available&#xff09;机制&#xff0c;可以提升虚拟机的可用度&#xff0c;允许虚拟机所在的服务器节点…

采集Prestashop独立站

这是一个用Lua编写的爬虫程序&#xff0c;用于采集Prestashop独立站的内容。爬虫程序使用代理信息&#xff1a;proxy_host: jshk.com.cn。 -- 首先&#xff0c;我们需要导入所需的库 local http require(socket.http) local url require(socket.url)-- 然后&#xff0c;我们…

互联网线上预约洗衣洗鞋店软件功能介绍:

互联网线上预约洗衣洗鞋店软件功能介绍&#xff1a; 1. 在线下单&#xff1a;用户可以直接打开小程序&#xff0c;查看各类鞋子洗护服务的费用、细节等情况&#xff0c;方便用户按照需求进行对应的服务下单&#xff0c;并设置收货地址进行在线支付。用户可以选择不同的洗护服务…

广告垄断是对创业者的一种不公平

每次过节例如国庆节&#xff0c;中秋节&#xff0c;双十一&#xff0c;618&#xff0c;春节&#xff0c;抖音上面都会充满了各色各样的品牌广告&#xff0c;但是都有一个特点&#xff1a;几乎都是很少几个人的广告&#xff0c;但是小公司的广告几乎看不见&#xff0c;或者没有人…

宝塔部署QQ机器人,提示OpenSSL 1.0.2k-fips 26 Jan 2017

1、报错预览 Traceback (most recent call last):File "/www/wwwroot/python/bot-one/main.py", line 5, in <module>import requestsFile "/www/wwwroot/python/bot-one/343ae0eb0d491a10a1a00c0621b03ed0_venv/lib/python3.9/site-packages/requests/_…

XCTF刷题十一道(01)

文章目录 Training-WWW-RobotsPHP2unserialize3view-sourceget_postrobotsbackupcookiedisabled_buttonweak_authsimple_php Training-WWW-Robots robots.txt&#xff0c;防爬虫&#xff0c;访问urlrobots.txt PHP2 phps源码泄露 >phps文件就是php的源代码文件&#xff0…

MaHDE

FHM means ‘fitness hierarchical mutation’&#xff0c;DGS means ‘directed global search’&#xff0c;ELS means ‘elite local search’ 辅助信息 作者未提供代码

新生儿发烧:原因、科普和注意事项

引言&#xff1a; 新生儿发烧是新父母常常担心的问题之一&#xff0c;因为婴儿的免疫系统尚未完全发育&#xff0c;对感染更为脆弱。尽管发烧在婴儿中是相对常见的&#xff0c;但它可能引起家长的焦虑。本文将科普新生儿发烧的原因&#xff0c;提供相关信息&#xff0c;并为父…

面试题:经典常见排序算法 插入 冒泡 选择 归并 快速排序

1.插入排序 从头向尾不断扩大排序范围 (保持范围内顺序) 时间复杂度 O(n2) 2.冒泡排序 从第i1个数据和第i1个数据 进行比较 大的向后移 直到移动到他的为止&#xff08;以最大值为主要观察对象 最大值逐个排到正确位置&#xff09; 时间复杂度 O(n2) 3.选择排…

presto插件机制揭秘:探索无限可能的数据处理舞台

文章目录 1. 前言2. Presto插件架构3. Plugin接口3.1 插件协议3.2 插件实现类 4. 插件加载过程4.1 PluginManager 5. 插件应用6. 总结 关键词&#xff1a;Presto Plugin 1. 前言 本文源码环境&#xff1a; presto: prestoDb 0.275版本 在Presto框架中插件机制设计是一种非常常见…

ubuntu| sudo apt-get update 更新失败, 没有 Release 文件 无法安全地用该源进行更新,所以默认禁用该源

xiaoleubt:~$ sudo apt-get update -y 命中:1 https://dl.google.com/linux/chrome/deb stable InRelease 忽略:2 http://ppa.launchpad.net/ubuntu-desktop/ubuntu-make/ubuntu focal InRelease 命中:3 https://packages.microsoft.com/repos/code stable InRelease 命中:4 ht…

全志A40i应用笔记 | 3种常见的网卡软件问题以及排查思路

在飞凌嵌入式OKA40i-C开发板上虽然只有一个网口&#xff0c;但全志A40i-H处理器本身是有两个网络控制器的&#xff0c;因此在飞凌嵌入式提供的产品资料中提供了双网口解决方案。有的工程师小伙伴在开发过程中会遇见一些网卡的设计问题&#xff0c;今天小编为大家分享3种在使用O…

(待完善)python学习参考手册

这里写目录标题 观前浅谈:学习路线 :学习心得笔记:Step1:简单但一问不知怎么的组织语言去回答的小问题:什么是提示符?python解释器是什么?请正在阅读本文的朋友,安装一下PyCharm以及如何进行科学的省钱:Python中的命令行模式和交互模式的区别是什么?请正在阅读本文的朋友安装…

伦敦金开户需要多少资金,有开户条件吗?

伦敦金&#xff08;London Gold&#xff09;是黄金市场中备受瞩目的投资种类之一&#xff0c;无论是专业投资者还是新手&#xff0c;都对伦敦金感兴趣。但关于开户需要多少资金&#xff0c;以及是否有特定的开户条件&#xff0c;这些问题可能会让一些新手投资者感到困惑。 首先…

SpringCloud之Seata基本介绍与安装

目录 基本介绍 概述 核心组件 四种方案 部署TC服务&#xff08;安装&#xff09; 下载 修改registry.conf nacos添加配置 建表(仅db) 启动 基本介绍 概述 Seata 是一款开源的分布式事务解决方案&#xff0c;致力于提供高性能和简单易用的分布式事务服务。Seata 将…