文章目录
前言
uboot文章连载:
1.uboot命令集&环境变量
2.uboot配置,编译,移植
3.uboot启动过程
4.uboot命令体系
5.uboot的环境变量
6.uboot的驱动
7.uboot启动Linux内核
主要过程:
讲述uboot文件启动过程,加粗表示文件or跳转指令(箭头表示跳转)
Makefile
—>
u-boot.lds :ENTRY(_start)
—>
start.S、lowlevel_init.c :
(1)构建异常向量表
(2)设置CPU为SVC模式
(3)关看门狗
(4)开发板供电置锁
(5)时钟初始化
(6)DDR初始化
(7)串口初始化并打印"OK"
(8)重定位
(9)建立映射表并开启MMU
(10)跳转到第二阶段:ldr pc, _start_armboot
—>
uboot/lib_arm/board.c :
uboot有两种归宿:1.main_loop循环。2.进入倒数bootdelay秒然后执行bootcmd对应的启动命令,启动Linux。
start_armboot函数中:
— init_sequence函数指针数组中主要做了以下事情:
- cpu_init 空的
- board_init 网卡、机器码、内存传参地址
- dm9000_pre_init 网卡
- gd->bd->bi_arch_number 机器码(uboot中配置的这个机器码,会作为uboot给linux内核的传参的一部分传给linux内核,内核启动过程中会比对这个接收到的机器码,和自己本身的机器码相对比)
- gd->bd->bi_boot_params 内存传参地址
- interrupt_init 定时器
- env_init
- init_baudrate gd数据结构中波特率
- serial_init 空的
- console_init_f 空的
- display_banner 打印启动信息
- print_cpuinfo 打印CPU时钟设置信息
- checkboard 检验开发板名字
- dram_init gd数据结构中DDR信息
- display_dram_config 打印DDR配置信息表
- mem_malloc_init 初始化uboot自己维护的堆管理器的内存
- mmc_initialize inand/SD卡的SoC控制器和卡的初始化
- env_relocate 环境变量重定位
- gd->bd->bi_ip_addr gd数据结构赋值
- gd->bd->bi_enetaddr gd数据结构赋值
- devices_init 空的
- jumptable_init 不用关注的
- console_init_r 真正的控制台初始化
- enable_interrupts 空的
- loadaddr、bootfile 环境变量读出初始化全局变量
- board_late_init 空的
- eth_initialize 空的
- x210_preboot_init LCD初始化和显示logo
- check_menu_update_from_sd 检查自动更新
- main_loop 主循环
启动过程特征总结
(1)第一阶段为汇编阶段、第二阶段为C阶段
(2)第一阶段在SRAM中、第二阶段在DRAM中
(3)第一阶段注重SoC内部、第二阶段注重SoC外部Board内部
移植时的注意点
(1)x210_sd.h头文件中的宏定义
(2)特定硬件的初始化函数位置(譬如网卡)
细节
(1)start.S
主要过程:
(1)_start
.globl _start
_start: b reset
(2)reset
#1.设置为SVC模式,关IRQ & FIQ
#2.包含cpu_init_crit
(3)cpu_init_crit
这个其实是reset中的内容,源码中很清楚的解释了其要做的事情:
/*setup important registers
* setup memory timing*/
/** we do sys-critical inits only at reboot, not when booting from ram!*/
icache相关、mmu相关我们不细说,看一下识别并暂存启动介质选择:
(1)从哪里启动是由SoC的OM5:OM0这6个引脚的高低电平决定的。
(2)实际上在210内部有一个寄存器(地址是0xE0000004),这个寄存器中的值是硬件根据OM引脚的设置而自动设置值的。这个值反映的就是OM引脚的接法(电平高低),也就是真正的启动介质是谁。
(3)我们代码中可以通过读取这个寄存器的值然后判断其值来确定当前选中的启动介质是Nand还是SD还是其他的。
/* Read booting information */
ldr r0, =PRO_ID_BASE
ldr r1, [r0,#OMR_OFFSET]
bic r2, r1, #0xffffffc1
其中PRO_ID_BASE和OMR_OFFSET均在include/s5pc11.h(当然,不同的芯片有不同的定义)
之后根据上面的booting information来选择启动模式,读出来的值和启动模式的宏定义进行比较,相等则用r3记录其启动模式:
/* NAND BOOT */
cmp r2, #0x0 @ 512B 4-cycle
moveq r3, #BOOT_NAND
cmp r2, #0x2 @ 2KB 5-cycle
moveq r3, #BOOT_NAND
cmp r2, #0x4 @ 4KB 5-cycle 8-bit ECC
moveq r3, #BOOT_NAND
cmp r2, #0x6 @ 4KB 5-cycle 16-bit ECC
moveq r3, #BOOT_NAND
cmp r2, #0x8 @ OneNAND Mux
moveq r3, #BOOT_ONENAND
/* SD/MMC BOOT */
cmp r2, #0xc
moveq r3, #BOOT_MMCSD
/* NOR BOOT */
cmp r2, #0x14
moveq r3, #BOOT_NOR
其中启动模式和各自的cpu型号相关,我的在include/configs/x210_sd.h中,(在uboot配置,编译,移植讲过其被include/config.h包含)start.S开头包含了#include <config.h>。
#define BOOT_ONENAND 0x1
#define BOOT_NAND 0x2
#define BOOT_MMCSD 0x3
#define BOOT_NOR 0x4
#define BOOT_SEC_DEV 0x5
之后将r3的值写到用户自定义寄存器中备用:
ldr r0, =INF_REG_BASE
str r3, [r0, #INF_REG3_OFFSET]
赋值给INF_REG3_OFFSET寄存器,information register,用户自定义寄存器
(4)设置栈(SRAM中的栈)供lowlevel_init使用
ldr sp, =0xd0036000 /* end of sram dedicated to u-boot */
sub sp, sp, #12 /* set stack */
mov fp, #0
fp简单来讲就是指向栈底的指针。sp是栈顶指针,设置sp和fp。
关于fp写的很好的文章:https://blog.csdn.net/beyond702/article/details/52228683
(5)跳转lowlevel_init
bl lowlevel_init
(6)再次设置栈,作为c的运行环境
/* get ready to call C functions */
ldr sp, _TEXT_PHY_BASE /* setup temp stack pointer */
sub sp, sp, #12
mov fp, #0 /* no previous frame, so fp=0 */
从lowlevel_init返回之后,再次设置栈,_TEXT_PHY_BASE =CFG_PHY_UBOOT_BASE在x210_sd.h中定义,其值为0x33e00000,这个就是c的运行栈空间,是DDR的空间,也可以看出lowlevel_init初始化了DDR。
(7)检查现在在SRAM中还是dram中,如果在dram中,那么无需初始化nand等存储器了
(注意lowlevel_init已经初始化好了dram,所以之后只需要初始化nand等存储器,dram内存不用初始化了。)
/* when we already run in ram, we don't need to relocate U-Boot.
* and actually, memory controller must be configured before U-Boot
* is running in ram.
*/
ldr r0, =0xff000fff
bic r1, pc, r0 /* r0 <- current base addr of code */
ldr r2, _TEXT_BASE /* r1 <- original base addr in ram */
bic r2, r2, r0 /* r0 <- current base addr of code */
cmp r1, r2 /* compare r0, r1 */
beq after_copy /* r0 == r1 then skip flash copy */
检查当前的运行地址位置和_TEXT_BASE 比较就可以得出是否在DRAM中,如果在DRAM那么就直接跳转到after_copy
(8)紧接着上面,去初始化nand等存储器
如果不在dram中,那么现在代码还在sram中,那么表示还没有将代码从nand搬到sram,那么表示nand还没有被初始化:
还记得之前的INF_REG3_OFFSET寄存器吗,存着booting information,这里我们就把他取出来,用于不同存储器的初始化:
#if defined(CONFIG_EVT1)
/* If BL1 was copied from SD/MMC CH2 */
ldr r0, =0xD0037488
ldr r1, [r0]
ldr r2, =0xEB200000
cmp r1, r2
beq mmcsd_boot
#endif
ldr r0, =INF_REG_BASE
ldr r1, [r0, #INF_REG3_OFFSET]
cmp r1, #BOOT_NAND /* 0x0 => boot device is nand */
beq nand_boot
cmp r1, #BOOT_ONENAND /* 0x1 => boot device is onenand */
beq onenand_boot
cmp r1, #BOOT_MMCSD
beq mmcsd_boot
cmp r1, #BOOT_NOR
beq nor_boot
cmp r1, #BOOT_SEC_DEV
beq mmcsd_boot
(9)after_copy:
enable_mmu:
...
mmu_on:
...
stack_setup:
...
clear_bss:
...
ldr pc, _start_armboot
最重要就是最后一句,长跳转到dram中运行
(之前我在想这句话不是紧接着下面运行吗,为什么要添加这句话,这句话不能没有,ldr长跳转到dram中,如果没有,那么仍然在当前地址下运行)
以上过程总结:
(2)lowlevel_init.c
判断当前处于什么状态,处于深睡眠,浅睡眠等,如果处于这些状态那么lowlevel_init会负责初始化相关状态下未被boot的硬件,最终由exit_wakeup来直接跳转到kernel执行。
如果处于冷启动状态,那么在lowlevel_init初始化需要初始化的硬件之后,然后返回到start.S继续uboot启动。
其中代码如下:
检查复位状态:
/* check reset status */
ldr r0, =(ELFIN_CLOCK_POWER_BASE+RST_STAT_OFFSET)
ldr r1, [r0]
bic r1, r1, #0xfff6ffff
cmp r1, #0x10000
beq wakeup_reset_pre
cmp r1, #0x80000
beq wakeup_reset_from_didle
exit_wakeup:
exit_wakeup:
/*Load return address and jump to kernel*/
ldr r0, =(INF_REG_BASE+INF_REG0_OFFSET)
ldr r1, [r0] /* r1 = physical address of s5pc110_cpu_resume function*/
mov pc, r1 /*Jump to kernel */
nop
nop
(1)复杂CPU允许多种复位情况。譬如直接冷上电、热启动、睡眠(低功耗)状态下的唤醒等,这些情况都属于复位。所以我们在复位代码中要去检测复位状态,来判断到底是哪种情况。
(2)判断哪种复位的意义在于:冷上电时DDR是需要初始化才能用的;而热启动或者低功耗状态下的复位则不需要再次初始化DDR。
s5pv210有五种复位状态,即:
•硬件重置-当XnRESET被驱动到低位时生成硬件重置。它是一个不妥协的,无涂层的,完全复位,用于驱动S5PV210到一个已知的初始状态。
•看门狗重置-通过看门狗定时器重置信号 •软件重置-通过设置特殊控制寄存器重置信号
•热复位-通过XnWRESET引脚复位信号。
•唤醒复位-当具有正常F/F的模块断电,且模块再次通过唤醒事件通电时产生的复位信号;但在睡眠模式下,无论正常F/F或保持F/F如何,唤醒复位都会生成至所有断电的模块。
五次重置的优先级如下:硬件重置>看门狗重置>热重置>软件重置>唤醒重置
1.将其他位清零,只读16和19位,当16位为1时,处于Reset by SLEEP mode wake-up状态,我们执行wakeup_reset_pre,将其唤醒到下一状态,然后看19位是否为1,为1时处于ARM reset
from DEEP-IDLE状态,执行wakeup_reset_from_didle,将其唤醒到下一状态。
2. wakeup_reset_pre层:判断c1的32位,如果不是冷启动就不需要执行wakeup_reset了,wakeup_reset中的其他函数也将不被执行:不需要初始化时钟,ddr,tzpc,nand。mem_ctrl_asm_init是在cpu_init.S(cpu/s5pc11x/s5pc110)。但是需要执行disable_l2cache和v7_flush_dcache_all。
3. wakeup_reset_from_didle层:执行exit_wakeup,并最终跳到kernel中。跳转到E010F000地址。
粗略的过程如下图:
补充start.S的图为:start_armboot最终还是要去启动内核,所以uboot最终归宿就是启动内核。
细节部分
(1)lowlevel_init有一块代码和start.S几乎一模一样:
/* when we already run in ram, we don't need to relocate U-Boot.
* and actually, memory controller must be configured before U-Boot
* is running in ram.
*/
ldr r0, =0xff000fff
bic r1, pc, r0 /* r0 <- current base addr of code */
ldr r2, _TEXT_BASE /* r1 <- original base addr in ram */
bic r2, r2, r0 /* r0 <- current base addr of code */
cmp r1, r2 /* compare r0, r1 */
beq 1f /* r0 == r1 then skip sdram init */
//跳到1位置继续执行,跳过了初始化DDR和时钟,f表示向下找
其中1的上面就是去初始化DDR:
/* Memory initialize */
bl mem_ctrl_asm_init
这个代码在cpu/s5pc11x/s5pc110/cpu_init.S,只有一个功能就是初始化DDR,我的裸机初始化DDR的代码就是在这里来的,名字叫sdram_init.S,在ARM结构体系中有解释过。
(1)bic r1, pc, r0 这句代码的意义是:将pc的值中的某些bit位清0,剩下一些特殊的bit位赋值给r1(r0中为1的那些位清零)相当于:r1 = pc & ~(ff000fff)
ldr r2, _TEXT_BASE 加载链接地址到r2,然后将r2的相应位清0剩下特定位。
(2)最后比较r1和r2.
总结:这一段代码是通过读取当前运行地址和链接地址,然后处理两个地址后对比是否相等,来判定当前运行是在SRAM中(不相等)还是DDR中(相等)。从而决定是否跳过下面的时钟和DDR初始化。
我们裸机中判断链接地址和运行地址是否相同是用ldr和adr两个命令来判断的,而这里是将运行地址和链接地址进行比较:因为uboot的大小不会超过4k(程序将前三位清0,只比较后面的大的数,1000就是4k),这样就可以将运行地址通过处理和链接地址进行比较。
如图如果在DDR或者IRAM中运行地址可能如图标出,如果将低位的地址减去就可以将运行地址和链接地址进行比较:
(2)打印K
汇编中打印K,往uart缓存寄存器中直接写值,很简单,但是很实用,可以用来debug(说是这么说,其实打印的东西太多,屏幕上被淹没不太好找),uboot还是有很多精华的东西,而且在很多地方和Linux内核一脉相承比如驱动,所以多学源码并拿出来用很有用处。
/* Print 'K' */
ldr r0, =ELFIN_UART_CONSOLE_BASE
ldr r1, =0x4b4b4b4b
str r1, [r0, #UTXH_OFFSET]
(3)lib_arm/board.c
其最主要的函数为start_armboot,也就是start.S最终跳转的地方
头文件:
其头文件:
//主要的两个头文件
41:#include <common.h>
68:#include <s5pc110.h>
42:#include <command.h>
43:#include <malloc.h>
44:#include <devices.h>
45:#include <version.h>
46:#include <net.h>
47:#include <asm/io.h>
48:#include <movi.h>
49:#include <regs.h>
50:#include <serial.h>
51:#include <nand.h>
52:#include <onenand_uboot.h>
55:#include <mmc.h>
101:#include <i2c.h>
而common.h的头文件:
//主要的头文件:
35:#include <x210_sd.h>
130:#include <asm/u-boot.h> /* boot information for Linux kernel */
131:#include <asm/global_data.h> /* global data used for startup functions */
36:#include <linux/bitops.h>
37:#include <linux/types.h>
38:#include <linux/string.h>
39:#include <asm/ptrace.h>
40:#include <stdarg.h>
110:#include <part.h>
111:#include <flash.h>
112:#include <image.h>
s5pc110.h的头文件:
38:#include <asm/hardware.h>
48:#include <s5pc11x.h>
牵扯到板子的寄存器、库定义等,我们只看一下俩文件:
130:#include <asm/u-boot.h> /* boot information for Linux kernel */
131:#include <asm/global_data.h> /* global data used for startup functions */
定义了bd_t结构体、gd_t结构体。
gd_t:
bd:bd_t类型的指针,指向一个bd_t类型的变量,这个bd是开发板的板级信息(board information)的结构体,里面有不少硬件相关的参数,譬如波特率、IP地址、机器码、DDR内存分布。
flags:标志位
baudrate:
have_console:布尔类型的变量,表示console有没有,console是控制台(控制台是基于串口的,在控制台还没有建立起来之前,串口还只能非常简单的使用,建立之后就可以使用标准输入输出等等。)
reloc_off:重定位偏移量
env_addr:环境变量地址
env_valid:在内存中的环境变量在当前可不可以使用,布尔类型
fb_base:freambuffer的基地址,缓存的起始地址
start_armboot函数:
(1)执行init_sequence中的初始化函数,初始化一些硬件(其中包含了很多硬件初始化代码,有关硬件初始化可以参考这里,很使用)
(2)为gd和bd分配空间。填充gd和bd
(3)调用bootm启动内核,否则进入main_loop,uboot的命令行界面。
start_armboot可以说是uboot启动以来第一个c函数,其依赖于很多源代码,和汇编不一样的是,其架构变得庞大,组织变得复杂。(汇编大部分功能在自己的文件下完成任务,跳转,而不是调用,使其架构大多简单)学习他对于我这个初学者来说,对理解其组织十分有帮助。
start_armboot的init_sequence调用的函数不得不讲一讲,因为有些涉及到Linux的启动(当然这里和我们的开发板有关,不同的开发板init_sequence中做的事情不一样):
init_fnc_t *init_sequence[] = {
cpu_init, /* basic cpu dependent setup */
#if defined(CONFIG_SKIP_RELOCATE_UBOOT)
reloc_init, /* Set the relocation done flag, must
do this AFTER cpu_init(), but as soon
as possible */
#endif
board_init, /* basic board dependent setup */
interrupt_init, /* set up exceptions */
env_init, /* initialize environment */
init_baudrate, /* initialze baudrate settings */
serial_init, /* serial communications setup */
console_init_f, /* stage 1 init of console */
display_banner, /* say that we are here */
#if defined(CONFIG_DISPLAY_CPUINFO)
print_cpuinfo, /* display cpu info (and speed) */
#endif
#if defined(CONFIG_DISPLAY_BOARDINFO)
checkboard, /* display board info */
#endif
#if defined(CONFIG_HARD_I2C) || defined(CONFIG_SOFT_I2C)
init_func_i2c,
#endif
dram_init, /* configure available RAM banks */
display_dram_config,
NULL,
};
board_init:主要填充了gd的两个重要的成员,机器码和内核传参地址
int board_init(void)
{
DECLARE_GLOBAL_DATA_PTR;
gd->bd->bi_arch_number = MACH_TYPE; //填充机器码MACH_TYPE,机器码在Linux启动时会传给Linux
gd->bd->bi_boot_params = (PHYS_SDRAM_1+0x100); //uboot给linux kernel启动时的传参的内存地址
return 0;
}
env_init:
为什么有很多env_init函数,主要原因是uboot支持各种不同的启动介质(譬如norflash、nandflash、inand、sd卡·····),我们一般从哪里启动就会把环境变量env放到哪里。而各种介质存取操作env的方法都是不一样的。因此uboot支持了各种不同介质中env的操作方法。所以有好多个env_xx开头的c文件。实际使用的是哪一个要根据自己开发板使用的存储介质来定(这些env_xx.c同时只有1个会起作用,其他是不能进去的,通过x210_sd.h中配置的宏来决定谁被包含的(?我没找到)),对于x210来说,我们应该看env_movi.c中的函数。
extern uchar environment[];
env_t *env_ptr = (env_t *)(&environment[0]);
environment[]存储了每个environment的地址,那么可以初始化gd->env_addr了,但是源码只是在environment还存在存储器中时初始化gd->env_addr:(我觉得缺了点东西:gd->env_addr = (ulong)env_ptr )
gd->env_addr = (ulong)&default_environment[0];
gd->env_valid = 1;
在start_armboot函数中(776行)调用env_relocate才进行环境变量从SD卡中到DDR中的重定位。重定位之后需要环境变量时才可以从DDR中去取,重定位之前如果要使用环境变量只能从SD卡中去读取。
mem_malloc_init:
分配堆内存空间:头地址和空间大小。
(4)common/main.c
最主要的函数:main_loop,当start_armboot没有启动内核时,会进入此状态,死循环,不会返回,其功能就是uart上位机上作为人机交互界面shell。
补充整个启动流程图如下:
一些细节:
为什么要分配内存
(1)DECLARE_GLOBAL_DATA_PTR只是定义了一个指针,也就是说gd里的这些全局变量并没有被分配内存,我们在使用gd之前要给他分配内存,否则gd也只是一个野指针而已。
(2)gd和bd需要内存,内存当前没有被人管理(因为没有操作系统统一管理内存),大片的DDR内存散放着可以随意使用(只要使用内存地址直接去访问内存即可)。但是因为uboot中后续很多操作还需要大片的连着内存块,因此这里使用内存要本着够用就好,紧凑排布的原则。所以我们在uboot中需要有一个整体规划。
内存排布
(1)uboot区 CFG_UBOOT_BASE(0xc3e00000)~xx(长度为uboot的实际长度)
(2)堆区 长度为CFG_MALLOC_LEN,实际为912KB
(3)栈区 长度为CFG_STACK_SIZE,实际为512KB
(4)gd 长度为sizeof(gd_t),实际36字节
(5)bd 长度为sizeof(bd_t),实际为44字节左右
(6)内存间隔 为了防止高版本的gcc的优化造成错误。
这样算下来gd大概距离c3e00000有600k的距离,我们现在用的uboot<400k,如果将来uboot大小大于>600k,则只需要改一下CFG_UBOOT_SIZE的大小。
看源码时有碰到几个问题
1.涉及的一些汇编伪指令
.word
.balignl
.global _end_vect
_end_vect:
.balignl 16,0xdeadbeef
2.TEXT_BASE从哪来
start.S中有以下语句,但是TEXT_BASE没有声明过
_TEXT_BASE:
.word TEXT_BASE
那源码怎么能编译成功呢?在文章https://blog.csdn.net/weixin_44705391/article/details/123036847?spm=1001.2014.3001.5501中解决。
(1)第100行这个TEXT_BASE就是上个课程中分析Makefile时讲到的那个配置阶段的TEXT_BASE(2589行),其实就是我们链接时指定的uboot的链接地址。(值就是c3e00000)
(2)makefile将变量TEXT_BASEexport为环境变量了
(3)makefile中有两种export,shell的export和makefile的export。
(4)makefile中export的可以供子makefile使用,同级makefile不能通过export传递变量,makefile通过echo $( MAKELEVEL)可得到makefile的级别。
(5)makefile中的export是导出变量到子makfile,而目标对应执行的动作中的export,是属于shell中的export,其作用是导出变量到当前shell。此两个export的作用是不同的。
3.start.S中还初始化了MMU
MMU开启之后就不能用物理地址了
https://www.cnblogs.com/FarmPick/p/4941834.html
4.跳转的几种方式
pop {pc} /*此程序作为子程序,跳转回前一个程序中*/
mov pc, lr /*此程序作为子程序,跳转回前一个程序中*/
mov pc, r1 /*跳转到r1这个地址中*/
ldr pc, _start_armboot /*长跳转*/
b _start_armboot /*短跳转*/
5.__attribute__
关于mmc_initialize调用了cpu_mmc_init和board_mmc_init,这两个函数使用了__attribute__,定义函数的属性为weak和alias,weak表示函数调用时找不到其定义位置就使用其定义的位置,alias是函数的别名,事实是,board_mmc_init确实没有函数原型,那我们直接使用函数别名的函数__def_mmc_init:
static int __def_mmc_init(bd_t *bis)
{
return -1;
}
int cpu_mmc_init(bd_t *bis) __attribute__((weak, alias("__def_mmc_init")));
int board_mmc_init(bd_t *bis) __attribute__((weak, alias("__def_mmc_init")));