通过对 uboot 启动流程的梳理,我们就可以掌握一些外设是在哪里被初始化的,这样当我们需要修改这些外设驱动的时候就会心里有数。
一、链接脚本 u-boot.lds
分析 uboot 的启动流程,首先要找到“入口”,找到第一行程序在哪里。程序的链接是由链接脚本来决定的,所以通过链接脚本可以找到程序的入口。如果没有编译过 uboot 的话链接脚本为 arch/arm/cpu/u-boot.lds。但是这个不是最终使用的链接脚本,最终的链接脚本是在这个链接脚本的基础上生成的。编译一下 uboot,编译完成以后就会在 uboot 根目录下生成 u-boot.lds文件。跟上次一样的编译方法。
打开 u-boot.lds:
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start) # 代码当前入口点:_start
SECTIONS
{
. = 0x00000000;
. = ALIGN(4);
.text :
{
*(.__image_copy_start)
*(.vectors) # vectors 段保存中断向量表
arch/arm/cpu/armv7/start.o (.text*) # 编译出来的代码放到中断向量表后面
}
......
.dynsym _image_binary_end : { *(.dynsym) }
.dynbss : { *(.dynbss) }
.dynstr : { *(.dynstr*) }
.dynamic : { *(.dynamic*) }
.plt : { *(.plt*) }
.interp : { *(.interp*) }
.gnu.hash : { *(.gnu.hash) }
.gnu : { *(.gnu*) }
.ARM.exidx : { *(.ARM.exidx*) }
.gnu.linkonce.armexidx : { *(.gnu.linkonce.armexidx.*) }
}
ENTRY(_start) 为代码当前入口点: _start, _start 在文件 arch/arm/lib/vectors.S 中有定义:
.globl _start
.section ".vectors", "ax"
_start:
#ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
.word CONFIG_SYS_DV_NOR_BOOT_CFG
#endif
ARM_VECTORS
#endif /* !defined(CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK) */
.globl _reset
.globl _undefined_instruction
.globl _software_interrupt
.globl _prefetch_abort
.globl _data_abort
.globl _not_used
.globl _irq
.globl _fiq
_start 后面就是中断向量表,.section:是一个汇编器指令,用于指定代码的存储段;".vectors":这是存储段的名称,被双引号括起来表示它是一个字符串;"ax":这是存储段的属性。在这种情况下,"ax"表示该段是可执行(executable)且可写(writable)的。
使用如下命令在 uboot 中查找“__image_copy_start”:
grep -nR "__image_copy_start"
打开 u-boot.map,找到该位置。
u-boot.map 是 uboot 的映射文件,可以从此文件看到某个文件或者函数链接到了哪个地址,从上图的 1629 行可以看到__image_copy_start 为 0Xc0100000,而.text 的起始地址也是0Xc0100000。
我们知道了 vectors.S 的代码是存在 vectors 段中的。从上图可以看出, vectors 段的起始地址也是 0Xc0100000,说明整个 uboot 的起始地址就是 0Xc0100000。
二、U-Boot 启动流程
2.1 reset 函数源码
从 u-boot.lds 中我们已经知道了入口点是 arch/arm/lib/vectors.S 文件中的_start。
.globl _start
.section ".vectors", "ax"
_start: # 开始中断向量表
#ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
.word CONFIG_SYS_DV_NOR_BOOT_CFG
#endif
ARM_VECTORS
#endif /* !defined(CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK) */
.globl _reset #....globl指示告诉汇编器,_reset这个符号要被链接器用到,所以要在目标文件的符号表中标记它是一个全局符号
.globl _undefined_instruction
.globl _software_interrupt
.globl _prefetch_abort
.globl _data_abort
.globl _not_used
.globl _irq
.globl _fiq #...都是中断向量表
reset 函数在 arch/arm/cpu/armv7/start.S 里面:
/*************************************************************************
*
* Startup Code (reset vector)
*
* Do important init only if we don't start from memory!
* Setup memory and board specific bits prior to relocation.
* Relocate armboot to ram. Setup stack.
*
*************************************************************************/
.globl reset
.globl save_boot_params_ret
.type save_boot_params_ret,%function
#ifdef CONFIG_ARMV7_LPAE
.global switch_to_hypervisor_ret
#endif
reset:
/* Allow the board to save important registers */
b save_boot_params # 从reset函数跳转到 save_boot_params函数
而 save_boot_params 函数同样定义在 start.S 里面:
/*************************************************************************
*
* void save_boot_params(u32 r0, u32 r1, u32 r2, u32 r3)
* __attribute__((weak));
*
* Stack pointer is not yet initialized at this moment
* Don't save anything to stack even if compiled with -O0
*
*************************************************************************/
ENTRY(save_boot_params)
b save_boot_params_ret @ back to my caller # 也只有一个跳转语句
save_boot_params_ret函数代码如下:
save_boot_params_ret:
#ifdef CONFIG_ARMV7_LPAE // 由于没有定义CONFIG_ARMV7_LPAE,所以下面的都不会运行
/*
* check for Hypervisor support
*/
mrc p15, 0, r0, c0, c1, 1 @ read ID_PFR1
and r0, r0, #CPUID_ARM_VIRT_MASK @ mask virtualization bits
cmp r0, #(1 << CPUID_ARM_VIRT_SHIFT)
beq switch_to_hypervisor
switch_to_hypervisor_ret:
#endif
/*
* disable interrupts (FIQ and IRQ), also set the cpu to SVC32 mode,
* except if in HYP mode already
*/
mrs r0, cpsr // 读取寄存器cpsr的值并保存到r0寄存器
and r1, r0, #0x1f @ mask mode bits // 将寄存器r0的值与0x1f进行运算,结果保存到r1寄存器,目的是为了提取cpsr的bit0~bit4 的5位,五位分别为M4 M3 M2 M1 M0,M[4:0]这五位用来设置处理器的工作模式
teq r1, #0x1a @ test for HYP mode // 判断r1寄存的值是否等于0X1a(11010),也就是判断当前处理器是否是Hyp模式
bicne r0, r0, #0x1f @ clear all mode bits // 如果如果 r1 和 0X1A 不相等,也就是 CPU 不处于 Hyp 模式的话就将 r0 寄存器的 bit0~5 进行清零,其实就是清除模式位
orrne r0, r0, #0x13 @ set SVC mode // 如果处理器不处于 Hyp 模式的话就将 r0 的寄存器的值与 0x13 进行或运算,0x13(10011),也就是处理器进入 SVC 模式。
orr r0, r0, #0xc0 @ disable FIQ and IRQ // r0 寄存器的值再与 0xC0 进行或运算,那么 r0 寄存器此时的值就是 0xD3, cpsr的 I 位和 F 位分别控制 IRQ 和 FIQ 这两个中断的开关,设置为 1 就关闭了 FIQ 和 IRQ!
msr cpsr,r0 // 将 r0 寄存器写回到 cpsr 寄存器中。完成设置 CPU 处于 SVC32 模式,并且关闭FIQ 和 IRQ 这两个中断。
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) |
代码继续往下执行:
/* 这段代码其实就是在设置向量表重定位 */
/*
* Setup vector:
* (OMAP4 spl TEXT_BASE is not 32 byte aligned.
* Continue to use ROM code vector only in OMAP4 spl)
*/
#if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD)) // 如果没有定义CONFIG_OMAP44XX和CONFIG_SPL_BUILD,此处条件成立
/* Set V=0 in CP15 SCTLR register - for VBAR to point to vector */
mrc p15, 0, r0, c1, c0, 0 @ Read CP15 SCTLR Register // 读取从 CP15 的 c1, c0, 0 寄存器中读取值,并将其存储到 r0 寄存器中
bic r0, #CR_V @ V = 0 // 清除 SCTLR 寄存器中的 bit13
mcr p15, 0, r0, c1, c0, 0 @ Write CP15 SCTLR Register // 将 r0 寄存器的值重新写入到寄存器 SCTLR 中
#ifdef CONFIG_HAS_VBAR
/* Set vector address in CP15 VBAR register */
ldr r0, =_start // r0寄存器的值为_start, _start就是整个uboot的入口地址,其值为0XC0100000,相当于 uboot 的起始地址,因此 0Xc0100000 也是向量表的起始地址。
mcr p15, 0, r0, c12, c0, 0 @Set VBAR // 将 r0 寄存器的值(向量表起始地址)写入到 CP15 的 c12 寄存器中,也就是 VBAR 寄存器
#endif
#endif
CR_V
在 arch/arm/include/asm/system.h 中有如下所示定义 :
#define CR_V (1 << 13) /* Vectors relocated to 0xffff0000 */
SCTLR 寄存器结构如下图所示:
从上图可以看出, bit13 为 V 位,此位是向量表控制位,当为 0 的时候向量表基地址为 0X00000000,软件可以重定位向量表。为 1 的时候向量表基地址为 0XFFFF0000,软件不能重定位向量表。这里将 V 清零,目的就是为了接下来的向量表重定位。
代码继续往下执行:
/* the mask ROM code should have PLL and others stable */
#ifndef CONFIG_SKIP_LOWLEVEL_INIT // 如果没有定义CONFIG_SKIP_LOWLEVEL_INIT条件成立。我们没有定义CONFIG_SKIP_LOWLEVEL_INIT所以条件成立。
#ifdef CONFIG_CPU_V7A //.config定义了CONFIG_CPU_V7A,所以往下执行
bl cpu_init_cp15 // bl:进行函数调用或跳转
#endif
#ifndef CONFIG_SKIP_LOWLEVEL_INIT_ONLY // 没有定义CONFIG_SKIP_LOWLEVEL_INIT_ONLY,往下执行
bl cpu_init_crit
#endif
#endif
函数 cpu_init_cp15 用来设置 CP15 相关的内容,此函数同样在 start.S文件中定义的,代码如下:
ENTRY(cpu_init_cp15)
/*
* Invalidate L1 I/D
*/
mov r0, #0 @ set up for MCR
mcr p15, 0, r0, c8, c7, 0 @ invalidate TLBs
mcr p15, 0, r0, c7, c5, 0 @ invalidate icache
mcr p15, 0, r0, c7, c5, 6 @ invalidate BP array
mcr p15, 0, r0, c7, c10, 4 @ DSB
mcr p15, 0, r0, c7, c5, 4 @ ISB
......
mov pc, r5 @ back to my caller
ENDPROC(cpu_init_cp15)
函数 cpu_init_crit 也在是定义在 start.S 文件中,函数内容如下:
ENTRY(cpu_init_crit)
/*
* Jump to board specific initialization...
* The Mask ROM will have already initialized
* basic memory. Go here to bump up clock rate and handle
* wake up conditions.
*/
b lowlevel_init @ go setup pll,mux,memory
ENDPROC(cpu_init_crit)
#endif
看出函数 cpu_init_crit 内部仅仅是调用了函数 lowlevel_init。
2.2 lowlevel_init 函数
函数 lowlevel_init主要完成RAM的初始化,也就是通过写控制RAM的寄存器,对寄存器的存取方式进行控制。
函数 lowlevel_init 在文件 arch/arm/cpu/armv7/lowlevel_init.S 中定义,内容如下:
#include <asm-offsets.h>
#include <config.h>
#include <linux/linkage.h>
.pushsection .text.s_init, "ax"
WEAK(s_init)
bx lr
ENDPROC(s_init)
.popsection
.pushsection .text.lowlevel_init, "ax"
WEAK(lowlevel_init)
/*
* Setup a temporary stack. Global data is not available yet.
*/
#if defined(CONFIG_SPL_BUILD) && defin ed(CONFIG_SPL_STACK)
ldr sp, =CONFIG_SPL_STACK
#else
ldr sp, =CONFIG_SYS_INIT_SP_ADDR /* 用于将寄存器sp(堆栈指针寄存器)加载为一个特定的地址 */
#endif
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */ /* 寄存器sp的值进行8字节对齐 */
#ifdef CONFIG_SPL_DM
mov r9, #0
#else
/*
* Set up global data for boards that still need it. This will be
* removed soon.
*/
#ifdef CONFIG_SPL_BUILD
ldr r9, =gdata
#else
sub sp, sp, #GD_SIZE /* sp 指针减去 GD_SIZE */
bic sp, sp, #7 /* 对寄存器sp做8个字节对齐,此时的sp地址为:0XC1000000 - 240 = 0XC00FFDC0 */
mov r9, sp /* 将sp的值放在r9寄存器中 */
#endif
#endif
/*
* Save the old lr(passed in ip) and the current lr to stack
*/
push {ip, lr} /* 将寄存器 ip 和 lr 的值压入堆栈中 */
/*
* Call the very early init function. This should do only the
* absolute bare minimum to get started. It should not:
*
* - set up DRAM
* - use global_data
* - clear BSS
* - try to start a console
*
* For boards with SPL this should be empty since SPL can do all of
* this init in the SPL board_init_f() function which is called
* immediately after this.
*/
bl s_init /* 调用s_init,但s_init没用 */
pop {ip, pc} /* 入栈的 ip 和 lr 进行出栈,并将 lr 赋给 pc */
ENDPROC(lowlevel_init)
.popsection /* 当前的程序段从汇编器的堆栈中弹出 */
CONFIG_SYS_INIT_SP_ADDR 在include/configs/stm32mp1.h 文件中,在 stm32mp1.h 中有如下所示定义:
#define CONFIG_SYS_SDRAM_BASE STM32_DDR_BASE
#define CONFIG_SYS_INIT_SP_ADDR CONFIG_SYS_TEXT_BASE
CONFIG_SYS_TEXT_BASE 的定义在.config 文件中定义:
所以 CONFIG_SYS_INIT_SP_ADDR 的值为 0XC0100000。此时 sp 指向0XC0100000。
GD_SIZE地址:
#define GENERATED_GBL_DATA_SIZE 240 /* (sizeof(struct global_data) + 15) & ~15 @ */
#define GENERATED_BD_INFO_SIZE 80 /* (sizeof(struct bd_info) + 15) & ~15 @ */
#define GD_SIZE 240 /* sizeof(struct global_data) @ */
#define GD_BD 0 /* offsetof(struct global_data, bd) @ */
#define GD_MALLOC_BASE 164 /* offsetof(struct global_data, malloc_base) @ */
#define GD_RELOCADDR 64 /* offsetof(struct global_data, relocaddr) @ */
#define GD_RELOC_OFF 84 /* offsetof(struct global_data, reloc_off) @ */
#define GD_START_ADDR_SP 80 /* offsetof(struct global_data, start_addr_sp) @ */
#define GD_NEW_GD 88 /* offsetof(struct global_data, new_gd) @ */
lowlevel_init 运行完后,就返回函数 cpu_init_crit,函数 cpu_init_crit 也执行完成了,最终返回到 save_boot_params_ret,接下来要执行的是save_boot_params_ret 中的_main 函数,接下来分析_main 函数。
2.3 _main 函数
_main 函数定义在文件 arch/arm/lib/crt0.S 中,函数内容如下:
ENTRY(_main)
/*
* Set up initial C runtime environment and call board_init_f(0).
*/
#if defined(CONFIG_TPL_BUILD) && defined(CONFIG_TPL_NEEDS_SEPARATE_STACK)
ldr r0, =(CONFIG_TPL_STACK)
#elif defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
ldr r0, =(CONFIG_SPL_STACK)
#else
ldr r0, =(CONFIG_SYS_INIT_SP_ADDR) /* 前面条件全部不满足, sp 指向 0XC0100000 */
#endif
bic r0, r0, #7 /* sp 8字节对齐 */
mov sp, r0 /* 把r0的值放在sp寄存器中 */
bl board_init_f_alloc_reserve /* 调用这个函数 */
mov sp, r0 /* 将r0的值写入sp寄存器中, r0 保存着函数board_init_f_alloc_reserve 的返回值,所以这一句也就是设置 sp=0XC00FCF10。 */
/* set up gd here, outside any C code */
mov r9, r0 /* 将 r0 寄存器的值写到寄存器 r9 里面,因为 r9 寄存器存放着全局变量 gd 的地址 */
bl board_init_f_init_reserve /* 调用这个函数 */
#if defined(CONFIG_SPL_EARLY_BSS)
SPL_CLEAR_BSS
#endif
mov r0, #0
bl board_init_f /* 调用此函数,此函数定义在common/board_f.c 中, 主要用来初始化平台相关的 API,定时器,完成代码拷贝等 */
#if ! defined(CONFIG_SPL_BUILD)
/*
* Set up intermediate environment (new sp and gd) and call
* relocate_code(addr_moni). Trick here is that we'll return
* 'here' but relocated.
*/
ldr r0, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */ /* 重新设置环境(sp 和 gd),获取 gd->start_addr_sp 的值赋给 sp,在函数 board_init_f中会初始化 gd 的所有成员变量,最终 gd->start_addr_sp=0XF3AEDD20,所以这里相当于设置sp=gd->start_addr_sp=0XF3AEDD20。 GD_START_ADDR_SP=80 */
bic r0, r0, #7 /* sp 8字节对齐 */
mov sp, r0
ldr r9, [r9, #GD_NEW_GD] /* r9 <- gd->new_gd */ /*获取 gd->new_gd 的地址赋给 r9,此时 r9 存放的是老的 gd,这里通过获取gd->new_gd 的地址来计算出新的 gd 的位置。 GD_BD=0 */
adr lr, here /* 设置 lr 寄存器为 here,这样后面执行其他函数返回的时候就返回到了 here 位置处 */
ldr r0, [r9, #GD_RELOC_OFF] /* r0 = gd->reloc_off */ /* 读取 gd->reloc_off 的值复制给 r0 寄存器, GD_RELOC_OFF=84 */
add lr, lr, r0 /* lr 寄存器的值加上 r0 寄存器的值,重新赋值给 lr 寄存器 因为接下来要重定位代码,也就是把代码拷贝到新的地方去(现在的 uboot 存放的起始地址为 0XC0100000,下面要将 uboot 拷贝到 DDR 最后面的地址空间出,将 0XC0100000 开始的内存空出来),其中就包括here,因此 lr 中的 here 要使用重定位后的位置。*/
#if defined(CONFIG_CPU_V7M)
orr lr, #1 /* As required by Thumb-only */ /* 与1进行或操作返回lr寄存器中 */
#endif
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */ /* 读取 gd->relocaddr 的值赋给 r0 寄存器,此时 r0 寄存器就保存着 uboot 要拷贝的目的地址,为 0XF5C46000。 GD_RELOCADDR=40 */
b relocate_code /* 调用函数 relocate_code,也就是代码重定位函数,此函数负责将 uboot 拷贝到新的地方去 */
here:
/*
* now relocate vectors
*/
bl relocate_vectors /* 调用函数 relocate_vectors,对中断向量表做重定位,此函数定义在文件 arch/arm/lib/relocate.S */
/* Set up final (full) environment */
bl c_runtime_cpu_setup /* we still call old routine here */ /* 调用此函数 */
#endif
#if !defined(CONFIG_SPL_BUILD) || CONFIG_IS_ENABLED(FRAMEWORK)
#if !defined(CONFIG_SPL_EARLY_BSS)
SPL_CLEAR_BSS
#endif
# ifdef CONFIG_SPL_BUILD
/* Use a DRAM stack for the rest of SPL, if requested */
bl spl_relocate_stack_gd
cmp r0, #0
movne sp, r0
movne r9, r0
# endif
#if ! defined(CONFIG_SPL_BUILD)
bl coloured_LED_init
bl red_led_on
#endif
/* call board_init_r(gd_t *id, ulong dest_addr) */
mov r0, r9 /* gd_t */
ldr r1, [r9, #GD_RELOCADDR] /* dest_addr */
/* call board_init_r */
#if CONFIG_IS_ENABLED(SYS_THUMB_BUILD)
ldr lr, =board_init_r /* this is auto-relocated! */
bx lr
#else
ldr pc, =board_init_r /* this is auto-relocated! */
#endif
/* we should not return here. */
#endif
ENDPROC(_main)
其中 common/init/board_init.c 函数定义在文件中 board_init_f_alloc_reserve 函数代码如下:
ulong board_init_f_alloc_reserve(ulong top)
{
/* Reserve early malloc arena */
#if CONFIG_VAL(SYS_MALLOC_F_LEN)
top -= CONFIG_VAL(SYS_MALLOC_F_LEN);
#endif
/* LAST : reserve GD (rounded up to a multiple of 16 bytes) */
top = rounddown(top-sizeof(struct global_data), 16);
return top;
}
malloc内存区域:用于在堆(heap)区域动态分配内存。通过调用 malloc 函数,可以请求指定大小的连续内存块,并返回该内存块的首地址。 malloc 内存区域通常用于动态分配内存,以满足程序运行时不确定内存需求的情况。它在堆区分配的内存块可被手动释放,也可以在程序结束时由操作系统自动回收。
gd(Global Data)内存区域是指程序在运行时存储全局变量和静态变量的内存区域。gd 内存区域中的变量具有全局作用域,在整个程序执行过程中都存在,并且会在程序加载时就进行内存分配。
函数 board_init_f_alloc_reserve 主要是留出早期的 malloc 内存区域和 gd 内存区域,其中CONFIG_SYS_MALLOC_F_LEN=0X3000(在文件 include/generated/autoconf.h 中定义),另外sizeof(struct global_data)=240,也就是 GD_SIZE 值。内存结构如下图所示:
函数 board_init_f_alloc_reserve 是有返回值的,返回值为新的 top 值,此时 top=0XC00FCF10(地址刚好是 16 字节对齐)。
_main 函数中的r9在文件 arch/arm/include/asm/global_data.h 中有定义:
从上图可以看出, uboot 中定义了一个指向 gd_t 结构体类型的指针: gd, gd 存放在寄存器 r9 里面的,因此 gd 是个全局变量。 gd_t 是个结构体,在 include/asm-generic/global_data.h里面有定义, gd_定义如下:
typedef struct global_data {
bd_t *bd;
unsigned long flags;
unsigned int baudrate;
unsigned long cpu_clk; /* CPU clock in Hz! */
unsigned long bus_clk;
/* We cannot bracket this with CONFIG_PCI due to mpc5xxx */
unsigned long pci_clk;
unsigned long mem_clk;
......
#if defined(CONFIG_TRANSLATION_OFFSET)
fdt_addr_t translation_offset; /* optional translation offset */
#endif
#if CONFIG_IS_ENABLED(WDT)
struct udevice *watchdog_dev;
#endif
} gd_t;
因此这一行代码就是设置 gd 所指向的位置,也就是 gd 指向 0XC00FCF10。
_main 函数中的board_init_f_init_reserve 在 文件common/init/board_init.c 中:
void board_init_f_init_reserve(ulong base)
{
struct global_data *gd_ptr;
/*
* clear GD entirely and set it up.
* Use gd_ptr, as gd may not be properly set yet.
*/
gd_ptr = (struct global_data *)base;
/* zero the area */
memset(gd_ptr, '\0', sizeof(*gd));
/* set GD unless architecture did it already */
#if !defined(CONFIG_ARM)
arch_setup_gd(gd_ptr);
#endif
/* next alloc will be higher by one GD plus 16-byte alignment */
base += roundup(sizeof(struct global_data), 16);
/*
* record early malloc arena start.
* Use gd as it is now properly set for all architectures.
*/
#if CONFIG_VAL(SYS_MALLOC_F_LEN)
/* go down one 'early malloc arena' */
gd->malloc_base = base;
/* next alloc will be higher by one 'early malloc arena' size */
base += CONFIG_VAL(SYS_MALLOC_F_LEN);
#endif
if (CONFIG_IS_ENABLED(SYS_REPORT_STACK_F_USAGE))
board_init_f_init_stack_protection();
}
此函数用于初始化 gd,其实就是清零处理。另外,此函数还设置了gd->malloc_base为gd基地址+gd大小=0XC00FCF10+240=0XC00FD000,这个也就是early malloc的起始地址。
2.3 board_init_f 函数
_main 中会调用 board_init_f 函数, board_init_f 函数主要有两个工作:
1、初始化一系列外设,比如串口、定时器,或者打印一些消息等。
2、初始化 gd 的各个成员变量, uboot 会将自己重定位到 DRAM 最后面的地址区域,也就是将自己拷贝到 DRAM 最后面的内存区域中。这么做的目的是给 Linux 腾出空间,防止 Linux kernel 覆盖掉 uboot,将 DRAM 前面的区域完整的空出来。在拷贝之前肯定要给 uboot 各部分分配好内存位置和大小,比如 gd 应该存放到哪个位置, malloc 内存池应该存放到哪个位置等等。 这些信息都保存在 gd 的成员变量中,因此要对 gd 的这些成员变量做初始化。最终形成一个完整的内存“分配图”,在后面重定位 uboot 的时候就会用到这个内存“分配图”。
board_init_f 函数定义在文件 common/board_f.c 中定义,代码如下:
void board_init_f(ulong boot_flags)
{
gd->flags = boot_flags; // 初始化gd->flags = boot_flags
gd->have_console = 0; // 设置gd->have_console = 0
if (initcall_run_list(init_sequence_f)) // 通过函数 initcall_run_list 来运行初始化序列 init_sequence_f 里面的一些列函数, init_sequence_f 里面包含了一系列的初始化函数, init_sequence_f 也定义在该文件中
hang();
#if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \
!defined(CONFIG_EFI_APP) && !CONFIG_IS_ENABLED(X86_64) && \
!defined(CONFIG_ARC)
/* NOTREACHED - jump_to_copy() does not return */
hang();
#endif
}
init_sequence_f 也定义在文件 common/board_f.c 中 ,代码如下(条件编译代码删除):
static const init_fnc_t init_sequence_f[] = {
setup_mon_len, // 设置gd的mon_len成员变量,此处为__bss_end -_start,也就是整个代码的长度。 0XC01B966C-0XC0100000=0XB966C,这个就是代码长度
fdtdec_setup, // 函数设置 gd 的 fdt_dlob 指针变量, fdt_blob 保存着设备树(.dtb)文件地址。此处为_image_binary_end,也就是我们的设备地址为 0XC01C2938
initf_malloc, // initf_malloc 函数初始化 gd 中跟 malloc 有关的成员变量,比如 malloc_limit,此函数会设置 gd->malloc_limit = CONFIG_SYS_MALLOC_F_LEN=0X3000。malloc_limit 表示 malloc内存池大小。
log_init, // log_init 函数将 struct log_driver 结构体加入到 gd->log_head 的循环链表中,并初始化 gd->default_log_level。
initf_bootstage, // initf_bootstage 函数为 gd->bootstage 分配空间,并初始化 gd->bootstage。
setup_spl_handoff, // setup_spl_handoff 函数和 SPL 相关, STM32MP1 是使用 TF-A 引导 U-BOOT,此函数什么都没有做。
initf_console_record, // STM32MP1的 uboot 没有定义宏 CONFIG_CONSOLE_RECORD,所以此函数直接返回 0
arch_cpu_init, // 初始化架构相关的内容, CPU 级别的操作
mach_cpu_init, // 初始化 SOC 相关的内容, CPU 级别的操作
initf_dm, // 驱动模型有关的初始化操作。如果定义了 CONFIG_DM,则调用 dm_init_and_scan 初始化并扫描系统所有的 device。如果定义了 CONFIG_TIMER_EARLY,调用 dm_timer_init 初始化 driver model 所需的 timer 。 STM32MP1 定义了 CONFIG_DM,会扫描所有的 device
arch_cpu_init_dm, // 空函数
/* get CPU and bus clocks according to the environment variable */
get_clocks, // 获取时钟值
timer_init, // timer_init,初始化定时器。主要用来初始化 gd->arch.timer_rate_hz,设置定时器频率
env_init, // env_init 函数是和环境变量有关的,设置 gd 的成员变量 env_addr,也就是环境变量的保存地址
init_baud_rate, // 用于初始化波特率,根据环境变量 baudrate 来初始化gd->baudrate
serial_init, // 初始化串口,设置 gd 的 flags 成员变量为 GD_FLG_SERIAL_READY,开启当前选中的串口
console_init_f, // 设置 gd->have_console 为 1,表示有个控制台,此函数也将前面暂存在缓冲区中的数据通过控制台打印出来
display_options, // 通过串口输出一些信息,比如 uboot 版本号,编译时间等
display_text_info, // 打印一些文本信息,如果开启 UBOOT 的 DEBUG 功能的话就会输出 text_base、 bss_start、 bss_end。
#if defined(CONFIG_PPC) || defined(CONFIG_SH) || defined(CONFIG_X86)
checkcpu, // PPC、X86、SH 架构的芯片会执行, ARM 架构不会执行
#endif
print_cpuinfo, // 用于打印 CPU 信息
show_board_info, // 打印板子信息,如果在 uboot 中使用了设备树,那么此函数会先从设备树里面获取到 model 属性信息并且打印出出来,然后调用 checkboard 函数获取板子信息并打印出来
INIT_FUNC_WATCHDOG_INIT // 初始化看门狗,对于STM32MP1是空函数
INIT_FUNC_WATCHDOG_RESET // 复位看门狗,是空函数
init_func_vid, // 一个空函数,返回值为 0
announce_dram_init, // 输出字符串"DRAM:"
dram_init, // dram_init,并非真正的初始化 DDR,只是设置 gd->ram_size 的值,对于正点原子 STM32MP1 开发板核来说, DDR 大小为 1G,所以 gd->ram_size 的值为 0X40000000
INIT_FUNC_WATCHDOG_RESET
testdram, // 测试 DRAM,空函数
INIT_FUNC_WATCHDOG_RESET
init_post,
INIT_FUNC_WATCHDOG_RESET
/*
* Now that we have DRAM mapped and working, we can
* relocate the code and continue running from DRAM.
*
* Reserve memory at end of RAM for (top down in that order):
* - area that won't get touched by U-Boot and Linux (optional)
* - kernel log buffer
* - protected RAM
* - LCD framebuffer
* - monitor code
* - board info struct
*/
setup_dest_addr, // 设置目的地址,设置 gd->ram_base 、 gd->ram_size、gd->ram_top、 gd->relocaddr 这四个的值
reserve_pram,
reserve_round_4k, // 对 gd->relocaddr 对4KB对齐 , 因为gd->relocaddr=0XF6000000,已经是 4K 对齐了,所以调整后不变
reserve_mmu, // 留出 MMU 的 TLB 表的位置,分配 MMU 的 TLB 表内存以后会对 gd->relocaddr 做 64K 字节对齐
reserve_video, // 留出显示的内存
reserve_trace, // 留出跟踪调试的内存, STM32MP1 没有用到最终功能,因此无需留出对应的内存区域
reserve_uboot, // 留出重定位后的 uboot 所占用的内存区域, uboot 所占用大小由gd->mon_len 所指定,留出 uboot 的空间以后还要对 gd->relocaddr 做 4K 字节对齐,并且重新设置 gd->start_addr_sp
reserve_malloc, // reserve_malloc,留出 malloc 区域以及 MMU 相关内存
reserve_board, // 留出板子 bd 所占的内存区, bd 是结构体 bd_t, bd_t 大小为 80 字节
setup_machine, // 设置机器 ID, linux 启动的时候会和这个机器 ID 匹配,如果匹配的话 linux 就会启动正常。但是!! STM32MP1 不用这种方式了,这是以前老版本的 uboot 和linux 使用的,新版本使用设备树了,因此此函数无效
reserve_global_data, // 保留出新的 gd_t 的内存区域, gd_t 结构体大小为 240B
reserve_fdt, // 留出设备树相关的内存区域
reserve_bootstage, // 保留 bootstage 的内存区域
reserve_bloblist, // 不执行
reserve_arch, // 空函数
reserve_stacks, // 留出栈空间。如果使能 IRQ 的话还要留出 IRQ 相应的内存,具体工作是由 arch/arm/lib/stack.c 文件中的函数 arch_reserve_stacks 完成
/* 以上,uboot重定位完成 */
dram_init_banksize, // 设置 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,使用 memcpy 函数将原来存储在 gd->fdt_blob 中的设备树文件拷贝到 gd->new_fdt 处,然后将 gd->new_fdt 赋值给 gd->fdt_blob。经过这一步设备树拷贝到了新地址,而且 gd->fdt_blob 也保存了新的设备树地址
reloc_bootstage, // 用于 bootstage 重定位,使用 memcpy 函数将 gd->bootstage 中原来的 bootstage 拷贝到新的 gd->new_bootstage 处。然后将 gd->new_bootstage 赋值给gd->bootstage
reloc_bloblist,
setup_reloc, // 设置 gd 的其他一些成员变量,供后面重定位的时候使用,并且将以前的 gd 拷贝到 gd->new_gd 处
NULL,
};
至此, board_init_f 函数就执行完成了,最终的内存分配如下图所示:
2.3 relocate_code 函数
relocate_code 函数是用于代码拷贝的,此函数定义在文件 arch/arm/lib/relocate.S 中,代码如下:
先暂时不看了,这代码看得我头有点疼。