我们知道,bootloader是系统上电后最初加载运行的代码。它提供了处理器上电复位后最开始需要执行的初始化代码。
在PC机上引导程序一般由BIOS开始执行,然后读取硬盘中位于MBR(Main Boot Record,主引导记录)中的Bootloader(例如LILO或GRUB),并进一步引导操作系统的启动。
然而在嵌入式系统中通常没有像BIOS那样的固件程序,因此整个系统的加载启动就完全由bootloader来完成。它主要的功能是加载与引导内核映像
一个嵌入式的存储设备通过通常包括四个分区:
第一分区:存放的当然是u-boot
第二个分区:存放着u-boot要传给系统内核的参数
第三个分区:是系统内核(kernel)
第四个分区:则是根文件系统
如下图所示:
在本人的开发板,进入/sys/class/mtd/
mtd0 mtd1 mtd2 mtd3 mtd4 mtd5
mtd0ro mtd1ro mtd2ro mtd3ro mtd4ro mtd5ro
划分成5个区:
cat mtd0/name U-Boot
cat mtd1/name U-Boot Env
cat mtd2/name U-Boot Logo
cat mtd3/name Kernel
cat mtd4/name File System
——————————————————————————————————————————————-
u-boot是一种普遍用于嵌入式系统中的Bootloader。
Bootloader介绍
Bootloader的定义:Bootloader是在操作系统运行之前执行的一小段程序,通过这一小段程序,我们可以初始化硬件设备、建立内存空间的映射表,从而建立适当的系统软硬件环境,为最终调用操作系统内核做好准备。意思就是说如果我们要想让一个操作系统在我们的板子上运转起来,我们就必须首先对我们的板子进行一些基本配置和初始化,然后才可以将操作系统引导进来运行。具体在Bootloader中完成了哪些操作我们会在后面分析到,这里我们先来回忆一下PC的体系结构:PC机中的引导加载程序是由BIOS和位于硬盘MBR中的OS Boot Loader(比如LILO和GRUB等)一起组成的,BIOS在完成硬件检测和资源分配后,将硬盘MBR中的Boot Loader读到系统的RAM中,然后将控制权交给OS Boot Loader。Boot Loader的主要运行任务就是将内核映象从硬盘上读到RAM中,然后跳转到内核的入口点去运行,即开始启动操作系统。在嵌入式系统中,通常并没有像BIOS那样的固件程序(注:有的嵌入式cpu也会内嵌一段短小的启动程序),因此整个系统的加载启动任务就完全由Boot Loader来完成。比如在一个基于ARM7TDMI core的嵌入式系统中,系统在上电或复位时通常都从地址0x00000000处开始执行,而在这个地址处安排的通常就是系统的Boot Loader程序。(先想一下,通用PC和嵌入式系统为何会在此处存在如此的差异呢?)
Bootloader是基于特定硬件平台来实现的,因此几乎不可能为所有的嵌入式系统建立一个通用的Bootloader,不同的处理器架构都有不同的Bootloader,Bootloader不但依赖于cpu的体系结构,还依赖于嵌入式系统板级设备的配置。对于2块不同的板子而言,即使他们使用的是相同的处理器,要想让运行在一块板子上的Bootloader程序也能运行在另一块板子上,一般也需要修改Bootloader的源程序。
Bootloader的启动方式
Bootloader的启动方式主要有网络启动方式、磁盘启动方式和Flash启动方式。
1、网络启动方式
图1 Bootloader网络启动方式示意图
如图1所示,里面主机和目标板,他们中间通过网络来连接,首先目标板的DHCP/BIOS通过BOOTP服务来为Bootloader分配IP地址,配置网络参数,这样才能支持网络传输功能。我们使用的u-boot可以直接设置网络参数,因此这里就不用使用DHCP的方式动态分配IP了。接下来目标板的Bootloader通过TFTP服务将内核映像下载到目标板上,然后通过网络文件系统来建立主机与目标板之间的文件通信过程,之后的系统更新通常也是使用Boot Loader的这种工作模式。工作于这种模式下的Boot Loader通常都会向它的终端用户提供一个简单的命令行接口。
2、磁盘启动方式
这种方式主要是用在台式机和服务器上的,这些计算机都使用BIOS引导,并且使用磁盘作为存储介质,这里面两个重要的用来启动linux的有LILO和GRUB,这里就不再具体说明了。
3、Flash启动方式
这是我们最常用的方式。Flash有NOR Flash和NAND Flash两种。NOR Flash可以支持随机访问,所以代码可以直接在Flash上执行,Bootloader一般是存储在Flash芯片上的。另外Flash上还存储着参数、内核映像和文件系统。这种启动方式与网络启动方式之间的不同之处就在于,在网络启动方式中,内核映像和文件系统首先是放在主机上的,然后经过网络传输下载进目标板的,而这种启动方式中内核映像和文件系统则直接是放在Flash中的,这两点在我们u-boot的使用过程中都用到了。
U-boot的定义
U-boot,全称Universal Boot Loader,是由DENX小组的开发的遵循GPL条款的开放源码项目,它的主要功能是完成硬件设备初始化、操作系统代码搬运,并提供一个控制台及一个指令集在操作系统运行前操控硬件设备。U-boot之所以这么通用,原因是他具有很多特点:开放源代码、支持多种嵌入式操作系统内核、支持多种处理器系列、较高的稳定性、高度灵活的功能设置、丰富的设备驱动源码以及较为丰富的开发调试文档与强大的网络技术支持。另外u-boot对操作系统和产品研发提供了灵活丰富的支持,主要表现在:可以引导压缩或非压缩系统内核,可以灵活设置/传递多个关键参数给操作系统,适合系统在不同开发阶段的调试要求与产品发布,支持多种文件系统,支持多种目标板环境参数存储介质,采用CRC32校验,可校验内核及镜像文件是否完好,提供多种控制台接口,使用户可以在不需要ICE的情况下通过串口/以太网/USB等接口下载数据并烧录到存储设备中去(这个功能在实际的产品中是很实用的,尤其是在软件现场升级的时候),以及提供丰富的设备驱动等。
U-boot源代码的目录结构
board 中存放于开发板相关的配置文件,每一个开发板都以子文件夹的形式出现。
Commom 文件夹实现u-boot行下支持的命令,每一个命令对应一个文件。
cpu 中存放特定cpu架构相关的目录,每一款cpu架构都对应了一个子目录。
Doc 是文档目录,有u-boot非常完善的文档。
Drivers 中是u-boot支持的各种设备的驱动程序。
Fs 是支持的文件系统,其中最常用的是JFFS2文件系统。
Include 文件夹是u-boot使用的头文件,还有各种硬件平台支持的汇编文件,系统配置文件和文件系统支持的文件。
Net 是与网络协议相关的代码,bootp协议、TFTP协议、NFS文件系统得实现。
Tooles 是生成U-boot的工具。
对u-boot的目录有了一些了解后,分析启动代码的过程就方便多了,其中比较重要的目录就是/board、/cpu、/drivers和/include目录,如果想实现u-boot在一个平台上的移植,就要对这些目录进行深入的分析。
——————————————————————————————————————————————-
什么是《编译地址》?什么是《运行地址》?
1. 编译地址:32位的处理器,它的每一条指令是4个字节,以4个字节存储顺序,进行顺序执行,CPU是顺序执行的,只要没发生什么跳转,它会顺序进行执行行, 编译器会对每一条指令分配一个编译地址,这是编译器分配的,在编译过程中分配的地址,我们称之为编译地址。
2. 运行地址:是指程序指令真正运行的地址,是由用户指定的,用户将运行地址烧录到哪里,哪里就是运行的地址。
比如有一个指令的编译地址是0x5,实际运行的地址是0x200,如果用户将指令烧到0x200上,那么这条指令的运行地址就是0x200,
当编译地址和运行地址不同的时候会出现什么结果?结果是不能跳转,编译后会产生跳转地址,如果实际地址和编译后产生的地址不相等,那么就不能跳转。
C语言编译地址:都希望把编译地址和实际运行地址放在一起的,但是汇编代码因为不需要做C语言到汇编的转换,可以认为的去写地址,所以直接写的就是他的运行地址这就是为什么任何bootloader刚开始会有一段汇编代码,因为起始代码编译地址和实际地址不相等,这段代码和汇编无关,跳转用的运行地址。
编译地址和运行地址如何来算呢?
1.假如有两个编译地址a=0x10,b=0x7,b的运行地址是0x300,那么a的运行地址就是b的运行地址加上两者编译地址的差值,a-b=0x10-0x7=0x3,a的运行地址就是0x300+0x3=0x303。
2.假设uboot上两条指令的编译地址为a=0x33000007和b=0x33000001,这两条指令都落在bank6上,现在要计算出他们对应的运行地址,要找出运行地址的始地址,这个是由用户烧录进去的,假设运行地址的首地址是0x0,则a的运行地址为0x7,b为0x1,就是这样算出来的。
为什么要分配编译地址?这样做有什么好处,有什么作用?
比如在函数a中定义了函数b,当执行到函数b时要进行指令跳转,要跳转到b函数所对应的起始地址上去,编译时,编译器给每条指令都分配了编译地址,如果编译器已经给分配了地址就可以直接进行跳转,查找b函数跳转指令所对应的表,进行直接跳转,因为有个编译地址和指令对应的一个表,如果没有分配,编译器就查找不到这个跳转地址,要进行计算,非常麻烦。
什么是《相对地址》?
以NOR Flash为例,NOR Falsh是映射到bank0上面,SDRAM是映射到bank6上面,uboot和内核最终是在SDRAM上面运行,最开始我们是从Nor Flash的零地址开始往后烧录,uboot中至少有一段代码编译地址和运行地址是不一样的,编译uboot或内核时,都会将编译地址放入到SDRAM中,他们最终都会在SDRAM中执行,刚开始uboot在Nor Flash中运行,运行地址是一个低端地址,是bank0中的一个地址,但编译地址是bank6中的地址,这样就会导致绝对跳转指令执行的失败,所以就引出了相对地址的概念。
那么什么是相对地址呢?
至少在bank0中uboot这段代码要知道不能用b+编译地址这样的方法去跳转指令,因为这段代码的编译地址和运行地址不一样,那如何去做呢?
要去计算这个指令运行的真实地址,计算出来后再做跳转,应该是b+运行地址,不能出现b+编译地址,而是b+运行地址,而运行地址是算出来的。
——————————————————————————————————————————————-
U-Boot工作过程
大多数 Boot Loader 都包含两种不同的操作模式:”启动加载”模式和”下载”模式,这种区别仅对于开发人员才有意义。
但从最终用户的角度看,Boot Loader 的作用就是:用来加载操作系统,而并不存在所谓的启动加载模式与下载工作模式的区别。
(一)启动加载(Boot loading)模式:这种模式也称为”自主”(Autonomous)模式。
也即 Boot Loader 从目标机上的某个固态存储设备上将操作系统加载到 RAM 中运行,整个过程并没有用户的介入。
这种模式是 Boot Loader 的正常工作模式,因此在嵌入式产品发布的时侯,Boot Loader 显然必须工作在这种模式下。
(二)下载(Downloading)模式:在这种模式下,目标机上的 Boot Loader 将通过串口连接或网络连接等通信手段从主机(Host)下载文件,比如:下载内核映像和根文件系统映像等。从主机下载的文件通常首先被 Boot Loader保存到目标机的RAM 中,然后再被 BootLoader写到目标机上的FLASH类固态存储设备中。Boot Loader 的这种模式通常在第一次安装内核与根文件系统时被使用;此外,以后的系统更新也会使用 Boot Loader 的这种工作模式。工作于这种模式下的 Boot Loader 通常都会向它的终端用户提供一个简单的命令行接口。这种工作模式通常在第一次安装内核与跟文件系统时使用。或者在系统更新时使用。进行嵌入式系统调试时一般也让bootloader工作在这一模式下。
UBoot 这样功能强大的 Boot Loader 同时支持这两种工作模式,而且允许用户在这两种工作模式之间进行切换。
大多数 bootloader 都分为阶段 1(stage1)和阶段 2(stage2)两大部分,uboot 也不例外。依赖于 CPU 体系结构的代码(如 CPU 初始化代码等)通常都放在阶段 1 中且通常用汇编语言实现,而阶段 2 则通常用 C 语言来实现,这样可以实现复杂的功能,而且有更好的可读性和移植性。
**********************************************************第二、总体分析*******************************************************************
系统启动的入口点。既然我们现在要分析u-boot的启动过程,就必须先找到u-boot最先实现的是哪些代码,最先完成的是哪些任务。
另一方面一个可执行的image必须有一个入口点,并且只能有一个全局入口点,所以要通知编译器这个入口在哪里。由此我们可以找到程序的入口点是在/board/ti/ti8168_dvr/u-boot.lds中指定的,其中ENTRY(_start)说明程序从_start开始运行,而他指向的是cpu/arm_cortexa8/start.o文件。
因为我们用的是cortex-a8的cpu架构,在复位后从地址0x00000000取它的第一条指令,所以我们将Flash映射到这个地址上,这样在系统加电后,cpu将首先执行u-boot程序。u-boot的启动过程是多阶段实现的,分了两个阶段。依赖于cpu体系结构的代码(如设备初始化代码等)通常都放在stage1中,而且通常都是用汇编语言来实现,以达到短小精悍的目的。而stage2则通常是用C语言来实现的,这样可以实现复杂的功能,而且代码具有更好的可读性和可移植性。
U-Boot启动内核的过程可以分为两个阶段,两个阶段的功能如下:
(1)第一阶段的功能
Ø 硬件设备初始化
Ø 加载U-Boot第二阶段代码到RAM空间
Ø 设置好栈
Ø 跳转到第二阶段代码入口
(2)第二阶段的功能
Ø 初始化本阶段使用的硬件设备
Ø 检测系统内存映射
Ø 将内核从Flash读取到RAM中
Ø 为内核设置启动参数
Ø 调用内核
代码真正开始是在_start,设置异常向量表,这样在cpu发生异常时就跳转到/arch/arm/lib/interrupts中去执行相应得中断代码。
在interrupts文件中大部分的异常代码都没有实现具体的功能,只是打印一些异常消息,其中关键的是reset中断代码,跳到reset入口地址。
reset复位入口之前有一些段的声明。
因为我们用的是 cortex-a8 的 cpu 架构,在CPU复位后从iROM地址0x00000000取它的第一条指令,执行iROM代码的功能是把flash中的前16K的代码加载到iRAM中,系统上电后将首先执行 u-boot 程序。
1.stage1:cpu/arm_cortexa8/start.S
2.当系统启动时, ARM CPU 会跳到 0x00000000去执行,一般 BootLoader 包括如下几个部分:
1> 建立异常向量表
2> 显示的切换到 SVC 且 32 指令模式
3> 设置异常向量表
4> 关闭 TLB,MMU,cache,刷新指令 cache 数据 cache
5> 关闭内部看门狗
6> 禁止所有的中断
7> 串口初始化
8> tzpc(TrustZone Protection Controller)
9> 配置系统时钟频率和总线频率
10> 设置内存区的控制寄存器
11> 设置堆栈
12> 代码的搬移阶段
代码的搬移阶段:为了获得更快的执行速度,通常把stage2加载到RAM空间中来执行,因此必须为加载Boot Loader的stage2准备好一段可用的RAM空间范围。空间大小最好是memory page大小(通常是4KB)的倍数,一般而言,1M的RAM空间已经足够了。
flash中存储的u-boot可执行文件中,代码段、数据段以及BSS段都是首尾相连存储的,所以在计算搬移大小的时候就是利用了用BSS段的首地址减去代码的首地址,这样算出来的就是实际使用的空间。
程序用一个循环将代码搬移到0x81180000,即RAM底端1M空间用来存储代码。然后程序继续将中断向量表搬到RAM的顶端。
由于stage2通常是C语言执行代码,所以还要建立堆栈去。在堆栈区之前还要将malloc分配的空间以及全局数据所需的空间空下来,他们的大小是由宏定义给出的,可以在相应位置修改。
13> 跳到 C 代码部分执行
基本内存分布图:
图3 搬移后内存分布情况图
3. 下来是u-boot启动的第二个阶段,是用c代码写的,
这部分是一些相对变化不大的部分,我们针对不同的板子改变它调用的一些初始化函数,并且通过设置一些宏定义来改变初始化的流程,
所以这些代码在移植的过程中并不需要修改,也是错误相对较少出现的文件。
在文件的开始先是定义了一个函数指针数组,通过这个数组,程序通过一个循环来按顺序进行常规的初始化,并在其后通过一些宏定义来初始化一些特定的设备。
在最后程序进入一个循环,main_loop。这个循环接收用户输入的命令,以设置参数或者进行启动引导。
**********************************************************第二、代码分析*******************************************************************
1 定义入口
由于一个可执行的 Image 必须有一个入口点,并且只能有一个全局入口,通常这个入口放在 ROM(Flash)的 0x0
地址,因此,必须通知编译器以使其知道这个入口,该工作可通过修改连接器脚本来完成。
1. board/ti/ti8168_dvr/uboot.lds: ENTRY(_start) ==> arch/arm/cpu/cortex-a8/start.S: .globl _start
2. uboot 代码区(TEXT_BASE = 0x08070000)定义在 board/ti/ti8168_dvr/config.mk
第一阶段对应的文件是cpu/arm920t/start.S和board/samsung/mini2440/lowlevel_init.S。
U-Boot启动第一阶段流程如下:
详细分析
图 2.1 U-Boot启动第一阶段流程
根据cpu/cortex_a8/u-boot.lds中指定的连接方式:
看一下uboot.lds文件,在board/ti/ti8168_dvr/目录下面,uboot.lds是告诉编译器这些段改怎么划分,GUN编译过的段,最基本的三个段是RO,RW,ZI,RO表示只读,对应于具体的指代码段,RW是数据段,ZI是归零段,就是全局变量的那段。Uboot代码这么多,如何保证start.s会第一个执行,编译在最开始呢?就是通过uboot.lds链接文件进行
OUTPUT_FORMAT(“elf32-littlearm”, “elf32-littlearm”, “elf32-littlearm”)
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
. = 0x00000000;//起始地址
. = ALIGN(4);//4字节对齐
.text : //test指代码段,上面3行标识是不占用任何空间的
{
arch/arm/cpu/arm_cortexa8/start.o (.text) //这里把start.o放在第一位就表示把start.s编译时放到最开始,这就是为什么把uboot烧到起始地址上它肯定运行的是start.s
arch/arm/cpu/arm_cortexa8/ti81xx/lowlevel_init.o (.text)
*(.text)
}
. = ALIGN(4);//前面的 “.” 代表当前值,是计算一个当前的值,是计算上面占用的整个空间,再加一个单元就表示它现在的位置
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
.got : { *(.got) }
. = .;
__u_boot_cmd_start = .;
.u_boot_cmd : { *(.u_boot_cmd) }
__u_boot_cmd_end = .;
. = ALIGN(4);
__bss_start = .;
.bss (NOLOAD) : { *(.bss) . = ALIGN(4); }
_end = .;
}
OUTPUT_FORMAT(“elf32-littlearm”, “elf32-littlearm”, “elf32-littlearm”)
/*OUTPUT_FORMAT(“elf32-arm”, “elf32-arm”, “elf32-arm”)*/
OUTPUT_ARCH(arm)
ENTRY(_start)
第一个链接的是cpu/arm_cortexa8/start.o,因此u-boot.bin的入口代码在cpu/arm_cortexa8/start.o中,其源代码在cpu/arm_cortexa8/start.S中。下面我们来分析cpu/arm_cortexa8/start.S的执行。
1. 硬件设备初始化
———————————————————————————————————————————
(1)设置异常向量
下面代码是系统启动后U-boot上电后运行的第一段代码,它是什么意思?
u-boot对应的第一阶段代码放在cpu/arm_cortexa8/start.S文件中,入口代码如下:.
.globl _start /*声明一个符号可被其它文件引用,相当于声明了一个全局变量,.globl与.global相同*/
_start: b reset /* 复位 */b是不带返回的跳转(bl是带返回的跳转),意思是无条件直接跳转到reset标号出执行程序
ldr pc, _undefined_instruction /* 未定义指令向量 l—dr相当于mov操作*/
ldr pc, _software_interrupt /* 软件中断向量 */
ldr pc, _prefetch_abort /* 预取指令异常向量 */
ldr pc, _data_abort /* 数据操作异常向量 */
ldr pc, _not_used /* 未使用 */
ldr pc, _irq /* irq中断向量 */
ldr pc, _fiq /* fiq中断向量 */
/* 中断向量表入口地址 */
_undefined_instruction: .word undefined_instruction /*就是在当前地址,即_undefined_instruction 处存放 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 //word伪操作用于分配一段字内存单元(分配的单元都是字对齐的),并用伪操作中的expr初始化
.balignl 16,0xdeadbeef
他们是系统定义的异常,一上电程序跳转到reset异常处执行相应的汇编指令,下面定义出的都是不同的异常,比如软件发生软中断时,CPU就会去执行软中断的指令,这些异常中断在CUP中地址是从0开始,每个异常占4个字节
ldr pc, _undefined_instruction表示把_undefined_instruction存放的数值存放到pc指针上
_undefined_instruction: .word undefined_instruction表示未定义的这个异常是由.word来定义的,它表示定义一个字,一个32位的数
.word后面的数:表示把该标识的编译地址写入当前地址,标识是不占用任何指令的。把标识存放的数值copy到指针pc上面,那么标识上存放的值是什么?
是由.word undefined_instruction来指定的,pc就代表你运行代码的地址,她就实现了CPU要做一次跳转时的工作。
以上代码设置了ARM异常向量表,各个异常向量介绍如下:
表 2.1 ARM异常向量表
在cpu/arm_cortexa8/start.S中还有这些异常对应的异常处理程序。当一个异常产生时,CPU根据异常号在异常向量表中找到对应的异常向量,然后执行异常向量处的跳转指令,CPU就跳转到对应的异常处理程序执行。
其中复位异常向量的指令“b reset”决定了U-Boot启动后将自动跳转到标号“reset”处执行。
_TEXT_BASE:
.word TEXT_BASE //0x07080000,在board/ti/ti8168_dvr/config.mk中,这段话表示,用户告诉编译器编译地址的起始地址
.globl _armboot_start
_armboot_start:
.word _start /*_start 是uboot的第一行代码的标号,代表的是第一行代码的地址*/
.globl _bss_start
_bss_start:
.word __bss_start //在cpu/arm_cortexa8/u-boot.lds中定义
.globl _bss_end
_bss_end:
.word _end //在cpu/arm_cortexa8/u-boot.lds中定义
#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
——————————————————————————————————————-
(2)CPU进入SVC模式
reset:
/*
* set the cpu to SVC32 mode
*/
mrs r0, cpsr
bic r0, r0, #0x1f /*工作模式位清零 */
orr r0, r0, #0xd3 /*工作模式位设置为“10011”(管理模式),并将中断禁止位和快中断禁止位置1 */
msr cpsr, r0
以上代码将CPU的工作模式位设置为管理模式,即设置相应的CPSR程序状态字,并将中断禁止位和快中断禁止位置一,从而屏蔽了IRQ和FIQ中断。
操作系统先注册一个总的中断,然后去查是由哪个中断源产生的中断,再去查用户注册的中断表,查出来后就去执行用户定义的用户中断处理函数。
#if (CONFIG_OMAP34XX) // 这个宏没有定义,下面的代码不会预编译
/* Copy vectors to mask ROM indirect addr */
adr r0, _start @ r0 <- current position of code
add r0, r0, #4 @ skip reset vector
mov r2, #64 @ r2 <- size to copy
add r2, r0, r2 @ r2 <- source end address
mov r1, #SRAM_OFFSET0 @ build vect addr
mov r3, #SRAM_OFFSET1
add r1, r1, r3
mov r3, #SRAM_OFFSET2
add r1, r1, r3
next:
ldmia r0!, {r3 – r10} @ copy from source address [r0]
stmia r1!, {r3 – r10} @ copy to target address [r1]
cmp r0, r2 @ until source end address [r2]
bne next @ loop until equal */
#if !defined(CONFIG_SYS_NAND_BOOT) && !defined(CONFIG_SYS_ONENAND_BOOT)
/* No need to copy/exec the clock code – DPLL adjust already done
* in NAND/oneNAND Boot.
*/
bl cpy_clk_code @ put dpll adjust code behind vectors
#endif /* NAND Boot */
#endif
—————————————————————————————————————–
(3)硬件初始化
/* the mask ROM code should have PLL and others stable */
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
bl cpu_init_crit
#endif
cpu_init_crit这段代码在U-Boot正常启动时才需要执行,若将U-Boot从RAM中启动则应该注释掉这段代码。
下面分析一下cpu_init_crit到底做了什么:
/*
*************************************************************************
*
* CPU_init_critical registers
*
* setup important registers
* setup memory timing
*
*************************************************************************
*/
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
cpu_init_crit:
/*
* flush v4 I/D caches
*/
mov r0, #0
mcr p15, 0, r0, c7, c7, 0 /* flush v3/v4 cache */ //将0写入c7,使Cache内容无效
mcr p15, 0, r0, c8, c7, 0 /* flush v4 TLB */ //将0写入c8,使TLB内容无效
/*
* disable MMU stuff and caches
*/
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #0x00002300 /* clear bits 13, 9:8 (–V- –RS) */
bic r0, r0, #0x00000087 /* clear bits 7, 2:0 (B— -CAM) */
orr r0, r0, #0x00000002 /* set bit 2 (A) Align */
orr r0, r0, #0x00001000 /* set bit 12 (I) I-Cache */
mcr p15, 0, r0, c1, c0, 0 //修改CP15的c1寄存器来实现关闭MMU
/*
* Go setup Memory and board specific bits prior to relocation.
*/
mov ip, lr /* perserve link reg across call */
bl lowlevel_init /* go setup pll,mux,memory */
mov lr, ip /* restore link */
mov pc, lr /* back to my caller */
#endif /* CONFIG_SKIP_LOWLEVEL_INIT */
代码中的c0,c1,c7,c8都是cortex-a8的协处理器CP15的寄存器。其中c7是cache控制寄存器,c8是TLB控制寄存器。325~327行代码将0写入c7、c8,使Cache,TLB内容无效。
第332~337行代码关闭了MMU。这是通过修改CP15的c1寄存器来实现的,先看CP15的c1寄存器的格式(仅列出代码中用到的位):
表 2.3 CP15的c1寄存器格式(部分)
为什么要关闭catch和MMU呢?catch和MMU是做什么用的?
MMU是Memory Management Unit的缩写,中文名是内存管理单元,它是中央处理器(CPU)中用来管理虚拟存储器、物理存储器的控制线路, 同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权
概述:
一,关catch
catch和MMU是通过CP15管理的,刚上电的时候,CPU还不能管理他们
上电的时候MMU必须关闭,指令catch可关闭,可不关闭,但数据catch一定要关闭
否则可能导致刚开始的代码里面,去取数据的时候,从catch里面取,而这时候RAM中数据还没有catch过来,导致数据预取异常
二:关MMU
因为MMU是;把虚拟地址转化为物理地址得作用
而目的是设置控制寄存器,而控制寄存器本来就是实地址(物理地址),再使能MMU,不就是多此一举了吗?
详细分析—
Catch是cpu内部的一个2级缓存,它的作用是将常用的数据和指令放在cpu内部,MMU是用来把虚实地址转换为物理地址用的
我们的目的:是设置控制的寄存器,寄存器都是实地址(物理地址),如果既要开启MMU又要做虚实地址转换的话,中间还多一步,多此一举了嘛?
先要把实地址转换成虚地址,然后再做设置,但对uboot而言就是起到一个简单的初始化的作用和引导操作系统,如果开启MMU的话,很麻烦,也没必要,所以关闭MMU.
说到catch就必须提到一个关键字Volatile,以后在设置寄存器时会经常遇到,他的本质:是告诉编译器不要对我的代码进行优化,作用是让编写者感觉不倒变量的变化情况(也就是说,让它执行速度加快吧)
优化的过程:是将常用的代码取出来放到catch中,它没有从实际的物理地址去取,它直接从cpu的缓存中去取,但常用的代码就是为了感觉一些常用变量的变化
优化原因:如果正在取数据的时候发生跳变,那么就感觉不到变量的变化了,所以在这种情况下要用Volatile关键字告诉编译器不要做优化,每次从实际的物理地址中去取指令,这就是为什么关闭catch关闭MMU。
但在C语言中是不会关闭catch和MMU的,会打开,如果编写者要感觉外界变化,或变化太快,从catch中取数据会有误差,就加一个关键字Volatile。
初始化RAM控制寄存器
bl lowlevel_init下来初始化各个bank,把各个bank设置必须搞清楚,对以后移植复杂的uboot有很大帮助
设置完毕后拷贝uboot代码到4k空间,拷贝完毕后执行内存中的uboot代码
其中的lowlevel_init就完成了内存初始化的工作,由于内存初始化是依赖于开发板的,因此lowlevel_init的代码一般放在board下面相应的目录中。对于ti8168_dvr,lowlevel_init在arch/arm/arm_cortexa8/ti81xx/level_init.S中定义如下:
/*****************************************************************************
* lowlevel_init: – Platform low level init.
* Corrupted Register : r0, r1, r2, r3, r4, r5, r6
****************************************************************************/
.globl lowlevel_init
lowlevel_init:
/* The link register is saved in ip by start.S */
mov r6, ip
/* check if we are already running from RAM */ //判断程序是否已经在 RAM 中运行
ldr r2, _lowlevel_init
ldr r3, _TEXT_BASE
sub r4, r2, r3
sub r0, pc, r4
/* require dummy instr or subtract pc by 4 instead i’m doing stack init */
ldr sp, SRAM_STACK
mark1:
ldr r5, _mark1
sub r5, r5, r2 /* bytes between mark1 and lowlevel_init */
sub r0, r0, r5 /* r0 <- _start w.r.t current place of execution */
mov r10, #0x0 /* r10 has in_ddr used by s_init() */
#ifdef CONFIG_NOR_BOOT
cmp r0, #0x08000000 /* check for running from NOR */
beq ocmc_init_start /* if == then running from NOR */
ands r0, r0, #0xC0000000 /* MSB 2 bits <> 0 then we are in ocmc or DDR */
cmp r0, #0x40000000 /* if running from ocmc */
beq nor_init_start /* if == skip ocmc init and jump to nor init */
mov r10, #0x01 /* if <> we are running from DDR hence skip ddr init */
/* by setting in_ddr to 1 */
b s_init_start /* and jump to s_init */
#else
ands r0, r0, #0xC0000000 /* MSB 2 bits <> 0 then we are in ocmc or DDR */
cmp r0, #0x80000000
bne s_init_start
mov r10, #0x01
b s_init_start
#endif
#ifdef CONFIG_NOR_BOOT
ocmc_init_start:
/**** enable ocmc 0 ****/
/* CLKSTCTRL */
ldr r5, cm_alwon_ocmc_0_clkstctrl_addr
mov r2, #0x2
str r2, [r5]
/* wait for gpmc enable to settle */
ocmc0_wait0:
ldr r2, [r5]
ands r2, r2, #0x00000100
cmp r2, #0x00000100
bne ocmc0_wait0
/* CLKCTRL */
ldr r5, cm_alwon_ocmc_0_clkctrl_addr
mov r2, #0x2
str r2, [r5]
/* wait for gpmc enable to settle */
ocmc0_wait1:
ldr r2, [r5]
ands r2, r2, #0x00030000
cmp r2, #0
bne ocmc0_wait1
#ifdef CONFIG_TI816X
/**** enable ocmc 1 ****/
/* CLKSTCTRL */
ldr r5, cm_alwon_ocmc_1_clkstctrl_addr
mov r2, #0x2
str r2, [r5]
/* wait for gpmc enable to settle */
ocmc1_wait0:
ldr r2, [r5]
ands r2, r2, #0x00000100
cmp r2, #0x00000100
bne ocmc1_wait0
/* CLKCTRL */
ldr r5, cm_alwon_ocmc_1_clkctrl_addr
mov r2, #0x2
str r2, [r5]
/* wait for gpmc enable to settle */
ocmc1_wait1:
ldr r2, [r5]
ands r2, r2, #0x00030000
cmp r2, #0
bne ocmc1_wait1
#endif
nor_init_start:
/* gpmc init */
bl cpy_nor_gpmc_code /* copy nor gpmc init code to sram */
mov r0, pc
add r0, r0, #12 /* 12 is for next three instructions */
mov lr, r0 /* gpmc init code in sram should return to s_init_start */
ldr r0, sram_pc_start
mov pc, r0 /* transfer ctrl to nor_gpmc_init() in sram */
#endif
s_init_start:
mov r0, r10 /* passing in_ddr in r0 */
bl s_init // 调用C语言初始化系统时钟和MUX
/* back to arch calling code */
mov pc, r6
—————————————————————————————————————-
ti8168_dvr 开发板没有在这里进行重定位,但是我们还是要分析一下这里的重定位—-
#ifndef CONFIG_SKIP_RELOCATE_UBOOT
relocate: @ relocate U-Boot to RAM
adr r0, _start @ r0 <- current position of code
ldr r1, _TEXT_BASE @ test if we run from flash or RAM
cmp r0, r1 @ don’t reloc during debug
beq stack_setup
注释:
(1 ) 使用相对寻址,以 PC 值为基地址计算出当前代码的开始地址,通过反汇编u-boot.bin,_start在FLASH上代表地址:0x0000_06E0 ,指令 addr r0 ,_start地址为0x0000_0744 ,执行到该指令时,PC=0x0000_0744+8=0x0000_074c,这里,编译器将用指令SUB r0,PC,#6C ;因此 r0=0x0000_06E0,相当于把代码的开始地址送到了r0 中。事实上,在 0x0 地址,存在一条跳转指令,跳到了_start 标签处,而_start标签处才是 b reset 真正的跳转。所以,程序在 FLASH 上运行的真正 reset 跳转,是在_start=0x0000_06E0 地址对应的 b reset 指令。
(2 ) 把_TEXT_BASE 地址对应的内存里的值(TEXT_BASE) 送到r1 寄存器,这里是0x07080000
(3 ) 比较 r0 寄存器和 r1 寄存器的值。
(4 ) 如果 r0=r1,就跳去执行 stack_setup 程序段,设置内存中的栈空间。比较的目的就是看当前程序的运行是在内存里还是在FLASH里,如果是 debug 模式,那么 u-boot是在内存里运行,其开始地址就是TEXT_BASE ,即0x70800000 。上面的4 句代码,就是比较一下,看看当前程序代码在什么位置,如果已经在内存指定的0x07080000 这个位置了,就不必再进行重定位了,而直接进行堆栈设置。
ldr r2, _armboot_start
ldr r3, _bss_start
sub r2, r3, r2 @ r2 <- size of armboot
add r2, r0, r2 @ r2 <- source end address
注释:
(1 ) 把_armboot_start程序段的首地址读进 r2 寄存器。事实上,根据_armboot_start标号的定义:_armboot_star :.word _start,_armboot_start是内存地址,这个地址中存放着TEXT_BASE+_start,即 0x90708_06E0 。所以_armboot_start所对应的内存地址是真正的内存中u-boot 第一条指令的地址。
(2 ) 把_bss_start代码段首地址读进r3 寄存器
(3 ) 寄存器 r3 的值-寄存器r2 的值,差值送回寄存器 r2 ,这个差值就是_armboot_start代码(u-boot 代码段)所占用的内存空间大小。
(4 ) 计算出_armboot_start代码的结束地址。寄存器 r0 中保存着 FLASH上代码的开始地址,r2 中保存着u-boot 代码的容量,那么r0+r2?r2 后,r2 中保存的就是u-boot 在FLASH上的最后一句代码的地址。
————————————————————————————————————————————-
(6) 代码的搬移阶段
copy_loop: @ copy 32 bytes at a time
ldmia r0!, {r3 – r10} @ copy from source address [r0]
stmia r1!, {r3 – r10} @ copy to target address [r1]
cmp r0, r2 @ until source end addreee [r2]
ble copy_loop
#endif /* CONFIG_SKIP_RELOCATE_UBOOT */
注释:上面4 句话用来实现 U-BOOT 代码从FLASH—>DDR2 MEM 的搬移。
(1 ) r0=_start=0x0000_06E0 ,即指向 FLASH中代码开始地址;读 8 个字(32 字节),分别存到r3-r10 这个8 个32 为通用寄存器中。然后,r0+8 ?r0
(2 ) r1=TEXT_BASE=0x9780_0000,指向内存基地址;把 r3-r10 这8 个寄存器中的数据写入内存中,因此一次stmia 指令传输8 个字,共 32 字节。然后,r1+8 ?r1
(3 ) r2 为FLASH上代码结束地址。比较r0 和r2 ,即是否到达代码结尾。
(4 ) 如果没有到达代码结尾,继续循环复制,直到完成。
—————————————————————————————————————————————
(5) 设置堆栈
/* Set up the stack */
stack_setup:
ldr r0, _TEXT_BASE @ upper 128 KiB: relocated uboot
sub r0, r0, #CONFIG_SYS_MALLOC_LEN @ malloc area
sub r0, r0, #CONFIG_SYS_GBL_DATA_SIZE @ bdinfo //跳过全局数据区
注释:
这段代码在TEXT_BASE (0x0708_0000)的下面,也就是挨着这个地址往下,建立:动态内存区域和全局数据结构区域。只要将sp指针指向一段没有被使用的内存就完成栈的设置了。根据上面的代码可以知道U-Boot内存使用情况了。
使用SourceInsight 跟踪到 ti8168_dvr.h 文件中,有:
/*
* Size of malloc() pool
*/
#define CONFIG_SYS_MALLOC_LEN (CONFIG_ENV_SIZE + 32 * 1024)
/* size in bytes reserved for initial data */
#define CONFIG_SYS _GBL_DATA_SIZE 128
#define CONFIG_ENV_SIZE 0x2000
由此可见,从TEXT_BASE 往下的32kB+8KB 空间用作动态内存分配;再继续往下128 个字节做为全局数据结构指针。
#ifdef CONFIG_USE_IRQ
sub r0, r0, #(CONFIG_STACKSIZE_IRQ + CONFIG_STACKSIZE_FIQ)
#endif
sub sp, r0, #12 @ leav e 3 words for abort-stack
and sp, sp, #~7 @ 8 byte alinged for (ldr/str)d
注释:
如果使用外部中断IRQ ,在全局数据结构指针继续往低地址方向分配,分配的空间大小由CONFIG_STACKSIZE_IRQ 和CONFIG_STACKSIZE_FIQ 在I.MX51_bbg_android.h 文件中定义。默认时,该头文件中,没有定义 CONFIG_USE_IRQ,也没有定义空间大小,因此意味着此时不对IRQ 和FIQ 进行空间预留。
/* Clear BSS (if any). Is below tx (watch load addr – need space) */
clear_bss:
ldr r0, _bss_start @ find start of bss segment /* BSS段开始地址,在u-boot.lds中指定*/
ldr r1, _bss_end @ stop here /* BSS段结束地址,在u-boot.lds中指定*/
mov r2, #0x00000000 @ clear value
clbss_l:
str r2, [r0] @ clear BSS location /* 将bss段清零*/
cmp r0, r1 @ are we at the end yet
add r0, r0, #4 @ increment clear index pointer
bne clbss_l @ keep clearing till at end
注释:
这段代码,对bss 段进行初始化,从.lds 文件可以知道它的开始和结束位置。从对u-boot.bin的反汇编结果看,_bss_start=0x0708_55CC,这个地址恰好位于内存中 u-boot 映像的上方相邻地址;而bss 段的结束地址_bss_end=0x0708_AA14。前后地址相减,可以算出bss 段占用的空间是 213KB 。这段初始化的方式是把 bss 段全部写 0 ,寄存器 r0 所指示的目标地址指针按照+4递增方式循环,直到全部初始化完成。初始值为0,无初始值的全局变量,静态变量将自动被放在BSS段。应该将这些变量的初始值赋为0,否则这些变量的初始值将是一个随机的值,若有些程序直接使用这些没有初始化的变量将引起未知的后果。
———————————————————————————————————————-
(7) 跳转到第二阶段代码入口start_armboot处。
ldr pc, _start_armboot @ jump to C code
_start_armboot: .word start_armboot
———————————————————————————————————————————————————————————————-
问题一:如果换一块开发板有可能改哪些东西?
首先,cpu的运行模式,如果需要对cpu进行设置那就设置,管看门狗,关中断不用改,时钟有可能要改,如果能正常使用则不用改,关闭catch和MMU不用改,设置bank有可能要改。最后一步拷贝时看地址会不会变,如果变化也要改,执行内存中代码,地址有可能要改。
———————————————————————————————————————————————————————————————-
问题二:Nor Flash和Nand Flash本质区别:
就在于是否进行代码拷贝,也就是下面代码所表述:无论是Nor Flash还是Nand Flash,核心思想就是将uboot代码搬运到内存中去运行,但是没有拷贝bss后面这段代码,只拷贝bss前面的代码,bss代码是放置全局变量的。Bss段代码是为了清零,拷贝过去再清零重复操作
———————————————————————————————————————————————————————————————–