uboot和系统移植----------5、启动第一阶段
start.S分析
- 一、start.S分析
-
-
1、不简单的头文件包含
-
2、16字节的校验头
-
3、异常向量表(一级中断表)
-
4、 deadbeef
-
5、TEXT_BASE
-
6、真正的起始代码:reset
-
- (1)设置 cpsr_c 位
- (2)设置L2、L1cache和MMU
- (3)识别并暂存启动介质选择
- (4)设置栈(SRAM中的栈)并调用lowlevel_init
-
- (a)、检查复位状态
-
(b)、判断当前代码在哪里运行(SRAM还是DDR)
-
(d)、system_clock_init
-
(e)、mem_ctrl_asm_init
-
(f)、uart_asm_init
-
(g)、pop {pc}以返回
- (5)再次设置栈(DDR中的栈)
- (6)再次判断当前地址以决定是否重定位
- (7)uboot 的重定位
- (8)引入MMU
-
- (a)、物理地址
-
(b)、虚拟地址
-
(c)、MMU单元的作用
-
(d)、地址映射的额外收益1:访问控制
-
(e)、地址映射的额外收益2:cache
-
(8)MMU 实现细节
-
- (a)、分析转化表
-
(9)第三次设置栈(DDR)
-
(10)清除 bss段
-
(11)
ldr pc, _start_armboot
-
-
start.s 引入
拿到一个项目,我们要 从哪里开始分析 这份代码呢?
1、u-boot.lds中找到start.S入口
(1)在C语言中整个项目的入口就是main函数(这是C语言规定的),所以譬如说一个有10000个.c文件的项目,第一个要分析的文件就是包含了main函数的那个文件 。
(2)在uboot中因为有汇编阶段参与 ,因此不能直接找main.c。 整个程序的入口取决于链接脚本中ENTRY声明的地方。ENTRY(_start) 因此_start符号所在的文件就是整个程序的起始文件,_start所在处的代码就是整个程序的起始代码。
一、start.S分析
1、不简单的头文件包含
#include <config.h>
#include <version.h>
#if defined(CONFIG_ENABLE_MMU)
#include <asm/proc/domain.h>
#endif
#include <regs.h>

(1)
#include <config.h>
config.h 是在include目录下的,这个文件不是源码中本身存在的文件,而是配置过程中自动生成的文件 。(详见mkconfig脚本)。
这个文件的内容其实是包含了一个头文件:#include <configs/x210_sd.h>
include/configs/x210_sd.h,这个文件是整个uboot移植时的配置文件。
这里面是好多宏 。因此这个头文件包含将include/configs/x210_sd.h文件和start.S文件关联了起来。因此之后在分析start.S文件时,主要要考虑的就是x210_sd.h文件。
(2)
#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的版本号 ,那个版本号信息就是从这来的 。

(3)
#include <asm/proc/domain.h>
asm --> asm-arm
proc --> proc-armv
asm目录不是uboot中的原生目录,uboot中本来是没有这个目录的。asm目录是配置时创建的一个符号链接 ,实际指向的是就是asm-arm (详解上一章节分析mkconfig脚本时).
实际文件 是:include/asm-arm/proc-armv/domain.h
从这里可以看出之前配置时创建的符号链接的作用 ,如果没有这些符号链接则编译时根本通不过,因为找不到头文件。(所以uboot不能在windows的共享文件夹下配置编译,因为windows中没有符号链接 )
思考 :为什么start.S不直接包含 asm-arm/proc-armv/domain.h,而要用asm/proc/domain.h。
这样的设计主要是为了可移植性。因为如果直接包含,则start.S文件和CPU架构(和硬件)有关了,可移植性就差了。譬如我要把uboot移植到mips架构下,则start.S源代码中所有的头文件包含全部要修改。我们用了符号链接之后,则start.S中源代码不用改,只需要在具体的硬件移植时配置不同,创建的符号链接指向的不同,则可以具有可移植性。
2、16字节的校验头

(1)裸机中讲过,在SD卡启动 或者Nand启动 等整个镜像开头需要16字节的校验头 。
(mkv210image.c 中就是为了计算这个校验头)。我们以前做裸机程序时根本没考虑这16字节校验头。
因为:
1、如果我们是usb启动直接下载 的方式启动的则不需要16字节校验头 ( 具体可以查看 irom application note );
2、如果是SD卡启动 ,mkv210image.c ,中会给原镜像前加16字节的校验头。
(2)uboot这里start.S中在开头位置放了16字节的填充占位 ,这个占位的16字节只是保证正式的image的头部确实有16字节,但是这16字节的内容是不对的 ,还是需要后面去计算校验和然后重新填充的。
在 uboot 当中也有一个专门计算的文件 。

