完全理解ARM启动流程:Uboot 您所在的位置:网站首页 进uboot后连不上 完全理解ARM启动流程:Uboot

完全理解ARM启动流程:Uboot

2024-06-17 18:35| 来源: 网络整理| 查看: 265

内容共计5W+字数,但是我还是很多地方说的不够尽兴。那么下次聊!前言

bootloader是系统上电后最初加载运行的代码。它提供了处理器上电复位后最开始需要执行的初始化代码。

PC机上引导程序一般由BIOS开始执行,然后读取硬盘中位于MBR(Main Boot Record,主引导记录)中的Bootloader(例如LILO或GRUB),并进一步引导操作系统的启动。

嵌入式系统中通常没有像BIOS那样的固件程序,因此整个系统的加载启动就完全由bootloader来完成,它主要的功能是加载与引导内核映像。

一个嵌入式的存储设备通过通常包括四个分区:

第一个分区:存放的当然是u-boot第二个分区:存放着u-boot要传给系统内核的参数第三个分区:是系统内核(kernel)第四个分区:则是根文件系统

进入开发板/sys/class/mtd/目录下,执行ls命令查看 :

mtd0 mtd1 mtd2 mtd3 mtd4 mtd5mtd0ro 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 SystemBootloader介绍

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启动方式,当然还可以有其他启动方式,例如:MMC等。

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是一种普遍用于嵌入式系统中的Bootloader。

第一、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在一个平台上的移植,就要对这些目录进行深入的分析。

什么是《编译地址》?什么是《运行地址》?

编译地址:32位的处理器,它的每一条指令是4个字节,以4个字节存储顺序,进行顺序执行,CPU是顺序执行的,只要没发生什么跳转,它会顺序进行执行行, 编译器会对每一条指令分配一个编译地址,这是编译器分配的,在编译过程中分配的地址,我们称之为编译地址。运行地址:是指程序指令真正运行的地址,是由用户指定的,用户将运行地址烧录到哪里,哪里就是运行的地址。

比如有一个指令的编译地址是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工作在这一模式下。

U­Boot 这样功能强大的 Boot Loader 同时支持这两种工作模式,而且允许用户在这两种工作模式之间进行切换。

大多数 bootloader 都分为阶段 1(stage1)和阶段 2(stage2)两大部分,u­boot 也不例外。

依赖于 CPU 体系结构的代码(如 CPU 初始化代码等)通常都放在阶段 1 中且通常用汇编语言实现;而阶段 2 则通常用 C 语言来实现,这样可以实现复杂的功能,而且有更好的可读性和移植性。

第二、U-Boot总体分析

系统启动的入口点。既然我们现在要分析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 程序。

基本内存分布图(只供参考):

下来是u-boot启动的第二个阶段,是用c代码写的,这部分是一些相对变化不大的部分,我们针对不同的板子改变它调用的一些初始化函数,并且通过设置一些宏定义来改变初始化的流程,所以这些代码在移植的过程中并不需要修改,也是错误相对较少出现的文件。在文件的开始先是定义了一个函数指针数组,通过这个数组,程序通过一个循环来按顺序进行常规的初始化,并在其后通过一些宏定义来初始化一些特定的设备。在最后程序进入一个循环,main_loop。这个循环接收用户输入的命令,以设置参数或者进行启动引导。第三、代码分析1 定义入口

由于一个可执行的 Image 必须有一个入口点,并且只能有一个全局入口,通常这个入口放在 ROM(Flash)的 0x0地址,因此,必须通知编译器以使其知道这个入口,该工作可通过修改连接器脚本来完成。

board/ti/ti8168_dvr/u­boot.lds:  ENTRY(_ start)   ==> arch/arm/cpu/cortex-a8/start.S: .globl _ startuboot 代码区(TEXT_BASE = 0x08070000) 定义在 board/ti/ti8168_dvr/config.mk

第一阶段对应的文件是 arch/arm/cpu/cortex-a8/start.S 和 arch/arm/cpu/cortex-a8/ti81xx/lowlevel_init.S。

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的执行。

2. 硬件设备初始化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初始化 _pad:       .word 0x12345678 /* now 16*4=64 */ .global _end_vect _end_vect: .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异常向量表,各个异常向量介绍如下:

在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 #endif2> 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 设置堆栈/* 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 进行空间预留。

只要将sp指针指向一段没有被使用的内存就完成栈的设置了。根据上面的代码可以知道U-Boot内存使用情况了,如下图所示:

7> 代码的搬移阶段 ldr r2, _armboot_start ldr r3, _bss_start sub r2, r3, r2 @ r2 清除BSS段/* 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 /* 将bss段清零*/ clbss_l: str r2, [r0] @ clear BSS location 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,否则这些变量的初始值将是一个随机的值,若有些程序直接使用这些没有初始化的变量将引起未知的后果。

9> 跳转到第二阶段代码入口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段代码是为了清零,拷贝过去再清零重复操作3. U-Boot启动第二阶段代码分析:start_armboot

start_armboot函数在lib_arm/board.c中定义,是U-Boot第二阶段代码的入口。U-Boot启动第二阶段流程如下:

在分析start_armboot函数前先来看看一些重要的数据结构:

(1)gd_t结构体

U-Boot使用了一个结构体gd_t来存储全局数据区的数据,这个结构体在include/asm-arm/global_data.h中定义如下:

typedef struct global_data { bd_t *bd; unsigned long flags; unsigned long baudrate; unsigned long have_console; /* serial_init() was called */ 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 */ #ifdef CONFIG_VFD unsigned char vfd_type; /* display type */ #endif #ifdef CONFIG_FSL_ESDHC unsigned long sdhc_clk; #endif #if 0 unsigned long cpu_clk; /* CPU clock in Hz! */ unsigned long bus_clk; phys_size_t ram_size; /* RAM size */ unsigned long reset_status; /* reset status register at boot */ #endif void **jt; /* jump table */ } gd_t;

U-Boot使用了一个存储在寄存器中的指针gd来记录全局数据区的地址:

#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r8")

DECLARE_GLOBAL_DATA_PTR定义一个gd_t全局数据结构的指针,这个指针存放在指定的寄存器r8中。

这个声明也避免编译器把r8分配给其它的变量。

任何想要访问全局数据区的代码,只要代码开头加入“DECLARE_GLOBAL_DATA_PTR”一行代码,然后就可以使用gd指针来访问全局数据区了。

根据U-Boot内存使用图中可以计算gd的值:

gd = TEXT_BASE -CONFIG_SYS_MALLOC_LEN - sizeof(gd_t)

(2)bd_t结构体

bd_t在include/asm/u-boot.h中定义如下:typedef struct bd_info { int bi_baudrate; /* 串口通讯波特率 */ unsigned long bi_ip_addr; /* IP 地址*/ struct environment_s *bi_env; /* 环境变量开始地址 */ ulong bi_arch_number; /* 开发板的机器码 */ ulong bi_boot_params; /* 内核参数的开始地址 */ struct /* RAM配置信息 */ { ulong start; ulong size; }bi_dram[CONFIG_NR_DRAM_BANKS]; } bd_t;

U-Boot启动内核时要给内核传递参数,这时就要使用gd_t,bd_t结构体中的信息来设置标记列表。

第一阶段调用start_armboot指向C语言执行代码区,首先它要从内存上的重定位数据获得不完全配置的全局数据表格和板级信息表格,即获得gd_t和bd_t,

这两个类型变量记录了刚启动时的信息,并将要记录作为引导内核和文件系统的参数,如bootargs等等,并且将来还会在启动内核时,由uboot交由kernel时会有所用。

(3)init_sequence数组

U-Boot使用一个数组init_sequence来存储对于大多数开发板都要执行的初始化函数的函数指针。

init_sequence数组中有较多的编译选项,去掉编译选项后init_sequence数组如下所示:

typedef int (init_fnc_t) (void); int print_cpuinfo (void); init_fnc_t *init_sequence[] = { #if defined(CONFIG_ARCH_CPU_INIT) arch_cpu_init, /* basic arch cpu dependent setup */ #endif board_init, /*开发板相关的配置--board/samsung/mini2440/mini2440.c */ #if defined(CONFIG_USE_IRQ) interrupt_init, /* set up exceptions */ #endif timer_init, /*开发板相关的配置--board/samsung/mini2440/mini2440.c */ #ifdef CONFIG_FSL_ESDHC get_clocks, #endif env_init, /*初始化环境变量--common/env_flash.c 或common/env_nand.c*/ init_baudrate, /*初始化波特率-- lib_arm/board.c */ serial_init, /* 串口初始化-- drivers/serial/serial_s3c24x0.c */ console_init_f, /* 控制通讯台初始化阶段1-- common/console.c */ display_banner, /*打印U-Boot版本、编译的时间-- gedit lib_arm/board.c */ #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, /*配置可用的RAM-- board/samsung/mini2440/mini2440.c */ #if defined(CONFIG_CMD_PCI) || defined (CONFIG_PCI) arm_pci_init, #endif display_dram_config, /* 显示RAM大小-- lib_arm/board.c */ NULL, };

其中的board_init函数在board/ti/ti8168_dvr/dvr.c中定义,该函数设置了软复位UART,以及一些GPIO寄存器的值,还设置了U-Boot机器码和内核启动参数地址 :

/* * Basic board specific setup */ int board_init(void) { u32 regVal; /* Get Timer and UART out of reset */ /* UART 软复位 */ regVal = __raw_readl(UART_SYSCFG); regVal |= 0x2; __raw_writel(regVal, UART_SYSCFG); while( (__raw_readl(UART_SYSSTS) & 0x1) != 0x1); /* 关闭smart idle */ regVal = __raw_readl(UART_SYSCFG); regVal |= (1bi_arch_number = MACH_TYPE_TI8168EVM; /* 内核启动参数地址 */ gd->bd->bi_boot_params = PHYS_DRAM_1 + 0x100; /*通用存储控制模块 初始化*/ gpmc_init(); /*gpio 初始化*/ gpio_clkctrl_enable(); gpio_dvr_init();    /*初始化模拟spi的IO*/ #if defined(CONFIG_3WIRE_EEPROM) dvr_netra_eeprom_init(); #endif //# RDK 支持8bit NAND gpmc_set_cs_buswidth(0, 0); return 0; }

其中的dram_init函数在board/ti/ti8168_dvr/dvr.c中定义如下:

/* * Configure DRAM banks * * Description: sets uboots idea of sdram size */ int dram_init(void) { /* Fill up board info */ gd->bd->bi_dram[0].start = PHYS_DRAM_1; gd->bd->bi_dram[0].size = PHYS_DRAM_1_SIZE; gd->bd->bi_dram[1].start = PHYS_DRAM_2; gd->bd->bi_dram[1].size = PHYS_DRAM_2_SIZE; return 0; }

ti8168 dvr使用2片1GB的DRAM组成了2GB的内存,接在存储控制器的DDR3,地址空间是0x80000000~0xFFFFFFFFF。

在include/configs/ti8168_dvr.h中PHYS_DRAM_1和PHYS_DRAM_1_SIZE, 分别被定义为0x80000000和0xC0000000。

分析完上述的数据结构,下面来分析start_armboot函数:

void start_armboot (void) { init_fnc_t **init_fnc_ptr; char *s; … … /* 计算全局数据结构的地址gd */ gd = (gd_t*)(_armboot_start - CONFIG_SYS_MALLOC_LEN - sizeof(gd_t)); … … memset ((void*)gd, 0, sizeof (gd_t)); gd->bd = (bd_t*)((char*)gd - sizeof(bd_t)); memset (gd->bd, 0, sizeof (bd_t)); gd->flags |= GD_FLG_RELOC; monitor_flash_len = _bss_start - _armboot_start; /* 逐个调用init_sequence数组中的初始化函数 */ for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) { if ((*init_fnc_ptr)() != 0) { hang (); } } /* armboot_start 在cpu/arm920t/start.S 中被初始化为u-boot.lds连接脚本中的_start */ mem_malloc_init (_armboot_start - CONFIG_SYS_MALLOC_LEN, CONFIG_SYS_MALLOC_LEN); /* NOR Flash初始化 */ #ifndef CONFIG_SYS_NO_FLASH /* configure available FLASH banks */ display_flash_config (flash_init ()); #endif /* CONFIG_SYS_NO_FLASH */ … … /* NAND Flash 初始化*/ #if defined(CONFIG_CMD_NAND) puts ("NAND: "); nand_init(); /* go init the NAND */ #endif … … /*配置环境变量,重新定位 */ env_relocate (); … … /* 从环境变量中获取IP地址 */ gd->bd->bi_ip_addr = getenv_IPaddr ("ipaddr"); stdio_init (); /* get the devices list going. */ jumptable_init (); … … console_init_r (); /* fully init console as a device */ … … /* enable exceptions */ enable_interrupts (); #ifdef CONFIG_USB_DEVICE usb_init_slave(); #endif /* Initialize from environment */ if ((s = getenv ("loadaddr")) != NULL) { load_addr = simple_strtoul (s, NULL, 16); } #if defined(CONFIG_CMD_NET) if ((s = getenv ("bootfile")) != NULL) { copy_filename (BootFile, s, sizeof (BootFile)); } #endif … … /* 网卡初始化 */ #if defined(CONFIG_CMD_NET) #if defined(CONFIG_NET_MULTI) puts ("Net: "); #endif eth_initialize(gd->bd); … … #endif /* main_loop() can return to retry autoboot, if so just run it again. */ for (;;) { main_loop (); } /* NOTREACHED - no way out of command loop except booting */ }

main_loop函数在common/main.c中定义。

void main_loop (void) { #ifndef CONFIG_SYS_HUSH_PARSER static char lastcommand[CONFIG_SYS_CBSIZE] = { 0, }; int len; int rc = 1; int flag; #endif #if defined(CONFIG_BOOTDELAY) && (CONFIG_BOOTDELAY >= 0) char *s; int bootdelay; #endif #ifdef CONFIG_PREBOOT char *p; #endif #ifdef CONFIG_BOOTCOUNT_LIMIT unsigned long bootcount = 0; unsigned long bootlimit = 0; char *bcs; char bcs_set[16]; #endif /* CONFIG_BOOTCOUNT_LIMIT */ #if defined(CONFIG_VFD) && defined(VFD_TEST_LOGO) ulong bmp = 0; /* default bitmap */ extern int trab_vfd (ulong bitmap); #ifdef CONFIG_MODEM_SUPPORT if (do_mdm_init) bmp = 1; /* alternate bitmap */ #endif trab_vfd (bmp); #endif /* CONFIG_VFD && VFD_TEST_LOGO */ #ifdef CONFIG_BOOTCOUNT_LIMIT bootcount = bootcount_load(); bootcount++; bootcount_store (bootcount); sprintf (bcs_set, "%lu", bootcount); setenv ("bootcount", bcs_set); bcs = getenv ("bootlimit"); bootlimit = bcs ? simple_strtoul (bcs, NULL, 10) : 0; #endif /* CONFIG_BOOTCOUNT_LIMIT */ #ifdef CONFIG_MODEM_SUPPORT debug ("DEBUG: main_loop: do_mdm_init=%d\n", do_mdm_init); if (do_mdm_init) { char *str = strdup(getenv("mdm_cmd")); setenv ("preboot", str); /* set or delete definition */ if (str != NULL) free (str); mdm_init(); /* wait for modem connection */ } #endif /* CONFIG_MODEM_SUPPORT */ #ifdef CONFIG_VERSION_VARIABLE { extern char version_string[]; setenv ("ver", version_string); /* set version variable */ } #endif /* CONFIG_VERSION_VARIABLE */ #ifdef CONFIG_SYS_HUSH_PARSER u_boot_hush_start (); #endif #if defined(CONFIG_HUSH_INIT_VAR) hush_init_var (); #endif #ifdef CONFIG_AUTO_COMPLETE install_auto_complete(); //安装自动补全的函数,分析如下 #endif #ifdef CONFIG_PREBOOT if ((p = getenv ("preboot")) != NULL) { # ifdef CONFIG_AUTOBOOT_KEYED int prev = disable_ctrlc(1); /* disable Control C checking */ # endif # ifndef CONFIG_SYS_HUSH_PARSER run_command (p, 0); # else parse_string_outer(p, FLAG_PARSE_SEMICOLON | FLAG_EXIT_FROM_LOOP); # endif # ifdef CONFIG_AUTOBOOT_KEYED disable_ctrlc(prev); /* restore Control C checking */ # endif } #endif /* CONFIG_PREBOOT */ #if defined(CONFIG_UPDATE_TFTP) update_tftp (); #endif /* CONFIG_UPDATE_TFTP */ #if defined(CONFIG_BOOTDELAY) && (CONFIG_BOOTDELAY >= 0) s = getenv ("bootdelay"); bootdelay = s ? (int)simple_strtol(s, NULL, 10) : CONFIG_BOOTDELAY; debug ("### main_loop entered: bootdelay=%d\n\n", bootdelay); # ifdef CONFIG_BOOT_RETRY_TIME init_cmd_timeout (); # endif /* CONFIG_BOOT_RETRY_TIME */ #ifdef CONFIG_POST if (gd->flags & GD_FLG_POSTFAIL) { s = getenv("failbootcmd"); } else #endif /* CONFIG_POST */ #ifdef CONFIG_BOOTCOUNT_LIMIT if (bootlimit && (bootcount > bootlimit)) { printf ("Warning: Bootlimit (%u) exceeded. Using altbootcmd.\n", (unsigned)bootlimit); s = getenv ("altbootcmd"); } else #endif /* CONFIG_BOOTCOUNT_LIMIT */ s = getenv ("bootcmd"); //获取引导命令。分析见下面。 printf ("### main_loop: bootcmd=\"%s\"\n", s ? s : ""); if (bootdelay >= 0 && s && !abortboot (bootdelay)) { //如果延时大于等于零,并且没有在延时过程中接收到按键,则引导内核。abortboot函数的分析见下面。 # ifdef CONFIG_AUTOBOOT_KEYED int prev = disable_ctrlc(1); /* disable Control C checking */ # endif # ifndef CONFIG_SYS_HUSH_PARSER run_command (s, 0);//运行引导内核的命令。这个命令是在配置头文件中定义的。run_command的分析在下面。 # else parse_string_outer(s, FLAG_PARSE_SEMICOLON | FLAG_EXIT_FROM_LOOP); # endif # ifdef CONFIG_AUTOBOOT_KEYED disable_ctrlc(prev); /* restore Control C checking */ # endif } # ifdef CONFIG_MENUKEY if (menukey == CONFIG_MENUKEY) { s = getenv("menucmd"); if (s) { # ifndef CONFIG_SYS_HUSH_PARSER run_command (s, 0); # else parse_string_outer(s, FLAG_PARSE_SEMICOLON | FLAG_EXIT_FROM_LOOP); # endif } } #endif /* CONFIG_MENUKEY */ #endif /* CONFIG_BOOTDELAY */ #ifdef CONFIG_AMIGAONEG3SE { extern void video_banner(void); video_banner(); } #endif /* * Main Loop for Monitor Command Processing */ #ifdef CONFIG_SYS_HUSH_PARSER parse_file_outer(); /* This point is never reached */ for (;;); #else for (;;) { #ifdef CONFIG_BOOT_RETRY_TIME if (rc >= 0) { /* Saw enough of a valid command to * restart the timeout. */ reset_cmd_timeout(); } #endif len = readline (CONFIG_SYS_PROMPT); //CONFIG_SYS_PROMPT的意思是回显字符,一般是“>”。这是由配置头文件定义的 flag = 0; /* assume no special flags for now */ if (len > 0) strcpy (lastcommand, console_buffer); //保存输入的数据。 else if (len == 0) flag |= CMD_FLAG_REPEAT; ;//如果输入数据为零,则重复执行上次的命令,如果上次输入的是一个命令的话 #ifdef CONFIG_BOOT_RETRY_TIME else if (len == -2) { /* -2 means timed out, retry autoboot */ puts ("\nTimed out waiting for command\n"); # ifdef CONFIG_RESET_TO_RETRY /* Reinit board to run initialization code again */ do_reset (NULL, 0, 0, NULL); # else return; /* retry autoboot */ # endif } #endif if (len == -1) puts ("\n"); else rc = run_command (lastcommand, flag); //执行命令 if (rc


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有