必威体育Betway必威体育官网
当前位置:首页 > IT技术

03. Uboot启动流程详解

时间:2019-10-08 21:44:43来源:IT技术作者:seo实验室小编阅读:75次「手机版」
 

uboot

1. start.S解析

1)uboot入口分析

要分析uboot的流程首先要找到uboot的入口函数,从x210开发板的链接器脚本可以获得该信息

根据ENTRY(_start)可知,uboot的入口函数为start;再根据.text段的链接顺序,可知start函数位于cpu/s5pc11x/start.S

2)头文件包含

在start.S的起始处包含了如下4个头文件,

A. <config.h>

include/config.h由mkconfig脚本生成,其中包含了板级配置头文件include/configs/x210_sd.h

B. <version.h>

include/version.h包含了include/version_autogenerated.h,该头文件由makefile在编译过程中生成,其中定义了标识uboot版本的宏。后续源代码中使用的版本信息,即由此而来。

C. <asm/proc/domain.h>

由于x210的uboot定义了CONFIG_ENABLE_MMU宏,所以会包含该文件。而include/asm/proc目录通过软链接会指向include/asm-arm/proc-armv

D. <regs.h>

include/regs.h文件是include/s5pc110.h的软链接,而s5pc110.h中定义了S5PV210芯片的片内外设地址及操作用宏

说明:上述头文件的包含就体现了uboot的跨平台移植性,通过配置编译脚本和软链接,使得源代码中能够以统一的方式包含头文件,而实际指向却因平台而异

3)启动代码的16B头部信息

为配合S5PV210的iROM启动流程,在uboot的起始位置预留了16B用于存放BL1的头部信息(sd_fusing.sh脚本会截取uboot.bin的前8KB作为BL1)。

根据头部信息的字段要求,此处的0x2000为BL1长度,即8KB。

4)构建异常向量表

异常向量表的功能在中断相关笔记中已有详细说明,此处不再赘述。异常向量表后的 .balignl 16,0xdeadbeef 用于实现地址对齐

此处的含义是让当前当前地址16字节对齐,如果不对齐,使用0xdeadbeef填充(deedbeef正好在十六进制数标识范围内~~)

说明1:进行内存对齐的2个原因

① 提高读写效率

硬件特殊要求

说明2:需要注意的是,在x210 uboot代码中并没有将异常向量表的地址写入CP15的c12寄存器,其实在整个x210 uboot中并没有使用中断

5)重要的变量定义

A. _TEXT_BASE

uboot的链接地址,该值首先由x210_sd_config配置项写入board/samsung/x210/config.mk配置文件,然后在顶层目录的config.mk中会解析该文件,然后在编译选项中添加-DTEXT_BASE,从而可以在源代码中获得链接地址的值(0xc3e00000)

B. _TEXT_PHY_BASE

#define MEMORY_BASE_ADDRESS 0x30000000

#define CFG_PHY_UBOOT_BASE MEMORY_BASE_ADDRESS + 0x3e00000

所以_TEXT_PHY_BASE的值为0x33e00000,即uboot的物理地址(正确的理解:链接地址VA对应PA)

C. _armboot_start

该变量实际是_start的链接地址,由于起始处预留头部信息的原因,_start的链接地址为0xc3e00010

D. _bss_start & _bss_end

这两个变量的值源自链接器脚本,后续用于清bss段

下面通过反汇编验证上述分析:

注:该方法可用于验证C语言对链接器脚本变量(e.g. _bss_start)的使用

6)设置SVC模式

由于直接将0xd3写入CPSR的低8位,所以此处在进入SVC模式的同时,还将CPU设置为ARM状态,并关闭 I & F中断(CPU端关中断)

7)设置cache & MMU

说明:对存储系统的设置都是通过操作cp15寄存器的实现

A. bl disable_l2cache

该函数将控制寄存器(c1)的bit 1清零,即不再对数据的地址对齐进行检查,该操作和cache也没啥关系啊~~

B. bl set_l2cache_auxctrl_cycle

该函数将L2 Cache辅助控制寄存器各位均置为0

C. bl enable_l2cache

调用该函数又将控制寄存器的bit 1置为1,即使能数据对齐检查,当数据不对齐时将触发异常

D. invalidate L1 I/D

mcr p15, 0, r0, c8, c7, 0用于使I / D TLB均失效

mcr p15, 0, r0, c7, c5, 0用于使I cache失效

E. disable MMU and caches

此段流程对控制寄存器进行如下操作,

bit 13 --> 0:使用一般模式的异常向量表地址(即默认地址为0x0,也可通过c12配置)

bit [2:0] --> 0b000:关闭MMU / 关闭地址对齐检查 / 关闭D-cache

bit 1 --> 1:使能地址对齐检查

bit 11 --> 1:使能program flow prediction

说明:从整个cache和MMU的设置流程分析,控制寄存器A位的设置被反复进行,而且有些函数名与功能并不相符,应该是移植时造成的残留问题

8)识别 & 保存启动设备信息

代码首先从0xE0000004寄存器中读取启动设备的信息,需要注意的是,手册中并没有描述该寄存器的内容(可见某些接近底层的移植,只能由原厂进行,居然还TM留了一手~~)

在经过一系列比较后,会将启动设备的信息保存在INFORM3寄存器中,但是芯片手册中并没有该寄存器的详细说明

9)调用lowlevel_init

A. 栈的设置

在调用lowlevel_init之前,需要先设置栈(此时栈在iRAM中)。虽然lowlevel_init函数也是用汇编语言完成,但是由于他并非叶子函数,所以在lowlevel_init调用其他函数之前需要将当前的lr入栈,以便lowlevel_init函数返回

说明:该栈并没有设置在iRAM规划的SVC stack区,而是设置在iRAM规划的RW / ZI区域(已初始化变量 / 未初始化变量)及堆区

B. 检查复位状态

说明:目前稍微复杂的CPU均支持多种复位状态(e.g. 上电启动 / 睡眠唤醒),此处检查复位状态的意义在于,如果是睡眠唤醒,很多上电启动需要进行的步骤此时必须略过(e.g. 时钟设置 / DDR初始化)

C. 释放IO的保持状态

实现的方式就是将MISC寄存器的bit 31置为1

D. 关闭看门狗

E. SROM相关GPIO管脚设置

此处设置了GPJ 1 & GPJ 4的工作模式及上下拉模式,其中,

GPJ 1:

将GPJ 1[0] ~ GPJ 1[5]的模式设置为SROM_ADDR_16to22[0] ~ [5]

将上下拉状态设置为Reserved(这就比较尴尬了~~)

GPJ 4:

将GPJ 4[4]的模式设置为SROM_ADDR_16to22[6]

将上下拉状态也设置为Reserved

最后将SROM0的位宽设置为16 bit

说明:由x210核心板原理图可知,此处的SROM用于连接camera

F. 供电锁存

由于x210使用软开关设计,如果不进行供电锁存,就需要长按power键进行供电。关于供电锁存的具体原理及操作可见体系结构相关笔记

G. 初始化时钟 & DDR

① 判断当前代码运行位置

在进行时钟和DDR初始化之前,首先要判断当前代码的运行位置。如果已经在内存中运行,则说明此时是从睡眠中被唤醒,无需进行时钟和内存初始化(根据实际经验,如果此时重复初始化内存,内存中的数据会丢失,导致程序无法正确运行)

判断的方式还是比较运行地址与链接地址的关系。经过掩码0xff000fff的处理后,

r2 = 0x00e00000

如果此时代码在iRAM中运行,

r1 = 0x00200000(iRAM起始地址位0xd0020010)

注意:此处掩码将地址的低12位清零,即将比较的范围限制在4KB范围内,如果代码运行到此处已超过4KB,判断就会失效(e.g. uboot已经运行在内存中,但是超过4KB,按位与后就会出现如下现象,r2 = 0x00e00000,r1 = 0x00e01000,此时比较会失败,导致内存被错误初始化)

个人认为比较好的判断方法是比较_start标号的运行地址与链接地址

② 时钟初始化

x210时钟配置时的分频系数可配置,目前选择的为High performance方案,分频结果与之前裸机代码相同

最终配置的各时钟如下,

ARMCLK = 1GHz

SCLKA2M = 200MHz

HCLK_MSYS = 200MHz

PCKL_MSYS = 100MHz

HCLK_DSYS = 166MHz

PCLK_DSYS = 83MHz

HCLK_PSYS = 133MHz

PCLK_PSY = 66MHz

CAM0 = 24MHz

CAM1 = 24MHz

说明1:在x210的代码中,设置并使能各PLL后会检查MPLL是否成功锁存,如果锁存失败将重新设置一次时钟

说明2:根据x210官方移植的uboot,移植uboot时需要根据具体的硬件需求设置不同的时钟,比如此处就单独设置了camera时钟

