IMX6ULL学习笔记(四) —— uboot 启动流程

IMX6ULL 学习笔记


version : v1.0 「2023.4.27」


author: Y.Z.T.


摘要: 随记, 记录 I.MX6ULL 系列 SOC 的uboot 启动流程




⭐️ 目录




2.3 Uboot启动流程

2.3.1 链接脚本 u-boot.lds
2.3.1.1 链接脚本

通过链接脚本可以找到程序的入口地址 , uboot的最终链接脚本是 u-boot.lds , 是通过编译boot生成的

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
 . = 0x00000000;
 . = ALIGN(4);
 .text :
 {
  *(.__image_copy_start)
  *(.vectors)
  arch/arm/cpu/armv7/start.o (.text*)
  *(.text*)
 }
 . = ALIGN(4);
 .rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
 . = ALIGN(4);
 .data : {
  *(.data*)
 }
 . = ALIGN(4);
 . = .;
 . = ALIGN(4);
 .u_boot_list : {
  KEEP(*(SORT(.u_boot_list*)));
 }
 . = ALIGN(4);
 .image_copy_end :
 {
  *(.__image_copy_end)
 }
 .rel_dyn_start :
 {
  *(.__rel_dyn_start)
 }
 .rel.dyn : {
  *(.rel*)
 }
 .rel_dyn_end :
 {
  *(.__rel_dyn_end)
 }
 .end :
 {
  *(.__end)
 }
 _image_binary_end = .;
 . = ALIGN(4096);
 .mmutable : {
  *(.mmutable)
 }
 .bss_start __rel_dyn_start (OVERLAY) : {
  KEEP(*(.__bss_start));
  __bss_base = .;
 }
 .bss __bss_base (OVERLAY) : {
  *(.bss*)
   . = ALIGN(4);
   __bss_limit = .;
 }
 .bss_end __bss_limit (OVERLAY) : {
  KEEP(*(.__bss_end));
 }
 .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) 是整个函数的入口,_startarch/arm/lib/vectors.S 中有定义
  • 注意.text 代码段的内容
    • __image_copy_start ( uboot拷贝的首地址 )
    • vectors 段用于保存 中断向量表
    • arch/arm/cpu/armv7/start.o (.text*) 意思是将 arch/arm/cpu/armv7/start.o 编译出来的代码放到中断向量表后面
    • *(.text*) 用于存放其他的代码段

与地址有关的变量

变量数值描述
__image_copy_start0x87800000uboot 拷贝的首地址
__image_copy_end0x8785dd54uboot 拷贝的结束地址
__rel_dyn_start0x8785dd54.rel.dyn 段起始地址
__rel_dyn_end0x878668f4.rel.dyn 段结束地址
_image_binary_end0x878668f4镜像结束地址
__bss_start0x8785dd54.bss 段起始地址
__bss_end0x878a8e74.bss 段结束地址

除了__image_copy_start 的值 , 其他变量 每次编译的时候可能会变化,如果修改了 uboot 代码、修改了 uboot 配置、选用不同的优化等级等等都会影响到这些值。



2.3.1.2 vectors.S ( 入口点:_start )
  • 代码当前入口点:_star 存放在 文件 arch/arm/lib/vectors.S

image-20230511210751928


  • _start 后面就是中断向量表
  • .section ".vectors", "ax" 可以知道 , 中断向量表这部分代码是存放在 .vectors 段里面

2.3.1.3 映射文件 u-boot.map
  • u-boot.mapuboot 的映射文件,可以从此文件看到某个文件或者函数链接到了哪个地址

段 .text 的地址设置为 0x87800000
                0x0000000000000000                . = 0x0
                0x0000000000000000                . = ALIGN (0x4)

.text           0x0000000087800000    0x3cd64
 *(.__image_copy_start)
 .__image_copy_start
                0x0000000087800000        0x0 arch/arm/lib/built-in.o
                0x0000000087800000                __image_copy_start
 *(.vectors)
 .vectors       0x0000000087800000      0x300 arch/arm/lib/built-in.o
                0x0000000087800000                _start
                0x0000000087800020                _undefined_instruction
                0x0000000087800024                _software_interruptp
                0x0000000087800028                _prefetch_abort
                0x000000008780002c                _data_abort
                0x0000000087800030                _not_used
                0x0000000087800034                _irq
                0x0000000087800038                _fiq
                0x0000000087800040                IRQ_STACK_START_IN
 arch/arm/cpu/armv7/start.o(.text*)
 .text          0x0000000087800300       0xb0 arch/arm/cpu/armv7/start.o
                0x0000000087800300                reset
                0x0000000087800304                save_boot_params_ret
                0x0000000087800340                c_runtime_cpu_setup
                0x0000000087800350                save_boot_params
                0x0000000087800354                cpu_init_cp15
                0x00000000878003a8                cpu_init_crit
 *(.text*)
 .text          0x00000000878003b0       0x24 arch/arm/cpu/armv7/built-in.o
                0x00000000878003b0                lowlevel_init
  • 可以看到 .text 的起始地址为 0x87800000
  • 镜像启动地址 (.__image_copy_start) 也是 0x87800000
  • vectors段 的起始地址也是 0x87800000
  • vectors 段 后面则是 arch/arm/cpu/armv7/start.s 和 其他代码段的内容


2.3.2 U-boot启动流程

Uboot 的启动流程可以大致分成两个阶段 :

  • 第一阶段多使用汇编 , 主要完成一些板级的硬件初始化 , 如外设硬件初始化 , 如 DRAM , 串口 , 重定位等。
  • 第二阶段通常使用C语言来实现 , 方便实现更加复杂的功能 , 主要完成linux 内核 的启动


2.3.2.0 补充信息
2.3.2.0.1 ENTRY() 和 ENDPROC() 宏

使用ENTRYENDPROC两个宏来定义一个名为name的函数 , 这个伪指令实现了指定一个入口的同时数据对齐,同时提供了一个函数入口 :

ENTRY(name)
...
ENDPROC(name)

这两个宏定义在#include <linux/linkage.h>

.globl  save_boot_params
.align  4                       @4字节对齐
save_boot_params:
bx  lr							@ 带模式的返回

.type save_boot_params STT_FUNC; @ 说明该标识是函数
.size save_boot_params, .-save_boot_params  @ 计算整个函数的大小

.weak   save_boot_params   @ 弱标号,如果别处有使用别处的定义,如果没有使用当前定义


2.3.2.0.2 为什么选择SVC 模式

通过 设置CPSR寄存器 的bit0 ~ bit4 五位来设置 处理器的工作模式

如下表所示:

image-20230515173401632

在uboot的启动流程中选择SVC 模式

  • 7种模式中,除用户usr模式外,其它模式均为 特权模式

  • 中止ABT和未定义UND模式

    • 因为此时程序是正常运行的 , 所以不应该设置CPU为这两种模式的其中任何一种
  • 快中断FIQ和中断IRQ模式

    • 对于快中断FIQ和中断IRQ来说,此处uboot初始化的时候,中断已经被禁用
    • 即使是注册了终端服务程序后,能够处理中断,那么这两种模式,也是自动切换过去的
    • 所以,此处也不应该设置为这两种模式中的其中任何一种模式
  • 用户USR模式

    • 访问uboot初始化,就必须很多的硬件资源 , 而用户模式 USR非特权模式 不能访问系统所有资源, 所以CPU也不能设置成USR
  • 系统SYS模式 和 管理SVC模式

    • SYS模式和USR模式相比,所用的寄存器组,都是一样的,但是增加了一些访问一些在USR模式下不能访问的资源
    • SVC模式本身就属于特权模式,本身就可以访问那些受控资源 , 相比 SYS 多了 专属寄存器 R13(sp)R14(lr) 以及 备份程序状态寄存器SPSR_svc
    • 所以 , 相对SYS模式来说,可以 访问资源的能力相同,但是拥有 更多的硬件资源
    • 因为在初始化 uboot 的过程中 , 要做的事情是初始化系统相关硬件资源,需要 获取尽量多的权限,以方便操作硬件,初始化硬件 , 所以最终选择 SVC模式


2.3.2.0.3 条件执行指令

为了提高代码密度,减少ARM指令的数量,几乎所有的ARM指令都可以根据CPSR寄存器中的标志位,通过指令组合实现条件执行。

如:

  • 无条件跳转指令B,我们可以在后面加上条件码组成BEQBNE组合指令。
  • BEQ指令表示两个数比较,结果相等时跳转;
  • BNE指令则表示结果不相等时跳转
  • bicne 指令表示 标志位Z= 0 的时候 , 执行清零指令 bic

