Bootloader介绍和Uboot源码结构

2019-07-13 03:58发布

本文是对《嵌入式Linux应用开发完全手册》的一个自我总结!

一. Bootloader介绍

1.Bootload引入的原因

Bootloader的作用是在系统启动的时候初始化必要的硬件设备,引导内核镜像文件,传递参数给内核,然后将控制权交给内核以结束自己的使命。总的来说就是为了引导操作系统。
Bootloader非常依赖具体的硬件,对每一个板子都有一个独一无二的Bootloader。

2.Bootloader启动方式

Bootloader有两种启动方式,分别对应着成熟的产品和开发中的产品,分别是:启动加载模式下载模式
  • 启动加载模式
    在系统启动时,运行Bootloader,当Bootloader准备好加载内核的环境后就直接从FLASH中拷贝内核镜像到内存,并且运行内核。
  • 下载模式
    该模式适合开发者。在开发过程中,所有东西都没有定型,需要经常性的修改。所以一般不会将内核放到目标板上面,而是存在在宿主机上,通过串口、USB、网络将内核传递到目标板上。串口传输协议有xmodem、ymodem、zmodem;网络传输有tftp、nfs。一般采用TFTP传输,因为网络传输速度快很多。
像Uboot这种强大的Bootloader可以同时支持两种模式。在默认情况下使用启动加载模式,但是中间会有几秒时间供开发者进入下载模式。 Bootloader的结构和启动过程 嵌入式Linux软件分为4类:
  • 引导加载程序
    分为固化在固件中的boot(可选)和Bootloader两大部分。PC机上的BIOS就是boot中的一种,对于大多数嵌入式设备只有Bootloader。
  • Linux内核
    包含特制的Linux内核代码和内核启动参数。内核启动参数可以是Bootloader传递的,或者是编译内核时选定的默认参数。
  • 文件系统
    包含根文件系统和建立在FLASH内存设备上的文件系统,其他文件系统时挂载在根文件系统之上的。
  • 用户应用程序
    存放在文件系统里面。
boot parameters中存放的参数有IP地址等网络相关参数串口波特率命令行参数等等。这些参数会存放在与内核约定好的内存地址上。 Bootloader启动的两个过程 Bootloader的启动分为多阶段和单阶段两种方式。多阶段启动的Bootloader有更好的移植性,而且能实现更复杂的功能。第一阶段用汇编语言实现,第二阶段用C语言实现。
  • 第一阶段
    1.硬件设备初始化
    2.为加载Bootloader的第二阶段代码准备RAM空间
    3.复制Bootloader的第二段代码到RAM空间中
    4.设置好栈
    5.跳转到第二阶段代码的C入口点
在第一阶段的硬件初始化包括关闭watchdog、设置为SVC模式、关闭中断、设置CPU的速度和时钟频率、RAM初始化等。这些并不都是必须的,比如设置CPU的速度和时钟频率可以放在第二阶段。
其实将第二阶段拷贝到RAM中也不是必须的,对于NOR Flash可以直接在上面运行代码,但是效率将会降低。对于NAND FLASH必须要进行第二段代码的拷贝,因为一般Bootloader的二进制文件大于100K,而S3C2440内部自带的RAM才4K,不足以存放整个Bootloader。
  • 第二阶段
    1.初始化本阶段要使用的硬件设备(串口、网卡、NAND FLASH对于下载模式是必须的)
    2.检测系统内存映射(确定板上使用了多少内存、它们的地址空间)
    3.将内核镜像和根文件系统镜像从FLASH上读到RAM空间(对于不在FLASH上的内核和文件系统不执行)
    4.为内核设置启动参数
与内核的交互 Bootloader和内核的交互是单向的,只能从Bootloader向内核传递。与内核相互可以分为两种:以内核要求的形式设置硬件,以内核要求的地方存放数据。 1.设置硬件 (1)CPU寄存器设置
  • R0 = 0
  • R1 = 机器类型ID;可参见/linux/arch/arm/tools/mach-types(在启动内核时首先就是检测CPU类型和mach类型是否正确)
  • R2 = 启动参数标记列表在RAM中起始基地址
