imx6ull uboot启动流程
首先,NXP提供的uboot经过编译最终烧写进存储介质中的是uboot.imx文件,这个imx后缀的文件不同于传统的比如S3C2440最终烧写的uboot.bin文件。
imx文件是在bin文件的基础上加上了一个头部,IMX6ULL芯片内部自带的BOOTROM程序会根据拨码开关的高低电平选择对应的启动介质,从中读取这个头部信息,然后对头部信息进行解析,头部中最重要的是一个叫DCD表的东西。
DCD表中包含了时钟寄存器的地址和寄存器的值,引脚复用寄存器地址和寄存器的值,DDR控制器的寄存器地址和寄存器的值。imx6ull内部的BOOTROM程序会根据DCD表的内容打开时钟,初始化外部DDR。因此NXP提供的uboot代码的汇编阶段没有初始化时钟和初始化DDR的相关汇编代码!这也是NXP的uboot和传统的三星提供的uboot的重大区别。
还有一个区别就是,IMX6ULL的BOOTROM程序会根据解析出来的链接起始地址在一开始就把整个Uboot源码读取到DDR中去,也就是说Uboot的第一行代码就运行在DDR中,这是不同于三星的传统Uboot的,传统Uboot的第一句代码是运行在片内SRAM上的。
由于上述的区别,Uboot的重定位过程也就不同了,IMX6ULL的重定位过程是把Uboot整体从DDR的起始地址给挪到DDR的后端地址上去,给Linux内核腾位置。而三星Uboot中重定位是从Flash中把Uboot加载到DDR中去,这也算是一个不同之处。
除了上述提到了几点不同以外,Uboot代码的其他部分功能基本大差不差。
为什么uboot先是在DDR起始地址运行,再挪到后面,不能把uboot一开始就搬到后面去?
DDR的起始地址(CONFIG_SYS_TEXT_BASE)程序运行前就可以知道,重定位后的地址 gd->relocaddr 需要根据自身大小等信息计算出来
为什么uboot要挪到DDR后端地址去
给kernel腾空间,kernel 运行的起始地址一般也是写死在DDR起始地址,因为这个地址对于kernel来说也是可以在运行前确定
IMX6ULL在uboot的汇编阶段要做的事情仅仅包含了:
一、设置CPU运行模式为SVC模式,关闭FIQ和IRQ中断。
设置成SVC模式是为了让CPU可以使用SoC的各种资源。
关中断因为启动过程不允许被打断,否则可能发生错误。
二、设置CP15协处理器的若干个寄存器,包括了设置异常向量表重定位并写入异常向量表的地址,失效Cache,关闭MMU,清除Cache。
重定位异常向量表是因为,我们知道异常向量表的地址是0x00000000,而Uboot是在DDR中运行的,DDR的起始地址肯定不是0x00000000,那么异常向量表必须重定位,这里只是配置好了重定位所需要的寄存器内容,实际的向量表重定位由后续的relocate_vector函数完成。
关闭MMU是因为,MMU负责虚拟地址到实际物理地址的转换,此时还没有加载操作系统,操作的都是实际物理地址,不需要MMU来转换。
清除Cache是因为,Cache的内容是从DDR中缓存过来的,起始阶段DDR中还没有我们加载的内容,此时从DDR中缓存内容到Cache中,如果CPU从Cache中取数据可能导致错误。
三、设置芯片内部的IRAM
划分出一部分用来作为堆栈(方便后续调用C函数),划分一部分用来存储uboot中的重要变量:struct global_data,这个结构体中包含了cpu的时钟频率信息,总线的时钟频率,uboot重定位的地址,外部DDR的大小,Uboot本身的大小的起始地址与终止地址,malloc内存池的大小和位置等等众多重要数据,这些结构体内部的变量是在后续的board_init_f()函数中被初始化的,这些变量被初始化完成以后,Uboot会根据其值进行重定位和一系列对外设的操作。
上述就是imx6ull的uboot在汇编阶段做的事情,下面在arch/arm/lib/crt0.S文件中的_main函数中会调用若干个C函数。
一、board_init_f_alloc_reserve,用于设置内部IRAM,划分出malloc区和存储global_data变量的区域,并将这个变量的地址写入R9寄存器中。
二、board_init_f_init_reserve,将上述函数划分的存储空间进行清零,把早期malloc区的地址写入到global_data结构体变量的malloc_base成员中去。
三、board_init_f,用于初始化部分外设和初始化global_data,这个函数里面有一个函数数组,函数数组中的函数会被依次执行,以此来实现初始化部分外设和global_data,这个函数数组在common/board_f.c文件中定义。这里初始化的外设主要是串口,定时器,初始化global_data主要是初始化其中的地址成员,比如uboot重定位以后的地址,malloc区的基地址,新的global_data变量的地址(因为刚开始global_data是放在内部的IRAM中的)。这个函数执行以后,外部DDR由原本的一张白纸变成了一段一段划分好的区域,每一段用于存储不同的内容,如下图所示:
四、 relocate_code,就是重定位代码,这个重定位是从DDR到DDR的重定位,因为对于imx6ull来说,一开始uboot就被加载到了DDR上去运行,重定位就是为了把DDR前面的位置空出来以加载Linux内核,该函数定义在文件 arch/arm/lib/relocate.S中。
五、 relocate_vectors,重定位向量表。
六、board_init_r函数,该函数和board_init_f一样,其中有一个函数数组,其中的函数会依次执行,这个函数的作用也是初始化外设,初始化那些在board_init_f函数中没有初始化过的外设, 比如该函数会初始化中断,网络信息,控制台以及存储设备等等。注意:函数数组中有一个叫board_init的函数,这个函数就是imx6ull的板级初始化函数,该函数定义在board/freescale/mx6ullevk/mx6ullevk.c文件中。我们进行Uboot移植的时候如果需要增减代码,基本就是在这个文件中进行代码的编辑。
至此,uboot启动的主要部分就结束了。
接下来进入交互界面等待命令,主要包含三个函数。
一、run_main_loop()--->main_loop()--->autoboot_command(),如果stored_bootdelay秒(默认是3秒)倒计时按下任意键,进入uboot的命令模式,否则启动Linux内核。
二、cli_loop,解析命令行命令函数,执行此函数说明进入uboot命令模式。
三、cmd_process,执行相应的命令。
uboot启动Linux内核的过程:
自动模式
如果倒计时结束未按下任意键,会执行环境变量bootcmd内的命令启动Linux内核,由下图可知,bootcmd的内容是宏CONFIG_BOOTCOMMAND定义的
而CONFIG_BOOTCOMMAND是在XXX_defconfig内定义的
run findfdt,使用的是 uboot 的 run 命令来运行 findfdt, findfdt 是 NXP 自行添加的环境变量,定义如下图。 findfdt 是用来查找开发板对应的设备树文件(.dtb)
最终分析下来,CONFIG_BOOTCOMMAND干的事情浓缩为以下四步:
CONFIG_BOOTCOMMAND还干了一件事,设置环境变量bootargs,传递给linux kernel
root=/dev/mmcblk1p2”表示根文件系统存储在 /dev/mmcblk1p2 中(即SD卡控制器2控制的SD卡或EMMC等设备的分区2)
命令模式
一般进入到uboot命令以后,我们会使用tftp命令或者nfs命令把zImage加载到DDR中去,然后使用bootz命令启动,使用bootz命令就调用了第一个函数。
do_bootz:这个函数主要干三件事。
第一,调用bootz_start函数,这个函数会调用do_bootm_states执行BOOTM_STATE_START阶段;
设置images的ep变量,即系统镜像的地址。
调用bootz_setup去验证镜像;最后调用bootm_find_images查找设备树文件,放在images->ft_addr成员变量中。
第二,关中断
第三,设置images结构体变量的os成员,这个成员也是个结构体变量,设置它为IN_OS_LINUX。然后执行do_bootm_states函数,该函数使用参数标识不同的启动阶段,此时的启动阶段为BOOTM_STATE_OS_PREP | BOOTM_STATE_OS_FAKE_GO |BOOTM_STATE_OS_GO。
do_bootm_states中会根据images.os.os这个系统类型来查找对应的系统启动函数,这里找到的是do_bootm_linux;do_bootm_linux函数最终会调用boot_jump_linux函数,这是uboot跳转到linux执行的最后一个函数。
boot_jump_linux函数调用了一个叫做kernel_entry的函数,这个函数是Linux内核定义的,kernel_entry是Linux内核镜像文件的第一行代码,地址为images->ep,该函数有三个参数,第一个参数是0,第二个参数是机器ID,第三个参数是ATAGS或者设备树首地址。
一旦开始执行kernel_entry,uboot的生命周期就结束了。
参考资料——《正点原子Linux驱动开发手册》
具体代码分析
编译完 uboot,uboot源代码根目录会生成链接脚本:u-boot.lds,里面指明了执行的第一个函数是_start
_start 位于arch\arm\lib\vectors.S,其最开始的代码如下:
所以先跳转到 reset 函数,位于arch\arm\cpu\armv7\start.S,第一段代码如下:
设置CPU工作模式为SVC(管理模式),禁止FIQ和IRQ中断
接下来执行:
设置 _start 作为异常向量表的首地址(由前面可知,_start其实就是复位函数,异常向量表就是记录各种异常中断函数指针,复位函数是第一个中断函数)
接下来执行:
无效数据和指令TLB,无效指令缓存,无效分支预测,无效 MUU等
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
2016-12-07 Raspberry Pi 3 --- identify the version of linux kernal file
2016-12-07 Linux C/C++ ------ “” and <> in the use of head include file(Pending Verification)