③ DDR初始化

x210 uboot中的DDR初始化的流程和参数与裸机代码中基本一致,只有一处不同,

裸机代码中:

#define DMC0_MEMCONFIG_0 0x20F01323 // MemConfig0 256MB config, 8 banks,MAPPing Method[12:15]0:linear, 1:linterleaved, 2:Mixed

uboot中:

#define DMC0_MEMCONFIG_0 0x30F01313 // MemConfig0 256MB config, 8 banks,Mapping Method[12:15]0:linear, 1:linterleaved, 2:Mixed

该设置造成的差别为,DMC0可以容纳512MB内存,虽然都是使用了其中的256MB,但是,

在裸机代码中,DMC0的256MB内存范围:0x20000000 ~ 0x2fffffff

在uboot中,DMC0的256MB内存范围:0x30000000 ~ 0x3fffffff

说明1:最终uboot使用的物理内存范围如下,

DMC0:0x30000000 - 0x3fffffff

DMC1:0x40000000 - 0x4fffffff

说明2:注意内存配置与时钟配置的关联性,一般先配置时钟,因为在内存配置中会使用到时钟配置的结果

H. 初始化串口

说明1:首先将UART相关的GPIO均配置为UART相关工作模式,此处同时设置了UART 0 ~ 3

说明2:作为uboot控制台的是UART2,格式为8n1,波特率为115200

说明3:串口配置完成后向串口输出字符'O'以验证是否配置成功

I. lowlevel_init返回

说明1:在lowlevel_init函数返回之前,向串口打印字符'K'。根据上文,完成后会打在串口初始化印字符'O',所以在uboot启动过程中会打印出"OK"。这点与实际开机过程是吻合的

说明2:调用pop {pc}实现函数返回,就是将在lowlevel_init函数起始处入栈的lr弹出赋值给pc

10)代码重定位

说明:此处代码重定位的目的,是将BL2(内容为整个uboot.bin)从iNand或SD卡拷贝到uboot.bin的链接地址处

A. 重新设置栈

说明1:由于在lowlevel_init函数中已经完成DDR初始化,所以此处将栈设置在内存中,而且是以物理地址设置栈(sp = 0x33dffff4)。

说明2:此处设置栈是因为后续要调用movi_bl2_copy函数,而该函数用C语言实现

说明3:该栈的位置在uboot.bin链接地址之前,而ARM中默认使用满减栈,所以后续的BL2拷贝不会影响该栈的工作(理解栈的设置需要注意2点:① 设置栈的目的;② 栈的位置是否合理)

注意:此处只是设置了栈就进入了C函数movi_bl2_copy,并没有清bss段,这是因为该函数中没有使用未初始化或初始化为0的全局变量

B. 判断当前代码运行位置

说明:此处的判断方法与lowlevel_init函数一致,如果当前代码已经在内存中运行(对应睡眠唤醒状态),则无需进行代码重定位

C. 根据启动设备选择拷贝函数

说明1:如果从iRAM Global Variable区域获取当前的启动设备是SD/MMC channel 2,即此时是SD卡启动(second boot),那么跳转到mmcsd_boot标号运行

说明2:此处从INFORM 3寄存器读出启动设备信息,而该信息正是在start.S的起始部分写入的

D. movi_bl2_copy函数分析

说明1:此处代码重定位使用的是iROM中提供的拷贝函数,根据当前的SD/MMC channel从iNand或SD卡拷贝BL2

说明2:拷贝函数参数解析

① channel:拷贝源的SD/MMC channel,通过iROM的Global Variable确定

② start_block:拷贝源要拷贝的起始block数(1 block = 512B)

每个block的大小 = 512B:

eFUSE_SIZE占用block数 = 512 / 512 = 1:

BL1占用block数 = (8 * 1024) / 512 = 16:// 此处可见BL1为8KB

ENV占用block数 = 16384 / 512 = 32:

说明:所以MOVI_BL2_POS(即BL2的起始block数)为 1 + 16 + 32 = 49,这也就和我们烧写SD启动卡时的布局相匹配了~~

③ block_size:要拷贝的block数

说明:MOVI_BL2_BLKCNT可以理解为当前支持的BL2最大长度,此处为512KB,即1024个block

④ trg:拷贝到内存的目的地址

说明:拷贝的目的地址应该是uboot.bin的链接地址,此处需要注意的是,编译uboot.bin时使用的链接地址是虚拟地址0xc3e00000。但是在拷贝时尚未开启MMU,所以仍要使用与链接地址对应的物理地址

⑤ init:拷贝时是否初始化SD卡

小结:SD/MMC中的uboot布局

说明1:目前x210平台的iNand & SD均按此布局,根据之前笔记介绍,无论是iNand还是SD卡,在分区时均空出了前10MB空间,因此后续的分区操作不会破坏启动部分

说明2:MBR和BL1的位置是固定不变的

操作系统会从0扇区读取分区表,所以MBR的位置不能随意指定

S5PV210的iROM会从1扇区读取BL1,所以BL1的位置也不能随意指定

注意:这也就限制了S5PV210平台无法使用gpt分区表,如果要增加分区超过4个,只能使用扩展分区 + 逻辑分区

说明3:ENV和BL2的位置是可配置的,只是修改配置后实现代码重定位的拷贝函数必须做相应的修改

11)使能MMU

关于该部分使能MMU的详细分析,可见体系结构中MMU编程笔记。关键是一旦使能MMU,软件层面就不再有物理地址,只有虚拟地址

需要注意的是,写入CP15 c2寄存器的Translation Table起始地址是物理地址!!!

12)跳转进入第二阶段

A. 重新设置栈

此处设置栈是因为后续要进入uboot的第二阶段,即C语言阶段运行,需要注意的是,

① 由于已经开启了MMU,此次栈的设置使用了虚拟地址

② 此处sp = 0xc3e00000 + (2 * 1024 * 1024) - 0x1000 = 0xc3fff000,该位置距离uboot.bin链接地址约2MB,考虑到目前配置最大支持512KB的BL2,因此该栈的可用空间约1.5MB(这个理解是错误的!!!从0xc3e00000开始的2MB空间内,实际规划了多个内存区块,栈实际只用了512KB,内存布局的具体划分详见下文中的start_armboot函数分析)

小结:uboot中共设置了3次栈,小结如下

① 调用lowlevel_init函数前,iRAM中,物理地址

② 调用movi_bl2_copy函数前,DDR中,物理地址

③ 调用start_armboot函数前,DDR中,虚拟地址

B. 清bss段

说明:清bss段是为了可以安全使用未初始化或初始化为0的全局 & 静态局部变量。需要进行清空操作是因为bss段中仅包含句柄,并不包含实际数据,所以在进入C语言阶段前必须将bss段对应的内存清空

C. 长跳转到start_armboot函数

说明:该长跳转直接跳转到DDR中的start_armboot标号处继续执行,BL2中与BL1重叠的部分将被跳过,不会执行(虽然我们也将其拷贝到DDR中)

至此,uboot启动的第一阶段就结束了,后续的启动流程将由C语言完成~~

2. start_armboot函数解析

1)start_armboot概述

① start_armboot函数构成了uboot启动的第二阶段

② uboot启动第一阶段主要初始化了CPU和SoC片内外设控制器(e.g. 时钟、串口),然后初始化DDR并完成代码重定位,这些都是为运行第二阶段做准备

③ uboot启动第二阶段就是要初始化剩余未被初始化的硬件,主要是SoC外部的硬件(e.g. iNand、网卡芯片);还要建立uboot本身的相关组件(e.g. uboot命令和环境变量

最后进入uboot命令行或直接启动linux内核

2)global data & uboot内存布局

A. gd指针赋值

此处给gd指针与gd->bd指针赋值,并且将这2个指针指向的结构体清空。问题是这2个指针是如何定义的呢~~

B. global data结构体分析

① gd声明方式

在board.c头文件包含之后,所有声明之前,调用了DECLARE_GLOBAL_DATA_PTR宏,该宏在include/asm-arm/global_data.h文件中定义,具体如下,

实际上此处声明了一个gd_t类型的指针变量,

使用volatile修饰:每次到内存中读取,不从cache中读取

使用register修改:建议使用寄存器存储该变量(仅是对编译器的建议)

asm("r8"):如果将该变量存储在寄存器中,则使用r8寄存器

② gd_t结构体分析

说明1:设置global_data类型的目的就是将uboot要用到的全局变量用结构体管理起来,其中存储了全局的系统初始化参数。在uboot中使用该类型需要注意2点,

① 要保持该结构体尽可能小,即设置尽可能少的全局变量

② 在板级配置头文件中定义的CFG_GBL_DATA_SIZE宏值必须大于sizeof(gd_t),目前x210定义的宏值为128

