bootloader

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了bootloader相关的知识,希望对你有一定的参考价值。

参考技术A

ARM Linux启动流程大致为:bootloader ---->kernel---->root filesystem。bootloader 是一上电就拿到cpu 的控制权的,而bootloader实现了硬件的初始化,为kernel的运行创造好条件。

那么bootloader一般都会做些什么

/ ====================================================== /

实现细节

工作在启动加载模式时,uboot会自动执行bootcmd命令,

比如:

uboot首先把内核镜像拷贝到内存地址为0x80000000的地方,然后执行bootm 0x80000000命令。

bootm命令实际上调用的是do_bootm_linux函数:

内核调用函数:theKernel (0,bd->bi_arch_number, bd->bi_boot_params);

the kernel其实不是个函数,而是指向内核入口地址的指针,把它强行转化为带三个参数的函数指针,会把三个参数保存到通用寄存器中,实现了向kernel传递信息的功能,在这个例子里,会把R0赋值为0,R1赋值为机器号 R2赋值为启动参数数据结构的首地址。

这里的machine id,是让内核知道是哪个CPU,从而调用对应的初始化函数。

继续深入

1、需要在设备树文件中声明,单板需要什么样的machine_desc,(可以是一系列的字符串,kernel会从左到右匹配这些字符串,一直找到匹配的为止);

2、kernel中需要表明每个machine_desc需要表明它能支持哪些单板,用字符串表明支持哪些单板。

MACHINE_START和 MACHINE_END实际上被展开成一个结构体

3、 kernel有多个machine_desc跟设备树文件dts中的compatible 吻合,选择哪个?
设备树文件dts中compatible(属性值)从左到右的属性值与kernel中的machine_desc结构体中的dt_compat成员进行比较,匹配成功之后就不会再进行匹配(设备书的属性值从左右匹配优先级依次降低)。

从内核的第一个执行文件head.S开始分析

start_kernel的调用过程如下:

注意:

C语言中的变量在汇编语言中出现,变量名表示的是变量的地址

BootLoader介绍

文章目录

一.BootLoader的引入

首先我们知道对于pc机,他的启动过程是:
BIOS(启动)—>Windows内核(挂在C/D盘)—>系统盘/应用盘(启动)—>应用程序

而对于嵌入式系统(比如Android手机,工控设备等)他的启动过程是:
BootLoader(启动)—>Linux kernel(挂载)—>根文件系统(启动)—>APP

系统上电之后需要一段程序来进行初始化,比如关闭看门狗,改变系统时钟,初始化存储控制器,将更多的代码复制到内存中等等,这些主要是将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核准备好环境,而这一小段程序就是BootLoader。当然为了方便开发,也可以增加一些功能,比如增加网络功能,这样来增强BootLoader的功能。

二.BootLoader的启动方式

大多数的BootLoader都有两种不同的操作模式:“启动加载模式”和“下载模式”。但是注意BootLoader的最终目的是启动内核,因此其实这两种当时并没有所谓的差别。

1.启动加载(boot loading)模式
上电后,BootLoader从板子的某个固态存储设备上将操作系统加载到RAM中运行,整个过程没有用户介入。这种模式是BootLoader的正常工作模式,产品发布时候,BootLoader就工作在这种模式下。

2.下载(downloading)模式
这种模式下,开发人员使用各种命令,通过串口连接或者网络连接从主机上下载文件,将他们直接放在内存运行或者烧入flash类固态存储设备中。以后的系统更新也会使用 Boot Loader 的这种工作模式。工作于这种模式下的 Boot Loader 通常都会向它的终端用户提供一个简单的命令行接口。

对于串口通信的方式:使用xmodem/ymodem/zmodem 协议
对于网络通信的方式:使用tftp,nfs服务等

三.BootLoader的结构和启动过程

1.概述
嵌入式Linux系统的4个层次
(1)引导加载程序:BootLoader
(2)Linux内核:特定于嵌入式板子的定制内核和内核的启动参数。内核的启动参数可以是内核默认的,也可以是BootLoader传递给他的。
(3)文件系统:包括根文件系统和建立于flash内存设备之上的文件系统。里面包含了Linux系统能够运行所必须的应用程序、库等,比如可以给用户提供操作Linux界面的shell程序、动态链接的程序运行时需要的glibc等。
(4)用户应用程序:特定用户的应用程序。

