正点原子[第三期]Arm(iMX6U)Linux移植学习笔记-6.2uboot启动流程-lowlevel_init,s_init,_main函数执行

 前言:

本文是根据哔哩哔哩网站上“Arm(iMX6U)Linux系统移植和根文件系统构键篇”视频的学习笔记,在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。

引用:

正点原子IMX6U仓库 (GuangzhouXingyi) - Gitee.com

《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.5.2.pdf》

正点原子资料下载中心 — 正点原子资料下载中心 1.0.0 文档

正点原子imx6ull-mini-Linux驱动之Linux I2C 驱动实验(21)-优快云博客

正文:

本文是 “Arm(iMX6U)Linux系统移植和根文件系统构键篇--6.2 讲uboot顶层Makefile分析创建VSCode工程。本节将参考正点原子的视频教程和配套的正点原子开发指南文档进行学习。

0. 概述

1 reset函数源码详解

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

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

arch/arm/cpu/armv7/start.S

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

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

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

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

 第 45行,判断 r1寄存器的值是否等于 0X1A(0b11010),也就是判断当前处理器模式是否处于 Hyp模式。

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

bicne 指令是 bic + ne 

bic 是bitclean 位清除指令,加上条件 ne (non-equal)不相等条件满足时执行 bic 指令

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

orrne 指令时 orr + ne

orr 时或运算指令,加上条件 ne (non-equal)不相等条件满足时执行 orr 指令

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

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

ARMv7 CPSR寄存器

ARMv7 的CPSR 寄存器的定义,bit[0-4] 这5位控制ARMv7 CPU的工作模式,bit[6]控制FIQ中断,开关bit[7]控制FIQ中断开关。

继续执行执行下面的代码:

第56行,如果没有定义 CONFIG_OMAP44XX和 CONFIG_SPL_BUILD的话条件成立,此处条件成立。 

第58行读取 CP15中 c1寄存器的值到 r0寄存器中,根据 17.1.4小节可知,这里是读取SCTLR寄存器的值

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

 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可以看出,

  • bit13为 V位,此位是向量表控制位当为 0的时候向量表基地址为 0X00000000,软件可以重定位向量表
  • 为 1的时候向量表基地址为 0XFFFF0000,软件不能重定位向量表。

这里将 V清零,目的就是为了接下来的向量表重定位,这个我们在第十七章有过详细的介绍了。

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

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

第64行将 r0寄存器的值 (向量表值 )写入到 CP15的 c12寄存器中,也就是 VBAR寄存器。因此第 58~64行就是设置向量表重定位的

代码继续往下执行:

第 68 行如果没有定义 CONFIG_SKIP_LOWLEVEL_INIT 的话条件成立。我们没有定义CONFIG_SKIP_LOWLEVEL_INIT,因此条件成立,执行下面的语句。

示例代码 32.2.1.6 中的内容比较简单,

  • 就是分别调用函数 cpu_init_cp15、
  • cpu_init_crit
  • 和_main。

函数 cpu_init_cp15 用来设置 CP15 相关的内容,比如关闭 MMU 啥的,此函数同样在 arch/arm/cpu/armv7/start.S文件中定义的,代码如下:

arch/arm/cpu/armv7/start.S

函数 cpu_init_cp15 都是一些和 CP15 有关的内容,我们不用关心,有兴趣的可以详细的看一下。

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

arch/arm/cpu/armv7/start.S

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

2 lowlevel_init 函数详解

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

arch/arm/cpu/armv7/lowlevel_init.S

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

include/configs/mx6ullevk.h


 

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

arch/arm/include/asm/arch-mx6/imx-regs.h

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

include/generated/generic-asm-offsets.h

 GENERATED_GBL_DATA_SIZE=256, GENERATED_GBL_DATA_SIZE 的含义为(sizeof(struct global_data) + 15) & ~15 。

综上所述, CONFIG_SYS_INIT_SP_ADDR 值如下:
 

CONFIG_SYS_INIT_SP_OFFSET = 0x00020000 – 256 = 0x1FF00。
CONFIG_SYS_INIT_SP_ADDR = 0x00900000 + 0X1FF00 = 0X0091FF00,

结果如下图所示:

此时 sp 指向 0X91FF00,这属于 IMX6UL/IMX6ULL 的内部 ram。

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