ARM指令的条件码

image-20220910151306942


BL跳转指令

格式 : BL{条件} 目标地址

作用 :

  • 但跳转之前,会在寄存器RL(即R14)中保存PC的当前内容
  • BL指令一般用在函数调用的场合

BL Label  ;当程序无条件跳转到标号Label处执行时,同时将当前的PC值保存到R14中
...		  ; 子程序返回后接着从此处继续执行

2.3.2.0.4 CP15协处理器

CP15 协处理器一般用于存储系统管理,但是在中断中也会使用到,CP15 协处理器一共有

16 个 32 位寄存器 ( c0~c15 )CP15 协处理器的访问通过如下另个指令完成:

  • MRC:CP15 协处理器中的寄存器数据读到 ARM 寄存器中

  • MCR:ARM 寄存器的数据写入到 CP15 协处理器寄存器中

    MCR{cond} p15, <opc1>, <Rt>, <CRn>, <CRm>, <opc2>
    
    • **cond:**指令执行的条件码,如果忽略的话就表示无条件执行
    • opc1:协处理器要执行的操作码
    • RtARM 源寄存器,要写入到 CP15 寄存器的数据就保存在此寄存器中
    • CRnCP15 协处理器的目标寄存器
    • CRm: 协处理器中附加的目标寄存器或者源操作数寄存器,如果不需要附加信息就将CRm 设置为 C0,否则结果不可预测
    • opc2:可选的协处理器特定操作码,当不需要的时候要设置为 0

例:CP15C0 寄存器的值读取到 R0 寄存器中,

MRC p15, 0, r0, c0, c0, 0

其中四个寄存器

  • 通过 c0 寄存器可以获取到处理器内核信息
  • 通过 c1 寄存器可以使能或禁止 MMU、I/D Cache 等;
  • 通过 c12 寄存器可以设置中断向量偏移 ( 如设置中断向量表偏移的时候就需要将新的中断向量表基地址写入 VBAR 中 )
  • 通过 c15 寄存器可以获取 GIC (中断控制器) 基地址

例 :

/*
 * 设置中断向量表:
 * (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))

	/* 在CP15 SCTLR寄存器中设置V=0,并 用 VBAR 重新定位向量表 */
	mrc	p15, 0, r0, c1, c0, 0	@  将CP15协处理器的 C1寄存器值读到r0寄存器
	bic	r0, #CR_V				@  将SCTLR寄存器的bit13位V 清零 , (即此时向量表基地址为 0X00000000,软件可以重定位向量表)
	mcr	p15, 0, r0, c1, c0, 0	@   将CP15协处理器的 C1寄存器值写到r0寄存器

	/* 在CP15 VBAR寄存器中设置向量地址 */
	ldr	r0, =_start
	mcr	p15, 0, r0, c12, c0, 0	@重定位向量表 将VBAR寄存器值设置为 _start , 即整个uboot 的入口地址
#endif



2.3.2.1 阶段1 : 初始化外设硬件
2.3.2.1.1 uboot程序入口点 __start

位置: arch/arm/lib/vectors.S

上电启动后,代码执行到 _start 函数,调用 reset 函数

reset 的函数目的是将处理器设置为SVC模式,并且关闭FIQIRQ,然后设置中断向量以及初始化 CP15 协处理器

_start:

#ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
	.word	CONFIG_SYS_DV_NOR_BOOT_CFG
#endif

	b	reset
	ldr	pc, _undefined_instruction
	ldr	pc, _software_interrupt
	ldr	pc, _prefetch_abort
	ldr	pc, _data_abort
	ldr	pc, _not_used
	ldr	pc, _irq
	ldr	pc, _fiq
  • 下面的 8~14 行 就是是 中断向量表

  • 可以看到 直接跳到reset 函数 (reset 函数直接跳转到save_boot_params 函数)

    reset:
    	/* Allow the board to save important registers */
    	b	save_boot_params
    
  • save_boot_params 也同样是直接跳转到 save_boot_params_ret 函数



2.3.2.1.2 save_boot_params_ret函数

位置 : arch/arm/lib/vectors.S


save_boot_params_ret 函数主要完成以下功能:

  • 当前处理器模式不为 HYP模式时 , 将处理器模式设置为 SVC模式 ,并禁用IRQFIQ两个中断
  • 重定位 中断向量表 ,将其定位到uboot 的起始地址 ( 这里取0x8780 0000)
  • 调用cpu_init_cp15 函数 , 设置其他和CP15有关的设置(cache, MMU, tlb) , 打开I-cache
  • 调用 cpu_init_crit 函数 , 并最终生成一个属于 IMX6ULL 内部RAM的临时堆栈
  • 调用 _main函数 ,
save_boot_params_ret:
	/*
	 * 当前系统不处于 HYP 模式时
	 * 禁用中断(FIQ和IRQ),也将cpu设置为SVC (管理)模式
	 */
	mrs	r0, cpsr			@ 读cpsr的值 , 并保存到 r0寄存器中
	and	r1, r0, #0x1f		@ 使用位与操作 , 提取 CPSR寄存器的 bit0 ~ bit4 四位, 即用于设置 处理器工作模式的四位 
	teq	r1, #0x1a			@ 检查当前是否是 HYP模式 , 使用teq将 r1 与 0x1a进行异或运算 ,并将结果更新 CPSR标志位 
	bicne	r0, r0, #0x1f		@ 当 CPSR寄存器的标志位Z != 1 (即之前运算结果不为0 , 即不处于HYP模式),清除r0的低5位
	orrne	r0, r0, #0x13		@ 设置处理器模式为 SVC模式
	orr	r0, r0, #0xc0		@ 禁用 FIQ 和 IRQ (SPCR寄存器的 I为和F位 控制IRQ和FIQ,设置为1则禁用)
	msr	cpsr,r0				@ 将寄存器的值写回CPSR

/*
 * 设置中断向量表
 * c1寄存器 的bit13位是 'V' (向量表控制位), 
 * 为0时,向量表基地址为0x00000000(可重定位),
 * 为1时,向量表基地址为0xFFFF0000(不可重定位)
 */
#if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD))
	mrc	p15, 0, r0, c1, c0, 0		@ 读取CP15协处理器的c1寄存器,即SCTLR
	bic	r0, #CR_V					@ CR_V = (1 << 13) 所以是清除c1寄存器 的bit13位(V) 
	mcr	p15, 0, r0, c1, c0, 0		@ 写SCTLR

	/* 在CP15协处理器的 VBAR寄存器(C12)中 设置向量表的重定位地址 , */
	ldr	r0, =_start				@ 设置向量表的重定位地址 , 即整个uboot起始地址 (0x8780 0000)
	mcr	p15, 0, r0, c12, c0, 0	@ 将r0的值写入 VBAR
#endif

	/* the mask ROM code should have PLL and others stable */
#ifndef CONFIG_SKIP_LOWLEVEL_INIT	
	bl	cpu_init_cp15		@ 调用cpu_init_cp15函数, 用来设置和CP15有关的设置(cache, MMU, tlb),打开I-cache
	bl	cpu_init_crit		@ 调用cpu_init_crit函数 , 再调用lowlevel_init函数	
#endif

	bl	_main				@ 调用_main函数, 
  • 在第33行 , 调用cpu_init_crit , 这个函数内部仅仅调用了lowlevel_init函数
  • lowlevel_init 用于创建一个IMX6ULL内部的 临时堆栈

补充:

🅰️ SCTLR寄存器 ( CP15c1寄存器)

20211223110110


🅱️ save_boot_params_ret 函数调用路径


image-20230515204430974



2.3.2.1.3 lowlevel_init函数

位置 : arch/arm/cpu/armv7/lowlevel_init.S


lowlev el_init 函数主要完成如下功能

  • 初始化一个临时堆栈 , 这个堆栈属于 IMX6ULL的内部 RAM
  • 设置r9寄存器 , 用于保存 GD结构体的基地址
  • 这个临时堆栈 , 保留了 Global dataGBL_DATA 的地址位置
  • 调用早期初始化函数 s_init , 但对于IMX6ULL来说相当于 空函数

ENTRY(lowlevel_init)

/* 设置一个临时堆栈,  暂时还没有Global data(全局数据GD) , 但留出GD的大小*/
	ldr	sp, =CONFIG_SYS_INIT_SP_ADDR  	@ 将sp指针指向 系统初始化指针地址(0X0091FF00) ,定义如后文
	bic	sp, sp, #7 						@ 对sp指针进行8字节对齐 ,对齐原理如后文所示
		
#ifdef CONFIG_SPL_DM
	mov	r9, #0				@条件编译不成立 , 未使用