嵌入式Linux系统的典型分区结构:

boot parameters:分区中存放一些可以设置的参数,如:IP地址,串口波特率、要传递给内核的命令行参数等。正常启动过程中,BootLoader先运行,然后他将内核复制到内存中,并且在内核某个固定位置设置好要传递给内核的参数,最后运行内核,内核启动之后,他会挂在根文件系统(root filesystem),启动文件系统中的应用程序。

2.BootLoader的两个阶段
BootLoader的启动过程可以分为单阶段、多阶段两种,多阶段的BootLoader能提供更加复杂的功能以及更好的移植性。

从固态存储设备上启动的BootLoader大多都是两阶段的启动过程。第一阶段用汇编实现,完成一些依赖于CPU体系结构的初始化,并调用第二阶段的代码;第二阶段则是通常使用C语言来实现,这样可以实现更加复杂的功能,而且代码会有更好的可读性和可移植性。

第一阶段
1、硬件设备初始化:关闭看门狗、关中断、设置CPU的速度和时钟频率、RAM初始化等。
2、为加载BootLoader的第二阶段准备RAM空间
3、复制BootLoader的第二阶段代码到RAM空间中
4、设置好栈
5、跳转到第二阶段代码的C入口处

第二阶段
1、初始化本阶段要使用的硬件设备
2、检查系统内存映射(memory map):就是确定板子使用了多少内存,他们的地址空间是什么。
3、将内核映象和根文件映象从flash上读到RAM空间中
4、为内核映象设置启动参数
5、调用内核

为了开发的方便,至少要初始化一个串口以便程序员与BootLoader交互。

四.自己写一个BootLoader

BootLoader是裸板程序组成,因此可以参考uboot来写。
写一个简单的BootLoader,他的功能就是能够启动内核。因此,BootLoader所要实现的功能就是:
(1)关闭看门狗:开发板默认是打开的,如果不关闭,那么在开发板起来之后一段时间之后会复位
(2)初始化时钟:设置分频系数,为了让系统能够跑的更快
(3)初始化sdram
(4)重定位代码:如果BootLoader太大,那么就需要重定位,也就是将BootLoader本身的代码从flash复制到他的链接地址去
(5)执行main函数,就是执行启动的第二阶段

1.BootLoader第一阶段

首先写出一个汇编文件start.s

1、关闭看门狗

/* 1、关闭看门狗 
 * 对于s3c2440来说看门狗默认是关闭的,如果不关闭,那么在开发板起来之后一段时间后会复位
 * 对于2440来说,看门狗的地址是0x53000000,只要将它置为0即可
 */

	/* 这是一条伪汇编指令 
	 * 编译器在编译的时候发现指令比较复杂,会把他拆分成两条指令,先把他放到某个地址,然后再去这个地址读出来
	 */
	ldr r0, =0x53000000
	ldr r1, =0	 /* 值比较简单的话就直接用mov指令 */
	str r1, [r0]	/* 将r1的值存放到r0所在的地址 */

2、设置时钟
设置时钟的目的就是为了让系统能够跑的更快,因此在这里将FCLK设置为400MHz,将HCLK设置为100MHz,将PCLK设置为50MHz。

也就是FCLK:HCLK:PCLK = 1:4:8

怎么编程控制MPLL、HDIV、PDIV,使FCLK=400MHz,HCLK=100MHz,PLCK=50MHz?

因此需要设置`MPLLCON`的FCLK=400MHz,设置`CLKDIVN`的HCLK=FCLK/4,PCLK=FCLK/8。

看芯片手册得知:使用CLKDIVN寄存器来设置这些CLK


那么就可以写出一下代码:
根据PDIVN的第0位和HDVIN的第2位为1,即101,可以计算出为0x5

/* 设置分频比为FCLK:HCLK:PCLK=1:4:8 HDIVN=2,PDIVN=1*/
	ldr r0, =0x4C000014
	ldr r1, =0x5
	str r1, [r0]

同时看到芯片手册notes:

这里我们设置的HDIVN=2,并不为0,因此就需要设置为异步模式(手册要求),CPU的总线模式应该从“fast bus mode”变为“asynchronous bus mode”

