九鼎-uboot源码分析
1 前言
1.1 uboot出现的原因
uboot本身是一个裸机程序,uboot出现的目的就是为内核的运行提供环境,内核是不会去进行相关的硬件配置的,因为硬件的种类很多,所以硬件的初始化和配置分出来变成了bootloader,uboot是bootloader的一种。
1.2 uboot主要做哪些工作
1 设置中断向量表
2 设置CPU时钟
3 配置串口
4 配置定时器
5 关闭看门狗
6 初始化DDR
7 记录DDR的大小和物理地址
8 传递机器码
1.3 uboot启动流程
下面是study210开发板 S5PV210芯片的启动流程
1 开发板内置了一块64K的IROM,存放代码的功能主要是初始化SD卡
和对SD卡的读取,简称BL0,起始地址0x0000_0000,arm架构的
cpu刚启动就会到这个地址运行读取代码,这是由硬件决定的。
2 开发板内置了一块96k的SRAM,SRAM不用初始化,上电就可以用,
但是价格比较贵,起始地址: 0xD002_0000
3 BL0读取SD卡的前8k内容到SRAM中的0xD002_0000中,其中前
16字节为校验头,具体是怎么校验的是由BL0中的算法决定的,真
正的代码从0xD002_0010开始,简称BL1
4 BL1只是uboot代码的前8k,是uboot的一部分。因为uboot大小为
几百K,而且SRAM比较贵,DDR又不能上电直接用,所以先使用
SRAM运行前8k代码初始化DDR。
5 前8k代码不止用于初始化DDR,在九鼎的uboot源码中,BL1进行了
设置中断向量表,设置CPU为SVC模式,禁用中断,关闭看门狗,
初始化时钟,初始化串口,设置上电锁存等。
6 DDR初始化完成后,BL1拷贝SD卡49扇区开始的内容到DDR中,49
扇区存放着完整的uboot,拷贝完成后,BL1跳转到BL2工作,这里
跳转的DDR地址是0x33e00000,DDR的物理地址范围0x3000_0000~
0x4fff_ffff共计512M,这个物理地址是可以选择的,这个范围是在
BL1中设置的。
7 BL2也会执行前8K代码的工作,但是会判断当前PC指针的运行位置,
当PC指针在DDR中时,会跳过BL1中已经运行过的函数,BL2会进行
更加细致的硬件设备初始化,并且记录一些必要的硬件参数,把这些
参数放到0x3000_0100的位置,内核也会从这个位置读取参数,引导
内核的运行。
8 BL2在初始化完硬件设备后,会拷贝内核镜像到0x30008000这个地址,
uboot在最后会跳转执行内核时,会传递三个参数,分别使用r0,r1,r2这
三个寄存器传递,分别是0,机器码,和参数的存储地址。至此,
uboot的工作全部结束。
2 源码分析
请看注释,下面的代码已经去掉了没有使用到的条件编译
//BL1部分的前16字节,用来存储校验信息,现在只是内容填充,数据的内容不重要
#if defined(CONFIG_EVT1) && !defined(CONFIG_FUSED)
.word 0x2000
.word 0x0
.word 0x0
.word 0x0
#endif
/*.globl关键字的作用是把变量声明为类似环境变量一样的东西,在整个Makefile编译
过程中,其他的源文件也可以使用这个变量*/
.globl _start
//设置中断向量表
_start: b reset //跳转到reset函数,向下找"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
_undefined_instruction:
.word undefined_instruction
_software_interrupt:
.word software_interrupt
_prefetch_abort:
.word prefetch_abort
_data_abort:
.word data_abort
_not_used:
.word not_used
_irq:
.word irq
_fiq:
.word fiq
_pad:
.word 0x12345678 /* now 16*4=64 */
.global _end_vect
_end_vect:
.balignl 16,0xdeadbeef
/*
*************************************************************************
*
* 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
*
*************************************************************************
*/
//后面会有ldr pc,_TEXT_BASE的代码,意思是跳转到TEXT_BASE地址去执行代码
_TEXT_BASE:
.word TEXT_BASE
/*
* Below variable is very important because we use MMU in U-Boot.
* Without it, we cannot run code correctly before MMU is ON.
* by scsuh.
*/
_TEXT_PHY_BASE:
.word CFG_PHY_UBOOT_BASE
.globl _armboot_start
_armboot_start:
.word _start
/*
* These are defined in the board-specific linker script.
*/
.globl _bss_start
_bss_start:
.word __bss_start
.globl _bss_end
_bss_end:
.word _end
/*
* the actual reset code
*/
//跳转到这里执行复位代码
reset:
/*
* set the cpu to SVC32 mode and IRQ & FIQ disable
*/
@;mrs r0,cpsr //@开头的是注释
@;bic r0,r0,#0x1f
@;orr r0,r0,#0xd3
@;msr cpsr,r0
//失能中断,设置CPU为SVC模式
msr cpsr_c, #0xd3 @ I & F disable, Mode: 0x13 - SVC
/*
*************************************************************************
*
* CPU_init_critical registers
*
* setup important registers
* setup memory timing
*
*************************************************************************
*/
/*
* we do sys-critical inits only at reboot,
* not when booting from ram!
*/
cpu_init_crit:
bl disable_l2cache //做一些关于缓存的配置,我不懂,老师说和主体无关不用管
bl set_l2cache_auxctrl_cycle
bl enable_l2cache
/*
* 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
/*
* disable MMU stuff and caches
*/
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #0x00002000 @ clear bits 13 (--V-)
bic r0, r0, #0x00000007 @ clear bits 2:0 (-CAM)
orr r0, r0, #0x00000002 @ set bit 1 (--A-) Align
orr r0, r0, #0x00000800 @ set bit 12 (Z---) BTB
mcr p15, 0, r0, c1, c0, 0
/* Read booting information */
//0xE000_0004这个寄存器会被自动设置,被设置值和选择的启动介质有关
ldr r0, =PRO_ID_BASE
ldr r1, [r0,#OMR_OFFSET]
bic r2, r1, #0xffffffc1 //r2中根据启动介质的不同,被设置成不同的值
/* SD/MMC BOOT */
cmp r2, #0xc //当r2中的值为0xc的时候,执行下一句代码
/*"moveq"指令用于将一个立即数加载到寄存器中,其中"eq"表示条件码,表示只有
在上一条指令影 响标志位并且结果为零时才执行该指令。*/
moveq r3, #BOOT_MMCSD //BOOT_MMCSD=0X3,r3=0x3
/*
* Go setup Memory and board specific bits prior to relocation.
*/
//根据芯片手册推荐,设置栈指针到合适的SRAM中的区段,为C代码运行做准备
ldr sp, =0xd0036000 /* end of sram dedicated to u-boot */
sub sp, sp, #12 /* set stack */
mov fp, #0 //这个fp指针不知道是干什么的
//跳转到C函数执行代码
bl lowlevel_init /* go setup pll,mux,memory */
lowlevel_init:
//压栈,因为lr寄存器只能保存一个地址,且SVC模式下只有一个lr寄存器,函数跳转必须压栈
push {lr}
/* 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
/* IO Retention release 和主要流程关系不大,后面我就不再赘述,有注释就是重要*/
ldr r0, =(ELFIN_CLOCK_POWER_BASE + OTHERS_OFFSET) /* 0xE0100000 + 0xE000 */
ldr r1, [r0]
ldr r2, =IO_RET_REL
orr r1, r1, r2
str r1, [r0]
/* Disable Watchdog */
ldr r0, =ELFIN_WATCHDOG_BASE /* 0xE2700000 */
mov r1, #0
str r1, [r0]
/* SRAM(2MB) init for SMDKC110 */
/* GPJ1 SROM_ADDR_16to21 */
ldr r0, =ELFIN_GPIO_BASE
ldr r1, [r0, #GPJ1CON_OFFSET]
bic r1, r1, #0xFFFFFF
ldr r2, =0x444444
orr r1, r1, r2
str r1, [r0, #GPJ1CON_OFFSET]
ldr r1, [r0, #GPJ1PUD_OFFSET]
ldr r2, =0x3ff
bic r1, r1, r2
str r1, [r0, #GPJ1PUD_OFFSET]
/* GPJ4 SROM_ADDR_16to21 */
ldr r1, [r0, #GPJ4CON_OFFSET]
bic r1, r1, #(0xf<<16)
ldr r2, =(0x4<<16)
orr r1, r1, r2
str r1, [r0, #GPJ4CON_OFFSET]
ldr r1, [r0, #GPJ4PUD_OFFSET]
ldr r2, =(0x3<<8)
bic r1, r1, r2
str r1, [r0, #GPJ4PUD_OFFSET]
/* CS0 - 16bit sram, enable nBE, Byte base address */
ldr r0, =ELFIN_SROM_BASE /* 0xE8000000 */
mov r1, #0x1
str r1, [r0]
/* PS_HOLD pin(GPH0_0) set to high 上电所存,按下POWER键,板子持续供电*/
ldr r0, =(ELFIN_CLOCK_POWER_BASE + PS_HOLD_CONTROL_OFFSET)
ldr r1, [r0]
orr r1, r1, #0x300
orr r1, r1, #0x1
str r1, [r0]
/* init system clock 初始化时钟*/
bl system_clock_init
//初始化DDR
bl mem_ctrl_asm_init
//汇编语言的执行顺序是流水型,bl执行完函数会跳转回来,然后执行1:
1:
/* for UART */
bl uart_asm_init
bl tzpc_init
bl onenandcon_init
//出栈
pop {pc}
/* get ready to call C functions */
//再次设置栈指针,因为DDR已经初始化,且SRAM的栈太小,所以再次设置栈指针
ldr sp, _TEXT_PHY_BASE /* setup temp stack pointer */
sub sp, sp, #12
mov fp, #0 /* no previous frame, so fp=0 */
/* 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.
*/
//判断pc寄存器现在运行的地址,如果在SRAM中,进行BL2拷贝到DDR中,否则不进行
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 */
//注意一下哈,这里我把after_copy函数拷贝过来了,并不是紧挨着的
after_copy:
//使能mmu,设置虚拟地址转换表,这个内容比较难懂,我看不懂虚拟地址是怎么转换的
enable_mmu:
/* enable domain access */
ldr r5, =0x0000ffff
mcr p15, 0, r5, c3, c0, 0 @load domain access register
/* Set the TTB register */
ldr r0, _mmu_table_base
ldr r1, =CFG_PHY_UBOOT_BASE
ldr r2, =0xfff00000
bic r0, r0, r2
orr r1, r0, r1
mcr p15, 0, r1, c2, c0, 0
/* Enable the MMU */
mmu_on:
mrc p15, 0, r0, c1, c0, 0
orr r0, r0, #1
mcr p15, 0, r0, c1, c0, 0
nop
nop
nop
nop
//after_copy 结尾处
//再次设置栈指针,这次设置的目的是把栈放置到合适的位置
ldr sp, =(CFG_UBOOT_BASE + CFG_UBOOT_SIZE - 0x1000)
sub sp, r0, #12 /* leave 3 words for abort-stack */
//bss段变量的值设置为0
clear_bss:
ldr r0, _bss_start /* find start of bss segment */
ldr r1, _bss_end /* stop here */
mov r2, #0x00000000 /* clear */
clbss_l:
str r2, [r0] /* clear loop... */
add r0, r0, #4
cmp r0, r1
ble clbss_l
/*跳转执行_start_armboot,本人认为啊,上面的代码知道具体干什么就行了,代码
只是对功能的实现,知道功能,再去实现代码不是一件难事,所以学习要学习思路*/
ldr pc, _start_armboot
_start_armboot:
.word start_armboot
上面是uboot的BL1部分,主要进行了:
1 异常中断向量表的创建
2 关闭看门狗
3 上电锁存
4 DDR初始化
5 系统时钟初始化
6 串口初始化
7 设置栈指针
8 使能MMU,创建内存映射表(老版本uboot并没有使用虚拟地址)
接下来进行BL2源码的解析,注意一下哈,我贴出来的代码都是去掉条件编译的
void start_armboot (void)
{
init_fnc_t **init_fnc_ptr;
char *s;
int mmc_exist = 0;
ulong gd_base;
//设置全局变量存储的地址,事实上,你想怎么设置就怎么设置,毕竟DDR随便操作,但是不要出现数据覆盖的情况
gd_base = CFG_UBOOT_BASE + CFG_UBOOT_SIZE - CFG_MALLOC_LEN - CFG_STACK_SIZE - sizeof(gd_t);
//转成gd_t结构体类型,因为/**/这种注释不能嵌套,所以我使用条件编译的形式进行注释的添加
#if 0
typedef struct global_data {
bd_t *bd;
unsigned long flags;
unsigned long baudrate;
unsigned long have_console; /* serial_init() was called */
unsigned long reloc_off; /* Relocation Offset */
unsigned long env_addr; /* Address of Environment struct */
unsigned long env_valid; /* Checksum of Environment valid? */
unsigned long fb_base; /* base address of frame buffer */
void **jt; /* jump table */
} gd_t;
#endif
gd = (gd_t*)gd_base;
/* compiler optimization barrier needed for GCC >= 3.4 */
//内存优化方面的知识,我不了解
__asm__ __volatile__("": : :"memory");
memset ((void*)gd, 0, sizeof (gd_t));
//从这句代码来看,结构体在内存中的存储形式是大端模式
gd->bd = (bd_t*)((char*)gd - sizeof(bd_t));
memset (gd->bd, 0, sizeof (bd_t));
//_bss_start和_armboot_start是makefile链接脚本指定的
monitor_flash_len = _bss_start - _armboot_start;
//和BL1的硬件初始化不同,这里是存储硬件的相关信息,比如:内存的地址分布、数量和大小
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
if ((*init_fnc_ptr)() != 0) {
hang ();
}
}
//uboot中实现了一个malloc函数,用来申请内存,堆区的大小由宏配置文件的宏定义决定
//如果没记错的话,这里的堆区应该是900多K。实际上malloc的底层实现原理只是有一个
//内存管理表,这个表是个数据结构,我好像记得里面存储的是一个个的结构体,记录了每一块
//内存的起始地址,类型,大小等,free函数就是删除这个结构体
mem_malloc_init (CFG_UBOOT_BASE + CFG_UBOOT_SIZE - CFG_MALLOC_LEN - CFG_STACK_SIZE);
//这里进行更细致的SD卡初始化,IROM中只是进行了简单的初始化
puts ("SD/MMC: ");
mmc_exist = mmc_initialize(gd->bd);
if (mmc_exist != 0)
{
puts ("0 MB\n");
}
/* initialize environment */
env_relocate (); //我没了解,但是老师讲了
/* IP Address */
gd->bd->bi_ip_addr = getenv_IPaddr ("ipaddr"); //获取IP地址
/* MAC Address */
{
int i;
ulong reg;
char *s, *e;
char tmp[64];
i = getenv_r ("ethaddr", tmp, sizeof (tmp));
s = (i > 0) ? tmp : NULL;
for (reg = 0; reg < 6; ++reg) {
gd->bd->bi_enetaddr[reg] = s ? simple_strtoul (s, &e, 16) : 0;
if (s)
s = (*e) ? e + 1 : e;
}
}
//这个函数里面存储的应该是函数名,然后去执行函数
devices_init (); /* get the devices list going. */
jumptable_init (); //不懂
/* enable exceptions */
enable_interrupts ();
cs8900_get_enetaddr (gd->bd->bi_enetaddr); //应该和网卡驱动有关
/* Initialize from environment 从环境变量配置那里读取信息,并给一些变量赋值*/
if ((s = getenv ("loadaddr")) != NULL) {
load_addr = simple_strtoul (s, NULL, 16);
}
if ((s = getenv ("bootfile")) != NULL) {
copy_filename (BootFile, s, sizeof (BootFile));
}
/*这后面的知识,我偷懒了,没学,哈哈哈,大概的作用就是为后面向内核传递
硬件设备的参数做准备*/
board_late_init ();
puts ("Net: ");
eth_initialize(gd->bd);
puts("IDE: ");
ide_init();
extern int x210_preboot_init(void);
x210_preboot_init();
/* check menukey to update from sd */
extern void update_all(void);
if(check_menu_update_from_sd()==0)//update mode
{
//按下LEFT按键好像可以使用SD卡向iNand烧录uboot kernel rootfs,使用SD卡快速烧录
puts ("[LEFT DOWN] update mode\n");
run_command("fdisk -c 0",0);
update_all();
}
else
puts ("[LEFT UP] boot mode\n");
/* main_loop() can return to retry autoboot, if so just run it again. */
for (;;) {
//进入uboot的类型shell的命令终端窗口,进入后,后自动调用一些设置的命令,比如:bootm 0x30008000;跳转到指定地址执行程序。具体kernel和rootfs的挂载地址我忘记了,这里可能不准确。bootm对应的函数是 do_bootm_linux,请看下一个函数。
main_loop ();
}
/* NOTREACHED - no way out of command loop except booting */
}
//bootm调用的函数
void do_bootm_linux (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[],
bootm_headers_t *images)
{
ulong initrd_start, initrd_end;
ulong ep = 0;
bd_t *bd = gd->bd;
char *s;
//机器码,正常来说,每个板子的机器码是唯一的,可以通过查看好像叫type-machines的文件获取
int machid = bd->bi_arch_number;
//定义个执行内核的函数,并进行传参
void (*theKernel)(int zero, int arch, uint params);
int ret;
#ifdef CONFIG_CMDLINE_TAG
//获取在BL2开头部分存储的硬件信息,这里是获取自己设置的环境变量
char *commandline = getenv ("bootargs");
#endif
/* find kernel entry point */
if (images->legacy_hdr_valid) {
//ep:entry point获取内核的程序入口,就行C语言中的main入口
ep = image_get_ep (&images->legacy_hdr_os_copy);
#if defined(CONFIG_FIT)
} else if (images->fit_uname_os) {
ret = fit_image_get_entry (images->fit_hdr_os,
images->fit_noffset_os, &ep);
if (ret) {
puts ("Can't get entry point property!\n");
goto error;
}
#endif
} else {
puts ("Could not find kernel entry point!\n");
goto error;
}
//跳转到内核的程序入口地址,这里并不是真正的入口地址,而是zImage头部解压代码的入口
theKernel = (void (*)(int, int, uint))ep;
s = getenv ("machid");
if (s) {
machid = simple_strtoul (s, NULL, 16);
printf ("Using machid 0x%x from environment\n", machid);
}
//不懂
ret = boot_get_ramdisk (argc, argv, images, IH_ARCH_ARM,
&initrd_start, &initrd_end);
if (ret)
goto error;
//不懂,有的没注释是因为有英文注释,或者自己不懂
show_boot_progress (15);
debug ("## Transferring control to Linux (at address %08lx) ...\n",
(ulong) theKernel);
//内核中取参数时,setup_start_tag会作为开始标志。传参按照tag结构体的形式,内核也会使用这种方式取
setup_start_tag (bd);
setup_memory_tags (bd); //这里重要,会传递几块DDR,起始地址和大小。这些信息在BL2中设置
setup_commandline_tag (bd, commandline);
if (initrd_start && initrd_end)
setup_initrd_tag (bd, initrd_start, initrd_end);
setup_mtdpartition_tag();
setup_end_tag (bd);
printf ("\nStarting kernel ...\n\n"); //uboot最后打印的信息
cleanup_before_linux ();
theKernel (0, machid, bd->bi_boot_params); //开始跳转执行kernel
/* does not return */
return;
error:
do_reset (cmdtp, flag, argc, argv);
return;
}
至此,uboot源码全部分析完毕,上面内容是自己的理解,难免有错误,读者注意辨别。