第一部分uboot源代码分析
U-Boot源代码架构分析
U-Boot的源码顶层目录说明目 录特 性解 释 说 明
board平台依赖存放电路板相关的目录文件,例如:RPXlite(mpc8xx)、hi3520v100 (arm1176)、sc520_cdp(x86) 等目录
cpu平台依赖存放CPU相关的目录文件,例如:mpc8xx、ppc4xx、arm1176、arm920t、 xscale、i386等目录
lib_ppc平台依赖存放对PowerPC体系结构通用的文件,主要用于实现PowerPC平台通用的函数
lib_arm平台依赖存放对ARM体系结构通用的文件,主要用于实现ARM平台通用的函数
lib_i386平台依赖存放对X86体系结构通用的文件,主要用于实现X86平台通用的函数
include通用头文件和开发板配置文件,所有开发板的配置文件都在configs目录下
common通用通用的多功能函数实现
lib_generic通用通用库函数的实现
Net通用存放网络的程序
Fs通用存放文件系统的程序
Post通用存放上电自检程序
drivers通用通用的设备驱动程序,主要有FLASH驱动、以太网接口的驱动
Disk通用硬盘接口程序
examples应用例程一些独立运行的应用程序的例子,例如helloworld
tools工具存放制作S-Record 或者 U-Boot格式的映像等工具,例如mkimage
Doc文档开发使用文档
1.顶层目录下的Makefile
它负责U-Boot整体配置编译。按照配置的顺序阅读其中关键的几行。
每一种开发板在Makefile都需要有板子配置的定义。例如hi3520v100板的定义如下。
#make hiconfig
在顶层目录的Makefile下
hiconfig: unconfig
@./hiconfig
执行hiconfig脚本,hiconfig脚本最终执行选中的板hi3520v100_mchip_200M的目标
make -C ./ hi3520v100_mchip_200M_config
在hisilicon_board-Hi3520v100.mk下
hi3520v100_mchip_200M_config: unconfig
@./mkconfig hi3520v100_mchip_200M arm arm1176 hi3520v100 NULL hi3520v100
执行配置U-Boot的命令make hiconfig,通过./mkconfig脚本生成include/config.
mk的配置文件。文件内容正是根据Makefile对开发板的配置生成的。
ARCH = arm
CPU = arm1176
BOARD = hi3520v100
SOC = hi3520v100
上面的include/config.mk文件定义了ARCH、CPU、BOARD、SOC这些变量。这样硬件平台依赖的目录文件可以根据这些定义来确定。hi3520v100平台相关目录如下。
board/hi3520v100/
cpu/arm1176/
cpu/ arm1176/hi3520v100/
lib_arm/
include/asm/
include/configs/ hi3520v100_mchip_200M.h
再回到顶层目录的Makefile文件开始的部分,其中下列几行包含了这些变量的定义。
# load ARCH,BOARD, and CPU configuration
include include/config.mk
export ARCH CPU BOARD VENDOR SOC
Makefile的编译选项和规则在顶层目录的config.mk文件中定义。各种体系结构通用的规则直接在这个文件中定义。通过ARCH、CPU、BOARD、SOC等变量为不同硬件平台定义不同选项。不同体系结构的规则分别包含在ppc_config.mk、arm_config.mk、mips_config.mk等文件中。
顶层目录的Makefile中还要定义交叉编译器,以及编译U-Boot所依赖的目标文件。
ifeq($(ARCH),arm)
CROSS_COMPILE= arm-linux- //交叉编译器的前缀
#endif
export CROSS_COMPILE
…
#U-Boot objects....order is important (i.e. start must be first)
OBJS =cpu/$(CPU)/start.o //处理器相关的目标文件
…
LIBS = lib_generic/libgeneric.a //定义依赖的目录,每个目录下先把目标文件连接成*.a文件。
LIBS+= board/$(BOARDDIR)/lib$(BOARD).a
LIBS+= cpu/$(CPU)/lib$(CPU).a
ifdef SOC
LIBS+= cpu/$(CPU)/$(SOC)/lib$(SOC).a
endif
LIBS+= lib_$(ARCH)/lib$(ARCH).a
…
然后还有U-Boot映像编译的依赖关系。
ALL =u-boot.srec u-boot.bin System.map
all: $(ALL)
u-boot.srec: u-boot
$(OBJCOPY) ${OBJCFLAGS} -Osrec $< $@
u-boot.bin:u-boot
$(OBJCOPY) ${OBJCFLAGS} -Obinary $< $@
……
u-boot: depend $(SUBDIRS) $(OBJS) $(LIBS) $(LDSCRIPT)
UNDEF_SYM='$(OBJDUMP) -x$(LIBS) \
|sed -n -e's/.*\(__u_boot_cmd_.*\)/-u\1/p'|sort|uniq`;\
$(LD) $(LDFLAGS) $$UNDEF_SYM$(OBJS) \
--start-group $(LIBS) $(PLATFORM_LIBS) --end-group \
-Map u-boot.map -o u-boot
Makefile缺省的编译目标为all,包括u-boot.srec、u-boot.bin、System.map。u-boot.srec和u-boot.bin又依赖于U-Boot。U-Boot就是通过ld命令按照u-boot.map地址表把目标文件组装成u-boot。
2.开发板配置头文件
编译时(mkconfig脚本运行时)会在include目录下生成config.h头文件,这个头文件#include /configs/.h头文件。用相应的BOARD定义代替。
/configs/.h头文件中主要定义了两类变量。
一类是选项,前缀是CONFIG_,用来选择处理器、设备接口、命令、属性等。例如:
#define CONFIG_BOOTARGS "mem=32Mconsole=ttyAMA0,115200"
#define CONFIG_BAUDRATE 115200
另一类是参数,前缀是CFG_,用来定义总线频率、串口波特率、Flash地址等参数。例如:
/*Slave boot kernel initrd addr*/
#define CFG_SLAVE_BOOT_ADDR 0x80800000
#defineCFG_SLAVE_KERNEL_ADDR 0x80280000
#defineCFG_SLAVE_INITRD_ADDR 0x80620000
U-Boot启动流程
看一下board//u-boot.lds这个链接脚本,可以知道目标程序的各部分链接顺序。第一个要链接的是cpu//start.o,那么U-Boot的入口指令一定位于这个程序中。下面简单分析一下程序跳转和函数的调用关系以及函数实现。
1 第一阶段(Stage1)
第一阶段的启动代码在 cpu//start.s中,完成的工作主要有:
CPU自身初始化:包括 MMU,Cache,时钟系统,SDRAM 控制器等的初始化
重定位:把自己从非易失性存储器搬移到 RAM中
分配堆栈空间,设置堆栈指针
清零 BSS 数据段
跳转到第二阶段入口函数 start_armboot()
2 第二阶段(Stage2)
第二阶段是u-boot 的主体,入口点是lib_arm/board.c 中的start_armboot()函数,完成
的主要工作包括:
为 U-boot 内部私有数据分配存储空间,并清零
依次调用函数指针数组 init_sequence 中定义的函数进行一系列的初始化
如果系统支持 NOR Flash,调用 flash_init ()和 display_flash_config ()初始化并
显示检测到的器件信息(AT91SAM9260EK不需要)
如果系统支持LCD或VFD, 调用lcd_setmem()或vfd_setmem()计算帧缓冲(Framebuffer)
大小,然后在 BSS 数据段之后为 Framebuffer 分配空间,初始化gd->fb_base 为
Framebuffer的起始地址(AT91SAM9260EK不需要)
调用 mem_malloc_init()进行存储分配系统(类似于 C 语言中的堆)的初始化和空间分配
如果系统支持 NAND Flash,调用 nand_init ()进行初始化
如果系统支持 DataFlash,调用 AT91F_DataflashInit()和 dataflash_print_info()进行初始化并显示检测到的器件信息
调用 env_relocate ()进行环境变量的重定位,即从 Flash中搬移到 RAM 中(堆区)
如果系统支持 VFD,调用drv_vfd_init()进行 VFD 设备初始化(AT91SAM9260EK 不
需要)
从环境变量中读取 IP 地址和 MAC 地址,初始化 gd->bd-> bi_ip_addr 和
gd->bd->bi_enetaddr
调用 jumptable_init ()进行跳转表初始化,跳转表在 global_data 中,具体用途尚不清楚
调用 console_init_r()进行控制台初始化
如果需要,调用 misc_init_r ()进行杂项初始化
调用 enable_interrupts ()打开中断
如果需要,调用 board_late_init()进行单板后期初始化,对于 AT91SAM9260EK,主
要是以太网初始化
进入主循环:根据用户的选择启动 linux,或者进入命令循环执行用户输入的命令
第二部分HI3520的uboot启动分析
HI3520的ARM1176启动流程
Hi3520 内部使用的地址总线为 32bit位宽,可寻址的地址空间为 4GB。Hi3520支持以
下 3 种启动方式:
1) 从 NOR Flash 启动 执行下面的跳转
2) 从 NAND Flash 启动
3) 从 DDR 启动
上面3种启动方式仅针对ARM1176。
通过对管脚EBIADR23和EBIADR24 进行设置,选择启动方式。
从NOR Flash启动
按照表进行正确设置后,才可选择从 NORFlash 启动。此时外部连接的存储器一般为异步 NORFlash,Hi3520只支持8bit 的 NOR Flash 存储器。
从 NOR Flash 启动时芯片时,一上电复位后,地址空间处于remap 状态,(由寄存器0x20050000复位默认值决定)。
1) 系统实际上提供了 2KB的 ITCM(Instruction Tightly-Coupled Memory 指令紧耦合存储器) 地址空间,因此,软件需要保证在 ITCM的程序和数据必须在 2KB 的范围内。
2) 如果需要使用 ITCM,则必须通过 ARM的系统控制协处理器 CP15提供的寄存器设置 ITCM使能,并且配置 ITCM的大小等信息。使用 MCR指令配置 C9寄存器为0xD。
启动代码在uboot的 cpu//start.s中,当一上电复位时,地址空间处于remap 状态,跑的第一步先是从绝对地址(0x80000000)norflash映射过来的代码(从0x0开始跑),当跑到要取消remap状态时,进行跳转
cmp r4, #0 /*boot from nor flash */
ldreq pc, _clr_remap_rom_entry
其中_clr_remap_rom_entry的值是
_clr_remap_rom_entry:
.word ROM_TEXT_ADRS + do_clr_remap -TEXT_BASE
//ROM_TEXT_ADRS0x80000000
也就是跑0x80000000中的代码,如下:
do_clr_remap:
ldr r4, =REG_BASE_SCTL
@ldr r0, =REG_VALUE_SC_NOLOCK
@str r0, [r4, #REG_VALUE_SC_LOCKED]
ldr r0, [r4, #REG_SC_CTRL]
@Set clear remap bit.
orr r0, #(1<<8)
str r0, [r4, #REG_SC_CTRL]
。。。
接下来有一个把uboot代码搬运到内存的过程
#ifndef CONFIG_SKIP_RELOCATE_UBOOT
relocate: /*relocate U-Boot to RAM */
ldr r0, =REG_BASE_SCTL
ldr r6, [r0, #0x8c]
and r6, #0x60
lsr r4, r6, #5
adr r0, _start /* r0
/* adr r0, _start这句代码是将_start标签处运行时的地址值装进r0,这条指令到底取一个什么值呢?假设运行到该指令时pc寄存器的值是X,start标签相对此本指令有Y的偏移,则r0=X+Y(Y可能是负值).针对该程序来说。当被装载到0x0000处运行时,r0就是0,假设装载到TEXT_BASE处运行。则r0=TEXT_BASE*/
ldr r1, _TEXT_BASE /*test if we run from flash or RAM */
cmp r0, r1 /* don't reloc duringdebug */
beq stack_setup
ldr r2, _armboot_start
ldr r3, _img_end
sub r2, r3, r2 /* r2
cmp r4, #2
ldreq r2, =(CFG_NAND_U_BOOT_SIZE)
add r2, r0, r2 /* r2
copy_loop:
ldmia r0!, {r3-r10} /*copy from source address [r0] */
stmia r1!, {r3-r10} /*copy to target address [r1] */
cmp r0, r2 /*until source end addreee [r2] */
ble copy_loop
#endif /*CONFIG_SKIP_RELOCATE_UBOOT */
最后执行下面的跳转,跑到内存执行,如果上面的拷贝过程不存在,程序执行下面的跳转时就停住了
ldr pc, _start_armboot
_start_armboot:
.word start_armboot
至于为什么会跳到内存去,这个涉及到代码的重定位问题。
U-Boot中关于TEXT_BASE,代码重定位,链接地址相关说明:
我们知道U-BOOT分为两个阶段,第一阶段是(~/cpu/arm1176/start.S中)在FLASH上运行(一般情况下),完成对硬件的初始化,包括看门狗,中断缓存等,并且负责把代码搬移到SDRAM中(在搬移的时候检查自身代码是否在SDRAM中),然后完成C程序运行所需要环境的建立,包括堆栈的初始化等,最后执行一句跳转指令:
ldr pc, _start_armboot
_start_armboot: .word start_armboot,
进入到/lib_arm/board.c中的函数void start_armboot(void),从此就进入了第二阶段。这是在很多资料上都有讲述的,所以勿需多言了。
现在对于第一阶段有几个问题,既然在FLASH中的代码是把自己拷贝到SDRAM中,那么在地址空间,就有两份的启动代码,第一份就是在FLASH中,第二份就是在SDRAM中。根据链接脚本文件(~/board/ hi3520v100/u-boot.lds)OUTPUT_FORMAT("elf32-littlearm","elf32-littlearm", "elf32-littlearm")/*指定输出可执行文件是elf格式,32位ARM指令,小端模式*/
OUTPUT_ARCH(arm)/*指定输出可执行文件的平台是ARM */
ENTRY(_start)/*指定输出可执行文件的起始代码段为_start */
SECTIONS
{
. = 0x00000000;
. = ALIGN(4);/*代码以4字节对齐*/
.text :
{
cpu/arm1176/start.o (.text)/*代码的第一个代码部分*/
board/hi3520v100/libhi3520v100.a (.text)
common/dlmalloc.o (.text)
common/console.o (.text)
common/nand_boot.o (.text)
drivers/mtd/libmtd.a (.text)
drivers/mtd/nand/libnand.a (.text)
lib_generic/libgeneric.a(.text)
cpu/arm1176/libarm1176.a (.text)
cpu/arm1176/hi3520v100/libhi3520v100.a (.text)
lib_arm/libarm.a (.text)
drivers/serial/libserial.a (.text)
}
. = ALIGN(4);/* rodata以4字节对齐*/
.rodata : { *(.rodata) }/*指定只读数据段*/
. = ALIGN(4);/* .data以4字节对齐*/
.data : { *(.data) }/*指定读,写数据段*/
. = ALIGN(4);/* .got以4字节对齐*/
.got : { *(.got) }/*指定got段,got段是uboot自定义的一个段,非标准段*/
. = ALIGN(4);
.text1 :
{
*(.text)/*其他代码部分*/
}
. = ALIGN(4);
. = .;
__u_boot_cmd_start = .;/*把__u_boot_cmd_start赋值为当前位置,即起始位置*/
.u_boot_cmd : { *(.u_boot_cmd) }/*指定u_boot_cmd段,uboot把所有的uboot命令放在该段*/
__u_boot_cmd_end = .;/*把__u_boot_cmd_end赋值为当前位置,即结束位置*/
__img_end = .;
. = _start + 0x100000;
. = ALIGN(32);
__bss_start = .;/*把__bss_start赋值为当前位置,即bss段的开始位置*/
.bss1 (NOLOAD) : {*(.bss)}/*指定bss段,存放的是未初始化的全局变量和局部静态变量*/
_end = .;/*把_end赋值为当前位置,即bss段的结束位置*/
}其中的链接命令. = 0x00000000;表示地址计数器从0地址开始计数,而且_start是程序代码段的入口,那么*.text中的所有地址标号(cpu/arm1176/start.S中定义的)就应该从0地址开始计数,那么标号start_armboot(就是voidstart_armboot (void)函数的入口地址)应该在FLASH中才对啊,所以按照上边的分析,
ldr pc, _start_armboot
_start_armboot: .word start_armboot
此条语句后,并没有跳转到SDRAM中的void start_armboot (void),而是跳转到了FLASH中的void start_armboot (void)中。
所以就出现了这样的矛盾,在FLASH中有一段代码把自己拷贝到SDRAM中,产生了两份UBOOT可执行的指令流,但是最后却没有跳转到SDRAM中去运行以提高指令执行的速度。
产生以上的认识是基于以下几个认识(肯定是错误的):
1)
*.text中的所有地址标号(在链接时确定)是从0地址开始生成的。
实际上在arm-linux-ld 执行时,原来定义的0x0地址被更新为TEXT_BASE定义的地址。
2)#ifndefCONFIG_SKIP_RELOCATE_UBOOT
relocate: /* relocate U-Boot to RAM */
ldr r0, =REG_BASE_SCTL
ldr r6, [r0, #0x8c]
and r6, #0x60
lsr r4,r6, #5
adr r0,_start /* r0
ldr r1,_TEXT_BASE /* test if we run fromflash or RAM */
cmp r0, r1 /* don'treloc during debug */
beq stack_setup
ldr r2,_armboot_start
ldr r3,_img_end
sub r2,r3, r2 /* r2
cmp r4,#2
ldreq r2, =(CFG_NAND_U_BOOT_SIZE)
add r2,r0, r2 /* r2
copy_loop:
ldmia r0!,{r3-r10} /* copy from sourceaddress [r0] */
stmia r1!,{r3-r10} /* copy to target address [r1] */
cmp r0,r2 /* until source endaddreee [r2] */
ble copy_loop
#endif /* CONFIG_SKIP_RELOCATE_UBOOT */
如果不是出于调试阶段,这段搬移代码中的r0和r1肯定不相等的,r0=#0,r1=#TEXT_BASE: 0xe2500000(在./board/ hi3520v100/config.mk中),所以执行代码的自身拷贝与搬移。
注意:在GNU中:adr r0, _start 作用是获得 _start 的实际运行所在的地址值,而ldr r1, _TEXT_BASE 为获得地址_TEXT_BASE中所存放的数据,其中adr r0, _start翻译成 addr0,(PC+#offset),offset 就是 adr r0, _start 指令到_start 的偏移量,在链接时确定,这个偏移量是地址无关的。而 ldr r1, _TEXT_BASE 指令表示以程序相对偏移的方式加载数据,是索引偏移加载的另外一种形式,等同于ldr r1,[PC+#offset],offset 是 ldr r1, _TEXT_BASE 到 _TEXT_BASE 的偏移量。
经过把U-BOOT进行make后,从所生成的两个.map文件来看(~/u-boot.map和 Systen.map),所有的地址标号都是从0xe2500000开始的,就是从SDRAM的高地址开始,等于TEXT_BASE的值,因为在连接的时候, LDFLAGS中有-Ttext $(TEXT_BASE) ,其含义是将text section定位基址设置成TEXT_BASE,这样生成的结果里的诸如函数地址,等symbol对应的地址都是以TEXT_BASE作为基地址的。也就是说,链接器是从0xe2500000开始来链接所编译生成的目标文件的,而不是从0地址开始,经过查看,start_armboot=0xe251179c,就是说void start_armboot (void)函数的入口地址在SDRAM中(链接器决定),所以执行
ldr pc, _start_armboot
_start_armboot: .word start_armboot,
PC指针肯定就指向了SDRAM中,换句话就是说进入到SDRAM中了
主arm11和从arm9都工作的情况
启动ARM926 :
ARM926 对“0x00xx_xxxx”地址段进行访问时,实际访问的地址是“0xAAxx_xxxx” ,AA表示系统控制寄存器 SC_PERIPHCTRL17 bit[7:0]设置的值。对其他地址段的访问同 ARM1176 一致。
ARM926在 Hi3520芯片系统中是从地位的处理器,只能在 ARM1176启动后,通过配置相关寄存器才可动。
ARM926 启动过程如下:
步骤 1 配置系统控制寄存器 SC_PERIPHCTRL17 bit[7:0]。例如配置成 0xC8。
步骤 2 向相应的地址(0xC800_0000)写入ARM926 的启动程序。
步骤 3 向系统控制寄存器 SC_PERIPHCTRL8 bit[1]写1,撤消 ARM926 的复位。
步骤 4 ARM926 自动从 0xC800_0000 读取相关程序进行 boot。
从arm启动对应程序,下面的这段代码在start_armboot函数中,
#ifdefCFG_HAS_SLAVE
if ( (boot_flag & 0x1) == 1 ){
slave_start();
goto out;
}
e =getenv("slave_autostart");
if(e && (*e == '1')){
e =getenv("slave_kernel_addr");
if(e){
kernel_addr =simple_strtoul(e, NULL, 16);
}else{
kernel_addr =CFG_SLAVE_KERNEL_ADDR;
setenv = 1;
}
e =getenv("slave_initrd_addr");
if(e){
initrd_addr =simple_strtoul(e, NULL, 16);
}else{
initrd_addr =CFG_SLAVE_INITRD_ADDR;
setenv = 1;
}
if(setenv){
sprintf (cmd_buf,"setenv slave_kernel_addr %x", kernel_addr);
run_command(cmd_buf,0);
sprintf (cmd_buf,"setenv slave_initrd_addr %x", initrd_addr);
run_command(cmd_buf,0);
}
slave_start();
}
out:
#endif
slave_start函数拷贝从arm启动代码,内核等到内存(细看上面的DDRB分布图),设置软件复位让从arm启动,程序就自动从指定的地方跑。
根据上面norflash程序分布图,计算后得出下面的分区情况(分区必需是连续的)
384K(m-boot), 0x80000000 (不能超256K,因为主uboot环境变量默认保存在0x80040000占128k)
640K(config), 0x80000000+0x60000 = 0x80060000
1536K(m-kernel), 0x80100000
1536K(s-kernel), 0x80100000+0x180000 = 0x80280000
2176K(m-rootfs), 0x80400000
1920K(s-rootfs), 0x80400000+0x220000 = 0x80620000 //修改文件系统的/etc/init.d/rcS脚本挂载的分区对应关系,增加/etc/network目录(包含文件ifdown、ifup、ifcfg-eth0、ifcfg-lo四个文件,其中ifcfg-eth0修改ip成可用的ip)
128K(s-boot) 0x80800000
8192K(usr), 0x80800000+0x20000 = 0x80820000
Uboot的环境变量
slave_boot_addr = 0x80800000
slave_kernel_addr = 0x80280000
slave_initrd_addr = 0x80620000
指定从arm9 的boot、kernel、fs镜像文件拷贝起始地址.