直接复制即可

	mrc	p15, 0, r1, c1, c0, 0		/* 读出控制寄存器 */ 
	orr	r1, r1, #0xc0000000			/* 设置为“asynchronous bus mode” */
	mcr	p15, 0, r1, c1, c0, 0		/* 写入控制寄存器 */

r:寄存器
c:协处理器
mrc:从协处理器读取某个值放入寄存器中
mcr:从寄存器中读取某个值到协处理器中

既然我们让HCLK = 400MHz,我们就要去设置对应的寄存器MPLLCON

当晶振为12MHz的时候,MDIV = 92,PDIV = 1,SDIV = 1

因此可以通过计算得到
m = 100;
p = 3;
s = 1;
Fin为12M
MPLL = (210012)/(3*2) = 400MHz

再来看到MPLLCON寄存器:

因此需要设置为:MPLLCON = ((92<<12) | (1<<4) | (1<<0));

代码实现为:

/* 对应的开发板为2440,设置MPLLCON = S3C2440_MPLL_200MHZ */
	ldr r0, =0x4c000004
	ldr r1, =S3C2440_MPLL_400MHZ
	str r1, [r0]
	/* 一旦设置PLL, 就会锁定lock time直到PLL输出稳定
	 * 然后CPU工作于新的频率FCLK
	 */

对于S3C2440_MPLL_400MHZ的定义我们放在开头:

#define S3C2440_MPLL_400MHZ     ((0x5c<<12)|(0x01<<4)|(0x01))

3、初始化sdram
2440一共有8个BANK,关于各个BANK的性质,在这里不再叙述,需要按照数据手册,将BWSCON,BANKCON0~7,REFRESH,BANKSIZE,MRSRB6,MRSRB7这几个寄存器的值一一算出来,然后依次存到寄存器中

sdram_config是程序的一个标号,用来存放算出来的这些值,然后用上面的方法一一存进去

sdram_config:
	.long 0x22011110	 //BWSCON
	.long 0x00000700	 //BANKCON0
	.long 0x00000700	 //BANKCON1
	.long 0x00000700	 //BANKCON2
	.long 0x00000700	 //BANKCON3  
	.long 0x00000700	 //BANKCON4
	.long 0x00000700	 //BANKCON5
	.long 0x00018005	 //BANKCON6
	.long 0x00018005	 //BANKCON7
	.long 0x008C04F4	 // REFRESH
	.long 0x000000B1	 //BANKSIZE
	.long 0x00000030	 //MRSRB6
	.long 0x00000030	 //MRSRB7

汇编代码如下:

/*3、初始化SRAM */
	ldr r0, =MEM_CTL_BASE
	adr r1, sdram_config     /* sdram_config的当前地址 */
	add r3, r0, #(13*4)
1:
	ldr r2, [r1], #4 /*让r1地址的值读到r2,让后r1加4,也就是指向下一个地址*/
	str r2, [r0], #4 /*让r2的值写入到r0地址的寄存器,r0加上4,指向下一个地址*/
	cmp r0, r3 /*不断的循环把sdram_config里面的值写入到BWSCON开始的寄存器里面*/
	bne 1b /*b的含义代表调到这行代码前面的1,如果是1f就代表下面的1*/

对于MEM_CTL_BASE 的定义为:#define MEM_CTL_BASE 0x48000000

4、重定位
注意用C语言写的函数这些要先设置好栈,那么为什么汇编语言调用C函数要设置栈?
(1)保存现场:也就是寄存器的值,防止被破坏。因此,在函数调用之前,应该将这些寄存器等现场暂时保存(入栈push),等调用函数执行完毕后出栈(pop)再恢复现场。这样CPU就可以正确的继续执行了。
(2)传递参数:C语言函数调用时,会传给被调用函数一些参数,对于这些C语言级别参数,被编译器翻译成汇编语言时,要找个地方存放下来,并且让被调用函数能访问,否则没法传递。找个地方存放下来分2种情况。一是,本身传递的参数不多于4个,可以通过寄存器传送。因为在前面的保存现场动作中,已经保存好对应的寄存器的值,此时这些寄存器是空闲的,可以供我们使用存放参数。二是,参数多于4个,寄存器不够用,就得用栈。
(3)保存临时变量:这些临时变量包括函数的非静态局部变量以及编译器自动生成的其他临时变量。