CFG_GBL_DATA_SIZE宏在多处内存分配的场景会使用到,用于为global data预留空间(额。。。冲突没想象中严重)

说明2:struct global_data字段解析

a. bd_t *bd //指向bd_info结构体

b. unsigned long flags

全局标志位,x210 uboot中可用标志位如下,明显以位或方式使用

c. unsigned long baudrate //串口波特率

d. unsigned long have_console //是否有控制台

e. unsigned long reloc_off

重定位偏移量,即uboot.bin的下载地址与链接地址位置之差,一般为0(即将uboot.bin下载到链接地址处)

f. unsigned long env_addr // 环境变量内容的地址

g. unsigned long env_valid //环境变量CRC校验标志

h. unsigned long fb_base //帧缓冲区基地址

i. void **jt //jump table,最终为一个函数指针数组

说明3:struct bd_info字段解析(板级信息结构体)

a. int bi_baudrate //串口波特率

b. unsigned long bi_ip_addr //IP地址

c. unsigned char bi_enetaddr[6] //MAC地址

d. struct environment_s *bi_env //指向环境变量结构体

e. ulong bi_arch_number //开发板机器码

f. ulong bi_boot_params //uboot给Linux内核传参的内存地址

g.

struct

{

ulong start;

ulong size;

}bi_dram[CONFIG_NR_DRAM_BANKS]

内存配置参数,每个数组成员描述一个内存bank,包含物理起始地址和内存大小

C. uboot内存布局分析

#define CFG_UBOOT_BASE 0xc3e00000

#define CFG_UBOOT_SIZE (2 * 1024 * 1024) //2MB

#define CFG_malloc_LEN (CFG_ENV_SIZE + 896 * 1024) //16KB + 896KB

#define CFG_ENV_SIZE 0x4000 //16KB

#define CFG_STATCK_SIZE 512 * 1024 //512KB

说明1:CFG_MALLOC_LEN中包含了ENV区的16KB空间,内存布局中ENV的16KB与SD卡/iNand的uboot布局是匹配的

说明2:强制类型转换对指针运算的影响

代码中在给gd->bd指针赋值时,先将gd强制转换为char *类型,此处增补一例说明强制类型转换对指针运算的影响(之前的理解有误)

D. 其余主题

① __asm__ __volatile__("": : :"memory")的作用

使用一条空的内嵌汇编,但是将破坏部分指定为memory,这将强制gcc编译器假设所有内存单元均被汇编指令修改,这样CPU中的register和cache中已缓存的内存单元中的数据将作废,CPU将不得不在需要的时候重新读取内存中的数据

这就阻止了CPU使用register和cache中的数据去优化指令,从而避免访问内存

monitor_flash_len = _bss_start - _armboot_start

_armboot_start为_start标号的链接地址,此处为0xc3e00000 + 0x10(预留的头部信息)

_bss_start为bss段的链接起始地址,而bss段又是整个ELF文件中的最后一个段

3)循环调用初始化函数

uboot中定义了函数类型init_fnc_t,并据此定义了全局的函数指针数组init_sequence,在其中管理了需要在uboot启动第二阶段调用的初始化函数。注意该数组以NULL元素结尾,这将作为遍历数组的判断条件(同时也有利于数组的扩展)

个人:不理解此处为什么是定义函数类型而不是函数指针类型

typedef int (*init_fnc_t)(void)

start_armboot中将会循环调用init_sequence数组中的初始化函数,如果某个函数返回非零值(即执行失败),uboot启动将会hang住

4)初始化函数数组分析

说明:此处仅分析x210 uboot会执行到的函数

A. cpu_init

说明:由于x210的uboot没有使能中断,所以cpu_init相当于空函数。但是需要注意的是,如果使能了中断,中断的栈将设置在_arm_boot_start,即_start标号之前,并不会影响上文中分析的内存布局

但是需要注意中断栈不能覆盖uboot.bin的有效内容(虽然存在这种风险,但是冲突并不严重,毕竟每个中断栈只有4KB)

B. board_init

说明1:gd指针声明

对全局变量的另一种使用方式是在一个源文件中定义,并在头文件中声明,然后在需要使用的源文件中包含该头文件;uboot则采用了通过宏定义在使用时声明变量的方式。

两者相比较,传统方式声明的全局变量将具有文件作用域;uboot方式声明的变量则依据调用点的不同,具有文件或局部作用域

说明2:dm9000_pre_init

该函数完成如下2个任务,

① 设置SROM BANK1的属性(SROM_BW寄存器)与时钟(SROM_BC1寄存器)

② 设置SROM BANK1的CS管脚功能

注意:此处只涉及dm9000网卡与SoC连接用到SROM BANK的初始化,并不涉及dm9000驱动本身。实际在uboot的移植过程中,dm9000驱动是不变的,需要配置的是板级硬件相关的参数

说明3:开发板机器码设置

由于嵌入式设备的高度定制化,其硬件和软件往往是不通用的。因此给每个开发板设置一个唯一编号,即开发板机器码。

此处将开发板机器码存储在gd->bd->bi_arch_number字段,在启动内核时,这些参数会被传递给Linux内核,内核在启动过程中会对比该机器码和内核本身维护的机器码,只有二者匹配时才能顺利启动

注意:从理论上说,开发板机器码不能随便指定,应该提交给开源社区审定。但是在实际使用时,只要uboot与Linux内核中的机器码匹配即可~~

说明4:传递启动参数的地址

uboot向Linux内核传参过程:uboot将要传递的参数(bootargs字符串)存放到指定内存处,然后启动内核。uboot在启动内核时通过r0、r1、r2寄存器传递参数,其中一个就是bi_boot_params,Linux内核根据bi_boot_params就可以找到uboot传递给他的参数

此处将bi_boot_params字段设置为0x30000100,使用的是物理地址(因为启动Linux内核时需要关闭MMU)。

C. interrupt_init

说明1:与中断无关

虽然函数名是interrupt_init,但是本函数与中断并木有什么关系,从实现上看,该函数初始化了PWM模块的Timer 4,定时值为10ms循环工作,且未使能Timer 4中断

注意:PWM模块的Timer 4没有外部引脚,也没有TCMPB寄存器,所以一般作为内部定时器使用。

由于此处没有使用中断,在使用中CPU需要通过轮询的方式实现定时

说明2:将模块相关寄存器组织为结构体

S5PC11X_TIMERS结构体根据地址先后顺序,将PWM的寄存器组织起来使用

说明3:时钟值获取函数

该函数中调用get_PCLK函数获取PCLK_PSYS的时钟值,uboot根据时钟体系结构设置了一系列获取时钟值的函数。由于时钟体系与SoC高度相关,x210 uboot关于时钟获取的函数均在cpu/s5pc11x/s5pc110/speed.c中

以get_PCLK函数为例,时钟值是通过读取寄存器值并依据时钟体系结构计算得到,因此这些函数的适用性非常强

特别注意:此处在设置定时器计数值时是有问题的!!!

从get_PCLK函数的计算流程可知,该函数获取的是PCLK_MSYS,经过打印验证确实如此~~但是PWM Timer4使用的是PCLK_PSYS时钟,因此设置的计数值是错误的。正确的用法是应该调用get_PCLKP函数

D. env_init

说明1:找到正确的env_init函数

uboot代码中有多个env_init函数的实现,而且很多涉及的文件均被编译。之所以会有如此多的实现版本,是因为uboot支持多种启动设备,针对不同的启动设备,需要不同的函数将环境变量读取到内存中。

但是在最终连接的ELF文件中,只能有一个env_init函数的实现,否则就会链接出错。在uboot中是通过CFG_ENV_IS_IN_XXX宏实现控制的,在x210_sd.h中定义如下,

所以当前版本使用的是common/env_auto.c中的env_init

说明2:使用缺省参数初始化环境变量并标识环境变量CRC校验有效

首先说明一下default_environment数组的定义方式,

① 由于定义了CONFIG_S5PC110宏,所以数组形式为,

uchar default_environment[CFG_ENV_SIZE]

实际就是一个16KB的字符数组

② 由于所以环境变量以字符串的形式保存,所以如果配置项为字符串(e.g. CONFIG_BOOTARGS)则直接展开,如果配置项为"数字"(e.g. CONFIG_BOOTDELAY)则调用MK_STR宏实现字符串化

③ 由于default_environment数组定义为全局变量,因此这段16KB的缺省环境变量会被编译进uboot.bin镜像

说明3:ENV_IS_EMBEDDED机制简介[x210 uboot中并未使用]

如果定义了ENV_IS_EMBEDDED宏,就会使能该机制,该机制的特点就是环境变量存储在.text段

首先说明一下env_t类型,其中个字段含义如下,

crc:环境变量的CRC校验值

