1. Bootloader功能分析
-
Bootloader(如Uboot、Redboot、Blob、vivi等)直接和CPU、外围硬件设备(存储器、网卡、LCD等)打交道,负责初始化硬件设备,以及负责拉起内核:建立内存空间映射图,为内核的启动运行做好一切准备,最后将Linux内核代码加载到RAM中运行。
-
一般来说,bootload都会提供两种操作模式:
- 正常启动模式:无需开发者和用户干涉,上电后自动开始运行,完成启动内核的任务;
- 下载模式(开发者模式):需要用户干预,进入下载模式,使用uboot命令进行操作;
-
BootLoader程序是分级载入机制,通常分为 stage1 和 stage2 两大部分。
-
Stage1:
BootLoader 的 stage1 依赖于CPU体系结构的代码,例如CPU相关初始化代码等, 通常都用汇编语言来实现,达到高效操作的目的。 包括以下步骤:- ①硬件设备初始化
- 屏蔽所有的中断
- 设置CPU的速度和时钟频率
- 设置内存控制器
- 关闭 CPU 内部指令/数据 cache
- ②为加载 stage2 准备 RAM 空间
- ③拷贝 stage2 到 RAM 空间中
- ④设置好堆栈指针sp,为执行 C 语言代码作好准备;
- ⑤跳转到 stage2 阶段的C程序入口处。
- ①硬件设备初始化
-
Stage2:
BootLoader 的 stage2 通常用C语言来实现,可以实现更复杂的功能,而且代码会具有更好的可读性和可移植性。 包括以下步骤:
- ①初始化本阶段要使用到的硬件设备;
- 串口设备:以便输出信息
…… - ②检测系统内存映射(memory map):准备识别在整个 4GB 物理地址空间中有哪些地址范围被分配用来寻址系统的 RAM单元;
- ③将 kernel 映像和根文件系统映像从 flash 上读到 RAM 空间中;
- ④为内核设置启动参数;
- ⑤调用内核:跳至内核代码入口开始执行。
- 串口设备:以便输出信息
- ①初始化本阶段要使用到的硬件设备;
-
2. Uboot代码结构分析
开发环境:我使用的是JZ2440开发板,处理器是三星的S3C2440,CPU是ARM920T。
开发目标:修改Uboot源代码,使之适配我的S3C2440处理器。
开发方法:1、因为Uboot默认不支持S3C2440处理器,所以先研究Uboot支持的且与S3C2440相近的S3C2410的启动过程源代码,了解自己需要修改哪些文件。
首先,请看smdk2410的启动过程思维导图:
2.1 一看配置文件smdk2410.h
在查看uboot源码的时候,很多配置项都取决于单板配置文件中的宏定义,比如smdk2410这个单板的配置文件是include\configs\smdk2410.h
文件,在查看源码时,可以在此文件中查看宏定义是否存在。
2.2 二看链接文件u-boot.ds
可以看到,0地址处存放的是arch/arm/cpu/arm920t/start.S
文件编译产生的obj文件。
2.3 三看启动文件start.S
打开arch/arm/cpu/arm920t/start.S
.globl _start // .globl定义一个全局符号"_start",表明_start这个符号要被链接器用到
_start: //_start:系统复位设置,以下共8种不同的异常处理
b start_code //系统启动即跳转到start_code处执行
ldr pc, _undefined_instruction //未定义的指令异常 0x4
ldr pc, _software_interrupt // 软件中断异常 0x8
ldr pc, _prefetch_abort //内存操作异常 0xc
ldr pc, _data_abort //数据异常 0x10
ldr pc, _not_used //未使用 0x14
ldr pc, _irq //中断IRQ异常 0x18
ldr pc, _fiq //快速中断FIQ异常 0x1c
_undefined_instruction: .word undefined_instruction //0x20
_software_interrupt: .word software_interrupt //0x24
_prefetch_abort: .word prefetch_abort // 0x28
_data_abort: .word data_abort //0x2c
_not_used: .word not_used //0x30
_irq: .word irq //0x34
_fiq: .word fiq //0x38
.balignl 16,0xdeadbeef //0x3c
在第1行中".globl _start":使用.globol声明全局符号_start,在 board/100ask24x0/u-boot.lds中ENTRY(_start)这里用到。其中符号保存的地址都在顶层目录/system.map中列出来了
system.map文件开头部分如下:
33f80000 t $a
33f80000 T _start //_start符号被链接在33f80000,其中33f80000是生成bin文件的运行启始地址.
33f80020 t $d
33f80020 t _undefined_instruction //_undefined_instruction符号被链接在33f80020
...
33f80160 t undefined_instruction //_undefined_instruction指向的undefined_instruction符号被链接在33f80160
33f801c0 t software_interrupt
33f80220 t prefetch_abort
33f80280 t data_abort
33f802e0 t not_used
33f80340 T Launch
33f803b0 t On_Steppingstone
33f80400 t irq
...
在第2行中_start之所以有8种不同的异常处理,是在2440芯片手册已经规定好了的,如下图1:
从上图可以看出复位异常处理需要进入管理模式(0X00000000),所以start.S 中b start_code
也可以写作b reset
,都是跳转到start_code处执行,即设置CPU进入管理模式后进行核级初始化。
在linux中的异常向量地址是经过MMU(虚拟内存管理)产生的虚拟地址,比如中断地址:0x18映射到物理地址是0xc000 0018(映射地址由自己设定),所以linux把中断向量放在0xc000 0018就行了。
CPU一上电设置了入口地址"ENTRY(_start)“后,就会进入”_start"全局符号中执行上面第3行跳转到复位异常字段: b rese
或b start_code
。
1. 后面的异常处理为什么用ldr不用b指令?
之所以第一句使用b reset,是因为ldr指令属于绝对跳转,而b属于相对跳转,它的地址与代码位置无关。因为复位异常在CPU运行前是没有初始化SDRAM的(不能使用0X30000000以上地址)。
在正常工作后也可能触发复位,这时由于CPU已经对SDRAM、MMU(虚拟内存管理)等初始化了,此时的虚拟地址和物理地址完全不同,所以reset使用b指令相对跳转。
2. 后面的异常处理是怎么执行的?执行后异常处理又怎么退出?
(1)在2440芯片手册上给出,例如当处理一个中断IRQ异常时:
- a. 保存当前PC现场(返回地址)到寄存器R14;
- b. 把当前程序状态寄存器(CPSR)保存到备份程序状态寄存器(SPSR)中,从异常退出的时候,就可以由SPSR来恢复CPSR;
- c. 根据中断IRQ异常处理,强制将 CPSR 模式位设为中断模式,如下图:
- d. 强制 PC 执行相关异常向量处的语句(跳转到中断异常处理函数处)。
(2)当退出中断IRQ异常时:**
- a. 将中断IRQ所对应的R14_irq寄存器中的返回地址减4(PC总指向当前执行语句的下两条语句地址)得到被中断语句的下条地址放入到 PC 中。
- b. 复制 SPSR 的内容给 CPSR 中。
- c. 如果在异常进入时置位了中断禁止标志位异常,则清除中断禁止标志位。
3. 第12行中的
.word
类似于(unsigend long)以第12行中
_undefined_instruction: .word undefined_instruction
为例讲解:
_undefined_instruction
和undefined_instruction
都是一个标号
,表示_undefined_instruction
指向一个32位(4字节)地址,该地址用undefined_instruction
符号变量代替。用C语言表示就是:
_undefined_instruction = &undefined_instruction
相当于PC从_undefined_instruction取值时将符号变量undefined_instruction的地址存到了PC中。
4. 第20行中 .balignl 16,0xdeadbeef:
它的意思就是在以当前地址开始,在地址为16的倍数的指令位置的上一个指令填入0xdeadbeef的内容。
此时当前地址刚好0x3c=60,由于ARM每个指令间隔4个字节,且64%16=0,所以在0x3c中填入0xdeadbeef。
它们的作用就是为内存做标记插在那里,表示以此为界,往前有特殊作用的内存,禁止用户访问。
接下来继续往下看start.s
/*
*实际启动代码
*/
start_code:
/* 1、设置CPU为管理模式(SVC32 mode)*/
mrs r0,cpsr //MRS读出CPSR寄存器值到R0
bic r0,r0,#0x1f //将R0低5位清空
orr r0,r0,#0xd3 //R0与b'110 10011按位或,禁止IRQ和FIQ中断,10011:复位需要设为管理模式
msr cpsr,r0 //MSR写入CPSR寄存器
/* S3C2410的内核是ARM920T,这段代码用不到 */
#if defined(CONFIG_AT91RM9200DK) || defined(CONFIG_AT91RM9200EK)
/*
* relocate exception table
*/
ldr r0, =_start
ldr r1, =0x0
mov r2, #16
copyex:
subs r2, r2, #1
ldr r3, [r0], #4
str r3, [r1], #4
bne copyex
#endif
#ifdef CONFIG_S3C24X0
/* turn off the watchdog */
/* 2、关看门狗 */
//经过查看S3C2410/S3C2440数据手册,寄存器地址一致,不用修改
# if defined(CONFIG_S3C2400)
#define pWTCON 0x15300000
#define INTMSK 0x14400008 /* Interrupt-Controller base addresses */
#define CLKDIVN 0x14800014 /* clock divisor register */
#else
#define pWTCON 0x53000000 //看门狗定时器寄存器(0表示关闭看门狗)
#define INTMOD 0X4A000004 //(中断模式寄存器(0:IRQ模式,1:FRQ模式)
#define INTMSK 0x4A000008 //中断屏蔽寄存器(0:开启中断服务,1:关闭中断服务)
#define INTSUBMSK 0x4A00001C //中断次级屏蔽寄存器(0:开启中断服务,1:关闭中断服务)
#define CLKDIVN 0x4C000014 //时钟分频寄存器
#endif
/* 关看门狗 */
ldr r0, =pWTCON //R0等于WTCON地址
mov r1, #0x0 //R1=0x0
str r1, [r0] //关闭WTCON寄存器,pWTCON=0;
/* 3、关掉所有中断 */
mov r1, #0xffffffff //R1=0XFFFFFFFF
ldr r0, =INTMSK //R0等于INTMSK地址
str r1, [r0] //*0x4A000008=0XFFFF FFFF(关闭所有中断)
# if defined(CONFIG_S3C2410)
ldr r1, =0x3ff //R1=0x3FF
ldr r0, =INTSUBMSK //R0等于INTSUBMSK地址
str r1, [r0] //*0x4A00001C=0x3FF(关闭次级所有中断)
# endif
/* 4. 设置系统时钟分频系数 */
/* FCLK:HCLK:PCLK = 1:2:4 */
/* default FCLK is 120 MHz ! */
ldr r0, =CLKDIVN
mov r1, #3
str r1, [r0]
/*
* we do sys-critical inits only at reboot,
* not when booting from ram!
* 5. CPU内部初始化
*判断系统是从nand启动还是直接将程序下载到SDRAM中运行,若系统从nand启动,这里得到r0和r1值
*是不一样的,r1=0x33f80000,而r0=0x00000000。说明没初始化SDRAM,ne(no equal)标识符为
*真,所以bl cpu_init_crit执行跳转.
*/
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
adr r0, _start
ldr r1, _TEXT_BASE
cmp r0, r1
blne cpu_init_crit
#endif
/* Set stackpointer in internal RAM to call board_init_f */
/* 6. 设置好栈顶指针sp = 0x30000f80,然后调用C程序中的board_init_f 函数 */
call_board_init_f:
ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
ldr r0,=0x00000000
bl board_init_f
- CPU复位后从这里开始执行bootloader的 stage1 阶段,这里初始化了:
- 1.设置CPU为管理模式:SVC32
- 2.关看门狗
- 3.屏蔽所有中断
- 4.设置系统时钟分频系数
- 5.CPU内部(核级)初始化(进入cpu_init_crit函数关闭MMU,进入lowlevel_init初始化13个BANK寄存器来初始化SDRAM)
- 6.设置好栈顶指针sp,调用C程序中的board_init_f 函数。
2.4 四看CPU核级初始化文件cpu_init_crit函数
- cpu_init_crit函数的主要功能是:
- 设置CPU中重要的寄存器
- 设置内存控制器时序
2.4.1 设置CPU中的寄存器
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
cpu_init_crit:
/*
*flush v4 I/D caches
*/
mov r0, #0
//关闭ICaches(指令缓存,关闭是为了降低MMU查表带来的开销)和DCaches(数据缓存,DCaches使用的是虚拟地址,开启MMU之前必须关闭)
mcr p15, 0, r0, c7, c7, 0
//使无效整个数据和指令TLB(TLB就是负责将虚拟内存地址翻译成实际的物理内存地址)
mcr p15, 0, r0, c8, c7, 0
/*
* disable MMU stuff and caches
*/
mrc p15, 0, r0, c1, c0, 0
//bit8:系统不保护,bit9:ROM不保护,bit13:设置正常异常模式0x0~0x1c,即异常模式基地址为0X0
bic r0, r0, #0x00002300 @ clear bits 13, 9:8 (--V- --RS)
//bit0~2:禁止MMU,禁止地址对齐检查,禁止数据Cache.bit7:设为小端模式
bic r0, r0, #0x00000087 @ clear bits 7, 2:0 (B--- -CAM)
orr r0, r0, #0x00000002 @ bit2:开启地址对齐
orr r0, r0, #0x00001000 @ bit12:开启ICache
mcr p15, 0, r0, c1, c0, 0
/*
*Caches:是一种高速缓存存储器,用于保存CPU频繁使用的数据。在使用Cache技术的处理器上,当一条
*指令要访问内存的数据时,首先查询cache缓存中是否有数据以及数据是否过期,如果数未过期则从
*cache读出数据。处理器会定期回写cache中的数据到内存。根据程序的局部性原理,使用cache后可以
*大大加快处理器访问内存数据的速度。其中DCaches和ICaches分别用来存放数据和执行这些数据的指令。
*TLB:就是负责将虚拟内存地址翻译成实际的物理内存地址,TLB中存放了一些页表文件,文件中记录了虚
*拟地址和物理地址的映射关系。当应用程序访问一个虚拟地址的时候,会从TLB中查询出对应的物理地址,
*然后访问物理地址。TLB通常是一个分层结构,使用与Cache类似的原理。处理器使用一定的算法把最常用
*的转换表放在最先访问的层次。这里禁用MMU,是方便后面直接使用物理地址来设置控制寄存。
*/
/*
* before relocating, we have to setup RAM timing
* because memory timing is board-dependend, you will
* find a lowlevel_init.S in your board directory.
*/
mov ip, lr //临时保存当前子程序返回地址,因为接下来执行bl会覆盖当前返回地址.
bl lowlevel_init //跳转到lowlevel_init(/board/samsung/smdk2410/lowlevel_init.S)
mov lr, ip //恢复当前返回地址
mov pc, lr //退出
#endif /* CONFIG_SKIP_LOWLEVEL_INIT */
2.4.2 调用lowlevel_init函数 设置内存控制器
每个单板的内存控制器设置代码都是不一样的,所以 lowlevel_init
函数放在了单板目录中的lowlevel_init.S
文件中,即 board\samsung\smdk2410\lowlevel_init.S
。
_TEXT_BASE:
.word CONFIG_SYS_TEXT_BASE
.globl lowlevel_init
lowlevel_init:
/* memory control configuration */
/* make r0 relative the current location so that it */
/* reads SMRDATA out of FLASH rather than memory ! */
ldr r0, =SMRDATA //将SMRDATA的首地址(0x33F806C8)存到r0中
ldr r1, _TEXT_BASE //r1等于_TEXT_BASE内容,也就是TEXT_BASE(0x33F80000)
sub r0, r0, r1 //将0x33F806C8与0x33F80000相减,得到现在13个寄存器值在NOR Flash上存放的开始地址
ldr r1, =BWSCON //将BWSCON寄存器地址值存到r1中 (第一个存储器寄存器首地址)
add r2, r0, #13*4 //每个寄存器4字节,r2=r0+13*4=NOR Flash上13个寄存器值最后一个地址
0:
ldr r3, [r0], #4 //将r0的内容存到r3的内容中(r3等于SMRDATA里面值), 同时r0地址+=4;
str r3, [r1], #4 //将r3的内容存到r1所指的地址中(向寄存器地址里写入r3值),同时r1地址+=4;
cmp r2, r0 // 判断r2和r0
bne 0b //不等则跳转到第6行继续执行
mov pc, lr //跳回到返回地址中继续执行
SMRDATA:
.word (0+(B1_BWSCON<<4)+(B2_BWSCON<<8)+(B3_BWSCON<<12)+(B4_BWSCON<<16)+(
B5_BWSCON<<20)+(B6_BWSCON<<24)+(B7_BWSCON<<28)) //设置每个BWSCON,注意BANK0由硬件连线决定了
.word ((B0_Tacs<<13)+(B0_Tcos<<11)+(B0_Tacc<<8)+(B0_Tcoh<<6)+(B0_Tah<<4)+(B0_Tacp<<2)+(B0_PMC))
.word ((B1_Tacs<<13)+(B1_Tcos<<11)+(B1_Tacc<<8)+(B1_Tcoh<<6)+(B1_Tah<<4)+(B1_Tacp<<2)+(B1_PMC))
.word ((B2_Tacs<<13)+(B2_Tcos<<11)+(B2_Tacc<<8)+(B2_Tcoh<<6)+(B2_Tah<<4)+(B2_Tacp<<2)+(B2_PMC))
.word ((B3_Tacs<<13)+(B3_Tcos<<11)+(B3_Tacc<<8)+(B3_Tcoh<<6)+(B3_Tah<<4)+(B3_Tacp<<2)+(B3_PMC))
.word ((B4_Tacs<<13)+(B4_Tcos<<11)+(B4_Tacc<<8)+(B4_Tcoh<<6)+(B4_Tah<<4)+(B4_Tacp<<2)+(B4_PMC))
.word ((B5_Tacs<<13)+(B5_Tcos<<11)+(B5_Tacc<<8)+(B5_Tcoh<<6)+(B5_Tah<<4)+(B5_Tacp<<2)+(B5_PMC))
//设置BANKCON0~BANKCON5
.word ((B6_MT<<15)+(B6_Trcd<<2)+(B6_SCAN))
.word ((B7_MT<<15)+(B7_Trcd<<2)+(B7_SCAN))
//设置BANKCON6~BANKCON7
.word ((REFEN<<23)+(TREFMD<<22)+(Trp<<20)+(Trc<<18)+(Tchr<<16)+REFCNT)
//设置REFRESH,在S3C2440中11~17位是保留的,也即(Tchr<<16)无意义
.word 0xb1 //设置BANKSIZE,对于容量可以设置大些,多出来的空内存会被自动检测出来
.word 0x30 //设置MRSRB6
.word 0x30 //设置MRSRB7
2.5 五看CPU板级初始化文件board_init_f函数
该函数在arch/arm/lib/board.c
中定义:
2.5.1 gd指针
/* Pointer is writable since we allocated a register for it */
gd = (gd_t *) ((CONFIG_SYS_INIT_SP_ADDR) & ~0x07);
gd指针变量是一个寄存器变量,在arch/arm/include/asm/global_data.h
文件中定义:#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r8")
。这个宏定义将gd定义为一个指向gd_t类型的寄存器变量(优势:读写效率高),并将这个寄存器指定为CPU寄存器组中的r8寄存器。
那么,gd变量指向内存中的哪个地址呢?
在stage1阶段跳转到 board_init_f 函数之前,使用汇编指令将 sp 设置为CONFIG_SYS_INIT_SP_ADDR,通过直接查看反汇编代码,得到该值为0x30000f80。
接下来具体研究一下CONFIG_SYS_INIT_SP_ADDR是如何计算出来的,在include/configs/smdk2410.h文件中可以看到计算公式:
/* additions for new relocation code, must be added to all boards */
//为了新的重定位代码添加
#define CONFIG_SYS_SDRAM_BASE PHYS_SDRAM_1
#define CONFIG_SYS_INIT_SP_ADDR (CONFIG_SYS_SDRAM_BASE + 0x1000 - GENERATED_GBL_DATA_SIZE)
同样在该配置文件中,定义了 PHYS_SDRAM_1 的大小:
* Physical Memory Map
*/
#define CONFIG_NR_DRAM_BANKS 1 /* we have 1 bank of DRAM */
#define PHYS_SDRAM_1 0x30000000 /* SDRAM Bank #1 */
#define PHYS_SDRAM_1_SIZE 0x04000000 /* 64 MB */
#define PHYS_FLASH_1 0x00000000 /* Flash Bank #0 */
#define CONFIG_SYS_FLASH_BASE PHYS_FLASH_1
2.5.2 执行init_sequence(初始化序列)中的所有函数
接下来继续研读uboot源代码,在设置完gd指针之后,uboot调用执行了 init_sequence
中的所有函数:
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
if ((*init_fnc_ptr)() != 0) {
hang ();
}
}
这个函数指针数组init_sequence具体的定义如下(方便起见,我将其中没有用到的代码标示了“未用到”):
init_fnc_t *init_sequence[] = {
/*未用到*/
#if defined(CONFIG_ARCH_CPU_INIT)
arch_cpu_init, /* basic arch cpu dependent setup */
#endif
#if defined(CONFIG_BOARD_EARLY_INIT_F)
board_early_init_f,
#endif
/*未用到*/
#ifdef CONFIG_OF_CONTROL
fdtdec_check_fdt,
#endif
timer_init, /* initialize timer */
/*未用到*/
#ifdef CONFIG_FSL_ESDHC
get_clocks,
#endif
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 */
NULL,
};
总结一下,初始化序列 init_sequence 主要是设备初始化工作:
- ① 硬件平台初始化:时钟系统初始化,GPIO初始化;
- ② 定时器初始化;
- ③ 外围设备初始化:串口、Flash等;
- ④ 打印CPU信息;
- ⑤ 初始化DRAM(SDRAM);
接下来挨个查看这些函数的源码:
- board_early_init_f
这个函数在
board/samsung/smdk2410/smdk2410.c
文件中定义,是一些与硬件平台相关的初始化,包括时钟初始化、GPIO初始化:
/*
* Miscellaneous platform dependent initialisations
* 各种各样的硬件平台相关初始化
*/
int board_early_init_f(void)
{
struct s3c24x0_clock_power * const clk_power =
s3c24x0_get_base_clock_power();
struct s3c24x0_gpio * const gpio = s3c24x0_get_base_gpio();
/* to reduce PLL lock time, adjust the LOCKTIME register */
writel(0xFFFFFF, &clk_power->locktime);
/* configure MPLL */
writel((M_MDIV << 12) + (M_PDIV << 4) + M_SDIV,
&clk_power->mpllcon);
/* some delay between MPLL and UPLL */
pll_delay(4000);
/* configure UPLL */
writel((U_M_MDIV << 12) + (U_M_PDIV << 4) + U_M_SDIV,
&clk_power->upllcon);
/* some delay between MPLL and UPLL */
pll_delay(8000);
/* set up the I/O ports */
writel(