正点原子嵌入式linux驱动开发——U-boot启动流程详解

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

链接脚本u-boot.lds详解

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

打开u-boot.lds,内容如下(有省略):
示例代码12.1.1 u-boot.lds文件部分代码截图
第3行为代码当前入口点:_start ,_start在文件 arch/arm/lib/vectors.S中有定义,如下示例代码所示(省略注释):
示例代码12.1.2 _start入口
从上述示例代码中可以看出,_start后面就是中断向量表,第2行的“.section “.vectors”,“ax””可以得到,此代码存放在.vectors段里面。

使用如下命令在uboot中查找“__image_copy_start”:

grep -nR "__image_copy_start"

搜索结果如下图所示:
查找结果
打开u-boot.map,找到如下图所示位置:
u-boot.map
u-boot.map是uboot的映射文件,可以从此文件看到某个文件或者函数链接到了哪个地址,从上图的1629行可以看到__image_copy_start为0Xc0100000,而.text的起始地址也是0Xc0100000

继续回到上述示例代码中,第11行是vectors段,vectors段保存中断向量表,从上图中我们知道了vectors.S的代码是存在vectors段中的。从上图可以看出,vectors段的起始地址也是0Xc0100000,说明整个uboot的起始地址就是0Xc0100000

上述示例代码中第12行将arch/arm/cpu/armv7/start.s编译出来的代码放到中断向量表后面。

u-boot.lds中有一些跟地址有关的“变量”需要注意一下,后面分析u-boot源码的时候会用到,这些变量要最终编译完成才能确定的!!!比如教程中编译完成以后这些“变量”的值如下图所示:
uboot相关变量表
上图中的“变量”值可以在u-boot.map文件中查找,上图中除了 __image_copy_start以外,其他的变量值每次编译的时候可能会变化,如果修改了uboot代码、修改了uboot配置、选用不同的优化等级等等都会影响到这些值。所以,一切以实际值为准!

U-Boot启动流程详解

reset函数源码详解

从u-boot.lds中已经看出入口点是arch/arm/lib/vectors.S文件中的_start,回到上面的示例代码。

第4行_start开始的是中断向量表,其中11-18行就是中断向量表,和裸机例程里面一样。第11行跳转到reset函数里面,reset函数在arch/arm/cpu/armv7/start.S里面,代码如下:
示例代码12.2.1.1 start.S代码段截图
第31行就是reset函数。

第40行从reset函数跳转到了save_boot_params函数,而save_boot_params函数同样定义
在start.S里面,定义如下:
示例代码12.2.1.2 start.S代码段截图
save_boot_params函数也是只有一句跳转语句,跳转到save_boot_params_ret函数,save_boot_params_ret函数代码如下:
示例代码12.2.1.3 start.S代码段截图
第42-51行,CONFIG_ARMV7_LPAE没有定义所以不会运行。

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

第57行,将寄存器r0中的值与0X1F进行与运算,结果保存到r1寄存器中,目的就是提取cpsr的 bit0-bit4这5位,这5位为M4 M3 M2 M1 M0,M[4:0]这五位用来设置处理器的工作模式,如下图所示:
Cortex-A7工作模式
第58行,判断r1寄存器的值是否等于0X1A(0b11010),也就是判断当前处理器模式是否处于Hyp模式。

第59行,如果r1和0X1A不相等,也就是CPU不处于Hyp模式的话就将r0寄存器的bit0-5进行清零,其实就是清除模式位。

第60行,如果处理器不处于Hyp模式的话就将r0的寄存器的值与0x13进行或运算,0x13=0b10011,也就是设置处理器进入SVC模式。

第61行,r0寄存器的值再与0xC0进行或运算,那么r0寄存器此时的值就是0xD3,cpsr的I为和F位分别控制IRQ和FIQ这两个中断的开关,设置为1就关闭了FIQ和IRQ。

第62行,将r0寄存器写回到cpsr寄存器中。完成设置CPU处于SVC32模式,并且关闭FIQ和IRQ这两个中断

继续看下面代码:
示例代码12.2.1.4 start.S代码段截图
第69行,如果没有定义CONFIG_OMAP44XX和CONFIG_SPL_BUILD的话条件成立,此处条件成立。

第71行读取CP15中c1寄存器的值到r0寄存器中,读取SCTLR寄存器的值。

第72行,CR_V在arch/arm/include/asm/system.h中有如下所示定义:

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

因此这一行的目的就是清除SCTLR寄存器中的 bit13,SCTLR寄存器结构如下图所示:
SCTLR寄存器结构图
从上图中可以看出,bit13为V位,此位是向量表控制位,当为0的时候向量表基地址为0X00000000,软件可以重定位向量表。为1的时候向量表基地址为0XFFFF0000,软件不能重定位向量表。这里将V清零,目的就是为了接下来的向量表重定位。

第73行将r0寄存器的值重新写入到寄存器SCTLR中。

第77行设置r0寄存器的值为_start,_start就是整个uboot的入口地址,其值为0XC0100000,相当于uboot的起始地址,因此0Xc0100000也是向量表的起始地址

第78行将r0寄存器的值(向量表起始地址)写入到CP15的c12寄存器中,也就是VBAR寄存器。因此第71-78行就是设置向量表重定位的。

代码继续往下执行:
示例代码12.2.1.5 start.S代码段截图
第83行如果没有定义CONFIG_SKIP_LOWLEVEL_INIT的话条件成立。我们没有定义CONFIG_SKIP_LOWLEVEL_INIT,因此条件成立,执行下面的语句。

第84行,.config文件里定义CONFIG_CPU_V7A,所以第85行会执行。

第87行,没有定义CONFIG_SKIP_LOWLEVEL_INIT_ONLY,所以第88行会执行。

上述示例代码中的内容比较简单,就是分别调用函数cpu_init_cp15、 cpu_init_crit和_main

函数cpu_init_cp15用来设置CP15相关的内容,比如关闭MMU啥的,此函数同样在start.S文件中定义的,代码如下:
示例代码12.2.1.6 start.S代码段截图
函数cpu_init_cp15都是一些和CP15有关的内容,我们不用关心。

函数cpu_init_crit也在是定义在start.S文件中,函数内容如下:
示例代码12.2.1.7 start.S代码段截图
可以看出函数cpu_init_crit内部仅仅是调用了函数lowlevel_init,接下来就是详细的分析一
下lowlevel_init和_main这两个函数。

lowlevel_init函数详解

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

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

第31行设置sp指向CONFIG_SYS_INIT_SP_ADDR,CONFIG_SYS_INIT_SP_ADDR在
include/configs/stm32mp1.h文件中,在stm32mp1.h中有如下所示定义:
stm32mp1.h代码段截图
CONFIG_SYS_TEXT_BASE的定义在我们.config文件中定义,如下图所示:
CONFIG_SYS_TEXT_BASE定义
从上图中可得知CONFIG_SYS_INIT_SP_ADDR 的值为0XC0100000。此时sp指向0XC0100000

继续回到文件lowlevel_init.S,第33行对sp指针做8字节对齐处理!

第44行,sp指针减去GD_SIZE,GD_SIZE的定义在include/generated/generic-asm-offsets.h文件中,如下图所示:
示例代码12.2.2.3 generic-asm-offsets.h代码段截图
第11行,GD_SIZE大小为240

继续回到文件lowlevel_init.S,第45行对sp做8个字节对齐,此时sp的地址为0XC0100000-240=0XC00FFDC0。

第46行将sp地址保存在r9寄存器中。

第52行将ip和lr压栈。

第67行调用函数s_init,此函数没啥作用。

第88行将第36行入栈的ip和lr进行出栈,并将lr赋给pc。

lowlevel_init运行完后,就返回函数cpu_init_crit,函数cpu_init_crit也执行完成了,最终返回到save_boot_params_ret,函数调用路径如下图所示:
uboot函数调用路径
从上图可知,接下来要执行的是save_boot_params_ret中的_main函数,接下来分析_main函数。

_main函数详解

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

