Linux系统移植:U-Boot 启动流程(中)
Linux系统移植:U-Boot 启动流程(中)
一、board_init_f 函数详解
board_init_f 函数是 _main 函数初始化中调用的重要函数之一,函数主要有两个工作:
- 初始化一系列外设,比如串口、定时器,打印一些消息
- 初始化 gd 的各个成员变量,本质上 uboot 是 linux 的引导文件,引导完成后 linux 会在 DRAM 前面的地址区域启动,为了防止 linux 启动后对 uboot 进行干扰,uboot 会将自己重定位到 DRAM 最后面的地址区域
拷贝之前肯定要给 uboot 各部分分配好内存位置和大小,比如 gd 应该存放到哪个位置,malloc 内存池应该存放到哪个位置等。这些信息都保存在 gd 的成员变量中,因此首先要对 gd 的这些成员变量做初始化
此函数定义在文件 common/board_f.c 中:
void board_init_f(ulong boot_flags)
{
#ifdef CONFIG_SYS_GENERIC_GLOBAL_DATA
/*
* For some archtectures, global data is initialized and used before
* calling this function. The data should be preserved. For others,
* CONFIG_SYS_GENERIC_GLOBAL_DATA should be defined and use the stack
* here to host global data until relocation.
*/
gd_t data;
gd = &data;
/*
* Clear global data before it is accessed at debug print
* in initcall_run_list. Otherwise the debug print probably
* get the wrong vaule of gd->have_console.
*/
zero_global_data();
#endif
gd->flags = boot_flags;
gd->have_console = 0;
if (initcall_run_list(init_sequence_f))
hang();
#if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \
!defined(CONFIG_EFI_APP)
/* NOTREACHED - jump_to_copy() does not return */
hang();
#endif
/* Light up LED1 */
imx6_light_up_led1();
}
因为没有定义 CONFIG_SYS_GENERIC_GLOBAL_DATA,后面的代码无效
下面的代码初始化 gd 里面两个成员变量为 0
gd->flags = boot_flags;
gd->have_console = 0;
重点是下面的一个函数 initcall_run_list(init_sequence_f) 函数,从函数名可以知道,运行初始化调用列表里面的函数,传入的参数是 init_sequence_f ,一个存放各个函数入口的数组
这个数组里面有大量条件编译,我直接分析里面部分关键的函数**(这一段是本函数的灵魂)**:
-
setup_mon_len 函数:
设置 gd 的 mon_len 成员变量,此处为 __bss_end - _start,即整个代码的长度
-
initf_malloc 函数:
初始化 gd 中跟 malloc 有关的成员变量,比如 malloc_limit,表示 malloc 内存池大小
-
initf_console_record 函数:
如 果 定 义 了 宏 CONFIG_CONSOLE_RECORD 和 宏 CONFIG_SYS_MALLOC_F_LEN 的话此函数就会调用函数 console_record_init
-
arch_cpu_init 函数:
初始化 CPU
-
**initf_dm 函数: **
驱动模型的一些初始化
-
arch_cpu_init_dm 函数:
空函数
-
mark_bootstage 函数:
设置某些标记,具体作用不清楚
-
board_early_init_f 函数:
板子最先开始的初始化设置,I.MX6ULL 用来初始化串口的 IO 配置
-
timer_init 函数:
初始化定时器,因为 Cortex-A7 内核中有一个定时器,这里初始化的就是 Cortex-A 内核的那个定时器,该定时器来为 uboot 提供时间。类似于 Cortex-M 内核 Systick 定时器(内核定时器和外设定时器基本差不多)
-
board_postclk_init 函数:
I.MX6ULL 是设置 VDDSOC 电压
-
**get_clocks 函数: **
用于获取一些时钟值,I.MX6ULL 获取的是 sdhc_clk 时钟,也就是 SD 卡外设的时钟
-
env_init 函数:
设置 gd 的成员变量 env_addr,也就是环境变量的保存地址
-
init_baud_rate 函数:
用于初始化波特率,根据环境变量的 baudrate 值来初始化 gd->baudrate
-
serial_init 函数:
初始化串口
-
console_init_f 函数:
用于设置 gd->have_console 为 1,表示有个控制台,同时将前面暂存在缓冲区中的数据通过控制台打印出来
-
display_options 函数:
调用该函数来通过串口输出一些信息
这里是 u-boot 版本信息
-
display_text_info 函数:
打印一些文本信息,如果开启 UBOOT 的 DEBUG 功能的话就会输出 text_base、bss_start、bss_end
-
print_cpuinfo 函数:
打印 CPU 信息
-
show_board_info 函数:
打印板子信息
-
INIT_FUNC_WATCHDOG_INIT 函数:
初始化看门狗,对于 I.MX6ULL 来说是空函数
-
INIT_FUNC_WATCHDOG_RESET 函数:
复位看门狗,对于 I.MX6ULL 来说是空函数
-
init_func_i2c 函数:
初始化 I2C
-
announce_dram_init 函数:
输出字符串 “DRAM:”
-
dram_init 函数:
设置 gd->ram_size 的值,对于正点原子 I.MX6ULL 开发板 EMMC 版本核心板来说就是 512MB
-
post_init_f 函数:
完成一些测试,同时初始化 gd->post_init_f_time
-
testdram 函数:
测试 DRAM,空函数
-
setup_dest_addr 函数:
设置目的地址,主要设置 gd->ram_size,gd->ram_top,gd->relocaddr这三个的值,
-
reserve_round_4k 函数:
对 gd->relocaddr 做4KB对 齐
-
reserve_mmu 函数:
留出 MMU 的 TLB 表的位置,分配 MMU 的 TLB 表内存以后会对 gd->relocaddr 做 64K 字节对齐
//IMX 相关信息 gd->arch.tlb_size= 0X4000 //MMU 的 TLB 表大小 gd->arch.tlb_addr=0X9FFF0000 //MMU 的 TLB 表起始地址,64KB 对齐以后 gd->relocaddr=0X9FFF0000 //relocaddr 地址
-
reserve_trace 函数:
留出跟踪调试的内存,I.MX6ULL 没有用到
-
reserve_uboot 函数:
留出重定位后的 uboot 所占用的内存区域,uboot 所占用大小由gd->mon_len 所指定,留出 uboot 的空间以后还要对 gd->relocaddr 做 4K 字节对齐,并且重新设置 gd->start_addr_sp
-
**reserve_malloc 函数: **
留出 malloc 区域,调整 gd->start_addr_sp 位置,malloc 区域由宏 TOTAL_MALLOC_LEN 定义
#define TOTAL_MALLOC_LEN (CONFIG_SYS_MALLOC_LEN + CONFIG_ENV_SIZE)
-
reserve_board 函数:
留出板子 bd 所占的内存区,bd 是结构体 bd_t,bd_t 大小为 80 字节
-
setup_machine 函数:
设置机器 ID,linux 启动的时候会和这个机器 ID 匹配,如果匹配的话 linux 就会启动正常。I.MX6ULL 不用该方式了,该方式是老版本的 uboot 和 linux 使用的,新版本使用设备树启动,因此此函数无效
-
reserve_global_data 函数:
保留出 gd_t 的内存区域
-
reserve_fdt 函数:
留出设备树相关的内存区域
-
reserve_arch 函数:
空函数
-
reserve_stacks 函数:
留出栈空间,先对 gd->start_addr_sp 减去 16,然后做 16 字节对齐
-
setup_dram_config 函数:
设置 dram 信息,就是设置 gd->bd->bi_dram[0].start 和 gd->bd->bi_dram[0].size,后面会传递给 linux 内核,告诉 linux DRAM 的起始地址和大小
-
show_dram_config 函数:
显示 DRAM 的配置
-
display_new_sp 函数:
显示新的 sp 位置,即 gd->start_addr_sp 存放的值
-
reloc_fdt 函数:
用于重定位 fdt,没有用到
-
setup_reloc 函数:
设置 gd 的其他一些成员变量,供后面重定位的时候使用,并且将以前的 gd 拷贝到 gd->new_gd 处
board_init_f 函数就执行完成后,内存分配如下:
二、relocate_code 函数详解
relocate_code 函数是用于代码拷贝,此函数定义在文件 arch/arm/lib/relocate.S 中
首先获取各个地址:
ENTRY(relocate_code)
ldr r1, =__image_copy_start /* r1 <- SRC &__image_copy_start */
subs r4, r0, r1 /* r4 <- relocation offset */
beq relocate_done /* skip relocation */
ldr r2, =__image_copy_end /* r2 <- SRC &__image_copy_end */
- r1=__image_copy_start, r1 寄存器保存镜像源地址
- r0 是 uboot 拷贝的目标首地址,r4=r0-r1 因此 r4 保存偏移量
- r2 中保存拷贝之前的代码结束地址
然后函数 copy_loop 完成代码拷贝工作:
copy_loop:
ldmia r1!, {r10-r11} /* copy from source address [r1] */
stmia r0!, {r10-r11} /* copy to target address [r0] */
cmp r1, r2 /* until source end address [r2] */
blo copy_loop
从 r1 读取 uboot 代码保存到 r10 和 r11 中,一次就只拷贝这 2 个 32 位的数据。拷贝完成以后 r1 的值会更新,保存下一个要拷贝的数据地址,然后将 r10 和 r11 的数据写到 r0 开始的地方,也就是目的地址,写完以后 r0 的值也会更新,更新为下一个要写入的数据地址,末尾比较 r1 是否和 r2 相等,检查是否拷贝完成
后面的代码就是重定位.rel.dyn 段,.rel.dyn 段是存放.text 段中需要重定位地址的集合
重定位就是 uboot 将自身拷贝到 DRAM 的另一个地放去继续运行(DRAM 的高地址处)
/*
* fix .rel.dyn relocations
*/
ldr r2, =__rel_dyn_start /* r2 <- SRC &__rel_dyn_start */
ldr r3, =__rel_dyn_end /* r3 <- SRC &__rel_dyn_end */
fixloop:
ldmia r2!, {r0-r1} /* (r0,r1) <- (SRC location,fixup) */
and r1, r1, #0xff
cmp r1, #23 /* relative fixup? */
bne fixnext
/* relative fix: increase location by offset */
add r0, r0, r4
ldr r1, [r0]
add r1, r1, r4
str r1, [r0]
fixnext:
cmp r2, r3
blo fixloop
relocate_done:
#ifdef __XSCALE__
/*
* On xscale, icache must be invalidated and write buffers drained,
* even with cache disabled - 4.2.7 of xscale core developer's manual
*/
mcr p15, 0, r0, c7, c7, 0 /* invalidate icache */
mcr p15, 0, r0, c7, c10, 4 /* drain write buffer */
#endif
/* ARMv4- don't know bx lr but the assembler fails to see that */
#ifdef __ARM_ARCH_4__
mov pc, lr
#else
bx lr
#endif
ENDPROC(relocate_code)
注意:重定位以后,运行地址就和链接地址不同了,那寻址的时候会不会出问题?原因如下:
首先 uboot 函数寻址时使用到了 bl 指令,而 bl 指令时位置无关指令,bl 指令是相对寻址的 (pc+offset),因此 uboot 中函数调用是与绝对位置无关的,其次函数对变量的访问没有直接进行,而是使用了一个第三方偏移地址,专业术语叫做 Label,这个第三方偏移地址就是实现重定位后运行不会出错的重要原因!
uboot 对于重定位后链接地址和运行地址不一致的解决方法就是 采用位置无关码,在使用 ld 进行链接的时候使用选项“-pie”生成位置无关的可执行文件,生成一个.rel.dyn 段,uboot 就是靠这个.rel.dyn 来解决重定位问题的
三、relocate_vectors 函数详解
函数 relocate_vectors 用于重定位向量表,此函数定义在文件 relocate.S 中
如果定义了 CONFIG_CPU_V7M 的话就执行下面的代码,这是 Cortex-M内核单片机执行的语句,对于 I.MX 来说是无效的
ENTRY(relocate_vectors)
#ifdef CONFIG_CPU_V7M
/*
* On ARMv7-M we only have to write the new vector address
* to VTOR register.
*/
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
ldr r1, =V7M_SCB_BASE
str r0, [r1, V7M_SCB_VTOR]
#else
定义了 CONFIG_HAS_VBAR 的话就执行下面的语句,主要是向量表偏移,Cortex-A7 是支持向量表偏移,重定位后 uboot 的首地址即 r0=gd->relocaddr,因为向量表肯定是从这个地址开始存放的,所以将 r0 的值写入到 CP15 的 VBAR 寄存器中,也就是将新的向量表首地址写入到寄存器 VBAR 中,设置向量表偏移,一般正确执行的就是这段代码
#ifdef CONFIG_HAS_VBAR
/*
* If the ARM processor has the security extensions,
* use VBAR to relocate the exception vectors.
*/
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
mcr p15, 0, r0, c12, c0, 0 /* Set VBAR */
#else
后面的代码将重新定位的异常向量复制到正确的地址
/*
* Copy the relocated exception vectors to the
* correct address
* CP15 c1 V bit gives us the location of the vectors:
* 0x00000000 or 0xFFFF0000.
*/
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
mrc p15, 0, r2, c1, c0, 0 /* V bit (bit[13]) in CP15 c1 */
ands r2, r2, #(1 << 13)
ldreq r1, =0x00000000 /* If V=0 */
ldrne r1, =0xFFFF0000 /* If V=1 */
ldmia r0!, {r2-r8,r10}
stmia r1!, {r2-r8,r10}
ldmia r0!, {r2-r8,r10}
stmia r1!, {r2-r8,r10}
#endif
#endif
bx lr
ENDPROC(relocate_vectors)
四、board_init_r 函数详解
board_init_f 并没有初始化所有的外设,还需要做一些后续工作,后续工作就是由函数 board_init_r 来完成
board_init_r 函数定义在文件 common/board_r.c中,该函数和 board_init_f 差不多,通过调用 initcall_run_list 函数来执行初始化序列 init_sequence_r,init_sequence_r 是一个函数集合,init_sequence_r 也定义在文件 common/board_r.c 中,由于 init_sequence_f 的内容比较长,里面有大量的条件编译代码,这里为了缩小篇幅,将条件编译部分删除掉了
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 };
-
initr_trace 函数:
初始化和调试跟踪有关的内容
-
initr_reloc 函数
设置 gd->flags,标记重定位完成
-
initr_caches 函数
初始化 cache,使能 cache
-
initr_reloc_global_data 函数
初始化重定位后 gd 的一些成员变量
-
initr_barrier 函数
I.MX6ULL 未用到
-
initr_malloc 函数
初始化 malloc
-
initr_console_record 函数
初始化控制台相关的内容,I.MX6ULL 未用到,空函数
-
bootstage_relocate 函数
启动状态重定位
-
initr_bootstage 函数
初始化 bootstage
-
board_init 函数
板级初始化,包括 74XX 芯片,I2C、FEC、USB 和 QSPI 等。这里执行的是 mx6ull_alientek_emmc.c 文件中的 board_init 函数
-
stdio_init_tables 函数
stdio 相关初始化
-
initr_serial 函数
初始化串口
-
initr_announce 函数
通知已经在 RAM 中运行
-
power_init_board 函数
初始化电源芯片,正点原子的 I.MX6ULL 开发板没有用到
-
initr_flash 函数
此函数无效
-
initr_nand 函数
始化 NAND,如果使用 NAND 版本核心板的话就会初始化 NAND
-
initr_mmc 函数
初始化 EMMC,如果使用 EMMC 版本核心板的话就会初始化 EMMC
-
initr_env 函数
初始化环境变量
-
initr_secondary_cpu 函数
初始化其他 CPU 核,I.MX6ULL 只有一个核,因此此函数没用
-
stdio_add_devices 函数
各种输入输出设备的初始化,如 LCD driver,I.MX6ULL 使用 drv_video_init 函数初始化 LCD
-
initr_jumptable 函数
初始化跳转表
-
console_init_r 函数
控制台初始化,初始化完成以后此函数会调用 stdio_print_current_devices 函数来打印出当前的控制台设备
-
interrupt_init 函数
初始化中断
-
initr_enable_interrupts 函数
使能中断
-
initr_ethaddr 函数
初始化网络地址,也就是获取 MAC 地址。读取环境变量 “ethaddr” 的值
-
board_late_init 函数
板子后续初始化,如果环境变量存储在 EMMC 或者 SD 卡中的话此函数会调用 board_late_mmc_env_init 函数初始化 EMMC/SD
-
run_main_loop 函数
跳转到主循环