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内核的第一条语句开始运行
相关阅读
华为的招聘流程一直非常复杂,本人最近参加了华为的社招,对全部流程有一个总体了解,包括流程,面试题目类型,分享给大家,希望大家能有所帮
function.php的代码如下: <?php function sayhello4(){echo("hello world4");} ?> inde.php的代码如下: <?php require_once
作为时常周旋于公司各部门之间的关键人物,产品经理的决策牵动着各方利益。当上线后的产品效果未达到预期时,难免会成为“背锅侠”的
在it圈混迹了这么久,做过各种各样的工作。但是我确一直不知道一个软件从无到有到底是怎么开发的。于是就产生了强烈的好奇心:一个软
1、登陆开发者官网: https://developer.apple.com/ 2、如果有苹果设备,可以直接使用你设备的appleID,后面的步骤一样的,这是少了这个