#else

/* 预留出全局数据(GD)的大小 */
#ifdef CONFIG_SPL_BUILD
	ldr	r9, =gdata
#else
	sub	sp, sp, #GD_SIZE		@ 将sp指针减去 GD的大小(GD_SIZE = 248)
	bic	sp, sp, #7				@ 将指针进行8字节对齐 (此时SP =  0X0091FF00-248=0X0091FE08)
	mov	r9, sp					@ 将SP指针地址保存在 r9寄存器, 此时r9保存着 dg 结构体的基地址
#endif
#endif

/* 将旧的lr(通过ip传递)和当前的lr保存到堆栈中 */
	push	{ip, lr}			@ 将ip和lr压栈

	/* s_init:
	 * 调用最早期的init函数。这应该只做最基本的初始化,它不应该做以下的事情:
	 * 
	 * - 设置DRAM
	 * - 使用全局数据(global_data)
	 * - 清除BSS段
	 * - 尝试启动控制台
	 */	 
	bl	s_init					@ 调用s_init , 对于 IMX6ULL来说是空函数
	pop	{ip, pc}				@ 将lr出栈并赋给pc,将ip出栈赋给ip
ENDPROC(lowlevel_init)


补充:

🅰️ 宏CONFIG_SYS_INIT_SP_OFFSET和 宏 CONFIG_SYS_INIT_SP_ADDR 计算:

这两个宏定义如下:

#define CONFIG_SYS_INIT_RAM_ADDR     IRAM_BASE_ADDR       (这里是RAM基地址, 取0x00900000)
#define CONFIG_SYS_INIT_RAM_SIZE     IRAM_SIZE			  (这里是RAM的大小, 取0X20000 = 128KB)

#define CONFIG_SYS_INIT_SP_OFFSET \			 			  (值取 0x1FF00)
(CONFIG_SYS_INIT_RAM_SIZE - GENERATED_GBL_DATA_SIZE)	(GENERATED_GBL_DATA_SIZE = 256)
#define CONFIG_SYS_INIT_SP_ADDR \						  (值取 0X0091FF00)
(CONFIG_SYS_INIT_RAM_ADDR + CONFIG_SYS_INIT_SP_OFFSET)
  • IRAM_BASE_ADDRIRAM_SIZE 两个宏都定义在 arch/arm/include/asm/arch-mx6/imx-regs.h

  • GENERATED_GBL_DATA_SIZE 宏定义在 include/generated/generic-asm-offsets.h

  • GENERATED_GBL_DATA_SIZE 的含义为 (sizeof(struct global_data) + 15) & ~15

  • 则可以得到如下值

    CONFIG_SYS_INIT_RAM_ADDR = IRAM_BASE_ADDR = 0x00900000
    CONFIG_SYS_INIT_RAM_SIZE = 0x00020000 =128KB
    GENERATED_GBL_DATA_SIZE  = 256
    
  • 计算可得:

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

🅱️ sp指针8位对齐

bic sp, sp, #7     @ sp指针8位对齐
  • 实现 8位对齐 的原理就是将最低三位清零因为 #7 对应 (0111),清除后就可以被 8 (1000)整除
  • 不过前提是栈地址要 向下生长 ( FD | ED),这样被清除的地址不会与数据冲突

🆎 此时的堆栈内存情况

image-20230515221338851


🔤 s_init 函数

位置: arch/arm/cpu/armv7/mx6/soc.c

  • s_init函数里面 , 代码会判断CPU类型

  • 如果 CPU 为 MX6SX、MX6UL、MX6ULL 或 MX6SLL 中的任意 一 种 , 那么就会直接返回

    	if (is_cpu_type(MXC_CPU_MX6SX) || is_cpu_type(MXC_CPU_MX6UL) ||
    	    is_cpu_type(MXC_CPU_MX6ULL) || is_cpu_type(MXC_CPU_MX6SLL))
    		return;
    
  • 所以对 I.MX6UL/I.MX6ULL 来说,s_init 就是个空函数


2.3.2.1.4 _main函数

位置 : arch/arm/lib/crt0.S

_main 函数主要完成以下功能

  • 初始化C语言运行环境、堆栈设置
  • 各种板级设备初始化、初始化NAND FlashSDRAM
  • 初始化全局结构体变量GD,在GD里有U-boot实际加载地址
  • 调用relocate_code,将U-boot镜像从Flash复制到RAM
  • Flash跳到内存RAM中继续执行程序
  • BSS段清零,跳入bootcmdmain_loop交互模式

_main 执行顺序 :

  • 设置 可以调用board_init_f() 的初始C运行环境

    • 这个运行环境只提供 一个堆栈 和一个用来存储GD (global data) 结构体的 位置
    • 堆栈和储存位置都位于 RAM中( 如SRAM , 锁定缓存等)中 , 在这种情况下, 变量 GD 无论是否初始化(BSS段) 都不能使用
    • 只有 常量初始化的数据才能可用 , GD 在被 board_init_f() 调用前 ,应该先被清零 ( 调用board_init_f_init_reserve 函数 清零GD)
  • 调用board_init_f() 函数

    • 这个函数 从系统 外部 RAM (如DRAM, DDR …) 执行硬件准备 , 初始化一系列外设,比如串口、定时器,或者打印一些消息等
    • 因为此时 , 系统RAM还不可用 , board_init_f() 函数必须 使用当前的GD 变量来储存必须传递到后续阶段的 任何数据 , 所以 初始化 gd 的各个成员变量
    • 这些数据包括 : 重定位的目的地址未来的堆栈未来的GD的内存位置 , 在DRAM最后部分预留各数据的内存空间 (如 ubootmallocgdbd等) , 最终一个完整的内存 分配图 , 在后面重定位 uboot时 使用
  • 设置中间环境 , 在DRAM的最后预留 各数据的内存空间 ,方便后面重定位

    • 其中 堆栈GD 是由board_init_f()在系统RAM (DRAM) 中分配的 ,
    • 但是BSS段和已初始化的 非const数据仍不可用
  • 调用 relocate_code 函数对uboot 进行真正的数据拷贝 和重定位 (不是 SPL)

    • 这个函数将U-Boot从当前位置 (片上RAM) 重新定位到由board_init_f()计算的重定位目的地 (DDR)
    • 对于SPL, board_init_f()只返回(到crt0)。在SPL中没有代码重定位。
  • 设置 能够 调用board_init_r()的最终环境 , 这个环境 存在以下条件 :

    • BSS段 ( 已初始化为0 )
    • 已初始化的非 const 数据 ( 初始化为预期值)
    • 在DRAM 上的堆栈
    • GD 保留了board_init_f() 设置的值
  • 调用 c_runtime_cpu_setup 函数 设置关于 CPU 此时的一些 内存设置

  • 调用board_init_r函数

    • 进行一些后续的初始化操作 , 如初始化 emmc、中断、环境变量等
    • board_init_r 中读取 uboot控制台指令 ,或跳转到系统内核运行

ENTRY(_main)

/* 设置初始C运行时环境并调用board_init_f(0) */
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
	ldr	sp, =(CONFIG_SPL_STACK)   
#else
	ldr	sp, =(CONFIG_SYS_INIT_SP_ADDR)	@ 设置sp指针指向CONFIG_SYS_INIT_SP_ADDR (即0x0091FF00)
#endif
#if defined(CONFIG_CPU_V7M)				@ 条件不成立
	mov	r3, sp
	bic	r3, r3, #7
	mov	sp, r3	
#else
	bic	sp, sp, #7						@ sp指针8字节对齐
#endif
	mov	r0, sp							@ 将sp保存到r0寄存器 , 此时r0 = 0x0091FF00
	bl	board_init_f_alloc_reserve		@ (具体见补充 '1' ) 参数通过r0传递,作用是留出早期的 malloc 内存区域和 gd 内存区域 
	mov	sp, r0							@ r0保存着返回值,将sp 设置为返回值 , 即0x0091FA00
    										
/* 在这里设置 gd的值, 在所有c代码之外 */
	mov	r9, r0							@ (具体见 补充 '2') 设置gd (r9)指向 0x0091FA00(r0),因为 r9 寄存器存放着全局变量 gd 的地址 
	bl	board_init_f_init_reserve	    @ (具体见 补充 '3')用于初始化 gd,即清零处理 , 设置early malloc起始地址 为 (gd即地址 + gd的大小)

	mov	r0, #0							@ 设置r0为0 ,用于传递参数0 方便后面调用 board_init_f(0) 即形参boot_flags = 0,
	bl	board_init_f					@ (具体见'2.3.2.1.5'小节)初始化DDR, 定时器,串口, 预留各数据在DRAM中的内存空间等,
	
