超详细分析Bootloader(Uboot)到内核的启动流程(万字长文!)_bootloader启动过程

188 阅读5分钟
void nand\_init\_ll(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);	
}

复制 Bootloader的第二阶段代码到SDRAM空间中(重定位)

  首先判断是NOR启动还是NAND启动,如果是NAND启动就直接拷贝数据。拷贝代码之前,要传递给拷贝函数三个参数,源,目的,长度。读取NAND的话要参考芯片手册的NAND读取数据的时序,选中NAND,发出读命令,发出地址,发出读命令,判断状态,读取数据,取消选中等。

防止恶意转载:
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:blog.csdn.net/qq\_1693360…

	bl copy_code_to_sdram
	bl clear_bss                         //清除bss段(参考自制uboot章节)

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\_init();
		nand\_read\_ll((unsigned int)src, dest, len);
	}
}

void clear\_bss(void)
{
	extern int __bss_start, __bss_end__;
	int \*p = &__bss_start;
	
	for (; p < &__bss_end__; p++)
		\*p = 0;
}

  最后要清除bss。bss段不占用空间,都是未初始化的全局变量或者已经初始化为零的变量,本来就是零,直接清零就好。不清零的话未初始化的变量可能会存在未知的数值。

设置好栈

  设置栈跳转到SDRAM执行。

ldr pc,=call_board_init_f            //绝对跳转,跳到SDRAM上执行

跳转到第二阶段代码的C入口点

  跳转到SDRAM执行剩下的程序。

call_board_init_f:
.globl base_sp
base_sp:
	.long 0

	ldr	r0,=0x00000000
	bl	board_init_f
	
	/\*unsigned int id 的值存在r0中,正好给board\_init\_r使用\*/
	ldr r1, =_TEXT_BASE
	/\*重新设置栈到之前的位置 指向原来addr\_sp += 128;\*/
	ldr sp,base_sp  
	/\*调用第二阶段代码\*/
	bl	board_init_r

Bootloader第二阶段的功能

初始化本阶段要使用到的硬件设备

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

检测系统内存映射( memory map)

  所谓检测内存映射,就是确定板上使用了多少内存、它们的地址空间是什么。由于嵌入式开发中 Bootloader多是针对某类板子进行编写,所以可以根据板子的情况直接设置,不需要考虑可以适用于各类情况的复杂算法。

将内核映象和根文件系统映象从 Flash上读到SDRAM空间中

  Flash上的内核映象有可能是经过压缩的,在读到SDRAM之后,还需要进行解压。当然,对于有自解压功能的内核,不需要 Bootloader来解压。将根文件系统映象复制到SDRAM中,这不是必需的。这取决于是什么类型的根文件系统以及内核访问它的方法。

  将内核存放在适当的位置后,直接跳到它的入口点即可调用内核。调用内核之前,下列条件要满足:
(1)CPU寄存器的设置
R0=0(规定)。
R1=机器类型ID;对于ARM结构的CPU,其机器类型ID可以参见 linux/arch/arm tools/ mach-types
R2=启动参数标记列表在RAM中起始基地址(下面会详细介绍如何传递参数)。
(2)CPU工作模式
必须禁止中断(IRQ和FIQ,uboot启动是一个完整的过程,没有必要也不能被打断)
CPU必须为SVC模式(为什么呢?主要是像异常模式、用户模式都不合适。具体深入的原因自己可以查下资料)。
(3) Cache和MMU的设置
MMU必须关闭。
指令 Cache可以打开也可以关闭。
数据 Cache必须关闭。