对于重定位,我们为什么要进行重定位?
我们板子中有nor flash、SDRAM和nand flash,还有一个4k的片内内存SRAM。

CPU能直接访问的地方有:nor flash、SDRAM、SRAM和各种控制器(包括NAND flash控制器)。所以当我们的程序烧写到SDRAM或者NOR flash的时候,程序能直接运行。但是如果烧写到NAND flash,芯片会把程序的头4K先拷贝到SRAM中执行,如果NAND flash中的程序小于4K的话,程序还能正常运行,如果大于4K,那大于4K的这部分就运行不了。所以我们就引入了重定位,NAND flash的代码中的前4K的代码中需要把整个代码拷贝到SDRAM去执行。
另外,对于NOR FLASH来说,虽然能在上面执行代码,但是我们却无法写NOR FLASH,所以一旦程序中有需要写的变量,比如全局变量和静态变量,我们在无法在NOR FLASH上直接修改它们的值。因此,我们还是需要将代码重定位到SDRAM中去执行。

这里重定位涉及到了链接脚本,编写出一个boot.lds如下:

/* 链接脚本 */
SECTIONS
	/* 使用位置无关码,只用一个链接地址 也就是代码运行时地址的起始地址 */
	. = 0x33f80000;	/* 刚好跟最高地址相差512k,足够用的 */
	.text :  *(.text) 	/* 所有文件的代码段 */
	
	. = ALIGN(4);	/* 对齐 */
	.rodata :  *(.rodata) 	/* 所有文件的只读数据段 */
	
	. = ALIGN(4);
	.data :  *(.data) 	/* 所有文件的数据段 */
		
	. = ALIGN(4);
	__bss_start = .;	/* 等于当前地址 */
	.bss :  *(.bss)  *(COMMON) 	/* 所有文件的bss段,程序运行之前先把这段内存清0 */
	__bss_end = .;

重定位是为了将BootLoader本身的代码从flash复制到链接地址去。查看芯片手册得知:

我们的内存为64M,基地址是0x30000000,所以在这里我们让sp指向最高地址就好了,栈是向下增长,因此0x30000000再加上64M就位0x34000000

代码如下:

ldr sp, =0x34000000 //设置栈
bl nand_init //不管是nor还是nand启动,都需要初始化nand flash,因为内核存在nand flash上

后续代码如下:

	/* r0为第一个参数,r1为第二个参数,就是链接地址*/
	mov r0, #0	/* 源是0,就是从0地址开始读东西 */
	ldr r1, =_start /* 第一条标号的地址 */
	ldr r2, =__bss_start	/* bss起始地址 */
	sub r2, r2, r1	/* 除去bss段之后的二进制文件的大小 */
    bl copy_code_to_sdram  
    /* 对于bss段(没有初始化或者初始化为0的全局变量),不会存放在最后生成的二进制文件中,因此会把他清0 */
    bl clear_bss

注意到copy_code_to_sdram 这个函数需要三个参数,所以需要在上面写出对应的r0,r1,r2;

1、把地址0作为第一个参数(如果是NAND启动,就是NAND FLASH零地址的位置,如果是NOR启动,就是NOR FLASH 零地址的位置),为拷贝代码的源地址;

2、_start(为代码一开始的地址,也就是链接脚本中定义的0x33f80000)作为第二个参数,为代码拷贝的目的地址;

3、而要拷贝的代码有多长呢?这里就要用到有关ELF文件中BSS段的知识。我们都知道编译出来的bin文件是不包含BSS段的,BSS段存放的是未初始化的全局变量和静态变量,所以我们可以把它想象为初始化为0值,如果bin文件中存放一堆0值的变量是很浪费空间的。这里我们就知道,拷贝的代码长度为BSS段开始的地址减去代码的起始地址,也就是__bss_start - _start 。

对于copy_code_to_sdram函数如下:

