s5pv210开发板学习笔记(14)——uboot(2)

3 篇文章 0 订阅

uboot主Makefile分析

uboot version确定
VERSION = 1            # VERSION:主板本号
PATCHLEVEL = 3         # PATCHLEVEL:次版本号
SUBLEVEL = 4           # SUBLEVEL:再次版本号
EXTRAVERSION =         # EXTRAVERSION:另外附加的版本信息
U_BOOT_VERSION = $(VERSION).$(PATCHLEVEL).$(SUBLEVEL)$(EXTRAVERSION)
# 这4个用.分隔开共同构成了最终的版本号
VERSION_FILE = $(obj)include/version_autogenerated.h

注:include/version_autogenerated.h文件是编译过程中自动生成的一个文件,所以源目录中没有,但是编译过后的uboot中就有了。它里面的内容是一个宏定义,宏定义的值内容就是我们在Makefile中配置的uboot的版本号。

// include/version_autogenerated.h
#define U_BOOT_VERSION "U-Boot 1.3.4"
HOSTARCH和HOSTOS
HOSTARCH := $(shell uname -m | \        # uname -m得到主机cpu架构
    sed -e s/i.86/i386/ \         # sed是进行字符串匹配和替换的工具,用法就是sed -e s/XX/XX,这句就是将i.86替换为i386
        -e s/sun4u/sparc64/ \
        -e s/arm.*/arm/ \
        -e s/sa110/arm/ \
        -e s/powerpc/ppc/ \
        -e s/ppc64/ppc/ \
        -e s/macppc/ppc/)
# $(shell uname -m) 与 `uname -m` 效果相同,都表示执行uname -m 
HOSTOS := $(shell uname -s | tr '[:upper:]' '[:lower:]' | \        # uname -s得到的是操作系统
        sed -e 's/\(cygwin\).*/cygwin/')
  1. 直接在shell中执行uname -m得到的值是当前执行这个命令的电脑的CPU的版本号

  1. shell中的|叫做管道,管道的作用就是把管道前面一个运算式的输出作为后面一个的输入再去做处理,最终的输出才是我们整个式子的输出。

  1. HOSTARCH这个名字:HOST是主机,就是当前在做开发用的这台电脑就叫主机;ARCH是architecture(架构)的缩写,表示CPU的架构。所以HOSTARCH就表示主机的CPU的架构。

  1. 这两个环境变量是主机的操作系统和主机的CPU架构,得出后保存备用,后面自然会用到。

静默编译
ifeq (,$(findstring s,$(MAKEFLAGS)))
XECHO = echo
else
XECHO = :
endif
  1. 平时默认编译时命令行会打印出来很多编译信息。但是有时候我们不希望看到这些编译信息,就后台编译即可。这就叫静默编译。

  1. 使用方法就是编译时make -s,-s会作为MAKEFLAGS传给Makefile,在50-54行这段代码作用下XECHO变量就会被变成空(默认等于echo),于是实现了静默编译。

2种编译方法(原地编译和单独输出文件夹编译)
  1. 编译复杂项目,Makefile提供2种编译管理方法。默认情况下是当前文件夹中的.c文件,编译出来的.o文件会放在同一文件夹下。这种方式叫原地编译。原地编译的好处就是处理起来简单。

  1. 原地编译有一些坏处:

  1. 污染了源文件目录。

  1. 一套源代码只能按照一种配置和编译方法进行处理,无法同时维护2个或2个以上的配置编译方式。

  1. 为了解决以上2种缺陷,uboot支持单独输出文件夹方式的编译(linux kernel也支持,而且uboot的这种技术就是从linux kernel学习来的)。基本思路就是在编译时另外指定一个输出目录,将来所有的编译生成的.o文件或生成的其他文件全部丢到那个输出目录下去。源代码目录不做任何污染,这样输出目录就承载了本次配置编译的所有结果。

  1. 具体用法:默认的就是原地编译。如果需要指定具体的输出目录编译则有2种方式:

  • make O=输出目录

  • export BUILD_DIR=输出目录 然后再make

如果两个都指定了(既有BUILD_DIR环境变量存在,又有O=xx),则O=xx具有更高优先级。

OBJTREE、SRCTREE、TOPDIR
  1. OBJTREE:编译出的.o文件存放的目录的根目录。在默认编译下,OBJTREE等于当前目录;在O=xx编译下,OBJTREE就等于我们设置的那个输出目录。

  1. SRCTREE: 源码目录,其实就是源代码的根目录,也就是当前目录。

总结:在默认编译下,OBJTREE和SRCTREE相等;在O=xx这种编译下OBJTREE和SRCTREE不相等。Makefile中定义这两个变量,其实就是为了记录编译后的.o文件往哪里放,就是为了实现O=xx的这种编译方式的。

  • MKCONFIG

MKCONFIG    := $(SRCTREE)/mkconfig

Makefile中定义的一个变量(在这里定义,在后面使用),它的值就是我们源码根目录下面的mkconfig。这个mkconfig是一个脚本,这个脚本就是uboot配置阶段的配置脚本。

  • include $(obj)include/config.mk

  1. include/config.mk不是源码自带的(你在没有编译过的源码目录下是找不到这个文件的),要在配置过程(make x210_sd_config)中才会生成这个文件。因此这个文件的值和我们配置过程有关,是由配置过程根据我们的配置自动生成的。

