【嵌入式Linux】Uboot源码分析笔记

整体简介

本笔记为记录嵌入式Linux的uboot部分基础知识,结合源码对uboot的实现原理和应用展开学习

授课老师:朱有鹏
时间:2018.09

part2主要进行uboot的源码分析和组成原理学习。
先分析,再移植。本part暂不涉及移植过程。笔记的后面一部分会引入一部分驱动的概念


笔记正文

5.1.2、SourceInsight中如何找到文件

首先要找到代码的起始:ENTRY(_start)
汇编中ENTRY里面的符号,即是程序的起始代码符号。找这个_start用SI搜索。
点菜单栏那个蓝色的“R”按钮,reference。

uboot第一阶段
1.硬件设备初始化
(1)设置异常向量
(2)CPU进入SVC模式(管理模式)
(3)设置控制寄存器地址
(4)关闭看门狗(防止无限重启)
(5)屏蔽中断
(6)设置MPLLCON,UPLLCON, CLKDIVN
	//为了提高系统时钟,需要用软件来启用PLL
	//设置频率,就为了CPU能去操作外围设备
(7)关闭MMU,cache  ------(也就是做bank的设置)
	//为什么关MMU和cache?
	上电的时候MMU必须关闭,指令cache可关闭,可不关闭,但数据cache一定要关闭
	否则可能导致刚开始的代码里面,去取数据的时候,从cache里面取,而这时候RAM中数据		还没有过来,导致数据预取异常
	//关闭MMU。因为MMU是把 虚拟地址 转化为 物理地址 得作用,麻烦,暂时不需要。

	_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>

这个config.h就是 include/config.h makefile 100行左右生成的,里面包含了一个x210_sd.h
这个x210_sd.h太重要。配置和代码就在这里结合起来的。
实际这个#include <config.h>包含的就是x210_sd.h
里面就是定义了好多宏,而且下面马上就用到了

【start.S 35行】

#ifndef CONFIG_ENABLE_MMU		//如果宏定义了MMU
#ifndef CFG_PHY_UBOOT_BASE
	#define CFG_PHY_UBOOT_BASE	CFG_UBOOT_BASE
#endif
#endif

#include <asm/proc/domain.h>	
                这里的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字节的内容是不对的,还是需要后面去计算校验和然后重新填充的。

【校验头——准备工作】

#if defined(CONFIG_EVT1) && !defined(CONFIG_FUSED)
	.word 0x2000
	.word 0x0
	.word 0x0
	.word 0x0
#endif

.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		    	// 禁止L2 cache
bl	set_l2cache_auxctrl_cycle	// l2 cache相关初始化
bl	enable_l2cache	    		// 使能l2 cache

