当前位置:网站首页 > 技术博客 > 正文

uboot bootz



嗨喽,大家好,我是程序猿老王,程序猿老王就是我。

今天给大家全面的分析一下u-boot启动流程。整理这篇文章花费时间较长,中间很长时间未更新,希望这篇文章对大家有所帮助。

本章主要是详细的分析一下uboot的启动流程,理清uboot是如何启动的。通过对uboot启动流程的梳理,我们就可以掌握一些外设是在哪里被初始化的,这样当我们需要修改这些外设驱动的时候就会心里有数。另外,通过分析uboot的启动流程可以了解Linux内核是如何被启动的。

在看本章之前,个人建议先去个人微信公众号(文末有)看一下前几篇文章。对u-boot的开发环境搭建、u-boot整体移植和u-boot下网络调试有一点了解后,再来看本篇文章,这样可能比较容易看明白。

Linux系统开发环境搭建篇:Linux系统开发环境搭建 u-boot移植篇:u-boot移植:详细讲解移植u-boot.2022.10版本到imx6ull开发板 u-boot网络移植于调试篇:详细讲解u-boot之网络移植与调试 本章主要是详细的分析一下uboot的启动流程,理清uboot是如何启动的。通过对uboot启动流程的梳理,我们就可以掌握一些外设是在哪里被初始化的,这样当我们需要修改这些外设驱动的时候就会心里有数。另外,通过分析uboot的启动流程可以了解Linux内核是如何被启动的。

首先给大家先看一下,u-boot启动从入口函数到启动内核的详细函数调用流程于层级关系图,对u-boot启动的整体有一个快速了解,后面回详细介绍各个函数的作用。

U-Boot 源码文件众多,我们如何知道最开始的启动文件(程序入口)是哪个呢?程序的链接是由链接脚本来决定的,所以通过链接脚本可以找到程序的入口,链接脚本为arch/arm/cpu/u-boot.lds,它描述了如何生成最终的二进制文件,其中就包含程序入口。

u-boot.lds:文件所在位置arch/arm/cpu/u-boot.lds

通过上面的分析可以看出: 由于在链接脚本中规定了文件start.o(对应于start.S)作为整个uboot的起始点,因此启动uboot时会执行首先执行start.S。 一般来说,内存空间可分为代码段、数据段、全局变量段、未初始化变量区、栈区、堆区等.其中,栈区由指针SP决定,堆区实质上是由C代码实现的,其它段则由编译器决定.从上面的分析可以看出,从0x00000000地址开始,编译器首先将代码段放在最开始的位置,然后是数据段,然后是bss段(未初始化变量区)。

u-boot.map 是uboot的映射文件,可以从此文件看到某个文件或者函数链接到了哪个地址,下面打开 u-boot.map,查看各个段的起始地址和结束分别是多少;

从u-boot.map映射文件种,可以知道__image_copy_start为0X,而.text的起始地址也是0X,vectors 段的起始地址也是0X,可以得出各个段的地址关系表,如下;

变量名地址描述 __image_copy_start 0x u-boot拷贝的起始地址 __image_copy_end 0x87850ff0 u-boot拷贝的结束地址 .vectors 0x 中断向量表的起始地址 .text 0xe8 .text段的起始地址 __rel_dyn_start 0x87850ff0 .rel_dyn段的起始地址 __rel_dyn_end 0x8785cf30 .rel_dyn段的结束地址 _image_binary_end 0x8785cf30 镜像结束地址 __bss_start 0x87850ff0 .bss段的起始地址 __bss_end 0xc0 .bss段的结束地址

注:表中的变量除了__image_copy_start以外,其他的变量值每次编译的时候可能会变化。修改uboot 代码、配置等都会影响到这些值。所以,一切以实际值为准!

从链接文件(u-boot.lds) 中知道了程序入口是 _start,_start 在文件 arch/arm/lib/vectors.S 中有定义,具体代码如下;