# include/config.mk
ARCH   = arm
CPU    = s5pc11x
BOARD  = x210
VENDOR = samsung
SOC    = s5pc110

# Makefile 133行
include $(obj)include/config.mk
export    ARCH CPU BOARD VENDOR SOC

# Makefile 2589行
x210_sd_config :    unconfig
    @$(MKCONFIG) $(@:_config=) arm s5pc11x x210 samsung s5pc110
    @echo "TEXT_BASE = 0xc3e00000" > $(obj)board/samsung/x210/config.mk
  1. 在下一行(10)export导出了这5个变量作为环境变量。所以着两行加起来其实就是为当前makefile定义了5个环境变量而已。之所以不直接给出这5个环境变量的值,是因为我们希望这5个值是可以被人很容易的、集中的配置的。

  1. 这里的配置值来自于2589行那里的配置项。如果我们要更改这里的某个配置值要到2589行那里调用MKCONFIG脚本传参时的参数。

ARCH CROSS_COMPILE
  1. 接下来有2个很重要的环境变量。一个是ARCH,上面导出的,值来自于我们的配置过程,它的值会影响后面的CROSS_COMPILE环境变量的值。ARCH的意义是定义当前编译的目标CPU的架构。

  1. CROSS_COMPILE是定义交叉编译工具链的前缀的。定义这些前缀是为了在后面用(用前缀加上后缀来定义编译过程中用到的各种工具链中的工具)。我们把前缀和后缀分开还有一个原因就是:在不同CPU架构上的交叉编译工具链,只是前缀不一样,后缀都是一样的。因此定义时把前缀和后缀分开,只需要在定义前缀时区分各种架构即可实现可移植性。

  1. CROSS_COMPILE在136-182行来确定。CROSS_COMPILE是被ARCH所确定的,只要配置了ARCH=arm,那么我们就只能在ARM的那个分支去设置CROSS_COMPILE的值。这个设置值只要能保证找到那个交叉编译工具链即可,不一定非得是全路径的,相对路径也可以。(如果已经将工具链导出到环境变量,并且设置了符号链接,这样CROSS_COMPILE = arm-linux-就可以)

  1. 实际运用时,我们可以在Makefile中去更改设置CROSS_COMPILE的值,也可以在编译时用make CROSS_COMPILE=xxxx来设置,而且编译时传参的方法可以覆盖Makefile里面的设置。

$(TOPDIR)/config.mk
编译工具定义:
# config.mk 94-107行
AS    = $(CROSS_COMPILE)as
LD    = $(CROSS_COMPILE)ld
CC    = $(CROSS_COMPILE)gcc
CPP    = $(CC) -E
AR    = $(CROSS_COMPILE)ar
NM    = $(CROSS_COMPILE)nm
LDR    = $(CROSS_COMPILE)ldr
STRIP    = $(CROSS_COMPILE)strip
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump
RANLIB    = $(CROSS_COMPILE)RANLIB
包含开发板配置项目
# config.mk, 112行
sinclude $(OBJTREE)/include/autoconf.mk
  1. autoconfig.mk文件不是源码提供的,是配置过程自动生成的。

  1. 这个文件的作用就是用来指导整个uboot的编译过程。这个文件的内容其实就是很多CONFIG_开头的宏(可以理解为变量),这些宏/变量会影响我们uboot编译过程的走向(原理就是条件编译)。在uboot代码中有很多地方使用条件编译进行编写,这个条件编译是用来实现可移植性的。(可以说uboot的源代码在很大程度来说是拼凑起来的,同一个代码包含了各种不同开发板的适用代码,用条件编译进行区别。)

  1. 这个文件不是凭空产生的,配置过程也是需要原材料来产生这个文件的。原材料在源码目录的include/configs/xxx.h头文件。(X210开发板中为include/configs/x210_sd.h)。这个h头文件里面全都是宏定义,这些宏定义就是我们对当前开发板的移植。每一个开发板的移植都对应这个目录下的一个头文件,这个头文件里每一个宏定义都很重要,这些配置的宏定义就是我们移植uboot的关键所在。

2.4.5.1、链接脚本

# config.mk 142-149行
ifndef LDSCRIPT
#LDSCRIPT := $(TOPDIR)/board/$(BOARDDIR)/u-boot.lds.debug
ifeq ($(CONFIG_NAND_U_BOOT),y)
LDSCRIPT := $(TOPDIR)/board/$(BOARDDIR)/u-boot-nand.lds
else
LDSCRIPT := $(TOPDIR)/board/$(BOARDDIR)/u-boot.lds
endif
endif
  1. 如果定义了CONFIG_NAND_U_BOOT宏,则链接脚本叫u-boot-nand.lds,如果未定义这个宏则链接脚本叫u-boot.lds。

  1. 从字面意思分析,即可知:CONFIG_NAND_U_BOOT是在Nand版本情况下才使用的,我们使用的X210都是iNand版本的,因此这个宏没有的。

  1. 实际在board\samsung\x210目录下有u-boot.lds,这个就是链接脚本。我们在分析uboot的编译链接过程时就要考虑这个链接脚本。