为内核设置启动参数

  Bootloader与内核的交互是单向的, Bootloader将各类参数传给内核。由于它们不能同时行,传递办法只有一个:Bootloader将参数放在某个约定的地方之后,再启动内核,内核启动后从这个地方获得参数

  除了约定好参数存放的地址外,还要规定参数的结构。Linu2.4x以后的内核都期望以标记列表( tagged_list)的形式来传递启动参数。标记,就是一种数据结构;标记列表,就是挨着存放的多个标记。标记列表以标记 ATAG CORE开始,以标记 ATAG NONE结束。

  标记的数据结构为tag,它由一个 tag_header结构和一个联合(union)组成。 tag_ header结构表小标记的类型及长度,比如是表示内存还是表示命令行参数等。对于不同类型的标记使用不同的联合(union),比如表示内存时使用 tag_mem32,表示命令行时使用 tag_cmdline。

  bootloader与内核约定的参数地址,设置内存的起始地址和大小,指定根文件系统在那个分区,系统启动后执行的第一个程序linuxrc,控制台ttySAC0等。

调用内核

  调用内核就是uboot启动的最后一步了。到这里就uboot就完成了他的使命。

uboot启动内核详解

  下面我们来展开说下uboot具体是如何调用内核的,引导内核启动的。

uboot与Linux内核之间的参数传递

  我们知道,uboot启动后已经完成了基本的硬件初始化(如:内存、串口等),接下来它的主要任务就是加载Linux内核到开发板的内存,然后跳转到Linux内核所在的地址运行。

  具体是如何跳转呢?做法很简单,直接修改PC寄存器的值为Linux内核所在的地址,这样CPU就会从Linux内核所在的地址去取指令,从而执行内核代码。

  在前面我们已经知道,在跳转到内核以前,uboot需要做好以下三件事情:

  (1) CPU寄存器的设置