flags:标识是否有冗余的环境变量env_t结构体

data:实际存储环境变量的字符数组,数组大小则是16KB - env_t结构头部信息

common/environment.c中定义的environment结构体如下,

① __PPCENV__宏用与指定将environment结构体链接到.text段

② 如果要使用environment结构体中的环境变量,需要定义ENV_CRC宏,且使其具有有效值,否则校验不通过也是无法使用的

③ 如果定义了CFG_ENV_ADDR_REDUND宏,将会紧接着environment结构体定义redundand_environment结构体,作为冗余数据

④ 在env_init中使用tmp_env1和tmp_env2分别指向environment结构和redundand_environment结构,并分别进行校验。

这里需要注意的是env_ptr的赋值方式,environment本身是一个结构体类型变量,对结构体类型变量取下标(相当于解一次引用)算是个什么东东呢 ???

根据在X86上的验证,根本无法编译通过~~(在uboot源码中尚无法测试,因为ENV_IS_EMBEDDED宏只在CFG_ENV_IS_IN_FLASH/NAND时才生效;单独定义ENV_IS_EMBEDDED宏,编译时会报出其他错误)

E. init_baudrate

说明:由于在lowlevel_init函数中已经对串口对应的GPIO和工作模式进行了初始化,此处只是获取串口的波特率信息。

首先尝试在环境变量中查找,如果环境变量中没有,则使用x210_sd.h中的配置值

F. serial_init

说明1:该函数只是简单计数延时,未做其他操作

说明2:完善的serial_setbrg函数是用于设置串口波特率的

G. console_init_f

说明1:该函数只是将gd->have_console字段置为1,即使用控制台

说明2:带_f 后缀的函数

在uboot中,有些初始化需要分段进行,一般加_f 后缀的就是第一阶段初始化(before),加_r 后缀的就是第二阶段初始化(after)

示例:控制台初始化就分为前后两个阶段,因此在start_armboot中会先后调用如下2个函数,

console_init_f

console_init_r

应用场景:有些初始化不能连续完成,中间必须依赖其他部件的初始化

H. display_banner

说明1:该函数实现2个功能,

① 打印uboot版本信息

② 开启LCD背光(x210定制功能)

其中打印的字符串为,

version_string中的U_BOOT_VERSION宏定义在include/version_autogenerated.h中,由Makefile在编译过程中生成

说明2:debug函数打印的信息必须定义DEBUG宏才能从串口输出

说明3:控制台目前还没有初始化完成,为何能使用printf函数?

printf函数会调用puts函数,puts函数会根据GD_FLG_DEVINIT(设备是否初始化完成)确定使用哪个输出函数。

如果控制台已经初始化完毕,将会调用fputs函数,该函数会根据当前的句柄(stdin / stdout / stderr)调用不同的hook函数(详见后文分析)。

如果控制台尚未初始化完毕,则直接调用串口输出函数serial_puts,将打印信息输出到串口

说明4:如果不初始化控制台也能使用printf,那为何要使用控制台功能?

控制台是一个软件抽象层,控制台有一套专用的通信函数(e.g. 发送 / 接收函数),但是在实现时他们只是hook点。根据实际hook的输出函数,控制台可以映射到不同的物理设备(比如LCD;或者是另一个串口,即构成一个控制台串口,一个debug串口)

同时还可以在这个软件抽象层上进行优化,比如加入缓冲机制(uboot中可以不用,但是操作系统中需要实现)

说明5:开启LCD背光

说明:该函数将GPF3[5]管脚设置为输出模式,并且输出高电平。根据x210原理图可知,GPF3[5]为LCD的SYS_OE引脚

I. print_cpuinfo

说明1:print_cpuinfo函数用于打印当前各级时钟的配置值

说明2:代码中会对ARMCLK频率的有效性进行检查,有效值范围为设置值的[95%, 105%]

J. checkboard

说明:checkboard也是一个很水的函数,只是打印了开发板型号

K. dram_init

说明:由于lowlevel_init函数中已经对内存进行了初始化,此处只是将2个bank的内存信息(物理起始地址 + 内存大小)记录到gd结构体中

需要注意的是,此处内存大小单位为字节(B)

L. display_dram_config

说明1:该函数用于打印当前开发板内存总量,其中print_size函数会依次尝试当前内存总量等级,然后处理显示内存总量的整数和小数部分

说明2:uboot中的bdinfo命令可以打印gd->bd结构体中的信息,注意其中的env_t字段没赋值,仍为清空后的零值

5)CFG_NO_FLASH宏分析

说明1:此处的FLASH指NorFlash,虽然NandFlash和NorFlash都是Flash,但是一般NandFlash简称为Nand,而不是Flash。(其实和二者出现的先后顺序有关~~)

说明2:当前uboot中没有定义CFG_NO_FLASH宏,因此会调用flash_init & display_flash_config函数

实际上x210开发板并未配置NorFlash,但是如果在x210_sd.h中定义CFG_NO_FLASH宏,会引发其他文件的编译错误。可见保留此处的函数调用,是uboot移植时的遗留问题~~

6)堆内存初始化

说明1:根据上文对uboot内存布局的分析,uboot中维护了一片堆内存,配合uboot自身的管理代码,可以实现在uboot中使用malloc & free函数来申请和释放内存

说明2:mem_malloc_init函数记录堆内存的起始 & 终止地址,并将该段内存清空。需要注意的是,uboot将内存的env段也在malloc段之内,并同时将其清空(存疑:根据分析,下图中的env段和heap段应该对调,后续在代码中确证后会修改)

说明3:uboot中malloc & free的实现在common/dlmalloc.c文件中,需要注意的是实现的函数名并非全是小写。但是经过验证,在代码中调用malloc确实使用的就是该函数。(准确的原因我目前还无法解释~~)

uboot中malloc & free的代码颇有难度,而且学习该段代码也有利于对Linux内核中内存分配的理解,更详细的说明可参考链接中提供的文档。

https://wenku.baidu.com/view/76bf7df9fab069dc502201d1.html

7)开发板特有初始化分析

说明1:开发板特有初始化由CONFIG_X210宏控制,此处实际只运行了mmc_initialize函数

说明2:mmc_initalize函数分析

① 初始化mmc设备链表

uboot中的mmc设备通过双向链表mmc_devices进行管理,同时使用cur_dev_num标识当前mmc设备的个数

② SoC & 板级MMC初始化(MMC controller / host初始化)

uboot中会首先尝试调用板级的MMC初始化函数board_mmc_init,在调用失败的情况下才会调用SoC的MMC初始化函数cpu_mmc_init

之所以MMC有两级初始化函数,是因为有些SoC内部没有SD/MMC控制器,而是使用外界的SD/MMC控制器(e.g. USB接口的读卡器)。

我们使用的S5PV210内部包含SD/MMC控制器,所以board_mmc_init函数始终返回-1(实现方式是将board_mmc_init定义为__def_mmc_init函数的别名弱函数,同时不提供board_mmc_init函数的定义。需要注意的是,虽然cpu_mmc_init函数也被定义为__def_mmc_init函数的别名弱函数,但是代码中提供了cpu_mmc_init函数的定义,所以会调用实际定义的函数)

而cpu_mmc_init主要完成MMC的时钟初始化、GPIO管脚初始化、controller初始化,具体分析见体系结构SD卡编程相关笔记

③ SD卡/iNand芯片初始化

通过向SD卡/iNand芯片发送命令的方式实现初始化

补充:uboot中的SD/MMC实际由Linux内核移植而来,因此借用了分层管理的驱动架构

说明3:打印SD卡容量的语句

此处有2点值得注意,

① mmc->capacity获取的SD卡容量以扇区(512B)为单位

② 此处先除以1024 * 1024,然后再乘以512(1 << 9);而不是先乘以512再除以1MB。这样处理的好处是可以避免mmc->capacity * 512可能导致的范围越界,这就是细节中体现出的谨慎。

8)env_relocate分析

说明1:分配内存

env_ptr为ent_t类型的结构体指针,具体结构如下,

由于目前没有使用ENV_IS_EMBEDDED功能,所以需要为存储该结构分配内存,此处分配16KB内存,分配的内存地址如下,

说明2:env_relocate_spec函数分析

env_relocate_spec函数会根据INFO3寄存器的值调用不同的拷贝函数,其中INFO3寄存器中保存的是启动设备信息,在start.S中进行设置。

在调用movi_read_env函数之前,会先判断2个魔数的值。这2个魔数来自bank 1内存的前8B,我们将这2个数字dump如下,

需要注意的是,调用movi_read_env函数时使用virt_to_phys获取了虚拟地址对应的物理地址值。此处env_ptr指针指向的是malloc分配的堆内存,地址为0xc3e9c008,这是一个虚拟地址。