TEXT_BASE
# config.mk 156-158行
ifneq ($(TEXT_BASE),)
CPPFLAGS += -DTEXT_BASE=$(TEXT_BASE)
endif
  1. Makefile中在配置X210开发板时,在board/samsung/x210目录下生成了一个文件config.mk,其中的内容就是:TEXT_BASE = 0xc3e00000相当于定义了一个变量。

  1. TEXT_BASE是将来我们整个uboot链接时指定的链接地址。因为uboot中启用了虚拟地址映射,因此这个C3E00000地址就等于0x23E00000(也可能是33E00000具体地址要取决于uboot中做的虚拟地址映射关系)。

第一个目标all
  1. 291行出现了整个主Makefile中第一个目标all(也就是默认目标,我们直接在uboot根目录下make其实就等于make all,就等于make这个目标)

  1. 目标中有一些比较重要的。譬如:u-boot是最终编译链接生成的elf格式的可执行文件,

  1. unconfig字面意思来理解就是未配置。这个符号用来做为我们各个开发板配置目标的依赖。目标是当我们已经配置过一个开发板后再次去配置时还可以配置。

  1. 我们配置开发板时使用:make x210_sd_config,因此分析x210_sd_config肯定是主Makefile中的一个目标。

uboot配置过程详解
mkconfig脚本的6个参数
# Makefile 2589~2591行
x210_sd_config :    unconfig
    @$(MKCONFIG) $(@:_config=) arm s5pc11x x210 samsung s5pc110
    @echo "TEXT_BASE = 0xc3e00000" > $(obj)board/samsung/x210/config.mk
$(@:_config=) arm s5pc11x x210 samsung s5pc110
x210_sd_config里的_config部分用空替换,得到:x210_sd,这就是第一个参数,所以:
$1:x210_sd
$2:arm
$3: s5pc11x
$4:x210
$5: samsumg
$6:s5pc110
所以,$# = 6
# mkconfig 23行
[ "${BOARD_NAME}" ] || BOARD_NAME="$1"

就是看BOARD_NAME变量是否有值,如果有值就维持不变;如果无值就给他赋值为$1,实际分析结果:BOARD_NAME=x210_sd

