第3章 U-boot移植 A

时间:2022-08-03 04:27:10

3.1 mini-uboot 启动过程简单分析

单片机有最小系统,所谓最小系统,就是单片机能正常工作所需要的最少外设。对于Uboot来说,同样有个最小系统,因为Uboot最主要的功能就是引导内核。下面我们通过一个简单的Mini-Uboot来分析Uboot的启动加载过程。(只是分析过程,此Uboot具有引导内核功能)
注:这个uboot 只是具有基本的内核引导功能,只是作为前期简单的学习使用,入门而已,并不是正常的uboot 启动流程
下面是mini-uboot 的根目录树状图:

第3章 U-boot移植 A

图1根目录树状图

我们拿到一个工程,想了解它的功能,最方便的就是读它的makefile。

3.1.1 Makefile

sinclude include/config.mk

#ARCH=arm
#CPU=arm920t
#VENDOR=samsung
#SOC=s3c2410
#BOARD=smdk2410

SRC_TREE:=$(shell pwd)
MKCONFIG=$(SRC_TREE)/mkconfig

INCLUDE_PATH=include
DRIVER_PATH=driver
LIB_DIR=lib

CFLAG=-mabi=apcs-gnu -fno-builtin -fno-builtin-function -g -O0 -c -I$(INCLUDE_PATH) -I$(DRIVER_PATH) -o
LDFLAG=-Tcpu/arm/arm_cortexa8/map.lds -o
OBJS= cpu/$(ARCH)/$(CPU)/start.o
OBJS+=lib_arm/board.o
OBJS+=board/$(VENDOR)/$(BOARD)/lowlevel_init.o
OBJS+=board/$(VENDOR)/$(BOARD)/mem_setup.o
OBJS+=board/$(VENDOR)/$(BOARD)/nand.o
OBJS+=driver/uart.o
OBJS+=lib/string.o
OBJS+=common/do_go.o
OBJS+=common/main.o

ifeq ($(ARCH), arm)
CROSS_COMPILE=arm-cortex_a8-linux-gnueabi-
endif

PROJ_NAME=mini_uboot

all: $(OBJS)
$(CROSS_COMPILE)ld $(OBJS) $(LDFLAG) $(PROJ_NAME).elf
$(CROSS_COMPILE)objcopy -O binary $(PROJ_NAME).elf $(PROJ_NAME).bin
$(CROSS_COMPILE)objdump -D $(PROJ_NAME).elf > $(PROJ_NAME).dis
cp *.bin /tftpboot

%.o: %.S
$(CROSS_COMPILE)gcc $(CFLAG) $@ $<
%.o: %.s
$(CROSS_COMPILE)gcc $(CFLAG) $@ $<

%.o: %.c
$(CROSS_COMPILE)gcc $(CFLAG) $@ $<

fsc100_config: # ARCH CPU VENDOR BOARD SOC
$(MKCONFIG) $(@:_config=) arm arm_cortexa8 samsung fsc100 s5pc100
#mkconfig fsc100 arm arm_cortexa8 samsung fsc100 s5pc100

smdk2410_config: # ARCH CPU VENDOR BOARD SOC
$(MKCONFIG) $(@:_config=) arm arm920t samsung smdk2410 s3c2410

clean:
@rm -rf $(OBJS) *.bin *.elf config.mk

这里以2440为例,咱们来分析:

#ARCH=arm
#CPU=arm920t
#VENDOR=samsung
#SOC=s3c2410
#BOARD=smdk2410

架构为arm,CPU为arm920t,生产商 samsung,片上系统sc2410,板子为smdk2410。

OBJS= cpu/$(ARCH)/$(CPU)/start.o
OBJS+=lib_arm/board.o
OBJS+=board/$(VENDOR)/$(BOARD)/lowlevel_init.o
OBJS+=board/$(VENDOR)/$(BOARD)/mem_setup.o
OBJS+=board/$(VENDOR)/$(BOARD)/nand.o
OBJS+=driver/uart.o
OBJS+=lib/string.o
OBJS+=common/do_go.o
OBJS+=common/main.o

OBJS为依赖文件,生成的.o文件。

ifeq ($(ARCH), arm) 
CROSS_COMPILE=arm-cortex_a8-linux-gnueabi-
endif

根据相应的架构,制作相应的交叉编译工具。

all: $(OBJS) 
$(CROSS_COMPILE)ld $(OBJS) $(LDFLAG) $(PROJ_NAME).elf
$(CROSS_COMPILE)objcopy -O binary $(PROJ_NAME).elf $(PROJ_NAME).bin
$(CROSS_COMPILE)objdump -D $(PROJ_NAME).elf > $(PROJ_NAME).dis

第一步:连接 ;第二步:格式转换;第三步:反汇编 ” >” 为重定向的意思;

%.o: %.S
$(CROSS_COMPILE)gcc $(CFLAG) $@ $<
%.o: %.s
$(CROSS_COMPILE)gcc $(CFLAG) $@ $<
%.o: %.c
$(CROSS_COMPILE)gcc $(CFLAG) $@ $<

将所有的.S 文件、.s文件、.c文件编译成.o文件。
注意:.S文件可以在编译过程接受参数,.s文件不可以。