(2)CPU工作模式
  • 必须禁止中断(IRQs和FIQs)
  • CPU必须是SVC模式
(3)Cache和MMU的设置
  • MMU必须关闭
  • 指令Cache可以打开也可以关闭
  • 数据Cache必须关闭
2.存放数据 必须约定好参数的存放地址(通过R2寄存器传递),同时必须规范数据的结构。在Linux 2.4之后,内核期望以标记列表的形式来传递参数。标记列表以ATAG_CORE开始,以ATAG_NONE作为结束。 struct tag { struct tag_header hdr; union { struct tag_core core; struct tag_mem32 mem; struct tag_videotext videotext; struct tag_ramdisk ramdisk; struct tag_initrd initrd; struct tag_serialnr serialnr; struct tag_revision revision; struct tag_videolfb videolfb; struct tag_cmdline cmdline; /* * Acorn specific */ struct tag_acorn acorn; /* * DC21285 specific */ struct tag_memclk memclk; } u; }; 上述代码是标记的结构体 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); 上述代码是设置标记ATAG_CORE #define tag_next(t) ((struct tag *)((u32 *)(t) + (t)->hdr.size)) tag_next(t)用来移动指针 常用Bootloader 对于x86常见的Bootloader是LILO、GRUB等,对于ARM架构的CPU,有U-Boot和Vivi。 Vivi是Mizi公司针对SAMSUNG的ARM架构CPU专门设计的,基本上可以直接使用。不过初始版本只支持串口下载,速度慢。网上有各种改进版本,支持网络功能、USB功能、烧写YAFFS文件系统映像等。
U-boot则支持大多数的CPU,支持的功能强大,不过它的使用复杂。但是可以用来更方便地调试程序。

二、Uboot源码结构

Uboot特性 U-boot有如下特性:
  • 开放源码
  • 支持多种内核:Linux、NetBSD、Vxworks、QNX、RTEMS、ARTOS、LynxOS
  • 支持多个处理器系列:PowerPC、ARM、x86、MIPS、XScale
  • 高度灵活的功能设置,适合U-Boot调试、操作系统不同引导要求、产品发布
  • 丰富的设备驱动源码
  • 较为丰富的开发调试文档与强大的网络支持
  • 支持NFS挂载、RAMDISK形式的根文件系统
  • 支持NFS挂载、从Flash中引导压缩或非压缩系统内核
  • 可灵活设置、传递多个关键参数给操作系统、对Linux支持较为强劲
  • 支持目标板环境变量多种存储方式,如Flash、NVRAM、EEPROM
  • CRC32校验,可检查Flash中内核、RAMDISK镜像文件是否完好
  • 上电自检功能:SDRAM、FLASH大小自动检测
  • 特殊功能:XIP内核引导
U-boot下载地址 U-boot源码结构 接下来的分析都是对于U-boot-2010-03 U-boot的源码目录可以分为4类:
  • 平台相关或开发板相关目录
  • 通用函数
  • 驱动程序
  • U-Boot工具、示例程序、文档
U-Boot顶层目录说明 目录 特性 说明 board 开发板相关 对应不同的电路板(即使CPU相同),比如smdk2410、sbc2410x cpu 平台相关 对应不同的CPU,比如arm920T、i386等,他们的子目录可以进一步细分,比如arm920t下有at91rm9200、s3c24x0 lib_i386类似 平台相关 某一架构下通用的文件 include 通用函数 头文件和开发板配置文件,开发板的配置文件都放在include/configs目录下,U-Boot没有图形化配置菜单,需要手动修改配置文件中的宏定义 lib_generic 通用函数 通用的库函数,比如printf等 comment 通用函数 通用的函数,对下一层驱动程序的进一步封装 disk 驱动程序 硬盘接口程序 drivers 驱动程序 各类具体设备的驱动程序,基本上可以通用,他们通过宏从外面引入平台/开发板相关的函数 dtt 驱动程序 数字温度测量器或者传感器的驱动 fs 驱动程序 文件系统 nand_spl 驱动程序 NAND驱动程序 net 驱动程序 各种网络协议 post 驱动程序 上电自检程序 rtc 驱动程序 实时时钟的驱动 doc 文档 开发、使用文档 examples 示例程序 一些测试程序,可以使用U-Boot下载后运行 tools 工具 制作S-Recond、U-Boot格式映像的工具,比如mkimage U-boot的配置、编译、连接过程 编译U-boot只需要两步: make _config make all 之后就会在源码根目录下生成三个文件:
  • U-Boot.bin : 二进制可执行文件,可以直接烧入ROM、NOR Flash
  • U-Boot : ELF格式的可执行文件
  • U-Boot.srec : Motorola S-Record格式的可执行文件