[ $# -lt 4 ] && exit 1
[ $# -gt 6 ] && exit 1

所以,mkconfig脚本传参只能是4、5、6,如果大于6或者小于4都不行。

符号链接
  • 从第33行到第118行,都是在创建符号链接。为什么要创建符号链接?

这些符号链接文件的存在就是整个配置过程的核心,这些符号链接文件(文件夹)的主要作用是给头文件包含等过程提供指向性连接。根本目的是让uboot具有可移植性。

  • uboot可移植性的实现原理:在uboot中有很多彼此平行的代码,各自属于各自不同的架构/CPU/开发板,我们在具体到一个开发板的编译时用符号连接的方式提供一个具体的名字的文件夹供编译时使用。这样就可以在配置的过程中通过不同的配置使用不同的文件,就可以正确的包含正确的文件。

  • 创建的符号链接:

# mkconfig 46行
cd ./include
    rm -f asm
    ln -s asm-$2 asm
# 第一个符号链接,在include目录下创建asm文件,指向asm-arm。

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
# 第二个符号链接,在include/asm-arm下创建一个arch文件,指向include/asm-arm/arch-s5pc110

if [ "$3" = "s5pc11x" ] ; then
        rm -f regs.h
        ln -s $6.h regs.h
        rm -f asm-$2/arch
        ln -s arch-$3 asm-$2/arch
fi
# 第三个符号链接,在include目录下创建regs.h文件,指向include/s5pc110.h,删除第二个符号链接。
# 第四个符号链接,在include/asm-arm下创建一个arch文件,指向include/asm-arm/arch-s5pc11x

if [ "$2" = "arm" ] ; then
    rm -f asm-$2/proc
    ln -s ${LNPREFIX}proc-armv asm-$2/proc
fi
# 第五个符号链接,在include/asm-arm下创建一个proc文件,指向include/asm-arm/proc-armv

总结:一共创建了4个符号链接。这4个符号链接将来在写代码过程中,头文件包含时非常有用。譬如一个头文件包含可能是:#include <asm/xx.h>

创建include/config.mk文件
# mkconfig文件123-129行
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
  1. 创建include/config.mk文件是为了让主Makefile在第133行去包含的。

  1. 创建(默认情况)/追加(make -a时追加)include/config.h文件。

# mkconfig文件的134-141行
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
echo "#include <configs/$1.h>" >>config.h
  1. include/config.h里面的内容就一行#include <configs/x210_sd.h>,这个头文件是我们移植x210开发板时,对开发板的宏定义配置文件。这个文件是我们移植x210时最主要的文件。

  1. x210_sd.h文件会被用来生成一个autoconfig.mk文件,这个文件会被主Makefile引入,指导整个编译过程。这里面的这些宏定义会影响我们对uboot中大部分.c文件中一些条件编译的选择。从而实现最终的可移植性。

注意:

  1. uboot的整个配置过程,很多文件之间是有关联的(有时候这个文件是在那个文件中创建出来的;有时候这个文件被那个文件包含进去;有时候这个文件是由那个文件的内容生成的决定的)。

  1. uboot中配置和编译过程,所有的文件或者全局变量都是字符串形式的(不是指的C语言字符串的概念,指的是都是字符组成的序列)。这意味着我们整个uboot的配置过程都是字符串匹配的,所以一定要细节,注意大小写,要注意不要输错字符,因为一旦错一个最后会出现一些莫名其妙的错误,很难排查,这个是uboot移植过程中新手来说最难的地方。

uboot的链接脚本

  1. ENTRY(_start)用来指定整个程序的入口地址。所谓入口地址就是整个程序的开头地址,可以认为就是整个程序的第一句指令。有点像C语言中的main。

  1. 指定程序的链接地址有2种方法:一种是在Makefile中ld的flags用-Ttext 0x20000000来指定;第二种是在链接脚本的SECTIONS开头用.=0x20000000来指定。两种都可以实现相同效果。其实,这两种技巧是可以共同配合使用的,也就是说既在链接脚本中指定也在ld flags中用-Ttext来指定。两个都指定以后以-Ttext指定的为准。

  1. uboot的最终链接起始地址就是在Makefile中用-Ttext 来指定的,为TEXT_BASE变量。最终来源是Makefile中配置对应的命令中,在make xxx_config时得到的。

  1. 在代码段中注意文件排列的顺序。指定必须放在前面部分的那些文件就是那些必须安排在前16KB内的文件,这些文件中的函数在前16KB会被调用。在后面第二部分(16KB之后)中调用的程序,前后顺序就无所谓了。

  1. 链接脚本中除了.text .data .rodata .bss段等编译工具自带的段之外,编译工具还允许我们自定义段。譬如uboot总的.u_boot_cmd段就是自定义段,自定义段很重要。

start.S

u-boot.lds中找到start.S入口
  1. 在C语言中整个项目的入口就是main函数(这是C语言规定的),所以譬如说一个有10000个.c文件的项目,第一个要分析的文件就是包含了main函数的那个文件。

  1. 在uboot中因为有汇编阶段参与,因此不能直接找main.c。整个程序的入口取决于链接脚本中ENTRY声明的地方。ENTRY(_start)因此_start符号所在的文件就是整个程序的起始文件,_start所在处的代码就是整个程序的起始代码。

start.S解析
不简单的头文件包含
  1. #include <config.h>。config.h是在include目录下的,这个文件不是源码中本身存在的文件,而是配置过程中自动生成的文件。(详见mkconfig脚本)。这个文件的内容其实是包含了一个头文件:#include <configs/x210_sd.h>".

  1. 经过分析后,发现start.S中包含的第一个头文件就是:include/configs/x210_sd.h,这个文件是整个uboot移植时的配置文件。这里面是好多宏。因此这个头文件包含将include/configs/x210_sd.h文件和start.S文件关联了起来。因此之后在分析start.S文件时,主要要考虑的就是x210_sd.h文件。

  1. #include <version.h>。include/version.h中包含了include/version_autogenerated.h,这个头文件就是配置过程中自动生成的。里面就一行内容:#define U_BOOT_VERSION "U-Boot 1.3.4"。这里面定义的宏U_BOOT_VERSION的值是一个字符串,字符串中的版本号信息来自于Makefile中的配置值。这个宏在程序中会被调用,在uboot启动过程中会串口打印出uboot的版本号,那个版本号信息就是从这来的。

  1. #include <asm/proc/domain.h>。asm目录不是uboot中的原生目录,uboot中本来是没有这个目录的。asm目录是配置时创建的一个符号链接,实际指向的是就是asm-arm。

  1. 经过分析后发现,实际文件是:include/asm-arm/proc-armv/domain.h

  1. 从这里可以看出之前配置时创建的符号链接的作用,如果没有这些符号链接则编译时根本通不过,因为找不到头文件。(所以uboot不能在windows的共享文件夹下配置编译,因为windows中没有符号链接)

思考:为什么start.S不直接包含asm-arm/proc-armv/domain.h,而要用asm/proc/domain.h?

这样的设计主要是为了可移植性。因为如果直接包含,则start.S文件和CPU架构(和硬件)有关了,可移植性就差了。譬如我要把uboot移植到mips架构下,则start.S源代码中所有的头文件包含全部要修改。我们用了符号链接之后,则start.S中源代码不用改,只需要在具体的硬件移植时配置不同,创建的符号链接指向的不同,则可以具有可移植性。

启动代码的16字节头部
  1. 裸机中讲过,在SD卡启动/Nand启动等整个镜像开头需要16字节的校验头。(mkv210image.c中就是为了计算这个校验头)。我们以前做裸机程序时根本没考虑这16字节校验头,因为:1、如果我们是usb启动直接下载的方式启动的则不需要16字节校验头(irom application note);2、如果是SD卡启动mkv210image.c中会给原镜像前加16字节的校验头。

  1. uboot这里start.S中在开头位置放了16字节的填充占位,这个占位的16字节只是保证正式的image的头部确实有16字节,但是这16字节的内容是不对的,还是需要后面去计算校验和然后重新填充的。

异常向量表的构建
  1. 异常向量表是硬件决定的,软件只是参照硬件的设计来实现它。

  1. 异常向量表中每种异常都应该被处理,否则真遇到了这种异常就跑飞了。但是我们在uboot中并未非常细致的处理各种异常。

  1. 复位异常处的代码是:b reset,因此在CPU复位后真正去执行的有效代码是reset处的代码,因此reset符号处才是真正的有意义的代码开始的地方。

有点意思的deadbeef
  1. .balignl 16,0xdeadbeef.这一句指令是让当前地址对齐排布,如果当前地址不对齐则自动向后走地址直到对齐,并且向后走的那些内存要用0xdeadbeef来填充。

  1. 0xdeadbeef这是一个十六进制的数字,这个数字很有意思,组成这个数字的十六进制数全是abcdef之中的字母,而且这8个字母刚好组成了英文的dead beef这两个单词,字面意思是坏牛肉。

  1. 为什么要对齐访问?有时候是效率的要求,有时候是硬件的特殊要求。

TEXT_BASE等
  1. 第100行这个TEXT_BASE就是上个课程中分析Makefile时讲到的那个配置阶段的TEXT_BASE,其实就是我们链接时指定的uboot的链接地址。(值就是c3e00000)

  1. 源代码中和配置Makefile中很多变量是可以互相运送的。简单来说有些符号的值可以从Makefile中传递到源代码中。

CFG_PHY_UBOOT_BASE 33e00000uboot在DDR中的物理地址

设置CPU为SVC模式
  1. msrcpsr_c, #0xd3将CPU设置为禁止FIQ IRQ,ARM状态,SVC模式。

  1. 其实ARM CPU在复位时默认就会进入SVC模式,但是这里还是使用软件将其置为SVC模式。整个uboot工作时CPU一直处于SVC模式。

设置L2、L1cache和MMU
  1. bl disable_l2cache // 禁止L2 cache

  1. bl set_l2cache_auxctrl_cycle // l2 cache相关初始化

  1. bl enable_l2cache // 使能l2 cache

  1. 刷新L1 cache的icache和dcache。

  1. 关闭MMU

总结:上面这5步都是和CPU的cache和mmu有关的,不用去细看,大概知道即可。

识别并暂存启动介质选择
  1. 从哪里启动是由SoC的OM5:OM0这6个引脚的高低电平决定的。

  1. 实际上在210内部有一个寄存器(地址是0xE0000004),这个寄存器中的值是硬件根据OM引脚的设置而自动设置值的。这个值反映的就是OM引脚的接法(电平高低),也就是真正的启动介质是谁。

  1. 我们代码中可以通过读取这个寄存器的值然后判断其值来确定当前选中的启动介质是Nand还是SD还是其他的。

  1. start.S的225-227行执行完后,在r2寄存器中存储了一个数字,这个数字等于某个特定值时就表示SD启动,等于另一个特定值时表示从Nand启动····

  1. 260行中给r3中赋值#BOOT_MMCSD(0x03),这个在SD启动时实际会被执行,因此执行完这一段代码后r3中存储了0x03,以后备用。

设置栈(SRAM中的栈)并调用lowlevel_init
  1. 284-286行第一次设置栈。这次设置栈是在SRAM中设置的,因为当前整个代码还在SRAM中运行,此时DDR还未被初始化还不能用。栈地址0xd0036000是自己指定的,指定的原则就是这块空间只给栈用,不会被别人占用。

  1. 在调用函数前初始化栈,主要原因是在被调用的函数内还有再次调用函数,而BL只会将返回地址存储到LR中,但是我们只有一个LR,所以在第二层调用函数前要先将LR入栈,否则函数返回时第一层的返回地址就丢了。

  1. lowlevel_init函数真正的地方,是在uboot/board/samsumg/x210/lowlevel_init.S中。

检查复位状态
    /* check reset status  */
    ldr    r0, =(ELFIN_CLOCK_POWER_BASE+RST_STAT_OFFSET)
    ldr    r1, [r0]
    bic    r1, r1, #0xfff6ffff
    cmp    r1, #0x10000
    beq    wakeup_reset_pre
    cmp    r1, #0x80000
    beq    wakeup_reset_from_didle
  1. 复杂CPU允许多种复位情况。譬如直接冷上电、热启动、睡眠(低功耗)状态下的唤醒等,这些情况都属于复位。所以我们在复位代码中要去检测复位状态,来判断到底是哪种情况。

  1. 判断哪种复位的意义在于:冷上电时DDR是需要初始化才能用的;而热启动或者低功耗状态下的复位则不需要再次初始化DDR。

供电锁存
// lowlevel_init.S的第100-104行,开发板供电锁存。
ldr    r0, =(ELFIN_CLOCK_POWER_BASE + PS_HOLD_CONTROL_OFFSET)
ldr    r1, [r0]
orr    r1, r1, #0x300    
orr    r1, r1, #0x1    
str    r1, [r0]

总结:在前100行,lowlevel_init.S中并没有做太多有意义的事情(除了关看门狗、供电锁存外),然后下面从110行才开始进行有意义的操作。

判断当前代码执行位置
// lowlevel_init.S的110-115行
ldr    r0, =0xff000fff
bic    r1, pc, r0        /* 将pc的值中的某些bit位清0,剩下一些特殊的bit位赋值给r1(r0中为1的那些位清零)相等于:r1 = pc & ~(ff000fff) */
ldr    r2, _TEXT_BASE        /* ldr r2, _TEXT_BASE加载链接地址到r2 */
bic    r2, r2, r0        /* r0 <- current base addr of code */
cmp    r1, r2                  /* 比较r1和r2 */
beq    1f            /* r0 == r1 then skip sdram init   */
  • 这几行代码的作用就是判定当前代码执行的位置在SRAM中还是在DDR中。为什么要做这个判定?

  1. BL1(uboot的前一部分)在SRAM中有一份,在DDR中也有一份,因此如果是冷启动那么当前代码应该是在SRAM中运行的BL1,如果是低功耗状态的复位这时候应该就是在DDR中运行的。

  1. 我们判定当前运行代码的地址是有用的,可以指导后面代码的运行。譬如在lowlevel_init.S中判定当前代码的运行地址,就是为了确定要不要执行时钟初始化和初始化DDR的代码。如果当前代码是在SRAM中,说明冷启动,那么时钟和DDR都需要初始化;如果当前代码是在DDR中,那么说明是热启动则时钟和DDR都不用再次初始化。

总结:这一段代码是通过读取当前运行地址和链接地址,然后处理两个地址后对比是否相等,来判定当前运行是在SRAM中(不相等)还是DDR中(相等)。从而决定是否跳过下面的时钟和DDR初始化。

system_clock_init
  1. 使用SI搜索功能,确定这个函数就在当前文件的205行,一直到第385行。这个初始化时钟的过程和裸机中初始化的过程一样的,只是更加完整而且是用汇编代码写的。

  1. 在x210_sd.h中300行到428行,都是和时钟相关的配置值。这些宏定义就决定了210的时钟配置是多少。也就是说代码在lowlevel_init.S中都写好了,但是代码的设置值都被宏定义在x210_sd.h中了。因此,如果移植时需要更改CPU的时钟设置,根本不需要动代码,只需要在x210_sd.h中更改配置值即可。

mem_ctrl_asm_init
  1. 该函数用来初始化DDR

  1. 函数位置在uboot/cpu/s5pc11x/s5pc110/cpu_init.S文件中。

  1. 之前在裸机中时配置为2开头的地址,当时并没有说可以配置为3开头。从分析九鼎移植的uboot可以看出:DMC0上允许的地址范围是20000000-3FFFFFFF(一共是512MB),而我们实际只接了256MB物理内存,SoC允许我们给这256MB挑选地址范围。

总结一下:在uboot中,可用的物理地址范围为:0x30000000-0x4FFFFFFF。一共512MB,其中30000000-3FFFFFFF为DMC0,40000000-4FFFFFFF为DMC1。

  1. 我们需要的内存配置值在x210_sd.h的438行到468行之间。分析的时候要注意条件编译的条件,配置头文件中考虑了不同时钟配置下的内存配置值,这个的主要目的是让不同时钟需求的客户都能找到合适自己的内存配置值。

  1. 在uboot中DMC0和DMC1都工作了,所以在裸机中只要把uboot中的配置值和配置代码全部移植过去,应该是能够让DMC0和DMC1都工作的。

uart_asm_init
  1. 这个函数用来初始化串口

  1. 初始化完了后通过串口发送了一个'O'

pop {pc}以返回:
// 返回前通过串口打印'K'
ldr    r0, =ELFIN_UART_CONSOLE_BASE
ldr    r1, =0x4b4b4b4b
str    r1, [r0, #UTXH_OFFSET]

pop    {pc}

分析;lowlevel_init.S执行完如果没错那么就会串口打印出"OK"字样。这应该是我们uboot中看到的最早的输出信息。

总结回顾:lowlevel_init.S中总共做了哪些事情:

检查复位状态、IO恢复、关看门狗、开发板供电锁存、时钟初始化、DDR初始化、串口初始化并打印'O'、tzpc初始化、打印'K'。

其中值得关注的:关看门狗、开发板供电锁存、时钟初始化、DDR初始化、打印"OK"

再次设置栈(DDR中的栈)
// start.S 297-299行  
/* get ready to call C functions */
    ldr    sp, _TEXT_PHY_BASE    /* setup temp stack pointer */
    sub    sp, sp, #12
    mov    fp, #0            /* no previous frame, so fp=0 */
  1. 再次开发板供电锁存。第一,做2次是不会错的;第二,做2次则第2次无意义;做代码移植时有一个古怪谨慎保守策略就是尽量添加代码而不要删除代码。

  1. 之前在调用lowlevel_init程序前设置过1次栈(start.S 284-287行),那时候因为DDR尚未初始化,因此程序执行都是在SRAM中,所以在SRAM中分配了一部分内存作为栈。本次因为DDR已经被初始化了,因此要把栈挪移到DDR中,所以要重新设置栈,这是第二次;这里实际设置的栈的地址是33E00000,刚好在uboot的代码段的下面紧挨着。

  1. 为什么要再次设置栈?

DDR已经初始化了,已经有大片内存可以用了,没必要再把栈放在SRAM中可怜兮兮的了;原来SRAM中内存大小空间有限,栈放在那里要注意不能使用过多的栈否则栈会溢出,我们及时将栈迁移到DDR中也是为了尽可能避免栈使用时候的小心翼翼。

感慨:uboot的启动阶段主要技巧就在于小范围内有限条件下的辗转腾挪。

再次判断当前地址以决定是否重定位
  1. 再次用相同的代码判断运行地址是在SRAM中还是DDR中,不过本次判断的目的不同(上次判断是为了决定是否要执行初始化时钟和DDR的代码)这次判断是为了决定是否进行uboot的relocate。

  1. 冷启动时当前情况是uboot的前一部分(16kb或者8kb)开机自动从SD卡加载到SRAM中正在运行,uboot的第二部分(其实第二部分是整个uboot)还躺在SD卡的某个扇区开头的N个扇区中。此时uboot的第一阶段已经即将结束了(第一阶段该做的事基本做完了),结束之前要把第二部分加载到DDR中链接地址处(33e00000),这个加载过程就叫重定位。

uboot重定位详解
// 312行
#if defined(CONFIG_EVT1)
    /* If BL1 was copied from SD/MMC CH2 */
    ldr    r0, =0xD0037488
    ldr    r1, [r0]
    ldr    r2, =0xEB200000
    cmp    r1, r2
    beq     mmcsd_boot
#endif
    ldr    r0, =INF_REG_BASE
    ldr    r1, [r0, #INF_REG3_OFFSET]
    cmp    r1, #BOOT_NAND        /* 0x0 => boot device is nand */
    beq    nand_boot
    cmp    r1, #BOOT_ONENAND    /* 0x1 => boot device is onenand */
    beq    onenand_boot
    cmp     r1, #BOOT_MMCSD
    beq     mmcsd_boot
    cmp     r1, #BOOT_NOR
    beq     nor_boot
    cmp     r1, #BOOT_SEC_DEV
    beq     mmcsd_boot

// 343行
mmcsd_boot:
#if DELETE
    ldr     sp, _TEXT_PHY_BASE      
    sub     sp, sp, #12
    mov     fp, #0
#endif
    bl      movi_bl2_copy
    b       after_copy
  1. D0037488这个内存地址在SRAM中,这个地址中的值是被硬件自动设置的。硬件根据我们实际电路中SD卡在哪个通道中,会将这个地址中的值设置为相应的数字。譬如我们从SD0通道启动时,这个值为EB000000;从SD2通道启动时,这个值为EB200000

  1. 我们在start.S的260行确定了从MMCSD启动,然后又在278行将#BOOT_MMCSD写入了INF_REG3寄存器中存储着。然后又在322行读出来,再和#BOOT_MMCSD去比较,确定是从MMCSD启动。最终跳转到mmcsd_boot函数中去执行重定位动作。

  1. 真正的重定位是通过调用movi_bl2_copy函数完成的,在uboot/cpu/s5pc11x/movi.c中。是一个C语言的函数。

  1. copy_bl2(2, MOVI_BL2_POS, MOVI_BL2_BLKCNT, CFG_PHY_UBOOT_BASE, 0);

分析参数:2表示通道2;MOVI_BL2_POS是uboot的第二部分在SD卡中的开始扇区,这个扇区数字必须和烧录uboot时烧录的位置相同;MOVI_BL2_BLKCNT是uboot的长度占用的扇区数;CFG_PHY_UBOOT_BASE是重定位时将uboot的第二部分复制到DDR中的起始地址(33E00000)。

MMU单元的作用
  1. MMU就是memory management unit,内存管理单元。MMU实际上是SOC中一个硬件单元,它的主要功能就是实现虚拟地址到物理地址的映射。

  1. MMU单片在CP15协处理器中进行控制,也就是说要操控MMU进行虚拟地址映射,方法就是对cp15协处理器的寄存器进行编程。

地址映射的额外收益1:访问控制
  1. 访问控制就是:在管理上对内存进行分块,然后每块进行独立的虚拟地址映射,然后在每一块的映射关系中同时还实现了访问控制(对该块可读、可写、只读、只写、不可访问等控制)

  1. 回想在C语言中编程中经常会出现一个错误:Segmentation fault。实际上这个段错误就和MMU实现的访问控制有关。当前程序只能操作自己有权操作的地址范围(若干个内存块),如果当前程序指针出错访问了不该访问的内存块则就会触发段错误。

地址映射的额外收益2:cache
  1. cache的工作和虚拟地址映射有关系。

  1. cache是快速缓存,意思就是比CPU慢但是比DDR块。CPU嫌DDR太慢了,于是乎把一些DDR中常用的内容事先读取缓存在cache中,然后CPU每次需要找东西时先在cache中找。如果cache中有就直接用cache中的;如果cache中没有才会去DDR中寻找。

使能域访问(cp15的c3寄存器)
// start.S的360行
enable_mmu:
    /* enable domain access */
    ldr    r5, =0x0000ffff
    mcr    p15, 0, r5, c3, c0, 0        @load domain access register
  1. cp15协处理器内部有c0到c15共16个寄存器,这些寄存器每一个都有自己的作用。我们通过mrc和mcr指令来访问这些寄存器。所谓的操作cp协处理器其实就是操作cp15的这些寄存器。

  1. c3寄存器在mmu中的作用是控制域访问。域访问是和MMU的访问控制有关的。

设置TTB(cp15的c2寄存器)
// start.S的366行
    /* Set the TTB register */
    ldr    r0, _mmu_table_base
    ldr    r1, =CFG_PHY_UBOOT_BASE
    ldr    r2, =0xfff00000
    bic    r0, r0, r2
    orr    r1, r0, r1
    mcr    p15, 0, r1, c2, c0, 0
  1. TTB就是translation table base,转换表基地址。首先要明白什么是TT(translation table转换表),TTB其实就是转换表的基地址。

  1. 转换表是建立一套虚拟地址映射的关键。转换表分2部分,表索引和表项。表索引对应虚拟地址,表项对应物理地址。一对表索引和表项构成一个转换表单元,能够对一个内存块进行虚拟地址转换。(映射中基本规定中规定了内存映射和管理是以块为单位的,至于块有多大,要看你的MMU的支持和你自己的选择。在ARM中支持3种块大小,细表1KB、粗表4KB、段1MB)。真正的转换表就是由若干个转换表单元构成的,每个单元负责1个内存块,总体的转换表负责整个内存空间(0-4G)的映射。

  1. 整个建立虚拟地址映射的主要工作就是建立这张转换表

  1. 转换表放置在内存中的,放置时要求起始地址在内存中要xx位对齐。转换表不需要软件去干涉使用,而是将基地址TTB设置到cp15的c2寄存器中,然后MMU工作时会自动去查转换表。

使能MMU单元(cp15的c1寄存器)
// start.S的374行
    /* Enable the MMU */
mmu_on:
    mrc    p15, 0, r0, c1, c0, 0
    orr    r0, r0, #1
    mcr    p15, 0, r0, c1, c0, 0

cp15的c1寄存器的bit0控制MMU的开关。只要将这一个bit置1即可开启MMU。开启MMU之后上层软件层的地址就必须经过TT的转换才能发给下层物理层去执行。

找到映射表待分析

通过符号查找,确定转换表在lowlevel_init.S文件的593行。

宏观上理解转换表:
  1. 整个转换表可以看作是一个int类型的数组,数组中的一个元素就是一个表索引和表项的单元。数组中的元素值就是表项,这个元素的数组下标就是表索引。

  1. ARM的段式映射中长度为1MB,因此一个映射单元只能管1MB内存,那我们整个4G范围内需要4G/1MB=4096个映射单元,也就是说这个数组的元素个数是4096。实际上我们做的时候并没有依次单个处理这4096个单元,而是把4096个分成几部分,然后每部分用for循环做相同的处理。

再次设置栈
// start.S的386行
stack_setup:
#if defined(CONFIG_MEMORY_UPPER_CODE)
    ldr    sp, =(CFG_UBOOT_BASE + CFG_UBOOT_SIZE - 0x1000)
#else
    ldr    r0, _TEXT_BASE        /* upper 128 KiB: relocated uboot   */
    sub    r0, r0, #CFG_MALLOC_LEN    /* malloc area                      */
    sub    r0, r0, #CFG_GBL_DATA_SIZE /* bdinfo                        */
#if defined(CONFIG_USE_IRQ)
    sub    r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ)
#endif
    sub    sp, r0, #12        /* leave 3 words for abort-stack    */
  1. 第三次设置栈。这次设置栈还是在DDR中,之前虽然已经在DDR中设置过一次栈了,但是本次设置栈的目的是将栈放在比较合适(安全,紧凑而不浪费内存)的地方。

  1. 我们实际将栈设置在uboot起始地址上方2MB处,这样安全的栈空间是:2MB-uboot大小-0x1000=1.8MB左右。这个空间既没有太浪费内存,又足够安全。

清理bss
// start.S的400行
clear_bss:
    ldr    r0, _bss_start        /* find start of bss segment        */
    ldr    r1, _bss_end        /* stop here                        */
    mov     r2, #0x00000000        /* clear                            */

clbss_l:
    str    r2, [r0]        /* clear loop...                    */
    add    r0, r0, #4
    cmp    r0, r1
    ble    clbss_l
    
    ldr    pc, _start_armboot

_start_armboot:
    .word start_armboot

注意:表示bss段的开头和结尾地址的符号是从链接脚本u-boot.lds得来的。

ldr pc, _start_armboot
  1. start_armboot是uboot/lib_arm/board.c中,这是一个C语言实现的函数。这个函数就是uboot的第二阶段。这句代码的作用就是将uboot第二阶段执行的函数的地址传给pc,实际上就是使用一个远跳转直接跳转到DDR中的第二阶段开始地址处。

  1. 远跳转的含义就是这句话加载的地址和当前运行地址无关,而和链接地址有关。因此这个远跳转可以实现从SRAM中的第一阶段跳转到DDR中的第二阶段。

  1. 这里这个远跳转就是uboot第一阶段和第二阶段的分界线。

总结:uboot的第一阶段做了哪些工作
  1. 构建异常向量表

  1. 设置CPU为SVC模式

  1. 关看门狗

  1. 开发板供电置锁

  1. 时钟初始化

  1. DDR初始化

  1. 串口初始化并打印"OK"

  1. 重定位

  1. 建立映射表并开启MMU

  1. 跳转到第二阶段

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值