3.1.2链接文件

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
/*OUTPUT_FORMAT("elf32-arm", "elf32-arm", "elf32-arm")*/
OUTPUT_ARCH(arm)
ENTRY(_start) //指定入口地址
SECTIONS //段信息
{
/* . */
. = 0x22000000; //elf文件的入口地址
. = ALIGN(4); //指定四字节对齐
.text : //代码段
{
cpu/arm/arm_cortexa8/start.o(.text) //确保执行的第一段代码是start.o
*(.text) //所有代码段融合在一起
}
. = ALIGN(4);
.rodata : //只读数据段
{ *(.rodata) } //所有数据段
. = ALIGN(4);
.data : //数据段
{ *(.data) }
. = ALIGN(4);

_start_bss = .; //bss段开始地址
.bss :
{ *(.bss) }
_end_bss = .; //bss段结束地址,两者可确定bss段大小
}

3.1.3 start.s文件

(Uboot执行的第一个文件)

@ 汇编中的宏
.equ USER_MODE, 0x10 @define USER_MODE 0x10
.equ IRQ_MODE, 0x12
.equ SVC_MODE, 0x13
.equ MODE_MASK, 0x1f

.section .text
.global _start
@ 不支持异常处理的,这里只写了复位异常处理
_start:
vector:
b reset_handler
nop @undef ......
nop
nop
nop
nop
nop
nop

reset_handler:
@step 1: svc close irq fiq //第一步:将运行模式改成SVC模式
mrs r0, cpsr //修改cpsr模式位
bic r0, r0, #0x1f
orr r0, r0, #0xc0 @IRQ FIQ //关闭IRQ FIQ
msr cpsr_c, r0
@step 2: cache 关闭I CACHE D CACHE //第二步:关闭cache,直接运行,不需缓存
mrc p15, 0, r0, c12, c0, 0
bic r0, #0x1000
bic r0, #0x2
mcr p15, 0, r0, c12, c0, 0

@step 3: //第三步:调用电路板级初始化程序, system clock , dram, watchdog
@bl low_level_init //初始化时钟、dram、关闭看门狗

@step 4: sp-> 0x30000000 //第四步:设置栈指针,使其指向一个地址即可
ldr sp, =0x2e000000

@step 5: mini_uboot.bin > 16KB bin < 16KB
@step 5 代码自搬移
@copy_miniuboot_rto_sdram 如果你的代码大于了16KB代码需要实现自我搬移
@step 6: //第六步:清除BSS段,BSS段大小由链接文件里确定
@STEP 6.1 , 清除 BSS段
@
clear_bss:
ldr r0, =_start_bss @| BSS 起始地址
ldr r1, =_end_bss @| BSS 终止地址
mov r2, #0
bss_loop:
cmp r0, r1
strne r2, [r0], #4
bne bss_loop

@step 7, 进入C //跳转到C程序入口
b start_armboot
stop:
b stop
.end

3.2 Exynos4412 所用外存 —— eMMC

Exynos4412所用外存不是原来的Nand Flash 与 Nor Flash,而是eMMC。eMMC是什么呢?和Nand Flash有什么区别呢?

3.2.1 eMMC概述

eMMC(Embeded MultiMedia Card):它并非是一种全新尺寸的存储卡,而是由MMC协会所订立的内嵌式存储器标准规格,而且还是专门为手机和移动嵌入式产品设计的。eMMC简单来说是一个嵌入式存储解决方案,除了常规意义的存储器之外,集成了一个控制器,并且提供了一个统一的标准接口。
eMMC的设计概念,就是为了简化手机内存储器的使用,将NAND Flash芯片和控制芯片设计成1颗MCP芯片,手机客户只需要采购eMMC芯片,不需要处理其它繁琐的NAND Flash兼容性和管理问题。eMMC最大的优点,就是缩短新产品的上市周期和研发成本,加速产品的推陈出新速度。
eMMC说白了,其实就是在原有内置存储器的基础上,又额外加了一个控制芯片,最后再以统一的方式封装,并预留一个标准接口,以便手机客户拿来直接使用。这有点类似于联发科的MTK,或者是高通的Snapdragon解决方案,手机终端厂商买的不只是一颗CPU,而是一整套方案。如此一来,操作简化了不少,还避免了不同厂牌硬件之间的兼容性问题。
手机的内置存储读取速度提升,到来的直接好处就是手机执行效率的提升。无论是播放音乐视频,还是浏览网页,以及最耗硬件资源的玩游戏,在处理器、RAM之外,所谓的ROM读取速度也是相当重要的一项指标。
eMMC=NAND Flash+闪存控制芯片+标准接口封装

第3章 U-boot移植 A

图2

3.2.2 eMMC的优点