(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,以后备用。

/* NAND BOOT */
	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	/* go setup pll,mux,memory */

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		/* r0 <- current base addr of code */
	ldr	r2, _TEXT_BASE	/* r1 <- original base addr in ram */
	bic	r2, r2, r0		/* r0 <- current base addr of code */
	cmp r1, r2          /* compare r0, r1                  */
	beq 1f		        /* r0 == r1 then skip sdram init   */

	/* init system clock */	//时钟初始化
	bl system_clock_init

	/* Memory initialize */	//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_800_200_166_133
//#define CONFIG_CLK_800_100_166_133
#define CONFIG_CLK_1000_200_166_133
//#define CONFIG_CLK_400_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			/* r0 <- current base addr of code */
	ldr	r2, _TEXT_BASE		/* r1 <- original base addr in ram */
	bic	r2, r2, r0			/* r0 <- current base addr of code */
	cmp     r1, r2                 		 /* compare r0, r1                  */
	beq     after_copy		/* r0 == r1 then skip flash copy   */

#if defined(CONFIG_EVT1)
	/* If BL1 was copied from SD/MMC CH2 */,//比较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寄存器中存储着。

/* SD/MMC BOOT */
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:
	/* enable domain access */
	ldr	r5, =0x0000ffff		选用
	mcr	p15, 0, r5, c3, c0, 0		@load domain access register

	/* 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)物理地址就是物理设备设计生产时赋予的地址。
像裸机中使用的寄存器的地址就是CPU设计时指定的,这个就是物理地址。物理地址是硬件编码的,是设计生产时确定好的,只能通过查询数据手册,一旦确定了就不能改了。

因为他不够灵活。解决方案就是使用虚拟地址。

(3)虚拟地址意思就是在我们软件操作和硬件被操作之间增加一个层次,叫做虚拟地址映射层。有了虚拟地址映射后,软件操作只需要给虚拟地址,硬件操作还是用原来的物理地址,映射层建立一个虚拟地址到物理地址的映射表。当我们软件运行的时候,软件中使用的虚拟地址在映射表中查询得到对应的物理地址再发给硬件去执行(虚拟地址到物理地址的映射是不可能通过软件来实现的)。

    VA虚拟地址
( 中间为虚拟地址映射层 ) ----- 反映为虚拟地址映射表
	PA物理地址

虚拟地址映射表:

表索引	  表项
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 (\base << 20) | (\ap << 10) | \
	      (\d << 5) | (1<<4) | (\c << 3) | (\b << 2) | (1<<1)

\base << 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		/* find start of bss segment        */
	ldr	r1, _bss_end		/* stop here                        */
	mov 	r2, #0x00000000		/* clear                            */

链接脚本中的
bss_start是__bss_start
bss_end 是__bss_end

我们这里的_bss_start和链接脚本的写法不一样,但是值是一样的,_bss_start赋值就是__bss_start

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

【长跳转,从SRAM中跳转到DDR中第二阶段开头处】

ldr	pc, _start_armboot		//uboot第一阶段的最后一句话	
					//指向一个C函数。即是uboot第二阶段

长跳转的含义是这句话加载的地址和 运行地址无关,而是和链接地址有关。
注意:本课程讲的uboot第一阶段都是在SRAM中进行的。DDR已经被我们初始化好了,这个长跳转就跳转到DDR中去运行了

SRAM					DDR
uboot 1+2 	 ---------- -------- ----	uboot 1+2
(在此运行uboot1)		跳转		(在此运行uboot2)
		(这个远跳转就是1/2阶段分界线)

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环境变量对应的启动命令。

【代码规范】

/* Pointer is writable since we allocated a register for it */
#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");	//c语言支持的语法,要把这个变量指定放到r8里

“DECLARE_GLOBAL_DATA_PTR”这一句代码很常见,是一个声明。声明的内容是使用gd这个结构体内的全局变量。它的作用和头文件比较像,头文件也能起这个作用。只不过头文件有些不方便,和路径有关,可能经不住版本变迁的考验。

综合定义:这是一个要放在r8寄存器的全局变量,类型为指针,用寄存器读写,可变。大小为4字节。

指向变量的类型为gd_t型。gd_t定义在/inculde/asm-arm/global_data.h中
	{
	board信息(全都是开发板的相关信息,即bd)
		一个重复了的波特率	
		一个重复的环境变量地址
		ip地址
		机器码
		启动参数
		dram信息
		网卡
		标志位
		波特率
		控制台(have console)
		重定位偏移量
		环境变量相关地址
		环境变量可用与否	//内存里面的有可能不能使用。源变量在SD卡中
						  //要先知道有没有从SD中搬移过去
	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;	//c语言中强制类型转换。C++中叫实例化
				//之间空有一个指针,现在跟一段内存绑定了,也就有了实体。
	memset ((void*)gd, 0, sizeof (gd_t));
				//很好的编程习惯。避免申请到脏内存。
	gd->bd = (bd_t*)((char*)gd - sizeof(bd_t));
	
	//bd本身也是个指针,本体就是(char*)gd - sizeof(bd_t)

转char*, 很细节,这里-1就是-1。如果是int*这里-1就是 - (4 * 1)
这句话就是给gd和bd找一个容身之处,并且将其实例化

【内存间隔:防止高版本的GCC的优化造成错误】
我们用的gcc是4.4.1版本的 ,所以3.4已经很老了,所以我们还是要加的。

/* compiler optimization barrier needed for GCC >= 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指针数组
查看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  2456

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 被定义为一个 vu32的指针

	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;
} /*__attribute__((__packed__))*/ 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)控制台也是通过串口输出,非控制台也是通过串口输出。究竟什么是控制台?和不用控制台的区别?实际上分析代码会发现,控制台就是一个用软件虚拟出来的设备,这个设备有一套专用的通信函数(发送、接收···),控制台的通信函数最终会映射到硬件的通信函数中来实现。uboot中实际上控制台的通信函数是直接映射到硬件串口的通信函数中的,也就是说uboot中用没用控制器其实并没有本质差别。
(5)但是在别的体系中,控制台的通信函数映射到硬件通信函数时可以用软件来做一些中间优化,譬如说缓冲机制。(操作系统中的控制台都使用了缓冲机制,所以有时候我们printf了内容但是屏幕上并没有看到输出信息,就是因为被缓冲了。我们输出的信息只是到了console的buffer中,buffer还没有被刷新到硬件输出设备上,尤其是在输出设备是LCD屏幕时)
(6)U_BOOT_VERSION在uboot源代码中找不到定义,这个变量实际上是在makefile中定义的,然后在编译时生成的include/version_autogenerated.h中用一个宏定义来实现的。

2.6.8.3、print_cpuinfo
(1)uboot启动过程中:

CPU:  S5PV210@1000MHz(OK)
        APLL = 1000MHz, HclkMsys = 200MHz, PclkMsys = 100MHz
        MPLL = 667MHz, EPLL = 96MHz
                       HclkDsys = 166MHz, PclkDsys = 83MHz
                       HclkPsys = 133MHz, PclkPsys = 66MHz
                       SCLKA2M  = 200MHz
Serial = CLKUART

这些信息都是print_cpuinfo打印出来的。
(2)回顾ARM裸机中时钟配置一章的内容,比对这里调用的函数中计算各种时钟的方法,自己去慢慢分析体会这些代码的原理和实现方法。这就是学习=v=


2.6.9.1、checkboard
(1)checkboard看名字是检查、确认开发板的意思。这个函数的作用就是检查当前开发板是哪个开发板并且打印出开发板的名字。
2.6.9.2、init_func_i2c
(1)这个函数实际没有被执行,X210的uboot中并没有使用I2C。如果将来我们的开发板要扩展I2C来接外接硬件,则在x210_sd.h中配置相应的宏即可开启。

2.6.9.3、uboot学习实践【修改、配置和烧录】
(1)对uboot源代码进行完修改 //(修改内容根据自己的理解和分析来修改)
(2)

make distclean			//清除,关键
make x210_sd_config		//配置	
make				//编译

(3)编译完成得到u-boot.bin,然后去烧录。烧录方法按照裸机第三部分讲的linux下使用dd命令来烧写的方法来烧写。
(4)烧写过程:

第一步:进入sd_fusing目录下			//此目录是三星附加 的烧录工具 目录
第二步:	make clean			//清除原有的64位配置 
第三步:	make				//更新为32位配置
					//以上操作是纠正烧录工具的问题

第四步:插入sd卡,ls /dev/sd*得到SD卡在ubuntu中的设备号(一般是/dev/sdb,注意SD卡要连接到虚拟机ubuntu中,不要接到windows中)
	【我自己的:	 /dev/mmcblk0,fdisk -l 查看	】
	【file  mkbl1 ,sd_fusing下,可以看到mkbl1属性是80386,那么可以使用 】
第五步:	
	./sd_fusing.sh /dev/sdb		//完成烧录(注意不是sd_fusing2.sh)

(5)总结:uboot就是个庞大点复杂点的裸机程序而已,我们完全可以对他进行调试。调试的方法就是按照上面步骤,根据自己对代码的分析和理解对代码进行更改,然后重新编译烧录运行,根据运行结果来学习。
//注意:每次烧录的时候 可以插拔一下SD卡,确保没有问题。不更改x210_sd.h文件可以直接make

后来发生错误排查汇总:

#<u-boot fusing>
echo "u-boot fusing"
dd iflag=dsync oflag=dsync if=../uboot_inand.bin of=$1 seek=$uboot_position

注意:这里环境和教程不一样:
第一点
uboot_inand.bin 应该是uboot.bin 我们不是用inand启动uboot的
//这个bug在sd_fusing工具中,老师不说,很无语

第二点
ls /dev/sd* 应该是ls /dev/mmc*

第三点
./sd_fusing.sh 应该是./sd_fusing.sh /dev/mmcblk0

第四点 擦除inand头,inand启动失败。
movi write u-boot 0x30000000;


dram_init
(1)dram_init看名字是关于DDR的初始化。疑问:在汇编阶段已经初始化过DDR了否则也无法relocate到第二部分运行,怎么在这里又初始化DDR?
(2)dram_init都是在给gd->bd里面关于DDR配置部分的全局变量赋值,让gd->bd数据记录下当前开发板的DDR的配置信息,以便uboot中使用内存。
(3)从代码来看,其实就是初始化gd->bd->bi_dram这个结构体数组。

display_dram_config
(1)看名字意思就是打印显示dram的配置信息。
(2)启动信息中的:(DRAM: 512 MB)就是在这个函数中打印出来的。
(3)思考:如何在uboot运行中得知uboot的DDR配置信息?uboot中有一个命令叫bdinfo,这个命令可以打印出gd->bd中记录的所有硬件相关的全局变量的值,因此可以得知DDR的配置信息。

DRAM bank   = 0x00000000
-> start    = 0x30000000
-> size     = 0x10000000
DRAM bank   = 0x00000001
-> start    = 0x40000000
-> size     = 0x10000000

init_sequence总结
(1)都是板级硬件的初始化以及gd、gd->bd中的数据结构的初始化。譬如:
网卡初始化、机器码(gd->bd->bi_arch_number)、内核传参DDR地址(gd->bd->bi_boot_params)、Timer4初始化为10ms一次、波特率设置(gd->bd->bi_baudrate和gd->baudrate)、console第一阶段初始化(gd->have_console设置为1)、打印uboot的启动信息、打印cpu相关设置信息、检查并打印当前开发板名字、DDR配置信息初始化(gd->bd->bi_dram)、打印DDR总容量。

总结回顾:本节课结束后已经到了start_armboot的第487行。下节课开始继续往下看。

2.6.11.start_armboot解析9
2.6.11.1、CFG_NO_FLASH
(1)虽然NandFlash和NorFlash都是Flash,但是一般NandFlash会简称为Nand而不是Flash,一般讲Flash都是指的Norflash。这里2行代码是Norflash相关的。
(2)flash_init执行的是开发板中对应的NorFlash的初始化、display_flash_config打印的也是NorFlash的配置信息(Flash: 8 MB就是这里打印出来的)。但是实际上X210中是没有Norflash的。所以着两行代码是可以去掉的(我也不知道为什么没去掉?猜测原因有可能是去掉着两行代码会导致别的地方工作不正常,需要花时间去移植调试,然后移植的人就懒得弄。实际上不去掉除了显示有8MB Flash实际没用之外也没有别的影响)

CONFIG_VFD和CONFIG_LCD是显示相关的,这个是uboot中自带的LCD显示的软件架构。但是实际上我们用LCD而没有使用uboot中设置的这套软件架构,我们自己在后面自己添加了一个LCD显示的部分。

2.6.11.2、mem_malloc_init
(1)mem_malloc_init函数用来初始化uboot的堆管理器。
(2)uboot中自己维护了一段堆内存,肯定自己就有一套代码来管理这个堆内存。有了这些东西uboot中你也可以malloc、free这套机制来申请内存和释放内存。我们在DDR内存中给堆预留了896KB的内存。

2.6.11.3、代码实践,去掉Flash看会不会出错。
结论:加上CONFIG_NOFLASH宏之后编译出错,说明代码移植的不好,那个文件的包含没有被这个宏控制。于是乎移植的人就直接放这没管。


6.12.1、开发板独有初始化:mmc初始化

(1)从536到768行为开发板独有的初始化。意思是三星用一套uboot同时满足了好多个系列型号的开发板,然后在这里把不同开发板自己独有的一些初始化写到了这里。用#if条件编译配合CONFIG_xxx宏来选定特定的开发板。
(2)X210相关的配置在599行到632行。
(3)mmc_initialize看名字就应该是MMC相关的一些基础的初始化,其实就是用来初始化SoC内部的SD/MMC控制器的。函数在uboot/drivers/mmc/mmc.c里。

drivers目录下面的文件基本上是从linux内核中移植过来的驱动、对硬件的操作。
这些驱动因为架构本身的问题有些复杂,uboot作为一个裸机使用了一小部分内核功能的移植的成果。现在去看这些有些早。

【mmc_initialize 和 board_mmc_init区别】
(5)mmc_initialize是具体硬件架构无关的一个MMC初始化函数,所有的使用了这套架构的代码都掉用这个函数来完成MMC的初始化。mmc_initialize中再调用board_mmc_init和cpu_mmc_init来完成具体的硬件的MMC控制器初始化工作。
总结: mmc_initialize是具有MMC处理器硬件通用的
board_mmc_init是具体开发板的init函数。 //x210中该函数为空

有关INIT_LIST_HEAD的部分是在初始化一个链表,维护这个内核链表用于记录了所有的MMC设备。通过这个链表管理我们的MMC设备(我们210支持最多4个MMC设备)

(6)cpu_mmc_init在uboot/cpu/s5pc11x/cpu.c中,这里面又间接的调用了drivers/mmc/s3c_mmcxxx.c中的驱动代码来初始化硬件MMC控制器。这里面分层很多,分层的思想一定要有,否则完全就糊涂了。

SD/MMC初始化和nand初始化是在一个地方的。这里有两个版本。sd烧录版本没有nand,nand烧录版本没有SD。我们的板子是不用nand启动的,所以选错的uboot烧录版本就会失败。问题的所在就在我们现在学的地方。
#if defined(CONFIG_CMD_NAND)
		puts("NAND:    ");
		nand_init();
	#endif

env_relocate();

里面有一段代码没有执行

#ifdef ENV_IS_EMBEDDED
	/*
	 * The environment buffer is embedded with the text segment,
	 * just relocate the environment pointer
	 */
	env_ptr = (env_t *)((ulong)env_ptr + gd->reloc_off);
	DEBUGF ("%s[%d] embedded ENV at %p\n", __FUNCTION__,__LINE__,env_ptr);
#else

我们的环境变量在此时并没有在代码段,而是在SD卡里。

DATAFLASH:单片机常用的小体积flash,3~5M左右,spi控制。我们板子没有。

(1)env_relocate是环境变量的重定位,完成从SD卡中将环境变量读取到DDR中的任务。
(2)环境变量到底从哪里来?SD卡中有一些(8个)独立的扇区作为环境变量存储区域的。但是我们烧录/部署系统时,我们只是烧录了uboot分区、kernel分区和rootfs分区,根本不曾烧录env分区。所以当我们烧录完系统第一次启动时ENV分区是空的,本次启动uboot尝试去SD卡的ENV分区读取环境变量时失败(读取回来后进行CRC校验时失败),我们uboot选择从uboot内部代码中设置的一套默认的环境变量出发来使用(这就是默认环境变量);这套默认的环境变量在本次运行时会被读取到DDR中的环境变量中,然后被写入(也可能是你saveenv时写入,也可能是uboot设计了第一次读取默认环境变量后就写入)SD卡的ENV分区。然后下次再次开机时uboot就会从SD卡的ENV分区读取环境变量到DDR中,这次读取就不会失败了。
(3)真正的从SD卡到DDR中重定位ENV的代码是在env_relocate_spec内部的movi_read_env完成的。


6.14.start_armboot解析12

IP地址、MAC地址的确定
(1)开发板的IP地址是在gd->bd中维护的,来源于环境变量ipaddr。getenv函数用来获取字符串格式的IP地址,然后用string_to_ip将字符串格式的IP地址转成字符串格式的点分十进制格式。
(2)IP地址由4个0-255之间的数字组成,因此一个IP地址在程序中最简单的存储方法就是一个unsigend int。但是人类容易看懂的并不是这种类型,而是点分十进制类型(192.168.1.2)。这两种类型可以互相转换。

devices_init
(1)devices_init看名字就是设备的初始化。这里的设备指的就是开发板上的硬件设备。放在这里初始化的设备都是驱动设备,这个函数本来就是从驱动框架中衍生出来的。uboot中很多设备的驱动是直接移植linux内核的(譬如网卡、SD卡),linux内核中的驱动都有相应的设备初始化函数。linux内核在启动过程中就有一个devices_init(名字不一定完全对,但是差不多),作用就是集中执行各种硬件驱动的init函数。
(2)uboot的这个函数其实就是从linux内核中移植过来的,它的作用也是去执行所有的从linux内核中继承来的那些硬件驱动的初始化函数。

jumptable_init
(1)jumptable跳转表,本身是一个函数指针数组,里面记录了很多函数的函数名。看这阵势是要实现一个函数指针到具体函数的映射关系,将来通过跳转表中的函数指针就可以执行具体的函数。这个其实就是在用C语言实现面向对象编程。在linux内核中有很多这种技巧。
(2)通过分析发现跳转表只是被赋值从未被引用,因此跳转表在uboot中根本就没使用。


6.15.1

console_init_r
(1)console_init_f是控制台的第一阶段初始化,console_init_r是第二阶段初始化。实际上第一阶段初始化并没有实质性工作,第二阶段初始化才进行了实质性工作。
(2)uboot中有很多同名函数,使用SI工具去索引时经常索引到不对的函数处(回忆下当时start.S中找lowlevel_init.S时,自动索引找到的是错误的,真正的反而根本没找到。)
(3)console_init_r就是console的纯软件架构方面的初始化(说白了就是去给console相关的数据结构中填充相应的值),所以属于纯软件配置类型的初始化。
(4)uboot的console实际上并没有干有意义的转化,它就是直接调用的串口通信的函数。所以用不用console实际并没有什么分别。(在linux内console就可以提供缓冲机制等不用console不能实现的东西)。

enable_interrupts
(1)看名字应该是中断初始化代码。这里指的是CPSR中总中断标志位的使能。
(2)因为我们uboot中没有使用中断,因此没有定义CONFIG_USE_IRQ宏,因此我们这里这个函数是个空壳子。
(3)uboot中经常出现一种情况就是根据一个宏是否定义了来条件编译决定是否调用一个函数内部的代码。uboot中有2种解决方案来处理这种情况:方案一:在调用函数处使用条件编译,然后函数体实际完全提供代码。方案二:在调用函数处直接调用,然后在函数体处提供2个函数体,一个是有实体的一个是空壳子,用宏定义条件编译来决定实际编译时编译哪个函数进去。

loadaddr、bootfile两个环境变量
(1)这两个环境变量都是内核启动有关的,在启动linux内核时会参考这两个环境变量的值。

board_late_init
(1)看名字这个函数就是开发板级别的一些初始化里比较晚的了,就是晚期初始化。所以晚期就是前面该初始化的都初始化过了,剩下的一些必须放在后面初始化的就在这里了。侧面说明了开发板级别的硬件软件初始化告一段落了。
(2)对于X210来说,这个函数是空的。

6.16.start_armboot解析14

2.6.16.1、eth_initialize
(1)看名字应该是网卡相关的初始化。这里不是SoC与网卡芯片连接时SoC这边的初始化,而是网卡芯片本身的一些初始化。
(2)对于X210(DM9000)来说,这个函数是空的。X210的网卡初始化在board_init函数中,网卡芯片的初始化在驱动中。

2.6.16.2、x210_preboot_init(LCD和logo显示)
(1)x210开发板在启动起来之前的一些初始化,以及LCD屏幕上的logo显示。

2.6.16.3、check menukey to update from sd
(1)uboot启动的最后阶段设计了一个自动更新的功能。就是:我们可以将要升级的镜像放到SD卡的固定目录中,然后开机时在uboot启动的最后阶段检查升级标志(是一个按键。按键中标志为"LEFT"的那个按键,这个按键如果按下则表示update mode,如果启动时未按下则表示boot mode)。如果进入update mode则uboot会自动从SD卡中读取镜像文件然后烧录到iNand中;如果进入boot mode则uboot不执行update,直接启动正常运行。
(2)这种机制能够帮助我们快速烧录系统,常用于量产时用SD卡进行系统烧录部署。

2.6.16.4、死循环
(1)解析器
(2)开机倒数自动执行
(3)命令补全


6.17.uboot启动2阶段总结
2.6.17.1、启动流程回顾、重点函数标出

(1)第二阶段主要是对开发板级别的硬件、软件数据结构进行初始化。
(2)
	init_sequence
		cpu_init	空的
		board_init	网卡、机器码、内存传参地址
			dm9000_pre_init			网卡
			gd->bd->bi_arch_number	机器码
			gd->bd->bi_boot_params	内存传参地址
		interrupt_init	定时器(4)
		env_init		判定DDR里面有没有能用的环境变量
		init_baudrate	gd数据结构中波特率
		serial_init		空的
		console_init_f	空的
		display_banner	打印启动信息
		print_cpuinfo	打印CPU时钟设置信息
		checkboard		检验开发板名字
		dram_init		gd数据结构中DDR信息
		display_dram_config	打印DDR配置信息表

	mem_malloc_init		初始化uboot自己维护的堆管理器的内存
	mmc_initialize		inand/SD卡的SoC控制器和卡的初始化
	env_relocate		环境变量重定位
	gd->bd->bi_ip_addr	gd数据结构赋值
	gd->bd->bi_enetaddr	gd数据结构赋值
	devices_init		空的
	jumptable_init		不用关注的
	console_init_r		真正的控制台初始化
	enable_interrupts		空的
	loadaddr、bootfile 		环境变量读出初始化全局变量
	board_late_init		空的
	eth_initialize		空的
	x210_preboot_init		LCD初始化和显示logo
	check_menu_update_from_sd	检查自动更新
	main_loop		主循环

2.6.17.2、启动过程特征总结
(1)第一阶段为汇编阶段、第二阶段为C阶段
(2)第一阶段在SRAM中、第二阶段在DRAM中
(3)第一阶段注重SoC内部、第二阶段注重SoC外部Board内部

问题:第一阶段没有JLINK、JTAG等调试器,还没有printf输出,那是不是没法调试了。
这种问题就是单片机做多了。在什么层次上就做什么事。那部分代码已经由原厂工程师调好了,如果一直纠结这个回头什么都学不到了。

2.6.17.3、移植时的注意点
(1)x210_sd.h头文件中的宏定义
(2)特定硬件的初始化函数位置(譬如网卡)


7.1.uboot和内核到底是什么

(1)uboot的本质就是一个复杂点的裸机程序。和我们在ARM裸机全集中学习的每一个裸机程序并没有本质区别。
(2)ARM裸机第十六部分写了个简单的shell,这东西其实就是个mini型的uboot。
2.7.1.2、内核本身也是一个"裸机程序"
(1)操作系统内核本身就是一个裸机程序,和uboot、和其他裸机程序并没有本质区别。他比起裸机程序不一样的地方就是在于更加细致复杂的管控能力。

(2)操作系统运行起来后在软件上分为内核层和应用层,分层后两层的权限不同,内存访问和设备操作的管理上更加精细(内核可以随便访问各种硬件,而应用程序只能被限制的访问硬件和内存地址)。

直观来看:uboot的镜像是u-boot.bin,linux系统的镜像是zImage,这两个东西其实都是两个裸机程序镜像。从系统的启动角度来讲,内核其实就是一个大的复杂点裸机程序。

zImage没有.bin后缀,但实际上和.bin文件没有区别,他其实也是个裸机程序

2.7.1.3、部署在SD卡中特定分区内
(1)一个完整的软件+硬件的嵌入式系统,静止时(未上电时)bootloader、kernel、rootfs等必须的软件都以镜像的形式存储在启动介质中(X210中是iNand/SD卡);运行时都是在DDR内存中运行的,与存储介质无关。上面2个状态都是稳定状态,第3个状态是动态过程,即从静止态到运行态的过程,也就是启动过程。
(2)动态启动过程就是一个从SD卡逐步搬移到DDR内存,并且运行启动代码进行相关的硬件初始化和软件架构的建立,最终达到运行时稳定状态。
(3)静止时u-boot.bin zImage rootfs都在SD卡中,他们不可能随意存在SD卡的任意位置,因此需要对SD卡进行一个分区(原始分区)。
然后将各种镜像各自存在各自的分区中,这样在启动过程中uboot、内核等就知道到哪里去找谁。(uboot和kernel中的分区表必须一致,同时和SD卡的实际使用的分区要一致)
当时是怎么部署的,回头就要怎么分配!

运行是必须先加载到DDR中链接地址处
uboot加载到C3E0 0000
内核要求也类似,在内核加载的DDR的重定位中,也在放在链接地址处3000 8000

回忆:bootcmd: 
movi read kernel 30008000;bootm 30008000

uboot和kernel在启动条件方面的差异

uboot启动:无条件启动
内核启动:需要uboot帮他重定位到DDR中,也需要提供启动参数。


7.2
uboot的两步
第一加载内核到DDR
第二启动内核镜像

内核代码根本不考虑重定位,他知道会有帮忙的把自己加载到DDR中链接地址处。

2.7.2.1、静态内核镜像在哪里?
(1)SD卡/iNand/Nand/NorFlash等:raw分区
常规启动时各种镜像都在SD卡中,因此uboot只需要从SD卡的kernel分区去读取内核镜像到DDR中即可。读取要使用uboot的命令来读取(譬如X210的iNand版本是movi命令,X210的Nand版本就是Nand命令)
(2)这种启动方式来加载ddr,使用命令:movi read kernel 30008000。其中kernel指的是uboot中的kernel分区(就是uboot中规定的SD卡中的一个区域范围,这个区域范围被设计来存放kernel镜像,就是所谓的kernel分区)
uboot中查看分区:fastboot 即可
为什么叫raw分区?
raw分区也就是“原始分区”,是用来存放kernel镜像的位置。
有原始分区也就有“先进分区”,这是linux通过文件系统来划分和管理的分区方式

分区表:
uboot维护一份
内核维护一份
两者必须一样。

查看kernel分区:
movi read kernel 0x30008000

关于下载方式:
SD卡加载启动 广泛用于量产
tftp、nfs等网络下载 广泛用于开发


7.3 启动之前的校验机制——保证镜像是正确的
bootm命令:对应do_bootm函数
	这个函数不仅仅能启动内核,还能启动其他程序。
一般设备是不需要secureboot的,没那么严密。这个函数很复杂,他们的细节我们略过,里面的内容和数字签名有关

这里有个宏很重要:CONFIG_ZIMAGE_BOOT

ifdef CONFIG_ZIMAGE_BOOT
#define LINUX_ZIMAGE_MAGIC	0x016f2818
	/* find out kernel image address */
	if (argc < 2) {
		addr = load_addr;
		debug ("*  kernel: default image load address = 0x%08lx\n",
				load_addr);
	} else

这段代码是用来支持zIMAGE启动的。

vmlinux、zimage和uImage
uboot编译后生成elf格式的可执行程序,在OS下可以直接执行的,但不能直接下载的
u-boot使用arm-linux-objcopy,由elf价格得到烧录的镜像.bin
具体是拿掉了一些无用的东西,linux能把78M精简成7.5M的

.bin即镜像(image),镜像就是用来烧录到inand执行的



linux内核经过编译后叫vmlinux、或者vmlinuz,它和uboot编译后一样得到一个elf格式的文件。ubuntu也有。在我们/boot目录下。

其实78M大小的vmlinux也是可以用的,不简单,所以方便起见还是 进行了删减处理。

vmlinux大小为78M,objcopy处理后的镜像只有7.5M 的Image。但linux的作者还是觉得太大了,于是进行了压缩,并且 在前面添加了解压缩代码。这个方式叫做“自解压”,自己释放自己再释放自己,这个自解压镜像就叫做zImage。当年就是为了节省软盘那点大小产生的方法。
ubuntu中没有zImage,只有vmlinux,是因为都做成图形界面了,不需要再省了,获得更高的启动效率.

【zImage结构】

[未经压缩的代码]			//头。 用来解压下面的内容,如果这个也压缩;
				//那么只能让别人(如uboot)来解压自己了
--------------------
[经过压缩的代码]
[经过压缩的代码]
[经过压缩的代码]

uImage
uboot可以把zImage加工成uImage。这个过程不关linux内核的事,是uboot加工来的
这个过程只是加了一段64bit的头,贴了一个标签。
uboot可以通过这些标签获得一些有效信息

原则上应该给uboot uImage格式的内核镜像,但实际上我们的uboot也可以支持zImage,这段代码由
CONFIG_ZIMAGE_BOOT 这个宏控制

不是每个uboot都支持zimage,但肯定都支持uimage!

【编译内核】

在开发板的/kernel目录下,make zImage
但是我们在做的时候会缺少一个mkimage文件。这个文件在uboot/tools中,有一个mkimage.c
有一个mkimage.h,这两个文件编译生成mkimage。把这个文件放进系统目录下即可编译内核。
	PS:64K的头信息就在 mkimage.c中
	ps:系统目录:提示:环境变量PATH,usr/local/bin

1   然后make zImage
2   然后我们make uImage即可!生成在kernel/arch/arm/boot下,很快
3   cp uImage /tftpboot

上开发板,uboot
1 tftp 3000---- uImage
	打印出一堆信息,启动内核。。。
	Image Name :  linux 2.6.34.7
		//可以看到我们是使用2.6的晚期内核
		//现在3.n  4.n的内核用的比较少

	Created:		时间信息
	Image Type:	ARM Linux Kernel Image (uncompressed)
	Data Size:	3.4M

7.4.zImage启动细节

(1)do_bootm函数中一直到397行的after_header_check这个符号处,都是在进行镜像的头部信息校验。校验时就要根据不同种类的image类型进行不同的校验。所以do_bootm函数的核心就是去分辨传进来的image到底是什么类型,然后按照这种类型的头信息格式去校验。校验通过则进入下一步准备启动内核;如果校验失败则认为镜像有问题,所以不能启动。

2.7.4.1、LINUX_ZIMAGE_MAGIC
(1)这个是一个定义的魔数,这个数等于0x016f2818,表示这个镜像是一个zImage。也就是说zImage格式的镜像中在头部的一个固定位置存放了这个数作为格式标记。如果我们拿到了一个image,去他的那个位置去取4字节判断它是否等于LINUX_ZIMAGE_MAGIC,则可以知道这个镜像是不是一个zImage。
(2)命令 bootm 0x30008000,所以do_boom的argc=2,argv[0]=bootm argv[1]=0x30008000。但是实际bootm命令还可以不带参数执行。如果不带参数直接bootm,则会从CFG_LOAD_ADDR地址去执行(定义在x210_sd.h中)。
(3)zImage头部开始的第37-40字节处存放着zImage标志魔数,从这个位置取出然后对比LINUX_ZIMAGE_MAGIC。可以用二进制阅读软件来打开zImage查看,就可以证明。很多软件都可以打开二进制文件,如winhex、UltraEditor。

2.7.4.2、image_header_t
(1)这个数据结构是我们uboot启动内核使用的一个标准启动数据结构,zImage头信息也是一个image_header_t,但是在实际启动之前需要进行一些改造。hdr->ih_os = IH_OS_LINUX;

hdr->ih_ep = ntohl(addr);这两句就是在进行改造。

(2)images全局变量是do_bootm函数中使用,用来完成启动过程的。zImage的校验过程其实就是先确认是不是zImage,确认后再修改zImage的头信息到合适,修改后用头信息去初始化images这个全局变量,然后就完成了校验。

2.7.5.uImage启动
2.7.5.1、uImage启动
(1)LEGACY(遗留的),在do_bootm函数中,这种方式指的就是uImage的方式。
(2)uImage方式是uboot本身发明的支持linux启动的镜像格式,但是后来这种方式被一种新的方式替代,这个新的方式就是设备树方式(在do_bootm方式中叫FIT)
(3)uImage的启动校验主要在boot_get_kernel函数中,主要任务就是校验uImage的头信息,并且得到真正的kernel的起始位置去启动。

2.7.5.2、设备树方式内核启动
(1)设备树方式启动暂时不讲,课程结束后会用补充专题的方式来讲解(很多类似的知识点都会这样处理,譬如前面讲的MMU)

总结1:uboot本身设计时只支持uImage启动,原来uboot的代码也是这样写的。后来有了fdt方式之后,就把uImage方式命令为LEGACY方式,fdt方式命令为FIT方式,于是乎多了写#if #endif添加的代码。后来移植的人又为了省事添加了zImage启动的方式,又为了省事把zImage启动方式直接写在了uImage和fdt启动方式之前,于是乎又有了一对#if #endif。于是乎整天的代码看起来很恶心。
总结2:第二阶段校验头信息结束,下面进入第三阶段,第三阶段主要任务是启动linux内核,调用do_bootm_linux函数来完成。

2.7.6.do_bootm_linux函数
2.7.6.1、找到do_bootm_linux函数
(1)函数在uboot/lib_arm/bootm.c中。
(2)SI找不到(是黑色的)不代表就没有,要搜索一下才能确定;搜索不到也不能代表就没有,因为我们在向SI工程中添加文件时,SI只会添加它能识别的文件格式的文件,有一些像Makefile、xx.conf等Makefile不识别的文件是没有被添加的。所以如果要搜索的关键字在makefile中或者脚本中,可能就是搜索不到的。(譬如TEXT_BASE)

2.7.6.2、镜像的entrypoint
(1)ep就是entrypoint的缩写,就是程序入口。一个镜像文件的起始执行部分不是在镜像的开头(镜像开头有n个字节的头信息),真正的镜像文件执行时第一句代码在镜像的中部某个字节处,相当于头是有一定的偏移量的。这个偏移量记录在头信息中。
(2)一般执行一个镜像都是:

第一步先读取头信息,然后在头信息的特定地址找MAGIC_NUM,由此来确定镜像种类;
	第二步对镜像进行校验;
	第三步再次读取头信息,由特定地址知道这个镜像的各种信息(镜像长度、镜像种类、入口地址);
	第四步就去entrypoint处开始执行镜像。

	//这个过程是很值得学习的,很多处理过程都是这样的
	//比如说读个jpg格式的图片,先读取头信息,看他是不是一个jpg呢
	//然后校验、读取更多信息、解析图片。

(3)

theKernel = (void (*)(int, int, uint))ep;

将ep赋值给theKernel,则这个函数指向就指向了内存中加载的OS镜像的真正入口地址(就是操作系统的第一句执行的代码)。
使用theKernel函数指针调用函数

2.7.6.3、机器码的再次确定
(1)uboot在启动内核时,机器码要传给内核。uboot传给内核的机器码是怎么确定的?第一顺序备选是环境变量machid,第二顺序备选是gd->bd->bi_arch_num(x210_sd.h中硬编码配置的)

2.7.6.4、传参并启动概述【启动失败原因】
(1)从110行到144行就是uboot在给linux内核准备传递的参数处理。
(2)Starting kernel … 这个是uboot中最后一句打印出来的东西。这句如果能出现,说明uboot整个是成功的,也成功的加载了内核镜像,也校验通过了,也找到入口地址了,也试图去执行了。如果这句后串口就没输出了,说明内核并没有被成功执行。
原因一般是:传参(80%)、内核在DDR中的加载地址·······

uboot能支持的启动方式:
	uImage	    (uboot原版支持,) 
	zImage  	改进了uImage的一些缺点
	fdt	        设备树
	

这里明确的吐个槽:设备树是与zImage等压缩方式的内核共同使用的。
设备树从内核源码编译而来,对系统而言本身也属于内核的一部分,并不可以脱离内核单独使用


2.7.7.传参详解

2.7.7.1、tag方式传参
(1)struct tag,tag是一个数据结构,在uboot和linux kernel中都有定义tag数据机构,而且定义是一样的。
(2)tag_header和tag_xxx。tag_header中有这个tag的size和类型编码,kernel拿到一个tag后先分析tag_header得到tag的类型和大小,然后将tag中剩余部分当作一个tag_xxx来处理。
(3)tag_start与tag_end。kernel接收到的传参是若干个tag构成的,这些tag由tag_start起始,到tag_end结束。
(4)tag传参的方式是由linux kernel发明的,kernel定义了这种向我传参的方式,uboot只是实现了这种传参方式从而可以支持给kernel传参。

2.7.7.2、x210_sd.h中配置传参宏
(1)CONFIG_SETUP_MEMORY_TAGS,tag_mem,传参内容是内存配置信息。
(2)CONFIG_CMDLINE_TAG,tag_cmdline,传参内容是启动命令行参数,也就是uboot环境变量的bootargs.
(3)CONFIG_INITRD_TAG
(4)CONFIG_MTDPARTITION,传参内容是iNand/SD卡的分区表。
(5)起始tag是ATAG_CORE、结束tag是ATAG_NONE,其他的ATAG_XXX都是有效信息tag。
思考:内核如何拿到这些tag?
uboot最终是调用theKernel函数来执行linux内核的,uboot调用这个函数(其实就是linux内核)时传递了3个参数。这3个参数就是uboot直接传递给linux内核的3个参数,通过寄存器来实现传参的。(第1个参数就放在r0中,第二个参数放在r1中,第3个参数放在r2中)第1个参数固定为0,第2个参数是机器码,第3个参数传递的就是大片传参tag的首地址。

2.7.7.3、移植时注意事项
(1)uboot移植时一般只需要配置相应的宏即可
(2)kernel启动不成功,注意传参是否成功。传参不成功首先看uboot中bootargs设置是否正确,其次看uboot是否开启了相应宏以支持传参。

2.7.8.uboot启动内核的总结
2.7.8.1、启动4步骤
第一步:将内核搬移到DDR中
第二步:校验内核格式、CRC等
第三步:准备传参
第四步:跳转执行内核
2.7.8.2、涉及到的主要函数是:do_boom和do_bootm_linux
2.7.8.3、uboot能启动的内核格式:zImage uImage fdt方式
2.7.8.4、跳转与函数指针的方式运行内核

-============================================
链表:为了灵活起见,解决数组大小容易溢出或不够的问题。
比如需要int[200],实际上使用了23个
但是链表需要额外的内存开销。插入、遍历和删除。
优点:动态添加和删除。

uboot处理命令集能让其易扩展并且好用?
可以是链表,但uboot没有 用链表。而是用cmd_tbl
其命令管理的思路是 :命令结构体附加特定段属性,链接时被分拣。有点类似命令结构体数组。我们可以用数组的方式来完成一个遍历。

找函数的定义如果不好找:SI reference,在头文件中查找。小技巧

(1)U_BOOT_CMD宏基本分析
这个宏定义在uboot/common/command.h中。

U_BOOT_CMD(
	version,	1,		1,	do_version,
	"version - print monitor version\n",
	NULL
);

这个宏替换后变成:

#define U_BOOT_CMD(name,maxargs,rep,cmd,usage,help) \
cmd_tbl_t __u_boot_cmd_version __attribute__ ((unused,section (".u_boot_cmd"))) = {#name, maxargs, rep, cmd, usage, help}

其中用到了一些编译器附加技巧:
unused,section (“.u_boot_cmd”) 附加了.u_boot_cmd属性
未来会放进用户自定义.u_boot_cmd段
##name 会被替换成name参数

这个U_BOOT_CMD的定义,两个重点:一个是名字,一个是附加属性。
##name是为了防止重复定义。他可以连到宏定义的name上


【2.8.5 添加自定义命令:方法一】
uboot/common/command.c中添加一个命令,叫:mycmd

【已独立完成】
在uboot/common/command.c中添加如下命令:

do_mycmd (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
	
	printf ("\nThis is my test cmd\n" );
	return 0;
}

U_BOOT_CMD(
	mycmd,	1,		1,	do_mycmd,
	"usage",
	NULL
);

2.8.5 添加自定义命令:自建一个c文件并添加命令

(1)在uboot/common目录下新建一个命令文件,叫cmd_aston.c(对应的命令名就叫aston,对应的函数就叫do_aston函数),然后在c文件中添加命令对应的U_BOOT_CMD宏和函数。注意头文件包含不要漏掉。
(2)在uboot/common/Makefile中添加上aston.o,目的是让Make在编译时能否把cmd_aston.c编译链接进去。
(3)重新编译烧录。重新编译步骤是:make distclean; make x210_sd_config; make

2.8.5.3、体会:uboot命令体系的优点
(1)uboot的命令体系本身稍微复杂,但是他写好之后就不用动了。我们后面在移植uboot时也不会去动uboot的命令体系。我们最多就是向uboot中去添加命令,就像本节课所做的这样。
(2)向uboot中添加命令非常简单。

10.1 硬件驱动

uboot要操控串口、LCD显示需要硬件驱动
想要把镜像烧录到inand也需要驱动。

嵌入式最后的学习目的是驱动开发。所谓驱动就是硬件操控的代码。
这里的“驱动”是狭义驱动,是与操作系统有关系的,操控硬件那部分的代码。
裸机本身是没有驱动的概念的。

广义驱动是和硬件操作有关就算驱动
uboot的代码有一部分是驱动,如操控串口、如LCD显示
有一部分不是驱动,如命令体系

inand/SD/MMC的驱动是典型的uboot移植的linux内核驱动程序

机驱动和内核驱动的区别?】
分层!主要是分层的思想。

linux下的地址都是虚拟地址,硬件是物理地址,这里面有一些问题。

软件这个东西,开源的目的就是为了让你抄的,你能抄来变成自己的那么你也很厉害。这里面有很多诸如硬件接法等等很多的细节。这就是移植。
驱动的编写不是我们应该做的事情,而是硬件厂商去写。电容触摸屏的驱动这种东西,让我自己写几个月也写不出来,我们要做的就是把厂家的代码移植过来,让他能够驱动,然后把性能调到最优。能做到这一点,工资拿到1万5~2万都不算高

【linux驱动的特性】
非强耦合性
linux驱动和linux系统本身不是强耦合的。linux驱动本身采取了模块化设计,天生就是让人拿去移植的。

linux驱动的开源性:
移植是源码级别的移植。如果有源代码才有事做。没有源代码是什么都做不了的。

Linux:read the fucking soucre code
学习的最终还是落在源代码上。80%的精力要看懂原来的代码。未来工作里面,只要你会读源代码,那么很快就懂了。
自己学习最好的老师就是源代码,源代码里面包含一切。
内核源代码也没有那么难,届时我们也是可以读的。

linux本身有更复杂的框架,比如虚拟文件目录,sys和proc。uboot移植linux驱动时,只是借用了linux的关键部件而已,所以uboot驱动比linux驱动简单。

【驱动的一点儿模糊的意思】

分离和分层

10.2 驱动整体比较庞大

贸然看根本不知道看哪里。必须有顺序、有思路。
从start_armboot中,第一次初始化调用mmc看起。

【重点在这里:】
mmc_initialize中,初始化函数看起来有两片

cpu_mmc_init	//这部分有s3cxxxxxx的函数,是SoC内部的初始化
			//如gpio和时钟,是SoC自娱自乐	

	mmc_init		//初始化SD/inand芯片
#if defined(CONFIG_X210)

	#if defined(CONFIG_GENERIC_MMC)
		puts ("SD/MMC:  ");
		mmc_exist = mmc_initialize(gd->bd);
		if (mmc_exist != 0)
		{
			puts ("0 MB\n");
		}

mmc_initialize(gd->bd) 这句就被我们找到了。在drivers/mmc/MMc.c
初始化 板上 mmc卡
SD卡里面是有自己处理器的,能够支持一些函数和命令。

这个函数包含几部分:
	SoC里SD/MMC控制器的初始化(MMC系统时钟的初始化)
	SD/MMC相关GPIO的初始化
	SD/inand芯片的初始化

INIT_LIST_HEAD(&mmc_devices);
内核链表的初始化 指向自己
mmc_devices 是一个内核链表类型的【全局变量】(类型:struct list_head)
用来记录系统中所有已经注册的 SD/inand 设备。
eg:我系统中本来有inand,一会插了SD,那么就会有3个inand设备。
系统就知道我有几个卡了。“发现新硬盘”

i

nt cpu_mmc_init(bd_t *bis)
{
#ifdef CONFIG_S3C_HSMMC
	setup_hsmmc_clock();			 MMC系统时钟的初始化
	setup_hsmmc_cfg_gpio();			SD/MMC相关GPIO的初始化
	return smdk_s3c_hsmmc_init();		
#else
	return 0;
#endif
}
	//注解:“HSMMC” -- high speed MMC,高速MMC



	/* MMC0 clock div */
	tmp = CLK_DIV4_REG & ~(0x0000000f);
	clock = get_MPLL_CLK()/1000000;			
	for(i=0; i<0xf; i++)
	{					//配出来的时钟不能超过50M
		if((clock / (i+1)) <= 50) {
			CLK_DIV4_REG = tmp | i<<0;
			break;
		}
	}

smdk_s3c_hsmmc_init
 s3c_hsmmc_initialize

2.10.3.2、s3c_hsmmc_initialize

static int s3c_hsmmc_initialize(int channel)
{
	struct mmc *mmc;

	mmc = &mmc_channel[channel];

	sprintf(mmc->name, "S3C_HSMMC%d", channel);
	mmc->priv = &mmc_host[channel];
	mmc->send_cmd = s3c_hsmmc_send_command;
	mmc->set_ios = s3c_hsmmc_set_ios;
	mmc->init = s3c_hsmmc_init;
	
	...
}

(1)函数位于:uboot/drivers/mmc/s3c_hsmmc.c中。
(2)定义并且实例化一个struct mmc类型的对象(定义了一个指针,并且给指针指向有意义的内存,或者说给指针分配内存),然后填充它的各种成员,最后调用mmc_register函数来向驱动框架注册这个mmc设备驱动。
(3)mmc_register功能是进行mmc设备的注册,注册方法其实就是将当前这个struct mmc使用链表连接到mmc_devices这个全局变量中去。

(4)我们在X210中定义了USE_MMC0和USE_MMC2,因此在我们的uboot初始化时会调用2次s3c_hsmmc_initialize函数,传递参数分别是0和2,因此完成之后系统中会注册上2个mmc设备,表示当前系统中有2个mmc通道在工作。
因此我们可以用通道0和通道2,即inand和sd卡
(5)至此cpu_mmc_init函数分析完成。

2.10.3.3、find_mmc_device

(1)这个函数位于:uboot/drivers/mmc/mmc.c中。
(2)这个函数其实就是通过mmc设备编号来在系统中查找对应的mmc设备(struct mmc的对象,根据上面分析系统中有2个,编号分别是0和2)。
(3)函数工作原理就是通过遍历mmc_devices链表,去依次寻找系统中注册的mmc设备,然后对比其设备编号和我们当前要查找的设备编号,如果相同则就找到了要找的设备。找到了后调用mmc_init函数来初始化它。

2.10.3.4、mmc_init
(1)函数位于:drivers/mmc/mmc.c中。
(2)分析猜测这个函数应该要进行mmc卡的初始化了(前面已经进行了SoC端控制器的初始化)
(3)函数的调用关系为:

mmc_init
	mmc_go_idle			// idle:空闲
		mmc_send_cmd		// 
	mmc_send_if_cond			
		mmc_send_cmd
	······


mmc_send_cmd
	对MMC进行一些操作,要对mmc发出一些命令来进行操作。

2.10.4.1、struct mmc

(1)驱动的设计中有一个关键数据结构。譬如MMC驱动的结构体就是struct mmc这些结构体中包含一些变量和一些函数指针,变量用来记录驱动相关的一些属性,函数指针用来记录驱动相关的操作方法。这些变量和函数指针加起来就构成了驱动。驱动就被抽象为这个结构体。
(2)一个驱动工作时主要就分几部分:驱动构建(构建一个struct mmc然后填充它)、驱动运行时(调用这些函数指针指针的函数和变量)

2.10.4.2、分离思想
(1)分离思想就是说在驱动中将 操作方法 和 数据 分开。【即函数 和 变量分家】
(2)操作方法就是函数,数据就是变量。所谓操作方法和数据分离的意思就是:在不同的地方来存储和管理驱动的操作方法和变量,这样的优势就是驱动便于移植。
Linux内核中驱动经常一句不变移植

分层思想
表面:分成很多个源文件,安排到很多的 文件夹当中。
内在:一个整个的驱动被分成了多个层次。

分层和分离是相关的,分层是为了分离

【驱动运行的程序文件】
Uboot/drivers/mmc/mmc.c [本文件没有具体硬件操作函数,指向mmc结构体函数]
与MMC相关的 操作 和 方法
如操作MMc为空闲,读和写(movi read)

【驱动构建的程序文件】
Uboot/drivers/mmc/s3c_hsmmc.c
mmc结构体中的函数指针,这些函数是和 真正操作硬件的函数挂接的

set_ios

eg

/* Clear Error Interrupt Status Register before issuing cmd */		//硬件操作的函数
	writew(readw(host->ioaddr + SDHCI_ERRINT_STATUS),
	host->ioaddr + SDHCI_ERRINT_STATUS);

host->ioaddr + SDHCI_ERRINT_STATUS		//基地址+偏移量 这是在访问寄存器

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谦谦青岫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值