从u-boot.map映射文件可以得出.vectors段的最开始就是_start,而从_start定义我们可以知道首先是跳转到reset函数,再设置中断向量表。

从程序入口_start定义中得出,_start中首先是跳转到reset函数,reset在文件arch/arm/cpu/armv7/start.S中有定义,具体代码如下;

reset函数只有一句跳转语句,直接跳转到了save_boot_params函数,而save_boot_params函数同样定义在start.S里面,定义如下:

save_boot_params_ret函数主要的操作如下:

  • 1.如果定义宏CONFIG_POSITION_INDEPENDENT,则进行修正重定位的问题(pie_fixup、pie_fix_loop、pie_skip_reloc);
  • 2.如果定义宏CONFIG_ARMV7_LPAE,LPAE(Large Physical Address Extensions)是ARMv7系列的一种地址扩展技术,可以让32位的ARM最大能支持到1TB的内存空间,由于嵌入式ARM需求的内存空间一般不大,所以一般不使用LPAE技术;
  • 3.设置CPU为SVC32模式,除非已经处于HYP模式,同时禁止中断(FIQ和IRQ);
  • 4.设置中断向量表地址为_start函数的地址,在map文件中可以看到,为0x;
  • 5.进行CPU初始化,调用函数cpu_init_cp15和cpu_init_crit分别初始化CP15和CRIT;
  • 6.最后跳转到_main函数。

cpu_init_cp15函数,在文件arch/arm/cpu/armv7/start.S中定义,具体代码如下;

cpu_init_cp15函数主要的操作如下:

  • 1.失效 L1 I/D Cache;
  • 2.禁用MMU和缓存。

cpu_init_crit在文件arch/arm/cpu/armv7/start.S中定义,具体代码如下;

可以看到函数cpu_init_crit内部又只是一句跳转语句,调用了函数lowlevel_init,接下来就是详细的分析一下lowlevel_init和_main这两个函数。

lowlevel_init函数,在文件arch/arm/cpu/armv7/lowlevel_init.S 中有定义,具体代码如下;

lowlevel_init函数主要的操作如下:

  • 1.设置SP指针为CONFIG_SYS_INIT_SP_ADDR
  • 2.对sp指针做8字节对齐处理
  • 3.SP减去#GD_SIZE = 248,GD_SIZE同样在generic-asm-offsets.h 中定了
  • 4.对 sp 指针做8字节对齐处理
  • 5.将SP保存到R9,ip和lr入栈,程序跳转到s_init(对于I.MX6ULL来说,s_init 就是个空函数)
  • 6.函数一路返回,直到_main,s_init函数-->函数lowlevel_ini-->cpu_init_crit-->save_boot_params_ret-->_main。

_main函数在文件 arch/arm/lib/crt0.S中有定义 _main函数执行可以大致分为如下4个部分:

  • 设置初始化C运行环境并调用board_init_f函数
  • 设置新的sp指针和gd指针,设置中间环境位,调用代码重定位
  • 重定位向量表
  • 设置最后的运行环境并调用board_init_r函数

代码部分,具体如下;

  • 1.设置sp指针为 CONFIG_SYS_INIT_SP_ADDR;
  • 2.对sp指针做8字节对齐处理;
  • 3.读取sp到寄存器r0里面;
  • 4.调用函数board_init_f_alloc_reserve;
  • 5.调用函数board_init_f_init_reserve;
  • 6.调用函数board_init_f。 1.board_init_f_alloc_reserve函数 board_init_f_alloc_reserve函数,在common/init/board_init.c文件中定义,如下;

board_init_f_alloc_reserve函数的作用是根据传入参数是栈顶地址,计算出预留空间的底部,并将其返回。 主要是留出早期的 malloc 内存区域和gd内存区域。如果宏CONFIG_MALLOC_F_ADDR没有被定义,则为malloc预留部分内存空间,大小为CONFIG_SYS_MALLOC_F_LEN;其次为GD变量(global_data结构体类型)预留空间,并且对齐到16个字节的倍数。 2.board_init_f_init_reserve函数 board_init_f_init_reserve函数,在common/init/board_init.c文件中定义,如下;