R0=0。
R1=机器类型ID;对于ARM结构的CPU,其机器类型ID可以参见 linux/arch/arm tools/ mach-types
R2=启动参数标记列表在RAM中起始基地址。
(2) CPU工作模式
必须禁止中断(IRQs和FIQs)
CPU必须为SVC模式
(3) Cache和MMU的设置
MMU必须关闭
指令 Cache可以打开也可以关闭
数据 Cache必须关闭

  其中上面第一步CPU寄存器的设置中,就是通过R0,R1,R2三个参数给内核传递参数的。(ATPCS规则可以参考

为什么要给内核传递参数呢?

  在此之前,uboot已经完成了硬件的初始化,可以说已经”适应了“这块开发板。然而,内核并不是对于所有的开发板都能完美适配的(如果适配了,可想而知这个内核有多庞大,又或者有新技术发明了,可以完美的适配各种开发板),此时对于开发板的环境一无所知。所以,要想启动Linux内核,uboot必须要给内核传递一些必要的信息来告诉内核当前所处的环境

如何给内核传递参数?

  因此,uboot就把机器ID通过R1传递给内核,Linux内核运行的时候首先就从R1中读取机器ID来判断是否支持当前机器。这个机器ID实际上就是开发板CPU的ID,每个厂家生产出一款CPU的时候都会给它指定一个唯一的ID,大家可以到uboot源码的arch\arm\include\asm\mach-type.h文件中去查看。
在这里插入图片描述
R2存放的是块内存的基地址,这块内存中存放的是uboot给Linux内核的其他参数。这些参数有内存的起始地址、内存大小、Linux内核启动后挂载文件系统的方式等信息。很明显,参数有多个,不同的参数有不同的内容,为了让Linux内核能精确的解析出这些参数,双方在传递参数的时候要求参数在存放的时猴需要按照双方规定的格式存放

  除了约定好参数存放的地址外,还要规定参数的结构。Linux2.4.x以后的内核都期望以标记列表(tagged_list)的形式来传递启动参数。标记,就是一种数据结构;标记列表,就是挨着存放的多个标记。标记列表以标记ATAG_CORE开始,以标记ATAG_NONE结束。

  标记的数据结构为tag,它由一个tag_header结构和一个联合(union)组成。tag_header结构表示标记的类型及长度,比如是表示内存还是表示命令行参数等。对于不同类型的标记使用不同的联合(union),比如表示内存时使用tag_ mem32,表示命令行时使用 tag_cmdline。具体代码见arch\arm\include\asm\setup.h。
在这里插入图片描述
从上面可以看出,struct_tag结构体由structtag_header+联合体union构成,结构体struct tag_header用来描述每个tag的头部信息,如tag的类型,tag大小。联合体union用来描述每个传递给Linux内核的参数信息。
下面以传递内存标记、传递命令行参数为例来说明参数的传递。
(1)设置开始标记ATAG_CORE

	tag->hdr.tag  = ATAG_CORE;
	tag->hdr.size = tag\_size(tag_core);
	tag->u.core.flags = params->u1.s.flags & FLAG_READONLY;
	tag->u.core.pagesize = params->u1.s.page_size;
	tag->u.core.rootdev = params->u1.s.rootdev;

	tag = tag\_next(tag);

  涉及到的结构体定义如下

struct tag_header {
	__u32 size;
	__u32 tag;
};

/\* The list must start with an ATAG\_CORE node \*/
#define ATAG\_CORE 0x54410001

struct tag_core {
	__u32 flags;		/\* bit 0 = read-only \*/
	__u32 pagesize;
	__u32 rootdev;
};

  其中tag_next,tag_size定义如下,指向当前标记的结尾

#define tag\_next(t) ((struct tag \*)((u32 \*)(t) + (t)->hdr.size))
#define tag\_size(type) ((sizeof(struct tag\_header) + sizeof(struct type)) >> 2)

  (2)设置内存标记

	t->hdr.tag = ATAG_MEM;
	t->hdr.size = tag\_size(tag_mem32);
	t->u.mem.start = CFG_GLOBAL_RAM_BASE;
	t->u.mem.size = CFG_GLOBAL_RAM_SIZE;

	t = tag\_next(t);

  相关结构体定义如下

#define ATAG\_MEM 0x54410002

struct tag_mem32 {
	__u32	size;
	__u32	start;	/\* physical start address \*/
};

  (3)设置命令行参数标记

  命令行参数是一个字符串,一般用它来告诉内核挂载根文件系统的方式。由uboot的bootargs环境变量提供,它的内容有如下两种格式

root=nfs nfsroot=202.193.61.237:/work/nfs_root/first_fs ip=202.193.61.196 init=/linuxrc console=ttySAC0,115200

root=/dev/mtdblock2 ip=202.193.61.196 init=/linuxrc console=ttySAC0,115200

名称含义
root告诉Linux内核挂载根文件系统的方式,nfs表示以NFS服务的方式挂载根文件系统,/dev/mtdblock2表示根文件系统在MTD设置的第二个分区上。
nfsroot告诉Linux内核,以NFS方式挂载根文件系统时,根文件系统所在主机的P地址和路径
ip告诉Linux内核,启动后它的p地址
init告诉Linux内核,启动的第一个应用程序是根目录下的linuxrc程序
console告诉Linux区内核,控制台为ttySAC0,波特率为115200
tag = tag\_next(tag);
tag->hdr.tag = ATAG_CMDLINE;
tag->hdr.size = (strlen(params->commandline) + 3 +
		 sizeof(struct tag_header)) >> 2;
strcpy(tag->u.cmdline.cmdline, params->commandline);

tag = tag\_next(tag);

  相关结构体定义如下

/\* command line: \0 terminated string \*/
#define ATAG\_CMDLINE 0x54410003

struct tag_cmdline {
	char	cmdline[1];	/\* this is the minimum size \*/
};

  (4)设置结束标记

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

  我们明白了运行Linux区内核的时候,uboot需要给内核的传递的参数,接下来我们就来看看如何从uboot中跳到Linux内核。

uboot跳转到Linux内核

  在uboot中可以使用go和bootm来跳转到内核,这两个命令的区别如下:

  (1) go命令仅仅修改pc的值到指定地址

  格式:go addr

  (2) bootm命令是uboot专门用来启动uImage格式的Linux内核,它在修改pc的值到指定地址之前,会设置传递给Linux内核的参数,用法如下:

  格式:bootm addr

uboot中bootm命令实现

  bootm命令在uboot源码common/cmd_bootm.c中实现,它的功能如下:

  (1)读取uImage头部,把内核拷贝到合适的地方。

  (2)把参数给内核准备好。

  (3)引导内核。

  当我们使用我们在uboot使用bootm命令后,bootm命令会从uImage头中读取信息后,发现是Linux内核,就会调用do_bootm_linux()函数,函数的具体实现bootm.c中

int do\_bootm\_linux(int flag, int argc, char \*argv[], bootm_headers_t \*images)
{
	/\* No need for those on ARM \*/
	if (flag & BOOTM_STATE_OS_BD_T || flag & BOOTM_STATE_OS_CMDLINE)
		return -1;

	if (flag & BOOTM_STATE_OS_PREP) {
		boot\_prep\_linux(images);
		return 0;
	}

	if (flag & BOOTM_STATE_OS_GO) {
		boot\_jump\_linux(images);
		return 0;
	}

	boot\_prep\_linux(images);
	boot\_jump\_linux(images);
	return 0;
}

  do_bootm_linux 函数最终会 跳转执行 boot_prep_linux 和 boot_jump_linux 函数,首先分析 boot_prep_linux 函数(位于 bootm.c 文件中):

static void boot\_prep\_linux(bootm_headers_t \*images)
{
    char \*commandline = getenv("bootargs");      //从环境变量中获取 bootargs 的值

  。。。。。。。
        setup\_board\_tags(&params);      
        setup\_end\_tag(gd->bd);    //将 tag 参数保存在指定位置
    } else {
        printf("FDT and ATAGS support not compiled in - hanging\n");
        hang();
    }
    do\_nonsec\_virt\_switch();
}

   从代码可以看出来,boot_prep_linux,主要功能是将 tag 参数保存到指定位置,比如 bootargs 环境变量 tag,串口 tag,接下来分析 boot_jump_linux 函数(位于 bootm.c 文件中):