在读取环境变量时,默认使用了channel 0,也就是从iNand读取环境变量。读取的范围使用raw_area_control.image[2].start_blk + raw_area_control.image[2].used_blk指定,我们打印出这2个值,

可见这2个值与SD卡 / iNand的布局是匹配的,那么raw_area_control又是个什么结构呢?

raw_area_control是一个raw_area_t类型的结构体,用于管理iNand中的raw区域。根据体系结构SD卡编程相关笔记,我们在分区时空出了SD卡 / iNand的前10MB空间,在这段空间中存储了MBR / BL1 / ENV / BL2。

该结构的初始化由init_raw_area_table函数完成,该函数根据x210_sd.h中的配置项,将raw区域的划分信息保存在raw_area_control结构体中

在x210的uboot中,有效初始化的是image[1] ~ image[5],从image[6]开始将image中标识raw区域范围的字段置为0

有效初始化的部分列表如下(已通过打印验证),

index

start_blk

used_blk

size

attribute

description

1

1

16

8KB

0x1:BL1

u-boot parted

2

17

32

16KB

0x10:environment area

environment

3

49

1024

512KB

0x2:BL2

u-boot

4

1073

8192

4MB

0x4:kernel

kernel

5

9265

53248

26MB

0x8: root file system

rfs

需要注意的是,虽然此处root file system的空间已经越过了10MB的限制,但是由于实际并未将根文件系统存储在raw area(根据之前的分析,是存储在了mmcblk0p2分区),所以此处维护上的缺陷并不会导致什么问题

从iNand中读出环境变量后,会进行CRC校验,如果校验失败会使用默认值替换从iNand中读取的内容

根据上文分析,default_environment就是保存环境变量默认值的字符数组,在完成环境变量替换后,重写校验值并标识校验成功

总结:环境变量从何而来?

iNand中有32个扇区(16KB)作为环境变量的存储区域,但是在烧写系统时只烧录了uboot、Linux kernel和rootfs,并没有烧录ENV分区。所以当我们在iNand空片上第一次烧写并启动时,ENV分区中为无效数据,uboot在启动过程中会从中读取环境变量,但是校验会失败。此时uboot会使用default_environment数组中的值作为默认环境变量使用。

如果调用了uboot的saveenv命令将当前内存中的环境变量烧写到iNand的ENV分区,下次uboot启动时就能够从ENV分区读取到正确可用的环境变量

注意:x210的uboot是将从iNand读取到的环境变量保存在malloc分配的堆空间,并没有读取到内存布局中的ENV区域!!!

说明3:设置gd->env_addr字段

在env_relocate函数的最后,会将保存环境变量值的数组地址保存在gd->env_addr字段。由于env_ptr->data为字符数组,所以此处可以不进行&操作

当然此处增加&操作,并不会导致指针值的不同,只是指针类型会有不同,

env_prt->data:char *类型

&(env_prt->data):char (*)[ENV_SIZE]类型

9)确定IP & MAC地址

说明1:IP地址和MAC地址均来自对环境变量的解析,且uboot代码中支持最多2块网卡

说明2:IP地址转换

获取ip地址时要进行2种转换,首先是将字符串转换成unsigned long类型,然后调用htonl函数实现大小端的转换。

说明3:simple_strtoul函数分析

参数说明:

const char *cp:入参,待解析的字符串

char **endp:出参,用于返回指向解析结束位置的指针

unsigned int base:入参,指定转换进制

进制判断模块:

① 如果以字符'0' 'x'开头,且下一个字符是字母或数字,则为十六进制

② 如果仅以字符'0'开头,且传入的base值为0,则为八进制

③ 如果不是以'0'或'0'和'x'开头,且传入的base为0,则为十进制

转换模块:

转换模块的核心是while条件的判断条件,注意关系运算符< 比逻辑与运算符&&优先级高

isxdigit(*cp) && (value = isdigit(*cp) ? *cp-'0' : (islower(*cp) ? toupper(*cp) : *cp)-'A'+10) < base

首先判断是否是字母或数字,如果是则转换为相应数值,并判断该数值是否小于转换进制

示例:

char *p = "192.168.0.1"

char **end;

调用simple_strtoul(p, &end, 10),返回值为192,*end指向字符'.'

10)devices_init函数分析

说明1:devices_init函数首先建立设备管理链表,然后根据配置项调用不同的设备初始化函数。uboot中的这种设备驱动管理方式,是从Linux内核的驱动框架衍生而来,并进行了简化

根据x210 uboot的配置,只会调用drv_system_init函数

说明2:设备管理链表分析

此处的设备管理链表结构可以拆分为设备结构和链表结构

① 设备结构device_t

device_t结构用于描述一个设备(此处为统一类型的设备,后文还有分析),根据该结构的注释,结构成员可分为5类:属性部分、start/stop函数、输出函数、输入函数、私有数据指针

② 链表结构ListStruct

该链表采用顺序方式实现,配合handlerecord结构实现内存管理,下面就ListStructTag的重要字段进行说明,

unsigned char itemList[1]:配合malloc函数,作为可变长数组使用,用于存放实际的设备结构体信息(C99中可以直接用零长度数组)

int percentIncrease + int minNumItemsIncrease:用于指导可变数组的扩张,一个为默认增长的百分比;一个为默认增长的元素个数。在ExpandListSpace函数中,可以通过参数识别不同的扩张策略

int listSize:当前顺序表可容纳的元素个数

int numItems:当前顺序表中的元素个数

int itemSize:每个元素的大小

在devices_init函数中,首先调用ListCreate函数新建设备管理链表,下面就分析一下ListCreate函数的实现,进而更清晰地说明ListStruct的使用方式

可见实际分配的内存是由HandleRecord结构管理的,该结构由指向无类型内存的指针和内存大小组成,NewHandle函数放回的指针类型为void **,数值为指向HandleRecord结构的指针

在ListCreate函数中,将NewHandle返回的void **类型指针强制转换为ListStruct **类型并加以使用。

也就是说在分配和使用的是同一片内存,但是解释的方式不同,通过强制类型转换实现在不同场景的应用(讲真~~这真的不是一种很清晰高明的方法~~尤其是为啥一定要在作为顺序表使用时,强制转换为ListStruct **这个二级指针类型~~,个人觉得一级指针就够用了)

说明3:serial_devices_init函数分析

该函数并未被调用,此处分析仅为说明uboot中的驱动管理框架。

① serial_initialize函数注册串口类设备结构

注意:serial_initialize函数实际也未调用,但是要分析serial_devices_init函数的原理,必须从serial_initialize说起

serial_initialize函数会根据配置注册不同的serial_device结构,其中serial_devices链表就是用于管理uboot中所有注册的串口类设备

② serial_devices_init函数注册统一的设备结构

serial_devices_init函数会遍历serial_devices链表,然后据此构建并注册统一的设备结构。

根据注释,这套驱动管理体系仅用于管理控制台类的设备

说明4:drv_system_init函数分析

drv_system_init函数注册了一个串口设备(使用统一设备类型),使用的hook函数就是串口的输入输出函数

注:目前的uboot中也只注册了这一个控制台类设备

11)jumptable_init函数分析

jumptable_init函数使用global data结构中的void **jt字段构建了函数跳转表,但是,这表在uboot中就没用到过~~

但是我们还是说明一下XF_MAX这个枚举常量

该枚举类型依靠EXPORT_FUNC宏来实现,该宏通过字符串拼接构成枚举常量值,根据<_exports.h>文件的内容,就是用函数名构成枚举常量值

12)console_init_r函数分析

说明1:控制台初始化的第二阶段

上文已经分析过控制台初始化第一阶段的函数console_init_f,该函数只是标识目前uboot中启用控制台,但并没有给控制台hook任何操作函数;console_init_r函数则是完成这部分操作。

说明2:查找第一个可用的输入输出设备

使用循环在devlist顺序表中查找可用的输入输出设备,此处能找到的也就是drv_system_init函数中注册的serial设备

从该循环的写法可知,当有多个输入输出设备可用时,只有第一个查找到的设备生效

说明3:绑定stdio文件与控制台设备

console_setfile函数用于实现stdio文件(stdin / stdout / stderr)与输入输出设备的绑定,该函数完成3个 任务,

① 尝试调用设备的start函数

② 设置stdio文件对应的设备

③ 更新函数跳转表指针

说明4:标识设备初始化完成

在gd->flags中增加GD_FLG_DEVINIT标志,标识设备初始化完毕。此时再调用printf函数,将会调用fputs(stdout, s)实现输出。而fputs函数则是调用stdout文件对应设备中hook的puts函数

说明5:打印stdio文件绑定的设备名

13)enable_interrupts函数分析