示例代码代码12.2.3.1 crt0.S代码段 
91 ENTRY(_main) 
92 
93 /* 
94 * Set up initial C runtime environment and call board_init_f(0). 
95 */ 
96 
97 #if defined(CONFIG_TPL_BUILD) && defined(CONFIG_TPL_NEEDS_SEPARATE_STACK) 
98     ldr r0, =(CONFIG_TPL_STACK) 
99 #elif defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK) 
100     ldr r0, =(CONFIG_SPL_STACK) 
101 #else 
102     ldr r0, =(CONFIG_SYS_INIT_SP_ADDR) 
103 #endif 
104     bic r0, r0, #7 /* 8-byte alignment for ABI compliance */ 
105     mov sp, r0 
106     bl board_init_f_alloc_reserve 
107     mov sp, r0 
108     /* set up gd here, outside any C code */ 
109     mov r9, r0 
110     bl board_init_f_init_reserve 
111 
112 #if defined(CONFIG_SPL_EARLY_BSS) 
113     SPL_CLEAR_BSS 
114 #endif 
115 
116     mov r0, #0 
117     bl board_init_f 
118 
119 #if ! defined(CONFIG_SPL_BUILD) 
120 
121 /* 
122 * Set up intermediate environment (new sp and gd) and call 
123 * relocate_code(addr_moni). Trick here is that we'll return 
124 * 'here' but relocated. 
125 */ 
126
127     ldr r0, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */ 
128     bic r0, r0, #7 /* 8-byte alignment for ABI compliance */ 
129     mov sp, r0 
130     ldr r9, [r9, #GD_NEW_GD] /* r9 <- gd->new_gd */ 
131 
132     adr lr, here 
133     ldr r0, [r9, #GD_RELOC_OFF] /* r0 = gd->reloc_off */ 
134     add lr, lr, r0 
135 #if defined(CONFIG_CPU_V7M) 
136     orr lr, #1 /* As required by Thumb-only */ 
137 #endif 
138     ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */ 
139     b relocate_code 
140 here: 
141 /* 
142 * now relocate vectors 
143 */ 
144 
145     bl relocate_vectors 
146 
147 /* Set up final (full) environment */ 
148 
149     bl c_runtime_cpu_setup /* we still call old routine here */ 
150 #endif 
151 #if !defined(CONFIG_SPL_BUILD) || CONFIG_IS_ENABLED(FRAMEWORK) 
152 
153 #if !defined(CONFIG_SPL_EARLY_BSS) 
154     SPL_CLEAR_BSS 
155 #endif 
156 
157 # ifdef CONFIG_SPL_BUILD 
158     /* Use a DRAM stack for the rest of SPL, if requested */ 
159     bl spl_relocate_stack_gd 
160     cmp r0, #0 
161     movne sp, r0 
162     movne r9, r0 
163 # endif 
164 
165 #if ! defined(CONFIG_SPL_BUILD) 
166     bl coloured_LED_init 
167     bl red_led_on 
168 #endif 
169     /* call board_init_r(gd_t *id, ulong dest_addr) */
170     mov r0, r9 /* gd_t */ 
171     ldr r1, [r9, #GD_RELOCADDR] /* dest_addr */ 
172     /* call board_init_r */ 
173 #if CONFIG_IS_ENABLED(SYS_THUMB_BUILD) 
174     ldr lr, =board_init_r /* this is auto-relocated! */ 
175     bx lr 
176 #else 
177     ldr pc, =board_init_r /* this is auto-relocated! */ 
178 #endif 
179     /* we should not return here. */ 
180 #endif 
181 
182 ENDPROC(_main) 
183

代码太长了很难截图,就直接把源码拷贝过来了,行数的话主要还是对着讲解的内容过一遍。

第102行,设置sp指针为CONFIG_SYS_INIT_SP_ADDR,也就是sp指向0XC0100000

第104行,sp做8字节对齐。

第105行,读取sp到寄存器r0里面,此时r0=0XC0100000。

第106行,调用函数board_init_f_alloc_reserve,此函数有一个参数,参数为r0中的值,也就是0XC0100000,此函数定义在文件common/init/board_init.c中,内容如下:
示例代码12.2.3.2 board_init.c代码段截图
函数board_init_f_alloc_reserve主要是留出早期的malloc内存区域和gd内存区域,其中CONFIG_SYS_MALLOC_F_LEN=0X3000(在文件include/generated/autoconf.h中定义),另外sizeof(struct global_data)=240,也就是GD_SIZE值。内存结构如下图所示:
内存结构图
函数board_init_f_alloc_reserve是有返回值的,返回值为新的top值,此时top=0XC00FCF10(地址刚好是16字节对齐)。

继续回到之前的示例代码中,第107行,将r0写入到sp里面,r0保存着函数board_init_f_alloc_reserve的返回值,所以这一句也就是设置sp=0XC00FCF10

第109行,将r0寄存器的值写到寄存器r9里面,因为r9寄存器存放着全局变量gd的地址,在文件arch/arm/include/asm/global_data.h中有如下图所示宏定义:
DECLARE_GLOBAL_DATA_PTR宏定义
从上图可以看出,uboot中定义了一个指向gd_t结构体类型的指针:gd,gd存放在寄存器r9里的,因此gd是个全局变量。gd_t是个结构体,在include/asm-generic/global_data.h里面有定义,gd_定义如下:
示例代码12.2.3.3 global_data结构体
因此这一行代码就是设置gd所指向的位置,也就是gd指向0XC00FCF10

继续回到示例代码12.2.3.1中,第110行调用函数board_init_f_init_reserve,此函数在文件common/init/board_init.c中有定义,函数内容如下:
示例代码12.2.3.4 board_init_f_init_reserve函数
可以看出,此函数用于初始化gd,其实就是清零处理。另外,此函数还设置了gd->malloc_base为gd基地址+gd大小=0XC00FCF10+240=0XC00FD000,这个也就是early malloc的起始地址

继续回到上述示例代码中,第116行设置R0为0。

第117行,调用board_init_f函数,此函数定义在文件common/board_f.c中,主要用来初始化平台相关的API,定时器,完成代码拷贝等等,此函数后面在详细的分析。

第127行,重新设置环境(sp和gd),获取gd->start_addr_sp的值赋给sp,在函数board_init_f中会初始化gd的所有成员变量,最终gd->start_addr_sp=0XF3AEDD20,所以这里相当于设置sp=gd->start_addr_sp=0XF3AEDD20。 GD_START_ADDR_SP=80,之前有一个示例代码已经有过分析,这里不再赘述。

第128行,sp做 8字节对齐。

第130行,获取gd->new_gd的地址赋给r9,此时r9存放的是老的gd,这里通过获取gd->new_gd的地址来计算出新的gd的位置。 GD_BD=0,之前的示例代码已经有过分析。

第132行,设置lr寄存器为here,这样后面执行其他函数返回的时候就返回到了第122行的here位置处

第133行,读取gd->reloc_off的值复制给r0寄存器,GD_RELOC_OFF=84,参考之前的示例代码。

第134行,lr寄存器的值加上r0寄存器的值,重新赋值给lr寄存器。因为接下来要重定位代码,也就是把代码拷贝到新的地方去(现在的uboot存放的起始地址为0XC0100000,下面要将uboot拷贝到DDR最后面的地址空间出,将0XC0100000开始的内存空出来),其中就包括here,因此lr中的here要使用重定位后的位置。

第138行,读取gd->relocaddr的值赋给r0寄存器,此时r0寄存器就保存着uboot要拷贝的目的地址,为0XF5C46000。GD_RELOCADDR=40,参考之前的示例代码。

第139行,调用函数relocate_code,也就是代码重定位函数,此函数负责将uboot拷贝到新的地方去,此函数定义在文件arch/arm/lib/relocate.S中稍后会详细分析此函数。

继续回到之前的示例代码,第145行,调用函数relocate_vectors,对中断向量表做重定位,此函数定义在文件arch/arm/lib/relocate.S中,稍后会详细分析此函数。

继续回到示例代码12.2.3.1第149行,调用函数c_runtime_cpu_setup,此函数定义在文件arch/arm/cpu/armv7/start.S中 ,函数内容如下:
示例代码12.2.3.5 start.S代码段截图
第170行,设置函数board_init_r的两个参数,函数board_init_r声明如下:

board_init_r(gd_t *id, ulong dest_addr)

第一个参数是gd,因此读取r9保存到r0里面。

第171行,设置函数board_init_r的第二个参数是目的地址,因此r1=gd->relocaddr。

第177行、调用函数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函数主要有两个工作:

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

board_init_f函数定义在文件common/board_f.c中定义,代码如下:
示例代码12.2.4.1 board_f.c代码段截图
第1023行,初始化gd->flags=boot_flags。

第1024行,设置gd->have_console=0。

重点在第1026行!通过函数initcall_run_list来运行初始化序列init_sequence_f里面的一系列函数,init_sequence_f里面包含了一系列的初始化函数,init_sequence_f也定义在文件common/board_f.c中,由于init_sequence_f的内容比较长,里面有大量的条件编译代码,这里为了缩小篇幅,将条件编译部分删除掉了,去掉条件编译以后的init_sequence_f定义如下:

示例代码12.2.4.2 board_f.c代码段 
/*****************去掉条件编译语句后的 init_sequence_f***************/ 
1 static const init_fnc_t init_sequence_f[] = { 
2     setup_mon_len, 
3     fdtdec_setup, 
4     trace_early_init, 
5 
6     initf_malloc, 
7     log_init, 
8     initf_bootstage, /* uses its own timer, so does not need DM */ 
9     bloblist_init, 
10    setup_spl_handoff, 
11    initf_console_record, 
12    arch_fsp_init, 
13    arch_cpu_init, /* basic arch cpu dependent setup */ 
14    mach_cpu_init, /* SoC/machine dependent CPU setup */ 
15    initf_dm, 
16    arch_cpu_init_dm, 
17    board_early_init_f, 
18    get_clocks, /* get CPU and bus clocks (etc.) */ 
19    timer_init, /* initialize timer */ 
20    board_postclk_init, 
21    env_init, /* initialize environment */ 
22    init_baud_rate, /* initialze baudrate settings */ 
23    serial_init, /* serial communications setup */ 
24    console_init_f, /* stage 1 init of console */ 
25    display_options, /* say that we are here */ 
26    display_text_info, /* show debugging info if required */ 
27    checkcpu, 
28    print_resetinfo, 
29    print_cpuinfo, /* display cpu info (and speed) */ 
30    embedded_dtb_select, 
31    show_board_info, 
32    INIT_FUNC_WATCHDOG_INIT 
33    misc_init_f, 
34    INIT_FUNC_WATCHDOG_RESET 
35    init_func_i2c, 
36    init_func_vid, 
37    announce_dram_init, 
38    dram_init, /* configure available RAM banks */ 
39    post_init_f,
40    INIT_FUNC_WATCHDOG_RESET 
41    testdram, 
42    INIT_FUNC_WATCHDOG_RESET 
43    init_post, 
44    INIT_FUNC_WATCHDOG_RESET 
45    /* 
46    * Now that we have DRAM mapped and working, we can 
47    * relocate the code and continue running from DRAM. 
48    * 
49    * Reserve memory at end of RAM for (top down in that order): 
50    * - area that won't get touched by U-Boot and Linux (optional) 
51    * - kernel log buffer 
52    * - protected RAM 
53    * - LCD framebuffer 
54    * - monitor code 
55    * - board info struct 
56    */ 
57    setup_dest_addr, 
58    reserve_pram, 
59    reserve_round_4k, 
60    reserve_mmu, 
61    reserve_video, 
62    reserve_trace, 
63    reserve_uboot, 
64    reserve_malloc, 
65    reserve_board, 
66    setup_machine, 
67    reserve_global_data, 
68    reserve_fdt, 
69    reserve_bootstage, 
70    reserve_bloblist, 
71    reserve_arch, 
72    reserve_stacks, 
73    dram_init_banksize, 
74    show_dram_config, 
75    setup_board_part1, 
76    INIT_FUNC_WATCHDOG_RESET 
77    setup_board_part2, 
78    display_new_sp, 
79    fix_fdt, 
80    INIT_FUNC_WATCHDOG_RESET 
81    reloc_fdt, 
82    reloc_bootstage,
83    reloc_bloblist, 
84    setup_reloc, 
85    copy_uboot_to_ram, 
86    do_elf_reloc_fixups, 
87    clear_bss, 
88    clear_bss, 
89    jump_to_copy, 
90    NULL, 
91 };

接下来分析以上函数执行完以后的结果:

第2行,setup_mon_len函数设置gd的mon_len成员变量,此处为__bss_end-_start,也就是整个代码的长度。0XC01B966C-0XC0100000=0XB966C,这个就是代码长度

第3行,fdtdec_setup函数设置gd的fdt_dlob指针变量,fdt_blob保存着设备树(.dtb)文件地址。此处为_image_binary_end,也就是我们的设备地址为0XC01C2938

第4行,CONFIG_TRACE_EARLY宏没有定义不会运行此函数。

第6行,initf_malloc函数初始化gd中跟malloc有关的成员变量,比如malloc_limit,此函数会设置gd->malloc_limit =CONFIG_SYS_MALLOC_F_LEN=0X3000。 malloc_limit表示malloc内存池大小

第7行,log_init 函数将struct log_driver结构体加入到gd->log_head的循环链表中,并初始化gd->default_log_level。

第8行,initf_bootstage函数为gd->bootstage分配空间,并初始化gd->bootstage。

第9行,CONFIG_BLOBLIST宏没有定义,此函数不执行。

第10行,setup_spl_handoff函数和SPL相关,STM32MP1是使用TF-A引导U-BOOT,此函数啥也没有做。

第11行initf_console_record如果定义了宏CONFIG_CONSOLE_RECORD和宏CONFIG_SYS_MALLOC_F_LEN,此函数就会调用函数console_record_init,但是STM32MP1的uboot没有定义宏CONFIG_CONSOLE_RECORD,所以此函数直接返回0。

第12行,如果定义CONFIG_HAVE_FSP宏,就会调用arch_fsp_init函数。STM32MP1没有定义此宏。

第13行,arch_cpu_init函数,初始化架构相关的内容,CPU级别的操作。

第14行,mach_cpu_init函数,初始化SOC相关的内容,CPU级别的操作。

第15行,initf_dm函数,驱动模型有关的初始化操作, 如果定义了CONFIG_DM,则调用dm_init_and_scan初始化并扫描系统所有的device。如果定义了CONFIG_TIMER_EARLY调用dm_timer_init初始化driver model所需的timer。STM32MP1定义了CONFIG_DM,会扫描所有的device。

第16行,arch_cpu_init_dm函数为空函数。

第17行,如果定义CONFIG_BOARD_EARLY_INIT_F,则调用board_early_init_f接口,执行板级的early初始化。这边STM32MP1没有定义。

第18行 ,用于获取一些时钟值。

第19行,timer_init,初始化定时器。主要用来初始化gd->arch.timer_rate_hz,设置定时器频率。

第20行,CONFIG_BOARD_POSTCLK_INIT宏没有定义,所以函数不会调用。

第21行,env_init函数是和环境变量有关的,设置gd的成员变量env_addr,也就是环境变量的保存地址。

第22行,init_baud_rate函数用于初始化波特率,根据环境变量baudrate来初始化gd->baudrate。

第23行,serial_init,初始化串口,设置gd的flags成员变量为GD_FLG_SERIAL_READY开启当前选中的串口。

第24行,console_init_f,设置gd->have_console 为1,表示有个控制台,此函数也将前面暂存在缓冲区中的数据通过控制台打印出来。

第25行,display_options,通过串口输出一些信息,比如uboot版本号,编译时间等,如下图所示:
串口信息输出
第26行,display_text_info,打印一些文本信息,如果开启UBOOT的DEBUG功能的话就会输出text_base、bss_start、bss_end。开启DEBUG方法很简单,在对应的板子的配置头文件里面定义DEBUG即可,比如在stm32mp1.h文件中添加下面一行:

#define DEBUG //使能 DEBUG

使能以后重新编译uboot,然后使用新的uboot启动,这个时候会输出大量的debug信息,如下图所示:
文本信息
上图中“U-Boot code: C0100000->C01C66E8 BSS: -> C01D57B4”就是输出的一条debug信息,包含了uboot代码起始地址,BSS段起始地址。还有其他很多debug信息,不建议打开DEBUG,因为 debug信息太多了,严重影响正常开发!

第27行,PPC、X86、SH架构的芯片会执行,ARM架构不会执行

第28行,如果使能了CONFIG_SYSRESET宏的话,会打印一些系统复位信息。

第29行,print_cpuinfo函数用于打印CPU信息,结果如下图所示:
CPU信息
第30行,CONFIG_DTB_RESELECT宏没有定义所以此函数不执行。

第31行,show_board_info函数用于打印板子信息,如果在uboot中使用了设备树,那么此函数会先从设备树里面获取到model属性信息并且打印出出来。然后会调用checkboard函数获取板子信息并打印出来,结果如下图所示:
板子信息
第32行,INIT_FUNC_WATCHDOG_INIT,初始化看门狗 ,对于STM32MP1来说是空函数。

第33行,CONFIG_MISC_INIT_F宏没有定义,所以此函数不会执行。

第34行,INIT_FUNC_WATCHDOG_RESET,复位看门狗,对于STM32MP1来说是空函数。

第35行,CONFIG_SYS_I2C宏没有定义,所以此函数不会执行。

第36行,init_func_vid是一个空函数,返回值为0。

第37行,announce_dram_init,此函数很简单,就是输出字符串。

第38行,dram_init,并非真正的初始化DDR,只是设置gd->ram_size的值,对于正点原子STM32MP1开发板核来说,DDR大小为1G,所以gd->ram_size的值为0X40000000

第39行,CONFIG_POST宏没有定义,此函数不执行。

第41行,testdram,测试DRAM,空函数。

第43行,CONFIG_POST宏没有定义,此函数不执行。

第57行,setup_dest_addr函数,设置目的地址,设置gd->ram_base 、gd->ram_size、gd->ram_top、gd->relocaddr这三个的值。接下来我们会遇到很多跟数值有关的设置,如果直接看代码分析的话就太费时间了,可以修改uboot代码,直接将这些值通过串口打印出来,比如这里打开文件common/board_f.c,找到setup_dest_addr函数,在函数输入如下图所示内容:
添加printf函数打印成员变量值
设置好以后重新编译uboot,然后烧写到SD卡中,选择SD卡启动,重启开发板,打开MobaXterm,uboot会输出如下图所示信息:
信息输出
从上图可以看出:

gd->ram_base = 0XC0000000 //DDR起始地址 0XC0000000
gd->ram_size = 0X40000000 //DDR大小为 0X40000000=1G
gd->ram_top = 0XF6000000 //DDR最高地址为 0XF6000000
gd->relocaddr = 0XF6000000 //重定位后最高地址为 0XF6000000

这里有一些需要讲解,为什么ram_top(也就是DDR)最高地址是0XF6000000,不应该是0XC0000000+0X40000000 -1=0XFFFFFFFF。这是因为0XF6000000-0XFFFFFFFF这段内存分配
给了GPU和OPTEE,打开设备树arch/arm/dts/stm32mp157d-atk.dts文件,找到如下内容:
内存保留配置
结合上面两张图得知,rom最高为0XF600000,剩余的0XA000000预留给GPU和optee使用。其中0XF6000000-0XFDFFFFFF分配给了GPU,0XFE000000-0XFFFFFFFF分配给了OPTEE。

第59行,reserve_round_4k函数用于对gd->relocaddr做4KB对齐,因为gd->relocaddr=0XF6000000,已经是4K对齐了,所以调整后不变。

第60行,reserve_mmu,留出MMU的TLB表的位置,分配MMU的TLB表内存以后会对gd->relocaddr做64K字节对齐。完成以后gd->arch.tlb_size、gd->arch.tlb_addr和gd->relocaddr如下图所示:
信息输出
从上图可以看出:

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

第61行,reserve_video函数,留出显示的内存,分配显示内存后gd->relocaddr如如图所示:
信息输出
从上图可以知道,留出显示内存以后,gd->relocaddr=0XF5D00000,显示相关内存大小为0X2F0000。

第62行,reserve_trace函数,留出跟踪调试的内存,STM32MP1没有用到最终功能,因此无需留出对应的内存区域。

第63行,reserve_uboot留出重定位后的uboot所占用的内存区域,uboot所占用大小由gd->mon_len所指定,留出uboot的空间以后还要对gd->relocaddr做4K字节对齐,并且重新设置gd->start_addr_sp,结果如下图所示:
信息输出
从上图可以看出:

gd->mon_len = 0XB98EC
gd->start_addr_sp = 0XF5C46000
gd->relocaddr = 0XF5C46000

gd->mon_len是当前uboot大小,只要在uboot里面添加了其他代码,那么这个编译出来的大小是会变的,所以如果发现正点原子教程的其他地方mon_len不等于0XB98EC不要以为写错了。

第64行,reserve_malloc,留出malloc区域以及MMU相关内存。malloc区域由宏TOTAL_MALLOC_LEN定义,宏定义如下:

#define TOTAL_MALLOC_LEN (CONFIG_SYS_MALLOC_LEN+CONFIG_ENV_SIZE)

.config文件中定义宏CONFIG_SYS_MALLOC_LEN为32MB=0X2000000,宏CONFIG_ENV_SIZE=8KB=0X2000,因此TOTAL_MALLOC_LEN=0X2002000。此时gd->start_addr_sp=0XF5C46000-0X2002000=0XF3C44000。

reserve_malloc还通过reserve_noncached函数来预留出MMU相关内存。经过各种调整以后gd->start_addr_sp如下图所示:
信息输出
从上图可以看出,最终预留出的malloc和MMU相关内存范围为0XF5C46000~0XF3AFFFF,此时start_addr_sp变成了0XF3B00000。

第65行,reserve_board函数,留出板子bd所占的内存区,bd是结构体bd_t bd_t大小为80字节,结果如下图所示:
信息输出
从上图可以看出:

gd->bd=0XF3AFFFB0
gd->start_addr_sp=0XF3AFFFB0

第66行,setup_machine,设置机器ID,linux启动的时候会和这个机器ID匹配,如果匹配的话linux就会启动正常。但是!!STM32MP1不用这种方式了,这是以前老版本的uboot和linux使用的,新版本使用设备树了,因此此函数无效

第67行,reserve_global_data函数,保留出新的gd_t的内存区域,gd_t结构体大小为240B,结果如下图所示:
信息输出
从上图可以看出,此时新的gd地址为0XF3AFFEC0。

gd->start_addr_sp=0XF3AFFEC0 //0XF3AFFFB0-240=0XF3AFFEC0
gd->new_gd=0XF3AFFEC0

第68行,reserve_fdt函数,留出设备树相关的内存区域,首先计算出设备树的空间大小,计算公式如下:

gd->fdt_size = ALIGN(fdt_totalsize(gd->fdt_blob) + 0x1000, 32);

fdt_totalsize函数负责计算出设备树的大小gd->fdt_blob是代码重定位前设备树地址。旧
的地址大小加4KB,在以32字节对齐。reserve_fdt函数运行后的结果如下图所示:
信息输出
从上图可以看出:

gd->fdt_blob = 0XC01C2C50 //设备树的旧位置
gd->fdt_size= 0X11F00 //设备树.dtb大小为 0X11F00
gd->new_fdt= 0XF3AEDFC0 //0XF3AFFEC0-0X11F00 = 0XF3AEDFC0
gd->start_addr_sp = 0XF3AEDFC0

第69行,reserve_bootstage函数,保留bootstage的内存区域。结果如下图所示:
信息输出
从上图可以看出,新的bootstage地址为0XF3AEDD40。

第70行,此函数不执行。

第71行,reserve_arch是个空函数。

第72行,reserve_stacks,留出栈空间。如果使能IRQ的话还要留出IRQ相应的内存,具体工作是由arch/arm/lib/stack.c文件中的函数arch_reserve_stacks完成。结果如下图所
示:
信息输出
至此,uboot内存重定位完成。

第73行,dram_init_banksize函数设置dram信息,就是设置gd->bd->bi_dram[0].start和gd->bd->bi_dram[0].size,后面会传递给linux内核,告诉linux DRAM的起始地址和大小。结果如下图所示:
信息输出
从上图可以看出,DRAM的起始地址为0XC0000000,大小为0X40000000(1G)。

第74行,show_dram_config函数,用于显示DRAM的配置,如下图所示:
信息输出
第78行,display_new_sp函数,显示新的sp位置,也就是gd->start_addr_sp,不过要定义宏DEBUG,结果如下图所示:
信息输出
从上图中的gd->start_addr_sp值和我们前面分析的最后一次修改的值一致。

第81行,reloc_fdt函数用于重定位fdt,使用memcpy函数将原来存储在gd->fdt_blob中的设备树文件拷贝到gd->new_fdt处,然后将gd->new_fdt赋值给gd->fdt_blob。经过这一步设备树拷贝到了新地址,而且gd->fdt_blob也保存了新的设备树地址。

第82行,reloc_bootstage函数用于bootstage重定位,使用memcpy函数将gd->bootstage中原来的bootstage拷贝到新的gd->new_bootstage处。然后将gd->new_bootstage赋值给gd->bootstage。

第84行,setup_reloc,设置gd的其他一些成员变量,供后面重定位的时候使用,并且将以前的gd拷贝到gd->new_gd处。需要使能DEBUG才能看到相应的信息输出,如下图所示:
信息输出
从上图可以看出,uboot重定位后的偏移为0X35b46000,重定位后的新地址为0XF5C46000,新的gd首地址为0XF3AFFEC0,最终的sp为0XF3AEDD20。

至此,board_init_f函数就执行完成了,最终的内存分配如下图所示:
最终的内存分配图

relocate_code函数详解

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

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

第81行, r1=__image_copy_start,也就是r1寄存器保存源地址,由之前的图可知,__image_copy_start=0XC0100000。

第82行,r0=0XF5C46000,这个地址就是uboot拷贝的目标首地址。r4=r0-r1=0XF5C46000-0XC0100000=0X35B46000,因此r4保存偏移量。

第83行,如果在第81行中,r0-r1等于 0,说明r0和r1相等,也就是源地址和目的地址是一样的,那肯定就不需要拷贝了!执行relocate_done函数。

第84行,r2=__image_copy_end,r2中保存拷贝之前的代码结束地址,由最开始的白变量截图可知,__image_copy_end =0XC01ACD98

第85行,函数copy_loop完成代码拷贝工作!从r1,也就是__image_copy_start开始,读取uboot代码保存到r10和r11中,一次就只拷贝这2个32位的数据。拷贝完成以后r1的值会更新,保存下一个要拷贝的数据地址。

第88行,将r10和r11的数据写到r0开始的地方,也就是目的地址。写完以后r0的值会更新,更新为下一个要写入的数据地址。

第89行,比较r1是否和r2相等,也就是检查是否拷贝完成,如果不相等的话说明没有拷贝完成,没有拷贝完成的话就跳转到copy_loop接着拷贝,直至拷贝完成。

接下来的第95行-110行是重定位.rel.dyn段,.rel.dyn是.relocation.dynamic的简写,也就是动态重定位,.rel.dyn段是存放.text段中需要重定位地址的集合。重定位就是uboot将自身拷贝到DRAM的另一个地放去继续运行(DRAM的高地址处)。我们知道,一个可执行的bin文件,其链接地址和运行地址要相等,也就是链接到哪个地址,在运行之前就要拷贝到哪个地址去。现在我们重定位以后,运行地址就和链接地址不同了,这样寻址的时候不会出问题吗?为了分析这个问题,需要在board/st/stm32mp1/stm32mp1.c中输入如下所示内容:

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

最后还需要在stm32mp1.c文件中的board_init函数里面调用rel_test函数,否则rel_reset不会被编译进uboot。修改完成后的stm32mp1.c如下图所示:
加入rel测试相关代码
board_init函数会调用rel_test,rel_test会调用全局变量rel_a。

重新编译uboot,编译完成以后,使用arm-none-inux-gnueabihf-objdump将编译出来的u-boot进行反汇编,得到u-boot.dis这个汇编文件,命令如下:

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

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

示例代码12.2.5.2 汇编文件代码段 
1 c010619c <rel_test>: 
2 c010619c: 4b03 ldr r3, [pc, #12] ; (c01061ac <rel_test+0x10>) 
3 c010619e: 4804 ldr r0, [pc, #16] ; (c01061b0 <rel_test+0x14>) 
4 c01061a0: 6819 ldr r1, [r3, #0] 
5 c01061a2: 3164 adds r1, #100 ; 0x64 
6 c01061a4: 6019 str r1, [r3, #0] 
7 c01061a6: f06d bbe6 b.w c0173976 <printf> 
8 c01061aa: bf00 nop 
9 c01061ac: c01ad378 andsgt sp, sl, r8, ror r3 
10 c01061b0: c018de67 andsgt sp, r8, r7, ror #28 
11 
12 c01061b4 <board_init>: 
13 c01061b4: b530 push {r4, r5, lr} 
14 c01061b6: b089 sub sp, #36 ; 0x24 
15 c01061b8: f7ff fff0 bl c010619c <rel_test> 
16 
17 ...... 
18 
19 c01ad378 <rel_a>: 
20 c01ad378: 00000000 andeq r0, r0, r0

第15行是borad_init调用rel_test函数,用到了bl指令,而bl指令是位置无关指令,bl指令是相对寻址的(pc+offset),因此uboot中函数调用是与绝对位置无关的。

再来看一下函数rel_test对于全局变量rel_a的调用,第2行设置r3的值为pc+12地址处的值,根据后面的提示,这个地址就是rel_test+0X10,也就是0XC010619C+0X10=0XC01061AC。第9行就是0XC01061AC这个地址,0XC01061AC处的值为0XC01AD378。根据第19行可知,0XC01AD378正是变量rel_a的地址,最终r3=0XC01AD378。

第4行,读取r3地址出的值,保存到r1寄存器面,也就是读取rel_a的值保存到r1里面。

第5行,给r1里面的值加100,也就是给rel_a的值加100。

第6行,将r1内的值写到r3地址处,这就是示例代码代码12.2.5.1中的第5行:rel_a += 100。

总结一下rel_a+=100的汇编执行过程:

  1. 在函数rel_test末尾处有一个地址为0XC01061AC的内存空间 (示例代码12.2.5.2第7行),此内存空间保存着变量rel_a的地址;
  2. 函数rel_test要想访问变量rel_a,首先访问末尾的0XC01061AC来获取变量rel_a的地址,而访问0XC01061AC是通过偏移来访问的,很明显是个位置无关的操作;
  3. 通过0XC01061AC获取到变量rel_a的地址,对变量rel_a进行操作;
  4. 可以看出,函数rel_test对变量rel_a的访问没有直接进行,而是使用了一个第三方偏移地址0XC01061AC,专业术语叫做Label。这个第三方偏移地址就是实现重定位后运行不会出错的重要原因!

uboot重定位后偏移为0X35B46000,那么重定位后函数rel_test的首地址就是0XC010619C+0X35B46000=0XF5C4C19C保存变量rel_a地址的Label就是0XF5C4C19+0X10=0XF5C4C1AC变量rel_a的地址就为0XC01AD378+0X35B46000=0XF5CF3378重定位后函数rel_test要想正常访问变量rel_a就得设置 0XF5C4C1AC(重定位后的 Label)地址处的值为0XF5CF3378 (重定位后的变量 rel_a地址)。这样就解决了重定位后链接地址和运行地址不一致的问题。

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

示例代码12.2.5.3 汇编文件代码段 
102 # needed for relocation 
103 LDFLAGS_u-boot += -pie

第103行就是设置uboot链接选项,加入了“-pie”选项。编译链接 uboot 的时候会使用到“-pie”选项,如下图所示:
链接命令
使用“-pie”选项以后会生成一个.rel.dyn段,uboot就是靠这个.rel.dyn来解决重定位问题的,在u-bot.dis的.rel.dyn段中有如下所示内容::

示例代码12.2.5.4 rel.dyn代码段 
1 Disassembly of section .rel.dyn: 
2 
3 c01ad2d8 <__efi_runtime_rel_stop>: 
4 c01ad2d8: c0100020 andsgt r0, r0, r0, lsr #32 
5 c01ad2dc: 00000017 andeq r0, r0, r7, lsl r0 
6 ......
7 c01ade08: c01061ac andsgt r6, r0, ip, lsr #3 
8 c01ade0c: 00000017 andeq r0, r0, r7, lsl r0 
9 c01ade10: c01061b0 ; <UNDEFINED> instruction: 0xc01061b0 
10 c01ade14: 00000017 andeq r0, r0, r7, lsl r0

先来看一下.rel.dyn段的格式,类似第7行和第8行这样的是一组,也就是两个4字节数据
为一组
高4字节(第7行的0XC01ADE0C)是Label地址标识,值为0X17。低4字节(第7行的0XC01ADE08)就是Label的地址。首先判断Label地址标识是否正确,也就是判断高4字节是否为0X17,如果是的话低4字节就是Label地址值。第7行值为0XC01061AC,第8行为0X00000017,说明第7行的0XC01061AC是个 Label。

继续返回示例代码12.2.5.1中。
第95行,r2=__rel_dyn_start,也就是.rel.dyn段的起始地址。

第96行,r3=__rel_dyn_end,也就是.rel.dyn段的终止地址。

第97行,从.rel.dyn段起始地址开始,每次读取两个4字节的数据存放到r0和r1寄存器中,r0存放低4字节的数据,也就是Label地址;r1存放高4字节的数据,也就是Label标志。

第99行,r1中给的值与0xff进行与运算,其实就是取r1的低8位。

第100行,判断r1中的值是否等于23(R_ARM_RELATIVE经过编译以后为23)。

第101行,如果r1不等于23的话就说明不是描述Label的,执行函数fixnext,否则的话继续执行下面的代码。

第104行,r0保存着 Label值,r4保存着重定位后的地址偏移,r0+r4就得到了重定位后的Label值。此时r0保存着重定位后的Label值,相当于0XC01006F0+0X35B46000=0XF5C466F0。

第105行,读取重定位后Label所保存的变量地址,此时这个变量地址还是重定位前的(相当于rel_a重定位前的地址0XC01006F0),将得到的值放到r1寄存器中。

第106行,r1+r4即可得到重定位后的变量地址,相当于rel_a重定位后的0XC01061AC+0X35B46000=0XF5C4C1AC。

第107行,重定位后的变量地址写入到重定位后的Label中,相等于设置地址0XC01061AC处的值为0XF5C4C1AC。

第109行,比较r2和r3,查看.rel.dyn段重定位是否完成。

第110行,如果r2和r3不相等,说明.rel.dyn重定位还未完成,因此跳到fixloop继续重定位.rel.dyn段。

可以看出,uboot中对.rel.dyn段的重定位方法和猜想的一致。.rel.dyn段的重定位比较复杂一点,有点绕,因为涉及到链接地址和运行地址的问题

relocate_vectors函数详解

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

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

第30行,如果定义了CONFIG_CPU_V7M的话就执行第30-36行的代码,这是Cortex-M内核单片机执行的语句,因此对于STM32MP157来说是无效的。

第39行,如果定义了CONFIG_HAS_VBAR的话就执行此语句,这个是向量表偏移,Cortex-A7是支持向量表偏移的。而且,在.config里面定义了CONFIG_HAS_VBAR,因此会执行这个分支。

第44行,r0=gd->relocaddr,也就是重定位后uboot的首地址,向量表肯定是从这个地址开始存放的。

第45行,将r0的值写入到CP15的VBAR寄存器中,也就是将新的向量表首地址写入到寄存器VBAR中,设置向量表偏移。

board_init_r函数详解

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

示例代码12.2.7.1 board_init_r代码段 
844 void board_init_r(gd_t *new_gd, ulong dest_addr) 
845 { 
...... 
869 
870    if (initcall_run_list(init_sequence_r)) 
871    	   hang(); 
872 
873     /* NOTREACHED - run_main_loop() does not return */ 
874     hang(); 
875 }

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

示例代码12.2.7.2 init_sequence_r结构体 
1 static init_fnc_t init_sequence_r[] = { 
2     initr_trace, 
3     initr_reloc, 
4     initr_caches, 
5     initr_reloc_global_data, 
6     initr_unlock_ram_in_cache, 
7     initr_barrier, 
8     initr_malloc, 
9     log_init, 
10    initr_bootstage, /* Needs malloc() but has its own timer */ 
11    initr_console_record, 
12    initr_noncached, 
13    initr_of_live, 
14    initr_dm, 
15    board_init, /* Setup chipselects */ 
16    set_cpu_clk_info, /* Setup clock information */ 
17    efi_memory_init, 
18    stdio_init_tables, 
19    initr_serial, 
20    initr_announce, 
21    initr_watchdog, 
22    INIT_FUNC_WATCHDOG_RESET 
23    initr_manual_reloc_cmdtable, 
24    initr_trap,
25    initr_addr_map, 
26    board_early_init_r, 
27    INIT_FUNC_WATCHDOG_RESET 
28    initr_post_backlog, 
29    INIT_FUNC_WATCHDOG_RESET 
30    initr_pci, 
31    arch_early_init_r, 
32    power_init_board, 
33    initr_flash, 
34    INIT_FUNC_WATCHDOG_RESET 
35    cpu_init_r, 
36    initr_nand, 
37    initr_onenand, 
38    initr_mmc, 
39    initr_env, 
40    initr_malloc_bootparams, 
41    INIT_FUNC_WATCHDOG_RESET 
42    initr_secondary_cpu, 
43    mac_read_from_eeprom, 
44    INIT_FUNC_WATCHDOG_RESET 
45    initr_pci, 
46    stdio_add_devices, 
47    initr_jumptable, 
48    initr_api, 
49    console_init_r, /* fully init console as a device */ 
50    console_announce_r, 
51    show_board_info, 
52    arch_misc_init, /* miscellaneous arch-dependent init */ 
53    misc_init_r, /* miscellaneous platform-dependent init */ 
54    INIT_FUNC_WATCHDOG_RESET 
55    initr_kgdb, 
56    interrupt_init, 
57    initr_enable_interrupts, 
58    timer_init, /* initialize timer */ 
59    initr_status_led, 
60    initr_ethaddr, 
61    gpio_hog_probe_all, 
62    board_late_init, 
63    INIT_FUNC_WATCHDOG_RESET 
64    initr_scsi, 
65    initr_bbmii, 
66    INIT_FUNC_WATCHDOG_RESET 
67    initr_net,
68    initr_post, 
69    initr_ide, 
70    INIT_FUNC_WATCHDOG_RESET 
71    last_stage_init, 
72    INIT_FUNC_WATCHDOG_RESET 
73    initr_bedbug, 
74    initr_mem, 
75    run_main_loop, 
76 };

第2行,initr_trace函数,如果定义了宏CONFIG_TRACE的话就会调用函数trace_init
初始化和调试跟踪有关的内容。

第3行,initr_reloc函数用于设置gd->flags,标记重定位完成。

第4行,initr_caches函数用于初始化cache,使能 cache。

第5行,initr_reloc_global_data函数,初始化重定位后gd的一些成员变量。

第6行,CONFIG_SYS_INIT_RAM_LOCK宏没有定义,因此函数不执行。

第7行,CONFIG_PPC宏没有定义,因此函数不执行。

第8行,initr_malloc函数,初始化malloc。

第9行,空函数。

第10行,初始化bootstage相关的内容。

第11行,初始化控制台相关内容。

第12行,保留MMU相关的1M内存。

第13行,CONFIG_OF_LIVE宏没有定义,此函数不会运行。

第14行,初始化设备树,主要是解析设备树,创建一个树形结构,根节点挂载在gd->dm_root。

第15行,board_init函数,板级初始化,包括时钟、电压。这里执行的是board/st/stm32mp1/stm32mp1.c文件中的board_init函数。

第16行,CONFIG_CLOCKS宏没有定义,此函数不执行。

第17行,efi相关初始化。

第18行,stdio_init_tables函数,stdio相关初始化。

第19行,initr_serial函数,初始化串口。

第20行,initr_announce函数,与调试有关,通知已经在RAM中运行。

第21行,初始化看门狗,串口输出如下图所示:
看门狗信息输出
第23行,CONFIG_NEEDS_MANUAL_RELOC宏没有定义,此函数不执行。

第24行,此函数不执行。

第25行,CONFIG_ADDR_MAP没有定义,此函数不执行。

第26行,CONFIG_BOARD_EARLY_INIT_R宏没有定义,此函数不执行。

第28行,CONFIG_POST宏没有定义,此函数不执行。

第30行,CONFIG_PCI宏没有定义,此函数不执行。

第31行,CONFIG_ARCH_EARLY_INIT_R宏没有定义,此函数不执行。

第32行,初始化电源,此函数为空函数。

第33行,CONFIG_MTD_NOR_FLASH宏没有定义,此函数不执行。

第35行,此函数不执行。

第36行,initr_nand函数,初始化NAND Flash,STM32MP1支持NAND,并且也定义了CONFIG_CMD_NAND宏,因此此函数会执行。正点原子STM32MP1开发板没有使用NAND,因此识别出来NAND为0MiB,如下图所示:
NAND信息输出
第37行,CONFIG_CMD_ONENAND宏没有定义,此函数不执行。

第38行,initr_mmc函数,初始化EMMC,串口输出如下图所示信息:
EMMC信息输出
从上图可以看出,此时有两个EMCM设备,MMC:0和MMC:1。

第39行,initr_env函数,初始化环境变量,如果在 EMMC中保存了环境变量,那么就会提示从EMMC中读取环境变量,如下图所示:
从EMMC中读取环境变量
如果EMMC中没有保存环境变量,那么就会输出“bad CRC”类的信息。

第40行,CONFIG_SYS_BOOTPARAMS_LEN宏没有定义,此函数不执行。

第42行,initr_secondary_cpu函数,初始化其他 CPU核。

第43行,CONFIG_ID_EEPROM宏没有定义,此函数不执行。

第46行,stdio_add_devices函数,各种输入输出设备的初始化。

第47行,initr_jumptable函数,初始化跳转表。

第48行,CONFIG_API宏没有定义,此函数不执行。

第49行,console_init_r函数,控制台初始化,初始化完成以后此函数会调用stdio_print_current_devices函数来打印出当前的控制台设备,如下图所示:
控制台信息
第50-51行,CONFIG_DISPLAY_BOARDINFO_LATE宏没有定义,这两个函数不执行。

第52行,arch_misc_init函数,设置一些其他的东西,函数的路径为:arch/arm/mach-stm32mp/cpu.c。这里重点是从STM32MP1的OTP里面读取MAC地址来设置“ethaddr”环境变量,信息如下图所示:
从OTP获取MAC地址
从上图可以看出,从OTP读取到的MAC地址是错误的,因为我们没有在OTP里面设置MAC地址。

第53行,CONFIG_MISC_INIT_R宏没有定义,此函数不执行。

第55行,CONFIG_CMD_KGDB宏没有定义,此函数不执行。

第56行,interrupt_init函数,初始化中断。

第57行,initr_enable_interrupts函数,使能中断。

第58-59行,函数不执行。

第60行,根据bd->bi_enetaddr变量成员,去设置ethaddr的MAC地址。

第61行,gpio_hog_orobe_all函数,初始化uboot下的gpio驱动框架。

第62行,board_late_init函数,板子后续初始化,此函数定义在文件board/st/stm32mp1/stm32mp1.c中。

第64-65行,此函数不执行。

第67行,网络的初始化,输出的信息如下图所示:
网络初始化
上图中由于没有设置网络相关地址信息,因此网络初始化失败, 提示“No ethernet found”。如果设置了网络地址环境变量,那么就会初始化成功网络,输出信息如下:
网络初始化成功
第68-74行,这里的函数都不执行。

第75行,run_main_loop行,主循环,处理命令。

run_main_loop函数详解

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

示例代码12.2.8.1 run_main_loop函数 
645 static int run_main_loop(void) 
646 { 
647 #ifdef CONFIG_SANDBOX 
648     sandbox_main_loop_init(); 
649 #endif 
650     /* main_loop() can return to retry autoboot, if so just run it again */ 
651     for (;;) 
652         main_loop(); 
653     return 0; 
654 }

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

示例代码12.2.8.2 main_loop函数 
41 void main_loop(void) 
42 { 
43     const char *s; 
44 
45     bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");
46 
47     if (IS_ENABLED(CONFIG_VERSION_VARIABLE)) 48         env_set("ver", version_string); /* set version variable */ 
49 
50     cli_init(); 
51 
52     if (IS_ENABLED(CONFIG_USE_PREBOOT)) 
53         run_preboot_environment_command(); 
54 
55     if (IS_ENABLED(CONFIG_UPDATE_TFTP)) 
56         update_tftp(0UL, NULL, NULL); 
57 
58     s = bootdelay_process(); 
59     if (cli_process_fdt(&s)) 
60         cli_secure_boot_cmd(s); 
61 
62     autoboot_command(s); 
63 
64     cli_loop(); 
65     panic("No CLI available"); 
66 }

第45行,调用bootstage_mark_name函数,打印出启动进度。

第47行,如果定义了宏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内容如下:

示例代码12.2.8.3 version_autogenerated.h文件代码 
1 #define PLAIN_VERSION "2020.01-stm32mp-r1" 
2 #define U_BOOT_VERSION "U-Boot " PLAIN_VERSION 
3 #define CC_VERSION_STRING "arm-none-linux-gnueabihf-gcc (GNU Toolchain for the A-profile Architecture 9.2-2019.12 (arm-9.10)) 9.2.1 20191025" 
4 #define LD_VERSION_STRING "GNU ld (GNU Toolchain for the A-profile Architecture 9.2-2019.12 (arm-9.10)) 2.33.1.20191209"

可以看出,U_BOOT_VERSION为“U-boot 2020.01-stm32mp-r1”,U_BOOT_DATE、U_BOOT_TIME和U_BOOT_TZ这定义在文件include/generated/timestamp_autogenerated.h中,如下所示:

示例代码12.2.8.4 timestamp_autogenerated.h文件代码 
1 #define U_BOOT_DATE "Oct 25 2021"
2 #define U_BOOT_TIME "16:07:12" 
3 #define U_BOOT_TZ "+0800" 
4 #define U_BOOT_DMI_DATE "10/25/2021" 
5 #define U_BOOT_BUILD_DATE 0x20211025

宏CONFIG_IDENT_STRING为空,所以U_BOOT_VERSION_STRING为“U-Boot 2020.01-stm32mp-r1(Jun 24 2021 - 18:05:23 +0800)”,进入uboot命令模式,输入命令“version”查看版本号,如下图所示:
版本查询
上图中的第一行就是uboot版本号,和我们分析的一致。

接着回到示例代码12.2.8.2中,第50行,cli_init函数,跟命令初始化有关,初始化hush shell相关的变量。

第53行,run_preboot_environment_command函数,获取环境变量perboot的内容,perboot是一些预启动命令,一般不使用这个环境变量。

第58行,bootdelay_process函数,此函数会读取环境变量bootdelay和bootcmd的内容,然后将bootdelay的值赋值给全局变量stored_bootdelay,返回值为环境变量bootcmd的值。

第59行,如果定义了CONFIG_OF_CONTROL的话函数cli_process_fdt执行。

第62行,autoboot_command函数,此函数就是检查倒计时是否结束?倒计时结束之前有没有被打断?此函数定义在文件common/autoboot.c中,内容如下:

示例代码12.2.8.5 autoboot_command函数 
359 void autoboot_command(const char *s) 
360 { 
361 	debug("### main_loop: bootcmd=\"%s\"\n", s ? s : "<UNDEFINED>"); 
362 
363 if (stored_bootdelay != -1 && s && !abortboot(stored_bootdelay)) { 
364 	bool lock; 
365 	int prev; 
366 
367 	lock = IS_ENABLED(CONFIG_AUTOBOOT_KEYED) && 
368 		!IS_ENABLED(CONFIG_AUTOBOOT_KEYED_CTRLC); 
369 	if (lock) 
370 		prev = disable_ctrlc(1); /* disable Ctrl-C checking */ 
371 
372 	run_command_list(s, -1, 0); 
373 
374 	if (lock) 
375 		disable_ctrlc(prev); /* restore Ctrl-C checking */ 
376 	} 
377 
378 	if (IS_ENABLED(CONFIG_USE_AUTOBOOT_MENUKEY) &&
379 	menukey == AUTOBOOT_MENUKEY) { 
380 	s = env_get("menucmd"); 
381 	if (s) 
382 		run_command_list(s, -1, 0); 
383 	} 
384 }

可以看出,autoboot_command函数里面有很多条件编译,条件编译一多就不利于我们阅读程序。宏CONFIG_AUTOBOOT_KEYED、CONFIG_AUTOBOOT_KEYED_CTRLC和CONFIG_MENUKEY这三个宏在STM32MP1里面没有定义,所以将示例代码12.2.8.6进行精简,得到如下代码:

示例代码12.2.8.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。

  1. stored_bootdelay不等于-1;
  2. s不为空;
  3. 函数 abortboot返回值为0。

stored_bootdelay等于环境变量bootdelay的值;s是环境变量bootcmd的值,一般不为空,因此前两个成立,就剩下了函数abortboot的返回值,abortboot函数也定义在文件common/autoboot.c中,内容如下:
示例代码12.2.8.7 abortboot函数截图
因为宏CONFIG_AUTOBOOT_KEYED未定义,因此执行函数abortboot_single_key。接着来看函数abortboot_single_key,此函数也定义在文件common/autoboot.c中,内容如下:

示例代码12.2.8.8 abortboot_single_key函数 
246 static int abortboot_single_key(int bootdelay) 
247 { 
248     int abort = 0; 
249     unsigned long ts; 
250 
251     printf("Hit any key to stop autoboot: %2d ", bootdelay); 
252 
253     /* 
254     * Check if key already pressed 
255     */ 
256     if (tstc()) { /* we got a key press */ 
257         (void) getc(); /* consume input */ 
258         puts("\b\b\b 0"); 
259         abort = 1; /* don't auto boot */ 
260     } 
261 
262     while ((bootdelay > 0) && (!abort)) { 
263         --bootdelay; 
264         /* delay 1000 ms */ 
265         ts = get_timer(0); 
266         do { 
267             if (tstc()) { /* we got a key press */ 
268                 int key; 
269 
270                 abort = 1; /* don't auto boot */ 
271                 bootdelay = 0; /* no more delay */ 
272                 key = getc(); /* consume input */ 
273                 if (IS_ENABLED(CONFIG_USE_AUTOBOOT_MENUKEY)) 
274                     menukey = key; 
275                 break; 
276         } 
277         udelay(10000); 
278     } while (!abort && get_timer(ts) < 1000); 
279 
280     printf("\b\b\b%2d ", bootdelay); 
281 } 
282 
283     putc('\n'); 
284 
285     return abort;

第248行的变量abort是函数abortboot_normal的返回值,默认值为0。

第251行通过串口输出“Hit any key to stop autoboot”字样,如下图所示:
倒计时
第262-278行就是倒计时的具体实现。

第267行判断键盘是否有按下,也就是是否打断了倒计时,如果键盘按下的话就执行相应的分支。比如设置abort为1,设置bootdelay为0等,最后跳出倒计时循环。

第285行,返回abort的值,如果倒计时自然结束,没有被打断abort就为0,否则的话abort的值就为1。

回到示例代码12.2.8.6的autoboot_command函数中,如果倒计时自然结束那么就执行函数run_command_list,此函数会执行参数s指定的一系列命令,也就是环境变量bootcmd的命令,bootcmd里面保存着默认的启动命令,因此linux内核启动!这个就是uboot中倒计时结束以后自动启动linux内核的原理。如果倒计时结束之前按下了键盘上的按键,那么run_command_list函数就不会执行,相当于autoboot_command是个空函数。

回到示例代码12.2.8.2中的main_loop函数中,如果倒计时结束之前按下按键,那么就会执行第64行的cli_loop函数,这个就是命令处理函数,负责接收好处理输入的命令。

此函数定义在文件common/cli.c 中,具体内容不是很重要,跳过就行。

cli_loop函数详解

cli_loop函数是uboot的命令行处理函数,在uboot中输入各种命令,进行各种操作就是有cli_loop来处理的,此函数定义在文件common/cli.c中,函数内容如下:
示例代码12.2.9.1 cli.c代码段截图
第220行,STM32MP157定义了宏CONFIG_SYS_HUSH_PARSER。因此会调用函数parse_file_outer。

第222行是个死循环,永远不会执行到这里。

函数parse_file_outer定义在文件common/cli_hush.c中,去掉条件编译内容以后的函数内容如下:

示例代码12.2.9.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中,精简版的函数内容如下:
示例代码12.2.9.3 parse_stream_outer函数代码截图
第9-23行中的do-while循环就是处理输入命令的。

第11行调用函数parse_stream进行命令解析。

第16行调用run_list函数来执行解析出来的命令。

函数run_list会经过一系列的函数调用,最终通过调用cmd_process函数来处理命令,过程如下:

示例代码12.2.9.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 rcode=0, flag_skip=1; 
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
41     return -1; 
42 }

第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中,定义如下:
示例代码12.2.10.1 U_BOOT_CMD宏定义截图
可以看出U_BOOT_CMD是U_BOOT_CMD_COMPLETE的特例,将U_BOOT_CMD_COMPLETE的最后一个参数设置成NULL就是U_BOOT_CMD。宏U_BOOT_CMD_COMPLETE如下:
示例代码12.2.10.2 U_BOOT_CMD_COMPLETE宏定义截图
宏U_BOOT_CMD_COMPLETE又用到了ll_entry_declare和U_BOOT_CMD_MKENT_COMPLETE。ll_entry_declar定义在文件include/linker_lists.h中,定义如下:
示例代码12.2.10.3 II_entry_declare宏定义截图
_type为cmd_tbl_t,因此ll_entry_declare就是定义了一个cmd_tbl_t变量,这里用到了C语言中的“##”连接符。其中的“##_list”表示用_list的值来替换,“##_name”就是用_name的值来替换。

宏U_BOOT_CMD_MKENT_COMPLETE定义在文件include/command.h中,内容如下:
示例代码12.2.10.4 U_BOOT_CMD_MKENT_COMPLETE宏定义截图
上述代码中的“#”表示将_name传递过来的值字符串化,U_BOOT_CMD_MKENT_COMPLETE又用到了宏_CMD_HELP和_CMD_COMPLETE,这两个宏的定义如下:
示例代码12.2.10.5 _CMD_HELP和_CMD_COMPLETE宏定义截图
可以看出,如果定义了宏CONFIG_AUTO_COMPLETE和CONFIG_SYS_LONGHELP的
话,_CMD_COMPLETE和_CMD_HELP就是取自身的值,然后在加上一个‘,’。CONFIG_AUTO_COMPLETE和CONFIG_SYS_LONGHELP这两个宏都有定义。

U_BOOT_CMD宏的流程我们已经清楚了,就以一个具体的命令为例,来看一下U_BOOT_CMD经过展开以后究竟是个什么模样的。以命令dhcp为例,dhcp命令定义在 cmd/net.c文件中,内容如下:
示例代码12.2.10.6 dhcp命令宏定义截图
将其展开,结果如下:

示例代码12.2.10.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}

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

示例代码12.2.10.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中,如下图所示:
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中,内容如下:

示例代码12.2.10.9 cmd_tbl_t结构体 
30 struct cmd_tbl_s { 
31     char *name; /* Command Name */ 
32     int maxargs; /* maximum number of arguments */ 
...... 
41     int (*cmd_rep)(struct cmd_tbl_s *cmd, int flags, int argc, 
42             char * const argv[], int *repeatable); 
43                         /* Implementation function */ 
44     int (*cmd)(struct cmd_tbl_s *, int, int, char * const []); 
45     char *usage; /* Usage message (short) */ 
46     #ifdef CONFIG_SYS_LONGHELP 
47     char *help; /* Help message (long) */
48     #endif 
49     #ifdef CONFIG_AUTO_COMPLETE 
50     /* do auto completion on the arguments */ 
51     int (*complete)(int argc, char * const argv[], char last_char, int maxv, char *cmdv[]); 
52     #endif 
53 }; 
54 
55 typedef struct cmd_tbl_s cmd_tbl_t;

结合示例代码12.2.10.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. cmd_rep = cmd_always_repeatable
_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中,函数内容如下:

示例代码12.2.10.10 command.c文件代码段 
582 enum command_ret_t cmd_process(int flag, int argc, 
583       char * const argv[], int *repeatable, ulong *ticks) 
584 { 
585     enum command_ret_t rc = CMD_RET_SUCCESS; 
586     cmd_tbl_t *cmdtp; 
...... 
602     /* Look up command in command table */ 
603     cmdtp = find_cmd(argv[0]); 
604     if (cmdtp == NULL) { 
605         printf("Unknown command '%s' - try 'help'\n", argv[0]); 
606         return 1; 
607 } 
608 
609 /* found - check max args */ 
610 if (argc > cmdtp->maxargs) 
611     rc = CMD_RET_USAGE; 
612
613 #if defined(CONFIG_CMD_BOOTD) 
614     /* avoid "bootd" recursion */ 
615     else if (cmdtp->cmd == do_bootd) { 
616         if (flag & CMD_FLAG_BOOTD) { 
617         puts("'bootd' recursion detected\n"); 
618         rc = CMD_RET_FAILURE; 
619     } else { 
620         flag |= CMD_FLAG_BOOTD; 
621     } 
622 } 
623 #endif 
624 
625 /* If OK so far, then do the command */ 
626 if (!rc) { 
627     int newrep; 
628 
629     if (ticks) 
630         *ticks = get_timer(0); 
631     rc = cmd_call(cmdtp, flag, argc, argv, &newrep); 
632     if (ticks) 
633         *ticks = get_timer(*ticks); 
634     *repeatable &= newrep; 
635     } 
636     if (rc == CMD_RET_USAGE) 
637         rc = cmd_usage(cmdtp); 
638     return rc; 
639 }

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

回到示例代码12.2.10.10的cmd_process函数中,找到命令以后肯定就要执行这个命令了。第631行调用函数cmd_call来执行具体的命令,cmd_call函数内容如下:

示例代码12.2.10.12 command.c文件代码段
571 static int cmd_call(cmd_tbl_t *cmdtp, int flag, int argc, 
572             char * const argv[], int *repeatable) 
573 { 
574     int result; 
575 
576     result = cmdtp->cmd_rep(cmdtp, flag, argc, argv, repeatable); 
577     if (result) 
578         debug("Command failed, result=%d\n", result); 
579     return result; 
580 }

第576行,调用cmd_tbl_t的cmd_rep成员变量,也就是cmd_always_repeatable,此函数定义在common/command.c文件里面,内容如下:
示例代码12.2.10.13 cmd_always_repeatable函数代码截图
从上述分析可知,cmd_tbl_t的cmd成员就是具体的命令处理函数,所以第540行调用cmdtp的cmd成员来处理具体的命令,返回值为命令的执行结果。

cmd_process中会检测cmd_tbl的返回值,如果返回值为CMD_RET_USAGE的话就会调用cmd_usage函数输出命令的用法,其实就是输出cmd_tbl_t的usage成员变量。

bootm启动Linux内核过程

images全局变量

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

414 extern bootm_headers_t images;

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

示例代码12.3.1.1 bootm_headers结构体 
348 typedef struct bootm_headers { 
349     /* 
350     * Legacy os image header, if it is a multi component image 
351     * then boot_get_ramdisk() and get_fdt() will attempt to get 
352     * data from second and third component accordingly. 
353     */ 
354     image_header_t *legacy_hdr_os; /* image header pointer */ 
355     image_header_t legacy_hdr_os_copy; /* header copy */ 
356     ulong legacy_hdr_valid;
...... 
378 #ifndef USE_HOSTCC 
379 image_info_t os; /* OS镜像信息 */ 
380     ulong ep; /* OS入口点 */ 
381 
382     ulong rd_start, rd_end; /* ramdisk开始和结束位置 */ 
383 
384     char *ft_addr; /* 设备树地址 */ 
385     ulong ft_len; /* 设备树长度 */ 
386 
387     ulong initrd_start; /* initrd开始位置 */ 
388     ulong initrd_end; /* initrd结束位置 */ 
389     ulong cmdline_start; /* cmdline开始位置 */ 
390     ulong cmdline_end; /* cmdline结束位置 */ 
391     bd_t *kbd; 
392 #endif 
393 
394     int verify; /* env_get("verify")[0] != 'n' */ 
395 
396 #define BOOTM_STATE_START (0x00000001) 
397 #define BOOTM_STATE_FINDOS (0x00000002) 
398 #define BOOTM_STATE_FINDOTHER (0x00000004) 
399 #define BOOTM_STATE_LOADOS (0x00000008) 
400 #define BOOTM_STATE_RAMDISK (0x00000010) 
401 #define BOOTM_STATE_FDT (0x00000020) 
402 #define BOOTM_STATE_OS_CMDLINE (0x00000040) 
403 #define BOOTM_STATE_OS_BD_T (0x00000080) 
404 #define BOOTM_STATE_OS_PREP (0x00000100) 
405 #define BOOTM_STATE_OS_FAKE_GO (0x00000200) 
406 #define BOOTM_STATE_OS_GO (0x00000400) 
407     int state; 
408 
409 #ifdef CONFIG_LMB 
410     struct lmb lmb; /* for memory mgmt */ 
411 #endif 
412 } bootm_headers_t;

第379行的os成员变量是image_info_t类型的,为系统镜像信息。

第396~406行这11个宏定义表示BOOT的不同阶段。

接下来看一下结构体image_info_t,也就是系统镜像信息结构体,此结构体在文件include/image.h中的定义如下:

示例代码12.3.1.2 image_info_t结构体 
336 typedef struct image_info { 
337     ulong start, end; /* blob开始和结束位置 */
338     ulong image_start, image_len; /* 镜像起始地址(包括blob)和长度*/ 
339     ulong load; /* 系统镜像加载地址 */ 
340     uint8_t comp, type, os; /* 镜像压缩、类型,OS类型 */ 
341     uint8_t arch; /* CPU架构 */ 
342 } image_info_t;

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

do_bootm函数

bootm命令的执行函数为do_bootm,在文件cmd/bootm.c中有如下定义(省略条件判断):

示例代码12.3.2.1 do_bootm函数 
92 int do_bootm(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[]) 
93 { 
..... 
108     /* determine if we have a sub command */ 
109     argc--; argv++; 
110     if (argc > 0) { 
111         char *endp; 
112 
113         simple_strtoul(argv[0], &endp, 16); 
114         /* endp pointing to NULL means that argv[0] was just a 
115         * valid number, pass it along to the normal bootm processing 
116         * 
117         * If endp is ':' or '#' assume a FIT identifier so pass 
118         * along for normal processing. 
119         * 
120         * Right now we assume the first arg should never be '-' 
121         */ 
122         if ((*endp != 0) && (*endp != ':') && (*endp != '#')) 
123             return do_bootm_subcommand(cmdtp, flag, argc, argv); 
124     } 
125 
126     return do_bootm_states(cmdtp, flag, argc, argv, 
127         BOOTM_STATE_START | BOOTM_STATE_FINDOS | 
128         BOOTM_STATE_FINDOTHER | BOOTM_STATE_LOADOS | 
129 #ifdef CONFIG_SYS_BOOT_RAMDISK_HIGH 
130         BOOTM_STATE_RAMDISK | 
131 #endif 
132 #if defined(CONFIG_PPC) || defined(CONFIG_MIPS) 
133         BOOTM_STATE_OS_CMDLINE | 
134 #endif 
135         BOOTM_STATE_OS_PREP | BOOTM_STATE_OS_FAKE_GO | 
136         BOOTM_STATE_OS_GO, &images, 1);
137 }

第109-124行,主要作用是解析bootm的命令参数和检查有没有子命令,在第113行,simple_strtoul函数将字符串转换成unsigend long型数据,endp指向尾字符。第122行,判断是否有子命令,一般不会有,所以这里也是不会运行的。

第126行,调用函数do_bootm_states来执行不同的BOOT 阶段,这里要执行的BOOT阶段有:BOOTM_STATE_START、BOOTM_STATE_FINDOS、BOOTM_STATE_FINDOTHER、BOOTM_STATE_LOADOS、BOOTM_STATE_RAMDISK、BOOTM_STATE_OS_PREP、BOOTM_STATE_OS_FAKE_GO和BOOTM_STATE_OS_GO。

do_bootm_states函数

do_bootm最后调用的就是函数do_bootm_states,此函数定义在文件common/bootm.c中,函数代码如下:

示例代码12.3.3.1 do_bootm_states函数 
521 int do_bootm_states(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[], int states, bootm_headers_t *images, 
522 int boot_progress) 
523 { 
524     boot_os_fn *boot_fn; 
525     ulong iflag = 0; 
526     int ret = 0, need_boot_fn; 
527 
528     images->state |= states; 
529 
530     /* 
531     * Work through the states and see how far we get. We stop on 
532     * any error. 
533     */ 
534     if (states & BOOTM_STATE_START) 
535         ret = bootm_start(cmdtp, flag, argc, argv); 
536 
537     if (!ret && (states & BOOTM_STATE_FINDOS)) 
538         ret = bootm_find_os(cmdtp, flag, argc, argv); 
539 
540     if (!ret && (states & BOOTM_STATE_FINDOTHER)) 
541         ret = bootm_find_other(cmdtp, flag, argc, argv); 
542 
543     /* Load the OS */ 
544     if (!ret && (states & BOOTM_STATE_LOADOS)) { 
545         iflag = bootm_disable_interrupts(); 
546         ret = bootm_load_os(images, 0); 
547         if (ret && ret != BOOTM_ERR_OVERLAP) 
548             goto err; 
549         else if (ret == BOOTM_ERR_OVERLAP)
550         ret = 0; 
551     } 
552 
553     /* Relocate the ramdisk */ 
554 #ifdef CONFIG_SYS_BOOT_RAMDISK_HIGH 
555     if (!ret && (states & BOOTM_STATE_RAMDISK)) { 
556         ulong rd_len = images->rd_end - images->rd_start; 
557 
558         ret = boot_ramdisk_high(&images->lmb, images->rd_start, 
559             rd_len, &images->initrd_start, &images->initrd_end); 
560         if (!ret) { 
561             env_set_hex("initrd_start", images->initrd_start); 
562             env_set_hex("initrd_end", images->initrd_end); 
563         } 
564     } 
565 #endif 
566 #if IMAGE_ENABLE_OF_LIBFDT && defined(CONFIG_LMB) 
567     if (!ret && (states & BOOTM_STATE_FDT)) { 
568         boot_fdt_add_mem_rsv_regions(&images->lmb, images->ft_addr); 
569         ret = boot_relocate_fdt(&images->lmb, &images->ft_addr, 
570                 &images->ft_len); 
571 } 
572 #endif 
573 
574     /* From now on, we need the OS boot function */ 
575     if (ret) 
576         return ret; 
577     boot_fn = bootm_os_get_boot_func(images->os.os); 
578     need_boot_fn = states & (BOOTM_STATE_OS_CMDLINE | 
579         BOOTM_STATE_OS_BD_T | BOOTM_STATE_OS_PREP | 
580         BOOTM_STATE_OS_FAKE_GO | BOOTM_STATE_OS_GO); 
581     if (boot_fn == NULL && need_boot_fn) { 
582         if (iflag) 
583             enable_interrupts(); 
584         printf("ERROR: booting os '%s' (%d) is not supported\n", 
585             genimg_get_os_name(images->os.os), images->os.os); 
586         bootstage_error(BOOTSTAGE_ID_CHECK_BOOT_OS); 
587         return 1; 
588     } 
589 
590 
591     /* Call various other states that are not generally used */ 
592     if (!ret && (states & BOOTM_STATE_OS_CMDLINE))
593         ret = boot_fn(BOOTM_STATE_OS_CMDLINE, argc, argv, images); 
594     if (!ret && (states & BOOTM_STATE_OS_BD_T)) 
595         ret = boot_fn(BOOTM_STATE_OS_BD_T, argc, argv, images); 
596     if (!ret && (states & BOOTM_STATE_OS_PREP)) { 
597 #if defined(CONFIG_SILENT_CONSOLE) && !defined(CONFIG_SILENT_U_BOOT_ONLY) 
598         if (images->os.os == IH_OS_LINUX) 
599             fixup_silent_linux(); 
600 #endif 
601         ret = boot_fn(BOOTM_STATE_OS_PREP, argc, argv, images); 
602     } 
603 
604 #ifdef CONFIG_TRACE 
605     /* Pretend to run the OS, then run a user command */ 
606     if (!ret && (states & BOOTM_STATE_OS_FAKE_GO)) { 
607         char *cmd_list = env_get("fakegocmd"); 
608 
609         ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_FAKE_GO, 
610                 images, boot_fn); 
611         if (!ret && cmd_list) 
612             ret = run_command_list(cmd_list, -1, flag); 
613     } 
614 #endif 
615 
616     /* Check for unsupported subcommand. */ 
617     if (ret) { 
618         puts("subcommand not supported\n"); 
619         return ret; 
620     } 
621 
622     /* Now run the OS! We hope this doesn't return */ 
623     if (!ret && (states & BOOTM_STATE_OS_GO)) 
624         ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_GO, 
625                 images, boot_fn); 
626 
627     /* Deal with any fallout */ 
628 err: 
629     if (iflag) 
630         enable_interrupts(); 
631 
632     if (ret == BOOTM_ERR_UNIMPLEMENTED) 
633         bootstage_error(BOOTSTAGE_ID_DECOMP_UNIMPL); 
634     else if (ret == BOOTM_ERR_RESET)
635         do_reset(cmdtp, flag, argc, argv); 
636 
637     return ret; 
638 }

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

states & BOOTM_STATE_XXX

根据示例代码12.3.2.1里的126行,有8个阶段,分别为:

BOOTM_STATE_START
BOOTM_STATE_FINDOS
BOOTM_STATE_FINDOTHER
BOOTM_STATE_LOADOS
BOOTM_STATE_RAMDISK
BOOTM_STATE_OS_PREP
BOOTM_STATE_OS_FAKE_GO
BOOTM_STATE_OS_GO

第一阶段 BOOTM_STATE_START

示例代码12.3.3.1里534-535行,就是第一阶段BOOTM_STATE_START,运行bootm_start函数,函数在common/bootm.c文件里,打开此文件找到如下代码:
示例代码12.3.3.2 bootm_start函数代码截图
第71行,清空images结构体。

第72行,获取uboot的环境变量verify的值,并赋值给images的verify成员。

第74行,执行boot_start_lmb函数,如果定义了宏CONFIG_LMB,此函数就会初始化images的lmb成员,没有定义的话此函数为空函数。STM32MP157没有定义宏CONFIG_LMB,因此
此函数为空函数。

第76行,执行bootstage_mark_name函数,主要是记录启动阶段的名字。

第77行,设置images的阶段为BOOTM_STATE_START。

第二阶段BOOTM_STATE_FINDOS

示例代码12.3.3.1里537-538行,就是第二阶段BOOTM_STATE_FINDOS,运行bootm_find_os函数,函数在common/bootm.c文件里,打开此文件找到如下代码(省略了条件相关的判断代码):
示例代码12.3.3.3 boot,bootm_find_os函数代码截图
第9行,boot_get_kernel函数很复杂,这里就不具体分析了。它的主要作用是:首先会根据bootm传过来的参数去获取uImage(镜像)的存储地址,如果bootm没有参数就使用全局变量load_addr。如果使能了宏LEGACY_IMAGE_FORMAT,那么就会按照老的格式获取Linux镜像。STM32MP157使能了宏LEGACY_IMAGE_FORMAT,所以会按照老的格式得到镜像,并且会输出相应的提示信息,如下图所示:
kernel使用老格式
boot_get_kernel会调用image_get_kernel函数进行kernel格式校验,校验老格式kernel的完整性并且返回kernel的镜像头。校验过程中会输出kernel相关信息以及校验结果,如下图所示:
kernel信息以及校验结果
获取到内核镜像以后,将镜像的起始地址赋值给参数os_data,镜像的长度赋值给参数os_len。最后将镜像头部拷贝到images的legacy_hdr_os_copy成员中,并且设置legacy_hdr_valid成员为1。

第17行,使用genimg_get_format函数来检查镜像的类型,STM32MP157的镜像为老的格式类型 (legacy),所以genimg_get_format函数会返回IMAGE_FORMAT_LEGACY,表示这是一个老的类型。

第19-25行,使用image_get_XXXX相关的函数去获取image的type(内核的类型)、comp(内核压缩方式)、os(内核是什么操作系统)、end、load(加载地址)和arch(芯片架构),然后将其这些信息赋值给images的对应成员变量。

第37行,把内核的起始地址赋值给全局变量images.os.start。

到这里bootm_fimd_os函数结束,用一句话概括就是:主要工作是解析image,获取一些基本参数信息赋值到images全局结构体中

第三阶段BOOTM_STATE_FINDOTHER

示例代码12.3.3.1里540-541行,就是第三阶段BOOTM_STATE_FINDOTHER,运行bootm_find_other函数,函数在common/bootm.c文件里,打开此文件找到如下代码:

示例代码12.3.3.4 bootm_find_other函数 
286 static int bootm_find_other(cmd_tbl_t *cmdtp, int flag, int argc, 
287                 char * const argv[]) 
288 { 
289     if (((images.os.type == IH_TYPE_KERNEL) || 
290         (images.os.type == IH_TYPE_KERNEL_NOLOAD) || 
291         (images.os.type == IH_TYPE_MULTI)) && 
292         (images.os.os == IH_OS_LINUX || 
293         images.os.os == IH_OS_VXWORKS)) 
294         return bootm_find_images(flag, argc, argv);
295 
296     return 0; 
297 }

第294行,调用bootm_find_images函数来获取ramdisk或者设备树信息,STM32MP157没用ramdisk,因此bootm_find_images主要用来获取设备树,bootm_find_images函数内容如下 (省略掉条件编译部分代码):
示例代码12.3.3.5 bootm_find_images函数代码截图
第6行,调用boot_get_ramdisk函数来获取ramdisk,这里没有用到ramdisk,所以这段代
码无效。

第15行,调用boot_get_fdt函数来获取设备树,并且将设备树首地址赋值给images的ft_addr成员。

第四阶段BOOTM_STATE_LOADOS

示例代码12.3.3.1里544-551行,就是第四阶段BOOTM_STATE_LOADOS,第545行先使用bootm_disable_interrupts函数禁用中断,再运行bootm_load_os函数,函数在common/bootm.c文件里,打开此文件找到如下代码:

示例代码12.3.3.6 bootm_load_os函数 
341 static int bootm_load_os(bootm_headers_t *images, int boot_progress)
342 { 
343     image_info_t os = images->os; 
344     ulong load = os.load; 
345     ulong load_end; 
346     ulong blob_start = os.start; 
347     ulong blob_end = os.end; 
348     ulong image_start = os.image_start; 
349     ulong image_len = os.image_len; 
350     ulong flush_start = ALIGN_DOWN(load, ARCH_DMA_MINALIGN); 
351     bool no_overlap; 
352     void *load_buf, *image_buf; 
353     int err; 
354 
355     load_buf = map_sysmem(load, 0); 
356     image_buf = map_sysmem(os.image_start, image_len); 
357     err = image_decomp(os.comp, load, os.image_start, os.type, 
358             load_buf, image_buf, image_len, 
359             CONFIG_SYS_BOOTM_LEN, &load_end); 
360     if (err) { 
361         err = handle_decomp_error(os.comp, load_end - load, err); 
362         bootstage_error(BOOTSTAGE_ID_DECOMP_IMAGE); 
363         return err; 
364     } 
365 
366     flush_cache(flush_start, ALIGN(load_end, ARCH_DMA_MINALIGN) – flush_start); 
367 
368     debug(" kernel loaded at 0x%08lx, end = 0x%08lx\n", load, load_end); 
369     bootstage_mark(BOOTSTAGE_ID_KERNEL_LOADED); 
370 
371     no_overlap = (os.comp == IH_COMP_NONE && load == image_start); 
372 
373     if (!no_overlap && load < blob_end && load_end > blob_start) { 
374         debug("images.os.start = 0x%lX, images.os.end = 0x%lx\n", 
375             blob_start, blob_end); 
376         debug("images.os.load = 0x%lx, load_end = 0x%lx\n", load, 
377             load_end); 
378 
379         /* Check what type of image this is. */ 
380         if (images->legacy_hdr_valid) { 
381             if (image_get_type(&images->legacy_hdr_os_copy) 
382                     == IH_TYPE_MULTI)
383                 puts("WARNING: legacy format multi component image overwritten\n"); 
384             return BOOTM_ERR_OVERLAP; 
385         } else { 
386             puts("ERROR: new format image overwritten - must RESET the board to recover\n"); 
387             bootstage_error(BOOTSTAGE_ID_OVERWRITTEN); 
388             return BOOTM_ERR_RESET; 
389         } 
390     } 
391 
392     lmb_reserve(&images->lmb, images->os.load, (load_end - 
393                         images->os.load)); 
394     return 0; 
395 }

第357行,调用image_decomp函数来解压内核。函数的参数都是在第二阶段里获取到参数os.image_start=0Xc2000000和load_buf=0xc2000040,os.image_start是内核未解压时所在的地址,load_buf是内核的启动地址也就是解压后内核所在的地址。

第五阶段BOOTM_STATE_RAMDISK

示例代码12.3.3.1里554-564行,就是第五阶段BOOTM_STATE_RAMDISK,这里没有用ramdisk。

示例代码12.3.3.1里577行,非常重要!通过函数bootm_os_get_boot_func来查找系统启动
函数,参数images->os.os就是系统类型,根据这个系统类型来选择对应的启动函数。示例代码12.3.3.3中的bootm_find_os函数会根据系统类型设置images->os.os的值为IH_OS_LINUX。数
bootm_os_get_boot_func函数返回值就是找到的系统启动函数,这里找到的Linux系统启动函数
为do_bootm_linux
。因此boot_fn=do_bootm_linux,后面执行boot_fn函数的地方实际上是执行的do_bootm_linux函数

第六阶段BOOTM_STATE_PREP

示例代码12.3.3.1里596-602行,就是第六阶段BOOTM_STATE_PREP主要是解析bootargs环境变量和设置内核的启动参数。

第599行,fixup_silent_linux函数应该是重新设置bootagrs的环境变量,不知道有啥作用。

第601行,调用boot_fn函数,前面已经说了,boot_fn为 do_bootm_linux,所以这里也就
是运行do_bootm_linux函数。此处do_bootm_linux函数主要是调用boot_prep_linux函数去解析bootargs变量。

第七阶段BOOTM_STATE_FAKE_GO

示例代码12.3.3.1里606-613行,就是第七阶段BOOTM_STATE_FAKE_GO,代码不执行。

第八阶段BOOTM_STATE_OS_GO

示例代码12.3.3.1里623-625行,就是第八阶段BOOTM_STATE_OS_GO,第624行,运行bootm_selected_os函数,函数在common/ bootm_os.c文件里,打开此文件找到如下代码:

示例代码12.3.3.7 boot_selected_os函数
551 int boot_selected_os(int argc, char * const argv[], int state, 
552         bootm_headers_t *images, boot_os_fn *boot_fn) 
553 { 
554     arch_preboot_os(); 
555     board_preboot_os(); 
556     boot_fn(state, argc, argv, images); 
557 
558     /* Stand-alone may return when 'autostart' is 'no' */ 
559     if (images->os.type == IH_TYPE_STANDALONE || 
560         IS_ENABLED(CONFIG_SANDBOX) || 
561         state == BOOTM_STATE_OS_FAKE_GO) /* We expect to return */ 
562         return 0; 
563     bootstage_error(BOOTSTAGE_ID_BOOT_OS_RETURNED); 
564     debug("\n## Control returned to monitor - resetting...\n"); 
565 
566     return BOOTM_ERR_RESET; 
567 }

第554-555行,这两个函数都是空函数,不执行。

重点是第556行,执行boot_fn,也就是do_bootm_linux函数,注意boot_fn函数的第一个参数state为BOOTM_STATE_OS_GO。

bootm_os_get_boot_func函数

do_bootm_states会调用bootm_os_get_boot_func来查找对应系统的启动函数,此函数定义在文件common/bootm_os.c中,函数内容如下:
示例代码12.3.4.1 bootm_os_get_boot_func函数代码截图
第571-584行,这是一个宏定义,CONFIG_NEEDS_MANUAL_RELOC宏没有定义,这里的代码就不会编译。

第585行,boot_os是一个全局函数数组,数组里面存放着不同的系统对应的启动函数,boot_os也定义在文件common/bootm_os.c中,如下所示:
示例代码12.3.4.2 boot_os数组
第504行,do_bootm_linux就是Linux系统对应的启动函数,可以看出查找方法很简单,就是读取数组中指定元素

do_bootm_linux函数

经过上述分析可知,do_bootm_linux就是最终启动Linux内核的函数,此函数定义在文件arch/arm/lib/bootm.c,函数内容如下:
示例代码12.3.5.1 do_bootm_linux函数代码截图
在八个阶段里,其中在第六阶段和第八阶段都回调do_bootm_linux函数,示例代码12.3.3.1中第601行和第624行。第六阶段传进的flag为BOOTM_STATE_PREP,就会运行示例代码12.3.3.10里429行,boot_prep_linux函数就是负责解析bootargs变量。第八阶段传进的fiag为BOOTM_STATE_OS_GO,就会运行434行的boot_jump_linux函数,此函数定义在文件arch/arm/lib/bootm.c中,函数内容如下:

示例代码12.3.5.2 boot_jump_linux函数 
331 static void boot_jump_linux(bootm_headers_t *images, int flag) 
332 { 
333 #ifdef CONFIG_ARM64 
...... 
371 #else 
372     unsigned long machid = gd->bd->bi_arch_number; 
373     char *s; 
374     void (*kernel_entry)(int zero, int arch, uint params); 
375     unsigned long r2; 
376     int fake = (flag & BOOTM_STATE_OS_FAKE_GO); 
377 
378     kernel_entry = (void (*)(int, int, uint))images->ep; 
379 #ifdef CONFIG_CPU_V7M 
380     ulong addr = (ulong)kernel_entry | 1; 
381     kernel_entry = (void *)addr; 
382 #endif 
383     s = env_get("machid"); 
384     if (s) { 
385         if (strict_strtoul(s, 16, &machid) < 0) { 
386             debug("strict_strtoul failed!\n"); 
387             return; 
388         } 
389         printf("Using machid 0x%lx from environment\n", machid); 
390     } 
391 
392     debug("## Transferring control to Linux (at address %08lx)" \ 
393         "...\n", (ulong) kernel_entry); 
394     bootstage_mark(BOOTSTAGE_ID_RUN_OS); 
395     announce_and_cleanup(fake); 
396 
397     if (IMAGE_ENABLE_OF_LIBFDT && images->ft_len) 
398         r2 = (unsigned long)images->ft_addr; 
399     else 
400         r2 = gd->bd->bi_boot_params; 
...... 
410             kernel_entry(0, machid, r2); 
411     }
412 #endif

第333-371行是64位ARM芯片对应的代码,Cortex-A7是32位芯片,因此用不到。

第372行,变量machid保存机器ID,如果不使用设备树的话这个机器ID会被传递给Linux内核,Linux内核会在自己的机器ID列表里面查找是否存在与uboot传递进来的machid匹配的项目,如果存在就说明Linux内核支持这个机器,那么Linux就会启动!如果使用设备树的话这个machid就无效了,设备树存有一个“兼容性”这个属性,Linux内核会比较“兼容性”属性的值(字符串)来查看是否支持这个机器。

第374行,**函数kernel_entry,此函数是进入Linux内核的,也就是最终的大 boss!!**此函数有三个参数:zero,arch,params,第一个参数zero要为0;第二个参数为机器ID;第三个参数ATAGS或者设备树(DTB)首地址, ATAGS是传统的方法,用于传递一些命令行信息,如果使用设备树的话就要传递设备树(DTB)。

第378行,获取kernel_entry函数,函数kernel_entry并不是uboot定义的,而是Linux内
核定义的,Linux内核镜像文件的第一行代码就是函数kernel_entry,而images->ep保存着Linux内核镜像的起始地址,起始地址保存的正是Linux内核第一行代码!

第395行,调用函数announce_and_cleanup来打印一些信息并做一些清理工作,此函数定义在文件arch/arm/lib/bootm.c中,函数内容如下:
示例代码12.3.5.3 announce_end_cleanup函数代码截图
第110行,在启动Linux之前输出“Starting kernel …”信息,如下图所示:
系统启动提示信息
第119行调用cleanup_before_linux函数做一些清理工作。

继续回到示例代码12.3.5.2的函数boot_jump_linux,第397-400行是设置寄存器r2的值。Linux内核一开始是汇编代码,因此函数kernel_entry就是个汇编函数向汇编函数传递参数要使用r0、r1和r2(参数数量不超过3个的时候),所以r2寄存器就是函数kernel_entry的第三个参数

第398行,如果使用设备树的话,r2应该是设备树的起始地址,而设备树地址保存在images的ftd_addr成员变量中

第400行,如果不使用设备树的话,r2应该是uboot传递给Linux的参数起始地址,也就是环境变量bootargs的值。

第410行,调用kernel_entry函数进入Linux内核,此行将一去不复返,uboot的使命也就完成了

总结

u-boot.lds链接脚本

通过_start进入链接,_start后面就是中断向量表,而中断向量表存放在.vectors段中。使用到的__image_copy_start和.text是在u-boot.map中,__image_copy_start地址为0Xc0100000,而.text的起始地址也是0Xc0100000。

.vectors段的起始地址也是0XC0100000,说明整个uboot的起始地址就是0XC0100000。而后将arch/arm/cpu/armv7/start.s编译出来的代码放到中断向量表后面。

U-Boot启动流程

整个uboot函数调用可以精简成下图所示:
uboot调用路径

进入vectors.S之后,会在_start之后进入中断向量表,执行到11行会跳转到reset函数,reset会跳转到save_boot_params函数,这只是一句跳转函数,会跳转到save_boot_params_ret函数;这其中会设置CPU处于SVC32模式且关闭FIQ和IRQ两个中断,然后把r0设为_start的值0XC0100000,并将r0xieruVBAR寄存器来准备向量表重定位;之后分别调用函数cpu_init_cp15、cpu_init_crit和_main;cpu_init_cp15不重要,cpu_init_crit内部仅仅是调用了函数lowlevel_init。

lowlevel_init会行设置sp指向CONFIG_SYS_INIT_SP_ADDR,就是0XC0100000,然后8字节对齐;然后sp指针减去GD_SIZE=240并再次8字节对齐,得到sp=0XC00FFDC0后保存到r9寄存器并将ip和lr压栈,之后会让他们出栈并让lr给pc;lowlevel_init 运行完后,就返回函数cpu_init_crit,函数cpu_init_crit 也执行完成了,最终返
回到save_boot_params_ret。

_main函数,设置sp指针为CONFIG_SYS_INIT_SP_ADDR,其值为0XC0100000,并完成8字节对齐,读取sp到r0寄存器中后调用board_init_f_alloc_reserve(1个参数且为r0寄存器的值),会返回新的top值=0XC00FCF10;而后将r0写入sp,此时r0就=top值=0XC00FCF10,也就是sp=0XC00FCF10;将r0传入r9,r9中存放全局变量gd的地址,也就是0XC00FCF10;然后调用board_init_f_init_reserve来初始化gd(清零);然后设置r0=0,调用board_init_f并重新设置sp和gd,获取gd->start_addr_dp给sp,此时sp=0XF3AEDD20;然后8字节对齐并获取gd->new_gd给r9=0,将gd->reloc_off给r0=84;然后lr+=r0来重定位;gd->relocaddr给r0,r0就保存下了要拷贝的目的地址=0XF5C46000;然后调用函数relocate_code,也就是代码重定位函数,此函数负责将uboot拷贝到新地址;然后调用函数relocate_vectors从定位中断向量表,并调用c_runtime_cpu_setup;然后调用board_init_r函数,把r存入r0,r1=gd->relocaddr。在_main函数里面调用了board_init_f、relocate_code、relocate_vectors和board_init_r这4个函数

board_init_f就是来完成一系列初始化并初始化gd的各个成员变量,uboot将自己重定位到DRAM最后的地址区域来给Linux腾出空间;gd->flags=boot_flags,gd->have_console=0;然后调用函数initcall_run_list运行初始化序列init_sequence_f;最后的内存分配经过board_init_f会变成下图
最终内存分配
函数relocate_code用于代码拷贝,会先给r1=__image_copy_start=0XC0100000保存原地址,然后r0=0XF5C46000目标首地址,r4=r0-r1=0X35B46000偏移量;然后r2=__image_copy_end保存拷贝之前的代码结束地址=0XC01ACD98;然后通过函数copy_loop完成代码拷贝工作,从r1读取uboot代码保存到r10和r11,然后r1会更新;然后重定位.rel.dyn段,存放的是.text段中需要重定位地址的集合;这里的重定位会采取位置无关码,来解决重定位后链接地址和运行地址不一致的问题。

函数relocate_vectors用于重定位向量表,会给r0赋值gd->relocaddr,就是重定位后的uboot首地址,然后把r0写入CP15的VBAR寄存器,设置向量表偏移。

board_init_r函数会完成board_init_f没有初始化的一些外设以及后续工作;调用initcall_run_list函数来执行初始化序列init_sequence_r,这个函数集合会完成一系列的初始化。

run_main_loop函数完成了uboot启动以后会进入 N(N=1.2.3…)秒倒计时,如果在N秒倒计时结束之前按下回车键,那么就会进入uboot的命令模式,如果倒计时结束以后都没有按下回车键,那么就会自动启动Linux内核这么一个功能;调用bootstage_mark_name函数,打印出启动进度;然后会打印出版本号,执行cli_init函数来初始化命令以及hush shell相关变量;然后调用run_preboot_environment_command函数,获取环境变量perboot的内容(这个环境变量一般不使用);然后调用bootdelay_process函数,此函数会读取环境变量bootdelay和bootcmd的内容,然后将bootdelay的值赋值给全局变量stored_bootdelay,返回值为环境变量;然后调用autoboot_command函数检查倒计时是否结束以及是否有打断倒计时,如果自然结束倒计时就会执行run_command_list,此函数会执行参数s指定的一系列命令,也就是环境变量bootcmd的命令,bootcmd里面保存着默认的启动命令,因此linux内核启动;如果倒计时结束前按下按键就不执行run_command_list,回调出去执行cli_loop函数,负责接收处理输入的命令

函数cli_loop是uboot的命令行处理函数,我们在 uboot中输入各种命令,进行各种操作就
是有cli_loop来处理;因为STM32MP157定义了宏CONFIG_SYS_HUSH_PARSER。因此会调用函数parse_file_outer;这个函数会调用函数setup_file_in_str初始化变量input的成员变量,调用函数parse_stream_outer,这个函数就是hush shell的命令解释器,负责接收命令行输入,然后解析并执行相应的命令,他会在do-while循环中通过parse_stream进行命令解析并调用run_list来执行解析的命令;run_list经过一系列调用(调用 run_list_real函数,run_list_real函数调用 run_pipe_real函数,最后调用cmd_process)最终调用cmd_process来处理解析的命令

uboot使用宏U_BOOT_CMD来定义命令U_BOOT_CMD是U_BOOT_CMD_COMPLETE的特例,将U_BOOT_CMD_COMPLETE的最后一个参数设置成NULL就是U_BOOT_CMD;宏U_BOOT_CMD_COMPLETE又用到了ll_entry_declare和U_BOOT_CMD_MKENT_COMPLETE,其中ll_entry_declare定义了一个cmd_tbl_t变量,而宏U_BOOT_CMD_MKENT_COMPLETE又用到了宏_CMD_HELP和_CMD_COMPLETE,这两个宏就是取自身的值,然后在加上一个‘,’。总结来说,uboot中使用U_BOOT_CMD来定义一个命令,最终的目的就是为了定义一个cmd_tbl_t类型的变量,并初始化这个变量的各个成员,然后调用存储在.u_boot_list段中对应的do_xxx函数来处理命令

函数cmd_process处理中,调用函数find_cmd在命令表中找到指定的命令,命令表就是cmd_tbl_t结构体数组,通过函数 ll_entry_start得到数组的第一个元素,也就是命令表起始地址,在通过ll_entry_count得到数组长度,也就是命令表的长度,最终通过函数find_cmd_tbl在命令表中找到所需的命令;找到命令后调用cmd_call执行命令;他会调用cmd_tbl_t的cmd_rep成员变量,也就是cmd_always_repeatable,这个就会调用cmd_tbl_t的成员cmd来执行命令

bootm启动Linux内核

通过全局变量images,images是bootm_headers_t类型的全局变量,bootm_headers_t是个boot头结构体,包含了os成员变量保存系统镜像信息,是image_info_t类型;还保存了11个宏定义表示BOOT不同阶段。

bootm的执行函数为do_bootm,会先解析bootm的命令参数和检查有没有子命令,然后调用do_bootm_states执行不同的BOOT阶段。

BOOT的阶段通过以下来进行判断:

states & BOOTM_STATE_XXX

而BOOT一共有8个阶段,分别为:

BOOTM_STATE_START
BOOTM_STATE_FINDOS
BOOTM_STATE_FINDOTHER
BOOTM_STATE_LOADOS
BOOTM_STATE_RAMDISK
BOOTM_STATE_OS_PREP
BOOTM_STATE_OS_FAKE_GO
BOOTM_STATE_OS_GO

第一阶段BOOTM_STATE_START会清空images结构体,获取uboot环境变量verify并赋值给images的verify,然后执行bootstage_mark_name记录启动阶段名字,并设置images阶段为BOOTM_STATE_START。

第二阶段BOOTM_STATE_FINDOS,汇之星bootm_find_os函数,其中会调用boot_get_kernel函数很复杂,这里就不具体分析了。它的主要作用是:根据bootm传过来的参数去获取uImage(镜像)的存储地址,STM32MP157使能了宏LEGACY_IMAGE_FORMAT就会按照老格式读出镜像;获取之后起始地址和长度会给到os_data和os_len,最后将镜像头部拷贝到images的legacy_hdr_os_copy中并设置legacy_hdr_valid=1;最后会使用image_get_XXXX获取images的type、comp、os、end、load、arch等参数并对应传给images的成员变量,并把内核起始地址给到全局变量images.os.start。总结来说就是解析 image,获取一些基本参数信息赋值到images全局结构体中。

第三阶段BOOTM_STATE_FINDOTHER,运行bootm_find_other,调用bootm_find_images函数来获取设备树信息,其中就是调用boot_get_fdt函数来获取设备树,并且将设备树首地址赋值给images的ft_addr成员。

第四阶段BOOTM_STATE_LOADOS,会使用bootm_disable_interrupts禁用中断,在运行bootm_load_os函数,其中调用image_decomp函数来解压内核。

第五阶段BOOTM_STATE_RAMDISK,对于STM32MP157来说,没有ramdisk。

第六阶段BOOTM_STATE_PREP主要是解析 bootargs环境变量和设置内核的启动参数,调用boot_fn函数,也就是运行do_bootm_linux函数来调用boot_prep_linux解析bootargs变量。

第七阶段BOOTM_STATE_FAKE_GO,代码不执行。

第八阶段BOOTM_STATE_OS_GO,运行bootm_selected_os函数,执行boot_fn,也就是 do_bootm_linux函数。

以上之中,do_bootm_states会调用bootm_os_get_boot_func来查找对应系统的启动函数,对本次开发板也就是linux启动,所有最终调用do_bootm_linux函数,在第六阶段BOOTM_STATE_PREP会运行boot_prep_linux解析bootargs变量;第八阶段BOOTM_STATE_OS_GO就会运行boot_jump_linux函数,变量machid保存机器ID,然后通过kernel_entry函数进入Linux内核,传入zero=0,arch传入机器ID以及ATAGS传入设备树(DTB)首地址;获取kernel_entry是通过images->ep保存的Linux内核镜像的其实地址来获得的,然后最后调用announce_and_cleanup来打印信息并清理

总结之总结

通过看u-boot.lds链接文件找到了uboot的启动入口在_start,然后通过地址找到保存着uboot代码的位置,最后进入执行_main函数,其中包括了4个主要函数来初始化外设,并把uboot往后移腾出空间给Linux内核;而Linux内核启动分为8个阶段,最后就是第8个阶段通过do_bootm_linux启动内核。

  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值