eMMC目前是最当红的移动设备本地存储解决方案,目的在于简化手机存储器的设计,由于NAND Flash芯片的不同厂牌包括三星、KingMax、东芝(Toshiba)或海力士(Hynix)、美光(Micron)等,入时,都需要根据每家公司的产品和技术特性来重新设计,过去并没有哪个技术能够通用所有厂牌的NAND Flash芯片。
而每次NAND Flash制程技术改朝换代,包括70纳米演进至50纳米,再演进至40纳米或30纳米制程技术,手机客户也都要重新设计,但半导体产品每1年制程技术都会推陈出新,存储器问题也拖累手机新机种推出的速度,因此像eMMC这种把所有存储器和管理NAND Flash的控制芯片都包在1颗MCP上的概念,逐渐风行起来。
eMMC的设计概念,就是为了简化手机内存储器的使用,将NAND Flash芯片和控制芯片设计成1颗MCP芯片,手机客户只需要采购eMMC芯片,放进新手机中,不需处理其它繁复的NAND Flash兼容性和管理问题,最大优点是缩短新产品的上市周期和研发成本,加速产品的推陈出新速度。
闪存Flash的制程和技术变化很快,特别是TLC技术和制程下降到20nm阶段后,对Flash的管理是个巨大挑战,使用eMMC产品,主芯片厂商和客户就无需关注Flash内部的制成和产品变化,只要通过eMMC的标准接口来管理闪存就可以了。这样可以大大的降低产品开发的难度和加快产品上市时间。
eMMC可以很好的解决对MLC和TLC的管理,ECC除错机制(Error Correcting Code)、区块管理(Block Management)、平均抹写储存区块技术(Wear Leveling)、区块管理(Command Management),低功耗管理等。
eMMC核心优点在于生产厂商可节省许多管理NAND Flash芯片的时间,不必关心NAND Flash芯片的制程技术演变和产品更新换代,也不必考虑到底是采用哪家的NAND Flash闪存芯片,如此,eMMC可以加速产品上市的时间,保证产品的稳定性和一致性。

3.2.3 eMMC的结构与规格

eMMC 结构由一个嵌入式存储解决方案组成,带有MMC (多媒体卡)接口、快闪存储器设备及主控制器—— 所有在一个小型的BGA 封装。接口速度高达每秒52MB,eMMC具有快速、可升级的性能。同时其接口电压可以是1.8v 或者是3.3v。
eMMC ( Embedded Multi Media Card) 采用统一的MMC标准接口, 把高密度NAND Flash以及MMC Controlle封装在一颗BGA芯片中。针对Flash的特性,产品内部已经包含了Flash管理技术,包括错误探测和纠正,flash平均擦写,坏块管理,掉电保护等技术。用户无需担心产品内部flash晶圆制程和工艺的变化。同时eMMC单颗芯片为主板内部节省更多的空间。

第3章 U-boot移植 A

图3

3.3 Exynos4412启动过程分析

学习Exynos4412启动流程前,我们先看看三星4412芯片启动框图

第3章 U-boot移植 A

图4 启动流程

我们从图中可以看到4412内部有64K的ROM和256K SRAM,在ROM中已经固化好了一段代码,当硬件上电后首先运行的就是这段代码,这段代码三星起名为BLO(iROM BOOT 代码)。其作用是初始化SRAM,并将eMMC中256k代码拷贝到SRAM中,进行初始化DRAM。在图中我们很清楚看到这一个运行过程。
1、在芯片的iROM中已经固化一个代码,当硬件上电后就读取OM电平从而确定硬件设置的启动模式:0110为从eMMC启动,1000位SD卡启动;
2、把已经设置启动存储单元代码复制到内部RAM中并跳转到RAM运行;
3、运行OS;

3.3.1 iROM

iROM把启动设备上特定位置处的程序读入片内存 (iRAM) ,并执行它。这个程序被称为 BL1(Bootloader 1) ,BL1 是三星公司提供的,无源码。
BL1又把启动设备上另一个特定位置处的程序读入片内内存,并执行它。这个被称为 BL2(Bootloader 2) ,是我们编写的源码。
下图是 iROM 启动流程图:

第3章 U-boot移植 A

图5 iROM 启动流程图

由上图可以看出,首先关闭看门狗,关闭中断及MMU,关闭数据缓存,打开指令缓存,清除TLB,然后将其他核进入IDLE模式,只留CPU0,这里有了第一个跳转分支,IROM判断当前启动模式,是冷启动还是唤醒,如果是唤醒模式,那么就是直接跳转到BL1,在BL1里面我们会再次判断是否是唤醒模式,如果是就直接跳转到唤醒函数,一般都是linux内核的唤醒句柄。当然在裸机里都是冷启动的,休眠唤醒一般是不需要关注的,当然如果你的裸机程序需要支持休眠唤醒,就需要增加相应的代码了。
继续分析,设置IRQ及SVC模式的栈空间,这个时间,栈地址是其内部的一片IRAM,这小片RAM是IROM运行的外部随机存储器,没有这片小内存,IROM是无法运行的。接下了就是初始化IROM里面所使用的各种变量,初始化只读数据段,未初始化数据段清零,导出部分核心函数,这个函数可以在BL1中使用,获取当前复位的状态,设置系统时钟分频,获取OM管脚配置模式,这里可以从多种外设启动,具体启动模式如下表:
表1

第3章 U-boot移植 A

简单地说,iROM就是先设置程序运行环境 (比如关看门狗、关中断、关MMU 、设置栈 、设置栈 、启动 PLL 等 );然后根据OM引脚确定启动设备 (NAND Flash/SD 卡/其他 ),把 BL1 从里面读出存入iRAM;最后启动 BL1 。

3.3.2 BL1

下图是 BL1 的启动过程

第3章 U-boot移植 A

图6

单地说,也是设置程序运行环境(初始化中断、设置栈等 );然后从启动设备上把 BL2读入iRAM;最后启动它。

3.3.3 SD卡作为启动方式

第3章 U-boot移植 A

图7

