概述:
本文将从两个方面来阐述uboot:
1、启动流程
2、架构
一、uboot流程图:
从上图中看到红色1,2,3,4,5,7,8,9的标号,下面分别说明每个过程:
1、启动入口:
-
(1)确定链接脚本文件:
在根目录下Makefile下LDSCRIPT宏值,就是指定链接脚本(如:arch/arm/cpu/u-boot.lsds)路径的
-
(2)从脚本文件找入口:
在链接脚本中可以看到ENTRY()指定的入口,如:ENTRY(_start),_start就是入口 -
(3)链接脚本简要分析:
...
...
...
ENTRY(_start)
SECTIONS
{
.=0x00000000;
.=ALIGN(4);
.text:
{
*(.__image_copy_start)
*(.vectors)
CPUDIR/start.o (.text*)
*(.text*)
}
}
...
...
...
像*(.__image_copy_start)这种,指定将标记为这个标记的代码段编译链接放入这个地方,上面的事例表示0x00000000地址(在编译完,生成的System.map文件中可以看到),在汇编中可以用如下方式将一段代码做标记(直到遇到下一个section为止,要是没有就到文件结束):
.section ".__image_copy_start", "ax"
在C文件中可以用如下方式,把一个变量或者函数标记到指定段:
char __image_copy_start[0] __attribute__((section(".__image_copy_start")));
在本人手中的例子,被编译的文件中没有__image_copy_start这样标记的代码段,只能找.vectors,在arch/arm/lib/vectors.S开头地方,找到被.vectors标记的代码段,并且_start也是位于有效代码第一行,最终启动入口位置确定了(假设_start没有位于vectors.S有效代码第一行,更或者,在start.o中,而ENTRY()又指定ENTRY(_start),会是什么结果?)。
2、初始化:
(1)启动过程中memory变化:
如上图:
a、上电,首先在CPU片上的bootrom空间里运行(CPU出厂就固化的code),检查外部flash(nand , emmc , nor , sdmmc等,看CPU支持情况):
——检测到,就会读取flash中前2K code到片上Sram,然后由这2K code负责将flash中全部code加载到外部RAM(EXT RAM),最终跳转到外部ram运行,然后继续初始化环境,加载系统等。
——检测不到外部flash,或者在外部flash检测不到有效bank,则会初始化USB(视具体板子),启动下载,等待img数据。
(验证:本人,验证结果是,在uboot前2k,里面取出的PC值,始终是外部RAM所在的地址空间,但是各种资料都描述有片上2K RAM)
(2)初始化过程,分board_init_f前,board_init_f,relocate, board_init_r这四个过程描述:
- a、board_init_f前,做的都是关于CPU体系结构相关的初始化,对于做电子产品开发的,基本都不会去修改,就简单描述,首先,8个异常入口:
b reset
b pc, _undefined_instuction
b pc, _software_interrupt
b pc, _prefetch_abort
b pc, _data_abort
b pc, _not_used
b pc, _irq
b pc, _fiq
从上面字面意思大概也知道,重点关注下reset(定位reset时,结合链接脚本文件很容易找到),reset首先做的是将FIQ,IRQ关闭,将CPU(arm)模式设置成SVC32(保护模式),然后配置CP15协处理器,CP15作用:
-
第一,配置异常入口,以前都认为CPU发生reset异常后,都是PC为0x0地址,
然而,通过配置CP15后,可以修改:
ldr r0, =_start
mcr p15,0,r0,c12,c0,0
如上,之后,再出现reset异常后,PC就是跳到_start处,不再是0x0地址
- 第二,cpu_init_cp15在这个函数里面,配置(CP15配置)cache,MMU,TLBS,I-cache,L2 cache。
CP15完成配置,cpu_init_crit会调用lowlevel_init,这个一般定义板级初始化,需要很早初始化的,主要初始化pll,mux,memory(ddr)。
cpu_init_crit返回后,进入_main,这个_main入口有点难找,搜索会出来一大堆,我的是arm体系的,在arch/arm/lib/crt0.S里面找到的,从_main开始到board_init_f都是对ram空间的划分,有个重点宏CONFIG_SYS_INIT_SP_ADDR需要根据具体硬件RAM空间划分配置,例如,我的板子2G RAM空间映射到0x0地址开始2G地址空间,厂商将128M空间到0x0地址作为系统用,因此CONFIG_SYS_INIT_SP_ADD=0x0+SZ128M。
在_main第一条指令:
ldr sp, =(CONFIG_SYS_INIT_SP_ADD)
然后,对sp按照8字节对齐处理后,将SP-sizeof(gd_t):
sub sp, sp, #GD_SIZE
GD_SIZE定义在include/generated/generic-asm-offsets.h里面,从这里面看:
#define GD_SIZE 232
gd_t结构体size与结构体系有关,这里写死,估计是厂商根据自己板子cpu体系算过。
这里sp是按照向下生长的,这条指令后sp,再通过:
mov r9, sp
这样r9就指向gd全局变量起始地址了(空间就是从gd到CONFIG_SYS_INI_SP_ADD),至于为何这么说,且看下面:
gd_t(通用)定义在include/asm-generic/global_data.h,然后在gd_t结构体重嵌入了与体系有关的struct arch_global_data,我的是arm的,所以在arch/arm/include/asm/global_data.h中定义了arch_global_data,在这个头文件中还发现了:
#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm("r9")
结合上面r9的值,可以确定gd=CONFIG_SYS_INIT_SP_ADD-sizeof(gd_t)。
然后_main完成r9赋值后,要对gd这段空间清零处理,清零完成后就进入board_init_f函数。
-
b、board_init_f中,主要是做了gd成员值的初始化,这些成员值,最后会为copy code提供地址和size。
最终划分的空间大致情况(根据firefly-rk3288,2G的ram空间,图子格子大小不代表ram空间大小),整理如下图:
更详细的分析图(relocaddr表示的是gd->relocaddr,基于firefly-rk3288开发板分析的):
图看不清楚的可以
百度盘
下载看。
各分区说明:
tlb
: 一种cach,百度吧,我没很明白
globa buff
:firefly-rk3288用作flash管理buff(nand/emmc坏块等)
boot /fastboot buff
:
fastboot log buff
:
uboot code data&bss
:relocate后uboot code会copy到此处运行
malloc
: malloc()分配的内存空间
bd_t
: DDR信息,即几个bank,每个bank多大(bank信息的获取,就要看具体的板子 了,例如,我的firefly-rk3288,做得够隐秘了,代码(函数dram_init_banksize())分析看,将信息放在了0x0+32M的地方,然后用这个函数解析存入gd->bd->rk_dram里面,bd_t是一个与CPU体系结构相关的,也是可以根据在基础上增加成员)
gd_t
:new gd所在空间,原来在r9指定的gd空间,会copy到此处后,在board_init_f调用完成后由汇编把r9修改指向这块空间
在board_init_f中也有对serial,console初始化:
serial_init()在这里的初始化,做两件事,
首先,初始化gd->flags|=GD_FLG_SERIAL_READY表示serial以就绪
然后,调用具体serial初始化函数,将所用的serial端口和波特率等设置到就绪状态。
console_init_f()之中了一件事情,就是将gd->have_console=1,表示有控制台。
board_init_f最后调用setup_reloc()两个很重要的动作:
gd->reloc_off = gd->relocaddr - CONFIG_SYS_TEXT_BASE;
memcpy(gd->newd_gd,(char *)gd,sizeof(gd_t))
重要的宏CONFIG_SYS_TEXT_BASE,可以说虚拟地址空间,编译的时候,uboot会根据这值来链接。实际真实的硬件RAM起始地址和CONFIG_SYS_TEXT_BASE差,就是虚拟地址和物理地址偏移量。在这里,relocate后uboot在实际的物理地址gd->relocaddr(起始地址),CONFIG_SYS_TEXT_BASE是虚拟地址的起始,差值自然就是relocate后虚拟地址和物理地址的偏移量。
然后将重要的gd数据从(CONFIG_SYS_INIT_SP_ADD-SZ128M,CONFIG_SYS_INIT_SP_ADD)区间,copy到新的new gd区间
c、relocate:
这部分就是将,当前uboot运行空间的code,copy到上面“uboot code data&bss”空间里,即gd->relocaddr地址处,接着上面的运行。具体可以看这个博客地址:
uboot的relocation原理详细分析
d、board_init_r做的事情主要概括如下:
- 标记uboot启动已经到了board_init_r阶段
- 设置machine id & 启动参数存放位置
- 最后硬件的初始化
- Flash接口初始化
- 环境变量最后的初始化
- console最后初始化
- 检查key是否进入下载模式
-
进入main loop或者直接启动系统
检查key是否进入下载模式、进入main loop或者直接启动系统放到流程图中3、4、5、6、7、8、9降
标记uboot启动已经到了board_init_r阶段
:
看下面代码很好理解:
static int initr_reloc(void)
{
gd->flags |= GD_FLG_RELOC;
bootstage_mark_name(BOOTSTAGE_ID_START_UBOOT_R,"board_init_r");
}
设置machine id & 启动参数存放位置
如下就是对gd两个重要成员的初始化:
int board_init(void)
{
gd->bd->bi_arch_number = MACH_TYPE_XX;
gd->bd->bi_boot_params = PHYS_SDRAM;
}
gd->bd->bi_arch_number作为machine id通过R1传递到linux kernel,也就是linux kernel会将这个值与宏MACHINE_START定义的nr比较,要相同,才能通过linux kernel的检查,否则会导致系统不能启动,这是旧版linux方式,新版的用宏DT_MACHINE_START,从宏定义也可以看出nr已无作用,这两个宏都定义在linux根目录下arch/arm/include/asm/mach/arch.h:
#define MACHINE_START(_type,_name) \
static const struct machine_desc __mach_desc_##_type \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_##_type, \
.name = _name,
#define MACHINE_END \
};
#define DT_MACHINE_START(_name, _namestr) \
static const struct machine_desc __mach_desc_##_name \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = ~0, \
.name = _namestr,
现在来看下linux是怎么来兼容新旧两种方式的,在linux根目录下init/main.c中start_kernel()调用setup_arch():
void __init setup_arch(char **cmdline_p)
{
const struct machine_desc *mdesc;
setup_processor();
mdesc = setup_machine_fdt(__atags_pointer);
if (!mdesc)
mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);
machine_desc = mdesc;
.......
.......
.......
}
setup_machine_fdt()就是新版,参数__atags_pointer就是r2传过来的地址(下面会讲到uboot对r2的处理),匹配的方式就是r2地址开始的内存中,前4byte必须是0xd00dfeed:
#define FDT_MAGIC 0xd00dfeed
头部匹配通过后,开始匹配compatible属性,r2是dtb存放的内存起始地址,在dtb中有这么样值:
compatible = "rockchip,rk3288"
在r2中搜索这个属性,然后将属性值与linux中DT_MACHINE_START定义的dt_compat值对比,相同才最终匹配成功。DT_MACHINE_START定义变量都存储在.arch.info.init段中,搜索这段中的dt_compat就可以了。匹配成功后,会搜索dtb中如下节点:
chosen{
bootargs = "console=ttyS2 init=/init initrd=0x62000000,0x00800000";
};
搜索到后,将bootargs的值copy到boot_command_line中,到此linux新版获取启动参数方式成功。
当linux新版获取启动参数方式失败,调用setup_machine_tags(__atags_pointer, __machine_arch_type)用旧版获取启动参数的方式,形参__atags_pointer当然就是r2的值,__machine_arch_type就是machine id,在哪里获取?
在linux根目录arch/arm/boot/compressed/misc.c中:
decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p,
unsigned long free_mem_ptr_end_p,
int arch_id)
{
...
...
...
__machine_arch_type = arch_id;
...
...
...
}
参数arch_id又是哪里传入的:
在linux根目录下arch/arm/boot/compressed/head.S中:
/*
* The C runtime environment should now be setup sufficiently.
* Set up some pointers, and start decompressing.
* r4 = kernel execution address
* r7 = architecture ID
* r8 = atags pointer
*/
mov r0, r4
mov r1, sp @ malloc space above stack
add r2, sp, #0x10000 @ 64k max
mov r3, r7
bl decompress_kernel
bl cache_clean_flush
bl cache_off
mov r1, r7 @ restore architecture number
mov r2, r8 @ restore atags pointer
根据汇编和C互相调用时,传参数的规定,arch_id就等于r3的值,r3==r7,根据注释r7是architecture ID,至于r7什么时候得到,我就不继续追下去了,有兴趣的可以跟踪下代码。接下来就是用__machine_arch_type与MACHINE_START定义的nr比较,相等就匹配成功,获取linux中struct machine_desc结构体变量(描述cpu相关的):
/*
* locate machine in the list of supported machines.
*/
for_each_machine_desc(p)
if (machine_nr == p->nr) {
pr_info("Machine: %s\n", p->name);
mdesc = p;
break;
}
到此新旧两种获取启动参数的方式结束。我另外一篇博客:
linux之early_param()和__setup
对跟踪启动参数作用或许在理解上有一定帮助
回到uboot:
在arch/arm/lib/bootm.c中函数boot_jump_linux()如下代码,将bi_arch_number传到r1,然后传到linux:
static void boot_jump_linux(bootm_headers_t *images, int flag)
{
unsigned long machid = gd->bd->bi_arch_number;
unsigned long r2;
void (*kernel_entry)(int zero, int arch, uint params);
kernel_entry = (void (*)(int,int,uint))images->ep;
if(IMAGE_ENABLE_OF_LIBFDT && images->ft_len)
r2 = (unsigned long)images->ft_addr;
else
r2 = gd->bd->bi_boot_params;
kernel_entry(0,machid,r2);
}
images->ep就是linux在ram里面起始地址,地址直接赋值给一个函数指针,然后就当一个函数调用。uboot和linux传参,规定函数的第一个参数为0(r0 = 0),第2个参数machine id(r1= machine id),第三个参数为命令行起始地址(r2 = params addr)。
(c语言中具体地址当作函数调用,参数一般都是第一个对应r0,第2个对应r1…依次类推,这也是c调用汇编的规定)
关于MACH_TYPE_XX生成,由linux kernel一个awk脚本arch/arm/tools/gen-mach-types在编译的时候,会根据在同目录下的mach-types(修改这个文件,就可以在mach-types.h下生成形如MACH_TYPE_XX宏)文件生成linux根目录下include/generated/mach-types.h。与uboot根目录下arch/arm/include/asm/mach-types.h很相似,但在uboot下面没找到生成的脚本(个人感觉从linux下面copy过去,哈哈)。
gd->bd->bi_arch_number,gd->bd->bi_boot_params这个启动参数的方式是linux老式传参方式了,但新版本linux都是兼容的,bi_boot_params地址一般都设置在靠近ram起始地址(fireflye-rk3288设置在0x0+0x88000)。
uboot对新版命令行传参方式的支持:
在arch/arm/lib/bootm.c中boot_prep_linux()中有调用到image_setup_linux(bootm_headers_t *images) ,函数参数struct boot_headers_t结构体中成员ft_addr,ft_len就是新版参数的关键。ft_addr是dtb装载到ram中的地址,dtb可以从两个地方resource.img和boot.img装载,
boot.img是android中用到的,在android中有mkbootimg工具,将zImage(Linux),ramdisk,second stage(可选,可以将dtb放到这段)。这样,uboot就可以从boot.img中解析出sencond stage加载到内存,并将地址赋给fd_addr。关于boot.img和工具mkbootimg理解,可以参考这篇博客:
通过分析mkbootimg源代码了解boot.img文件结构
dtb是linux根目录下scripts/dts/dtc(dtc工具编译的时候会产生)工具,将dts生成dtb二进制文件。dtc的工具使用如下:
dts编译成二进制文件dtb:
scripts/dtc/dtc -I dtb -O dts ./product1.dtb -o ./my.dts
dtb反编译成dts:
scripts/dtc/dtc -I dts -O dtb ./a.dts -o ./b.dtb
dts中命令行参数放在如下的节点中:
chosen{
bootargs = "console=ttyS2 init=/init initrd=0x62000000,0x00800000";
};
从这个获取resource.img启动参数,在uboot根目录下common/resource.c,include/resource.h来看:
struct resource_content:这个结构体,是解析时的内存结构
struct resource_ptn_header:这个结构体,是存在于resource.img开始地方,即文件头
struct index_tbl_entry:存在于resource.img每个数据块的头
如下resource.img在nand/emmc结构示意图:
捣事的CSDN,写完整的博客,却不见了,这是我写到一半时保存在自己电脑上的先放这,心好累,以后有时间在看着补全吧,心累啊