在编译完成之后会在tools子目录下生成一些工具,比如mkimage,可以用来生成U_boot格式的内核镜像uImage U-Boot的配置过程 顶层makefile文件中代码 ... MKCONFIG := $(SRCTREE)/mkconfig ... SRCTREE := $(CURDIR) ... smdk2410_config : unconfig @$(MKCONFIG) $(@:_config=) arm arm920t smdk2410 samsung s3c24x0 $(@:_config=) 将目标中的config后缀去掉,结果为smdk2410 实际上上述代码等价于: ./mkconfig smdk2410 arm arm920t smdk2410 samsung s3c24x0 mkconfig是位于根目录下的脚本文件,接下来的很大一部分内容由它完成 mkconfig解析 # Parameters: Target Architecture CPU Board [VENDOR] [SOC] 说明了mkconfig中各个参数的含义 (1)确定开发板名称BOARD_NAME APPEND=no # Default: Create new config file BOARD_NAME="" # Name to print in make output TARGETS="" while [ $# -gt 0 ] ; do case "$1" in --) shift ; break ;; -a) shift ; APPEND=yes ;; -n) shift ; BOARD_NAME="${1%%_config}" ; shift ;; -t) shift ; TARGETS="`echo $1 | sed 's:_: :g'` ${TARGETS}" ; shift ;; *) break ;; esac done [ "${BOARD_NAME}" ] || BOARD_NAME="$1" 因为在命令中没有-a、-n等符号,所以while开始的语句没有执行。
执行完上述程序后BOARD_NAME = smdk2410 (2)创建头文件之间的连接 if [ "$SRCTREE" != "$OBJTREE" ] ; then ... else cd ./include rm -f asm ln -s asm-$2 asm fi rm -f asm-$2/arch if [ -z "$6" -o "$6" = "NULL" ] ; then ln -s ${LNPREFIX}arch-$3 asm-$2/arch else ln -s ${LNPREFIX}arch-$6 asm-$2/arch fi if [ "$2" = "arm" ] ; then rm -f asm-$2/proc ln -s ${LNPREFIX}proc-armv asm-$2/proc fi 一般我们会在源码根目录进行编译,所以SRCTREE=OBJTREE,执行else部分的代码。 删除原有的/include/asm,建立新的连接 ln -s /include/asm-arm /include/asm $6!=NULL,所以执行else语句,删除原有的/include/asm-arm/arm,建立新的连接,LNPREFIX为空 ln -s /include/asm-arm/arch-s3c24x0 /include/asm-arm/arch 建立另一个连接 ln -s /include/asm-arm/proc-armv /include/asm-arm/proc (3)为顶层makefile创建config.mk 下面代码摘自顶层makefile include $(obj)include/config.mk export ARCH CPU BOARD VENDOR SOC 接下来看mkconfig为config.mk中写了什么内容 echo "ARCH = $2" > config.mk echo "CPU = $3" >> config.mk echo "BOARD = $4" >> config.mk [ "$5" ] && [ "$5" != "NULL" ] && echo "VENDOR = $5" >> config.mk [ "$6" ] && [ "$6" != "NULL" ] && echo "SOC = $6" >> config.mk # Assign board directory to BOARDIR variable if [ -z "$5" -o "$5" = "NULL" ] ; then BOARDDIR=$4 else BOARDDIR=$5/$4 fi 创建/config.mk,输出如下内容 ARCH = arm CPU = arm920t BOARD = smdk2410 VENDOR = samsung SOC = s3c24x0 BORADDIR = samsung/smdk2410 (4)创建目标板相关头文件 if [ "$APPEND" = "yes" ] # Append to existing config file then echo >> config.h else > config.h # Create new config file fi echo "/* Automatically generated - do not edit */" >>config.h for i in ${TARGETS} ; do echo "#define CONFIG_MK_${i} 1" >>config.h ; done cat << EOF >> config.h #define CONFIG_BOARDDIR board/$BOARDDIR #include #include #include EOF APPEND=no,所以在include目录下创建config.h for i in ${TARGETS} ; do echo "#define CONFIG_MK_${i} 1" >>config.h ; done 该语句不知道什么意思,但是它没有执行 所以最后会生成/include/config.h,其内容如下: /* Automatically generated - do not edit */ #define CONFIG_BOARDDIR board/samsung/smdk2410 #include #include #include 可以看出我们需要在/include/config目录下创建和我们的目标板相应的头文件。需要在/board目录下创建属于开发板的目录。 总结上述过程:
(1)BOARD_NAME=smdk2410
(2)创建头文件之间的软连接 ln -s /include/asm-arm /include/asm ln -s /include/asm-arm/arch-s3c24x0 /include/asm-arm/arch ln -s /include/asm-arm/proc-armv /include/asm-arm/proc (3)为顶层makefile创建/include/config.mk ARCH = arm CPU = arm920t BOARD = smdk2410 VENDOR = samsung SOC = s3c24x0 BORADDIR = samsung/smdk2410 (4)创建/include/config.h /* Automatically generated - do not edit */ #define CONFIG_BOARDDIR board/samsung/smdk2410 #include #include #include 配置U-Boot U-Boot并没有图形化配置界面,所以需要手动配置,配置的文件是/include/configs/board_name.h
以/include/configs/smdk2410.h为例讲解如何配置U-Boot,并且配置是如何影响编译过程的。 配置文件中有两类宏:
  • 定义宏的存在