第 34 行, sp 指针减去 GD_SIZE, GD_SIZE 同样在 generic-asm-offsets.h 中定了,大小为248,见示例代码 32.2.2.4 第 11 行。

第 35 行对 sp 做 8 字节对齐,此时 sp 的地址为 0X0091FF00-248=0X0091FE08,此时 sp 位置如图 32.2.2.2 所示:


 

第 36 行将 sp 地址保存在 r9 寄存器中。
第 42 行将 ip 和 lr 压栈
第 57 行调用函数 s_init,得,又来了一个函数。
第 58 行将第 36 行入栈的 ip 和 lr 进行出栈,并将 lr 赋给 pc。


3 s_init 函数详解

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


 在第 816 行会判断当前 CPU 类型,如果 CPU 为 MX6SX、 MX6UL、 MX6ULL 或 MX6SLL中 的 任 意 一 种 , 那 么 就 会 直 接 返 回 , 相 当 于 s_init 函 数 什 么 都 没 做 。 所 以 对 于I.MX6UL/I.MX6ULL 来说, s_init 就是个空函数。从 s_init 函数退出以后进入函数 lowlevel_init,但是 lowlevel_init 函数也执行完成了,返回到了函数 cpu_init_crit,函数 cpu_init_crit 也执行完成了,最终返回到 save_boot_params_ret,函数调用路径如图 32.2.3.1 所示:

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

4 _main 函数详解

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

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

 函数 board_init_f_alloc_reserve 主要是留出早期的 malloc 内存区域和 gd 内存区域,

其中CONFIG_SYS_MALLOC_F_LEN=0X400( 在 文 件 include/generated/autoconf.h 中 定 义 ) ,

sizeof(struct global_data)=248(GD_SIZE 值),

完成以后的内存分布如图 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 可以看出, uboot 中定义了一个指向 gd_t 的指针 gd, gd 存放在寄存器 r9 里面的,因此 gd 是个全局变量。 gd_t 是个结构体,在 include/asm-generic/global_data.h 里面有义,gd_定义如下:

因此这一行代码就是设置 gd 所指向的位置,也就是 gd 指向 0X0091FA00。

继续回到示例代码 32.2.4.1 中,第 90 行调用函数 board_init_f_init_reserve,此函数在文件common/init/board_init.c 中有定义,函数内容如下:


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

继续回到示例代码 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 中,函数内容如下:

第 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 个函数都是干啥的。

5 board_init_f 函数详解

main 中会调用 board_init_f 函数, board_init_f 函数主要有两个工作:

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

此函数定义在文件 common/board_f.c 中定义,代码如下:

因为没有定义CONFIG_SYS_GENERIC_GLOBAL_DATA,所以第1037~1054行代码无效。

第 1056 行,初始化 gd->flags=boot_flags=0。

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

重点在第 1059 行!通过函数 initcall_run_list 来运行初始化序列 init_sequence_f 里面的一些列函数, init_sequence_f 里面包含了一系列的初始化函数

init_sequence_f 也是定义在文件common/board_f.c 中

,由于 init_sequence_f 的内容比较长,里面有大量的条件编译代码,这里为了缩小篇幅,将条件编译部分删除掉了,去掉条件编译以后的 init_sequence_f 定义如下:


 

 

 接下来分析以上函数执行完以后的结果:
第 2 行, setup_mon_len 函数设置 gd 的 mon_len 成员变量,此处为__bss_end -_start,也就是整个代码的长度。 0X878A8E74-0x87800000=0XA8E74,这个就是代码长度

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

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

第 5 行, arch_cpu_init 函数。

第 6 行, initf_dm 函数,驱动模型的一些初始化。

第 7 行, arch_cpu_init_dm 函数未实现。

第 8 行, mark_bootstage 函数应该是和啥标记有关的

第 9 行, board_early_init_f 函数,板子相关的早期的一些初始化设置, I.MX6ULL 用来初始化串口的 IO 配置

第 10 行, timer_init,初始化定时器, Cortex-A7 内核有一个定时器,这里初始化的就是 CortexA内核的那个定时器。通过这个定时器来为 uboot 提供时间。就跟 Cortex-M 内核 Systick 定时器一样。关于 Cortex-A 内部定时器的详细内容,请参考文档《ARM ArchitectureReference ManualARMv7-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 所示:

 第 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 所示:
 第 19 行, print_cpuinfo 函数用于打印 CPU 信息,结果如图 32.2.5.3 所示:

