以下内容源于朱有鹏嵌入式课程的学习与整理,如有侵权请告知删除。
一、start_armboot函数简介
uboot第一阶段,start.S文件中进行一系列的SoC内部硬件的初始化,然后长跳转到start_armboot 函数中。
uboot第二阶段,start_armboot函数进一步初始化SoC外部硬件(比如inand,网卡芯片等),以及设置uboot本身内容(命令行、环境变量、基本命令等)。
uboot启动后自动运行,打印出很多信息(uboot在第一和第二阶段不断进行初始化时,打印出来的信息),然后uboot进入倒数bootdelay秒。如果用户没有干涉则会执行bootcmd进入自动启动内核流程(此时uboot就死掉了);如果用户按下回车键打断uboot的自动启动,则进入uboot的命令行,然后uboot就一直工作在命令行下。此时uboot的命令行就是一个死循环,循环体内不断重复:接收命令、解析命令、执行命令。
start_armboot函数在uboot/lib_arm/board.c文件中。
二、Start_armboot函数的解析1
start_armboot函数开头部分代码如下。
1、init_fnc_t类型
- 该类型由typedef int (init_fnc_t) (void)定义,是函数类型。
- init_fnc_ptr是一个二重函数指针,用来指向一个函数指针数组。
2、gd变量
(1)在\include\asm-arm\global_data.h中定义了变量gd,它是指针类型的。
- 用volatile修饰表示可变的,用register修饰表示这个变量要尽量放到寄存器中,后面的asm("r8")是gcc支持的一种语法,意思是把gd放到寄存器r8中。
- DECLARE_GLOBAL_DATA_PTR定义了一个要放在寄存器r8中的全局变量,名字叫gd,类型是一个指向gd_t类型变量的指针。
- gd是uboot中很重要的一个全局变量,在程序中经常被访问,放在register中可以提升效率。
(2)结构体gd_t,见博文:uboot源码——gd_t和bd_t数据结构_天糊土的博客-CSDN博客。
(3)将gd和gd->bd两指针指向的内存初始化为0。
3、for循环执行init_sequence
- init_sequence是一个函数指针数组,数组中存储了很多函数指针,指针指向的函数都是init_fnc_t类型(特征是接收参数是void类型,返回值是int)。
- init_sequence在定义时就同时给了初始化,初始化的函数指针都是一些函数名。
- init_sequence中的这些函数,都是board级别的各种硬件初始化。
- init_fnc_ptr是一个二重函数指针,可以指向init_sequence这个函数指针数组。
- init_fnc_t的这些函数的返回值定义方式一样的,函数执行正确时返回0,不正确时返回-1。
- 在遍历时去检查函数返回值,如果遍历中有一个函数返回值不等于0则hang()挂起。
- hang函数内部只有一行代码,即while(1)。可知uboot启动过程中初始化板级硬件时不能出任何错误,只要有一个错误整个启动就终止,除了重启开发板没有任何办法。
三、Start_armboot函数的解析2
下面开始分析init_sequence这个函数指针数组里面的函数。
1、cpu_init函数
- 因为cpu相关的初始化已经在start.S文件完成,所以这里什么也没有做。
2、board_init函数
(1)board_init函数在uboot/board/samsung/x210/x210.c文件中。此函数初始化了dm9000网卡,并且对gd->bd中的机器码、启动参数进行赋值。
- 因为有“#define MEMORY_BASE_ADDRESS 0x30000000”,则x210中bi_boot_params的值为0x30000100,它表示内核启动参数存放的首地址。
(2)CONFIG_DRIVER_DM9000宏在x210_sd.h中有定义,这个宏用来配置开发板的网卡。
(3)dm9000_pre_init函数是DM9000网卡的初始化函数。如果要移植网卡,主要的工作就在这里。这个函数中主要是网卡的GPIO和端口的配置,而不是驱动。
(4)MACH_TYPE在x210_sd.h中定义。值是2456,并没有特殊含义,只是代表x210这个开发板的机器码,将来在此开发板上面移植的linux内核中的机器码也必须是2456,否则就启动不起来。
3、interrupt_init函数
(1)这个函数实际是用来初始化定时器Timer4。
(2)S5PC11X_TIMERS定义了一个结构体类型,把与时钟有关的所有寄存器都存放在这个结构体内。
(3)S5PC11X_GetBase_TIMERS函数的作用:把timer寄存器的基地址强制类型转换为S5PC11X_TIMERS * 类型,然后赋值给 timers变量。
(4)timers->TCFG0=0x0f00,相当于把0x0f00这个值放到 TCFG0对应的寄存器里。寄存器必须设置为连续或者一一对应的,否则会造成赋值的地址错误。
(5)剩下的代码就和裸机的代码一致。
- TCON的timer4的相应控制位清0,设置为自动reload,并且第一次要手动载入,然后再清0,设置reload,开启timer4。
4、env_init函数
(1)此函数是与环境变量有关的初始化。
(2)为什么有很多env_init函数?uboot支持各种不同的启动介质,比如norflash、nandflash、inand、sd卡,一般从哪里启动就会把环境变量env放到哪里。不同启动介质存取env的方法是不一样的。uboot支持各种不同启动介质,所以有很多个env_xx开头的c文件。实际使用哪一个c文件,要根据自己开发板使用的存储介质来定。这些env_xx.c同时只有1个会起作用,其他是不能进去的,通过x210_sd.h中配置的宏来决定谁被包含的。x210对应的函数是env_movi.c中的函数。
(3)此函数把common.c中初始化好的default_environment地址赋值到gd->env_addr中,env_valid 赋值为1。此函数只是对uboot自带的env做了基本的初始化或者说是判定(判定里面有没有能用的环境变量)。因为当前还没有进行环境变量从SD卡到DDR中的重定位,因此当前的环境变量不能用。
(4)start_armboot调用env_relocate进行环境变量从SD卡中到DDR中的重定位。重定位之后需要环境变量时才可以从DDR中去取,重定位之前如果要使用环境变量只能从SD卡中去读取。uboot自带的环境变量以代码的形式存储在default_environmen[]数组中,是写死的,如果要修改,必须修改代码。后面随着uboot被加载到DDR中运行,从而在DDR中有了一份环境变量。实际上SD卡中的环境变量才是我们需要的,因此需要将SD卡中的环境变量重定位到DDR中,取代DDR中uboot自带的那一份环境变量。
5、init_baudrate函数
(1)此函数初始化波特率,即从环境变量中获取波特率,赋值给gd->bd->bi_baudrate、gd->baudrate。从之前的环境变量初始化函数看出,实际上环境变量中的波特率就是在x210_sd.h头文件中配置的波特率,即CONFIG_BAUDRATE。
(2)simple_strtoul函数作用是把字符串tmp中的波特率转成十进制数字。
(3)getenv_r函数跟踪。
- 上面函数作用:读取环境变量name到缓存buf中,读取成功返回n大于0,失败返回-1。
- 上面函数作用:判断环境变量是从内存还是从sd卡中赋值的,然后返回index对应的环境变量中的字符。
- 上面函数的作用:从内存中读取环境变量字符,作为返回值返回。
- 上面函数的作用是判断*s1,是否和i2对应的字符串相等,如果相等返回i2。
6、serial_init
(1)由于串口在start.S中已经初始化,这里不再进行初始化。
(2)有很多个serial_init函数,x210对应的是uboot/cpu/s5pc11x/serial.c中的serial_init函数。
(2)可以看出这函数中实际是调用了serial_setbrg函数,而这个函数什么也没有做。
7、console_init_f函数
(1)这是控制台的第一阶段的初始化,_f表示第一阶段,_r表示第二阶段。
(2)console_init_f函数在uboot/common/console.c中,仅仅将gd->have_console设置为1而已。
8、display_banner函数
(1)display_banner函数的作用:串口输出显示uboot的logo,以及打开背光。
(2)display_banner中使用printf函数向串口输出version_string这个字符串。根据上面的分析,console_init_f并没有初始化好console,为什么可以printf呢?
- 通过追踪printf的实现,发现printf->puts,而puts函数中会判断当前uboot中console有没有被初始化好。如果console初始化好了则调用fputs完成串口输出(这条线才是控制台);如果console尚未初始化好则会调用serial_puts,然后再调用serial_putc直接操作串口寄存器进行内容发送。
- 由此可知,uboot中控制台通过串口输出,非控制台也是通过串口输出。
- 究竟什么是控制台?和不用控制台的区别在于哪里?分析代码会发现,控制台就是一个用软件虚拟出来的设备,这个设备有一套专用的通信函数(发送、接收……),控制台的通信函数最终会映射到硬件的通信函数中来实现。uboot中,控制台的通信函数直接映射到硬件串口的通信函数中,即uboot中是否使用控制台其实并没有本质差别。
- 但是在别的体系中,控制台的通信函数映射到硬件通信函数时可以用软件来做一些中间优化,譬如说缓冲机制。
- 操作系统中的控制台都使用了缓冲机制,所以有时候printf了内容,但是屏幕上并没有看到输出信息,就是因为被缓冲了。此时输出的信息到了console的buffer中,buffer还没有被刷新到硬件输出设备上。这尤其体现在输出设备是LCD屏幕时。
(3)U_BOOT_VERSION在uboot源代码中找不到定义,这个变量实际上是在makefile中定义的,然后在编译时生成的include/version_autogenerated.h中用一个宏定义来实现的。
9、print_cpuinfo函数
(1)顾名思义,此函数实现打印cpu的一些信息的功能。如下:
(2)具体代码如下,包括get_ARMCLK函数、get_PLLCLK等几个函数。
(3)get_ARMCLK函数作用:查看时钟域24MHz经过APLL倍频以后,再经过分频器以后获得的cpu的频率。
(4)get_PLLCLK函数作用:获取PLL倍频以后的时钟频率:APLL、MPLL、 EPLL。
10、checkboard函数
- 打印“Board: x210”字符而已
11、init_func_i2c函数
(1)由于条件编译,此函数实际上没有执行。X210的uboot中并没有使用I2C。
(2)将来开发板如果要扩展I2C来外接硬件,则在x210_sd.h中配置相应的宏即可开启。有时间可以细细看一下。
12、dram_init函数
(1)真正的DDR初始化函数已经在汇编阶段中执行,此处只是把dram的信息赋值到全局变量gd->bd中,即把chip1的首地址和大小以及chip2的首地址和大小放入全局变量中。
(2)可以扩展chip3,只要定义相应的宏。
13、display_dram_config函数
(1)此函数作用是计算chip1、chip2一共多少内存并输出,即启动信息中的“DRAM: 512 MB”。
(2)uboot中有一个命令叫bdinfo,此命令可以打印出gd->bd中记录的所有与硬件相关的全局变量的值,因此可以得知DDR的配置信息。
四、Start_armboot函数的解析3
1、mem_malloc_init函数
(1)mem_malloc_init函数用来初始化uboot的堆管理器。malloc的初始化只设置了堆的start地址和end地址、以及一个malloc_brk。
(2)uboot中维护了一段堆内存,需要有一套代码来管理这个堆内存。在uboot中也可以malloc、free这套机制来申请内存和释放内存。我们在DDR内存中给uboot堆预留了896KB的内存。
2、mmc_initialize函数
(1)针对不同开发板进行对应的初始化。三星用一套uboot同时满足了好多个系列型号的开发板,用#if条件编译配合CONFIG_xxx宏来选定特定的开发板,然后进行独有的一些初始化。
(2)mmc_initialize用来初始化SoC内部的SD/MMC控制器,函数位于uboot/drivers/mmc/mmc.c。
(3)uboot中对硬件的操作(比如网卡、SD卡……)都是借用的linux内核中的驱动来实现的。uboot根目录底下有个drivers文件夹,这里面放的全都是从linux内核中移植过来的各种驱动源文件。
(4)mmc_initialize是与具体硬件架构无关的一个MMC初始化函数,所有的使用了这套架构的代码都可以调用此函数来完成MMC的初始化。mmc_initialize中再调用board_mmc_init和cpu_mmc_init来完成具体的硬件的MMC控制器初始化工作。cpu_mmc_init在uboot/cpu/s5pc11x/cpu.c中,其中又间接的调用了drivers/mmc/s3c_mmcxxx.c中的驱动代码来初始化硬件MMC控制器。
3、env_relocate函数
(1)环境变量的重定位,完成从SD卡中将环境变量读取到DDR中的任务。
(2)环境变量到底从哪里来?
- SD卡中有一些(8个)独立的扇区作为环境变量存储区域的。但是我们烧录/部署系统时,我们只是烧录了uboot分区、kernel分区和rootfs分区,根本不曾烧录env分区,所以当我们烧录完系统第一次启动时ENV分区是空的。
- 本次启动uboot时,尝试去SD卡的ENV分区读取环境变量时失败(读取回来后进行CRC校验时失败)。此时uboot选择uboot内部代码中设置的一套默认的环境变量(这就是默认环境变量);这套默认的环境变量在本次运行时会被读取到DDR中的环境变量中。
- 然后被写入(也可能是你saveenv时写入,也可能是uboot设计了第一次读取默认环境变量后就写入)SD卡的ENV分区。下次再次开机时uboot就会从SD卡的ENV分区读取环境变量到DDR中,这次读取就不会失败了。
(3)将环境变量从SD卡重定位到DDR中的代码,在env_relocate_spec内部的movi_read_env函数。
4、IP地址、MAC地址的确定和devices_init函数
(1)开发板的IP地址是在gd->bd中维护的,来源于环境变量ipaddr。
(2)getenv函数用来获取字符串格式的IP地址,然后用string_to_ip将字符串格式的IP地址转成字符串格式的点分十进制格式。
(2)devices_ini是设备的初始化函数。
- 放在这里初始化的设备都是驱动设备,这个函数本来就是从驱动框架中衍生出来的。
- uboot中很多设备的驱动是直接移植linux内核的(譬如网卡、SD卡),linux内核中的驱动都有相应的设备初始化函数。
- linux内核在启动过程中就有一个devices_init,作用就是集中执行各种硬件驱动的init函数。
- uboot的这个函数其实就是从linux内核中移植过来的,它的作用也是去执行所有的从linux内核中继承来的那些硬件驱动的初始化函数。
5、jumptable_init函数
(1)jumptable跳转表,本身是一个函数指针数组,里面记录了很多函数的函数名。实现一个函数指针到具体函数的映射关系,将来通过跳转表中的函数指针就可以执行具体的函数。这个其实就是在用C语言实现面向对象编程,在linux内核中有很多这种技巧。
(2)通过分析发现跳转表只是被赋值从未被引用,因此跳转表在uboot中根本就没使用。
6、console_init_r函数
(1)console_init_r是console的纯软件架构方面的初始化(给console相关的数据结构中填充相应的值),所以属于纯软件配置类型的初始化。
(2)uboot的console实际上并没有做有意义的事情,它直接调用的串口通信的函数。因此用不用console实际并没有什么分别(但在linux内console可以提供缓冲机制等作用,有不用console不能实现的东西)。
7、enable_interrupts函数
(1)CPSR中,总中断标志位的使能。
(2)因为uboot中没有使用中断,因此没有定义CONFIG_USE_IRQ宏,因此此函数无用。
8、loadaddr、bootfile两个环境变量
这两个环境变量都是内核启动有关的,在启动linux内核时会参考这两个环境变量的值。
9、board_late_init函数
(1)顾名思义,前面该初始化的都已经初始化,剩下的一些初始化都在此函数中,也侧面说明开发板级别的硬件软件初始化告一段落。
(2)对于x210来说,这个函数是空的。
10、eth_initialize函数
(1)此函数是网卡相关的初始化,是网卡芯片本身的一些初始化,而非SoC与网卡芯片连接时SoC这边的初始化。
(2)对于X210(DM9000)来说,此函数为空。X210的网卡初始化在board_init函数中,网卡芯片的初始化在驱动中。
11、x210_preboot_init函数(LCD和logo显示)
x210开发板在启动起来之前的一些初始化,以及LCD屏幕上的logo显示。
12、check_menukey_to_update_from_sd函数
(1)uboot启动的最后阶段设计了一个自动更新的功能。
- 我们可以将要升级的镜像放到SD卡的固定目录中,然后开机时在uboot启动的最后阶段检查升级标志(是一个按键,按键中标志为"LEFT"的那个按键,此按键如果按下则表示update mode,如果启动时未按下则表示boot mode)。
- 如果进入update mode则uboot会自动从SD卡中读取镜像文件然后烧录到iNand中;如果进入boot mode则uboot不执行update,直接启动正常运行。
(2)这种机制能够帮助我们快速烧录系统,常用于量产时用SD卡进行系统烧录部署。
13、main_loop函数
(1)解析器
(2)开机倒数自动执行
(3)命令补全
此函数的讲解,见博客:uboot源码——内核启动分析_天糊土的博客-CSDN博客。