1、链接脚本 u-boot.lds 详解
要分析 uboot 的启动流程,首先要找到“入口”,找到第一行程序在哪里。程序的链接是由
链接脚本来决定的,所以通过链接脚本可以找到程序的入口。如果没有编译过 uboot 的话链接
脚本为 arch/arm/cpu/u-boot.lds。但是这个不是最终使用的链接脚本,最终的链接脚本是在这个
链接脚本的基础上生成的。编译一下 uboot,编译完成以后就会在 uboot 根目录下生成 u-boot.lds
文件。只有编译 u-boot 以后才会在根目录下出现 u-boot.lds 文件!打开 u-boot.lds,内容如下:
1 OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
2 OUTPUT_ARCH(arm)
3 ENTRY(_start)
4 SECTIONS
5 {
6 . = 0x00000000;
7 . = ALIGN(4);
8 .text :
9 {
10 *(.__image_copy_start)
11 *(.vectors)
12 arch/arm/cpu/armv7/start.o (.text*)
13 *(.text*)
14 }
第 3 行为代码当前入口点: _start, _start 在文件 arch/arm/lib/vectors.S 中有定义。
可以看出, _start 后面就是中断向量表,从图中的“ .section ".vectors", "ax”可以
得到,此代码存放在.vectors 段里面。
使用如下命令在 uboot 中查找“ __image_copy_start”:
grep -nR "__image_copy_start"
打开 u-boot.map,找到如图 32.1.4 所示位置:
u-boot.map 是 uboot 的映射文件,可以从此文件看到某个文件或者函数链接到了哪个地址,
从图 32.1.4 的 932 行可以看到__image_copy_start 为 0X87800000,而.text 的起始地址也是
0X87800000。说明整个 uboot 的起始地址就是 0X87800000。
第 12 行将 arch/arm/cpu/armv7/start.s 编译出来的代码放到中断向量表后面。
第 13 行为 text 段,其他的代码段就放到这里
1、reset 函数源码详解
从 u-boot.lds 中我们已经知道了入口点是 arch/arm/lib/vectors.S 文件中的_start,代码如下:
48 _start:
49
50 #ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
51 .word CONFIG_SYS_DV_NOR_BOOT_CFG
52 #endif
53
54 b reset
55 ldr pc, _undefined_instruction
56 ldr pc, _software_interrupt
57 ldr pc, _prefetch_abort
58 ldr pc, _data_abort
59 ldr pc, _not_used
60 ldr pc, _irq
61 ldr pc, _fiq
第 48 行_start 开始的是中断向量表,其中 54~61 行就是中断向量表,和我们裸机例程里面一样。
54 行跳转到 reset 函数里面, reset 函数在 arch/arm/cpu/armv7/start.S 里面,代码如下:
32 .globl reset
33 .globl save_boot_params_ret
34
35 reset:
36 /* Allow the board to save important registers */
37 b save_boot_params
第 35 行就是 reset 函数。
第 37 行从 reset 函数跳转到了 save_boot_params 函数,而 save_boot_params 函数同样定义
在 start.S 里面,定义如下:
100 ENTRY(save_boot_params)
101 b save_boot_params_ret @ back to my caller
save_boot_params 函数也是只有一句跳转语句,跳转到 save_boot_params_ret 函数,
save_boot_params_ret 函数代码如下:
38 save_boot_params_ret:
39 /*
40 * disable interrupts (FIQ and IRQ), also set the cpu to SVC32
41 * mode, except if in HYP mode already
42 */
43 mrs r0, cpsr
44 and r1, r0, #0x1f @ mask mode bits
45 teq r1, #0x1a @ test for HYP mode
46 bicne r0, r0, #0x1f @ clear all mode bits
47 orrne r0, r0, #0x13 @ set SVC mode
48 orr r0, r0, #0xc0 @ disable FIQ and IRQ
49 msr cpsr,r0
第 43 行,读取寄存器 cpsr 中的值,并保存到 r0 寄存器中。
第 44 行,将寄存器 r0 中的值与 0X1F 进行与运算,结果保存到 r1 寄存器中,目的就是提
取 cpsr 的 bit0~bit4 这 5 位,这 5 位为 M4 M3 M2 M1 M0, M[4:0]这五位用来设置处理器的工作
模式,如表 32.2.1.1 Cortex-A7 工作模式所示:
M[4:0] | 模式 |
---|---|
10000 | User(usr) |
10001 | FIQ(fiq) |
10010 | IRQ(irq) |
10011 | Supervisor(svc) |
10110 | Monitor(mon) |
10111 | Abort(abt) |
11010 | Hyp(hyp) |
11011 | Undefined(und) |
11111 | System(sys) |
第 45 行,判断 r1 寄存器的值是否等于 0X1A(0b11010),也就是判断当前处理器模式是否
处于 Hyp 模式。
第 46 行,如果 r1 和 0X1A 不相等,也就是 CPU 不处于 Hyp 模式的话就将 r0 寄存器的
bit0~5 进行清零,其实就是清除模式位
第 47 行,如果处理器不处于 Hyp 模式的话就将 r0 的寄存器的值与 0x13 进行或运算,
0x13=0b10011,也就是设置处理器进入 SVC 模式。
第 48 行, r0 寄存器的值再与 0xC0 进行或运算,那么 r0 寄存器此时的值就是 0xD3, cpsr
的 I 为和 F 位分别控制 IRQ 和 FIQ 这两个中断的开关,设置为 1 就关闭了 FIQ 和 IRQ!
第 49 行,将 r0 寄存器写回到 cpsr 寄存器中。完成设置 CPU 处于 SVC32 模式,并且关闭
FIQ 和 IRQ 这两个中断。
继续执行执行下面的代码:
56 #if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD))
57 /* Set V=0 in CP15 SCTLR register - for VBAR to point to vector */
58 mrc p15, 0, r0, c1, c0, 0 @ Read CP15 SCTLR Register
59 bic r0, #CR_V @ V = 0
60 mcr p15, 0, r0, c1, c0, 0 @ Write CP15 SCTLR Register
61
62 /* Set vector address in CP15 VBAR register */
63 ldr r0, =_start
64 mcr p15, 0, r0, c12, c0, 0 @Set VBAR
65 #endif
第 56 行,如果没有定义 CONFIG_OMAP44XX 和 CONFIG_SPL_BUILD 的话条件成立,
此处条件成立。
第 58 行读取 CP15 中 c1 寄存器的值到 r0 寄存器中,根据 17.1.4 小节可知,这里是读取
SCTLR 寄存器的值。
第 59 行, CR_V 在 arch/arm/include/asm/system.h 中有如下所示定义:
#define CR_V (1 << 13) /* Vectors relocated to 0xffff0000 */
因此这一行的目的就是清除 SCTLR 寄存器中的 bit13,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 行就是设置向量表重定位的。
代码继续往下执行:
67 /* the mask ROM code should have PLL and others stable */
68 #ifndef CONFIG_SKIP_LOWLEVEL_INIT
69 bl cpu_init_cp15
70 bl cpu_init_crit
71 #endif
72
73 bl _main
第 68 行我们没有定义CONFIG_SKIP_LOWLEVEL_INIT,因此条件成立,执行下面的语句。
别调用函数 cpu_init_cp15、 cpu_init_crit 和_main。
函数 cpu_init_cp15 用来设置 CP15 相关的内容,比如关闭 MMU 啥的,此函数同样在 start.S
文件中定义的,代码如下:
113 ENTRY(cpu_init_cp15)
函数 cpu_init_crit 也在是定义在 start.S 文件中,函数内容如下:
268 ENTRY(cpu_init_crit)
269 /*
270 * Jump to board specific initialization...
271 * The Mask ROM will have already initialized
272 * basic memory. Go here to bump up clock rate and handle
273 * wake up conditions.
274 */
275 b lowlevel_init @ go setup pll,mux,memory
276 ENDPROC(cpu_init_crit)
可以看出函数 cpu_init_crit 内部仅仅是调用了函数 lowlevel_init,接下来就是详细的分析一
下 lowlevel_init 和_main 这两个函数。
2、lowlevel_init 函数详解
函数 lowlevel_init 在文件 arch/arm/cpu/armv7/lowlevel_init.S 中定义,内容如下:
14 #include <asm-offsets.h>
15 #include <config.h>
16 #include <linux/linkage.h>
17
18 ENTRY(lowlevel_init)
19 /*
20 * Setup a temporary stack. Global data is not available yet.
21 */
22 ldr sp, =CONFIG_SYS_INIT_SP_ADDR
23 bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
24 #ifdef CONFIG_SPL_DM
25 mov r9, #0
26 #else
27 /*
28 * Set up global data for boards that still need it. This will be
29 * removed soon.
30 */
31 #ifdef CONFIG_SPL_BUILD
32 ldr r9, =gdata
33 #else
34 sub sp, sp, #GD_SIZE
35 bic sp, sp, #7
36 mov r9, sp
37 #endif
38 #endif
39 /*
40 * Save the old lr(passed in ip) and the current lr to stack
41 */
42 push {ip, lr}
43
44 /*
45 * Call the very early init function. This should do only the
46 * absolute bare minimum to get started. It should not:
47 *
48 * - set up DRAM
49 * - use global_data
50 * - clear BSS
51 * - try to start a console
52 *
53 * For boards with SPL this should be empty since SPL can do all
54 * of this init in the SPL board_init_f() function which is
55 * called immediately after this.
56 */
57 bl s_init
58 pop {ip, pc}
59 ENDPROC(lowlevel_init)
第 22 行设置 sp 指向 CONFIG_SYS_INIT_SP_ADDR, CONFIG_SYS_INIT_SP_ADDR 在
include/configs/mx6ullevk.h 文件中,在 mx6ullevk.h 中有如下所示定义:
示例代码 32.2.2.2 mx6ullevk.h 代码段
234 #define CONFIG_SYS_INIT_RAM_ADDR IRAM_BASE_ADDR
235 #define CONFIG_SYS_INIT_RAM_SIZE IRAM_SIZE
236
237 #define CONFIG_SYS_INIT_SP_OFFSET \
238 (CONFIG_SYS_INIT_RAM_SIZE - GENERATED_GBL_DATA_SIZE)
239 #define CONFIG_SYS_INIT_SP_ADDR \
240 (CONFIG_SYS_INIT_RAM_ADDR + CONFIG_SYS_INIT_SP_OFFSET)
示 例 代 码 32.2.2.2 中 的 IRAM_BASE_ADDR 和 IRAM_SIZE 在 文 件
arch/arm/include/asm/arch-mx6/imx-regs.h 中有定义,如下所示,其实就是 IMX6UL/IM6ULL 内
部 ocram 的首地址和大小。
示例代码 32.2.2.3 imx-regs.h 代码段
71 #define IRAM_BASE_ADDR 0x00900000
......
408 #if !(defined(CONFIG_MX6SX) || defined(CONFIG_MX6UL) || \
409 defined(CONFIG_MX6SLL) || defined(CONFIG_MX6SL))
410 #define IRAM_SIZE 0x00040000
411 #else
412 #define IRAM_SIZE 0x00020000
413 #endif
如果 408 行的条件成立的话 IRAM_SIZE=0X40000,当定义了 CONFIG_MX6SX、
CONFIG_MX6U、 CONFIG_MX6SLL 和 CONFIG_MX6SL 中的任意一个的话条件就不成立,
在.config 中定义了 CONFIG_MX6UL,所以条件不成立,因此 IRAM_SIZE=0X20000=128KB。
结合示例代码 32.2.2.2,可以得到如下值:
CONFIG_SYS_INIT_RAM_ADDR = IRAM_BASE_ADDR = 0x00900000。
CONFIG_SYS_INIT_RAM_SIZE = 0x00020000 =128KB。
还需要知道 GENERATED_GBL_DATA_SIZE 的值,在文件 include/generated/generic-asm-offsets.h
中有定义,如下:
示例代码 32.2.2.4 generic-asm-offsets.h 代码段
1 #ifndef __GENERIC_ASM_OFFSETS_H__
2 #define __GENERIC_ASM_OFFSETS_H__
9#define GENERATED_GBL_DATA_SIZE 256
10 #define GENERATED_BD_INFO_SIZE 80
11 #define GD_SIZE 248
12 #define GD_BD 0
13 #define GD_MALLOC_BASE 192
14 #define GD_RELOCADDR 48
15 #define GD_RELOC_OFF 68
16 #define GD_START_ADDR_SP 64
18 #endif
GENERATED_GBL_DATA_SIZE=256, GENERATED_GBL_DATA_SIZE 的含义为
(sizeof(struct global_data) + 15) & ~15 。
综上所述, CONFIG_SYS_INIT_SP_ADDR 值如下:
CONFIG_SYS_INIT_SP_OFFSET = 0x00020000 – 256 = 0x1FF00。
CONFIG_SYS_INIT_SP_ADDR = 0x00900000 + 0X1FF00 = 0X0091FF00
此时 sp 指向 0X91FF00,这属于 IMX6UL/IMX6ULL 的内部 ram。
继续回到文件 lowlevel_init.S,第 23 行对 sp 指针做 8 字节对齐处理!
第 34 行, sp 指针减去 GD_SIZE, GD_SIZE 同样在 generic-asm-offsets.h 中定了,大小为
248,见示例代码 32.2.2.4 第 11 行。
第 35 行对 sp 做 8 字节对齐,此时 sp 的地址为 0X0091FF00-248=0X0091FE08
第 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 中,如下所示:
示例代码 32.2.3.1 soc.c 代码段
808 void s_init(void)
809 {
810 struct anatop_regs *anatop = (struct anatop_regs
*)ANATOP_BASE_ADDR;
811 struct mxc_ccm_reg *ccm = (struct mxc_ccm_reg *)CCM_BASE_ADDR;
812 u32 mask480;
813 u32 mask528;
814 u32 reg, periph1, periph2;
815
816 if (is_cpu_type(MXC_CPU_MX6SX) || is_cpu_type(MXC_CPU_MX6UL) ||
817 is_cpu_type(MXC_CPU_MX6ULL) || is_cpu_type(MXC_CPU_MX6SLL))
818 return;
在第 816 行会判断当前 CPU 类型,如果 CPU 为 MX6SX、 MX6UL、 MX6ULL 或 MX6SLL
中 的 任 意 一 种 , 那 么 就 会 直 接 返 回 , 相 当 于 s_init 函 数 什 么 都 没 做 。 所 以 对 于
I.MX6UL/I.MX6ULL 来说, s_init 就是个空函数。从 s_init 函数退出以后进入函数 lowlevel_init,
但是 lowlevel_init 函数也执行完成了,返回到了函数 cpu_init_crit,函数 cpu_init_crit 也执行完
成了,最终返回到 save_boot_params_ret,函数调用路径
save_boot_params_ret --> cpu_init_crit --> lowlevel_init --> s_init
|
--> _main
4、_main 函数详解
_main 函数定义在文件 arch/arm/lib/crt0.S 中,函数内容如下:
67 ENTRY(_main)
68
69 /*
70 * Set up initial C runtime environment and call board_init_f(0).
71 */
72
73 #if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
74 ldr sp, =(CONFIG_SPL_STACK)
75 #else
76 ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)
77 #endif
78 #if defined(CONFIG_CPU_V7M) /* v7M forbids using SP as BIC
destination */
79 mov r3, sp
80 bic r3, r3, #7
81 mov sp, r3
82 #else
83 bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
84 #endif
第 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 中,内容如下:
56 ulong board_init_f_alloc_reserve(ulong top)
57 {
58 /* Reserve early malloc arena */
59 #if defined(CONFIG_SYS_MALLOC_F)
60 top -= CONFIG_SYS_MALLOC_F_LEN;
61 #endif
62 /* LAST : reserve GD (rounded up to a multiple of 16 bytes) */
63 top = rounddown(top-sizeof(struct global_data), 16);
64
65 return top;
66 }
函数 board_init_f_alloc_reserve 主要是留出早期的 malloc 内存区域和 gd 内存区域,其中
CONFIG_SYS_MALLOC_F_LEN=0X400( 在 文 件 include/generated/autoconf.h 中 定 义 ) ,
sizeof(struct global_data)=248(GD_SIZE 值)
函数 board_init_f_alloc_reserve 是有返回值的,返回值为新的 top 值,此时 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 中, uboot 中定义了一个指向 gd_t 的指针 gd, gd 存放在寄存器 r9 里面
的,因此 gd 是个全局变量。 gd_t 是个结构体,在 include/asm-generic/global_data.h 里面有定义。
因此这一行代码就是设置 gd 所指向的位置,也就是 gd 指向 0X0091FA00。
第 90 行调用函数 board_init_f_init_reserve,此函数在文件common/init/board_init.c 中有定义。
可以看出,此函数用于初始化 gd,其实就是清零处理。另外,此函数还设置了
gd->malloc_base 为 gd 基地址+gd 大小=0X0091FA00+248=0X0091FAF8,在做 16 字节对齐,最
终 gd->malloc_base=0X0091FB00,这个也就是 early malloc 的起始地址。
第 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 中稍后会详细分析此函数。
第 127 行,调用函数 relocate_vectors,对中断向量表做重定位,此函数定义在文件
arch/arm/lib/relocate.S 中,稍后会详细分析此函数。
第 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 腾出空间,防止 Linux
kernel 覆盖掉 uboot,将 DRAM 前面的区域完整的空出来。在拷贝之前肯定要给 uboot 各部分
分配好内存位置和大小,比如 gd 应该存放到哪个位置, malloc 内存池应该存放到哪个位置等
等。这些信息都保存在 gd 的成员变量中,因此要对 gd 的这些成员变量做初始化。最终形成一
个完整的内存“分配图”,在后面重定位 uboot 的时候就会用到这个内存“分配图”。
此函数定义在文件 common/board_f.c 中定义,代码如下:
1035 void board_init_f(ulong boot_flags)
1036 {
1037 #ifdef CONFIG_SYS_GENERIC_GLOBAL_DATA
1044 gd_t data;
1046 gd = &data;
1053 zero_global_data();
1054 #endif
1055
1056 gd->flags = boot_flags;
1057 gd->have_console = 0;
1058
1059 if (initcall_run_list(init_sequence_f))
1060 hang();
1061
1062 #if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \
1063 !defined(CONFIG_EFI_APP)
1064 /* NOTREACHED - jump_to_copy() does not return */
1065 hang();
1066 #endif
1067 }
因为没有定义 CONFIG_SYS_GENERIC_GLOBAL_DATA,所以第 1037~1054行代码无效。
第 1056 行,初始化 gd->flags=boot_flags=0。
第 1057 行,设置 gd->have_console=0。
重点在第 1059 行!通过函数 initcall_run_list 来运行初始化序列 init_sequence_f 里面的一些
列函数, init_sequence_f 里面包含了一系列的初始化函数, init_sequence_f 也是定义在文件
common/board_f.c 中,由于 init_sequence_f 的内容比较长,里面有大量的条件编译代码,这里
为了缩小篇幅,将条件编译部分删除掉了,去掉条件编译以后的 init_sequence_f 定义如下:
/*****************去掉条件编译语句后的 init_sequence_f***************/
1 static init_fnc_t init_sequence_f[] = {
2 setup_mon_len,
3 initf_malloc,
4 initf_console_record,
5 arch_cpu_init, /* basic arch cpu dependent setup */
6 initf_dm,
7 arch_cpu_init_dm,
8 mark_bootstage, /* need timer, go after init dm */
9 board_early_init_f,
10 timer_init, /* initialize timer */
11 board_postclk_init,
12 get_clocks,
13 env_init, /* initialize environment */
14 init_baud_rate, /* initialze baudrate settings */
15 serial_init, /* serial communications setup */
16 console_init_f, /* stage 1 init of console */
17 display_options, /* say that we are here */
18 display_text_info, /* show debugging info if required */
19 print_cpuinfo, /* display cpu info (and speed) */
20 show_board_info,
21 INIT_FUNC_WATCHDOG_INIT
22 INIT_FUNC_WATCHDOG_RESET
23 init_func_i2c,
24 announce_dram_init,
25 /* TODO: unify all these dram functions? */
26 dram_init, /* configure available RAM banks */
27 post_init_f,
28 INIT_FUNC_WATCHDOG_RESET
29 testdram,
30 INIT_FUNC_WATCHDOG_RESET
31 INIT_FUNC_WATCHDOG_RESET
32 /*
33 * Now that we have DRAM mapped and working, we can
34 * relocate the code and continue running from DRAM.
35 *
36 * Reserve memory at end of RAM for (top down in that order):
37 * - area that won't get touched by U-Boot and Linux (optional)
38 * - kernel log buffer
39 * - protected RAM
40 * - LCD framebuffer
41 * - monitor code
42 * - board info struct
43 */
44 setup_dest_addr,
45 reserve_round_4k,
46 reserve_mmu,
47 reserve_trace,
48 reserve_uboot,
49 reserve_malloc,
50 reserve_board,
51 setup_machine,
52 reserve_global_data,
53 reserve_fdt,
54 reserve_arch,
55 reserve_stacks,
56 setup_dram_config,
57 show_dram_config,
58 display_new_sp,
59 INIT_FUNC_WATCHDOG_RESET
60 reloc_fdt,
61 setup_reloc,
62 NULL,
63 };
接下来分析以上函数执行完以后的结果:
第 2 行, setup_mon_len 函数设置 gd 的 mon_len 成员变量,此处为__bss_end -_start,也就
是整个代码的长度。 0X878A8E74-0x87800000=0XA8E74,这个就是代码长度
第 3 行, initf_malloc 函数初始化 gd 中跟 malloc 有关的成员变量,比如 malloc_limit,此函
数会设置 gd->malloc_limit = CONFIG_SYS_MALLOC_F_LEN=0X400。 malloc_limit 表示 malloc
内存池大小。
第 4 行 , initf_console_record , 如 果 定 义 了 宏 CONFIG_CONSOLE_RECORD 和 宏
CONFIG_SYS_MALLOC_F_LEN 的话此函数就会调用函数 console_record_init,但是 IMX6ULL
的 uboot 没有定义宏 CONFIG_CONSOLE_RECORD,所以此函数直接返回 0。
第 5 行, arch_cpu_init 函数。
第 6 行, initf_dm 函数,驱动模型的一些初始化。
第 7 行, arch_cpu_init_dm 函数未实现。
第 8 行, mark_bootstage 函数应该是和啥标记有关的
第 9 行, board_early_init_f 函数,板子相关的早期的一些初始化设置, I.MX6ULL 用来初始
化串口的 IO 配置
第 10 行, timer_init,初始化定时器, Cortex-A7 内核有一个定时器,这里初始化的就是 CortexA 内核的那个定时器。通过这个定时器来为 uboot 提供时间。就跟 Cortex-M 内核 Systick 定时
器一样。关于 Cortex-A 内部定时器的详细内容,请参考文档《 ARM ArchitectureReference Manual
ARMv7-A and ARMv7-R edition.pdf》 的“ Chapter B8 The Generic Timer”章节。
第 11 行, board_postclk_init,对于 I.MX6ULL 来说是设置 VDDSOC 电压。
第 12 行, get_clocks 函数用于获取一些时钟值, I.MX6ULL 获取的是 sdhc_clk 时钟,也就
是 SD 卡外设的时钟。
第 13 行, env_init 函数是和环境变量有关的,设置 gd 的成员变量 env_addr,也就是环境变
量的保存地址。
第 14 行,init_baud_rate 函数用于初始化波特率,根据环境变量 baudrate 来初始化 gd->baudrate。
第 15 行, serial_init,初始化串口。
第 16 行, console_init_f,设置 gd->have_console 为 1,表示有个控制台,此函数也将前面
暂存在缓冲区中的数据通过控制台打印出来。
第 17 行、 display_options,通过串口输出一些信息。
第 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);
第 19 行, print_cpuinfo 函数用于打印 CPU 信息
第 20 行, show_board_info 函数用于打印板子信息,会调用 checkboard 函数
第 21 行, INIT_FUNC_WATCHDOG_INIT,初始化看门狗,对于 I.MX6ULL 来说是空函数
第 22 行, INIT_FUNC_WATCHDOG_RESET,复位看门狗,对于 I.MX6ULL 来说是空函数
第 23 行, init_func_i2c 函数用于初始化 I2C
第 24 行, announce_dram_init,此函数很简单,就是输出字符串“ DRAM:”
第 26 行, dram_init,并非真正的初始化 DDR,只是设置 gd->ram_size 的值,对于正点原
子 I.MX6ULL 开发板 EMMC 版本核心板来说就是 512MB。
第 27 行, post_init_f,此函数用来完成一些测试,初始化 gd->post_init_f_time
第 29 行, testdram,测试 DRAM,空函数。
第 44行,setup_dest_addr函数,设置目的地址,设置 gd->ram_size,gd->ram_top,gd->relocaddr
这三个的值。接下来我们会遇到很多跟数值有关的设置,如果直接看代码分析的话就太费时间
了,我可以修改 uboot 代码,直接将这些值通过串口打印出来,比如这里我们修改文件
common/board_f.c,因为 setup_dest_addr 函数定义在文件 common/board_f.c 中,在 setup_dest_addr
函数输入
printf("gd->ram_size %#x\r\n",gd->ram_size);
可以看出:
gd->ram_size = 0X20000000 //ram 大小为 0X20000000=512MB
gd->ram_top = 0XA0000000 //ram 最高地址为 0X80000000+0X20000000=0XA0000000
gd->relocaddr = 0XA0000000 //重定位后最高地址为 0XA0000000
第 45 行 , reserve_round_4k 函 数 用 于 对 gd->relocaddr 做 4KB 对 齐 , 因 为
gd->relocaddr=0XA0000000,已经是 4K 对齐了,所以调整后不变。
第 46 行, reserve_mmu,留出 MMU 的 TLB 表的位置,分配 MMU 的 TLB 表内存以后会
对 gd->relocaddr 做 64K 字节对齐。完成以后 gd->arch.tlb_size、gd->arch.tlb_addr 和 gd->relocaddr
gd->arch.tlb_size= 0X4000 //MMU 的 TLB 表大小
gd->arch.tlb_addr=0X9FFF0000 //MMU 的 TLB 表起始地址, 64KB 对齐以后
gd->relocaddr=0X9FFF0000 //relocaddr 地址
第 47 行, reserve_trace 函数,留出跟踪调试的内存, I.MX6ULL 没有用到!
第 48 行, reserve_uboot, 留出重定位后的 uboot 所占用的内存区域, uboot 所占用大小由
gd->mon_len 所指定,留出 uboot 的空间以后还要对 gd->relocaddr 做 4K 字节对齐,并且重新设
置 gd->start_addr_sp
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
可以看出:
TOTAL_MALLOC_LEN=0X1002000
gd->start_addr_sp=0X9EF45000 //0X9FF47000-16MB-8KB=0X9EF45000
第 50 行, reserve_board 函数,留出板子 bd 所占的内存区, bd 是结构体 bd_t, bd_t 大小为
80 字节
可以看出:
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
gd->start_addr_sp=0X9EF44EB8 //0X9EF44FB0-248=0X9EF44EB8
gd->new_gd=0X9EF44EB8
第 53 行, reserve_fdt,留出设备树相关的内存区域, I.MX6ULL 的 uboot 没有用到,因此
此函数无效。
第 54 行, reserve_arch 是个空函数。
第 55 行, reserve_stacks,留出栈空间,先对 gd->start_addr_sp 减去 16,然后做 16 字节对
其。如果使能 IRQ 的话还要留出 IRQ 相应的内存,具体工作是由 arch/arm/lib/stack.c 文件中的
函数 arch_reserve_stacks 完成。
在本 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 的起始地址和大小。
DRAM 的起始地址为 0X80000000,大小为 0X20000000(512MB)
第 57 行, show_dram_config 函数,用于显示 DRAM 的配置
第 58 行, display_new_sp 函数,显示新的 sp 位置,也就是 gd->start_addr_sp,不过要定义
宏 DEBUG
第 60 行, reloc_fdt 函数用于重定位 fdt,没有用到。
第 61 行, setup_reloc,设置 gd 的其他一些成员变量,供后面重定位的时候使用,并且将以
前的 gd 拷贝到 gd->new_gd 处。需要使能 DEBUG 才能看到相应的信息输出
uboot 重定位后的偏移为 0X18747000,重定位后的新地址为
0X9FF4700,新的 gd 首地址为 0X9EF44EB8,最终的 sp 为 0X9EF44E90。
6、relocate_code 函数详解
relocate_code 函数是用于代码拷贝的,此函数定义在文件 arch/arm/lib/relocate.S 中,代码如
下:
79 ENTRY(relocate_code)
80 ldr r1, =__image_copy_start /* r1 <- SRC &__image_copy_start */
81 subs r4, r0, r1 /* r4 <- relocation offset */
82 beq relocate_done /* skip relocation */
83 ldr r2, =__image_copy_end /* r2 <- SRC &__image_copy_end */
85 copy_loop:
86 ldmia r1!, {r10-r11} /* copy from source address [r1] */
87 stmia r0!, {r10-r11} /* copy to target address [r0] */
88 cmp r1, r2 /* until source end address [r2] */
89 blo copy_loop
90
94 ldr r2, =__rel_dyn_start /* r2 <- SRC &__rel_dyn_start */
95 ldr r3, =__rel_dyn_end /* r3 <- SRC &__rel_dyn_end */
96 fixloop:
97 ldmia r2!, {r0-r1} /* (r0,r1) <- (SRC location,fixup) */
98 and r1, r1, #0xff
99 cmp r1, #23 /* relative fixup? */
100 bne fixnext
101
102 /* relative fix: increase location by offset */
103 add r0, r0, r4
104 ldr r1, [r0]
105 add r1, r1, r4
106 str r1, [r0]
107 fixnext:
108 cmp r2, r3
109 blo fixloop
110
111 relocate_done:
113 #ifdef __XSCALE__
118 mcr p15, 0, r0, c7, c7, 0 /* invalidate icache */
119 mcr p15, 0, r0, c7, c10, 4 /* drain write buffer */
120 #endif
124 #ifdef __ARM_ARCH_4__
125 mov pc, lr
126 #else
127 bx lr
128 #endif
130 ENDPROC(relocate_code)
第 80 行, r1=__image_copy_start,也就是 r1 寄存器保存源地址,__image_copy_start=0X87800000。
第 81 行, r0=0X9FF47000,这个地址就是 uboot 拷贝的目标首地址。 r4=r0-r1=0X9FF47000-
0X87800000=0X18747000,因此 r4 保存偏移量。
第 82 行,如果在第 81 中, r0-r1 等于 0,说明 r0 和 r1 相等,也就是源地址和目的地址是
一样的,那肯定就不需要拷贝了!执行 relocate_done 函数
第 83 行, r2=__image_copy_end, r2 中保存拷贝之前的代码结束地址,由表 31.4.1.1 可知,
__image_copy_end =0x8785dd54。
第 84 行,函数 copy_loop 完成代码拷贝工作!从 r1,也就是__image_copy_start 开始,读
取 uboot 代码保存到 r10 和 r11 中,一次就只拷贝这 2 个 32 位的数据。拷贝完成以后 r1 的值会
更新,保存下一个要拷贝的数据地址。
第 87 行,将 r10 和 r11 的数据写到 r0 开始的地方,也就是目的地址。写完以后 r0 的值会
更新,更新为下一个要写入的数据地址。
第 88 行,比较 r1 是否和 r2 相等,也就是检查是否拷贝完成,如果不相等的话说明没有拷
贝完成, 没有拷贝完成的话就跳转到 copy_loop 接着拷贝,直至拷贝完成。
接下来的第 94 行~109 行是重定位.rel.dyn 段, .rel.dyn 段是存放.text 段中需要重定位地址的
集合。重定位就是 uboot 将自身拷贝到 DRAM 的另一个地放去继续运行(DRAM 的高地址处)。
我们知道,一个可执行的 bin 文件,其链接地址和运行地址要相等,也就是链接到哪个地址,
在运行之前就要拷贝到哪个地址去。现在我们重定位以后,运行地址就和链接地址不同了,这
样寻址的时候不会出问题吗?为了分析这个问题,我们需要在 mx6ull_alientek_emmc.c 中输入
如下所示内容:
1 static int rel_a = 0;
2 3
void rel_test(void)
4 {
5 rel_a = 100;
6 printf("rel_test\r\n");
7 }
最后还需要在 mx6ullevk.c 文件中的 board_init 函数里面调用 rel_test 函数,否则 rel_reset 不
会被编译进 uboot。
board_init 函数会调用 rel_test, rel_test 会调用全局变量 rel_a,使用如下命令编译 uboot:
./mx6ull_alientek_emmc.sh
编译完成以后,使用 arm-linux-gnueabihf-objdump 将 u-boot 进行反汇编,得到 u-boot.dis 这
个汇编文件,命令如下:
arm-linux-gnueabihf-objdump -D -m arm u-boot > u-boot.dis
在 u-boot.dis 文件中找到 rel_a、 rel_rest 和 board_init,相关内容如下所示:
示例代码 32.2.6.3 汇编文件代码段
1 87804184 <rel_test>:
2 87804184: e59f300c ldr r3, [pc, #12] ; 87804198 <rel_test+0x14>
3 87804188: e3a02064 mov r2, #100 ; 0x64
4 8780418c: e59f0008 ldr r0, [pc, #8] ; 8780419c <rel_test+0x18>
5 87804190: e5832000 str r2, [r3]
6 87804194: ea00d668 b 87839b3c <printf>
7 87804198: 8785da50 ; <UNDEFINED> instruction: 0x8785da50
8 8780419c: 878426a2 strhi r2, [r4, r2, lsr #13]
9
10 878041a0 <board_init>:
11 878041a0: e92d4010 push {r4, lr}
12 878041a4: ebfffff6 bl 87804184 <rel_test>
13
14 ......
15
16 8785da50 <rel_a>:
17 8785da50: 00000000 andeq r0, r0, r0
第 12 行是 borad_init 调用 rel_test 函数,用到了 bl 指令,而 bl 指令时位置无关指令, bl 指
令是相对寻址的(pc+offset),因此 uboot 中函数调用是与绝对位置无关的。
再来看一下函数 rel_test 对于全局变量 rel_a 的调用,第 2 行设置 r3 的值为 pc+12 地址处的
值,因为ARM流水线的原因,pc寄存器的值为当前地址+8,因此pc=0X87804184+8=0X8780418C,
r3=0X8780418C+12=0X87804198,第 7 行就是 0X87804198 这个地址, 0X87804198 处的值为
0X8785DA50。根据第 17 行可知, 0X8785DA50 正是变量 rel_a 的地址,最终 r3=0X8785DA50。
第 3 行, r2=100。
第 5 行,将 r2 内的值写到 r3 地址处,也就是设置地址 0X8785DA50 的值为 100,这不就
是示例代码代码 32.2.6.2 中的第 5 行: rel_a = 100。
总结一下 rel_a=100 的汇编执行过程:
①、在函数 rel_test 末尾处有一个地址为 0X87804198 的内存空间(示例代码 32.2.6.3 第 7
行),此内存空间保存着变量 rel_a 的地址。
②、函数 rel_test 要想访问变量 rel_a,首先访问末尾的 0X87804198 来获取变量 rel_a 的地
址,而访问 0X87804198 是通过偏移来访问的,很明显是个位置无关的操作。
③、通过 0X87804198 获取到变量 rel_a 的地址,对变量 rel_a 进行操作。
④、可以看出,函数 rel_test 对变量 rel_a 的访问没有直接进行,而是使用了一个第三方偏
移地址 0X87804198,专业术语叫做 Label。这个第三方偏移地址就是实现重定位后运行不会出
错的重要原因!
uboot 重 定 位 后 偏 移 为 0X18747000 , 那 么 重 定 位 后 函 数 rel_test 的 首 地 址 就 是
0X87804184+0X18747000=0X9FF4B184 。 保 存 变 量 rel_a 地 址 的 Label 就 是
0X9FF4B184+8+12=0X9FF4B198( 既 : 0X87804198+0X18747000) , 变 量 rel_a 的 地 址 就 为
0X8785DA50+0X18747000=0X9FFA4A50。重定位后函数 rel_test 要想正常访问变量 rel_a 就得
设置 0X9FF4B198(重定位后的 Label)地址出的值为 0X9FFA4A50(重定位后的变量 rel_a 地址)。
这样就解决了重定位后链接地址和运行地址不一致的问题。
可以看出, uboot 对于重定位后链接地址和运行地址不一致的解决方法就是采用位置无关
码,在使用 ld 进行链接的时候使用选项“-pie”生成位置无关的可执行文件。在文件
arch/arm/config.mk 下有如下代码:
示例代码 32.2.6.4 config.mk 文件代码段
82 # needed for relocation
83 LDFLAGS_u-boot += -pie
第 83 行就是设置 uboot 链接选项,加入了“ -pie”选项,编译链接 uboot 的时候就会使用到“ -pie”
使用“ -pie”选项以后会生成一个.rel.dyn 段, uboot 就是靠这个.rel.dyn 来解决重定位问题
的,在 u-bot.dis 的.rel.dyn 段中有如下所示内容:
示例代码 32.2.6.5 .rel.dyn 段代码段
1 Disassembly of section .rel.dyn:
2 3
8785da44 <__rel_dyn_end-0x8ba0>:
4 8785da44: 87800020 strhi r0, [r0, r0, lsr #32]
5 8785da48: 00000017 andeq r0, r0, r7, lsl r0
6 ......
7 8785dfb4: 87804198 ; <UNDEFINED> instruction: 0x87804198
8 8785dfb8: 00000017 andeq r0, r0, r7, lsl r0
先来看一下.rel.dyn 段的格式,类似第 7 行和第 8 行这样的是一组,也就是两个 4 字节数据
为一组。高 4 字节是 Label 地址标识 0X17,低 4 字节就是 Label 的地址,首先判断 Label 地址
标识是否正确,也就是判断高 4 字节是否为 0X17,如果是的话高 4 字节就是 Label 值。
第 7 行值为 0X87804198,第 8 行为 0X00000017,说明第 7 行的 0X87804198 是个 Label,
这个正是示例代码 32.2.6.3 中存放变量 rel_a 地址的那个 Label。根据前面的分析,只要将地址
0X87804198+offset 处的值改为重定位后的变量 rel_a 地址即可。我们猜测的是否正确,看一下
uboot 对.rel.dyn 段的重定位即可(示例代码代码 32.2.6.1 中的第 94~109 行), .rel.dyn 段的重定位
代码如下:
示例代码 32.2.6.6 relocate.S 代码段
91 /*
92 * fix .rel.dyn relocations
93 */
94 ldr r2, =__rel_dyn_start /* r2 <- SRC &__rel_dyn_start */
95 ldr r3, =__rel_dyn_end /* r3 <- SRC &__rel_dyn_end */
96 fixloop:
97 ldmia r2!, {r0-r1} /* (r0,r1) <- (SRC location,fixup) */
98 and r1, r1, #0xff
99 cmp r1, #23 /* relative fixup? */
100 bne fixnext
101
102 /* relative fix: increase location by offset */
103 add r0, r0, r4
104 ldr r1, [r0]
105 add r1, r1, r4
106 str r1, [r0]
107 fixnext:
108 cmp r2, r3
109 blo fixloop
第 94 行, r2=__rel_dyn_start,也就是.rel.dyn 段的起始地址。
第 95 行, r3=__rel_dyn_end,也就是.rel.dyn 段的终止地址。
第 97 行,从.rel.dyn 段起始地址开始,每次读取两个 4 字节的数据存放到 r0 和 r1 寄存器
中, r0 存放低 4 字节的数据,也就是 Label 地址; r1 存放高 4 字节的数据,也就是 Label 标志。
第 98 行, r1 中给的值与 0xff 进行与运算,其实就是取 r1 的低 8 位。
第 99 行,判断 r1 中的值是否等于 23(0X17)。
第 100 行,如果 r1 不等于 23 的话就说明不是描述 Label 的,执行函数 fixnext,否则的话
继续执行下面的代码。
第 103 行, r0 保存着 Label 值, r4 保存着重定位后的地址偏移, r0+r4 就得到了重定位后的
Label 值。此时 r0 保存着重定位后的 Label 值,相当于 0X87804198+0X18747000=0X9FF4B198。
第 104,读取重定位后 Label 所保存的变量地址,此时这个变量地址还是重定位前的(相当于 rel_a 重定位前的地址 0X8785DA50),将得到的值放到 r1 寄存器中。
第 105 行 , r1+r4 即 可 得 到 重 定 位 后 的 变 量 地 址 , 相 当 于 rel_a 重 定 位 后 的
0X8785DA50+0X18747000=0X9FFA4A50。
第 106 行,重定位后的变量地址写入到重定位后的 Label 中,相等于设置地址 0X9FF4B198
处的值为 0X9FFA4A50。
第 108 行,比较 r2 和 r3,查看.rel.dyn 段重定位是否完成。
第 109 行,如果 r2 和 r3 不相等,说明.rel.dyn 重定位还未完成,因此跳到 fixloop 继续重定
位.rel.dyn 段。
可以看出, uboot 中对.rel.dyn 段的重定位方法和我们猜想的一致。 .rel.dyn 段的重定位比较
复杂一点,有点绕,因为涉及到链接地址和运行地址的问题。
7、 relocate_vectors 函数详解
函数 relocate_vectors 用于重定位向量表,此函数定义在文件函数源码如下:
27 ENTRY(relocate_vectors)
28
29 #ifdef CONFIG_CPU_V7M
...
38 #ifdef CONFIG_HAS_VBAR
39 /*
40 * If the ARM processor has the security extensions,
41 * use VBAR to relocate the exception vectors.
42 */
43 ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
44 mcr p15, 0, r0, c12, c0, 0 /* Set VBAR */
45 #else
第 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
中,代码如下:
示例代码 32.2.8.1 board_r.c 代码段
991 void board_init_r(gd_t *new_gd, ulong dest_addr)
992 {
...
1010 if (initcall_run_list(init_sequence_r))
1011 hang();
1012
1013 /* NOTREACHED - run_main_loop() does not return */
1014 hang();
1015 }
第 1010 行调用 initcall_run_list 函数来执行初始化序列 init_sequence_r, init_sequence_r 是
一个函数集合, init_sequence_r 也定义在文件 common/board_r.c 中,由于 init_sequence_f 的内容
比较长,里面有大量的条件编译代码,这里为了缩小篇幅,将条件编译部分删除掉了,去掉条
件编译以后的 init_sequence_r 定义如下:
示例代码 32.2.8.2 board_r.c 代码段
1 init_fnc_t init_sequence_r[] = {
2 initr_trace,
3 initr_reloc,
4 initr_caches,
5 initr_reloc_global_data,
6 initr_barrier,
7 initr_malloc,
8 initr_console_record,
9 bootstage_relocate,
10 initr_bootstage,
11 board_init, /* Setup chipselects */
12 stdio_init_tables,
13 initr_serial,
14 initr_announce,
15 INIT_FUNC_WATCHDOG_RESET
16 INIT_FUNC_WATCHDOG_RESET
17 INIT_FUNC_WATCHDOG_RESET
18 power_init_board,
19 initr_flash,
20 INIT_FUNC_WATCHDOG_RESET
21 initr_nand,
22 initr_mmc,
23 initr_env,
24 INIT_FUNC_WATCHDOG_RESET
25 initr_secondary_cpu,
26 INIT_FUNC_WATCHDOG_RESET
27 stdio_add_devices,
28 initr_jumptable,
29 console_init_r, /* fully init console as a device */
30 INIT_FUNC_WATCHDOG_RESET
31 interrupt_init,
32 initr_enable_interrupts,
33 initr_ethaddr,
34 board_late_init,
35 INIT_FUNC_WATCHDOG_RESET
36 INIT_FUNC_WATCHDOG_RESET
37 INIT_FUNC_WATCHDOG_RESET
38 initr_net,
39 INIT_FUNC_WATCHDOG_RESET
40 run_main_loop,
41 };
第 2 行, initr_trace 函数,如果定义了宏 CONFIG_TRACE 的话就会调用函数 trace_init,
初始化和调试跟踪有关的内容。
第 3 行, initr_reloc 函数用于设置 gd->flags,标记重定位完成。
第 4 行, initr_caches 函数用于初始化 cache,使能 cache。
第 5 行, initr_reloc_global_data 函数,初始化重定位后 gd 的一些成员变量。
第 6 行, initr_barrier 函数, I.MX6ULL 未用到。
第 7 行, initr_malloc 函数,初始化 malloc。
第 8 行, initr_console_record 函数,初始化控制台相关的内容, I.MX6ULL 未用到,空函数。
第 9 行, bootstage_relocate 函数,启动状态重定位。
第 10 行, initr_bootstage 函数,初始化 bootstage 什么的。
第 11 行, board_init 函数,板级初始化,包括 74XX 芯片, I2C、 FEC、 USB 和 QSPI 等。
这里执行的是 mx6ull_alientek_emmc.c 文件中的 board_init 函数。
第 12 行, stdio_init_tables 函数, stdio 相关初始化。
第 13 行, initr_serial 函数,初始化串口。
第 14 行, initr_announce 函数,与调试有关,通知已经在 RAM 中运行。
第 18 行, power_init_board 函数,初始化电源芯片,正点原子的 I.MX6ULL 开发板没有用
到。
第 19 行, initr_flash 函数,对于 I.MX6ULL 而言,没有定义宏 CONFIG_SYS_NO_FLASH
的话函数 initr_flash 才有效。但是 mx6_common.h 中定义了宏 CONFIG_SYS_NO_FLASH,所以
此函数无效。
第 21 行, initr_nand 函数,初始化 NAND,如果使用 NAND 版本核心板的话就会初始化
NAND。
第 22 行, initr_mmc 函数,初始化 EMMC,如果使用 EMMC 版本核心板的话就会初始化
EMMC
第 23 行, initr_env 函数,初始化环境变量。
第 25 行, initr_secondary_cpu 函数,初始化其他 CPU 核, I.MX6ULL 只有一个核,因此此
函数没用。
第 27 行, stdio_add_devices 函数,各种输入输出设备的初始化,如 LCD driver, I.MX6ULL
使用 drv_video_init 函数初始化 LCD
第 28 行, initr_jumptable 函数,初始化跳转表。
第 29 行 , console_init_r 函 数 , 控 制 台 初 始 化 , 初 始 化 完 成 以 后 此 函 数 会 调 用
stdio_print_current_devices 函数来打印出当前的控制台设备
第 31 行, interrupt_init 函数,初始化中断。
第 32 行, initr_enable_interrupts 函数,使能中断。
第 33 行, initr_ethaddr 函数,初始化网络地址,也就是获取 MAC 地址。读取环境变量
“ ethaddr”的值。
第 34 行, board_late_init 函数,板子后续初始化,此函数定义在文件 mx6ull_alientek_emmc.c
中,如果环境变量存储在 EMMC 或者 SD 卡中的话此函数会调用 board_late_mmc_env_init 函数
初始化 EMMC/SD。会切换到正在时候用的 emmc 设备
第 38 行 , initr_net 函 数 , 初 始 化 网 络 设 备 , 函 数 调 用 顺 序 为 :
initr_net->eth_initialize->board_eth_init()
第 40 行, run_main_loop 行,主循环,处理命令。
9、run_main_loop 函数详解
uboot 启动以后会进入 3 秒倒计时,如果在 3 秒倒计时结束之前按下按下回车键,那么就
会进入 uboot 的命令模式,如果倒计时结束以后都没有按下回车键,那么就会自动启动 Linux 内
核 , 这 个 功 能 就 是 由 run_main_loop 函 数 来 完 成 的 。 run_main_loop 函 数 定 义 在 文 件
common/board_r.c 中,函数内容如下:
示例代码 32.2.9.1 board_r.c 文件代码段
753 static int run_main_loop(void)
754 {
755 #ifdef CONFIG_SANDBOX
756 sandbox_main_loop_init();
757 #endif
758 /* main_loop() can return to retry autoboot, if so just run it
again */
759 for (;;)
760 main_loop();
761 return 0;
762 }
第 7 行和第 8 行是个死循环,“ for(;;)”和“ while(1)”功能一样,死循环里面就一个
main_loop 函数, main_loop 函数定义在文件 common/main.c 里面,代码如下:
示例代码 32.2.9.2 main.c 文件代码段
43 /* We come here after U-Boot is initialised and ready to process
commands */
44 void main_loop(void)
45 {
46 const char *s;
47
48 bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");
49
50 #ifndef CONFIG_SYS_GENERIC_BOARD
51 puts("Warning: Your board does not use generic board. Please
read\n");
52 puts("doc/README.generic-board and take action. Boards not\n");
53 puts("upgraded by the late 2014 may break or be removed.\n");
54 #endif
55
56 #ifdef CONFIG_VERSION_VARIABLE
57 setenv("ver", version_string); /* set version variable */
58 #endif /* CONFIG_VERSION_VARIABLE */
60 cli_init();
61
62 run_preboot_environment_command();
63
64 #if defined(CONFIG_UPDATE_TFTP)
65 update_tftp(0UL, NULL, NULL);
66 #endif /* CONFIG_UPDATE_TFTP */
67
68 s = bootdelay_process();
69 if (cli_process_fdt(&s))
70 cli_secure_boot_cmd(s);
71
72 autoboot_command(s);
73
74 cli_loop();
75 }
第 48 行,调用 bootstage_mark_name 函数,打印出启动进度。
第 57 行,如果定义了宏 CONFIG_VERSION_VARIABLE 的话就会执行函数 setenv,设置
换将变量 ver 的值为 version_string,也就是设置版本号环境变量。 version_string 定义在文件
cmd/version.c 中,定义如下:
const char __weak version_string[] = U_BOOT_VERSION_STRING;
U_BOOT_VERSION_STRING 是个宏, 定义在文件 include/version.h,如下:
#define U_BOOT_VERSION_STRING U_BOOT_VERSION " (" U_BOOT_DATE " - " \
U_BOOT_TIME " " U_BOOT_TZ ")" CONFIG_IDENT_STRING
U_BOOT_VERSION 定 义 在 文 件 include/generated/version_autogenerated.h 中 , 文 件
version_autogenerated.h 内如如下:
示例代码 32.2.9.4 version_autogenerated.h 文件代码
1 #define PLAIN_VERSION "2016.03"
2 #define U_BOOT_VERSION "U-Boot " PLAIN_VERSION
3 #define CC_VERSION_STRING "arm-linux-gnueabihf-gcc (Linaro GCC 4.9-
2017.01) 4.9.4"
4 #define LD_VERSION_STRING "GNU ld (Linaro_Binutils-2017.01)
2.24.0.20141017 Linaro 2014_11-3-git"
可以看出, U_BOOT_VERSION 为“ U-boot 2016.03”,
U_BOOT_DATE 、 U_BOOT_TIME 和 U_BOOT_TZ 这 定 义 在 文 件
include/generated/timestamp_autogenerated.h 中,如下所示:
示例代码 32.2.9.5 timestamp_autogenerated.h 文件代码
1 #define U_BOOT_DATE "Apr 25 2019"
2 #define U_BOOT_TIME "21:10:53"
3 #define U_BOOT_TZ "+0800"
4 #define U_BOOT_DMI_DATE "04/25/2019"
宏 CONFIG_IDENT_STRING 为空,所以 U_BOOT_VERSION_STRING 为“ U-Boot 2016.03
(Apr 25 2019 - 21:10:53 +0800)”,进入 uboot 命令模式,输入命令“ version”查看版本号
第 60 行, cli_init 函数,跟命令初始化有关,初始化 hush
shell 相关的变量。
第 62 行, run_preboot_environment_command 函数,获取环境变量 perboot 的内容, perboot
是一些预启动命令,一般不使用这个环境变量。
第 68 行, bootdelay_process 函数,此函数会读取环境变量 bootdelay 和 bootcmd 的内容,
然后将 bootdelay 的值赋值给全局变量 stored_bootdelay,返回值为环境变量 bootcmd 的值。
第 69 行,如果定义了 CONFIG_OF_CONTROL 的话函数 cli_process_fdt 就会实现,如果
没有定义 CONFIG_OF_CONTROL 的话函数 cli_process_fdt 直接返回一个 false。在本 uboot 中
没有定义 CONFIG_OF_CONTROL,因此 cli_process_fdt 函数返回值为 false。
第 72 行, autoboot_command 函数,此函数就是检查倒计时是否结束?倒计时结束之前有
没有被打断?此函数定义在文件 common/autoboot.c 中,宏CONFIG_AUTOBOOT_KEYED 、 CONFIG_AUTOBOOT_KEYED_CTRLC 和
CONFIG_MENUKEY 这三个宏在 I.MX6ULL 里面没有定义,精简内容如下:
示例代码 32.2.9.6 autoboot_command 函数精简版本
1 void autoboot_command(const char *s)
2 {
3 if (stored_bootdelay != -1 && s && !abortboot(stored_bootdelay)) {
4 run_command_list(s, -1, 0);
5 }
6 }
当一下三条全部成立的话,就会执行函数 run_command_list。
①、 stored_bootdelay 不等于-1。
②、 s 不为空。
③、函数 abortboot 返回值为 0。
stored_bootdelay 等于环境变量 bootdelay 的值; s 是环境变量 bootcmd 的值,一般不为空,
因 此 前 两 个 成 立 , 就剩 下 了 函 数 abortboot 的 返 回 值 , abortboot 函数 也 定 义 在 文 件
common/autoboot.c 中,内容如下:
示例代码 32.2.9.7 abortboot 函数
283 static int abortboot(int bootdelay)
284 {
285 #ifdef CONFIG_AUTOBOOT_KEYED
286 return abortboot_keyed(bootdelay);
287 #else
288 return abortboot_normal(bootdelay);
289 #endif
290 }
因为宏 CONFIG_AUTOBOOT_KEYE 未定义,因此执行函数 abortboot_normal,好吧,绕
来绕去的!接着来看函数 abortboot_normal,此函数也定义在文件 common/autoboot.c 中,内容
如下:
示例代码 32.2.9.9 abortboot_normal 函数精简
1 static int abortboot_normal(int bootdelay)
2 {
3 int abort = 0;
4 unsigned long ts;
5
6 if (bootdelay >= 0)
7 printf("Hit any key to stop autoboot: %2d ", bootdelay);
9 while ((bootdelay > 0) && (!abort)) {
10 --bootdelay;
11 /* delay 1000 ms */
12 ts = get_timer(0);
13 do {
14 if (tstc()) { /* we got a key press */
15 abort = 1; /* don't auto boot */
16 bootdelay = 0; /* no more delay */
17 (void) getc(); /* consume input */
18 break;
19 }
20 udelay(10000);
21 } while (!abort && get_timer(ts) < 1000);
22
23 printf("\b\b\b%2d ", bootdelay);
24 }
25 putc('\n');
26 return abort;
27 }
第 3 行的变量 abort 是函数 abortboot_normal 的返回值,默认值为 0。
第 7 行通过串口输出“ Hit any key to stop autoboot”字样
第 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 函数详解
【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.3.pdf