Linux系统移植:U-Boot 启动流程(中)

Linux系统移植:U-Boot 启动流程(中)

一、board_init_f 函数详解

board_init_f 函数是 _main 函数初始化中调用的重要函数之一,函数主要有两个工作:

  1. 初始化一系列外设,比如串口、定时器,打印一些消息
  2. 初始化 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 函数:

    调用该函数来通过串口输出一些信息

    20211229170650

    这里是 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 函数就执行完成后,内存分配如下:

20211229172141

二、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 函数

    跳转到主循环