uboot中的start.S--uboot的第二阶段

本文详细介绍了U-Boot的start_armboot函数,该函数构成启动的第二阶段,主要负责初始化外部硬件和UBOOT自身。内容包括内存布局、初始化序列、环境变量迁移、设备初始化等步骤,最终进入命令行循环等待用户输入。通过对init_sequence中各函数的分析,揭示了U-Boot启动过程中的关键操作,如CPU、板级硬件、环境变量和网络设备的初始化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

start_armboot函数简介

start_armboot函数是一个很长的函数,这个函数在uboot/lib_arm/board.c的第444行开始到908行结束。 这个函数构成了整个uboot启动的第二阶段。

概括来讲,uboot第一阶段主要是初始化了SOC内部一些部件(譬如看门狗、时钟),然后初始化DDR并且完成重定位。而uboot第二阶段就是要初始化剩下的还没被初始化的硬件,主要是SOC外部硬件(譬如iNand、网卡芯片等)、uboot本身的一些东西(uboot的命令、环境变量等)。然后最终初始化完必要的东西进入uboot命令行准备接收命令。

uboot启动后自动运行打印出很多信息(这些信息就是uboot在第一和第二阶段不断进行初始化时,打印出来的信息)。然后uboot进入了倒数bootdelay秒然后执行bootcmd对应的启动命令。

如果没有用户干涉的情况下,uboot会执行bootcmd进入自动启动内核流程(uboot就死掉了);此时用户可以按下回车键打断ubbot的自动启动,进入到uboot的命令行下。然后uboot就一直工作在命令行下。

uboot的命令行就是一个死循环,循环体内不断重复:接收命令,解析命令,执行命令。这就是uboot的最终归宿。

init_fnc_t