/*知识背景:
  *对于nand flash: 开机启动的时候,从0地址开始的前4k内容会被拷贝到
  *芯片的片内0地址开始的RAM里面,并在RAM的0地址开始执行,所以
  *我们可以读写0地址开始的内容。
  *而对于nor flash : 是能直接在nor flash读的,但是不能写,开机启动
  *是在nor flash的0地址处开始执行,所以我们能读但是写不了。
  */
int isBootFromNorFlash(void)

	volatile int *p = (volatile int *)0;
	int val;
 
	val = *p;
	*p = 0x12345678;
	if(*p == 0x12345678)
	
		/*nand flash启动*/
		*p = val;
		return 0;
	
	else
	
		/*nor flash启动*/
		return 1;
	

 
void copy_code_to_sdram(unsigned char *src, unsigned char *dest, unsigned int len)

	int i = 0;
	
	/*如果是NOR启动*/
	if(isBootFromNorFlash())
	
		while(i < len)
		
			dest[i] = src[i];
			i++;
		
	
	else
	
		nand_read((unsigned int)src, dest, len);
	

对于nand_read()函数我们后面进行分析

对于clear_bss函数如下:

void clean_bss(void)

	extern int __bss_start, __bss_end;
	int *p = &__bss_start;
 
	for (; p<&__bss_end; p++)
		*p = 0;

5、执行main函数

	ldr lr, =halt
	ldr pc, =main
	
/* main函数有返回就会跳到这里,避免单板跑飞 */
halt:
	b halt

到此,BootLoader第一阶段汇编部分就是实现了。

2.BootLoader第二阶段

首先注意一点:BootLoader不依赖于任何其他的代码,因此所有的函数都要自己实现。

就像前面说的,为了开发的方便,至少要初始化一个串口以便程序员与BootLoader交互。

1.初始化串口
对于串口的初始化如下:

/* UART registers*/
#define ULCON0              (*(volatile unsigned long *)0x50000000)
#define UCON0               (*(volatile unsigned long *)0x50000004)
#define UFCON0              (*(volatile unsigned long *)0x50000008)
#define UMCON0              (*(volatile unsigned long *)0x5000000c)
#define UTRSTAT0            (*(volatile unsigned long *)0x50000010)
#define UTXH0               (*(volatile unsigned char *)0x50000020)
#define URXH0               (*(volatile unsigned char *)0x50000024)
#define UBRDIV0             (*(volatile unsigned long *)0x50000028)```

#define PCLK            50000000    // init.c中的clock_init函数设置PCLK为50MHz
#define UART_CLK        PCLK        //  UART0的时钟源设为PCLK
#define UART_BAUD_RATE  115200      // 波特率
#define UART_BRD        ((UART_CLK  / (UART_BAUD_RATE * 16)) - 1)

/*
 * 初始化UART0
 * 115200,8N1,无流控
 */
void uart0_init(void)

    GPHCON  |= 0xa0;    // GPH2,GPH3用作TXD0,RXD0
    GPHUP   = 0x0c;     // GPH2,GPH3内部上拉

    ULCON0  = 0x03;     // 8N1(8个数据位,无较验,1个停止位)
    UCON0   = 0x05;     // 查询方式,UART时钟源为PCLK
    UFCON0  = 0x00;     // 不使用FIFO
    UMCON0  = 0x00;     // 不使用流控
    UBRDIV0 = UART_BRD; // 波特率为115200

2.从nand flash里面把内核读入内存

	/* 1. 从NAND FLASH里把内核读入内存 */
	puts("Copy kernel from nand\\n\\r");
	nand_read(0x60000+64, (unsigned char *)0x30008000, 0x200000);
	puthex(0x1234ABCD);
	puts("\\n\\r");
	puthex(*p);
	puts("\\n\\r");

这里注意: 对于nand flash有一个缺陷,就是位反转,因此在nand flash的结构图中会有一个OOB(out of bank)用来解决位反转
的时候除了写一页nand flash之外,还要生成校验码,也就是ECC码,这个ECC码写到OOB中。
的时候除了读一页数据,还要把ECC码从OOB中读出来,读出来的数据要重新生成一个校验码,两个进行比较,如果相等,则读数据没错。有错误就会找出某一位,修正他的错误,
因此访问2048其实是在第二页,对于OBB,可以描述为OOB里的第几个字节

因此下面要实现nand_read()函数