board_init_f_init_reserve函数的作用是初始化gd,其实就是清零处理;设置了gd->malloc_base为gd基地址+gd 大小,并做16字节对齐处理。

代码部分,具体如下;

  • 1.设置新的栈顶指针为sp = gd->start_addr_sp;
  • 2.设置新的gd指针为r9 <- gd->new_gd;
  • 3.设置新r0指针为r0 = gd->reloc_off;
  • 4.设置r0寄存器的值为gd->relocaddr,跳转到代码重定位relocate_code。

代码部分,具体如下;

代码重定位后返回到here标号处,调用relocate_vectors函数,对中断向量表做重定位。

代码部分,具体如下;

board_init_r函数主要工作:

  • 1.调用函数c_runtime_cpu_setup,失效I-cache;
  • 2.清除BSS段;
  • 3.设置函数board_init_r的两个参数;
  • 4.调用函数board_init_r。

board_init_f函数,在common/board_f.c文件定义,具体代码如下;

board_init_f函数主要有两个工作:

  • 1.初始化gd的各个成员变量
  • 2.调用函数initcall_run_list,初始化序列init_sequence_f里面的一系列函数,来初始化一系列外设,比如串口、定时器,或者打印一些消息等。 init_sequence_f数组,在common/board_f.c文件中定义,如下,初始化函数表省略其中部分代码;

其中比较重要的一些初始化函数如下:

  • 1.setup_mon_len函数:设置gd的mon_len成员变量,也就是整个代码的长度;
  • 2.initf_malloc函数:设置gd中和malloc有关的成员变量;
  • 3.board_early_init_f函数:用来初始化串口的IO配置,在board/freescale/mx6ull_toto/mx6ull_toto.c文件中定义;
  • 4.timer_init函数:初始化内核定时器,为uboot提供时钟节拍,在arch/arm/imx-common/timer.c文件中定义;
  • 5.get_clocks函数:获取了SD卡外设的时钟(sdhc_clk),在arch/arm/imx-common/speed.c文件中定义;
  • 6.init_baud_rate函数:初始化波特率,在common/board_f.c文件中定义;
  • 7.serial_init函数:初始化串口通信设置,在drivers/serial/serial.c文件中定义;
  • 8.console_init_f函数:初始化控制台,在common/console.c文件中定义:
  • 9.display_options函数:打印uboot版本信息和编译信息,在lib/display_options.c文件中定义;
  • 10.print_cpuinfo函数:用来显示CPU信息和主频,在arch/arm/imx-common/cpu.c文件中定义;
  • 11.show_board_info函数:打印开发板信息,在common/board_info.c文件中定义;
  • 12.init_func_i2c函数:用于初始化I2C;
  • 13.announce_dram_init函数:此函数很简单,就是输出字符串“DRAM:”;
  • 14.dram_init函数:并非真正的初始化DDR,只是设置gd->ram_size的值。

relocate_code函数,在arch/arm/lib/relocate.S文件定义,具体代码如下;

relocate_code此函数主要完成镜像拷贝和重定位,镜像地址从__image_copy_start开始,到__image_copy_end结束,拷贝的目标地址由参数传进来,也就是r0寄存器的值。重定位的原理此处不展开,需要了解的自行去学习。

relocate_vectors函数,在arch/arm/lib/relocate.S文件定义,具体代码如下;

relocate_vectors函数用于重定位向量表,只有一步操作比较重要,就是将uboot重定位完之后的地址,装载到CP15的VBAR寄存器中设置向量表偏移,该寄存器自行去学习。

board_init_r函数,在common/board_r.c文件定义,具体代码如下;

board_init_f函数中,会初始化一些外设和gd的成员变量,但并没有初始化所有的外设,还需要一些后续工作,这些工作就是由board_init_r函数完成的,调用initcall_run_list函数执行初始化序列init_sequence_r,init_sequence_r是一个函数表,也定义在该文件中,部分代码如下;

