整体简介
本笔记为记录嵌入式Linux的uboot部分基础知识,结合源码对uboot的实现原理和应用展开学习
授课老师:朱有鹏
记录少女:宕机酱=v=
2017.03
本part2主要进行uboot的源码分析和组成原理学习。
先分析,再移植。本part暂不涉及移植过程。笔记的后面一部分会引入一部分驱动的概念
笔记正文
(接part1)
5.1.2、SourceInsight中如何找到文件
首先要找到代码的起始:ENTRY(_start)
汇编中ENTRY里面的符号,即是程序的起始代码符号。找这个_start用SI搜索。
点菜单栏那个蓝 {MOD}的“R”按钮,reference。
uboot第一阶段
1.硬件设备初始化
(1)设置异常向量
(2)CPU进入SVC模式(管理模式)
(3)设置控制寄存器地址
(4)关闭看门狗(防止无限重启)
(5)屏蔽中断
(6)设置MPLLCON,UPLLCON, CLKDIVN
(7)关闭MMU,cache ------(也就是做bank的设置)
上电的时候MMU必须关闭,指令cache可关闭,可不关闭,但数据cache一定要关闭
否则可能导致刚开始的代码里面,去取数据的时候,从cache里面取,而这时候RAM中数据 还没有过来,导致数据预取异常
_TEXT_BASE:
.word TEXT_BASE
(8)初始化RAM控制寄存器
(9)复制U-Boot第二阶段代码到RAM
(10)设置堆栈
(11)清除BSS段
初始值为0,无初始值的全局变量,静态变量将自动被放在BSS段。应该将这些变量的初始 值赋为0,否则这些变量的初始值将是一个随机的值,若有些程序直接使用这些没有初始化 的变量将引起未知的后果
(12)跳转到第二阶段代码入口
ldr pc, _start_armboot
问题:如果换一块开发板可能应该改哪里?
答:cpu设置 bank设置 时钟 拷贝地址
word伪操作用于分配一段字内存单元(分配的单元都是字对齐的),并用伪操作中的expr初始化
. word后面的数:表示把该标识的编译地址写入当前地址,标识是不占用任何指令的。把标识存放的数值copy到指针pc上面,那么标识上存放的值是什么?
是由.word undefined_instruction来指定的,pc就代表你运行代码的地址,她就实现了CPU要做一次跳转时的工作。
屏蔽所有中断,为什么要关中断?
中断处理中ldr pc是将代码的编译地址放在了指针上,而这段时间还没有搬移代码,所以编译地址上面没有这个代码,如果进行跳转就会跳转到空指针上面
5.2 头文件包含
#include
这个config.h就是 include/config.h makefile 100行左右生成的,里面包含了一个x210_sd.h
这个x210_sd.h太重要。配置和代码就在这里结合起来的。
实际这个#include
#ifndef CONFIG_ENABLE_MMU //如果宏定义了MMU
#ifndef CFG_PHY_UBOOT_BASE
#define CFG_PHY_UBOOT_BASE CFG_UBOOT_BASE
#endif
#endif
#include
这里的asm,uboot是没有这个原生目录的
这是我们之前创建的符号链接,指向asm-arm目录
proc也是指向的proc-armv文件
实际上domain.h的路径是…/asm-arm/proc-armv/domain.h
如果没有符号链接,我们在编译的时候根本通不过,因为找不到头文件。
为什么不在windows下开发?因为win不支持符号链接。
【使用符号链接好在哪了?】我们移植时无需再修改start.s,只需配置不同,从而符号链接指向不同即可。
5.3.start.S解析2
5.3.1、启动代码的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字节的内容是不对的,还是需要后面去计算校验和然后重新填充的。
【校验头——准备工作】
.word 0x2000
.word 0x0
.word 0x0
.word 0x0
.word 在汇编中的用法和C语言中的int用法基本相同。这里定义了4个变量是无意义的,后面进行计算校验和然后重新填充。
【构建中断向量表:实际工作是从此开始的】
.globl _start
_start: b reset
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq
ldr pc, _fiq
参照硬件的设计来实现它。当发生了哪个异常时,跳转到哪个地方去执行。
注意:异常向量表中每种异常都应该被处理,否则真遇到了这种异常就跑飞了。但是我们在uboot中并未非常细致的处理各种异常。
【deadbeef】无意义,取个名字其实和0x0000 0000没有区别。
.balignl 16,0xdeadbeef. //填充规则:16字节对齐,未对齐则使用0xdeadbeef填充
//对齐访问的意义:有时效率的要求,有时硬件的要求。
【TEXT_BASE】
TEXT_BASE就是上个课程中分析Makefile时讲到的那个配置阶段的TEXT_BASE
(值就是c3e00000)
你在工程中找不到 TEXT_BASE 的定义,因为这个值在配置阶段得到。
可能是在Makefile脚本中拿到的这个值。要注意:这些符号的值是可以在汇编和Makefile中互相传递的。
5.4.start.S解析3
【设置CPU为SVC模式】
(1)msr cpsr_c, #0xd3 将CPU设置为禁止FIQ IRQ,ARM状态,SVC模式。(特权模式)
(2)其实ARM CPU在复位时默认就会进入SVC模式,但是这里还是使用软件将其置为SVC模式。
这是因为妥善起见做的保护措施。
【刷新L2、L1cache、关闭MMU】
这里的L2、L1cache和2440的操作处理方式不太一样。2440禁用了cache,210做刷新处理
bl disable_l2cache
bl set_l2cache_auxctrl_cycle
bl enable_l2cache
(4)刷新L1 cache的icache和dcache。
//icache是指令cache,dcache是数据cache
(5)关闭MMU
总结:上面这5步都是和CPU的cache和mmu有关的,不用去细看,大概知道即可。
【识别并暂存启动介质选择:OMpin】
我们代码中可以通过读取这个寄存器(地址是0xE0000004)的值然后判断其值来确定当前选中的启动介质是Nand还是SD/MMC还是其他的。这个值存在r2中进行对比。
260行中给r3中赋值#BOOT_MMCSD(0x03),这个在SD启动时实际会被执行,因此执行完这一段代码后r3中存储了0x03,以后备用。
cmp r2, #0x0 @ 512B 4-cycle
moveq r3, #BOOT_NAND
cmp r2, #0x2 @ 2KB 5-cycle
moveq r3, #BOOT_NAND
【在sram第一次设置栈 并 调用lowlevel_init】
因为当前整个代码还在SRAM中运行,此时DDR还未被初始化还不能用。栈地址0xd0036000是自己指定的,指定的原则就是这块空间只给栈用,不会被别人占用。
ldr sp, =0xd0036000 /* end of sram dedicated to u-boot */
sub sp, sp, #12 /* set stack */
mov fp, #0
5.5
在调用函数前初始化栈,主要原因是在被调用的函数内还有再次调用函数,而BL只会将返回地址存储到LR中,但是我们只有一个LR,所以在第二层调用函数前要先将LR入栈,否则函数返回时第一层的返回地址就丢了。
bl lowlevel_init
lowlevel_init是重要的,未来我们初始化一些cpu相关的操作就和此函数有关联。
上一节课讲 的设置栈在lowlevel_init函数初始:
push {lr}
//为什么用这个?
因为在lowlevel_init中,后面我们还调用了其他函数。一旦调用其他函数我们的pc,lr就被占用了。为了防止在这个“递归调用”的 过程中还可以正常返回,我们要将lr压栈,后面返回时pop pc
检查复位状态
(1)复杂CPU允许多种复位情况。譬如直接冷上电、热启动、睡眠(低功耗)状态下的唤醒等,这些情况都属于复位。所以我们在复位代码中要去检测复位状态,来判断到底是哪种情况。
(2)判断哪种复位的意义在于:冷上电时DDR是需要初始化才能用的;而热启动或者低功耗状态下的复位则不需要再次初始化DDR
。
关看门狗
(1)参考裸机中看门狗章节
一些SRAM SROM相关GPIO设置
PS_HOLD
(1)与主线启动代码无关,不用管
供电锁存
(1)lowlevel_init.S的第100-104行,开发板供电锁存。
总结:在前100行,lowlevel_init.S中并没有做太多有意义的事情(除了关看门狗、供电锁存外),然后下面从110行才开始进行有意义的操作。
5.6 时钟和DDR的初始化(极重要)
ldr r0, =0xff000fff
bic r1, pc, r0
ldr r2, _TEXT_BASE
bic r2, r2, r0
cmp r1, r2
beq 1f
//时钟初始化
bl system_clock_init
//DDR初始化
bl mem_ctrl_asm_init
//如果当前代码是在SRAM中,则进行时钟和DDR的初始化,。
//如果当前代码是在DDR中,说明已经启动过了,跳过sdram init
【lowlevel_init 110-115】判断当前代码在SRAM中还是DDR中
为什么要做这个判定? 因为有可能冷启动,也有可能是热启动。
回想一下210 的启动过程:iROM执行BL0,然后在外部介质中载入BL1到SRAM(前16KB),初始化DDR,最后加载BL2到DDR中运行。
BL1(uboot的前一部分)是在SRAM中有一份,在DDR中也有一份。
uboot在设计的时候也是,在SRAM中有前8K,在DDR中有剩下的一部分。
//因此如果是冷启动,那么当前代码应该是在SRAM中运行的。如果是低功耗复位,那么应该是在DDR中运行的
【beq 1f】
1f不是一个数,这里的1 是下面一组代码的标号。
1f 实际代表1 forward,往下找
1b 实际代表1 backward,往前找。
这个判断SRAM / DDR的思路类似裸机中重定位那章节的内容
adr r0 , _start
ldr r1 ,= start
链接地址 = 运行地址?如果是或不是则证明现在是哪里运行。
【再看我们的uboot中代码】
bic r1, pc, r0 //将pc中的某些bit位清零
ldr r2, _TEXT_BASE //TEXT_BASE经配置得 C340 0000,是链接地址
cmp r1, r2
{
拿出当前位地址,某些bit位清零
拿出链接地址,某些bit位清零
}
为什么这样?因为pc和链接地址即使都在DDR中,两者也不可能相等。因为一个是链接地址开头,但pc来说程序都运行了一段了。我们就比较前,将他们的开头和末尾几位都拿掉。和裸机有点不一样。
裸机以前就是比开头,若在SRAM中那么链接地址和运行地址是相同的
【system_clock_init】
(1)使用SI搜索功能,确定这个函数就在当前文件的205行,一直到第385行。这个初始化时钟的过程和裸机中初始化的过程一样的,只是更加完整而且是用汇编代码写的。
(2)在x210_sd.h中300行到428行,都是和时钟相关的配置值。这些宏定义就决定了210的时钟配置是多少。也就是说代码在lowlevel_init.S中都写好了,但是代码的设置值都被宏定义在x210_sd.h中了。因此,如果移植时需要更改CPU的时钟设置,根本不需要动代码,只需要在x210_sd.h中更改配置值即可。
5.7
【mem_ctrl_asm_init】该函数用来初始化DDR
该函数和裸机中初始化DDR代码是一样的。实际上裸机中初始化DDR的代码就是从这里抄的。配置值也可以从这里抄,但是当时我自己根据理解+抄袭整出来的一份。
配置值中其他配置值参考裸机中的解释即可明白,有一个和裸机中讲的不一样。DMC0_MEMCONFIG_0,在裸机中配置值为0x20E01323;在uboot中配置为0x30F01313.这个配置不同就导致结果不同。
在 裸机中DMC0的256MB内存地址范围是0x20000000-0x2FFFFFFF;
在uboot中DMC0的256MB内存地址范围为0x30000000-0x3FFFFFFF。
我们实际只接了256MB物理内存,SoC允许我们给这256MB挑选地址范围。
总结一下:在uboot中,可用的物理地址范围为:0x30000000-0x4FFFFFFF。一共512MB,其中 30000000-3FFFFFFF为DMC0, //256MB
40000000-4FFFFFFF为DMC1。 //256MB
【插入一点小知识:x210_sd.h中对时钟的配置宏定义】
#define CONFIG_CLK_1000_200_166_133
可以看出没有屏蔽的是我们用的时钟配置。也是推荐配置,其他是降频的配置。
名称格式按照
CONFIG_CLK主频(MSYS)_DSYS_xxxxx设置。
复习:
MSYS 高频master
DSYS 中频。视频编解码
PSYS 低频。外设时钟
注意:现在有一个问题!如果我们选用不同的时钟,那么我们的 内存也要进行不同的配置。
在x210_sd.h中,三星已经帮我们做好了这样做的准备。根据不同的时钟的选择给我们配置好了DMC的时钟。我们只需要选择 时钟的宏 即可。
【DDR初始化函数返回之后 (注意这部分开始热启动也要进行该函数跳转)】
uart_asm_init
(1)这个函数用来初始化串口
(2)初始化完了后通过串口发送了一个’O’
tzpc_init
(1)trust zone初始化,没搞过,不管
pop {pc}以返回
//要返回到start.S了
(1)返回前通过串口打印’K’
分析;lowlevel_init.S执行完如果没错那么就会串口打印出”OK”字样。这应该是我们uboot中看到的最早的输出信息。
5.8.start.S解析7
总结回顾:lowlevel_init.S中总共做了哪些事情:
检查复位状态、IO恢复、关看门狗、开发板供电锁存、时钟初始化、DDR初始化、串口初始化并打印’O’、tzpc初始化、打印’K’。
其中值得关注的:关看门狗、开发板供电锁存、时钟初始化、DDR初始化、打印”OK”
2.5.8.1、再次设置栈(DDR中的栈)
(1)再次开发板供电锁存。第一,做2次是不会错的;第二,做2次则第2次无意义;做代码移植时有一个古怪谨慎保守策略就是尽量添加代码而不要删除代码。
(2)之前在调用lowlevel_init程序前设置过1次栈(start.S 284-287行),那时候因为DDR尚未初始化,因此程序执行都是在SRAM中,所以在SRAM中分配了一部分内存作为栈。本次因为DDR已经被初始化了,因此要把栈挪移到DDR中,所以要重新设置栈,这是第二次(start.S 297-299行);这里实际设置的栈的地址是33E00000,刚好在uboot的代码段的下面紧挨着。
注意:后面还会有第三次设置栈。
会不会担心冲掉呢?不会
如下图内存
uboot代码段
——————–sp
xx
xx…
由于ARM中设计使用满减栈,自sp之后会往下走,不会将uboot代码段冲掉。
(3)为什么要再次设置栈?DDR已经初始化了,已经有大片内存可以用了,没必要再把栈放在SRAM中可怜兮兮的了;原来SRAM中内存大小空间有限,栈放在那里要注意不能使用过多的栈否则栈会溢出,我们及时将栈迁移到DDR中也是为了尽可能避免栈使用时候的小心翼翼。
感慨:uboot的启动阶段主要技巧就在于小范围内有限条件下的辗转腾挪。
5.8.2、再次判断当前地址以决定是否重定位
(1)再次用相同的代码判断运行地址是在SRAM中还是DDR中,不过本次判断的目的不同(上次判断是为了决定是否要执行初始化时钟和DDR的代码)这次判断是为了决定是否进行uboot的relocate。
(2)冷启动时当前情况是uboot的前一部分(16kb或者8kb)开机自动从SD卡加载到SRAM中正在运行,uboot的第二部分(其实第二部分是整个uboot)还躺在SD卡的某个扇区开头的N个扇区中。
此时uboot的第一阶段已经即将结束了(第一阶段该做的事基本做完了),结束之前要把第二部分加载到DDR中链接地址处(33e00000),这个加载过程就叫重定位。
5.9 Uboot的重定位
(1)D0037488这个内存地址在SRAM中,这个地址中的值是被硬件自动设置的。硬件根据我们实际电路中SD卡在哪个通道中,会将这个地址中的值设置为相应的数字。譬如我们从SD0通道启动时,这个值为EB000000;从SD2通道启动时,这个值为EB200000
总结:D0037488这个地址要么是EB000000,要么是EB200000,由SD通道决定。
D0037488在iROM_appli中有声明,是一个环境变量,硬件自动维护的。
ldr r0, =0xff000fff
bic r1, pc, r0
ldr r2, _TEXT_BASE
bic r2, r2, r0
cmp r1, r2
beq after_copy
#if defined(CONFIG_EVT1)
,//比较0xD0037488中的值是不是 //0xEB200000
ldr r0, =0xD0037488
ldr r1, [r0]
ldr r2, =0xEB200000
cmp r1, r2 //如果相等,我们就是从SD通道2启动的
//
beq mmcsd_boot //执行mmcsd_boot
#endif
【start.S的260行:确定启动方式】
我们在start.S的260行确定了从MMCSD启动,然后又在278行将#BOOT_MMCSD写入了INF_REG3寄存器中存储着。
260 cmp r2, #0xc
moveq r3, #BOOT_MMCSD
.....
278 ldr r0, =INF_REG_BASE
str r3, [r0, #INF_REG3_OFFSET]
.....
328 ldr r0, =INF_REG_BASE
ldr r1, [r0, #INF_REG3_OFFSET]
...
cmp r1, #BOOT_MMCSD
beq mmcsd_boot
348
mmcsd_boot:
bl movi_bl2_copy //真正重定位的 函数,是一个C语言函数
b after_copy
【跳转到 movi_bl2_copy】
typedef u32(*copy_sd_mmc_to_mem)
(u32 channel, u32 start_block, u16 block_size, u32 *trg, u32 init);
void movi_bl2_copy(void)
{
(......)
if (ch == 0xEB000000) {
ret = copy_bl2(0, MOVI_BL2_POS, MOVI_BL2_BLKCNT,
CFG_PHY_UBOOT_BASE, 0);
(......)
}
分析参数:
1 通道数 2
2 uboot第二部分开始的扇区(必须与烧录一致) MOVI_BL2_POS
3 uboot占用扇区数 MOVI_BL2_BLKCNT
4 重定位时将uboot第二部分复制到DDR中的起始地址 CFG_PHY_UBOOT_BASE 33e00000
5 无关紧要 0
注意:这里的SD bl2_copy是不是很眼熟?他就是我们学的裸机那部分一样的,只不过稍微麻烦一点
5.10 【开启MMU,建立虚拟地址映射准备工作】
总MMU设置流程:
1 设置域访问
2 设置TTB地址到cp15的c2,建立虚拟地址映射表 //以上两者为前置工作
3 使能MMU
过程细节如下:
enable_mmu:
ldr r5, =0x0000ffff 选用
mcr p15, 0, r5, c3, c0, 0 @load domain access 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)物理地址就是物理设备设计生产时赋予的地址。
像裸机中使用的寄存器的地址就是CPU设计时指定的,这个就是物理地址。物理地址是硬件编码的,是设计生产时确定好的,只能通过查询数据手册,一旦确定了就不能改了。
因为他不够灵活。解决方案就是使用虚拟地址。
(3)虚拟地址意思就是在我们软件操作和硬件被操作之间增加一个层次,叫做虚拟地址映射层。有了虚拟地址映射后,软件操作只需要给虚拟地址,硬件操作还是用原来的物理地址,映射层建立一个虚拟地址到物理地址的映射表。当我们软件运行的时候,软件中使用的虚拟地址在映射表中查询得到对应的物理地址再发给硬件去执行(虚拟地址到物理地址的映射是不可能通过软件来实现的)。
-----
虚拟地址映射表:
表索引 表项
1 xx
2 xx
总结:硬件只能使用虚拟地址,软件帮我们多做了查表这一步,查表之后得到对应的物理地址再发给硬件去执行。
【MMU单元的作用】
(1)MMU就是memory management unit,内存管理单元。MMU实际上是SOC中一个硬件单元,它的主要功能就是实现虚拟地址到物理地址的映射。
(2)MMU单片在CP15协处理器中进行控制,也就是说要操控MMU进行虚拟地址映射,方法就是对cp15协处理器的寄存器进行编程。
2.5.10.3、地址映射的额外收益1:访问控制
(1)访问控制就是:在管理上对内存进行分块,然后每块进行独立的虚拟地址映射,然后在每一块的映射关系中同时还实现了访问控制(对该块可读、可写、只读、只写、不可访问等控制)
(2)回想在C语言中编程中经常会出现一个错误:Segmentation fault。实际上这个段错误就和MMU实现的访问控制有关。当前程序只能操作自己有权操作的地址范围(若干个内存块),如果当前程序指针出错访问了不该访问的内存块则就会触发段错误。
2.5.10.4、地址映射的额外收益2:cache
(1)cache的工作和虚拟地址映射有关系。
(2)cache是快速缓存,意思就是比CPU慢但是比DDR块。CPU嫌DDR太慢了,于是乎把一些DDR中常用的内容事先读取缓存在cache中,然后CPU每次需要找东西时先在cache中找。如果cache中有就直接用cache中的;如果cache中没有才会去DDR中寻找。
注意:在Linux中使用了二级映射。这个部分不值得我们去专心研究是怎么做到的,我们只需要知道很麻烦就行了。
5.11.填写转换表基地址,设置MMU完成
5.11.1、使能域访问(cp15的c3寄存器)
(1)cp15协处理器内部有c0到c15共16个寄存器,这些寄存器每一个都有自己的作用。我们通过mrc和mcr指令来访问这些寄存器。所谓的操作cp协处理器其实就是操作cp15的这些寄存器。
(2)c3寄存器在mmu中的作用是 控制域控制位 。域访问是和MMU的访问控制有关的。
5.11.2、设置TTB(cp15的c2寄存器)
(1)TTB就是translation table base,转换表基地址。首先要明白什么是TT(translation table转换表),TTB其实就是转换表的基地址。
(2)转换表是建立一套虚拟地址映射的关键。转换表分2部分,表索引和表项。
表索引对应虚拟地址,表项对应物理地址。一对表索引和表项构成一个转换表单元,能够对一个内存块进行虚拟地址转换。
映射中基本规定中规定了内存映射和管理是以块为单位的,至于块有多大要自选。
在ARM中支持3种块大小,细表1KB、粗表4KB、段1MB
一个映射单元 就分为这三种大小
真正的转换表就是由若干个转换表单元构成的,每个单元负责1个内存块,总体的转换表负责整个内存空间(0-4G)的映射。
(3)整个建立虚拟地址映射的主要工作就是建立这张转换表
(4)转换表放置在内存中的,放置时要求起始地址在内存中要xx位对齐。转换表不需要软件去干涉使用,而是将基地址TTB设置到cp15的c2寄存器中,然后MMU工作时会自动去查转换表。
5.11.3、使能MMU单元(cp15的c1寄存器)
(1)cp15的c1寄存器的bit0控制MMU的开关。只要将这一个bit置1即可开启MMU。
开启MMU之后上层软件层的地址就必须经过TT的转换才能发给下层物理层去执行。
//看起来好像只设置了一个表格,似乎没有开启表。实际上就是定一个表格。
//通过符号查找,确定转换表在lowlevel_init.S文件的593行。
总结:设置MMU的方法主要工作就是把 转换表的基地址TTB,放入在cp15中的c2中。
【至此虚拟地址映射的前置准备工作完成了!】
5.12 转换表的建立
虚拟地址映射表:
表索引 表项
1 xx
2 xx
宏观上:软件生成的转换表很像一个数组,000000000 000000200,只有表项没有索引
其实,数组中的元素值就是表项,数组的下标就是索引。
建立这个数组就是建立转换表。mmu_table
一个ARM的段式映射长度为1MB,因此一个映射单元只能管1MB内存,那么我们整个4G范围内需要
4G/1M = 4096个映射单元,也就说这个数组的元素个数 应该是4096。
实际上我们并没有依次去处理这4096个单元,而是分成了几部分,每部分用循环做相同的处理。
.macro 定义宏
.word 构建数
.endm 结束定义
.rept 伪指令:重复执行 n 次
{
循环体
}
.endr 结束循环
.macro FL_SECTION_ENTRY base,ap,d,c,b
.word (ase << 20) | (ap << 10) |
(d << 5) | (1<<4) | (c << 3) | ( << 2) | (1<<1)
ase << 20
//这里的20位是一个M的单位,也即是一个段的概念。详细的设置需要去网上查资料。
// Access for iRAM
.rept 0x100
FL_SECTION_ENTRY __base,3,0,0,0
.set __base,__base+1
.endr
// 0 -1000 0000 映射到 0 -1000 0000 不变
// Not Allowed
.rept 0x200 - 0x100
.word 0x00000000
.endr
// 1000 0000 -2000 0000 映射到 0 失效
【虚拟地址映射总表】
VA PA length
0-10000000 0-10000000 256MB
10000000-20000000 0 256MB
20000000-60000000 20000000-60000000 1GB 512-1.5G
60000000-80000000 0 512MB 1.5G-2G
80000000-b0000000 80000000-b0000000 768MB 2G-2.75G
b0000000-c0000000 b0000000-c0000000 256MB 2.75G-3G
c0000000-d0000000 30000000-40000000 256MB 3G-3.25G
d-完 d-完 768MB 3.25G-4G
DRAM有效范围:
DMC0: 0x30000000-0x3FFFFFFF
DMC1: 0x40000000-0x4FFFFFFF
结论:虚拟地址映射只是把虚拟地址的c0000000开头的
256MB映射到了DMC0的30000000开头的256MB物理内存上去了。
其他的虚拟地址空间根本没动,还是原样映射的。
思考:为什么配置时将链接地址设置为c3e00000?因为这个地址将来会被映射到33e00000这个物理地址。
硬件绑定的物理地址是33e0 0000 ,我们链接地址设为0xc3e00000。设为33e0 0000其实也可以的
但是要注意:
MMU开启之后就不能使用物理地址了!我们这里可以用33e0 0000,是因为33e0 0000本身也是个虚拟地址
33e0 0000也是虚拟地址哦!只不过和c3e00000链接到了同一个物理地址33e0 0000
x210里面的uboot写的不好,因为这里面虚拟地址和物理地址很混乱。三星和九鼎做的都很不美好
很多是硬编码,很多东西没有经过配置,甚至都不能通过改宏定义去修改。
这个东西要比较资深的工程师才有能力去改,而且要改一点链接地址都要修改很多东西。
为什么要用虚拟地址?——不知道,这两年流行起来了,之前uboot是没有虚拟地址映射的。
5.13 MMU开启之后,使用虚拟地址
【第三次设置栈】
业界称为设置堆栈
stack_setup:
#if defined(CONFIG_MEMORY_UPPER_CODE)
ldr sp, =(CFG_UBOOT_BASE + CFG_UBOOT_SIZE - 0x1000)
之前虽然设置过DDR中的栈,这次要把它放在更合适、更安全、更紧凑的位置。
1 为了安全不被冲
2 更紧凑的内存,避免浪费
要把栈放在紧靠Uboot,避免中间浪费
我们是往uboot上数了2M。(uboot一般只有200k,所以不用担心)
--------------- sp
(arm满减栈:从此处往下存)
{
(2M - uboot的大小约200K 约 1.8M大小)
}
----------------uboot 结尾
----------------uboot 起始
【清bss】
clear_bss:
ldr r0, _bss_start
ldr r1, _bss_end
mov r2, #0x00000000 /* clear */
链接脚本中的
bss_start是__bss_start
bss_end 是__bss_end
我们这里的_bss_start和链接脚本的写法不一样,但是值是一样的,_bss_start赋值就是__bss_start
clbss_l:
str r2, [r0]
add r0, r0, #4
cmp r0, r1
ble clbss_l
【长跳转,从SRAM中跳转到DDR中第二阶段开头处】
ldr pc, _start_armboot
长跳转的含义是这句话加载的地址和 运行地址无关,而是和链接地址有关。
注意:本课程讲的uboot第一阶段都是在SRAM中进行的。DDR已经被我们初始化好了,这个长跳转就跳转到DDR中去运行了
+ ---------- -------- ---- +
2.5.13.4、总结:uboot的第一阶段做了哪些工作
(1)构建异常向量表
(2)设置CPU为SVC模式
(3)关看门狗
(4)开发板供电置锁
(5)时钟初始化
(6)DDR初始化
(7)串口初始化并打印”OK”
(8)重定位
(9)建立映射表并开启MMU
(10)跳转到第二阶段 ldr pc, =main
6.1 start_armboot函数:第二阶段
在uboot/lib_arm/board.c中,这个函数从444行开始,非常长,很少见这么长的函数。
这么长的函数不分开,主要原因是因为这个函数整体构成了启动的第二阶段。
uboot不认为有必要分开
宏观分析:第一阶段:初始化内部控件,初始化DDR并重定位
第二阶段:初始化剩下剩下的硬件,主要是外部硬件如iNand和网卡。以及uboot命令等
uboot命令等待命令,解析命令,执行命令loop
uboot第二阶段完结于何处?
打印信息后, 3,2,1,0倒计时然后执行bootcmd环境变量对应的启动命令。
【代码规范】
#ifdef CONFIG_MEMORY_UPPER_CODE /* by scsuh */
ulong gd_base;
gd_base = CFG_UBOOT_BASE + CFG_UBOOT_SIZE - CFG_MALLOC_LEN - CFG_STACK_SIZE - sizeof(gd_t);
#ifdef CONFIG_USE_IRQ
gd_base -= (CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ);
#endif
gd = (gd_t*)gd_base;
#else //CONFIG_MEMORY_UPPER_CODE【!】
gd = (gd_t*)(_armboot_start - CFG_MALLOC_LEN - sizeof(gd_t));
#endif //CONFIG_MEMORY_UPPER_CODE【!】
[!]这里的代码层次不明确,应该加规范的注释
CONFIG_MEMORY_UPPER_CODE
ulong gd_base;
这里的 gd是globa data,是一个很重要的全局变量(准确说是一个结构体地址)
这个结构体里面是整个uboot中常用的全局变量,所以经常被访问。
【DECLARE_GLOBAL_DATA_PTR】
我们发现很多很多文件中都有这句话,我们看一眼他的定义:
#define DECLARE_GLOBAL_DATA_PTR
register volatile gd_t *gd asm ("r8")
asm ("r8");
“DECLARE_GLOBAL_DATA_PTR”这一句代码很常见,是一个声明。声明的内容是使用gd这个结构体内的全局变量。它的作用和头文件比较像,头文件也能起这个作用。只不过头文件有些不方便,和路径有关,可能经不住版本变迁的考验。
综合定义:这是一个要放在r8寄存器的全局变量,类型为指针,用寄存器读写,可变。大小为4字节。
指向变量的类型为gd_t型。gd_t定义在/inculde/asm-arm/global_data.h中
{
board信息(全都是开发板的相关信息,即bd)
一个重复了的波特率
一个重复的环境变量地址
ip地址
机器码
启动参数
dram信息
网卡
标志位
波特率
控制台(have console)
重定位偏移量
环境变量相关地址
环境变量可用与否
fb(frame buffer 基地址)
}
6.3 gd和bd的内存排布
DECLARE_GLOBAL_DATA_PTR 仅仅只定义了一个指针,占4个字节内存。
也就是说gd里的全局变量并没有被分配内存,否则gd不过是一个野指针而已。
gd和bd 都需要内存,但内存没有人管理,要分配内存在C语言中要malloc,但裸机情境下根本不能malloc。我们要靠自己去手动分配内存,去DDR上自己去获取。
uboot使用内存的时候还多着呢,我们还不能随意的使用内存。分配的原则:紧凑、安全。
我们在uboot中需要有一个内存的 整体规划
目前已经定了的排布:
4fff ffff()
sp(uboot向上约2M)
33e0 0000 uboot
3000 0000
【这些东西不用改,配置才是一切】
gd_base =
起始地址 大小
堆区 CFG_MALLOC_LEN 912K
栈区 CFG_STACK_SIZE 512K
gd sizeof gd_t 约44B
bd sizeof bd_t 36B
(中间空余有400K + 200Kuboot实际大小,除非uboot桑心病况了否则不会超过600K)
uboot CFG_UBOOT_BASE uboot实际大小
【对于栈和内存的生长】
ARM用满减栈,栈是向下生长的,但内存是向上生长的!
gd = (gd_t*)gd_base;
memset ((void*)gd, 0, sizeof (gd_t));
gd->bd = (bd_t*)((char*)gd - sizeof(bd_t));
转char*, 很细节,这里-1就是-1。如果是int*这里-1就是 - (4 * 1)
这句话就是给gd和bd找一个容身之处,并且将其实例化
【内存间隔:防止高版本的GCC的优化造成错误】
我们用的gcc是4.4.1版本的 ,所以3.4已经很老了,所以我们还是要加的。
__asm__ __volatile__("": : :"memory");
这里使用的语法是C语言内嵌汇编。一般是用不到的。
6.4 init_sequence数组:函数指针数组,遍历该数组将进行多个初始化。
gd和bd只是封装起来的 环境变量而已,没有很高级。我们往后
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
if ((*init_fnc_ptr)() != 0) {
hang ();
}
}
【Board.c 416 行】
init_sequence,查找到定义处,里面是大量的init
init_fnc_t *init_sequence[]
查看init_fnc_t类型:
typedef int (init_fnc_t) (void); 函数类型!
//所以 init_fnc_t *init_sequence[] 为函数指针数组类型。
【Board.c 483 行,遍历init_sequence 中的函数】
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) { //*init_fnc_ptr可写为
//*init_fnc_ptr !=NULL
if ((*init_fnc_ptr)() != 0) {
hang (); //返回值不为0,挂起
}
}
【init_sequence中有哪些函数?】
cpu_init
看名字这个函数应该是cpu内部的初始化,所以这里是空的。
board_init
board_init在uboot/board/samsung/x210/x210.c中,是x210开发板相关的初始化。
dm9000网卡初始化。
CONFIG_DRIVER_DM9000这个宏是x210_sd.h中定义的,这个宏用来配置开发板的网卡的。dm9000_pre_init函数就是对应的DM9000网卡的初始化函数。开发板移植uboot时,如果要移植网卡,主要的工作就在这里。这个函数中主要是网卡的GPIO和端口的配置,而不是驱动。因为网卡的驱动都是现成的正确的,移植的时候驱动是不需要改动的,关键是这里的基本初始化。因为这些基本初始化是硬件相关的。
6.5 机器码
【背景知识:机器码和uboot给linux传参】
软件层次初始化DDR的原因:对于uboot来说,他怎么知道开发板上到底有几片DDR内存,每一片的起始地址、长度这些信息呢?在uboot的设计中采用了一种简单直接有效的方式:程序员在移植uboot到一个开发板时,程序员自己在x210_sd.h中使用宏定义去配置出来板子上DDR内存的信息,然后uboot只要读取这些信息即可。(实际上还有另外一条思路:就是uboot通过代码读取硬件信息来知道DDR配置,但是uboot没有这样。实际上PC的BIOS采用的是这种)
为什么嵌入式不使用自动配置?
因为我们笔记本电脑是使用同一接口的,插槽上插上一个4G的内存条BIOS就会知道。嵌入式设备是一个定制化的设计思路,不能通用,而是我们程序员去x210_sd.h中配置。
【X210.c 96行】
gd->bd->bi_arch_number = MACH_TYPE;
MACH_TYPE在x210_sd.h中定义,值是2456,并没有特殊含义,只是当前开发板对应的编号。这个编号就代表了x210这个开发板的唯一编号:机器码,将来这个开发板上面移植的linux内核中的机器码也必须是2456,否则就启动不起来。
uboot不去对比这个值,而是把这个值作为参数给 linux内核传参,linux决定是否启动。
随意编号的问题:发布之后容易与别人冲突,但是uboot和linux之间只要相同就能启动成功
如果是在公司做大项目要注意一下。
gd->bd->bi_boot_params = (PHYS_SDRAM_1+0x100);
bd_info中另一个主要元素,bi_boot_params表示uboot给linux kernel启动时的传参的内存地址。也就是说uboot给linux内核传参的时候是这么传的:uboot事先将准备好的传参(字符串,就是bootargs)放在内存的一个地址处(就是bi_boot_params),然后uboot就启动了内核(uboot在启动内核时真正是通过寄存器r0 r1 r2来直接传递参数的,其中有一个寄存器中就是bi_boot_params)。内核启动后从寄存器中读取bi_boot_params就知道了uboot给我传递的参数到底在内存的哪里。然后自己去内存的那个地方去找bootargs。
说白了就是给linux内核用寄存器传了一个指针、一个地址过去。让linux自己去地址处找参数
(2)经过计算得知:X210中bi_boot_params的值为0x30000100,这个内存地址就被分配用来做内核传参了。所以在uboot的其他地方使用内存时要注意,千万不敢把这里给淹没了。
6.6
interrupt_init
看名字函数是和中断初始化有关的,但是实际上不是,实际上这个函数是用来初始化定时器的(实际使用的是Timer4,该定时器没有输出引脚、没有 TCMPB寄存器,所以 不是PWM用的而是单纯计时)。
//裸机中讲过:210共有5个PWM定时器。其中Timer0-timer3都有一个对应的PWM信号输出的引脚。而Timer4没有引脚,无法输出PWM波形。Timer4在设计的时候就不是用来输出PWM波形的(没有引脚,没有TCMPB寄存器),这个定时器被设计用来做计时。
注意:这里的函数 是轮询方式来查看是否到达时间,CPU没有干别的事情。
(3)Timer4用来做计时时要使用到2个寄存器:TCNTB4、TCNTO4。TCNTB中存了一个数,这个数就是定时次数(每一次时间是由时钟决定的,其实就是由2级时钟分频器决定的)。我们定时时只需要把定时时间/基准时间=数,将这个数放入TCNTB中即可;我们通过TCNTO寄存器即可读取时间有没有减到0,读取到0后就知道定的时间已经到了。
(5)interrupt_init函数将timer4设置为定时10ms。
1 get_PCLK函数获取系统设置的PCLK_PSYS时钟频率
2 设置TCFG0和TCFG1进行分频,然后计算出10ms时需要的值,将其写入TCNTB
3 设置为auto reload模式,然后开定时器开始计时
结构体访问寄存器:在linux内核中所有的寄存器都是这么做的,如下
S5PC11X_TIMERS *const timers = S5PC11X_GetBase_TIMERS();
timers->TCFG0 = 0x0f00;
【S5PC11X_TIMERS】
typedef struct {
S5PC11X_REG32 TCFG0;
S5PC11X_REG32 TCFG1;
S5PC11X_REG32 TCON;
S5PC11X_TIMER ch[4];
S5PC11X_REG32 TCNTB4;
S5PC11X_REG32 TCNTO4;
} S5PC11X_TIMERS;
总结:在学习这个函数时,注意标准代码和之前裸机代码中的区别,重点学会:
1 通过定义 结构体 的方式来访问寄存器
2 通过函数来 自动计算 设置值以设置定时器。
env_init
(1)env_init,看名字就知道是和环境变量有关的初始化。
(2)为什么有很多env_init函数,主要原因是uboot支持各种不同的启动介质(譬如norflash、nandflash、inand、sd卡·····),我们一般从哪里启动就会把环境变量env放到哪里。而各种介质存取操作env的方法都是不一样的。因此uboot支持了各种不同介质中env的操作方法。所以有好多个env_xx开头的c文件。实际使用的是哪一个要根据自己开发板使用的存储介质来定
有好多env_init怎么办?
这些env_xx.c同时只有1个会起作用,其他是不能进去的,通过x210_sd.h中配置的宏来决定谁被包含的,对于x210来说,我们应该看env_movi.c中的函数。
(3)经过基本分析,这个函数只是对内存里维护的那一份uboot的env做了基本的初始化或者说是判定(判定里面有没有能用的环境变量)。当前因为我们还没进行环境变量从SD卡到DDR中的relocate,因此当前环境变量是不能用的。无法从DDR中读取环境变量
【在armboot 776行才 进行 环境变量的重定位】
(4)在start_armboot函数中(776行)调用env_relocate才进行环境变量从SD卡中到DDR中的重定位。重定位之后需要环境变量时才可以从DDR中去取,重定位之前如果要使用环境变量只能从SD卡中去读取。
6.7.start_armboot解析5
2.6.7.1、init_baudrate
(1)init_baudrate看名字就是初始化串口通信的波特率的。
(2)getenv_r函数用来读取环境变量的值。用getenv函数读取环境变量中“baudrate”的值(注意读取到的不是int型而是字符串类型),然后用simple_strtoul函数将字符串转成数字格式的波特率。
(3)baudrate初始化时的规则是:先去环境变量中读取”baudrate”这个环境变量的值。如果读取成功则使用这个值作为环境变量,记录在gd->baudrate和gd->bd->bi_baudrate中;如果读取不成功则使用x210_sd.h中的的CONFIG_BAUDRATE的值作为波特率。从这可以看出:环境变量的优先级是很高的。
2.6.7.2、serial_init
(1)serial_init看名字是初始化串口的。(疑问:start.S中调用的lowlevel_init.S中已经使用汇编初始化过串口了,这里怎么又初始化?这两个初始化是重复的还是各自有不同?)
(2)SI中可以看出uboot中有很多个serial_init函数,我们使用的是uboot/cpu/s5pc11x/serial.c中的serial_init函数。
(3)进来后发现serial_init函数其实什么都没做。因为在汇编阶段串口已经被初始化过了,因此这里就不再进行硬件寄存器的初始化了。
debug的方法:
一般debug函数其实就是printf
#ifdef DEBUG
debug()....
#endif
如果我们不想要debug,那么在文件的开头不应加DEBUG宏定义,最好还加一句
#undef DEBUG
2.6.8.1、console_init_f
(1)console_init_f是console(控制台)的第一阶段初始化。_f表示是第一阶段初始化,_r表示第二阶段初始化。有时候初始化函数不能一次一起完成,中间必须要夹杂一些代码,因此将完整的一个模块的初始化分成了2个阶段。(我们的uboot中start_armboot的826行进行了console_init_r的初始化)
(2)console_init_f在uboot/common/console.c中,仅仅是对gd->have_console设置为1而已,其他事情都没做。
2.6.8.2、display_banner
(1)display_banner用来串口输出显示uboot的logo
(2)display_banner中使用printf函数向串口输出了version_string这个字符串。那么上面的分析表示console_init_f并没有初始化好console怎么就可以printf了呢?
(3)通过追踪printf的实现,发现printf->puts,而puts函数中会判断当前uboot中console有没有被初始化好。如果console初始化好了则调用fputs完成串口发送(这条线才是控制台);如果console尚未初始化好则会调用serial_puts(再调用serial_putc直接操作串口寄存器进行内容发送)。
(4)控制台也是通过串口输出,非控制台也是通过串口输出。究竟什么是控制台?和不用控制台的区别?实际上分析代码会发现,控制台就是一个用软件虚拟出来的设备,这个设备有一套专用的通信函数(发送、接收···),控制台的通信函数最终会