/* NAND FLASH控制器 */
#define NFCONF (*((volatile unsigned long *)0x4E000000))
#define NFCONT (*((volatile unsigned long *)0x4E000004))
#define NFCMMD (*((volatile unsigned char *)0x4E000008))
#define NFADDR (*((volatile unsigned char *)0x4E00000C))
#define NFDATA (*((volatile unsigned char *)0x4E000010))
#define NFSTAT (*((volatile unsigned char *)0x4E000020))

void nand_init(void)

#define TACLS   0
#define TWRPH0  1
#define TWRPH1  0
	/* 设置时序 */
	NFCONF = (TACLS<<12)|(TWRPH0<<8)|(TWRPH1<<4);
	/* 使能NAND Flash控制器, 初始化ECC, 禁止片选 */
	NFCONT = (1<<4)|(1<<1)|(1<<0);	


void nand_select(void)

	NFCONT &= ~(1<<1);	


void nand_deselect(void)

	NFCONT |= (1<<1);	


void nand_cmd(unsigned char cmd)

	volatile int i;
	NFCMMD = cmd;
	for (i = 0; i < 10; i++);


void nand_addr(unsigned int addr)

	unsigned int col  = addr % 2048;
	unsigned int page = addr / 2048;
	volatile int i;

	NFADDR = col & 0xff;
	for (i = 0; i < 10; i++);
	NFADDR = (col >> 8) & 0xff;
	for (i = 0; i < 10; i++);
	
	NFADDR  = page & 0xff;
	for (i = 0; i < 10; i++);
	NFADDR  = (page >> 8) & 0xff;
	for (i = 0; i < 10; i++);
	NFADDR  = (page >> 16) & 0xff;
	for (i = 0; i < 10; i++);	


void nand_wait_ready(void)

	while (!(NFSTAT & 1));


unsigned char nand_data(void)

	return NFDATA;


void nand_read(unsigned int addr, unsigned char *buf, unsigned int len)

	int col = addr % 2048;
	int i = 0;
		
	/* 1. 选中 */
	nand_select();

	while (i < len)
	
		/* 2. 发出读命令00h */
		nand_cmd(0x00);

		/* 3. 发出地址(分5步发出) */
		nand_addr(addr);

		/* 4. 发出读命令30h */
		nand_cmd(0x30);

		/* 5. 判断状态 */
		nand_wait_ready();

		/* 6. 读数据 */
		for (; (col < 2048) && (i < len); col++)
		
			buf[i] = nand_data();
			i++;
			addr++;
		
		
		col = 0;
	

	/* 7. 取消选中 */		
	nand_deselect();

辅助代码如下:

/*
 * 发送一个字符
 */
void putc(unsigned char c)

    /* 等待,直到发送缓冲区中的数据已经全部发送出去 */
    while (!(UTRSTAT0 & TXD0READY));
    
    /* 向UTXH0寄存器中写入数据,UART即自动将它发送出去 */
    UTXH0 = c;


void puts(char *str)

	int i = 0;
	while (str[i])
	
		putc(str[i]);
		i++;
	


void puthex(unsigned int val)

	/* 0x1234abcd */
	int i;
	int j;
	
	puts("0x");

	for (i = 0; i < 8; i++)
	
		j = (val >> ((7-i)*4)) & 0xf;
		if ((j >= 0) && (j <= 9))
			putc('0' + j);
		else
			putc('A' + j - 0xa);
		
		

3.设置参数

怎么为内核设置启动参数?
BootLoader和内核的交互是单向的,方法就是BootLoader将参数放在某个约定的地方,再启动内核,内核启动后就去这个地址获得参数。
除了约定好参数的地址,还要规定参数的结构,是以标记列表TAG的形式来启动内核,标记就是一种数据结构,标记以标记 ATAG CORE开始,以标记 ATAG NONE结束。

标记的数据结构是tag,他是由一个tag_handler和一个联合体union组成
tag_hander结构体的两个成员分别表示类型和长度,比如表示的是内存还是命令行参数
对于不同类型的标记使用不同的联合体,比如内存使用tag_mem32,命令行参数使用tag_cmdline

参数的开始:setup_start_tag
参数的结束:setup_end_tag