BL1 位于SD卡偏移地址 512字节处(即从第一个扇区开始,前面有一个扇区保留,每个扇区512字节,为什么保留第一个扇区,如果有同学对DOS分区表有过研究,就能明白其中的道理了,第一个扇区是分区表的配置区),iROM从这个位置读入8K 字节的数据,存在iRAM地址 0x02021400位置处。 所以 BL1不能大于8K 。
IROM计算校验和且验证通过后并解密BL1成功后就可以跳转到BL1了,至此IROM已执行完备,权限已交由BL1了,补充说明一下,解密BL1是加密模式启动时才需要的,非加密模式启动是无需解密BL1的。
BL2 位于 SD 卡偏移地址 (512 +8K)字节处,BL1从这个位置读入14K 字节的数据,存在iRAM 地址 0x02023400 处。 BL2 不能大于(14K – 4) 字节,最后 4字节用于存放较验码(在汇编流水灯试验中我们用mkbl2工具制作的BL2,其中mkbl2工具最主要的作用就是计算出校验码)。
如果我们的程序大于 (14K – 4) 字节,那么需要截取前面 (14K – 4) 字节用来制作BL2并烧入SD卡偏移地址 (512 +8K) 字节处。当BL2启动后,由它来将存放在SD卡另外位置的、完整程序读入内存。

3.4 U-boot 编译流程分析

u-boot-2013.01 中有上千文件,要想了解对于某款开发板,使用哪些文件、哪些文件首先执行、可执行文件占用内存的情况,最好的方法就是阅读它的Makefile。
根据顶层Readme文件的说明:
第3章 U-boot移植 A
可以知道如果使用开发板board/,就先执行“make _config”命令进行配置,然后执行“make all”, 就可以生成如下3个文件:
U-Boot.bin:二进制可执行文件,它就是可以直接烧入eMMC中的文件。
U-Boot : ELF格式的可执行文件。
U-Boot.srec : 摩托罗拉格式的可执行文件。
对于Exynos4412开发板,这里用的其实是Fs4412,执行“make fs4412_config”、“make all”后生成的u-boot-fs4412.bin可以烧入eMMC中执行。

3.4.1 U-Boot 配置过程

1、在顶层Makefile中可以看到如下代码:(Makefile)
第3章 U-boot移植 A
假定在u-boot-2013.01的根目录下编译,则其中的MKCONFIG 就是根目录下的mkconfig文件(mkconfig是shell脚本文件)。
%_config是GUNmake语法层,表示的是所有以”.config”结尾的文件。$(@:_config=)的结果就是将“fs4412”中的“——config”去掉,结果为fs4412。
第3章 U-boot移植 A
MKCONFIG 就是执行mkconfig。所以 “make fs4412_config”实际上就是执行如下命令:
./mkconfig fs4412 arm armv7 fs4412 samsung exynos
2、mkconfig 的作用
前面已经提到,mkconfig就是一shell脚本, 具体作用如下:
a – 解析boards.cfg fs4412相关数据(boards.cfg)
第3章 U-boot移植 A
这是我们自行添加的.确定开发板名称BOARD_NAME。

b – 针对平台作了一系列链接,创建到平台、开发板相关的头文件的链接,即mkconfig。
第3章 U-boot移植 A
c –创建顶层Makefile包含的文件 include/ config.mk
第3章 U-boot移植 A
inlucde / config.mk 内容如下:
第3章 U-boot移植 A
d – 创建开发板相关的头文件inlucde/config.h
第3章 U-boot移植 A
Include/config.h导出结果如下:
第3章 U-boot移植 A
U-Boot 还没有类似Linux一样的可视化配置界面(比如使用 make menuconfig 来配置),要手动修改配置文件 include/configs/.h 来裁剪、设置U-Boot.
配置文件中有以下两类宏。
1) 一类是选项(Options),前缀为“CONFIG”,它们用于选择CPU、SOC、开发板类型,设置系统时钟、选择设备驱动等。
第3章 U-boot移植 A
2)另一类是参数(Setting),前缀为“CFG_”,它们用于设置malloc缓冲池的大小、U-Boot 下载文件时的默认加载地址、Flash的起始地址等。

uboot 执行通过宏来判断:宏在头文件中定义。

#ifdef CONFIG_TEST
run_test();
#endif

某头文件
#define CONFIG_TEST
可以这样认为,“CONFIG_”除了设置一些参数外,主要用来设置U-Boot的功能、选择使用文件中的哪一部分;而“CFG_”用来 设置更细节的参数。

3.4.2 U-Boot 的编译、链接过程

配uboot 编译通过Makefile来判断:
obj-y += xx.o xx.o 在编译时,只编译obj-y
obj-$(CONFIG_XX) = xx.o xx.o 如果CONFIG_XX为y,则此文件会被编译进u-boot.bin置完后,执行“make all” 即可编译:
找第一个目标all:
第3章 U-boot移植 A

上面代码是对u-boot进行格式转换,变成二进制bin格式之后,再加一些校验与4412开如平台加密信息。
依赖u-boot:
第3章 U-boot移植 A
先总结一下U-Boot 的编译流程:
a – 首先编译 cpu /$(CPU)/start.S,对于不同的CPU,还可能编译 cpu/$(CPU)下的其他文件;
b – 然后,对于平台/开发板相关的每个目录、每个通用目录都使用它们个字的Makefile生成相应的库;
c – 将a、b 步骤生成的.o .a文件按照 board /$(BOARDDIR)/config.mk文件中指定的代码段起始地址、board/$(BOARDDIR)/config.mk文件中指定的代码段起始地址、board/$(BOARDDIR)/U-Boot.lds链接脚本进行链接。
d – 第c步得到的是ELF格式的U-Boot,后面的Makefile还会将它转换成二进制格式、S-Record格式。