3、异常向量表(一级中断表)

(1)异常向量表是硬件决定的,软件只是参照硬件的设计来实现它。
(2)异常向量表中每种异常都应该被处理,否则真遇到了这种异常就跑飞了。但是我们在uboot中并未非常细致的处理各种异常 。
因为 uboot 的主要目的是为了启动内核 ,所以它的异常处理也不需要很详细。
(3)复位异常处的代码是:b reset,因此在CPU复位后真正去执行的有效代码是reset处的代码,因此 reset 符号处才是真正的有意义 的代码开始的地方。
4、 deadbeef

(1).balignl 16,0xdeadbeef. 这一句指令是让 当前地址对齐排布,如果当前地址不对齐则自动向后走地址直到对齐,并且向后走的那些内存要用 0xdeadbeef 来填充。
(2)0xdeadbeef 这是一个十六进制的数字 ,这个数字很有意思,组成这个数字的十六进制数全是abcdef之中的字母,而且这8个字母刚好组成了英文的dead beef这两个单词 ,字面意思是坏牛肉 ,只是用来标记填充的,并没有特殊意义 。
(3)为什么要对齐访问?
有时候是效率的要求 ,有时候是硬件的特殊要求 。
5、TEXT_BASE

(1)第100行这个TEXT_BASE就是上个课程中分析Makefile时讲到的那个配置阶段的TEXT_BASE,其实就是我们链接时指定的uboot的链接地址 。(值就是c3e00000)
(2) TEXT_BASE :这个变量在源代码当中并没有定义 ,而是在 makefile 当中被定义
源代码中和配置Makefile中很多变量是可以互相运送的 。简单来说有些符号的值可以从Makefile中传递到源代码中。
补充:下面有一个 物理地址 :
(1)CFG_PHY_UBOOT_BASE: 33e00000 uboot在DDR中的物理地址


6、真正的起始代码:reset
(1)设置 cpsr_c 位



总结:
(1)禁止FIQ IRQ ,ARM状态 ,SVC模式 。
为什么要设为 svc 模式,详情参考此处。
用户usr模式 :
虽然从理论上来说,可以设置CPU为用户usr模式,但是由于此模式无法直接访问很多的硬件资源 ,而uboot初始化,就必须要去访问这类资源 ,所以此处可以排除,不能设置为用户usr模式。
系统sys模式 vs 管理svc模式 :
首先,sys模式和usr模式相比,所用的寄存器组,都是一样的,但是增加了一些访问一些在usr模式下不能访问的资源。
而svc模式本身就属于特权模式,本身就可以访问那些受控资源,而且,比sys模式还多了些自己模式下的影子寄存器 ,所以,相对sys模式来说,可以访问资源的能力相同,但是拥有更多的硬件资源。
(2)设置L2、L1cache和MMU


(3)识别并暂存启动介质选择
(1)从哪里启动 是由SoC的 OM5:OM0 这6个引脚的高低电平决定的。
(2)实际上在210内部有一个寄存器 (地址是0xE0000004),这个寄存器中的值是硬件根据OM引脚的设置而自动设置值的。这个值反映的就是OM引脚的接法(电平高低) ,也就是真正的启动介质是谁。
(3)我们代码中可以通过读取这个寄存器的值然后判断其值来确定当前选中的启动介质 是Nand还是SD还是其他的。

注意:这个寄存器,我们拿到的手册当中,并没有明确给出,被三星官方给隐藏了。
(4)start.S的225-227行执行完后,在r2寄存器中存储了一个数字 ,这个数字等于某个特定值时就表示SD启动,等于另一个特定值时表示从Nand启动····
(5)260行中给r3中赋值#BOOT_MMCSD(0x03),这个在SD启动时实际会被执行,因此执行完这一段代码后r3中存储了0x03,以后备用。

(4)设置栈(SRAM中的栈)并调用lowlevel_init

(1)284-286行第一次设置栈。这次设置栈是在 SRAM 中设置的,因为当前整个代码还在SRAM中运行 ,此时DDR还未被初始化还不能用 。栈地址 0xd0036000 是自己指定 的,指定的原则就是这块空间只给栈用,不会被别人占用。
(2)在调用函数前初始化栈 ,主要原因是在被调用的函数内还有再次调用函数,而BL只会将返回地址存储到LR中,但是我们只有一个LR ,所以在第二层调用函数前要先将LR入栈,否则函数返回时第一层的返回地址就丢了。
(a)、检查复位状态
使用SourceInsight的Reference功能,找到 lowlevel_init 函数真正的地方,是在 uboot/board/samsumg/x210/lowlevel_init.S 中。