第 20 行, show_board_info 函数用于打印板子信息,会调用 checkboard 函数,结果如图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 所示信息:

 第 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 所示内容:

设置好以后重新编译 uboot,然后烧写到 SD 卡中,选择 SD 卡启动,重启开发板,打开SecureCRT, uboot 会输出如图 32.2.5.7 所示信息:


 从图 32.2.5.7 可以看出:

gd->ram_size = 0X20000000
gd->ram_top = 0XA0000000
gd->relocaddr = 0XA0000000
//ram 大小为 0X20000000=512MB
//ram 最高地址为 0X80000000+0X20000000=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 可以看出:

gd->arch.tlb_size= 0X4000
gd->arch.tlb_addr=0X9FFF0000
gd->relocaddr=0X9FFF0000
//MMU 的 TLB 表大小
//MMU 的 TLB 表起始地址, 64KB 对齐以后
//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 可以看出:
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 可以看出:

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.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 所示:


 

gd->start_addr_sp=0X9EF44EB8
gd->new_gd=0X9EF44EB8
//0X9EF44FB0-248=0X9EF44EB8

第 53 行, reserve_fdt,留出设备树相关的内存区域, I.MX6ULL 的 uboot 没有用到,因此此函数无效。

第 54 行, reserve_arch 是个空函数。

第 55 行, reserve_stacks,留出栈空间,先对 gd->start_addr_sp 减去 16,然后做 16 字节其。如果使能 IRQ 的话还要留出 IRQ 相应的内存,具体工作是由 arch/arm/lib/stack.c 文件中的
函数 arch_reserve_stacks 完成。结果如图 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 可以看出, DRAM 的起始地址为 0X80000000,大小为 0X20000000(512MB)。

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


 第 58 行, display_new_sp 函数,显示新的 sp 位置,也就是 gd->start_addr_sp,不过要定义宏 DEBUG,结果如图 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 可以看出, uboot 重定位后的偏移为 0X18747000,重定位后的新地址为0X9FF4700,新的 gd 首地址为 0X9EF44EB8,最终的 sp 为 0X9EF44E90。

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

6 relocate_code 函数详解

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

 

第 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 接着拷贝,直至拷贝完成。

u-boot代码copy之后,需要进行代码的重定位 relocation.

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

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

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,相关内容如下所示:


 

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

因此 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 的地址,最终

第 3 行, r2=100。

第 5 行,将 r2 内的值写到 r3 地址处,也就是设置地址 0X8785DA50 的值为 100,这不就是示例代码代码 32.2.6.2 中的第 5 行: rel_a = 100。r3=0X8785DA50。

总结一下 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 下有如下代码:

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


 

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

 先来看一下.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 段的重定位代码如下:

第 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 段的重定位比较复杂一点,有点绕,因为涉及到链接地址和运行地址的问题。

7 relocate_vectors 函数详解

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

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

8 board_init_r 函数详解

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

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

第 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 可以看出,此时有两个 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 所示信息:

第 28 行, initr_jumptable 函数,初始化跳转表。
第 29 行 , console_init_r 函 数 , 控 制 台 初 始 化 , 初 始 化 完 成 以 后 此 函 数 会 调 用stdio_print_current_devices 函数来打印出当前的控制台设备,如图 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 中的第 46 行和第 47 行就是运行“mmc dev xx”命令,用于切换到正在使用的EMMC 设备,串口输出信息如图 32.2.8.5 所示:


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


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

9 run_main_loop 函数详解

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

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

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

const char __weak version_string[] = U_BOOT_VERSION_STRING;

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

#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 内如如下:

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


 宏 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 中的第一行就是 uboot 版本号,和我们分析的一致。

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

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


 

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

 当一下三条全部成立的话,就会执行函数 run_command_list。
①、 stored_bootdelay 不等于-1。
②、 s 不为空。
③、函数 abortboot 返回值为 0。