3.5 U-boot 启动流程分析

u-boot启动流程分析如下
第一阶段:
a – 设置cpu工作模式为SVC模式
b – 关闭中断,mmu,cache
v – 关看门狗
d – 初始化内存,串口
e – 设置栈
f – 代码自搬移
g – 清bss
h – 跳c
第二阶段:
a – 初始化外设,进入超循环
b – 超循环处理用户命令
可见, U-Boot 属于两阶段的Bootloader
第一阶段的文件:
arch/arm/cpu/armv7 /start.S 平台相关,CPU工作模式设为SVC模式,关MMU,关icahce(CPU相关)
board/samsung/fs4412/lowlevel_init.S 开发板相关:关看门狗,内存初始化,时钟初始化,串口初始化(board相关,初始化最基本设备)

第二阶段的文件:
arch/arm/lib/crt0.S _main 函数所在处,初始化SP,为C语言准备,代码重定位,清BSS,设置R0 R1 R2 R8相应寄存器
arch/arm/lib/board.c board_init_f 函数 ,填充GD结构体,初始化外设, main_loop()函数超循环
arch/arm/cpu/armv7 /start.S 代码自搬移时会用到
针对uboot2013启动流程图如下:

第3章 U-boot移植 A

图8

下面是具体分析:

3.5.1 U-Boot 第一阶段代码分析

通常我们通过连接文件知晓程序入口点,入口查看 u-boot.lds
第3章 U-boot移植 A
通过链接脚本可知入口为_start,位于arch/arm/cpu/armv7/start.o。

1、进入arch/arm/cpu/armv7/start.S
a – 异常向量表设置
第3章 U-boot移植 A
b – 设置CPU处于SVC工作模式
第3章 U-boot移植 A
d – 协处理器 p15 的 c12 寄存器来重新定位
第3章 U-boot移植 A
e—Bl cpu_init_cp15(使分支预测无效,数据)
第3章 U-boot移植 A
关闭数据预取功能;
DSB:多核CPU对数据处理指令
ISB:流水线清空指令;
第3章 U-boot移植 A
关闭MMU,使能I-cache
NOTE:
分支预测:在流水线里,会将后面的代码优先加载到处理器中,由于是循环,会使后面加载的代码无效,故出现了分支预测技术。(统计跳的次数来选择装载循环的代码还是下面的代码)。
f—Bl cpu_init_crit
第3章 U-boot移植 A
2、跳到Low_level_init,位于board/samsung/fs4412/lowlevel_init.S
a、关闭看门狗
b、比较当前pc指针域TEXT_BASE的高8位是否一样来判断,当前代码是否在内存中
第3章 U-boot移植 A
c、对系统时钟初始化
第3章 U-boot移植 A
d、对内存初始化
第3章 U-boot移植 A
e、对串口初始化
第3章 U-boot移植 A
结束后返回 start.S
第一阶段结束,总结如下:
1 前面总结过的部分,初始化异常向量表,设置svc模式;
2 配置cp15,初始化mmu cache tlb;
3 板级初始化,clk,memory,uart初始化。

3.5.2第二阶段代码分析

进入start.S
第3章 U-boot移植 A
这里我们选择第一个Bl _main,跳转到arch/arm/lib/crt0.S
1、初始c运行环境(看注释就知道,初始化C运行环境,并调用board_init_f 函数)
第3章 U-boot移植 A
功能:
初始化sp ,为支持C语言做准备;
保存128B 放GD结构体,存放全局信息,GD的地址存放在r8中;
跳转到 board_init_f 函数,其在arch/arm/lib/board.c 处定义;
2、跳转到arch/arm/lib/board.c
第3章 U-boot移植 A
功能:
对全局信息GD结构体进行填充:
291行:mon_len 通过链接脚本可以知道存放的是uboot代码大小;
294行:fdt_blob 存放设备数地址;
303行:循环执行init_fnc_t数组的函数,作硬件初始化;
a –init_fnc_t数组的函数定义
初始化硬件
第3章 U-boot移植 A
b – Dram_init初始化成功之后,剩余代码将会对sdram空间进行规划。
第3章 U-boot移植 A
可以看到addr的值由CONFIG_SYS_SDRAM_BASE加上ram_size。也就是到了可用sdram的顶端。
c–继续对gd结构体填充
第3章 U-boot移植 A
如果icahe 与 dcache 是打开的,就留出 64K 的空间作为 tlb 空间,最后 addr 就是tlb 地址,4K对齐。

d–填充完成将信息拷贝到内存指定位置
第3章 U-boot移植 A
2 – 继续回到 _main
按”CTRL + O”回到跳转前的函数,即 arch/arm/lib/crt0.S
第3章 U-boot移植 A
功能:
将 r8 指向新的 gd 地址;
代码重定位;
对lr 的操作为了让返回时,返回的是重定位的here处。
3 – 代码自搬移
代码自搬移,防止与内核冲突,代码位于arch/arm/cpu/armv7/start.S
第3章 U-boot移植 A
循环将代码搬移到指定高地址,这里只是将链接脚本中_image_copy_end到_start中的代码,其它段还没有操作。在这里我们有疑惑就是将代码重定位到高地址,那运行的地址不就和链接地址不一样了,那运行可能不正常?这个疑惑就是.rel.dyn帮我们解决了,主要还是编译器帮我们做的工作。