说明1:使能 / 关闭中断通过内嵌汇编实现,操作的是CPSR中的I位,是CPU端的中断使能(中断使能分为CPU端、VIC端、中断源端)

由于我们没有定义CONFIG_USE_IRQ宏,所以调用的是一个空函数

说明2:2个enable_interrupts实现

在lib_arm/interrupts.c和cpu/s5pc11x/interrupts.c文件中,均有enable / disable_interrupts函数的实现,经过验证,这2个版本的实现均被编译。最终通过打印验证,被执行的是cpu/s5pc11x/interrupts.c文件中的版本,具体原因我目前还无法解释~~

14)获取loadaddr & bootfile环境变量

说明:将loadaddr & bootfile这2个环境变量的值保存在全局变量中,这2个环境变量与系统启动相关

15)board_late_init函数分析

说明:目前版本中使用的是一个仅return 0的空函数,其他版本会设置bootcmd、bootdelay等环境变量

16)eth_initialize函数分析

说明:此处的eth_initialize仅会调用miiphy_init以初始化MII部件,而初始化的内容只是初始化mii_devs链表,并将current_mii指针置为NULL

关于uboot中网卡编程的具体实现,可参考体系结构相关笔记~~

17)x210_preboot_init函数分析

说明1:x210_preboot_init为x210开发板定制功能,用于在uboot阶段使用LCD显示logo

说明2:相关配置函数

fb_init函数用于设置frame buffer模式

lcd_port_init函数用于初始化与LCD相关的GPIO模式

lcd_reg_init函数用于配置LCD相关功能寄存器

说明3:display_log函数用于在LCD上显示logo,,而logo图像则以全局变量的形式硬编码在uboot代码中。可以通过专门的格式转换工具,将图像转换为显示所需的位图模式

18)check_menu_update_from_sd函数分析

说明1:check_menu_update_from_sd为x210开发板定制功能,用于实现镜像文件的自动烧录(常用于量产SD卡烧录系统)

说明2:工作流程

对SD卡进行分区,将升级镜像放置到SD卡的固定目录中,然后在uboot的启动过程中会检测升级标志位(此处为LEFT按键),如果该按键按下,则进入升级流程;如果该按键没有按下,则继续启动流程

启动流程打印如下,

升级流程打印如下(由于没有准备升级镜像,打印会显示升级失败),

在调用update_all函数升级之前,会先运行uboot命令fdisk -c 0用于给iNand分区,分区结果如下,

分区大小

分区用途

10MB

空出iNand的前10MB空间(目前这部分有MBR、uboot[BL1 & BL2]、Linux kernel)

256MB

CONFIG_PART_SIZE(p1分区)

120MB

SYSTEM_PART_SIZE(p2分区)

100MB

CACHE_PART_SIZE(p3分区)

剩余空间

用户空间(p4分区)

此处之所以将SYSTEM_PART_SIZE分区的大小标红,是因为在x210提供的源代码中,该分区为120MB;但是在提供的刷机镜像中,分区为256MB(实际配套的文件系统镜像也是256MB)

说明3:check_menu_update_from_sd函数分析

此处的检测方式就是将LEFT对应按键的GPIO设置为Input模式,然后读取输入值。当按键按下时,读到的值为0,因此会进入update mode

说明4:update_all函数分析

下面就逐个阶段分析update_all函数如何实现从SD卡升级

① 查找并初始化SD卡

由于是从SD卡中读取镜像进行升级,所以以参数1调用find_mmc_device函数,即查找第2个注册的mmc设备

根据之前的分析,由于iNand使用SD/MMC channel 0,SD卡使用SD/MMC channel 2,所以在smdk_s3c_hsmmc_init函数中对这2个channel进行了注册。由于先注册channel 0,后注册channel 2,因此以参数1查找mmc设备,找到的就是SD卡

② 设置fastboot升级参数

SD卡自动升级借用了fastboot架构,只是数据的来源不同。fastboot是从USB获得数据,而SD卡升级是从SD卡中读取升级镜像

fastboot的升级参数由struct cmd_fastboot_interface结构管理

在update_all函数中会重置interface结构中的3个字段,

nand_block_size = 2048 * 64 = 128KB

transfer_buffer = 0x3e000000

transfer_buffer_size = 0x11000000(272MB)

③ 设置fastboot分区表

fastboot中的分区由struct fastboot_ptentry结构描述,分区表则是一个fastboot_ptentry结构数组

在设置fastboot分区信息了,分为2个部分,

bootloader / kernel / rAMDisk分区是代码中直接设置,类型为FASTBOOT_PTENTRY_FLAGS_USE_MOVI_CMD,即uboot的movi命令会操作这些分区

config / system / cache / userdata分区则是从MBR分区表中获得,类型为FASTBOOT_PTENTRY_FLAGS_USE_MMC_CMD

④ 读取镜像实现升级

在获取SD的mmc设备结构体,然后在该设备上注册FAT文件系统,最后调用file_fat_read函数读取升级镜像文件。在获取文件后,调用rx_handler实现烧写

⑤ 重启系统

在完成升级后,调用do_reset函数实现重启。do_reset会调用reset_cpu函数,通过向SWRESET寄存器中写入1,实现software reset

至此,uboot启动的第二阶段就完成了,后续就进入main_loop函数实现Linux内核的启动或者进入uboot命令行~~

3. main_loop函数分析

1)配置命令行解析方式

uboot提供了2种命令执行方式,

① cli_simple_loop方式:

通过readline函数读取命令行内容,通过run_command函数执行命令

② hush parser方式

通过setup_file_in_str函数读取命令行内容,通过parse_stream_outer函数执行命令

两种方式的选择由CFG_HUSH_PARSER宏控制,x210 uboot中定义了该宏,因此使用的是hush parser方式(已上机验证)

2)判断是否进入fastboot模式

说明:在启动过程中,有2种情况会使得uboot进入fastboot模式,

① fastboot传输buffer的起始处被设置为"REBOOT-FASTBOOT"字符串

② keyboard按键被按下

此处将用到的GPIO均设置为keyboard模式,代码中共设置了KP_COL[0] ~ KP_COL[7]以及KP_ROW[0] ~ KP_ROW[7],并同时使能了keyboard中断

由于当前uboot并未使能中断,因此采用轮询的方式检测KP_ROW[7]按键是否按下。

根据底板原理图,x210中只预先安装了KP_KOL[0] ~ KP_KOL[3]。因此,此处的检查不会导致uboot进入fastboot模式

而且fastboot_preboot中的打印与x210启动过程也是符合的~~

3)获取bootdelay & bootcmd

说明1:获取这2个环境变量,是为了后续的自动启动Linux内核做准备。需要注意的是,自动启动内核的流程都是由CONFIG_BOOTDELAY宏控制的,如果不定义该宏,uboot会直接进入命令行。

因此,如果不想进入uboot命令行,而且要立即启动内核,需要将CONFIG_BOOTDELAY宏设置为0,而不是不设置该宏

说明2:如果未能从环境变量中却得bootdelay的值(e.g. uboot启动后删除内存中bootdelay变量),则使用x210_sd.h中定义的宏值

说明3:当前uboot的bootcmd如下,

① 从raw area的kernel分区读取Linux内核并加载到0x30008000处

② 从raw area的rootfs分区读取3MB的根文件系统(意指initrd)并加载到0x30B00000处

③ 调用bootm启动Linux内核同时传递initrd的地址

注意:实际上x210并未使用initrd这一虚拟根文件系统(实际也未烧写raw area的rootfs分区),而是使用了iNand中的system分区作为根文件系统。因此,后文将看到ramdisk的格式校验会失败

4)判断在倒计时前是否有按键按下

说明1:此处会检查在bootdelay的时间内串口是否有输入,如果串口有输入,则打断自动启动Linux内核的流程,进入uboot命令行;如果串口没有输入,则按照bootcmd环境变量的定,启动Linux内核

说明2:abortboot函数分析

① 检测方式

abortboot函数通过检查串口的UTRSTAT寄存器的bit 0,确定串口是否有输入,如果有则标识自动启动被打断

需要注意的是,此处需要将用户的输入读出,否则后续的输入会导致overrun ERROR(详细说明可参考体系结构串口编程笔记~~)

② 延时方式

bootdelay以秒为单位,此处构造循环,每10ms检查一次,连续检查100次后更新一次bootdelay时间值

内外两重循环的检测条件都是:计时/计数没到期 && 串口无输入

③ printf("\b")

'\b'为BS(backspace)退格字符,用于退回到上次打印bootdelay的位置,造成倒计时的效果

说明3:udelay函数分析

udelay函数是实现倒计时的核心函数,其实现就依赖于之前设置的PWM Timer4

实现udelay需要依赖3个全局变量,

time_load_val:初始化Timer 4时的计数值,此处为62500