#define CONFIG_ARM920T 1 #define CONFIG_S3C24X0 1 #define CONFIG_S3C2410 1 #define CONFIG_SMDK2410 1 这类宏能决定编译文件中的哪一部分
  • 对宏进行赋值
#define CONFIG_SYS_CLK_FREQ 12000000 #define CONFIG_CS8900_BASE 0x19000300 #define CONFIG_BAUDRATE 115200 宏定义可以影响makefile的编译具体文件中的某一段的编译 COBJS-$(CONFIG_RTC_S3C24X0) += s3c24x0_rtc.o 上述代码取自/driver/rtc/Makefile #ifdef CONFIG_S3C2410 return (readl(&gpio->GPEDAT) & 0x8000) >> 15; #endif #ifdef CONFIG_S3C2400 return (readl(&gpio->PGDAT) & 0x0020) >> 5; #endif 上述代码取自/driver/i2c/s3c24x0_i2c.c U-Boot的编译、连接过程 接下来讲解的是执行make all时编译和连接的过程。从makefile中可以了解到U-Boot使用了哪些文件、哪些文件首先执行、可执行文件占用内存的情况。 确定makefile中与ARM相关的部分 include $(obj)include/config.mk export ARCH CPU BOARD VENDOR SOC ... ifeq ($(HOSTARCH),$(ARCH)) CROSS_COMPILE ?= endif 我们编译的HOSTARCH为x86,而ARCH为arm,所以上述代码的CROSS_COMPILE ?= 不执行,所以我们要在make的时候加上CROSS_COMPILE = arm-linux- 在/lib-arm/config.mk文件中定义了 CROSS_COMPILE ?= arm-linux- 但为了保险起见,还是在编译时加上CROSS_COMPILE = arm-linux- include $(TOPDIR)/config.mk 在makefile中还包含了根目录下在config.mk /config.mk分析 config.mk根据ARCH CPU BOARD VENDOR SOC定义了编译器和编译选项。 下列的代码摘自/config.mk ifdef VENDOR BOARDDIR = $(VENDOR)/$(BOARD) else BOARDDIR = $(BOARD) endif 其实这一段代码是多余的,因为在/include/config.mk已经对BORADDIR赋了相同的值。 ifdef ARCH sinclude $(TOPDIR)/lib_$(ARCH)/config.mk endif ifdef CPU sinclude $(TOPDIR)/cpu/$(CPU)/config.mk endif ifdef SOC sinclude $(TOPDIR)/cpu/$(CPU)/$(SOC)/config.mk endif ifdef BOARD sinclude $(TOPDIR)/board/$(BOARDDIR)/config.mk 包含了各目录下的config.mk文件 其中/board/samsung/smdk2410/config.mk中包含了一个和连接有关的重要参数 TEXT_BASE = 0x33F80000 该文件中只有这一行代码 PLATFORM_LDFLAGS = ... LDFLAGS += -Bstatic -T $(obj)u-boot.lds $(PLATFORM_LDFLAGS) ifneq ($(TEXT_BASE),) LDFLAGS += -Ttext $(TEXT_BASE) endif 上述语句相当于下列语句: LDFLAGS = -T u-boot.lds -Ttext 0x33F80000 其中的obj为空 makefile中具体编译内容 makefile中的目标文件只有两种OBJS、LIBS,其中OBJS是.o文件,LIBS是.a文件。 OBJS = cpu/$(CPU)/start.o ifeq ($(CPU),i386) OBJS += cpu/$(CPU)/start16.o OBJS += cpu/$(CPU)/resetvec.o endif ifeq ($(CPU),ppc4xx) OBJS += cpu/$(CPU)/resetvec.o endif ifeq ($(CPU),mpc85xx) OBJS += cpu/$(CPU)/resetvec.o endif 上述是所有和OBJS有关源文件,可以看出只有一个原文件/cpu/arm920t/start.o,除了这个文件,源码树中的所有其它文件都被编译成了静态库。 OBJS := $(addprefix $(obj),$(OBJS)) 为OBJS加上前缀 LIBS = lib_generic/libgeneric.a LIBS += lib_generic/lzma/liblzma.a LIBS += lib_generic/lzo/liblzo.a ... 和LIBS有关的原文件太多了 $(OBJS): depend $(MAKE) -C cpu/$(CPU) $(if $(REMOTE_BUILD),$@,$(notdir $@)) $(LIBS): depend $(SUBDIRS) $(MAKE) -C $(dir $(subst $(obj),,$@)) 上述代码显示进入相应的目录后调用子目录中的makefile,生成相应的目标文件。 $(obj)u-boot: depend $(SUBDIRS) $(OBJS) $(LIBBOARD) $(LIBS) $(LDSCRIPT) $(obj)u-boot.lds $(GEN_UBOOT) ifeq ($(CONFIG_KALLSYMS),y) smap=`$(call SYSTEM_MAP,u-boot) | awk '$$2 ~ /[tTwW]/ {printf $$1 $$3 "\\000"}'` ; $(CC) $(CFLAGS) -DSYSTEM_MAP=""$${smap}"" -c common/system_map.c -o $(obj)common/system_map.o $(GEN_UBOOT) $(obj)common/system_map.o endif $(obj)u-boot.srec: $(obj)u-boot $(OBJCOPY) -O srec $< $@ $(obj)u-boot.bin: $(obj)u-boot $(OBJCOPY) ${OBJCFLAGS} -O binary $< $@ 可见u-boot.bin是由u-boot经过转换而成的,主要考虑u-boot的编译过程。 连接方式由LDFLAGS决定 LDFLAGS = -T u-boot.lds -Ttext 0x33F80000 下面分析u-boot.lds文件 $(obj)u-boot.lds: $(LDSCRIPT) $(CPP) $(CPPFLAGS) $(LDPPFLAGS) -ansi -D__ASSEMBLY__ -P - <$^ >$@ 在源码的根目录中会生成我们需要的u-boot.lds,但是它是从/cpu/arm920t/u-boot.lds拷贝的,中间的过程不知道。 OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") OUTPUT_ARCH(arm) ENTRY(_start) SECTIONS { . = 0x00000000; . = ALIGN(4); .text : { cpu/arm920t/start.o (.text) *(.text) } . = ALIGN(4); .rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) } . = ALIGN(4); .data : { *(.data) } . = ALIGN(4); .got : { *(.got) } . = .; __u_boot_cmd_start = .; .u_boot_cmd : { *(.u_boot_cmd) } __u_boot_cmd_end = .; . = ALIGN(4); __bss_start = .; .bss (NOLOAD) : { *(.bss) . = ALIGN(4); } _end = .; } OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") OUTPUT_ARCH(arm) 这两句在执行file命令时,会显示 ENTRY(_start) 定义了入口点为_start,它存在在start.S中。 .globl _start _start: b start_code 后面的一大段可以看出cpu/arm920t/start.o 在最前面的,所以它最先执行,从该文件的_start标号处开始执行。
U-Boot启动过程源代码分析 1.第一阶段代码分析 第一阶段的启动代码包含两个文件:/cpu/arm920t/start.S/board/samsung/smdk2410/lowlevel_init .S
其中一个是和平台相关的、另一个是和开发板相关的。 (1)硬件设备初始化 以此完成如下设置:将CPU设置为SVC管理模式、关闭WATCHDOG、设置FCLK、HCLK、PCLK的比例(即设置CLKDIVN寄存器)、关闭MMU、CACHE。 (2)为加载Bootloader的第二阶段代码准备RAM空间 所谓准备RAM空间,对于s3c2410通过在调用start.S中调用lowlevel_init函数来设置存储控制器。 (3)复制Bootloader的第二阶段代码到RAM空间 (4)设置好栈 (5)清零BBS段、跳转到第二阶段代码的C入口点start_amrboot函数 U-Boot内存使用情况 2.第二阶段代码分析 第二阶段调用的函数位于文件/lib-arm/board.c
大致可以分为3个阶段:
  • init_sequence中的函数
  • start_armboot函数后续调用的初始化函数
  • 调用main_loop(),位于/commen/main.c
