不同芯片的 uboot 不同
uboot 内容不同
- NXP 提供的 uboot 经过编译最终烧写进存储介质中的是 uboot. imx 文件,
- 传统的比如 S3C2440 最终烧写进存储介质中的是 uboot. bin 文件。
imx 文件是在 bin 文件的基础上加上了一个头部。
头部的内容:
- IVT
- DCD:Device Configuration Data 设备配置数据(DCD)
- DCD 表中包含了时钟寄存器的地址和寄存器的值,引脚复用寄存器地址和寄存器的值,DDR 控制器的寄存器地址和寄存器的值。
- imx6ull 内部的 BOOTROM (iROM) 程序会根据 DCD 表的内容打开时钟,初始化外部 DDR。
- 这样boot ROM程序就会帮我们初始化DDR和其他硬件,然后才可以把bin程序读到DDR中并运行。
NOTE因此 NXP 提供的 uboot 代码的汇编阶段没有初始化时钟和初始化 DDR 的相关汇编代码!
这也是 NXP 的 uboot 和传统的三星提供的 uboot 的重大区别。
不同uboot 启动过程概要
主要区别
参考:
《韦东山u-boot完全分析和移植》03.1B_答疑_哔哩哔哩_bilibili
第一个重要区别:
- IMX6ULL 的 BOOTROM 程序会 UBoot 前面的 DCD(ddr 配置信息)直接初始化 ddr,并根据解析出来的链接起始地址在一开始就把整个 Uboot 源码读取到 DDR 中去,所以Uboot 的直接就运行在 DDR 中。
- 三星的传统 Uboot 是 BootRom 先将 Uboot 前面的 4 K 的程序读入片上 SRAM,由这个程序来初始化 DDR,再将后面的程序读入 ddr 运行,所以 Uboot 先运行在片上 SRAM,再运行在 DDR 中。
第二个重要区别:
- 由于上述的区别,Uboot 的重定位过程也就不同了,IMX6ULL 的重定位过程是把 Uboot 整体从 DDR 的起始地址给挪到 DDR 的后端地址上去,给 Linux 内核腾位置。
- 而三星 Uboot 中重定位是从 Flash 中把 Uboot 加载到 DDR 中去。
除了上述提到了几点不同以外,Uboot 代码的其他部分功能基本大差不差。
IMX6ULL Uboot 启动方式概要
不需要特地记忆具体都做了什么,只要知道整个不同阶段的流程以及运行在哪,如何进入下个阶段即可。
I .MX6ULL 的最终可烧写文件组成
①、 Image vector table,简称 IVT, IVT 里面包含了一系列的地址信息,这些地址信息在 ROM 中按照固定的地址存放着。
②、 Boot data,启动数据,包含了镜像要拷贝到哪个地址,拷贝的大小是多少等。
③、 Device configuration data,简称 DCD,设备配置信息,重点是 DDR 3 的初始化配置。
BootRom
- 运行介质:CPU 片上 SRAM
- 为什么: 在系统初始化时,cpu 只能访问可以直接寻址的存储器,如 ROM。而像 SPI FLASH 或 NAND FLASH 等外部存储器,都需要相应的驱动才可以访问,故在启动的最初阶段 cpu 无法访问。因此 cpu 执行的第一级启动镜像一般都是产商固化在 SOC 内部存储器(通常为 ROM)上。
- 功能:
- imx6ull内部的BOOTROM程序会根据DCD表的内容打开时钟,初始化外部DDR。
- ==因此NXP提供的uboot代码的汇编阶段没有初始化时钟和初始化DDR的相关汇编代码!====这也是NXP的uboot和传统的三星提供的uboot的重大区别。==
- DCD中列出的是对某些寄存器的读写操作,我们可以在DCD中设置DDR控制器的寄存器值,可以在DCD中使用更优的参数设置必需的硬件。这样boot ROM程序就会帮我们初始化DDR和其他硬件,然后才可以把bin程序读到DDR中并运行。
- IMX6ULL的BOOTROM程序会根据解析出来的链接起始地址在一开始就把整个Uboot源码读取到DDR中去,
- 也就是说Uboot的第一行代码就运行在DDR中,这是不同于三星的传统Uboot的,传统Uboot的第一句代码是运行在片内SRAM上的。
- imx6ull内部的BOOTROM程序会根据DCD表的内容打开时钟,初始化外部DDR。
BL1(汇编阶段)
源码 U-boot/arch/arm/cpu/xxx(armv7)/start.S
- 运行介质:外部 DDR
- 为什么: IMX6ULL 中板子上电后 boot ROM程序会从启动设备上读出DCD数据,根据DCD 初始化了 ddr
- 功能:
- 用汇编, 是因为机器刚开始就从某个内存地址开始取值操作, 这个时候内存没有初始化好, 汇编不需要堆栈
- 硬件设备初始化(关闭看门口, 关中断, 设置CPU的速度和时钟频率, RAM初始化)
- 为加载第二阶段的 Bootloader 的代码准备 RAM 空间(初始化RAM芯片,使可用, 调用lowlevel_init使外接SDRAM可用)
- 复制 Bootloader 的第二阶段代码到 RAM 空间
- 设置好栈
- 跳转到第二阶段代码的入口 C 处
BL2(C 语言阶段)
源码 start.S 中, 初始化相关硬件之后, 会跳转 _main
, 文件名: arch/arm/lib/crt0.S
- 运行介质:外部 DDR
- 为什么: IMX 6 ULL 中板子上电后 boot ROM 程序会从启动设备上读出 DCD 数据,根据 DCD 初始化了 ddr
- 功能:
- 主要完成板级初始化、emmc初始化、控制台初始化、中断初始化及网络初始化等
- 初始化本阶段要使用的硬件设备(CPU中断, 系统时钟,定时器, 检查flash, 串口初始化, 检测系统内存映射)
- 检测系统内存映射
- 将内核映像和根文件系统映像从 flash 上读到 RAM 空间中
- 为内核设置启动参数
- 调用内核, 至少初始化串口用于调试
传统芯片 Uboot 启动方式概要
U-Boot启动流程:
- 三个阶段:BootROM (bl0) —> SPL (bl1) —> U-Boot (bl2)
BL0(BootRom)
- 运行介质:CPU 片上SRAM
- 为什么: 在系统初始化时,cpu 只能访问可以直接寻址的存储器,如 ROM。而像 SPI FLASH 或 NAND FLASH 等外部存储器,都需要相应的驱动才可以访问,故在启动的最初阶段 cpu 无法访问。因此 cpu 执行的第一级启动镜像一般都是产商固化在 SOC 内部存储器(通常为 ROM)上。
- 功能:主要负责加载 SPL (b1) 代码到芯片内部sram(Internal SRAM)中运行
BL1 (SPL)
- 运行介质:CPU 片上 SRAM
- 功能:初始化一些基本硬件
- 1)设置cpu的状态,如cache,mmu,大小端设置等
- 2)准备c语言的执行环境,它包括设置栈指针和清空BSS段的内容
- 3)为GD分配内存空间
- 4)初始化RAM,并将BL2的代码拷贝到RAM中执行
- 最关键的是初始化 ddr(外部 ram),然后加载 U-Boot(bl2) 到 ddr 上运行。
BL2(Uboot)
运行介质:DDR 上
功能:Uboot 的完整功能,完成全面的硬件初始化和加载OS到内存中,接着运行OS.
IMX6ULL Uboot 详细启动过程(待完成)
参考:u-boot启动流程分析-史上最全最详细_u-boot 2023-CSDN博客
一些主要问题
- 为什么 uboot 先是在 DDR 起始地址运行,再挪到后面,不能把 uboot 一开始就搬到后面去?
- DDR 的起始地址(CONFIG_SYS_TEXT_BASE)程序运行前就可以知道,重定位后的地址 gd->relocaddr 需要根据自身大小等信息计算出来
- 为什么 uboot 要挪到 DDR 后端地址去
- 给 kernel 腾空间,kernel 运行的起始地址一般也是写死在 DDR 起始地址,因为这个地址对于 kernel 来说也是可以在运行前确定
BootRom
BL1(汇编阶段)
一、设置 CPU 运行模式为 SVC 模式,关闭 FIQ 和 IRQ 中断。
设置成 SVC 模式是为了让 CPU 可以使用 SoC 的各种资源。
关中断因为启动过程不允许被打断,否则可能发生错误。
二、设置 CP 15 协处理器的若干个寄存器,包括了设置异常向量表重定位并写入异常向量表的地址,失效 Cache,关闭 MMU,清除 Cache。
重定位异常向量表是因为,我们知道异常向量表的地址是 0 x 00000000,而 Uboot 是在 DDR 中运行的,DDR 的起始地址肯定不是 0 x 00000000,那么异常向量表必须重定位,这里只是配置好了重定位所需要的寄存器内容,实际的向量表重定位由后续的 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 会根据其值进行重定位和一系列对外设的操作。
BL2 (C 语言阶段)
上述就是 imx 6 ull 的 uboot 在汇编阶段做的事情,下面在 arch/arm/lib/crt 0. S 文件中的_main 函数中会调用若干个 C 函数。
一、board_init_f_alloc_reserve,用于设置内部 IRAM,划分出 malloc 区和存储 global_data 变量的区域,并将这个变量的地址写入 R 9 寄存器中。
二、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 的重定位,因为对于 imx 6 ull 来说,一开始 uboot 就被加载到了 DDR 上去运行,重定位就是为了把 DDR 前面的位置空出来以加载 Linux 内核,该函数定义在文件 arch/arm/lib/relocate. S 中。
五、 relocate_vectors,重定位向量表。
六、board_init_r 函数,该函数和 board_init_f 一样,其中有一个函数数组,其中的函数会依次执行,这个函数的作用也是初始化外设,初始化那些在 board_init_f 函数中没有初始化过的外设,比如该函数会初始化中断,网络信息,控制台以及存储设备等等。注意:函数数组中有一个叫 board_init 的函数,这个函数就是 imx 6 ull 的板级初始化函数,该函数定义在board/freescale/mx 6 ullevk/mx 6 ullevk. 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/mmcblk 1 p 2”表示根文件系统存储在 /dev/mmcblk 1 p 2 中(即 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 驱动开发手册》