4 – 重定位到高地址之后,再次回到 _main(arch/arm/lib/crt0.S)
此时回到的是刚才的重定位的 here 处
第3章 U-boot移植 A
关 icache,保证数据从SDRAM中更新,更新异常向量表,因为代码被重定位了;
清BBS;
第3章 U-boot移植 A
调用board_init_r主要是对外设的初始化。
R0=gd
R1=RELOCADDR

5 – Main_loop 函数进入超循环(arch/arm/lib/board.c)
第3章 U-boot移植 A
Main_loop函数主要功能是处理环境变量,解析命令
install_auto_complete(); //安装自动补全的函数,分析如下 。
getenv(bootcmd)
bootdelay(自启动)
如果延时大于等于零,并且没有在延时过程中接收到按键,则引导内核。

3.6 U-boot添加自定义命令

U-boot添加自定义命令:u-boot中的命令使用U_BOOT_CMD这个宏声明来注册进系统,链接脚本会把所有的cmd_tbl_t结构体放在相邻的地方。U-Boot版本:u-boot-2013.01

3.6.1 U-Boot命令的格式

即使是内核的启动,也是通过U-Boot命令来实现的。U-Boot中每个命令都是通过 U_BOOT_CMD 宏来定义的,格式如下:
U_BOOT_CMD(name,maxargs,repeatable,command,”usage”,”help”)
各项参数的意义如下:
1) – name:命令的名字,注意,它不是一个字符串(不要用双引号括起来);
2)– maxargs:最大的参数个数;
3)– repeatable:命令是否可以重复,可重复是指运行一个命令后,下次敲回车即可再次运行;
4)– command:对应的函数指针,类型为(cmd)(struct cmd_tbl_s , int, int, char *[]);
5) – usage:简单的使用说明,这是个字符串;
6)– help:较详细的使用说明,这是个字符串。

宏U_BOOT_CMD 在include/command.h中定义,如下所示
第3章 U-boot移植 A
而U_BOOT_CMD 是用一个struct cmd_tbl_s 结构体定义,这个结构体仍是在include/command.h中实现:
第3章 U-boot移植 A
可以看出,对于每个使用U_BOOT_CMD 宏来定义的命令,就是宏 U_BOOT_CMD(name,maxargs,rep,cmd,usage,help)将struct cmd_tbl_s这样的一个命令结构体放到U-BOOT连接脚本 board/xxx/u-boot.lds中定义的”.u-boot_cmd”段所在的内存区域,即在”.u_boot_list.cmd”段中定义一个 cmd_tbl_t 结构。
连接脚本U-Boot.lds中有如下代码:
第3章 U-boot移植 A
当用户在u-boot的shell中输入命令时,就会在”.u_boot_list.cmd”这个内存区域中查找( _u_boot_list_cmd__start - _u_boot_list_cmd__end),当该区域中某一个cmd_tbl_s命令结构体的cmd_tbl_s.name和输入的命令字符串相符时,就调用该命令结构体的cmd_tbl_s.cmd()函数。

3.6.2添加自定义命令

自定义命令设为”myubootcmd”,不可与u-boot命令重名,
1、添加命令行配置信息
在u-boot-2013.01/include/configs/fs4412.h(由具体开发板来配置,这里使用fs4412)中添加 #define CONFIG_CMD_MYUBOOT,如下:
第3章 U-boot移植 A
2、编写命令行对应的源程序
在u-boot-2013.01/common/目录下,建立相应的命令执行文件cmd_hello.c
注意命名的规范,必须是cmd_xxx.c才行。里面的内容也是有格式要求的,如函数的格式,必须指定参数的;还有相应结尾部分的U_BOOT_CMD定义部分,使不能缺省的。如果命令不需要跟参数,则把maxargs设置为1即可了。内容如下所示:
第3章 U-boot移植 A
3、修改Makefile
在common/Makefile中增加一项.
第3章 U-boot移植 A
4、编译
第3章 U-boot移植 A
5、测试

3.7 U-boot引导内核过程分析

bootloader 要想启动内核,可以直接跳到内核的第一个指令处,即内核的起始地址,这样便可以完成内核的启动工作了。但是要想启动内核还需要满足一些条件,如下所示:
 cpu 寄存器设置
* R0 = 0
* R1 = 机器类型 id
* R2 = 启动参数在内存中的起始地址
 cpu 模式
* 禁止所有中断
* 必须为SVC(超级用户)模式
 Cache、MMU
* 关闭 MMU
* 指令Cache可以开启或者关闭
* 数据Cache必须关闭
 设备
* DMA 设备应当停止工作
 PC为内核的起始地址
这些需求都由 boot loader 实现,在常用的 uboot 中完成一系列的初始化后最后通过 bootm 命令加载 linux 内核。bootm 向将内核映像从各种媒介中读出,存放在指定的位置;然后设置标记列表给内核传递参数;最后跳到内核的入口点去执行。

Uboot版本:u-boot-2013.01

3.7.1 bootm命令用法介绍

在 common/cmd_bootm.c 中可以看到bootm 的定义:
第3章 U-boot移植 A
可以看到 bootm 命令使调用了do_bootm 函数。
do_bootm 函数
在cmd_bootm.c 第586行可以看到do_bootm函数的定义(为方便阅读,对其中一些代码进行了删减,完整代码请阅读uboot源码):

