概述
Linux系统启动之前还需要一段程序来进行引导工作,比如先初始化DDR内存等外设,然后将内核从外部的flash(nandflash、SD、EMMC等)中拷贝到DDR中,最后启动内核。这段程序就是BootLoader,它功能就是用于引导操作系统,类似于bios和windows的关系。UBOOT就是一款开源的BootLoader程序,可用于引导多种操作系统,并且支持多种体系结构(ARM、MIPCS、PPC、X86等),因此收到广泛的应用。
uboot 的全称是 Universal Boot Loader, uboot 是一个遵循 GPL 协议的开源软件, uboot 是一个裸机代码。
Uboot官方会定期发布各种版本。半导体厂商通常会较为关注这个事情,因为这个版本对于board的支持并不全面。不同的board需要在官方的版本上做一些修改,而这个修改工作通常由半导体厂商来做,比较自己对于自家的产品还是最清楚的,因此半导体厂商通常会选择一个官方的版本,然后再上边适配自家厂商的各种board。通常对于用户来说首选的也是去相应半导体厂商的官网上下载对应boarad的uboot来做一些定制化开发。
1、uboot 重点文件
1.1 arch文件夹
arch文件夹中存在于架构相关的代码,如下:
如上图,里边都是些架构相关的,如arm、mips、ppc等,本文主要是在armv7平台上演示,因此我们只关心arm即可,打开arm文件夹,如下:
cpu文件是arm的核心,进入cpu文件中有个u-boot.lds,就是uboot的链接脚本,是我们重点关注的东西。
dts是uboot的设备树文件,这里由各厂商自行维护,并不是所有的厂商都使用设备树来做驱动开发。
mach开头的文件是用于支持一些其他的machine的,本文中是用imx6ul,因此关注imx-common文件夹即可。
1.2 board文件夹
board 文件夹就是和具体的板子有关的,打开此文件夹,里面全是不同的板子,如下:
找到我们的主要关注的freescale(imx属于此公司),里边又分了很多freescale公司的各种信号的cpu及开发板型号。我们的移植主要是找到一块官方相近的板子,在此基础上来修改,此文件也是移植时需要修改的文件。
1.3 configs文件夹
此文件夹为 uboot 配置文件, uboot 是可配置的,通常是选择一个官网的文件,再次基础上进行修改。配置文件统一命名为“xxx_defconfig”, xxx 表示开发板名字,这些 defconfig 文件都存放在 configs
文件夹,如下:
mx6ull_alientek_emmc_defconfig就是根据官网的版本修改而来的。通常在编译uboot之前需要先进行配置,执行make mx6ull_14x14_ddr512_emmc_defconfig,即使用configs文件夹下的x6ull_alientek_emmc_defconfig配置文件进行配置,配置完成会生成**.config文件**。
1.4 Makefile文件
这个是顶层 Makefile 文件, Makefile 是支持嵌套的,也就是顶层 Makefile 可以调用子目录
中的 Makefile 文件。
1.5 u-boot.xxx 文件
u-boot.xxx 同样也是一系列文件,包括 u-boot、 u-boot.bin、 u-boot.cfg、 u-boot.imx、 u-boot.lds、
u-boot.map、 u-boot.srec、 u-boot.sym等。
- uboot:编译出来的 ELF 格式的 uboot 镜像文件
- uboot.bin:编译出来的二进制格式的 uboot 可执行镜像文件
- uboot.cfg:uboot 的另外一种配置文件
- u-boot.imx: u-boot.bin 添加头部信息以后的文件, NXP 的 CPU 专用文件
- u-boot.lds:链接脚本(如果没有编译过 uboot 的话链接脚本为 arch/arm/cpu/u-boot.lds。但是这个不是最终使用的链接脚本,最终的链接脚本是在这个链接脚本的基础上生成的。编译一下 uboot,编译完成以后就会在 uboot 根目录下生成 u-boot.lds文件)
- u-boot.map: uboot 映射文件,通过查看此文件可以知道某个函数被链接到了哪个地址上
- u-boot-nodtb.bin:和 u-boot.bin 一样, u-boot.bin 就是 u-boot-nodtb.bin 的复制文件
1.6 .config文件
uboot 配置文件, 使用命令“make xxx_defconfig”配置 uboot 以后就会自动生成
可以看出.config 文件中都是以“CONFIG_”开始的配置项,这些配置项就是 Makefile 中的变量,因此后面都跟有相应的值, uboot 的顶层 Makefile 或子 Makefile 会调用这些变量值。
在.config 中会有大量的变量值为‘y’,这些为‘y’的变量一般用于控制某项功能是否使能,为‘y’的话就表示功能使能。
2、uboot启动流程分析
2.1 链接脚本
要分析 uboot 的启动流程,首先要找到“入口”,找到第一行程序在哪里。程序的链接是由链接脚本来决定的,所以通过链接脚本可以找到程序的入口,打开链接脚本如下:
注:uboot是通过链接参数 -e 来指定链接起始地址的,makefile中CONFIG_SYS_TEXT_BASE这个宏来设置了链接起始地址,而这个宏是在include/config.h中定义的,如下图:
可以看出这个config.h是生成的,是在进行make xxx_defconfig时生成的,根据当前的配置来生成的,config.h头文件又包含了#include <configs/mx6ull_alientek_emmc.h> —>#include "mx6_common.h"这个宏的最终未知就在这个mx6_common.h中, 如下
如上图,最终这个链接起始地址就是0x87800000。
链接地址:也叫运行地址,程序定位的绝对地址,在编译连接时确定的地址,与位置相关的代码要放在对应的运行地址上。通常会把代码搬到链接地址上运行,以满足相对地址寻址的需求,保证程序的正确运行。
第三行定义了入口函数_start,此函数在arch/arm/lib/Vector.S中,如下:
可以看到start进来就是异常向量表。
2.2 启动流程-从reset开始
上节可以看到进入到start后,首先进入到复位函数reset,此函数在在 arch/arm/cpu/armv7/start.S,如下
进入到reset后,跳转到save_boot_params函数,而save_boot_params函数只有一句,就是跳转到save_boot_params_ret函数,为毛这么设计,恕在下愚钝,此函数也在上图中,此函数就做了2件事,首先是在非HYP模式的情况下,将模式设置到SVC模式,然后关闭FIQ和IRQ中断。
然后reset继续运行:
首先读取 CP15 中 c1 寄存器的值到 r0 寄存器中,就是SCTLR系统控制寄存器的值,CR_V定义在arch/arm/include/asm/system.h中,#define CR_V (1 << 13) /* Vectors relocated to 0xffff0000 */,将重定位向量表控制位设置为0,则可以重新定位中断向量表位置,然后更新SCTLR寄存器并将_start的地址设置为中断向量表的起始地址。
然后继续看代码:
首先跳转到cpu_init_cp15,看名字应该是初始化CP15,这个函数较长,这里就不展开了,就是主要做了关闭MMU和CACHE的操作。
然后跳转到cpu_init_crit,如下:
那么接下来就主要重点看看lowlevel_init和main。
2.2.1 lowlevel_init
函数 lowlevel_init 在文件 arch/arm/cpu/armv7/lowlevel_init.S 中定义,如下
首先初始化SP堆栈指针,然后8字节对齐。接着压栈GD_SIZE字节(generic-asm-offsets.h中定义sizeof(struct global_data),为248),然后再对齐,并将sp存到R9寄存器。后边要进行函数调用了,因此先保存lr寄存器,然后调用s_init函数,最后恢复lr。
global_data是个管理uboot的关键数据结构,定义如下
2.2.2 s_init
s_init 函数定义在文件arch/arm/cpu/armv7/mx6/soc.c 中(由于上一步已经初始化完了sp指针,内存初始化是在bootrom内已完成的,因此可以调用c函数了),如下
如图,cpu类型如果为imx6u则直接退出,因此此函数什么也没做直接返回了。
2.3 main - 第一部分
_main 函数定义在文件 arch/arm/lib/crt0.S 中,如下
设置SP指针值并8字节对齐,然后存储到r0寄存器中,然后调用board_init_f_alloc_reserve函数。V7M是cortex-M系列的,不用考虑。
2.3.1 board_init_f_alloc_reserve
此函数定义在文件 common/init/board_init.c 中,如下
top中是上边存储的sp指针的值,主要是留出早期的 malloc 内存区域和 gd 内存区域。
2.3.2 board_init_f_init_reserve
此函数也在common/init/board_init.c 中,如下
此函数用于初始化 gd,其实就是清零处理,同时此函数还设置了gd->malloc_base的值,也就是early malloc 的起始地址。
2.3.3 board_init_f
此函数定义在common/boardf_f.c中,如下
由于没有定义CONFIG_SYS_GENERIC_GLOBAL_DATA宏,因此此函数重点就是1059行,从名字可以看出来应该是个运行初始化序列,其实就是运行init_sequence_f序列内的一些列初始化函数
2.3.3.1 init_sequence_f 序列
由于此序列较长,里边包含个所有平台,因此阅读起来不太方便,取消掉不必关心的东西后,如下(正点原子整理)
- 第2行setup_mon_len:就一行代码gd->mon_len = (ulong)&__bss_end - (ulong)_start,设置了gd->mon_len的值,就是整个代码段的长度
- 第3行initf_malloc:主要代码也只有一行gd->malloc_limit = CONFIG_SYS_MALLOC_F_LEN,设置了gd->malloc_limit的值0x400,即内存池的大小
- 第4行initf_console_record:预编译宏未定义,返回0
- 第5行arch_cpu_init:init_aips(初始化Arm IP bus)->clear_mmdc_ch_mask(清除MMDC_CHx_MASK,以使热复位能工作)->init_bandgap->imx_set_wdog_powerdown(关闭看门狗)->init_src(System Reset Controller,热复位使能) 此函数主要是初始化一些cpu自身强相关的东西
- 第6行initf_dm:driver-module的一些初始化
- 第7行arch_cpu_init_dm:未实现
- 第8行mark_bootstage:记录board_init_f() bootstage
- 第9行board_early_init_f:板子相关的早期的一些初始化设置,IMX6ULL用来初始化串口的IO配置
- 第10行timer_init:初始化定时器, Cortex-A7 内核有一个定时器,这里初始化的就是 CortexA 内核的那个定时器。通过这个定时器来为 uboot 提供时间。就跟 Cortex-M 内核 Systick 定时器一样
- 第11行board_postclk_init:此处是Set VDDSOC to 1.175V
- 第12行get_clocks:获取一些时钟值,此处是初始化gd->arch.sdhc_clk,即SD卡外设时钟
- 第13行env_init:和环境变量有关,设置 gd->env_addr,也就是环境变量的保存地址
- 第14行init_baud_rate:初始化波特率,根据环境变量 baudrate 来初始化 gd->baudrate
- 第15行serial_init:初始化串口并设置gd->flags |= GD_FLG_SERIAL_READY
- 第16行console_init_f:设置 gd->have_console 为 1,表示有个控制台,此函数也将前面暂存在缓冲区中的数据通过控制台打印出来
- 第17行display_options:通过串口打印信息UBOOT第一行,如下
- 第18行display_text_info:若使能了debug模式,则会输出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 的值(本例程512M)
- 第27行post_init_f:此函数用来完成一些测试,初始化 gd->post_init_f_time
- 第28行INIT_FUNC_WATCHDOG_RESET:复位看门狗,对于 I.MX6ULL 来说是空函数
- 第29行testdram:测试 DRAM,空函数
- 第30、31行INIT_FUNC_WATCHDOG_RESET:复位看门狗,对于I.MX6ULL来说是空函数
- 第44行setup_dest_addr:设置目的地址,设置gd->ram_size= 0X20000000, gd->ram_top=0XA0000000, gd->relocaddr=0XA0000000这三个的值
- 第45行reserve_round_4k:对 gd->relocaddr 做 4KB 对 齐
- 第46行reserve_mmu:留出 MMU 的 TLB 表的位置,分配 MMU 的 TLB 表内存以后会
对 gd->relocaddr 做 64K 字节对齐。完成以后 gd->arch.tlb_size=0X4000、 gd->arch.tlb_addr=0X9FFF0000和 gd->relocaddr=0X9FFF0000 - 第47行reserve_trace:留出跟踪调试的内存, I.MX6ULL 没有用到
- 第48行reserve_uboot:留出重定位后的 uboot 所占用的内存区域, uboot 所占用大小由gd->mon_len=0XA8EF4 所指定,留出 uboot 的空间以后还要对 gd->relocaddr=0X9FF47000做 4K 字节对齐,并且重新设置 gd->start_addr_sp=0X9FF47000
- 第49行reserve_malloc:留出 malloc 区域,调整 gd->start_addr_sp 位置, malloc 区域由宏TOTAL_MALLOC_LEN 定义,调整后 gd->start_addr_sp=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,gd->new_gd=0X9EF44EB8
- 第54行reserve_fdt:留出设备树相关的内存区域, I.MX6ULL 的 uboot 没有用到,因此此函数无效
- 第54行reserve_arch:空函数
- 第55行reserve_stacks:留出栈空间,先对 gd->start_addr_sp 减去 16,然后做 16 字节对齐。gd->start_addr_sp=0X9EF44E90
- 第56行setup_dram_config:setup_dram_config 函数设置 dram 信息,就是设置 gd->bd->bi_dram[0].start=0X80000000 和gd->bd->bi_dram[0].size=0X20000000,后面会传递给 linux 内核,告诉 linux DRAM 的起始地址和大小
- 第57行show_dram_config:用于显示 DRAM 的配置,如下
- 第58行display_new_sp:显示新的 sp 位置,也就是 gd->start_addr_sp,不过要定义宏 DEBUG
- 第59行INIT_FUNC_WATCHDOG_RESET:复位看门狗,对于 I.MX6ULL 来说是空函数
- 第60行reloc_fdt:用于重定位 fdt,imx6ull在uboot中未使用设备树,因此为空
- 第61行setup_reloc:设置 gd 的其他一些成员变量,供后面重定位的时候使用,并且将以前的 gd 拷贝到 gd->new_gd 处。需要使能 DEBUG 才能看到相应的信息输出
- END
2.4 main - 第二部分
继续接着2.3中的mian函数往下看,如下图
- 103~109:重新初始化sp指针的值,sp=gd->start_addr_sp=0X9EF44E90。 0X9EF44E90 是 DDR 中的地址,说明新的 sp 和 gd 将会存放到 DDR 中,而不是内部的 RAM 了,然后8字节对齐。
- 111~114:获取 gd->bd 的地址赋给 r9,新的 gd 在 bd 下面,所以 r9 减去 gd 的大小就是新的 gd 的位置,获取到新的 gd的位置以后赋值给 r9,然后设置 lr 寄存器为 here,这样后面执行其他函数返回的时候就返回到了第 122 行的 here 位置处。
- 115~116:读取 gd->reloc_off 的值复制给 r0 寄存器。lr 寄存器的值加上 r0 寄存器的值,重新赋值给 lr 寄存器。因为接下来要重定位代码,也就是把代码拷贝到新的地方去(现在的 uboot 存放的起始地址为 0X87800000,下面要将 uboot 拷贝到 DDR 最后面的地址空间出,将 0X87800000 开始的内存空出来),其中就包括here,因此 lr 中的 here 要使用重定位后的位置
- 120~121:读取 gd->relocaddr 的值赋给 r0 寄存器,此时 r0 寄存器就保存着 uboot 要拷贝的目的地址,为 0X9FF47000,然后跳转到relocate_code进行重定位代码
2.4.1 relocate_code 重定位
此函数负责将 uboot 拷贝到新的地方去,此函数定义在文件 arch/arm/lib/relocate.S,如下
- 80:r1=__image_copy_start,也就是 r1 寄存器保存源地址,即0x87800000
- 81:r4=r0-r1=0X9FF47000-0x87800000=0X18747000,即偏移量
- 82:如果为0,则源地址和目的地址是一样的,那肯定就不需要拷贝了!执行 relocate_done 函数
- 83:r2=__image_copy_end,r2 中保存拷贝之前的结束地址,即0x8785dd54
- 85~89:执行数据cp,并比较是否cp完成,未完成则循环
- 94~95:重定位.rel.dyn 段, .rel.dyn 段是存放.text 段中需要重定位地址的集合,r2保存起始地址,r3保存中止地址
- 96~109:执行重定位段的重定位。重定位就是 uboot 将自身拷贝到 DRAM 的另一个地放去继续运行(DRAM 的高地址处)。我们知道,一个可执行的 bin 文件,其链接地址和运行地址要相等,也就是链接到哪个地址,在运行之前就要拷贝到哪个地址去。后边细讲,此处较为难理解。
- 111~130:完成重定位后,返回到lr,即上节提到的here,然后跳转到了relocate_vectors
这里重定位的是__image_copy_start~__image_copy_end,从链接脚本可以看到这个地址范围内不只是text段,还包含了data段和rodata段。
重定位细节:
从上述可知,重定位主要工作分两步:1、把image搬到相应的重定位地址上 2、重定位符号
首先内存数据搬运大家都懂,那为什么要重定位符号呢?我们一起来研究研究
uboot中函数跳转都是用的b和bl,而这两个跳转指令是相对地址跳转,与链接位置无关,是基于当前pc+offset来跳转的,因此代码重定位后,函数调用还是能够正确的找到位置去调用的。
而code中的数据访问确实采用的绝对地址,搬运之后,访问数据的地址也必须得进行修正,负责无法获取到正确的数据,在uboot中链接时指定了-pie参数,这个参数会生成位置无关代码,会生成一个.rel.dyn 段,rel.dyn 段是存放.text 段中需要重定位地址的集合,uboot 就是靠这个.rel.dyn 来解决data段的重定位问题。主要思想就是在代码里使用的全局变量用重定位rel.dyn段里边,而在rel.dyn段里存储一个标记,检测到标记的话,表示地址需要重定位,给原地址加上一个偏移就能得到新的位置。而这个标记的值就是上文代码中的23(0x17)。具体细节其他博客有更详细的介绍,此处就不展开描述了。
2.4.2 relocate_vectors 重定位向量表
函数 relocate_vectors 用于重定位向量表,此函数定义在文件也在relocate.S 中,如下
- 38:如果定义了 CONFIG_HAS_VBAR 的话就执行此语句,这个是向量表偏移, CortexA7 是支持向量表偏移的。而且,在.config里定义了 CONFIG_HAS_VBAR,因此会执行这个分支
- 43:r0=gd->relocaddr,也就是重定位后 uboot 的首地址,向量表肯定是从这个地址开始存放的
- 44:将 r0 的值写入到 CP15 的 VBAR 寄存器中,也就是将新的向量表首地址写入到寄存器 VBAR 中,设置向量表偏移
- 63:返回
2.5 main - 第三部分
继续接着2.4中的mian函数往下看,如下图
- 131:调用c_runtime_cpu_setup,此函数定义在文件 arch/arm/cpu/armv7/start.S 中,如下就是关闭I-CACHE
- 141~159:就是清空bss段,全部设置为0
- 167~168:设置r0=gd,设置r1=gd->relocaddr
- 174:调用board_init_r,not return!
2.5.1 board_init_r
在之前的的2.3.3中介绍了board_init_f 函数,在此函数里面会调用一系列的函数来初始化一些外设和gd的成员变量。但是 board_init_f 并没有初始化所有外设,还需要做一些后续工作这些后续工作就是由函数 board_init_r 来完成的,board_init_r 函数定义在common/board_r.c中,代码如下
跟board_init_f类型,主要是运行init_sequence_r序列,也定义在文件 common/board_r.c 中
2.5.1.1 init_sequence_r 序列
由于 init_sequence_f 的内容比较长,里面有大量的条件编译代码,这里为了缩小篇幅,将条件编译部分删除掉了,去掉条件编译以后的 init_sequence_r 定义如下:
- 第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:arm未使用
- 第7行initr_malloc:初始化 malloc
- 第8行initr_console_record:初始化控制台相关的内容, I.MX6ULL 未用到,空函数
- 第9行bootstage_relocate:启动状态重定位
- 第10行initr_bootstage:初始化 bootstage,一些mark信息
- 第11行board_init:板级初始化,包括 74XX 芯片, I2C、 FEC、 USB 和 QSPI 等
- 第12行stdio_init_tables:stdio 相关初始化
- 第13行initr_serial:初始化串口
- 第14行initr_announce:与调试有关,通知已经在 RAM 中运行
- 第15~17行:空函数
- 第18行power_init_board:初始化电源芯片
- 第19行initr_flash:函数无效
- 第20行:无效
- 第21行initr_nand:初始化nand,本例使用emmc,因此无效
- 第22行initr_mmc:初始化 EMMC,如果使用 EMMC 版本核心板的话就会初始化EMMC,串口输出以下信息:
- 第23行initr_env:初始化环境变量
- 第24行:无效
- 第25行initr_secondary_cpu:初始化其他CPU核, I.MX6ULL 只有一个核,因此此函数没用
- 第26行:无效
- 第27行stdio_add_devices:各种输入输出设备的初始化,如 LCD driver
- 第28行initr_jumptable:初始化跳转表,即gd->jt = malloc(sizeof(struct jt_funcs))
- 第29行console_init_r:控制台初始化,初始化完成以后此函数会调用stdio_print_current_devices 函数来打印出当前的控制台设备,如下
- 第30行:无效
- 第31行interrupt_init:初始化中断
- 第32行initr_enable_interrupts:使能中断
- 第33行initr_ethaddr:初始化网络地址,也就是获取 MAC 地址。读取环境变量“ethaddr”的值
- 第34行board_late_init:板子后续初始化,如果环境变量存储在 EMMC 或者 SD 卡中的话此函数会调用 board_late_mmc_env_init 函数初始化 EMMC/SD。会切换到正在时候用的 emmc 设备,打印如下信息:
- 第35~37行:无效
- 第38行initr_net:初始化网络设备 ,函数调用顺序为initr_net->eth_initialize->board_eth_init(), 串口输出如下信息
- 第39行:无效
- 第40行run_main_loop:
至此,uboot进入到主循环,具体内容下一章解析