lastdec:上次读取的TCNTO4的值

timestamp:本次计数已经完成的计数值(递增)

此处理解的关键是Timer计数是递减的,而timestamp计数是递增的,实现二者转化的关键就是全局变量lastdec。通过比较当前TCNTO4的值与lastdec的值,就可以计算出两次访问间已经过去的计数值。

uboot中的get_timer_masked函数用于完成timestamp的计算,其中还处理了计数绕回问题,即TCNTO4当前值 > lastdec时,说明Timer计数值至少归零一次,因此要分2段计算两次访问间经过的计数值,并将该值累加到timestamp中

有了上述基础就不难理解udelay函数了,该函数可以分为3个步骤,

① 根据定时值计算计数值

首先将定时值转换为对应的计数值,CFG_HZ宏定义了1s对应的计数值,因此tmo计算的就是usec us对应的计数值

此处之所以将usec > 1000的情况先除以1000,是防止usec * CFG_HZ计算结果溢出。需要注意的是,usec / 1000会导致精度损失,所以udelay函数并不能实现精确定时

② 根据计数值计算timestamp超时值

根据上文对get_timer_masked的分析,get_timer(0)可以获得当前的时间戳timestamp。在当前timestamp上加上所需计数值就是timestamp超时值,但是此处也要处理绕回问题。此处的绕回问题与之前的不同,get_timer_masked函数的绕回问题是Timer计数器减到0并开始reload计数;此处的绕回问题是记录timestamp的ulong类型溢出,进而发生绕回(即0xffffffff --> 0x0)。

当发生(tmo + tmp + 1) < tmp时,说明timestamp的剩余值已经小于所需计数值,此时的策略是将timestamp清零,重新开始计数。

此处的条件中为什么要"+1"呢? 这主要是为了防止tmp + tmo = 0xffffffff,因为这种情况一旦发生,可能导致永远无法退出循环(如果不能恰好读到timestamp = 0xffffffff时)。

如果tmp + tmo = 0xffffffff,由于判断式是tmp + tmo + 1,所以会进行绕回处理

③ 轮询timestamp并与超时值比较

构造while循环,反复读取当前timstamp,并将至与超时值进行比较。一旦达到超时值,则推出循环,计时结束。

分析至此,uboot要么开始执行bootcmd标识的命令,要么进入命令行。无论进入哪种状态,都是通过bootm命令实现Linux内核的启动。

4. bootm命令详解

1)Linux内核镜像格式

A. 从Image到zImage

根据Linux内核的编译过程打印可以得到如下信息

整理后可得如上图的Linux内核镜像格式关系,

① vmlinux文件是Linux内核编译链接后生成的ELF文件,该文件是不能作为镜像文件烧写的

② Image文件是vmlinux经过objcopy处理之后的纯二进制文件,其关系类似于uboot和uboot.bin

③ 对Image再次进行压缩,并将解压缩文件附加在文件开头,即构成了compress目录下的vmlinux文件(注意:解压缩文件本身是不压缩的!!!)

④ 将compress目录下的vmlinux文件经过objcopy处理为二进制文件,即zImage

此时Linux内核镜像文件已经从最初的78MB减小到3.5MB

B. 从zImage到uImage

uImage镜像格式为uboot制定,与Linux内核本身无关,但是编译内核时使用

make uImage

可以生成uImage镜像,但是需要注意的是,再未安装mkimage工具的情况下,调用make uImage会失败

而制作uImage镜像所需的mkimage工具在编译uboot的过程中,会在uboot的tools目录下生成,该工具的命令如下,

我们将该工具拷贝到/usr/local/bin目录下,即可在Linux内核目录中调用make uImage生成相应镜像

实际上uImage只是在zImage基础上增加了头部信息,供uboot启动时使用(通过文件比较工具可以得到验证~~)

2)uboot支持的Linux内核镜像格式

A. uImage & FIT uImage

uImage & FIT uImage为uboot原生支持的Linux内核镜像格式,上文中已经介绍了uImage,下面简略说明下FIT uImage的构成

FIT uImage的提出是为了适应Linux内核引入的FDT设备树(Flattened Device Tree)。引入FDT后,在编译Linux内核时,不必特意指定具体的架构和SoC,只需告诉内核本次编译需要支持哪些板级的platform即可。最终会生成一个kernel image以及多个和具体开发板相关(e.g. ARCH / SoC)的FDT image(即dtb文件)

为了适应该趋势,uboot在原有uImage的基础上引入了FIT uImage格式,其中的FIT就是flattened image tree的简称。在FIT uImage中包含多个dtb文件,进而可以方便地选择使用哪个dtb文件启动Linux内核

引入FIT uImage后,原先的uImage就被成为Legacy uImage

B. zImage

uboot本身其实不支持zImage格式(可参考uboot官网源代码),但是由于该格式使用简单(比如A10,即使内核使用了设备树,也依然使用zImage格式的Linux内核),在很多uboot的移植版本中均添加了对zImage格式的支持

支持zImage的方式就是按照uImage的格式改造zImage的头部信息,然后跳过原代码中对uImage头部信息的校验,直接设置校验通过,并跳转到do_bootm_linux函数运行(详情见下文分析)

3)头部信息数据结构

A. image_header_t

image_header_t即mkimage工具给zImage增加的64B头部信息,是uImage文件的头部结构

B. bootm_headers_t

bootm_headers_t为调用do_bootm函数时,Legacy & FIT uImage共用的结构,由于x210的uboot并未支持FIT,所以只使用了红框中的部分字段

注意:后续的分析会略过与LMB相关的内容,因为我还没看懂~~

4)zImage镜像文件启动流程

A. 准备bootm_headers_t结构

说明:经过验证,getenv_bootm_low函数将返回bank 0内存的起始地址(0x30000000),getenv_bootm_size函数将返回内存bank 0内存的大小(256MB)

上面2个函数中使用的CONFIG_ARM宏由uboot顶层目录的arm_config.mk提供

B. 改造zImage头信息

说明1:如果bootm命令中包含启动Linux内核的地址(即Linux内核的加载地址),将从命令行获得该地址;否则将使用默认值(load_addr = 0x30000000,可在x210_sd.h中配置)

说明2:验证当前镜像是否为zImage格式

判断zImage镜像 + 36B处的ulong值是否为0x016f2818

说明3:改造zImage头部信息

通过hdr = (image_header_t *)addr,以uImage头部信息的格式来解释zImage头部信息,然后填充相关字段

注意entry point字段调用了ntohl函数,即处理了多字节整型的大小端问题,打印结果如下,

说明4:最后直接标识头部信息验证成功,并跳转到after_header_check标号处继续运行,其间跳过的就是对uImage头部信息的校验

在after_header_check标号处就是根据头部信息中操作系统的类型调用不同的启动函数,此处将调用do_bootm_linux函数,而调用的参数就是do_bootm的参数 + bootm_headers_t结构体

C. 获取bootargs

bootargs为uboot向Linux内核传递的启动参数,此处将用于构建commandline tag

D. 获取Linux内核entry point

说明1:定义函数指针theKernel,该函数指针类型必须符合Linux内核规定的启动方式(后文有详述)。在获得了Linux内核的entry point后,将该值赋给theKernel指针,后续就是利用theKernel指针启动内核

说明2:image_get_ep的由来

uboot源代码中完全没有image_get_ep的实现,通过反汇编发现该标号与image_get_hdr_l相关

我们接着查找image_get_hdr_l,就可以发现image_get_ep的实现方式

#define image_get_hdr_l(ep) \

static inline uint32_t image_get_ep(image_header_t *hdr) \

{ \

return uimage_to_cpu(hdr->ih_ep); \

}

可见此处是利用带参数宏定义了一系列函数,用于取得image_header_t结构体中的各个字段。我们调用的image_get_ep就是调用uimage_to_cpu(hdr->ih_ep),而uimage_to_cpu实际上只是调用ntohl函数

这也解释了在改造zImage镜像文件头部信息时为什么在赋值ih_ep字段时要调用ntohl函数,实际打印如下。

E. 获取开发板机器码

说明:uboot中首先使用了gd->bd中设置的开发板机器码,然后再尝试从环境变量中获取开发板机器码,且环境变量中的优先级更高

F. 获取ramdisk

说明1:x210的uboot支持使用initrd作为虚拟根文件系统启动,因此需要调用boot_get_ramdisk函数对ramdisk进行校验

目前的bootcmd环境变量为,

bootcmd=movi read kernel 30008000; movi read rootfs 30B00000 300000; bootm 30008000 30B00000

其中的30B00000就是ramdisk的加载地址。但是需要注意2点,

① 此处从iNand中读取的ramdisk实际上是无效的,我们并没有在iNand的相应区域烧写有效的ramdisk镜像(而且从bootargs环境变量分析,当前是使用iNand中的part 2分区作为根文件系统)