/*******************************************************************/
/* bootm - boot application image from image in memory */
/*******************************************************************/

int do_bootm(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{
ulong iflag;
ulong load_end = 0;
int ret;
boot_os_fn *boot_fn;
if (bootm_start(cmdtp, flag, argc, argv))// 获取镜像信息
return 1;
iflag = disable_interrupts(); // 关闭中断
usb_stop();// 关闭usb设备

ret = bootm_load_os(images.os, &load_end, 1);//加载内核
lmb_reserve(&images.lmb, images.os.load, (load_end - images.os.load));
if (images.os.type == IH_TYPE_STANDALONE) {//如有需要,关闭内核的串口
if (iflag)
enable_interrupts();
/* This may return when 'autostart' is 'no' */
bootm_start_standalone(iflag, argc, argv);
return 0;
}

boot_fn = boot_os[images.os.os];//获取启动参数
arch_preboot_os();//启动前准备
boot_fn(0, argc, argv, &images);//启动,不再返回

#ifdef DEBUG
puts("\n## Control returned to monitor - resetting...\n");
#endif
do_reset(cmdtp, flag, argc, argv);

return 1;
}

该函数的实现分为 3 个部分:
a – 首先通过 bootm_start 函数分析镜像的信息;
b – 如果满足判定条件则进入 bootm_load_os 函数进行加载;
c – 加载完成后就可以调用 boot_fn 开始启动。

1、bootm_start
在cmd_bootm.c 第193行可以看到bootm_start函数的定义, 主要作用是填充内核相关信息。

static int bootm_start(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{
void *os_hdr;
int ret;

memset((void *)&images, 0, sizeof(images));
images.verify = getenv_yesno("verify");//获取环境变量
boot_start_lmb(&images);
bootstage_mark_name(BOOTSTAGE_ID_BOOTM_START, "bootm_start");

/*获取镜像头,加载地址,长度&nbsp;*/
os_hdr = boot_get_kernel(cmdtp, flag, argc, argv,
&images, &images.os.image_start, &images.os.image_len);
if (images.os.image_len == 0) {
puts("ERROR: can't get kernel image!\n");
return 1;
}

/*获取镜像参数*/
switch (genimg_get_format(os_hdr)) {
case IMAGE_FORMAT_LEGACY:
images.os.type = image_get_type(os_hdr);//镜像类型
images.os.comp = image_get_comp(os_hdr);//压缩类型
images.os.os = image_get_os(os_hdr);//系统类型

images.os.end = image_get_image_end(os_hdr);//镜像结束地址
images.os.load = image_get_load(os_hdr);/加载地址
break;

/* 查询内核入口地址*/
if (images.legacy_hdr_valid) {
images.ep = image_get_ep(&images.legacy_hdr_os_copy);

} else {
puts("Could not find kernel entry point!\n");
return 1;
}

if (images.os.type == IH_TYPE_KERNEL_NOLOAD) {
images.os.load = images.os.image_start;
images.ep += images.os.load;
}

if (((images.os.type == IH_TYPE_KERNEL) ||
(images.os.type == IH_TYPE_KERNEL_NOLOAD) ||
(images.os.type == IH_TYPE_MULTI)) &&
(images.os.os == IH_OS_LINUX)) {
/* 查询是否存在虚拟磁盘 */
ret = boot_get_ramdisk(argc, argv, &images, IH_INITRD_ARCH,
&images.rd_start, &images.rd_end);
if (ret) {
puts("Ramdisk image is corrupt or invalid\n");
return 1;
}

#if defined(CONFIG_OF_LIBFDT)
/* 找到设备树,设备树是linux 3.XX版本特有的 */
ret = boot_get_fdt(flag, argc, argv, &images,
&images.ft_addr, &images.ft_len);
if (ret) {
puts("Could not find a valid device tree\n");
return 1;
}

set_working_fdt_addr(images.ft_addr);
#endif
}

images.os.start = (ulong)os_hdr;//赋值加载地址
images.state = BOOTM_STATE_START;//更新状态

return 0;
}

该函数主要进行镜像的有效性判定、校验、计算入口地址等操作,大部分工作通过 boot_get_kernel -> image_get_kernel 完成。

2、bootm_load_os
在cmd_bootm.c 第317行可以看到bootm_load_os函数的定义,这个函数主要判断镜像是否需要解压,并且将镜像移动到加载地址:

static int bootm_load_os(image_info_t os, ulong *load_end, int boot_progress)  
{
uint8_t comp = os.comp; /* 压缩格式 */
ulong load = os.load; /* 加载地址 */
ulong blob_start = os.start; /* 镜像起始地址 */
ulong blob_end = os.end; /* 镜像结束地址 */
ulong image_start = os.image_start; /* 镜像起始地址 */
ulong image_len = os.image_len; /* 镜像长度 */
uint unc_len = CONFIG_SYS_BOOTM_LEN; /* 镜像最大长度 */

const char *type_name = genimg_get_type_name (os.type); /* 镜像类型 */

switch (comp) { /* 选择解压格式 */
case IH_COMP_NONE: /* 镜像没有压缩过 */
if (load == blob_start) { /* 判断是否需要移动镜像 */
printf (" XIP %s ... ", type_name);
} else {
printf (" Loading %s ... ", type_name);

if (load != image_start) {
memmove_wd ((void *)load, (void *)image_start, image_len, CHUNKSZ);
}
}
*load_end = load + image_len;
puts("OK\n");
break;
case IH_COMP_GZIP: /* 镜像采用 gzip 解压 */
printf (" Uncompressing %s ... ", type_name);
if (gunzip ((void *)load, unc_len, (uchar *)image_start, &image_len) != 0) { /* 解压 */
puts ("GUNZIP: uncompress, out-of-mem or overwrite error "
"- must RESET board to recover\n");
return BOOTM_ERR_RESET;
}

*load_end = load + image_len;
break;
...
default:
printf ("Unimplemented compression type %d\n", comp);
return BOOTM_ERR_UNIMPLEMENTED;
}
puts ("OK\n");
debug (" kernel loaded at 0x%08lx, end = 0x%08lx\n", load, *load_end);

if ((load < blob_end) && (*load_end > blob_start)) {
debug ("images.os.start = 0x%lX, images.os.end = 0x%lx\n", blob_start, blob_end);
debug ("images.os.load = 0x%lx, load_end = 0x%lx\n", load, *load_end);
return BOOTM_ERR_OVERLAP;
}

return 0;
}

3、do_bootm_linux
在bootm_load_os 执行结束后,回到do_bootm 函数,调用boot_fn 运行linux 内核;
第3章 U-boot移植 A
boot_os 为函数指针数组,在cmd_bootm.c 136行有定义
第3章 U-boot移植 A
可以看出 boot_fn 函数指针指向的函数是位于 arch/arm/lib/bootm.c的 do_bootm_linux,这是内核启动前最后的一个函数,该函数主要完成启动参数的初始化,并将板子设定为满足内核启动的环境,代码如下:
第3章 U-boot移植 A
可以看到 do_bootm_linux 实际调用的是 boot_jump_linux 函数。

4、boot_jump_linux
在arch/arm/lib/bootm.c 下第326行有定义

/* Subcommand: GO */
static void boot_jump_linux(bootm_headers_t *images)
{
unsigned long machid = gd->bd->bi_arch_number;//获取机器码
char *s;
void (*kernel_entry)(int zero, int arch, uint params);//内核入口函数
unsigned long r2;

kernel_entry = (void (*)(int, int, uint))images->ep;

s = getenv("machid");//从环境变量中获取机器码
if (s) {
strict_strtoul(s, 16, &machid);
printf("Using machid 0x%lx from environment\n", machid);
}

debug("## Transferring control to Linux (at address %08lx)" \
"...\n", (ulong) kernel_entry);
bootstage_mark(BOOTSTAGE_ID_RUN_OS);
announce_and_cleanup();

#ifdef CONFIG_OF_LIBFDT
if (images->ft_len)
r2 = (unsigned long)images->ft_addr;
else
#endif
r2 = gd->bd->bi_boot_params;//将启动参数地址赋给 r2

kernel_entry(0, machid, r2);
}

3.7.2为内核设置启动参数

U-boot 也是通过标记列表向内核传递参数,标记在源代码中定义为tag,是一个结构体,在 arch/arm/include/asm/setup.h 中定义。
第3章 U-boot移植 A
tag_header 结构体定义如下:
第3章 U-boot移植 A
在一些内存标记、命令行标记的示例代码就是取自Uboot 中的 setup_memory_tags、setup_commandline_tag函数,他们都是在 arch/arm/lib/bootm.c中定义。

#if defined(CONFIG_SETUP_MEMORY_TAGS) || \
defined(CONFIG_CMDLINE_TAG) || \
defined(CONFIG_INITRD_TAG) || \
defined(CONFIG_SERIAL_TAG) || \
defined(CONFIG_REVISION_TAG)
static void setup_start_tag (bd_t *bd)
{
params = (struct tag *)bd->bi_boot_params;

params->hdr.tag = ATAG_CORE;
params->hdr.size = tag_size (tag_core);

params->u.core.flags = 0;
params->u.core.pagesize = 0;
params->u.core.rootdev = 0;

params = tag_next (params);
}
#endif

#ifdef CONFIG_SETUP_MEMORY_TAGS
static void setup_memory_tags(bd_t *bd)
{
int i;

for (i = 0; i < CONFIG_NR_DRAM_BANKS; i++) {
params->hdr.tag = ATAG_MEM;
params->hdr.size = tag_size (tag_mem32);

params->u.mem.start = bd->bi_dram[i].start;//物理内存起始地址
params->u.mem.size = bd->bi_dram[i].size;//物理内存结束地址

params = tag_next (params);
}
}
#endif

#ifdef CONFIG_CMDLINE_TAG
static void setup_commandline_tag(bd_t *bd, char *commandline)
{
char *p;

if (!commandline)
return;

/* eat leading white space */
for (p = commandline; *p == ' '; p++);

/* skip non-existent command lines so the kernel will still
* use its default command line.
*/

if (*p == '\0')
return;

params->hdr.tag = ATAG_CMDLINE;
params->hdr.size =
(sizeof (struct tag_header) + strlen (p) + 1 + 4) >> 2;

strcpy (params->u.cmdline.cmdline, p);

params = tag_next (params);
}
#endif

一般有 setup_memory_tags、setup_commandline_tag 这两个标记就可以了,在配置文件Include/configs/fs4412.h中定义:
第3章 U-boot移植 A