其中比较重要的一些初始化函数如下:

  • 1.initr_caches函数:初始化cache,使能cache;
  • 2.board_init函数:FEC初始化,在board/freescale/mx6ull_toto/mx6ull_toto.c文件中定义;
  • 3.initr_mmc函数:初始化emmc,在common/board_r.c文件中定义;
  • 4.iinitr_env函数:初始化环境变量;
  • 5.console_init_r函数:初始化控制台,在common/console.c文件中定义;
  • 6.interrupt_init函数和initr_enable_interrupts函数:初始化中断并使能中断;在arch/arm/lib/interrupts.c文件中定义;
  • 7.initr_ethaddr函数:初始化网络地址,获取MAC地址,读取环境变量ethaddr的值;
  • 8.initr_net函数:初始化网络设备,函 数 调 用 顺 序 为 :initr_net->eth_initialize->board_eth_init(),在common/board_r.c文件中定义;
  • 9.run_main_loop函数:主循环,处理命令。

run_main_loop函数,在common/board_r.c文件定义,具体代码如下;

uboot启动以后会进入3秒倒计时,如果在3秒倒计时结束之前按下按下回车键,那么就会进入uboot的命令模式,如果倒计时结束以后都没有按下回车键,那么就会自动启动Linux内核,这个功能就是由run_main_loop函数来完成的。 main_loop函数,在common/main.c文件中定义,具体代码如下;

main_loop函数主要工作:

  • 1.调用bootstage_mark_name函数,打印出启动进度
  • 2.如果宏CONFIG_VERSION_VARIABLE定义了就会执行函数setenv,设置换将变量ver的值为version_string,也就是设置版本号环境变量;
  • 3.调用cli_init函数,初始化hushshell相关的变量
  • 4.调用bootdelay_process函数,此函数会读取环境变量bootdelay和bootcmd的内容,然后将bootdelay的值赋值给全局变量stored_bootdelay,返回值为环境变量bootcmd的值。
  • 5.autoboot_command函数,此函数就是检查倒计时是否结束?倒计时结束之前有没有被打断?在文件common/autoboot.c文件中定义,具体代码如下;

abortboot函数,在文件common/autoboot.c文件中定义,具体代码如下;

在倒计时结束之前有按键按下则执行函数 abortboot_single_key,abortboot_single_key函数在common/autoboot.c文件中定义,具体代码如下;

abortboot_single_key函数主要工作:

  • 1.倒计时的具体实现;
  • 2.判断键盘是否有按下,也就是是否打断了倒计时,如果键盘按下的话就执行相应的分支。比如设置abort为 1,设置 bootdelay为0等,最后跳出倒计时循环;
  • 3.返回abort的值,如果倒计时自然结束,没有被打断abort就为0,否则的话abort的值就为 1;
  • 4.在autoboot_command函数中,如果倒计时自然结束那么就执行函数run_command_list,此函数会执行参数s指定的一系列命令,也就是环境变量bootcmd的命令,bootcmd里面保存着默认的启动命令,因此linux内核启动!

上面给大家详细的讲解了各个函数的作用,以及调用关系。现在给大家总结一下,以流程框图的形式,展示u-boot启动流程;

版权声明


相关文章:

  • c解析json字符串2024-11-14 08:30:02
  • css中设置溢出隐藏2024-11-14 08:30:02
  • 使用ssh实现远程登录linux2024-11-14 08:30:02
  • 预测模型的构建及应用2024-11-14 08:30:02
  • sigfpe信号2024-11-14 08:30:02
  • 文件上传前端怎么写2024-11-14 08:30:02
  • android线程间的通讯2024-11-14 08:30:02
  • xml文件中注释怎么写2024-11-14 08:30:02
  • 小程序添加测试人员2024-11-14 08:30:02
  • 组策略教程2024-11-14 08:30:02