(1)复杂CPU允许多种复位情况。譬如直接冷上电 、热启动 、睡眠(低功耗)状态下的唤醒 等,这些情况都属于复位 。所以我们在复位代码中要去检测复位状态,来判断到底是哪种情况。
(2)判断哪种复位的意义在于:冷上电时DDR是需要初始化才能用的 ;而热启动或者低功耗状态下的复位则不需要再次初始化DDR 。
下面代码有一些和主线任务,没有关系。

IO状态恢复
关看门狗
一些SRAM SROM相关GPIO设置
供电锁存

总结:在前100行,lowlevel_init.S 中并没有做太多有意义的事情 (除了关看门狗、供电锁存外),然后下面从110行才开始进行有意义的操作。
(b)、判断当前代码在哪里运行(SRAM还是DDR)
sd卡启动,uboot 的工作原理:
1、将整个 uboot .bin 烧录到 sd 卡当中。
2、上电以后,硬件会自动将 sd 卡当中前 16kb,或者 8kb 的代码拷贝到 内部的 SRAM 当中。
3、然后从 SRAM 当中执行代码
4、在这前 16kb 的代码当中,我们会进行 DDR 的初始化,然后将 sd 卡当中整个 uboot.bin 拷贝到 DDR 当中运行。
总结:
第一部分:前 16kb 放到 SRAM 当中 (BL1)
第二部分:整个 uboot.bin 放到 DDR 当中
为什么要进行判断 ?
原因 1:BL1(uboot的前一部分)在SRAM中有一份,在DDR中也有一份,因此如果是冷启动 那么当前代码应该是在SRAM 中运行的BL1,如果是低功耗状态的复位 这时候应该就是在DDR 中运行的。
原因 2:我们判定当前运行代码的地址是有用的,可以指导后面代码的运行 。譬如在lowlevel_init.S中判定当前代码的运行地址,就是为了确定要不要执行时钟初始化和初始化DDR的代码 。如果当前代码是在SRAM中,说明冷启动,那么时钟和DDR都需要初始化;如果当前代码是在DDR中,那么说明是热启动则时钟和DDR都不用再次初始化 。
裸机时候的判断方法 :

uboot 的判断方法 :

(d)、system_clock_init
(1)使用SI搜索功能,确定这个函数就在当前文件的205 行,一直到第 385 行。这个初始化时钟的过程和裸机中初始化的过程一样的,只是更加完整而且是用汇编代码写的。
(2)在 x210_sd.h 中 300 行到 428 行,都是和时钟相关的配置值。这些宏定义就决定了210的时钟配置是多少 。也就是说代码在lowlevel_init.S中都写好了,但是代码的设置值都被宏定义在x210_sd.h中了。因此,如果移植时需要更改CPU的时钟设置,根本不需要动代码,只需要在x210_sd.h中更改配置值即可 。
(e)、mem_ctrl_asm_init
(1)该函数用来初始化DDR
(2)函数位置在 uboot/cpu/s5pc11x/s5pc110/cpu_init.S 文件中。
(3)配置值中其他配置值参考裸机中的解释即可明白,有一个和裸机中讲的不一样。DMC0_MEMCONFIG_0,在裸机中 配置值为0x20E01323;在uboot中 配置为 0x30F01313 .这个配置不同就导致结果不同。
在 裸机中DMC0的256MB内存地址范围是
0x20000000-0x2FFFFFFF;
在uboot中DMC0的256MB内存地址范围为0x30000000-0x3FFFFFFF。
(5)之前在裸机中时配置为2开头的地址,当时并没有说可以配置为3开头。从分析九鼎移植的uboot可以看出:DMC0上允许的地址范围是20000000-3FFFFFFF(一共是512MB),而我们实际只接了256MB物理内存,SoC允许我们给这256MB挑选地址范围。
(6)总结一下:在uboot中,可用的物理地址范围为:0x30000000-0x4FFFFFFF。一共512MB,其中30000000-3FFFFFFF为DMC0,40000000-4FFFFFFF为DMC1。
(7)我们需要的内存配置值在 x210_sd.h 的 438 行到 468 行之间。分析的时候要注意条件编译的条件,配置头文件中考虑了不同时钟配置下的内存配置值 ,这个的主要目的是让不同时钟需求的客户都能找到合适自己的内存配置值 。
(8)在uboot中DMC0和DMC1都工作了,所以在裸机中只要把uboot中的配置值和配置代码全部移植过去,应该是能够让DMC0和DMC1都工作的。
(f)、uart_asm_init
(1)这个函数用来初始化串口
(2)初始化完了后通过串口发送了一个’O’
tzpc_init :没有使用过,不管。
(g)、pop {pc}以返回
(1)返回前通过串口打印 ’K’
分析 ;lowlevel_init.S执行完如果没错那么就会串口打印出"OK"字样。这应该是我们uboot中看到的最早的输出信息 。
总结回顾 :lowlevel_init.S 中总共做了哪些事情:
检查复位状态、IO恢复、关看门狗、开发板供电锁存、时钟初始化、DDR初始化、串口初始化并打印’O’、tzpc初始化、打印’K’。
其中值得关注的 :关看门狗、开发板供电锁存、时钟初始化、DDR初始化、打印"OK"。
(5)再次设置栈(DDR中的栈)