/* 2. 设置参数 */
	puts("Set boot params\\n\\r");
	setup_start_tag();
	setup_memory_tags();
	setup_commandline_tag("noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0");
	setup_end_tag();

上述代码中我们用到了4个函数,来源于这里:

那我们可以模仿这四个函数
1.setup_start_tag(bd)函数:

static void setup_start_tag (bd_t *bd)

	/* static struct tag *params; */	
	/* gd->bd->bi_boot_params = 0x30000100; */
	params = (struct tag *) bd->bi_boot_params;

	params->hdr.tag = ATAG_CORE;	/* 表示一个参数的开始 */

	
	/*#define tag_size(type)	((sizeof(struct tag_header) + sizeof(struct type)) >> 2) */
	/*                                       4+4                        3*4             */
	params->hdr.size = tag_size (tag_core);	/* 20 >> 2 = 20/4 = 5 以4字节为单位 */
	/* 一共5个,tag和size占据两个,还有三个用来存储core里面的三个熟悉 */

	/* 都设置为0,说明没有用到这些 */
	params->u.core.flags = 0;
	params->u.core.pagesize = 0;
	params->u.core.rootdev = 0;

	params = tag_next (params); /* 下一个参数的位置 = 当前位置 + 头部的size;指针加5,相当于加上5*4*/

对于tag,定义如下:

struct tag 
	struct tag_header hdr;	/* tag的头部 */
	union 
		struct tag_core core;
		struct tag_mem_range mem_range;
		struct tag_cmdline cmdline;
		struct tag_clock clock;
		struct tag_ethernet ethernet;
	 u;
;

他有一个头部tag_hander,定义如下:

struct tag_header 
	u32 size;
	u32 tag;
;

2.setup_memory_tags (bd)函数:

static void setup_memory_tags (bd_t *bd)	/* tags说明可以设置多个memory */

	int i;

	for (i = 0; i < CONFIG_NR_DRAM_BANKS; i++) 
		params->hdr.tag = ATAG_MEM;
		params->hdr.size = tag_size (tag_mem32);

		params->u.mem.start = bd->bi_dram[i].start;	/* 起始地址 */
		params->u.mem.size = bd->bi_dram[i].size;	/* 大小 */

		params = tag_next (params);
	

3.setup_commandline_tag函数:

static void setup_commandline_tag (bd_t *bd, char *commandline)	/* 设置命令行参数 */

	char *p;

	if (!commandline)
		return;

	/* eat leading white space */
	for (p = commandline; *p == ' '; p++);

	/* skip non-existent command lines so the kernel will still
	 * use its default command line.
	 */
	if (*p == '\\0')
		return;

	params->hdr.tag = ATAG_CMDLINE;
	params->hdr.size =
		(sizeof (struct tag_header) + strlen (p) + 1 + 4) >> 2;	/* 向4取整 */

	strcpy (params->u.cmdline.cmdline, p);

	params = tag_next (params);

4.setup_end_tag 函数:

static void setup_end_tag (bd_t *bd)

	params->hdr.tag = ATAG_NONE;
	params->hdr.size = 0;

上面给出的是源码里面的实现方法,下面我们自己实现:

static struct tag *params;

void setup_start_tag(void)

	params = (struct tag *)0x30000100;

	params->hdr.tag = ATAG_CORE;
	params->hdr.size = tag_size (tag_core);

	params->u.core.flags = 0;
	params->u.core.pagesize = 0;
	params->u.core.rootdev = 0;

	params = tag_next (params);


void setup_memory_tags(void)

	params->hdr.tag = ATAG_MEM;
	params->hdr.size = tag_size (tag_mem32);
	
	params->u.mem.start = 0x30000000;
	params->u.mem.size  = 64*1024*1024;
	
	params = tag_next (params);


void setup_commandline_tag(char *cmdline)

	int len = strlen(cmdline) + 1;
	
	params->hdr.tag  = ATAG_CMDLINE;
	params->hdr.size = (sizeof (struct tag_header) + len + 3) >> 2;

	strcpy (params->u.cmdline.cmdline, cmdline);

	params = tag_next (params);


void setup_end_tag(void)

	params->hdr.tag = ATAG_NONE;
	params->hdr.size = 0;

对于上面4个函数,在内存中的分布如下:

以上我们就设置好了参数,下面就是要跳转执行

4.跳转执行kernel
这一步就更加简单了,我们都知道函数的名字其实就是一个地址值而已,我们前面已经把内核拷贝到内存0x3000800的地方,只要将这个地址值赋给函数指针变量thekernel,再执行就可以了,并传递相应的参数,比如tag的起始地址。

	theKernel = (void (*)(int, int, unsigned int))0x30008000;
	theKernel(0, 362, 0x30000100);  
	/* 
	 *  mov r0, #0
	 *  ldr r1, =362
	 *  ldr r2, =0x30000100
	 *  mov pc, #0x30008000 
	 */

void (*theKernel)(int zero, int arch, uint params);
第一个参数:0 相当于mov r0, #0
第二个参数:机器ID,单板属于那个ID 相当于mov r1, #362
第三个参数:参数的位置

为什么要为内核传递参数?
首先在第一阶段里面是进行一些初始化的工作,这是为了使用开发板,但是内核并不是适配所有的开发板,
内核对于开发板的环境一无所知,因此想要启动内核,还需要给他传递一些参数,告诉内核当前所处的环境

怎么传递参数?
前面讲过三个参数,先将机器ID通过R1传递给内核,内核运行的时候会去从R1中取出机器ID进行分析
是否支持当前机器,这个机器ID实质上就是开发板CPU的ID。再传递参数的位置,也就是这块内存的基地址
这块内存中存放的就是Uboot给linux内核的其他参数,有起始地址、内存大小等,这个参数需要按照指定的
格式,并且还要规定参数的结构,也就是TAG

5.改进
启动内核花了7s,时间很长。主要是花在nand_read里面,这个可以改进
1、提高CPU频率,200MHz—>400MHz
2、启动ICACHE
读协处理器,然后写协处理器 ---->最终不到2ms就读出来了

2440里有CPU,CPU里面有cache,cache里面有指令ICACHE,数据DCACHE。我们的程序在SDRAM中。

如果不使用指令cache,CPU取完指令回来执行,每执行一条指令都回去访问SDRAM,代码不断执行,不断取
但是当有了ICACEH之后,CPU去取指令的时候会把那一块指令全部放到ICACHE中,CPU下次取指令的时候会先去ICACHE中找有没有指令,有的话直接取出来执行
访问外部SDRAM还要发出各种命令来读内存,非常耗时

cache就是高速内存
DCACHE能够使用的前提是MMU要启动

以上我们就是实现了一个简单的bootloader
代码如下:
boot.lds:

SECTIONS 
    . = 0x33f80000;
    .text :  *(.text) 
    
    . = ALIGN(4);
    .rodata : *(.rodata*) 
    
    . = ALIGN(4);
    .data :  *(.data) 
    
    . = ALIGN(4);
    __bss_start = .;
    .bss :  *(.bss)  *(COMMON) 
    __bss_end = .;

init.c:


/* NAND FLASH控制器 */
#define NFCONF (*((volatile unsigned long *)0x4E000000))
#define NFCONT (*((volatile unsigned long *)0x4E000004))
#define NFCMMD (*((volatile unsigned char *)0x4E000008))
#define NFADDR (*((volatile unsigned char *)0x4E00000C))
#define NFDATA (*((volatile unsigned char *)0x4E000010))
#define NFSTAT (*((volatile unsigned char *)0x4E000020))

/* GPIO */
#define GPHCON              (*(volatile unsigned long *)0x56000070)
#define GPHUP               (*(volatile unsigned long *)0x56000078)

/* UART registers*/
#define ULCON0              (*(volatile unsigned long *)0x50000000)
#define UCON0               (*(volatile unsigned long *)0x50000004)
#define UFCON0              (*(volatile unsigned long *)0x50000008)
#define UMCON0              (*(volatile unsigned long *)0x5000000c)
#define UTRSTAT0            (*(volatile unsigned long *)0x50000010)
#define UTXH0               (*(volatile unsigned char *)0x5000002

以上是关于bootloader的主要内容,如果未能解决你的问题,请参考以下文章

IMX6ULL学习笔记——获取和编译U-Boot

IMX6ULL学习笔记——获取和编译U-Boot

IMX6ULL学习笔记——获取和编译U-Boot