#if ! defined(CONFIG_SPL_BUILD)

 /*  设置中间环境(新的sp和gd)并调用 relocate_code (addr_moni)
  *  最后设置 lr 寄存器为 here  ,后面执行其他函数如relocate_code 等, 返回的话 就会返回到here这个地址
  */
	ldr	sp, [r9, #GD_START_ADDR_SP]	 	@ sp = r9 + GD_START_ADDR_SP 即(sp = gd->start_addr_sp) ,因为r9是 结构体gd的基地址
										@ gd->start_addr_sp = 0x9EF44E90 ,这是属于DDR的地址,说明新的sp和gd放在ddr中而不是内部RAM

#if defined(CONFIG_CPU_V7M)				@ 条件不成立
	mov	r3, sp							
	bic	r3, r3, #7
	mov	sp, r3
#else
	bic	sp, sp, #7				@ sp做8字节对齐
#endif
	ldr	r9, [r9, #GD_BD]		@(具体见 补充 '4')将 gd->bd 的数据读入r9寄存器, r9存放的是就的gd基地址,通过 gd->bd计算新的gd地址
	sub	r9, r9, #GD_SIZE		@ 计算 gd 的新地址
								
	adr	lr, here                @ 将 lr 寄存器 赋值为 here , 这样后面执行其他函数返回的时候就返回到 下面53行here符号的地方 
	ldr	r0, [r9, #GD_RELOC_OFF]		@ r0 = gd->reloc_off   GD_RELOC_OFF = 68
	add	lr, lr, r0				@ 因为要重定位代码, 要把uboot拷贝到DDR的最后空间去 , 所以lr 中的here要使用重定位后的位置
#if defined(CONFIG_CPU_V7M)		
	orr	lr, #1					@ 条件不成立 , 这行不运行
#endif
	ldr	r0, [r9, #GD_RELOCADDR]	@ (r0 = gd->relocaddr, relocaddr保存uboot的目的地址)赋值后 , r0保存着 uboot 要拷贝的目的地址
	b	relocate_code			@ (具体见 '2.3.2.1.7' 小节)调用relocate_code 代码重定位函数 , 赋值将uboot 拷贝到新的地址
here:

/* 开始重定位向量表 */
	bl	relocate_vectors		@ (具体见 '2.3.2.1.8' 小节)调用 relocate_vectors ,重定位中断向量表

/* 设置最终(完整)环境 */

	bl	c_runtime_cpu_setup		@ 配置协处理器 ,关闭icache
#endif

#if !defined(CONFIG_SPL_BUILD) || defined(CONFIG_SPL_FRAMEWORK)		@条件不成立
# ifdef CONFIG_SPL_BUILD
	/* 如果请求,使用DRAM堆栈为其余的SPL堆栈 */
	bl	spl_relocate_stack_gd
	cmp	r0, #0
	movne	sp, r0
	movne	r9, r0
# endif

/********************************* 下面这段代码用于清除 BSS段   ********************************/
	ldr	r0, =__bss_start	/* bss段开始地址*/			

#ifdef CONFIG_USE_ARCH_MEMSET
	ldr	r3, =__bss_end		/* bss段结束地址 */
	mov	r1, #0x00000000		/* 将r1 赋 零用于清除 bss段 */

	subs	r2, r3, r0		/* r2 = r3-r0 , r2为bss段的长度 */
	bl	memset
#else 
	ldr	r1, =__bss_end		/* this is auto-relocated! */
	mov	r2, #0x00000000		/* prepare zero to clear BSS */

clbss_l:cmp	r0, r1			/* while not at end of BSS */
#if defined(CONFIG_CPU_V7M)
	itt	lo
#endif
	strlo	r2, [r0]		/* clear 32-bit BSS word */
	addlo	r0, r0, #4		/* move to next */
	blo	clbss_l
#endif

/**********************************************************************/

#if ! defined(CONFIG_SPL_BUILD)
	bl coloured_LED_init
	bl red_led_on
#endif
	/* 设置 board_init_r(gd_t *id, ulong dest_addr) 两个参数 ,用r0、r1传参 */
	mov     r0, r9                 @ 第一个参数是gd , 所以读取r9保存到r0
	ldr	r1, [r9, #GD_RELOCADDR]	   @ 第二个参数是目的地址 , 所以 r1= gd->relocaddr
	
	/* 调用 board_init_r 函数*/
#if defined(CONFIG_SYS_THUMB_BUILD)	@条件不成立
	ldr	lr, =board_init_r	
	bx	lr
#else
	ldr	pc, =board_init_r	  @ (具体见 '2.3.2.1.9' 小节5)调用board_init_r函数 ,继续完成初始化工作   
#endif
	/* we should not return here. */
#endif

ENDPROC(_main)

补充:

1️⃣ board_init_f_alloc_reserve 函数

位置 : common/init/board_init.c

函数功能如下:

  • 留出早期的 malloc 内存区域和 gd 内存区域

ulong board_init_f_alloc_reserve(ulong top)
{
	/* 预留早期 malloc的内存区域 */
#if defined(CONFIG_SYS_MALLOC_F)
	top -= CONFIG_SYS_MALLOC_F_LEN;
#endif
	/* LAST : 保留 GD 内存区域(四舍五入到16字节的倍数) */
	top = rounddown(top-sizeof(struct global_data), 16);

	return top;
}
  • 其中CONFIG_SYS_MALLOC_F_LEN=0X400

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

  • 完成后的 内存分配 :

    image-20230516151408214



2️⃣ 全局变量 global_data(gd)

uboot 中定义了一个指向 gd_t 的指针 gdgd 存放在寄存器 r9 里面 ,因此 gd是个 全局变量

#ifdef CONFIG_ARM64
#define DECLARE_GLOBAL_DATA_PTR		register volatile gd_t *gd asm ("x18")
#else
#define DECLARE_GLOBAL_DATA_PTR		register volatile gd_t *gd asm ("r9")
#endif

gd_t结构体

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_LCD) || defined(CONFIG_VIDEO)
	unsigned long fb_base;	/* Base address of framebuffer mem */
#endif
//.......................//

#ifdef CONFIG_DM_VIDEO
    ulong video_top;		/* Top of video frame buffer area */
    ulong video_bottom; 	/* Bottom of video frame buffer area */
#endif
}gd_t;


3️⃣ board_init_f_init_reserve 函数

位置 : common/init/board_init.c

功能 :

  • 用于初始化 gd , 即清零处理
  • 设置 gd->malloc_basegd 基地址 + gd 大小=0X0091FA00+248=0X0091FAF8
  • 并做16字节对齐 , 最终gd->malloc_base=0X0091FB00,这个也就是 early malloc起始地址


4️⃣ 通过 gd->bd 计算新的 gd 地址

涉及的代码

ldr	r9, [r9, #GD_BD]				// 获取gd -> bd的地址
sub	r9, r9, #GD_SIZE				// 减去bd 结构体占用的空间 即为 gd结构体的空间

通过前文 , 可以得到如下信息 :

  • r9寄存器存放的是 一个指向 gd_t结构体 的指针 gd , 即r9寄存器存放的是 gd 数据结构体 旧的 基地址 ( 片上 RAM, 不是DRAM)

  • 板信息 bdgd结构体的第一个成员 , 即 gd -> bd首地址 与 gd结构体的 基地址 (即r9寄存器保存的值) 是一致的

    typedef struct global_data {
    	bd_t *bd;
    	unsigned long flags;
    ...
    }gd_t;
    
  • gd结构体的 gd -> bd 成员在 调用 board_init_f 函数的时候 就已经被重定位在 DRAM上了 ( 即 新地址 )

    if (initcall_run_list(init_sequence_f))		// 调用initcall_run_list函数来运行初始化序列
    ....
    
    static init_fnc_t init_sequence_f[] = {		// init_sequence_f是initcall_run_list 的传入参数 (一个存放各个函数入口的数组) 
    ... 
    reserve_board, 					//	在DRAM留出板子 bd 所占的内存区 , 完成后 gd -> bd = 0X9EF44FB0	
    ...
    }
    
  • 两个宏其中 GD_BD = 0 ; GD_SIZE = 248

    • 因为bd 是 gd结构体 的第一个成员 所以 gd -> bd = r9 + GD_BD
    • GD_SIZEgd结构体的大小 , 为248B
  • 为什么 gd->bd 减去 gd 的大小就是 新的 gd 的位置

    • 因为 gd新的地址 (即在DRAM中的地址) 是在 bd数据的下面 ( 即 低地址位置 )

    • 图为 调用 board_init_f 函数后 在DRAM 中的内存空间图

      image-20230517172742492


5️⃣


2.3.2.1.5 board_init_f 函数

位置 : common/board_f.c

功能 :

  • 初始化一系列外设,比如串口、定时器,或者打印一些消息等

  • 初始化 gd 的各个成员变量, 将uboot DRAM 最后面的地址区域 预留区域 , 方便后面拷贝

    • 因为本质上 ubootlinux 的引导文件,引导完成后 linux 会在 DRAM 前面的地址区域启动
    • 为了防止 linux 启动后对 uboot 进行干扰,uboot 会将自己重定位到 DRAM 最后面的地址区域
    • 拷贝之前需要给 uboot 各部分分配好内存位置和大小 ,比如 gd 应该存放到哪个位置,malloc 内存池应该存放到哪个位置等
    • 这些信息都保存在 gd成员变量 中,因此首先要对 gd 的这些成员变量做初始化
  • DRAM最后部分预留各数据的内存空间 (如 ubootmallocgdbd等) , 最终一个完整的内存 分配图 , 在后面重定位 uboot时 使用


void board_init_f(ulong boot_flags)
{
#ifdef CONFIG_SYS_GENERIC_GLOBAL_DATA				// 条件不成立
	/*
	 * 对于某些架构来说, 全局变量在调用这个函数之前就被被初始化和使用,所以应该保存全局变量的数据
	 * 对于这些架构,应该定义CONFIG_SYS_GENERIC_GLOBAL_DATA这个宏,并在重定位之前使用这里的堆栈来承载全局数据
	 */
	gd_t data;

	gd = &data;
    
	zero_global_data();
#endif

	gd->flags = boot_flags;						//  初始化 gd->flags=boot_flags=0
	gd->have_console = 0;

	if (initcall_run_list(init_sequence_f))		// (具体见 补充'2.3.2.1.6'小节)通过 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();
}
  • 形参 boot_flags 是通过 r0传递的 , r0 = boot_flags = 0
  • 调用initcall_run_list函数来运行 初始化序列 , 传入参数是init_sequence_f (一个存放各个函数入口的数组)


2.3.2.1.6 init_sequence_f 数组

位置 : common/board_f.c

功能 :

  • 是一个存放了各个函数入口的数组
  • 通过initcall_run_list 来一系列初始化序列 , 设置GD的各个成员的值
  • 用于初始化一系列外设,比如串口、定时器,或者打印一些消息等

去除条件编译后的init_sequence_f如下 :

static init_fnc_t init_sequence_f[] = {
setup_mon_len, 			// 设置gd->mon_len ,此处为 __bss_end -_start = 0xA8E74, 即整个代码的长度
initf_malloc, 			// 数初始化gd中跟malloc有关的成员变量,比如 malloc_limit (malloc内存池大小), 这里会设置
initf_console_record, 	// 对于 IMX6ULL来说 是空函数
arch_cpu_init, 			// 基本的arch CPU相关设置 
initf_dm, 				// 驱动模型的一些初始化
arch_cpu_init_dm, 		// 函数未实现
mark_bootstage, 		// 设置某些标记
board_early_init_f, 	// 板子相关的
timer_init, 			// 初始化定时器  
board_postclk_init, 	// 对于 I.MX6ULL 来说是设置 VDDSOC 电压
get_clocks,				// 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, 		// 通过串口输出一些信息, 这里是uboot 的版本信息   
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, 				// 配置可用RAM组,并非真正的初始化 DDR,只是设置gd->ram_size ,即DDR的大小(如 512MB)  
post_init_f, 			// 完成一些测试,初始化 gd->post_init_f_time
INIT_FUNC_WATCHDOG_RESET// 复位看门狗,对于 I.MX6ULL 来说是空函数
testdram, 				// 测试 DRAM,空函数
INIT_FUNC_WATCHDOG_RESET// 复位看门狗,对于 I.MX6ULL 来说是空函数
INIT_FUNC_WATCHDOG_RESET// 复位看门狗,对于 I.MX6ULL 来说是空函数
/* 
* 到这里为止 , 已经映射了DRAM并开始工作, 
* 可以开始重定位代码 并继续从 DRAM 运行
*  
* 在RAM末端预留内存(按顺序从上到下):
* - mmu的TLB表 (reserve_mmu = 0x4000 = 16KB , 64K字节对齐)
* - 跟踪调试的内存 (reserve_trace = 0)
* - uboot 所占用的内存区域 (reserve_uboot = 0xA8EF4 , 4K字节对齐)
* - malloc 区域 (reserve_malloc = 0x01002000 =16MB + 8KB)
* - 板子bd结构体的内存 (reserve_board = 80B)
* - `gd_t` 的内存区域 (240B)
* -  栈空间 (16字节对齐)
*/
setup_dest_addr, 		// (补充 '1')设置目的地址,设置gd->ram_size; gd->ram_top; gd->relocaddr 这三个值
reserve_round_4k, 		// 对 gd->relocaddr 做 4KB 对齐 , 这里的值0XA0000000,已经是 4K 对齐了,所以调整后不变
reserve_mmu, 			// (补充 '2')留出 MMU 的 TLB 表的位置, 分配完后会对 gd->relocaddr 做 64K 字节对齐
reserve_trace, 			// 留出跟踪调试的内存,I.MX6ULL 没有用到
reserve_uboot, 			// (补充 '3')留出重定位后的 uboot 所占用的内存区域, 大小由gd->mon_len 所指定, 分配完后做 4K字节对齐
reserve_malloc, 		// (补充 '4')留出 malloc 区域, 调整 gd->start_addr_sp 位置;malloc 区域由宏TOTAL_MALLOC_LEN定义
reserve_board, 			// (补充 '5')留出板子 bd 所占的内存区,bd 是结构体 bd_t,bd_t 大小为80字节 , 后续根据 gd->bd 计算出新的 gd 的位置 ,用于uboot重定位
setup_machine, 			// 设置机器 ID,linux 启动的时候会和这个机器 ID 匹配,如果匹配的话 linux 就会启动正常 ; IMX6ULL使用设备树,所以此函数无效
reserve_global_data, 	// (补充 '6')保留出 gd_t 的内存区域,gd_t 结构体大小为 248 字节
reserve_fdt, 			// 留出设备树相关的内存区域, I.MX6ULL 的 uboot 没有用到,所以此函数无效
reserve_arch, 			// 空函数
reserve_stacks,			// (补充 '7')留出栈空间, 先对 gd->start_addr_sp 减去 16,然后做 16 字节对齐,如果使能IRQ的话也要留出对应内存 ,这里没有使用
setup_dram_config, 		// (补充 '8')设置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 存放的值
INIT_FUNC_WATCHDOG_RESET
reloc_fdt, 				// 重定位 fdt,没有用到
setup_reloc, 			// (补充 '9') 设置 gd 的其他一些成员变量,供后面重定位的时候使用,并且将以前的 gd 拷贝到 gd->new_gd 处
NULL,
};


补充:

1️⃣ setup_dest_addr 函数

setup_dest_addr 函数 主要用于设置目的地址 , 主要用于输出以下三个 值:

  • gd->ram_size ( RAM的大小 ) 这里是 0x20000000 , 512MB
  • gd->ram_top ( RAM的最高地址 ) 这里是 0x80000000 + 0x20000000 = 0xA0000000
  • gd->relocaddr (重定位后的最高地址 ) 这里是0xA000000


2️⃣ reserve_mmu 函数

留出 MMUTLB 表的位置,分配 MMUTLB 表内存以后会对 gd->relocaddr 做 64K 字节对齐

完成之后的 gd->arch.tlb_sizegd->arch.tlb_addrgd->relocaddr 如下所示:

  • gd->arch.tlb_size : MMUTLB表大小 (这里为 0x4000)
  • gd->arch.tlb_addr : MMUTLB 表起始地址,64KB 对齐以后 ( 这里为 0x9FFF0000)
  • gd->relocaddr : relocaddr 地址 ( 这里为 0x9FFF0000)


3️⃣ reserve_uboot 函数

  • 留出重定位后的 uboot 所占用的内存区域 ,
  • uboot 所占用大小由gd->mon_len 所指定,留出 uboot 的空间以后还要对 gd->relocaddr 做 4K 字节对齐
  • 并且重新设置 gd->start_addr_sp

完成之后, gd->mon_len , gd->start_addr_sp , gd->relocaddr 如下所示:

  • gd->mon_len : uboot 所占的大小 ( 这里为 0xA8EF4)
  • gd->start_addr_sp : 重设 gd->start_addr_sp 指针 ( 这里为 0x9FF47000)
  • gd->relocaddr : relocaddr 地址 ( 这里为 0x9FF47000)


4️⃣ reserve_malloc 函数

  • reserve_malloc 函数 留出 malloc 区域,
  • 调整 gd->start_addr_sp 位置,
  • malloc 区域由宏TOTAL_MALLOC_LEN 定义

宏定义如下 :

#define TOTAL_MALLOC_LEN (CONFIG_SYS_MALLOC_LEN + CONFIG_ENV_SIZE)
  • CONFIG_SYS_MALLOC_LEN16MB=0X1000000
  • CONFIG_ENV_SIZE=8KB=0X2000
  • 因此 TOTAL_MALLOC_LEN=0X1002000 (即malloc 的区域大小为 0X1002000)

可以得到:

TOTAL_MALLOC_LEN=0X1002000
gd->start_addr_sp=0X9EF45000  @0X9FF47000-16MB-8KB=0X9EF45000


5️⃣ reserve_board 函数

  • reserve_board 函数,用于留出板子 bd 所占的内存区
  • bd 是结构体 bd_tbd_t 大小为 80字节

调整之后结果如下:

gd->start_addr_sp=0X9EF44FB0
gd->bd=0X9EF44FB0		@ 调用完board_init_f这个函数之后 , 这个根据gd->bd, 来获取重定位后 , 新的gd的地址


6️⃣ reserve_global_data 函数

保留出 gd_t 的内存区域,gd_t 结构体大小为 248B

完成后结果如下 :

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


7️⃣ reserve_stacks 函数

  • reserve_stacks 函数 用于 留出栈空间
  • 先对 gd->start_addr_sp 减去 16 , 然后做 16字节对齐
  • 如果使能 IRQ 的话还要留出 IRQ 相应的内存 , 这里不使能

完成后结果如下

gd->start_addr_sp=0X9EF44E90    


8️⃣ setup_dram_config 函数

  • setup_dram_config 函数 用于设置 dram信息
  • 即设置 gd->bd->bi_dram[0].startgd->bd->bi_dram[0].size 两个成员
  • 用于后续传递给 linux 内核 , 告诉linux DRAM 的起始地址和大小
gd->bd->bi_dram[0].start=0x80000000
gd->bd->bi_dram[0].size=0x20000000
  • 即传递给linux内核 , DRAM 的起始地址为 0x80000000 , 大小为 0X20000000(512MB)


9️⃣ setup_reloc 函数

  • setup_reloc 函数 用于设置 gd其他一些成员变量 , 供后面重定位的时候使用
  • 并将之前的 gd拷贝到 gd->new_gd处
  • 重定位后 , uboot 的新地址为 0X9FF4700 ;
  • 新的gd首地址为 0X9EF44EB8 ;
  • 新的 sp为0X9EF44E90


🔟 重定位后的内存分配图

image-20230517172742492



2.3.2.1.7 relocate_code 函数

位置 : arch/arm/lib/relocate.S

功能 :

  • 代码拷贝 , 将uboot 拷贝到 DDR中 ,即uboot重定位 到 DRAM的高地址
  • 重定位就是 uboot 将自身拷贝到 DRAM 的另一个地放去继续运行 (DRAM 的高地址处)
ENTRY(relocate_code)

/************************************** 获取各个地址 ********************************************/

	ldr	r1, =__image_copy_start			@ r1保存寄存器源地址 , 即0x8780 0000 , (__image_copy_start)在链接文件中 , 使用零长度数组标记代码段
	subs	r4, r0, r1					@ 保存偏移量 , r0为 gd->relocaddr = 0x9FF4 7000 (即uboot拷贝的首地址) r4 = r0-r1 为偏移量
	beq	relocate_done					@ 判断r4是否为0, 即r0 - r1 运算结果 z = 0 ,如果是,则说明不用拷贝,直接执行relocate_done函数
	ldr	r2, =__image_copy_end			@ r2=__image_copy_end, 使用r2保存 拷贝之前的代码结束地址 (片上RAM)

/*************************完成拷贝工作 , 拷贝 r1到r2这段地址的内容, 并写到目的地址 r0中去 *********************/
copy_loop:
	ldmia	r1!, {r10-r11}				@ 从r1 开始 即(__image_copy_start) , 拷贝2个32位数据到 r10和r11中 , 拷贝完成后 ,r1的值会更新
	stmia	r0!, {r10-r11}		 		@ 将r10和 r11的值写到目的地址 r0 即(gd->reloc_of), 写完后 , r0的值会更新
	cmp	r1, r2							@ 比较r1 和 r2是否相等 , 即确定是否拷贝完成
	blo	copy_loop						@ 没有则跳转回 copy_loop 继续拷贝 (检查CPSR 寄存器C 标志位是否为0)


/*********************** 重定位.rel.dyn 段 , .rel.dyn 段是存放.text 段中需要重定位地址的集合 ***********/
	
	ldr	r2, =__rel_dyn_start	@ r2 =__rel_dyn_start, 即 .rel.dyn 段的起始地址
	ldr	r3, =__rel_dyn_end		@ r3 =__rel_dyn_end,  
fixloop:
	ldmia	r2!, {r0-r1}		@ 从起始地址开始 , 每次取两个 4字节数据放到r0和r1寄存器, r0存放低4字节(即Label 地址); r1存放高4字节(即Label 标志)
	and	r1, r1, #0xff			@ 取r1的低8位
	cmp	r1, #23					@ 判断r1 中的值是否等于 23(0x17)
	bne	fixnext					@  r1 不等于 23说明不是描述 Label的,执行fixnext,否则的话就继续执行下面的代码

	/* relative fix: increase location by offset */
	add	r0, r0, r4				@ r0 保存着 Label 值,r4 保存着重定位后的地址偏移,r0+r4 就得到了重定位后的Label 值  
	ldr	r1, [r0]				@ 读取重定位后 Label 所保存的变量地址 
	add	r1, r1, r4				@ r1+r4 可得到重定位后的变量地址 , 
	str	r1, [r0]				@ 重定位后的变量地址写入到重定位后的 Label 中
fixnext:
	cmp	r2, r3					@ 比较 r2 和 r3,查看.rel.dyn 段重定位是否完成
	blo	fixloop					@ 如果 r2 和 r3 不相等,说明.rel.dyn 重定位还未完成 ,继续重定位 .rel.dyn段

relocate_done:

#ifdef __XSCALE__

/*在xscale上,icache必须无效并且写缓冲区耗尽, 即使禁用缓存*/

	mcr	p15, 0, r0, c7, c7, 0	@ 禁用 icache (指令 cache) 
	mcr	p15, 0, r0, c7, c10, 4	@ 将写缓冲区耗尽 
#endif


#ifdef __ARM_ARCH_4__
	mov	pc, lr
#else
	bx	lr
#endif

ENDPROC(relocate_code)

补充:

1️⃣ 重定位后 寻址会不会有问题

重定位以后,运行地址就和链接地址不同了 , 但寻址的时候却不会出问题 , 原因如下:

  • 首先 uboot 函数寻址时使用到了 bl 指令,而 bl 指令时位置无关指令
  • bl 指令是相对寻址的 (pc+offset) ,因此 uboot 中函数调用是与 绝对位置 无关的
  • 其次函数对变量的访问没有直接进行,而是使用了一个第三方偏移地址,叫做 Label
  • 这个第三方偏移地址就是实现 重定位 后运行不会出错的重要原因
  • uboot 对于重定位后链接地址和运行地址不一致的解决方法就是 采用位置无关码,
  • 在使用 ld 进行链接的时候使用选项“-pie”生成位置无关的可执行文件生成一个.rel.dyn 段,uboot 就是靠这个.rel.dyn 来解决重定位问题的


2.3.2.1.8 relocate_vectors 函数

位置 : arch/arm/lib/relocate.S

功能 : relocate_vectors 函数用于重定位向量表

ENTRY(relocate_vectors)

#ifdef CONFIG_CPU_V7M					@ 是 Cortex-M 内核执行的语句 ,因此条件无效

	ldr	r0, [r9, #GD_RELOCADDR]			@ r0 = gd->relocaddr 
	ldr	r1, =V7M_SCB_BASE
	str	r0, [r1, V7M_SCB_VTOR]
#else

#ifdef CONFIG_HAS_VBAR					@ 支持向量表偏移则条件成立 , 这里条件成立

   /*如果ARM处理器有安全扩展,使用VBAR重新定位异常向量。*/

	ldr	r0, [r9, #GD_RELOCADDR]			@ gd->relocaddr为重定位后的 uboot首地址
	mcr p15, 0, r0, c12, c0, 0 			@ 将r0的值写入 CP15 的VBAR寄存器中 , 即将新的向量表写入到 寄存器 VBAR中
	#else								@ VBAR是向量表基地址寄存器。设置中断向量表偏移的时候就需要 将新的中断向量表基地址写入 VBAR 中

	/* 将重新定位的中断向量表复制到正确的地址, 在CP15 的c1寄存器的 V 位给出了中断向量表的基地址 0x00000000*/
	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)


2.3.2.1.9 board_init_r函数

位置 : common/board_r.c

功能 :

  • 在前面 的 board_init_f函数并没有 对所有的外设进行初始化 , 还需要做一些后续的初始化工作
  • 这些后续初始化 工作就是由 board_init_r 函数来完成的
  • 跟前面的 board_init_f 函数一样也是通过 调用 initcall_run_list 来运行初始化序列
  • 函数集合 init_sequence_r 用于存放一系列初始化函数

函数集合 init_sequence_r 如下所示 (已删去大量条件编译)

init_fnc_t init_sequence_r[] = {
	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 中运行
	INIT_FUNC_WATCHDOG_RESET
	INIT_FUNC_WATCHDOG_RESET
	INIT_FUNC_WATCHDOG_RESET
	power_init_board,				// 初始化电源芯片
	initr_flash,					// 对于 I.MX6ULL 此函数无效
	INIT_FUNC_WATCHDOG_RESET		
	initr_nand,						// 如果有NAND的话 初始化 NAND
	initr_mmc,						// 如果有emmc的话 初始化emmc
	initr_env,						// 初始化环境变量
	INIT_FUNC_WATCHDOG_RESET
	initr_secondary_cpu,			// 初始化其他 CPU 核,I.MX6ULL 只有一个核,因此此函数没用
	INIT_FUNC_WATCHDOG_RESET
	stdio_add_devices,				// 各种输入输出设备的初始化,如 LCD driver,I.MX6ULL 使用 drv_video_init 函数初始化 LCD
	initr_jumptable,				// 初始化跳转表
	console_init_r,					// 控制台初始化,初始化完成以后此函数会调用 stdio_print_current_devices 函数来打印出当前的控制台设备
	INIT_FUNC_WATCHDOG_RESET
	interrupt_init,					// 初始化中断
	initr_enable_interrupts,		// 使能中断
	initr_ethaddr,					// 初始化网络地址,也就是获取 MAC 地址。读取环境变量 "ethaddr" 的值
	board_late_init,				// 板子后续初始化,如果环境变量存储在 EMMC 或者 SD 卡中的话 , 此函数会调用 board_late_mmc_env_init 函数初始化 EMMC/S
	INIT_FUNC_WATCHDOG_RESET
	INIT_FUNC_WATCHDOG_RESET
	INIT_FUNC_WATCHDOG_RESET
	initr_net,						// 初始化网络设备
	INIT_FUNC_WATCHDOG_RESET
	run_main_loop,					// 主循环 , 处理命令
}



2.3.2.2 阶段2 : bootz启动linux 内核

在uboot内核启动流程中 , 已经完成了以下工作 :

  • 设置 CPU 工作模式

    • 禁用 中断 (FIQIRQ)
    • CPU设置为 SVC模式
  • 给linux 内核传递参数 如 DRAM起始地址和大小

  • 关闭 MMU 、关闭 数据cache


通过 bootz 启动linux 内核流程如下

20220213220954

2.3.2.2.1 images 全局变量

启动 Linux 内核的时候会用到一个重要的全局变量 ,

bootm_headers_t images;

bootm_headers_t 是个 boot 头结构体,在文件 include/image.h 中的定义 . 其中的os 成员变量是 image_info_t 类型的,为 系统镜像信息

#ifndef USE_HOSTCC
	image_info_t os;		 /* OS 镜像信息 */
	ulong ep; 				/* OS 入口点 */

结构体 image_info_t 是系统 镜像信息结构体 ,具体如下:

typedef struct image_info {
	ulong start, end; 					/* blob 开始和结束位置*/
	ulong image_start, image_len; 		/* 镜像起始地址(包括 blob)和长度 */
	ulong load; 						/* 系统镜像加载地址*/
	uint8_t comp, type, os; 			/* 镜像压缩、类型,OS 类型 */
	uint8_t arch; 						/* CPU 架构 */
} image_info_t;

下面的 11 个宏定义表示 U-BOOT 的不同阶段

#define BOOTM_STATE_START 				(0x00000001)
#define BOOTM_STATE_FINDOS 				(0x00000002)
#define BOOTM_STATE_FINDOTHER 			(0x00000004)
#define BOOTM_STATE_LOADOS 				(0x00000008)
#define BOOTM_STATE_RAMDISK 			(0x00000010)
#define BOOTM_STATE_FDT 				(0x00000020)
#define BOOTM_STATE_OS_CMDLINE 			(0x00000040)
#define BOOTM_STATE_OS_BD_T 			(0x00000080)
#define BOOTM_STATE_OS_PREP 			(0x00000100)
#define BOOTM_STATE_OS_FAKE_GO 			(0x00000200)		/*'Almost' run the OS*/
#define BOOTM_STATE_OS_GO 				(0x00000400)
	int state;


2.3.2.2.2 bootz 命令

bootz 命令完成以下的工作 :

  • do_bootz 函数

    bootz_start 函数

    • bootz_srart 函数中设置 imagesep 成员变量,也就是系统镜像的入口点 , 使用 bootz 命令启动系统的时候就会设置系统在 DRAM 中的存储位置,这个存储位置就是系统镜像的入口点,因此 images->ep=0X80800000
    • 查询镜像文件是否为 linux 镜像文件 , 以及用于查询设备树文件 ( dbt) ,

    • 调用函数 bootm_disable_interrupts 关闭中断
    • 设置 images.os.osIH_OS_LINUX,也就是设置系统镜像为 Linux ( 后面会用到 images.os.os 来挑选具体的启动函数 )

    do_bootm_states 函数

    • do_bootz 函数的最后 调用 了 do_bootm_states 函数 , 用于根据不同的 BOOT 状态执行不同的代码段,判断 BOOT 的状态 , 然后根据BOOT的状态执行不同的代码

      states & BOOTM_STATE_XXX
      
    • 通过函数 bootm_os_get_boot_func 来查找系统启动函数

      boot_fn = bootm_os_get_boot_func(images->os.os);
      
      • 参数 images->os.os 就是系统类型 , 即之前设置的 IH_OS_LINUX
      • bootm_os_get_boot_func 的返回值 就是 找到的 Linux 系统启动函数为 do_bootm_linux

    • (见 ‘2.3.2.2.3’ 小节) do_bootm_linux 函数

      • do_bootm_linux 函数最终会 跳转执行 boot_prep_linuxboot_jump_linux 函数

      • boot_prep_linux 主要用于 处理环境变量 bootargs , bootargs 保存着 传递给 linux内核的参数

        static void boot_prep_linux(bootm_headers_t *images)
        {
            char *commandline = getenv("bootargs");      //从环境变量中获取 bootargs 的值
        
          。。。。。。。
                setup_board_tags(&params);      
                setup_end_tag(gd->bd);    //将 tag 参数保存在指定位置
            } else {
                printf("FDT and ATAGS support not compiled in - hanging\n");
                hang();
            }
            do_nonsec_virt_switch();
        }
        
      • boot_jump_linux 函数 , 保存机器ID (如果不使用设备树的话这个机器 ID 会被传递给 Linux

        内核) , 并最终调用 kernel_entry 函数 ,进入Linux内核



2.3.2.2.3 do_bootm_linux 函数

位置 arch/arm/lib/bootm.c

功能 调用boot_prep_linuxboot_jump_linux 两个函数, 并最终启动 linux 内核

int do_bootm_linux(int flag, int argc, char * const argv[],
bootm_headers_t *images)
{
/* No need for those on ARM */
	if (flag & BOOTM_STATE_OS_BD_T || flag & BOOTM_STATE_OS_CMDLINE)
		return -1;

	if (flag & BOOTM_STATE_OS_PREP) {
		boot_prep_linux(images);
		return 0;
}

if (flag & (BOOTM_STATE_OS_GO | BOOTM_STATE_OS_FAKE_GO)) {
	boot_jump_linux(images, flag);
	return 0;
}

	boot_prep_linux(images);
	boot_jump_linux(images, flag);
	return 0;
}

可以看到 do_bootm_linux 函数 最终调用了 boot_prep_linuxboot_jump_linux 两个函数



补充:

1️⃣ boot_jump_linux 函数

位置 : arch/arm/lib/bootm.c

功能 :

  • 保存机器 ID,如果不使用 设备树 的话这个机器 ID 会被传递给 Linux , linux内核会查找 是否存在 与这个ID匹配的项目,那么 Linux 内核就会启动 ( 如果使用 设备树 的话 ,这个 ID 就无效了 )

  • 调用 kernel_entry 函数进入 Linux 内核

    • kernel_entry 函数 并不是 uboot 定义的 , 而是Linux 内核定义的 , Linux 内核镜像文件的第一行代码就是函数 kernel_entry 函数 , 因此要首先获取 kernel_entry 函数

      kernel_entry = (void (*)(int, int, uint))images->ep;
      
      • images->ep 保存着 Linux 内核镜像的起始地址 , 起始地址保存的是 Linux 内核的第一行代码
    • Linux 内核一开始是 汇编代码,因此函数 kernel_entry 就是个汇编函数 , 向汇编函数传递参数要使用 r0、r1 和 r2 (参数数量不超过3个的时候)

      • kernel_entry 函数 有三个参数 zero,arch,params
      • 第一个参数 zero 为 0
      • 第二个参数为机器 ID
      • 第三个参数 ATAGS 或者 设备树(DTB) 首地址,ATAGS 是传统的方法,用于传递一些命令行信息啥的,如果使用设备树的话就要传递设备树(DTB)。
      • 当使用设备树时 , r2 应该是设备树的起始地址,而设备树地址保存在 imagesftd_addr 成员变量中
      • 如果不使用设备树的话,r2 应该是 uboot 传递给 Linux 的参数起始地址 , 即 环境变量 bootargs 的值
static void boot_jump_linux(bootm_headers_t *images, int flag)
{
    unsigned long machid = gd->bd->bi_arch_number;      //获取机器id (在 board/samsung/jz2440/jz2440.c 中设置,为 MACH_TYPE_SMDK2410(193))
    char *s;
    void (*kernel_entry)(int zero, int arch, uint params);
    unsigned long r2;
    int fake = (flag & BOOTM_STATE_OS_FAKE_GO);

    kernel_entry = (void (*)(int, int, uint))images->ep;    //获取 kernel的入口地址,此处应为 30000000

    s = getenv("machid");        						//从环境变量里获取机器id (本例中还未在环境变量里设置过机器 id)
    if (s) {            							//判断环境变量里是否设置机器id
        strict_strtoul(s, 16, &machid);    				//如果设置则用环境变量里的机器id
        printf("Using machid 0x%lx from environment\n", machid);
    }

    debug("## Transferring control to Linux (at address %08lx)" \
        "...\n", (ulong) kernel_entry);
    bootstage_mark(BOOTSTAGE_ID_RUN_OS);
    announce_and_cleanup(fake);

    if (IMAGE_ENABLE_OF_LIBFDT && images->ft_len)
        r2 = (unsigned long)images->ft_addr;
    else
        r2 = gd->bd->bi_boot_params;   				 //获取 tag参数地址,gd->bd->bi_boot_params在 setup_start_tag 函数里设置
if (!fake) kernel_entry(0, machid, r2); } 			 //进入内核


2.3.2.2.4 补充

1️⃣ 内核镜像格式vmlinuzzImageuImage

  • uboot经过编译直接生成的elf格式的可执行程序是u-boot,这个程序类似于windows下的exe格式,在操作系统下是可以直接执行的。但是这种格式不能用来烧录下载。我们用来烧录下载的是u-boot.bin,这个东西是由u-boot使用arm-linux-objcopy工具进行加工(主要目的是去掉一些无用的)得到的。这个u-boot.bin就叫 镜像(image),镜像就是用来烧录到 EMMC 中执行的。

  • linux内核经过编译后也会生成一个elf格式的可执行程序,叫vmlinuxvmlinuz,这个就是原始的 未经任何处理加工的原版内核elf文件;嵌入式系统部署时烧录的一般不是这个vmlinuz/vmlinux,而是要用objcopy工具去制作成烧录镜像格式(就是u-boot.bin这种,但是内核没有.bin后缀),经过制作加工成烧录镜像的文件就叫Image(制作把78M大的精简成了7.5M,因此这个制作烧录镜像主要目的就是缩减大小,节省磁盘)。

  • 原则上Image就可以直接被烧录到Flash上进行启动执行(类似于u-boot.bin),但是实际上并不是这么简单。实际上linux的作者们觉得Image还是太大了所以对Image进行了压缩,并且在image压缩后的文件的前端附加了一部分解压缩代码。构成了一个压缩格式的镜像就叫zImage

  • uboot为了启动linux内核,还发明了一种内核格式叫uImageuImage是由zImage加工得到的,uboot中有一个工具,可以将zImage加工生成uImage。注意:uImage不关linux内核的事,linux内核只管生成zImage即可,然后uboot中的mkimage工具再去由zImage加工生成uImage来给uboot启动。这个加工过程其实就是在zImage前面加上64字节的uImage头信息 即可。

  • 原则上uboot启动时应该给他uImage格式的内核镜像,但是实际上uboot中也可以支持zImage,是否支持就看x210_sd.h中是否定义了LINUX_ZIMAGE_MAGIC这个宏。可以看出:有些uboot是支持zImage启动的,有些则不支持。但是所有的uboot肯定都支持uImage启动。

  • 如果直接在kernel底下去make uImage会提供mkimage command not found。解决方案是去uboot/toolscp mkimage /usr/local/bin/,复制mkimage工具到系统目录下。再去make uImage即可。

  • 因此如果通过uboot启动内核,Linux必须为uImage格式 ( 或部分支持zImage)。



2️⃣ 给内核传递参数

怎么从uboot 跳转 内核启动

只要 直接修改PC寄存器的值为Linux内核所在的地址 , 这样CPU就会从内核所在的地址 去取指令 , 从而执行内核代码

为什么要给内核传递参数呢?

uboot启动的第一阶段 , uboot 基本完成了 硬件的初始化 , 但内核 对于此时 开发板的环境 一无所知 , 所以要启动 Linux 内核 , uboot 必须要给 内核传递一些必要的信息 , 来告知内核 当前所处的环境


如何给内核传递参数

  • uboot 通过寄存器 r0、r1 和 r2 将参数传递给内核

例如

  • uboot机器ID通过R1传递给内核Linux内核运行的时候,首先就从R1中读取机器ID来判断是否支持当前机器。这个机器ID实际上就是开发板 CPU的ID,每个厂家生产出一款CPU的时候都会给它指定一个唯一的ID ( 当然使用设备树的话, 情况会有所不同)
  • *R2存放的是块内存的基地址 ,这块内存中存放的是ubootLinux内核的其他参数。这些参数有内存的 起 始地址、内存大小、 Linux 内核启动后挂载文件系统的方式等信息 。很明显,参数有多个,不同的参数有不同的内容,为了让Linux内核能精确的解析出这些参数,双方在传递参数的时候要求参数在存放的时候需要 按照双方规定的格式存放


3️⃣ 参数结构

  • 在 uboot 和 内核传递参数的过程中 , 除了约定好参数存放的地址外,还要规定参数的结构。Linux2.4.x以后的内核都期望以标记列表 (tagged_list)的形式来传递启动参数。
  • 标记,就是一种数据结构;标记列表,就是挨着存放的多个标 记。标记列表以标记 ATAG_CORE 开始,以标记 ATAG_NONE 结束。

标记数据结构 tag

标记的数据结构为tag,它由一个tag_header结构和一个联合(union)组成。tag_header结构表示标记的 类型及长度,比如是表示内存还是表示命令行参数等。对于不同类型的标记使用不同的 联合(union),比如表示内存时使用tag_mem32,表示命令行时使用 tag_cmdline。 ( 具体见 arch\arm\include\asm\setup.h )

struct tag {
	struct tag_header hdr;
	union {
		struct tag_core		core;
		struct tag_mem32	mem;
		struct tag_videotext	videotext;
		struct tag_ramdisk	ramdisk;
		struct tag_initrd	initrd;
		struct tag_serialnr	serialnr;
		struct tag_revision	revision;
		struct tag_videolfb	videolfb;
		struct tag_cmdline	cmdline;

		/*
		 * Acorn specific
		 */
		struct tag_acorn	acorn;

		/*
		 * DC21285 specific
		 */
		struct tag_memclk	memclk;
	} u;
};

可以看出 :

  • struct_tag结构体由struct tag_header+联合体union构成
  • 结构体struct tag_header用来描述每个tag的头部信息,如tag类型tag大小
  • 联合体union用来描述每个传递给Linux内核的 参数信息


  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值