U_Boot命令格式 U_BOOT_CMD(name,maxargs,rep,cmd,usage,help) name : 命令的名字,它不是一个字符串(不要用双引号括起来)
maxargs : 最大的参数个数
repeatable : 命令是否可重复,可重复是指运行一个命令后,下次敲回车即可再次运行
command : 对应的函数指针
usage : 简短的使用说明,这是个字符串
help : 较详细的使用说明,这是个字符串 U_BOOT_CMD宏在include/command.h中定义 比如bootm命令,它如下定义: U_BOOT_CMD { bootm, CFG_MAXARGS,1,do_bootm,"string1","string2" }; 宏U_BOOT_CMD扩展后如下所示: cmd_tbl_t _u_boot_cmd_bootm __attribute__ ((unused,section(".u_boot_cmd"))) = {"bootm",CFG_MAXARGS,1,do_bootm,"string1","string2"}; 对于每个使用U_BOOT_CMD宏来定义的命令,其实都是在“.u_boot_cmd”段中定义一个cmd_tbl_t结构。 __u_boot_cmd_start = .; .u_boot_cmd : { *(.u_boot_cmd) } __u_boot_cmd_end = .; 程序中就是根据命令的名字在段__u_boot_cmd_start和__u_boot_cmd_end 之间找到相应的cmd_tbl_t结构,然后调用它的函数(请参考*command/command.c中的find_cmd函数) 为内核设置启动参数 U_Boot是通过标记列表向内核传递参数的。一般而言,设置内存标志和命令行标记就可以了,在配置文件中include/configs/smdk2410.h 中添加如下两个配置项即可: #define CONFIG_SETUP_MEMORY_TAGS 1 #define CONFIG_CMDLINE_TAG 1