(3)为什么要再次设置栈?
DDR已经初始化了,已经有大片内存可以用了 ,没必要再把栈放在SRAM中可怜兮兮的了;原来SRAM中内存大小空间有限,栈放在那里要注意不能使用过多的栈否则栈会溢出,我们及时将栈迁移到DDR中也是为了尽可能避免栈使用时候的小心翼翼。
(6)再次判断当前地址以决定是否重定位

此时uboot的第一阶段已经即将结束了 (第一阶段该做的事基本做完了),结束之前要把第二部分加载到DDR中链接地址处(33e00000),这个加载过程就叫重定位。
(7)uboot 的重定位


(1) D0037488 这个内存地址在SRAM中 ,这个地址中的值是被硬件自动设置的。硬件根据我们实际电路中SD卡在哪个通道中,会将这个地址中的值设置为相应的数字。
譬如我们从SD0通道启动时 ,这个值为EB000000;从SD2通道启动时,这个值为EB200000
(2)我们在start.S的260行确定了从MMCSD启动,然后又在278行将#BOOT_MMCSD写入了INF_REG3寄存器中存储着。

然后又在322行读出来,再和#BOOT_MMCSD去比较,确定是从MMCSD启动。最终跳转到 mmcsd_boot 函数中去执行重定位动作。


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


else if (ch == 0xEB200000) {
ret = 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).
(8)引入MMU
(a)、物理地址
物理地址就是物理设备设计生产时赋予的地址。
像裸机中使用的 寄存器的地址 就是CPU设计时指定的,这个就是物理地址。物理地址是硬件编码的,是设计生产时确定好的,一旦确定了就不能改了 。
一个事实就是:寄存器的物理地址是无法通过编程修改的,是多少就是多少,只能通过查询数据手册获得并操作 。坏处就是不够灵活 。一个解决方案就是使用虚拟地址。
(b)、虚拟地址

虚拟地址意思就是在我们软件操作和硬件被操作之间增加一个层次,叫做虚拟地址映射层 。
有了虚拟地址映射后,软件操作只需要给虚拟地址 ,硬件操作还是用原来的物理地址,映射层建立一个虚拟地址到物理地址的映射表。
当我们软件运行的时候,软件中使用的虚拟地址在映射表中查询得到对应的物理地址再发给硬件去执行 (虚拟地址到物理地址的映射是不可能通过软件来实现的)。
(c)、MMU单元的作用
(1)MMU就是(memory management unit,内存管理单元)。MMU实际上是SOC中一个硬件单元 ,它的主要功能就是 实现虚拟地址到物理地址的映射
(2)MMU单片在 CP15协处理器 中进行控制,也就是说要操控MMU进行虚拟地址映射,方法就是 对cp15协处理器的寄存器进行编程。
(d)、地址映射的额外收益1:访问控制
(1)访问控制就是:在管理上对内存进行分块 ,然后每块进行独立的虚拟地址映射 ,然后在每一块的映射关系中同时还实现了访问控制(对该块可读、可写、只读、只写、不可访问等控制)
(2)回想在C语言中编程中经常会出现一个错误:Segmentation fault。实际上这个段错误就和MMU实现的访问控制有关。
当前程序只能操作自己有权操作的地址范围 (若干个内存块),如果当前程序指针出错访问了不该访问的内存块则就会触发段错误。
(e)、地址映射的额外收益2:cache
(1)cache的工作和虚拟地址映射有关系。
(2)cache是快速缓存,意思就是比CPU慢但是比DDR块。CPU嫌DDR太慢了,于是乎把一些DDR中常用的内容事先读取缓存在cache中,然后CPU每次需要找东西时先在cache中找 。如果cache中有就直接用cache中的;如果cache中没有才会去DDR中寻找 。
(8)MMU 实现细节
cp15协处理器内部有 c0到c15 共16个寄存器 ,这些寄存器每一个都有自己的作用。我们通过 mrc和mcr指令 来访问这些寄存器。所谓的操作cp协处理器其实就是操作cp15的这些寄存器。