static void boot\_jump\_linux(bootm_headers_t \*images, int flag)
{
    unsigned long machid = gd->bd->bi_arch_number;      //获取机器id (在 board/samsung/jz2440/jz2440.c 中设置,为 MACH\_TYPE\_SMDK2410(193))
    char \*s;
    void (\*kernel_entry)(int zero, int arch, uint params);
    unsigned long r2;
    int fake = (flag & BOOTM_STATE_OS_FAKE_GO);

    kernel_entry = (void (\*)(int, int, uint))images->ep;    //获取 kernel的入口地址,此处应为 30000000

    s = getenv("machid");        //从环境变量里获取机器id (本例中还未在环境变量里设置过机器 id)
    if (s) {            //判断环境变量里是否设置机器id
        strict\_strtoul(s, 16, &machid);    //如果设置则用环境变量里的机器id
        printf("Using machid 0x%lx from environment\n", machid);
    }

    debug("## Transferring control to Linux (at address %08lx)" \
        "...\n", (ulong) kernel_entry);
    bootstage\_mark(BOOTSTAGE_ID_RUN_OS);
    announce\_and\_cleanup(fake);

    if (IMAGE_ENABLE_OF_LIBFDT && images->ft_len)
        r2 = (unsigned long)images->ft_addr;
    else
        r2 = gd->bd->bi_boot_params;    //获取 tag参数地址,gd->bd->bi\_boot\_params在 setup\_start\_tag 函数里设置
if (!fake) kernel\_entry(0, machid, r2); }  //进入内核

  通过分析可以看出,最终进入内核的函数为 :

kernel\_entry(0, machid, r2)

  到这里bootm就成功给内核传递了参数,并跳转到了内核。关于go命令的实现可以自己参考内核,在cmd_boot.c文件中,所不同的是,go命令实现的时候没有设置参数,只是简单的跳转执行。如果想要使用go来跳转到Linux内核,我们需要做简单的修改,有兴趣的可以自己研究下,这里就不展开讲了。

收集整理了一份《2024年最新物联网嵌入式全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升的朋友。 img img

如果你需要这些资料,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人

都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!