② 使用bootm命令时,可以不使用ramdisk文件系统,即可以不加ramdisk加载地址

说明2:boot_get_ramdisk函数分析

从boot_get_ramdisk函数的注释可知,该函数用于获取并校验ramdisk,只有获取到ramdisk但校验失败才会返回1;如果无法获取或获取到并校验成功,则返回0。在获取到并校验成功时,rd_start和rd_end这2个出参将分别存储ramdisk的其实地址和结束地址。

下面简要分析下该函数的流程,

① 检查bootm命令参数

如果使用bootm命令时argc >= 3则说明可能有ramdisk地址参数,其中如果argv[2]为"-",则说明忽略ramdisk;否则从argv[2]解析ramdisk地址

② 获取并校验ramdisk

由于x210没有使用dataflash,genimg_get_image函数只是将形参值返回。在genimg_get_format函数中,将检查image_header_t类型的image字段,以判断当前Linux内核镜像的格式,如前所述,uboot本身并不支持zImage格式,所以此处返回的格式会导致switch语句进入default分支,这点与启动打印也是匹配的。

也就是说,在目前uboot的逻辑下,只有使用uImage(Legacy & FIT)格式的镜像才能使用ramdisk启动,使用zImage镜像则无法使用initrd机制

G. 建立tag list

Linux内核源码中的Documentation/arm/booting文档介绍了bootloader与内核的通信协议,目前有2种方式:标签列表(tag list)或设备树(device tree),x210 uboot中使用tag list方式

① struct tag介绍

struct tag结构中通过tag_header结构确定当前tag的类型及大小,然后使用联合体囊括可能的tag结构

tag list就是由各个结构拼接组成,其中以ATAG_CORE标识的start_tag开始,以ATAG_NONE标识的end_tag结束(这2个tag是必须的~~)

需要注意的是,bootloader向Linux内核传递参数的方式是由Linux内核确定的,uboot作为bootloader中的一种,要想成功启动Linux内核,必须遵照该结构进行参数传递

以tag list传参为例,uboot必须遵照Linux内核中关于tag list的类型定义传参,Linux内核在启动后才能正确解析并使用这些参数

内核中的相关定义在arch/arm/include/asm/setup.h中,

uboot中的相关定义在include/arm_asm/setup.h中

根据条件编译宏,x210 uboot共建立了如下tag,下面将逐个分析

② setup_start_tag

说明1:params为struct tag类型指针变量,值为bd->bi_boot_params,即要传递给内核的tag list地址。在board_init函数中,该字段被初始化为PHYS_sdram_1 + 0x100,即0x30000100

说明2:tag_size宏分析

tag_size宏实际上计算的是struct tag中的tag_header和当前tag类型的大小之和,注意,此处不能计算struct tag中联合体的大小

同时要注意,此处计算大小的单位是字(4B)而不是字节(B)

说明3:tag_next宏分析

使用tag_next宏可以让params指针越过当前tag的大小,指向下一个可用的位置。此处在进行指针运算时,会将params强制转换为u32 *类型,这也就是计算hdr.size字段时以字为单位的原因

③ setup_memory_tags

此处构造循环,根据内存bank的数量构造memory tag,其中struct tag_mem32结构记录的就是每个内存呢bank的物理起始地址和大小

④ setup_commandline_tag

说明1:如果bootargs为空(commandline指针为NULL或bootargs内容为空字符串)则不会建立commandline tag

说明2:commandline tag大小的计算

由于bootargs字符串的长度可变,所以不能用tag_size宏计算commandline tag的大小

(sizeof (struct tag_header) + strlen (p) + 1 + 4) >> 2

+1:计算\0的空间,strlen返回值不包含\0

+4:除法计算时向上取整

说明3:stuct tag_cmdline中的cmdline数组实际实现了零长度数组的功能

⑤ setup_initrd_tag

initrd tag记录了ramdisk的起始地址和大小,x210 uboot中initrd_start & initrd_end均为0

⑥ setup_mtdpartition_tag

说明1:mtdpartition tag实际是供nand flash使用的分区信息,x210并未使用(如前文分析,x210的SD卡和iNand均使用MBR分区表提供分区信息)

说明2:使用mdtpart可以向内核传递MTD层的分区信息,其实Linux内核中也会维护nand flash的分区信息,这2份信息最好一致。需要注意的是,此处最多只能传递3个分区信息(mtd_part_size数组按定长方式使用)

具体可参见下文:

https://www.cnblogs.com/pengdonglin137/p/4646269.html

⑦ setup_end_tag

说明:end_tag中将hdr.size置为0,即该tag只有头部信息,标识tag list的结束

H. Linux内核启动前清理

① 启动内核对CPU & cp15的要求

CPU:

关闭所有中断

CPU处于SVC模式

cp15:

关闭MMU

I Cache可以关闭也可以打开

D Cache必须关闭

② cleanup_before_linux函数分析

说明:cleanup_before_linux函数完成了如下任务,

a. 关闭中断(CPSR中I & F位置1)

b. 关闭I & D Cache(将cp15控制寄存器的bit 2 & bit 12清零)

c. 使I Cache中数据失效

d. 进行数据同步屏障操作

注意:该函数中并没有关闭MMU

分析:x210中之所以不关闭MMU就启动内核,是因为x210对0x30000000 ~ 0x3fffffff范围内的内存有2组映射关系

其一是0x30000000 ~ 0x3fffffff的线性映射,其二是0xc0000000 ~ 0xcfffffff的高端地址映射。所以使用0x30000000 ~ 0x3fffffff的虚拟地址,也能访问到物理地址0x30000000 ~ 0x3fffffff

I. 启动内核

说明1:由于此前已经将Linux内核entry point的值赋给theKernnel(0x30008000),此处直接以函数指针的方式启动内核

说明2:启动内核的参数

根据ATPCS规范,此处调用theKernel的三个参数将分别使用r0、r1、r2传递。根据Linux内核Documentation/arm/booting文档的要求,启动Linux内核时需要传递的参数如下,

r0 = 0

r1 = 开发板机器码

r2 = tag list在内存中的物理地址

补充:在ARM Linux支持设备树(Device Tree)后,bootloader将dtb的地址放入r2寄存器中。

当然,ARM Linux也支持直接把dtb和zImage绑定在一起的模式(内核ARM_appendED_DTB选项,Use appended device tree blob to zImage),此时r2寄存器就不再需要填充dtb的地址了

5. 总结:Linux启动对bootloader的要求

根据上文的分析,bootloader的设计都是围绕如何启动Linux内核展开的,现在就根据Linux内核Documentation/arm/booting文档,结合整篇笔记的内容,回顾启动ARM Linux内核的内容

1)启动ARM Linux内核需要完成的任务

A. 初始化内存(mandatory)

B. 初始化至少一个串口(optional

C. 获取开发板机器码(mandatory)

D. 设置tag list(mandatory)

E. 启动Linux内核(mandatory)

2)zImage镜像加载地址要求

A. 推荐将zImage镜像加载到内存起始地址偏移32KB(0x8000)处

这也就是x210将内存加载到0x30008000的原因

B. 内存中zImage镜像之后的16KB将用于存放一级页表

3)其他设置要求

A. 关闭DMA

B. CPU寄存器要求

r0 = 0

r1 = 开发板机器码

r2 = tag list在内存中的物理地址

C. CPU模式要求

关闭所有中断(IRQ & FIQ)

CPU处于SVC模式

D. Cache & MMU

关闭MMU

I Cache可以关闭也可以打开

D Cache必须关闭

E. bootler直接跳转到Linux内核的第一条语句开始运行

相关阅读

华为java社招面试题目及全部流程详解

华为的招聘流程一直非常复杂,本人最近参加了华为的社招,对全部流程有一个总体了解,包括流程,面试题目类型,分享给大家,希望大家能有所帮

require_once方法引入并执行的流程解析

function.php的代码如下: <?php function sayhello4(){echo("hello world4");} ?> inde.php的代码如下: <?php require_once

产品经理如何优雅地“背锅”?不妨设立一定的制度和流程

作为时常周旋于公司各部门之间的关键人物,产品经理的决策牵动着各方利益。当上线后的产品效果未达到预期时,难免会成为“背锅侠”的

完整的软件开发流程是怎样的?

在it圈混迹了这么久,做过各种各样的工作。但是我确一直不知道一个软件从无到有到底是怎么开发的。于是就产生了强烈的好奇心:一个软

最新Apple苹果开发者账号AppleID注册流程

1、登陆开发者官网: https://developer.apple.com/ 2、如果有苹果设备,可以直接使用你设备的appleID,后面的步骤一样的,这是少了这个

分享到:

栏目导航

推荐阅读

热门阅读