void start_armboot (void)
{
	init_fnc_t **init_fnc_ptr;

函数开头第一句:typedef int(init_fnc_t) (void); 这是一个函数类型。

init_fnc_ptr是一个二重函数指针。二重指针的类型有两个,一个用来指向一重指针,另一个用来指向指针数组。因此这里的init_fnc_ptr可以用来指向一个函数指针数组。

该内容之后:

	/* Pointer is writable since we allocated a register for it */
#ifdef CONFIG_MEMORY_UPPER_CODE /* by scsuh */
	ulong gd_base;

定义了一个长整型变量gd_base,它是由gd变量引申而来的。gd变量的定义在include/asm-arm/global_data.h中。定义如下:

#define DECLARE_GLOBAL_DATA_PTR     register volatile gd_t *gd asm ("r8")

这个gd其实是一个全局变量,它是一个指针类型,占4字节,后面的asm("r8")是gcc支持的一种语法,意思是把gd放在寄存器r8中。gd是指向gd_t类型变量的指针。

gd_t也定义在include/asm-arm/global_data.h中,如下所示:

typedef	struct	global_data {
	bd_t		*bd;
	unsigned long	flags;
	unsigned long	baudrate;
	unsigned long	have_console;	/* serial_init() was called */
	unsigned long	reloc_off;	/* Relocation Offset */
	unsigned long	env_addr;	/* Address  of Environment struct */
	unsigned long	env_valid;	/* Checksum of Environment valid? */
	unsigned long	fb_base;	/* base address of frame buffer */
#ifdef CONFIG_VFD
	unsigned char	vfd_type;	/* display type */
#endif
#if 0
	unsigned long	cpu_clk;	/* CPU clock in Hz!		*/
	unsigned long	bus_clk;
	phys_size_t	ram_size;	/* RAM size */
	unsigned long	reset_status;	/* reset status register at boot */
#endif
	void		**jt;		/* jump table */
} gd_t;

其中定义了很多全局变量,都是整个uboot使用的;其中有一个bd_t类型的指针,指向一个bd_t类型的变量,这个bd是开发板的板级信息的结构体。里面有不少硬件相关的参数,譬如波特率、IP地址、机器码、DDR内存分布。

内存使用排布

DECLARE_GLOBAL_DATA_PTR只是定义了一个指针,里面的全局变量并没有分配内存,因此需要在使用gd之前分配内存,否则他就是野指针。

gd和bd需要内存,而内存当前没有被管理(操作系统还不存在),大片的DDR内存散放着可以随意使用(只要使用内存地址直接去访问内存即可)。但是因为uboot中后续很多操作还需要大片的连着内存块,因此这里使用内存需要本着紧凑排布的原则,需要有一个整体规划。

init_sequence

init_fnc_t *init_sequence[] = {
	cpu_init,		/* basic cpu dependent setup */
#if defined(CONFIG_SKIP_RELOCATE_UBOOT)
	reloc_init,		/* Set the relocation done flag, must
				   do this AFTER cpu_init(), but as soon
				   as possible */
#endif
	board_init,		/* basic board dependent setup */
	interrupt_init,		/* set up exceptions */
	env_init,		/* initialize environment */
	init_baudrate,		/* initialze baudrate settings */
	serial_init,		/* serial communications setup */
	console_init_f,		/* stage 1 init of console */
	display_banner,		/* say that we are here */
#if defined(CONFIG_DISPLAY_CPUINFO)
	print_cpuinfo,		/* display cpu info (and speed) */
#endif
#if defined(CONFIG_DISPLAY_BOARDINFO)
	checkboard,		/* display board info */
#endif
#if defined(CONFIG_HARD_I2C) || defined(CONFIG_SOFT_I2C)
	init_func_i2c,
#endif
	dram_init,		/* configure available RAM banks */
	display_dram_config,
	NULL,
};

在start_armboot函数之前有一个init_sequence函数,如上所示。它是一个函数指针数组,数组中存储了很多个函数指针,这些指向的函数都是init_fnc_t类型(特点是接收参数是void,返回值是int)。

init_sequence在定义时就同时给了初始化,初始化的函数指针都是一些函数名。这些函数是对板级硬件的初始化。init_fnc_t是一个二重函数指针,可以指向init_sequence这个函数指针数组。

可以用for循环遍历这个函数指针数组,依次执行这个数组中的所有函数。遍历有两种方法,第一种是用下标去遍历,用数组个数来截至;第二种是在数组的有效元素后放一个标志,依次遍历到标志处即可截至。这里采用第二种方法遍历,用NULL作为结束标志。这种方法的优势是不用事先统计数组有多少元素。

init_fnc_t指向的函数序列的返回值定义方式是一样的,都是 函数执行正确时返回0,不正确时返回-1。在遍历时去检查返回值,如果遍历中有一个函数返回值不等于0则hang()挂起。由此可知:uboot启动过程中初始化板级硬件时不能出现任何错误,只要有一个错误整个启动就终止。

接下来依次分析init_sequence里面各个函数:

  • cpu_init,这是对cpu内部的初始化,这个事情在uboot第一阶段就做过,所以函数内部是空的。
  • board_init,它的定义在uboot/board/samsung/x210/x210.c中,这个开发板相关的初始化,代码如下:
int board_init(void)
{
	DECLARE_GLOBAL_DATA_PTR;
#ifdef CONFIG_DRIVER_SMC911X
	smc9115_pre_init();
#endif

#ifdef CONFIG_DRIVER_DM9000
	dm9000_pre_init();
#endif

	gd->bd->bi_arch_number = MACH_TYPE;
	gd->bd->bi_boot_params = (PHYS_SDRAM_1+0x100);

	return 0;
}

DECLARE_GLOBAL_DATA_PTR在这里声明是为了后面使用gd方便。CONFIG_DRIVER_DM9000这个宏是在x210_sd.h中定义的,用来配置开发板的网卡。在这个函数之上的dm9000_pre_init函数就是对应该网卡的初始化函数。开发板移植uboot时,如果要移植网卡,主要工作就在这里。

board_init函数只要是网卡的GPIO和端口的配置,而不是驱动。因为网卡的驱动是现成的,移植的时候也无需修改。

gd->bd->bi_arch_number是board_info中的一个元素,含义是:开发板的机器码,所谓机器码就是uboot给这个开发板定义的一个唯一编号。

机器码的作用就是在uboot和Linux内核之间进行比对和适配。因为:嵌入式设备的每一个硬件都是定制化的,不能通用,它的高度定制化的特性决定了软硬件不能随便适配使用。这就告诉我们:这个开发板移植的内核镜像绝对不能下载到另一个开发板去,否则不能启动,就算启动也不能正常工作,有很多隐患。因此Linux做了一个设置:给每个开发板做一个唯一编号(机器码),然后在uboot、Linux内核中都有一个软件维护的机器码编号。然后开发板、uboot、Linux三者去比对机器码,如果对上了就启动,否则不启动。

uboot中配置的这个机器码,会作为uboot给Linux内核的传参的一部分传给Linux内核,内核启动过程中会比对这个接收码。

gd->bd->bi_boot_params是board_info中的另一个元素,含义是:uboot给Linux内核启动时的传参的内存地址。uboot给Linux内核传参的过程是这样的:uboot事先将准备传递的参数(bootargs,字符串)放在内存的一个地址处,即bi_boot_params,然后uboot就启动了内核(uboot在启动内核时真正是通过寄存器r0 r1 r2来直接传递参数,其中有一个寄存器就是bi_boot_params)。

  • interrupt_init

这个函数并不是对中断的初始化,实际上这个函数是用来初始化定时器的(实际使用的是Timer4)

x210共有5个PWM定时器。其中Timer0-3都有一个对应的PWM信号输出的引脚。而Timer4没有引脚,无法输出波形,因为其本身设计用来做计时。

Timer4用来做计时要使用到2个寄存器:TCNTB4、TCNTO4。TCNTB中存了一个数,这个数就是定时次数(每一次时间是由时钟决定的,其实就是由2级时钟分频器决定的)。我们定时时只需要把定时时间/基准时间=数,将这个数放入TCNTB中。然后,我们通过TCNTO寄存器即可读取时间有没有减到0。

使用Timer4时,由于缺乏中断的支持,所以CPU在定时的时候,不能做其他的事情。所以CPU采取轮询方式。在uboot中,bootdelay就是一个很好的例子。

  • env_init

这是与环境变量初始化相关的初始化。之所以有很多env_init函数,主要原因就是uboot支持各种不同的启动介质,一般从哪里启动就把环境变量放到哪里。各种介质存取操作env的方式是不一样的,因此uboot支持各种不同介质中env的操作方法,表现在具有多个env_xx开头的c文件。这些文件同时只有一个起作用,其他是不能进去的,通过x210_sd.h中配置的宏来决定谁被包含。

env_init这个函数只是对内存里维护的那份环境变量做了基本的初始化或者说是判定。当前因为我们还没进行环境变量从SD卡到DDR中的relocate,因此当前环境变量是不能用的。

后续在start_armboot函数中调用env_relocate才进行环境变量从SD卡到DDR中的重定位。重定位之前要使用环境变量只能从SD卡读取。

  • init_baudrate

这个函数用来初始化串口通信的波特率。baudrate初始化的规则是:先去环境变量中读取“baudrate”这个环境变量的值,如果读取成功则使用这个值,并记录在gd->baudrate和gd->bd->bi_baudrate中;如果读取不成功,则使用x210_sd.h中的CONFIG_BAUDRATE的值作为波特率。从这里可以看出,环境变量的优先级是很高的。

  • serial_init

这个函数其实什么都没做,因为在汇编阶段串口已经被初始化过了,这里就不再进行硬件寄存器的初始化了。

  • console_init_f

这是控制台的第一阶段的初始化。有时候初始化函数不能一次一起完成,中间必须要夹杂一些代码,一次将完整的一个模块的初始化分成了两个阶段。

console_init_f在uboot/common/console.c中,仅仅是对gd->have_console设置为1而已,其他事情都没做。

  • display_banner

这个函数用来串口输出显示uboot的logo,它使用printf这个函数向函数输出version_string这个字符串。但是控制台还没初始化好,怎么就可以printf了呢?

通过分析,发现printf函数调用puts,puts函数会判断当前是否初始化好串口,如果console初始化好了则调用fputs完成串口发送;如果没有的话,则会调用serial_puts(这个函数直接操作串口寄存器进行内容发送)

既然有没有控制台,都是通过串口输出,那究竟什么是控制台呢?控制台就是一个用软件虚拟出来的设备,这个设备有一套专用的通信函数,控制台的通信函数最终会映射到硬件的通信函数中来实现。uboot中实际上控制台的通信函数是直接映射到硬件串口的通信函数中的。

但是在别的体系中,控制台的通信函数映射到硬件通信函数时可以用软件做一些优化,譬如说缓冲机制。操作系统中的控制台其实都采用了缓冲机制。

  • print_cpuinfo

uboot启动过程中,打印出来的CPU:... 和Serial = ... 这些信息都是print_cpuinfo打印出来的。

  • checkboard

这个函数的作用是检查当前开发板是哪个开发板并且打印出开发板的名字。

  • init_func_i2c

这个函数实际没有被执行,X210的uboot中并没有使用I2C,如果将来开发板要扩展I2C来接外接硬件,则在x210_sd.h配置相应的宏即可开启。

  • dram_init

dram_init是关于DDR的初始化。可是之前在汇编阶段已经初始化过DDR了否则也无法relocate到第二阶段运行,怎么又在这里进行初始化?

其实,dram_init都是在给gd->bd里面关于DDR配置部分的全局变量赋值,让gd->bd数据记录下当前开发板的DDR的配置信息,以便uboot使用内存。从代码角度来看,其实就是初始化gd->bd->bi_dram这个结构体数组。

  • dispaly_dram_config

这个函数是打印显示dram的配置信息,启动信息中的:(DRAM:512MB)就是在这个函数中打印出来的。

在uboot运行过程中,如何得知uboot的DDR配置信息?

uboot中有一个命令叫bdinfo,这个命令可以打印出gd->bd记录的所有硬件相关的全局变量的值,因此可以得知DDR的配置信息。

  • init_sequence总结:

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

CFG_NO_FLASH

虽然Nandflash和Norflash都是flash,但是一般Nandflash会简称为Nand而不是flash,一般讲flash都是指的Norflash。这里的代码是Norflash相关的。

flash_init执行的是开发板中对应的Norflash的初始化,display_flash_config打印的也是Norflash的配置信息(Flash:8MB就是这里打印出来的)。但是实际上X210中是没有Norflash的,所以这两行代码是可以去掉的(实际代码中没有去掉)。

mem_malloc_init

mem_malloc_init函数用来初始化uboot的堆管理器,uboot中自己维护了一段堆内存,肯定自己就有一套来管理这个堆内存。有了这些东西uboot中也可以使用malloc、free这套机制来申请内存和释放内存。

mmc初始化(开发板独有)

三星用一套uboot同时满足好多系列型号的开发板,然后在这里把不同开发板自己独有的一些初始化写下来。用#if条件编译配合CONFIG_xxx宏来选定特定的开发板。

uboot中对硬件的操作(譬如网卡、SD卡...)都是借用Linux内核中的驱动来实现的,uboot根目录底下有个drivers文件夹,这里面放的都是从Linux内核中移植过来的各种驱动源文件。

mmc_initialize是MMC一些基础的初始化,用来初始化SoC内部的SD/MMC控制器,函数在uboot/drivers/mmc/mmc.c中。这个函数是具体硬件架构无关的MMC初始化函数,所有使用这套架构的代码都调用这个函数来完成MMC的初始化。mmc_initialize中再调用board_mmc_init和cpu_mmc_init来完成具体的硬件的MMC控制器初始化工作。

env_relocate

env_relocate是环境变量的重定位,完成从SD卡中将环境变量读取到DDR中的任务。

环境变量从哪里来?SD卡中有一些独立的扇区作为环境变量存储区域的。但是我们烧录/部署系统时,我们只是烧录uboot分区、kernel分区和rootfs分区,根本未曾烧录env分区。所以当我们烧录完系统第一次启动时env分区时空的,本次启动uboot尝试去SD卡的ENV分区读取环境变量时失败(读取回来后进行CRC校验时失败),我们uboot选择从uboot内部代码中设置的一套默认的环境变量出发来使用(这就是默认环境变量);这套默认的环境变量在本次运行时会被读取到DDR中的环境变量中,然后被写入SD卡的env分区。然后下次开机时uboot就会从SD卡的env分区读取环境变量到DDR中,这次读取就不会失败。

真正从SD卡到DDR中重定位env的代码是在env_relocate_spec内部的movi_read_env完成的。

IP、MAC地址的确定

开发板的IP地址是在gd->bd中维护的,来源于环境变量ipaddr。getenv函数用来获取字符串格式的IP地址,然后用string_to_ip函数将字符串格式的IP地址转成字符串格式的点分十进制格式。

IP地址由4个0-255之间的数字组成,因此一个IP地址在程序中最简单的存储方法就是unsigned int,但是人不容易看懂这种类型,因此需要转换成点分十进制类型。

devices_init

这里的设备指的是开发板硬件上的设备,这个函数是从驱动框架中衍生出来的。uboot中很多设备的驱动是直接移植Linux内核的,Linux内核在启动过程中就有一个devices_init(或者类似名字),作用就是集中执行各种硬件驱动的init函数。

jumptable_init

跳转表本身是一个函数指针数组,里面记录很多函数的函数名,实现一个函数指针到具体函数的映射关系,将来通过跳转表中的函数就可以执行具体的函数。这个其实就是在C语言中实现面向对象编程,在Linux内核中由很多这种技巧。

console_init_r

这个函数是控制台第二阶段的初始化,做的是实质性工作。它是纯软件架构方面的初始化,其实就是去给console相关的数据结构中填充相应的值,所以属于纯软件配置类型的初始化。

enable_interrupts

这里是CPSR中总中断标志位的使能。因为uboot中没有使用中断,因此没有定义CONFIG_USE_IRQ宏,因此这里的函数是空壳子。

uboot中经常出现一种情况:根据一个宏是否定义来决定是否调用某个函数。uboot中有2种方案来处理这种情况。方案一:在调用函数处使用条件编译,然后函数体提供实际代码;方案二:在调用函数处直接调用,然后在函数处提供两个函数体,一个是有实体的,另一个是空壳,用宏定义条件编译来决定实际编译时编译哪个函数进去。

board_late_init

这个函数是开发板级别一些晚期的初始化,剩下一些必须放在后面初始化就放这里。对于X210来说,这个函数是空的。

eth_initialize

这里是网卡相关的初始化,这里不是SoC与网卡芯片连接时SoC这边的初始化,而是网卡芯片本身的一些初始化。

对于X210(网卡芯片DM9000)来说,这个函数是空的。X210的网卡初始化在board_init函数中,网卡芯片的初始化在驱动中。

x210_preboot_init

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

check menukey to update from sd

uboot启动的最后阶段设计了一个自动更新功能。就是:将要升级的镜像放到SD卡的固定目录中,然后开机时在uboot启动的最后阶段检查升级标志(是一个按键,按键中标志为“left”的那个按键,这个按键如果按下则表示update mode,启动时未按下则表示boot mode)。如果进入update mode,那么uboot会自动从SD卡中读取镜像文件然后烧录到iNand中。

main_loop(死循环)

uboot启动之后,如果在开机倒数阶段按下回车,就会进入uboot命令行,这其实就是一个死循环。

启动过程特征总结

  1. 第一阶段为汇编阶段,第二阶段为C阶段
  2. 第一阶段在SRAM中,第二阶段在DRAM中
  3. 第一阶段注重SoC内部,第二阶段注重SoC外部,board内部
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值