stored_bootdelay 等于环境变量 bootdelay 的值; s 是环境变量 bootcmd 的值,一般不为空,因 此 前 两 个 成 立 , 就剩 下 了 函 数 abortboot 的 返 回 值 , abortboot 函数 也 定 义 在 文 件common/autoboot.c 中,内容如下:

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

 

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

第 3 行的变量 abort 是函数 abortboot_normal 的返回值,默认值为 0。
第 7 行通过串口输出“Hit any key to stop autoboot”字样,如图 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 函数,这个就是命令处理函数,负责接收好处理输入的命令。

10 cli_loop 函数详解

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

在文件 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 中,去掉条件编译内容以后的函数内容如下:

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


 第 7~21 行中的 do-while 循环就是处理输入命令的。
第 9 行调用函数 parse_stream 进行命令解析。
第 14 行调用调用 run_list 函数来执行解析出来的命令。

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

 

第 5 行, run_list 调用 run_list_real 函数。
第 16 行, run_list_real 函数调用 run_pipe_real 函数。
第 36 行, run_pipe_real 函数调用 cmd_process 函数。

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

11 cmd_process 函数详解

在学习cmd_process 之前先看一下uboot中命令是如何定义的

uboot使用宏U_BOOT_CMD来定义命令,宏 U_BOOT_CMD 定义在文件 include/command.h 中,定义如下:

#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 如下:

#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_declareU_BOOT_CMD_MKENT_COMPLETE。 ll_entry_declar 定义在文件 include/linker_lists.h 中,定义如下:

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

#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,这两个宏的定义如下: 

可以看出,如果定义了宏 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 命令定义如下:

将其展开,结果如下:

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 命令最终展开结果为:

cmd_tbl_t _u_boot_list_2_cmd_2_dhcp __aligned(4) \
__attribute__((unused,section(.u_boot_list_2_cmd_2_dhcp))) \
{ "dhcp", 3, 1, do_dhcp, \
 "boot image via network using DHCP/TFTP protocol", \
 "[loadAddress] [[hostIPaddr:]bootfilename]",\
 NULL}

 第 1 行定义了一个 cmd_tbl_t 类型的变量,变量名为_u_boot_list_2_cmd_2_dhcp,此变量 4字节对齐。

第 2 行 , 使 用 __attribute__ 关 键 字 设 置 变 量 _u_boot_list_2_cmd_2_dhcp 存 储在.u_boot_list_2_cmd_2_dhcp 段中。

u-boot.lds 链接脚本中有一个名为“.u_boot_list”的段,所有.u_boot_list 开头的段都存放到.u_boot.list 中,如图 32.2.11.1 所示:

因此,第 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.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 中,函数内容如下:

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


 

参数 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 函数内容如下:


在前面的分析中我们知道, cmd_tbl_t 的 cmd 成员就是具体的命令处理函数,所以第 494 行调用 cmdtp 的 cmd 成员来处理具体的命令,返回值为命令的执行结果。

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


3 bootz 启动 Linux 内核过程

1 images 全局变量

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

bootm_headers_t images; /* pointers to os/initrd/fdt images */

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

第 335 行的 os 成员变量是 image_info_t 类型的,为系统镜像信息。
第 352~362 行这 11 个宏定义表示 BOOT 的不同阶段。

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


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

2 do_bootz 函数

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

第 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。

3 bootz_start 函数

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


 

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

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

bootz 80000000 – 900000000

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

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

接下来看一下函数 bootm_find_images,此函数定义在文件 common/bootm.c 中,函数内容如下:

第 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 的相关成员变量

4 do_bootm_states 函数

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



 

 

函数 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 函数如下所示:

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 doesnot 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 中,继续分析函数 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 中,函数内容如下:


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

5 bootm_os_get_boot_func 函数

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

 第 495~508 行是条件编译,在本 uboot 中没有用到,因此这段代码无效,只有 509 行有效。

在 509 行中 boot_os 是个数组,这个数组里面存放着不同的系统对应的启动函数。 boot_os 也定义在文件 common/bootm_os.c 中,如下所示:

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

6 do_bootm_linux 函数

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



 

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

 

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

第 74 行,在启动 Linux 之前输出“Starting kernel ...”信息,如图 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 所示:
 

 到这里 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 这条道路上,有众多的学习者陪着你,大家相互搀扶,终能踏出一条康庄大道,祝所有的同学终有所获!


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值