- 1、使能域访问(cp15的c3寄存器)

c3寄存器在mmu中的作用是控制域访问 。域访问是和MMU的访问控制有关的。
- 2、设置TTB(cp15的c2寄存器)

(1)TTB就是(translation table base ),转换表基地址。首先要明白什么是TT(translation table转换表),TTB其实就是转换表的基地址 。
(2)转换表是建立一套虚拟地址映射的关键。转换表分2部分,表索引和表项。表索引对应虚拟地址,表项对应物理地址 。
一对表索引和表项构成一个转换表单元,能够对一个内存块进行虚拟地址转换。(映射中基本规定中规定了内存映射和管理是以块为单位的,至于块有多大,要看你的MMU的支持和你自己的选择。在ARM中支持3种块大小,细表1KB、粗表4KB、段1MB)。
真正的转换表就是由若干个转换表单元构成 的,每个单元负责1个内存块 ,总体的转换表负责整个内存空间(0-4G)的映射。
(3)整个建立虚拟地址映射的主要工作就是 建立这张转换表。
(4)转换表放置在内存中的,放置时要求起始地址在内存中要xx位对齐。转换表不需要软件去干涉使用,而是将基地址TTB设置到cp15的c2寄存器中,然后MMU工作时会自动去查转换表。
- 使能MMU单元(cp15的c1寄存器)

cp15的c1寄存器的 bit0 控制MMU的开关 。只要将这一个bit置1即可开启MMU 。开启MMU之后上层软件层的地址就必须经过TT的转换才能发给下层物理层去执行。
(a)、分析转化表
(1)通过符号查找,确定转换表在 lowlevel_init.S 文件的593行。
(2)宏观上 理解转换表:整个转换表可以看作是一个int类型的数组 ,数组中的一个元素就是一个表索引和表项的单元 。数组中的元素值就是表项,这个元素的数组下标就是表索引。
(3)ARM的段式映射中长度为1MB ,因此一个映射单元只能管1MB内存,那我们整个4G范围内需要 4G/1MB=4096 个映射单元,也就是说这个数组的元素个数是4096.实际上我们做的时候并没有依次单个处理这4096个单元,而是把4096个分成几部分,然后每部分用for循环做相同的处理。

FL_SECTION_ENTRY 宏

总结 :

(9)第三次设置栈(DDR)


(1)第三次设置栈。这次设置栈还是在DDR中,之前虽然已经在DDR中设置过一次栈了,但是本次设置栈的目的是将栈放在比较合适 (安全,紧凑而不浪费内存)的地方。
(2)我们实际将栈设置在uboot起始地址上方2MB处 ,这样安全的栈空间是:2MB-uboot大小-0x1000=1.8MB左右。这个空间既没有太浪费内存,又足够安全。
(10)清除 bss段
清理bss段代码和裸机中讲的一样。注意表示bss段的开头和结尾地址的符号是从链接脚本u-boot.lds得来的。

(11)ldr pc, _start_armboot

(1)start_armboot是 uboot/lib_arm/board.c 中,这是一个C语言实现的函数。这个函数就是uboot的第二阶段 。这句代码的作用就是将uboot第二阶段执行的函数的地址传给pc,实际上就是使用一个远跳转直接跳转到DDR中的第二阶段开始地址处 。
(2)远跳转的含义就是这句话加载的地址和当前运行地址无关,而和链接地址有关 。因此这个远跳转可以实现从SRAM中的第一阶段跳转到DDR中的第二阶段 。
(3)这里这个远跳转就是uboot第一阶段和第二阶段的分界线 。
4、总结:uboot的第一阶段做了哪些重要工作
(1)构建异常向量表
(2)设置CPU为SVC模式
(3)关看门狗
(4)开发板供电置锁
(5)时钟初始化
(6)DDR初始化
(7)串口初始化并打印"OK"
(8)重定位
(9)建立映射表并开启MMU
(10)跳转到第二阶段
