初识UBoot
U-Boot(Universal Bootloader)是一个开源的引导加载程序,广泛应用于嵌入式系统中,特别是在嵌入式Linux系统中。它负责启动系统并加载操作系统内核到系统的内存中运行。U-Boot提供了一系列的引导选项和功能,使得开发者可以灵活地配置和定制系统的引导过程。
U-Boot可以在各种嵌入式平台上运行,包括但不限于ARM、PowerPC、x86等架构。它支持多种引导设备,如Flash存储器、SD卡、MMC卡、网络等。除了引导功能外,U-Boot还提供了一些调试和测试工具,如调试器、命令行接口等,方便开发人员进行系统调试和测试。
使用U-Boot时,开发者可以根据具体的需求和平台配置U-Boot的参数,如引导设备、内存、内核映像文件的路径等。同时,U-Boot也支持脚本编程,可以在启动过程中执行特定的任务和初始化动作。
总的来说,U-Boot是一个功能强大的开源引导加载程序,广泛应用于嵌入式系统中,提供灵活的引导配置和定制选项,方便开发者进行系统启动和调试。
如何选择UBoot版本
uboot 官网为 http://www.denx.de/wiki/U-Boot/ ;
git链接为 https://source.denx.de/u-boot/u-boot.git ;
版本选择条件是够用就行,并不是版本越新越好,尽量选择稳定版本,最新版本会新增功能,可能存在未被发现的bug。
UBoot源码分析
\arch\arm\cpu\armv7
这个目录下存放的是和CPU相关的文件, 我们会发现有很多芯片公司的目录,大多数代码都是共用的,我们所有的代码都是从这个start.S文件开始的,后面需要着重分析这个文件。
\board\samsung
/board目录下主要存放的是各个开发板的配置,比如不同公司的板子外设会有所不同,需要对这些做特定的初始化工作,如USB, EMMC, FLASH, 看门狗等。我们多对比几个版本会发现每更新一代基本上会增加一些不同板子的配置文件,届时我们可以根据自己的需求去对这些文件进行修改。
start.S代码分析
以下代码为2013.10版本
#include <asm-offsets.h>
#include <config.h>
#include <version.h>
#include <asm/system.h>
#include <linux/linkage.h>
这里为头文件,前四个头文件在/arch/arm/cpu/include/目录下,最后一个是在外部/include/linux/目录下,里面有些定义后面会用到,可以事先看下里面的内容。
.globl _start
_start: b 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 //快中断
#ifdef CONFIG_SPL_BUILD
_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 */ //调试用的标志
#else
_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 */ //调试用的标志
#endif /* CONFIG_SPL_BUILD */
.global _end_vect
_end_vect:
.balignl 16,0xdeadbeef //代码填充,16字节对齐
这里为异常向量表,当出现异常时会跳转到相应的代码去进行异常处理,ldr指令是将后面的地址的内容放入到pc当中。这里我们会发现有个SPL的宏定义,为了了解什么是SPL呢我们必须研究芯片的启动流程,这里以S5PV210启动流程为例:
BL2
芯片上电后第一步会在iROM里运行第一段程序,这里是厂商已经写好的代码我们不需要关心,第二步会将我们的BL1程序(也就是第一段引导程序)搬运到SRAM中去运行,然后进行一些必要的初始化工作,第三步初始化SDRAM过后再将BL2程序(也就是第二段引导程序,UBoot.bin文件,将引导linux等内核的加载)搬运到SDRAM中去运行,运行过后第四步将操作系统搬运到SDRAM中去运行,这就是大致的启动流程。
那么为什么会这样去做呢,我们编译一下UBoot可以得到UBoot.bin这个文件,我们会发现这个文件大小应该在200k左右,而芯片的SRAM大小只有96k,所以他并不支持一次性将UBoot拷贝到SRAM中去运行,就将此分为了两部分,那么带有CONFIG_SPL_BUILD这个宏定义的代码就是我们的第一段引导程序,是在SRAM中运行的。所以两个代码的中断向量表的地址是不同的,需要用这个宏定义进行区分。
/*************************************************************************
*
* 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
*
*************************************************************************/
.globl _TEXT_BASE
_TEXT_BASE:
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_TEXT_BASE)
.word CONFIG_SPL_TEXT_BASE
#else
.word CONFIG_SYS_TEXT_BASE
#endif
这一部分是代码段的起始地址,不同芯片将代码放在内存中的位置是不同的,这个宏在/include/config/目录下的文件里面定义。
/*
* These are defined in the board-specific linker script.
*/
.globl _bss_start_ofs
_bss_start_ofs:
.word __bss_start - _start
.globl _bss_end_ofs
_bss_end_ofs:
.word __bss_end - _start
.globl _end_ofs
_end_ofs:
.word _end - _start
这一部分是段的标号,没有太大的实际作用
#ifdef CONFIG_USE_IRQ
/* IRQ stack memory (calculated at run-time) */
.globl IRQ_STACK_START
IRQ_STACK_START:
.word 0x0badc0de
/* IRQ stack memory (calculated at run-time) */
.globl FIQ_STACK_START
FIQ_STACK_START:
.word 0x0badc0de
#endif
/* IRQ stack memory (calculated at run-time) + 8 bytes */
.globl IRQ_STACK_START_IN
IRQ_STACK_START_IN:
.word 0x0badc0de
这里的badcode是存放堆栈指针的地方,但是我们的程序还未开始运行,所有无法知道具体的位置,只能随机填充,后面程序运行时会修改这个部分。通过注释我们也可以看出(calculated at run-time 运行时计算)。
reset:
bl save_boot_params
/*
* disable interrupts (FIQ and IRQ), also set the cpu to SVC32 mode,
* except if in HYP mode already
*/
mrs r0, cpsr
and r1, r0, #0x1f @ mask mode bits
teq r1, #0x1a @ test for HYP mode
bicne r0, r0, #0x1f @ clear all mode bits
orrne r0, r0, #0x13 @ set SVC mode
orr r0, r0, #0xc0 @ disable FIQ and IRQ
msr cpsr,r0
跳转指令b和bl的区别在于b是直接跳转,bl是跳转后返回继续运行下一行代码。
bl save_boot_params 跳转到标号save_boot_params处运行,结束后返回继续运行mrs r0, cpsr指令。我们先看标号为save_boot_params处的代码:
/*************************************************************************
*
* void save_boot_params(u32 r0, u32 r1, u32 r2, u32 r3)
* __attribute__((weak));
*
* Stack pointer is not yet initialized at this moment
* Don't save anything to stack even if compiled with -O0
*
*************************************************************************/
ENTRY(save_boot_params)
bx lr @ back to my caller
ENDPROC(save_boot_params)
.weak save_boot_params
这里其实什么也没做,直接就返回到之前跳转的地方,bx是带模式的跳转,lr寄存器就保存了刚才跳转指令的下一条指令地址。在其他芯片可能会有一些参数传递到BootLoader当中,需要在这里做一些事情,我们这里没用到就不管。
这里有两个宏 ENTRY 和 ENDPROC 均在/include/linux/linkage.h头文件中定义,我们根据定义将这两个宏展开:
#define SYMBOL_NAME_STR(X) #X
#define SYMBOL_NAME(X) X
#ifdef __STDC__
#define SYMBOL_NAME_LABEL(X) X##:
#else
#define SYMBOL_NAME_LABEL(X) X:
#endif
#ifndef __ALIGN
#define __ALIGN .align 4
#endif
#ifndef __ALIGN_STR
#define __ALIGN_STR ".align 4"
#endif
#ifdef __ASSEMBLY__
#define ALIGN __ALIGN
#define ALIGN_STR __ALIGN_STR
#define LENTRY(name) \
ALIGN; \
SYMBOL_NAME_LABEL(name)
#define ENTRY(name) \
.globl SYMBOL_NAME(name); \
LENTRY(name)
#ifndef END
#define END(name) \
.size name, .-name
#endif
#ifndef ENDPROC
#define ENDPROC(name) \
.type name STT_FUNC; \
END(name)
#endif
展开后:
.globl save_boot_params; //输出一个全局标号
.align 4; //4字节对齐
save_boot_params;
bx lr @ back to my caller
.type save_boot_params STT_FUNC; //给链接器说明这是一个函数
.size save_boot_params, .-save_boot_params //计算函数子过程大小 .当前地址 减去 save_boot_params地址 得到代码段大小
.weak save_boot_params //.weak是一个弱标号,如果其它地方有定义这个标号,这里的定义将会失效,当只有这一个标号时才定义
接下来我们继续分析代码:
reset:
bl save_boot_params
/*
* disable interrupts (FIQ and IRQ), also set the cpu to SVC32 mode,
* except if in HYP mode already
*/
mrs r0, cpsr //将CPSR寄存器中的值读出
bicne r0, r0, #0x1f //清除cpsr的低五位
orrne r0, r0, #0x13 //设置cpsr的低五位为10011
orr r0, r0, #0xc0 //失能irq中断和fiq中断
msr cpsr,r0 //写入数据到cpsr寄存器
关于cpsr寄存器详细信息可以参考《ARM架构参考手册(ARM Architecture Reference Manual)》,这个规则由arm公司制定,cpsr的低五位为模式位,不同模式需写入不同的值,这里将其设置为svc模式。cpsr的第七位为irq中断使能位,第六位为fiq中断使能位,这里将irq和fiq中断禁用。
/*
* Setup vector:
* (OMAP4 spl TEXT_BASE is not 32 byte aligned.
* Continue to use ROM code vector only in OMAP4 spl)
*/
#if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD))
/* Set V=0 in CP15 SCTRL register - for VBAR to point to vector */
mrc p15, 0, r0, c1, c0v , 0 @ Read CP15 SCTRL Register
bic r0, #CR_V @ V = 0
mcr p15, 0, r0, c1, c0, 0 @ Write CP15 SCTRL Register
/* Set vector address in CP15 VBAR register */
ldr r0, =_start
mcr p15, 0, r0, c12, c0, 0 @Set VBAR
#endif
这里又是一个比较大的概念,这里主要操作是将中断向量表进行重定位,但具体是如何操作的呢;我们需要了解cp15协处理器,这个处理器包含了几十个寄存器,具体的操作语法也得查看《ARM架构参考手册》。当然我们的协处理器不止cp15,还有cp14、cp13~~等等。
mrc p15, 0, r0, c1, c0v , 0 :这句指令是将cp15中的SCTRL寄存器中的值读出来存放到r0中。
SCTRL寄存器全称为(system controller register)系统控制寄存器,他的第13位控制异常向量表的地址,如果此位为0将异常向量表地址设为默认的0x00000000;如果此位为1将异常向量表地址映射到0xffff0000;
bic r0, #CR_V :CR_V是宏定义,展开为(1 << 13),这句指令是将第13位清零,也就是将中断向量表地址设为0x00000000。
mcr p15, 0, r0, c1, c0, 0:将r0写入SCTRL寄存器。
接下来我们看后面两句
ldr r0, =_start
mcr p15, 0, r0, c12, c0, 0
c12寄存器的描述是如果中断向量表没有被映射到0xffff0000,那么还有一种更高级的映射,可以将中断向量表映射到任意位置,只需要设置这个寄存器即可。假设我们将代码搬运到SDRAM中的0x34800000中去运行,那么_start的值就为0x34800000,发生异常后,就会跳转到这个地址去执行异常处理。
/* the mask ROM code should have PLL and others stable */
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
bl cpu_init_cp15
bl cpu_init_crit
#endif
这里主要是在做底层的初始化工作,为什么要用宏定义隔开呢,之前我们讲过会有两端BootLoader代码,BL1是在片内sram中运行的,BL2是在SDRAM中运行,那么这里其实是在编写两份代码,因为BL1已经做过一次初始化了,所有当运行BL2时,不需要再去进行重复的初始化工作。
接下来我们跳转到 cpu_init_cp15处,看看具体做了些什么。
/*************************************************************************
*
* cpu_init_cp15
*
* Setup CP15 registers (cache, MMU, TLBs). The I-cache is turned on unless
* CONFIG_SYS_ICACHE_OFF is defined.
*
*************************************************************************/
ENTRY(cpu_init_cp15)
/*
* 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
mcr p15, 0, r0, c7, c5, 6 @ invalidate BP array
mcr p15, 0, r0, c7, c10, 4 @ DSB
mcr p15, 0, r0, c7, c5, 4 @ ISB
/*
* disable MMU stuff and caches
*/
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #0x00002000 @ clear bits 13 (--V-) //异常向量表地址0x00000000
bic r0, r0, #0x00000007 @ clear bits 2:0 (-CAM) //C - cache A - align M - MMU 0 失能 1 使能
orr r0, r0, #0x00000002 @ set bit 1 (--A-) Align //使能对齐检查
orr r0, r0, #0x00000800 @ set bit 11 (Z---) BTB //使能分支预测
#ifdef CONFIG_SYS_ICACHE_OFF
bic r0, r0, #0x00001000 @ clear bit 12 (I) I-cache //失能cache
#else
orr r0, r0, #0x00001000 @ set bit 12 (I) I-cache //使能cache
#endif
mcr p15, 0, r0, c1, c0, 0 //写入数据
mov pc, lr @ back to my caller //跳回
ENDPROC(cpu_init_cp15)
这里主要做了两件事,一个是对ICache的操作,另一个是对mmu进行操作。
那么再讲这部分代码之前我们先大致讲一下计算机体系结构:在计算机工业的早期我们一般是采用的哈佛结构,这个结构的特点是将数据和代码分开存放,代码被烧录到ROM中,数据被保存到RAM中运行。在1944年IBM公司为哈佛大学制造的Mark I计算机就采用的这种存储架构,因此被命名为哈佛结构。
哈佛结构的缺点是数据和代码分开存放,那么就需要更多的数据总线和地址总线去进行交互,这个对当时的计算机来说是一个很大的负担。
后面就有个名为冯诺依曼的人改进了这个体系结构,他提出了代码其实也是一种数据的理念,因此将代码也可以放到RAM中去,就诞生了后来的冯诺依曼体系结构,代码和数据共同存放。
冯诺依曼结构也有一定的缺点,由于数据和代码是在同一个内存里面存放的,所以当需要读取代码时会占用一次数据总线和地址总线,当读完代码后发现有需要读数据又需要单独发起一次命令去占用数据总线和地址总线,此时运行的效率就会变得很慢。
对于这种情况呢就引入了cache(高速缓存),CPU和RAM直接加入cache过后实际读取方式和之前一样,但由于cache的运行速率比RAM快特别多,所以在速度上整体会有特别大的提升。但是随着CPU速率的高速发展,这样的效率还是太低,于是在之前的基础上又做了一些改进。
将芯片内部用哈佛结构增加两个cache,一个为ICache用于缓存代码,另一个为DCache用于缓存数据,这两个缓存存在芯片内部,所以并不担心总线过多变得复杂的问题,这样操作我们的数据吞吐量就增加了将近一倍。
我们继续回到代码的讲解,这部分初始化代码将ICache关闭,因为在这个阶段我们还为严格区分代码段数据段,这里面的值还会被我们修改,如果将缓存打开,那么cpu所获取到的值将不会是最新的值。
mcr p15, 0, r0, c8, c7, 0 @ invalidate TLBs TLB是转换旁路缓冲器,他也是一种缓存,用来缓存虚拟地址到物理地址直接的转换规则。这句指令关闭TLB。
mcr p15, 0, r0, c7, c5, 0 @ invalidate icache 关闭ICache。
mcr p15, 0, r0, c7, c5, 6 @ invalidate BP array 使分支预测无效。
mcr p15, 0, r0, c7, c10, 4 @ DSB 这条指令相当于一条DSB指令,DSB指令的作用是在程序中插入一个障碍点,以确保在该点之前的所有指令都完成,且其修改的数据已经在所有处理器核心中可见。这样可以确保指令的执行顺序和数据的一致性。用于多核的处理器。
mcr p15, 0, r0, c7, c5, 4 @ ISB 这条指令相当于一条ISB指令,是和DSB指令差不多的作用,用于数据的同步。
bic r0, r0, #0x00002000 @ clear bits 13 (–V-) 前面讲过SCTRL寄存器的第13位是写0是将异常向量表地址设为0x00000000, 如果设置为1将映射到0xffff0000。
bic r0, r0, #0x00000007 @ clear bits 2:0 (-CAM) 这里将低三位都设置为0, C意思是cache缓存,写0关闭;A表示align字节对齐,写0关闭,不进行字节对齐检查;M意思是MMU,写0关闭mmu。
orr r0, r0, #0x00000002 @ set bit 1 (–A-) Align 这里将字节对齐检查打开。
orr r0, r0, #0x00000800 @ set bit 11 (Z—) BTB BTB是分支预测,这里将该功能打开。
mov pc, lr @ back to my caller 最后跳转回 bl cpu_init_cp15 处继续运行下一行代码。
下一句是 bl cpu_init_crit 这个标号未在start.S这个文件,我们在lowlevel.S文件里面去查看这个标号的定义
ENTRY(lowlevel_init)
/*
* Setup a temporary stack
*/
ldr sp, =CONFIG_SYS_INIT_SP_ADDR
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
#ifdef CONFIG_SPL_BUILD
ldr r9, =gdata
#else
sub sp, #GD_SIZE
bic sp, sp, #7
mov r9, sp
#endif
/*
* Save the old lr(passed in ip) and the current lr to stack
*/
push {ip, lr}
/*
* go setup pll, mux, memory
*/
bl s_init
pop {ip, pc}
ENDPROC(lowlevel_init)
这段代码主要一些低级初始化 ,首先设置一个临时的栈 ,再设置栈的字节对齐,最后调用s_init这个函数继续做一些初始化工作。这就是start.S文件做的全部工作,每个UBoot版本会有所不同,但内容基本都是一致的。
接下来我们继续分析剩余部分的代码。