cortex-a8 uboot系列:第七章 uboot源码分析3-启动第二阶段
0赞一、 start_armboot解析2
init_fnc_ptr是init_fnc_t函数类型的二重指针。
对于init_sequence,是一个函数指针数组。里面保存了多个函数指针,每个函数指针指向一个函数。不过函数的类型是固定的,为init_fnc_t函数类型(特征是输入参数为void,返回值为int)。
init_sequence在定义时就同时给了初始化,初始化的函数指针都是一些函数名。
init_fnc_ptr是一个二重函数指针,可以指向init_sequence这个函数指针数组。
这个地方的技巧就是将所有初始化函数都遍历依次执行。而不用单独的去执行初始化函数然后判断是否初始化成功。
遍历函数指针数组方法:
第一种:用下标去遍历,用数组元素个数来截止。
第二种:数组的有效元素末尾放一个标志(如NULL),依次遍历到标志处即可截止。(和字符串遍历思路一样)。
Uboot使用的第二种方法。因为数组中存的全是函数指针,因此选用NULL作为结束标志。遍历时从头依次进行,直到看到NULL标志截止。这种方法的优势是不用知道数组的大小个数是多少。
使用(*init_fnc_ptr)()执行对应的初始化函数,这里不能写成*init_fnc_ptr(),因为()优先级比*高,就会变成*(init_fnc_ptr()),执行就会未知。
函数执行成功,会返回0。否则会返回1。如果返回1。会去执行hang函数。该函数就是打印错误信息,然后进入一个死循环。
Init_sequence中的这些函数,都是board级别的各种硬件初始化。
初始化函数解析:
1. cpu_init
CPU内部的初始化。因为CPU相关的初始化在之前已经初始化了,所以这里为空。
2. board_init
该函数在board\samsung\x210\X210.c文件中。X210开发板相关的初始化。
DECLARE_GLOBAL_DATA_PTR申明全局变量gd指针。而gd的声明定义为一个宏的原因就是程序会经常用到gd,因此就要到处进行申明,定义成宏比较方面。
判断是否要用到DM9000网卡,要用的话,先对DM9000进行初始化。CONFIG_DRIVER_DM9000宏在x210_sd.h中定义。用来配置开发板的网卡。dm9000_pre_init函数在board\samsung\x210\X210.c程序中。
SROM_BW寄存器,DM9000使用的是通道1的SROM,对于这个通道要进行设置,如位宽,地址模式等等。
SROM_BC1寄存器是设置SROM通道1的时序。这个要根据DM9000的手册来设置。
MP01CON,设置MP0_1的GPIO,设置片选。
对于网卡,实际上是使用了的soc内部的SROM控制器来进行驱动的,因此对于网卡驱动,首先要对内部的SROM控制器进行初始化,使程序能通过SROM控制器来驱动外部的网卡,然后在对外部的网卡进行初始化。
这一步是对内部的SROM控制器进行初始化。
以后要移植uboot时,如果要移植网卡,主要的工作就是这里。这个函数主要是网卡的GPIO和SROM接口的配置,而不是驱动。因为网卡的驱动都是现成可用的,移植的时候驱动是不需要改动的,关键是这里的基本初始化。因为这些基本初始化和硬件相关的。
给bd的机器码bi_arch_number赋值,bi_arch_number是board_info中的一个元素,含义是:开发板的机器码。所谓机器码就是uboot给这个开发板定义的一个唯一编号。
机器码的主要作用就是在uboot和linux内核之间进行比对和适配。
嵌入式设备中每一个设备的硬件都是定制性的,不能通用。嵌入式设备的高度定制化导致硬件和软件不能随便适配使用。这就告诉我们:这个开发板移植的内核镜像不能下载到另一个开发板去,否则会工作不正常。因此linux做了个设置:给每个开发板做个唯一编号(机器码),然后再uboot、内核中都有一个软件维护的机器码编号。然后开发板、uboot、linux三者之间去比对这个机器码,如果机器码对上了就启动,否则就不启动(因为软件认为和硬件不适配)。
Uboot的机器码就是保存在变量bi_arch_number中。MACH_TYPE在x210_sd.c中定义,值没有特殊含义,只是当前开发板对应的编号。这个编号就代表了x210这个开发板的机器码。将来这个开发板上面移植的linux内核中的机器码也必须是MACH_TYPE。
Uboot中配置的这个机器码,会作为uboot给linux内核传参的一部分传给linux内核,内核启动过程中会比对这个接收到的机器码,和自己本身的机器码相比对,如果相等就启动,如果不等就不启动。
Uboot只是定义了机器码,而没有使用这个机器码。机器码的使用是在linux中使用的。
理论来说,一个开发板的机器码不能自己随便定(因为可能会有不同的开发板使用的机器码是一样的)。理论上来说有权利去发放机器码的只有uboot官方,所以做好一个开发板并且移植了uboot之后,理论上应该提交给uboot官方审核并发放机器码。
存放启动参数的地址bi_boot_params赋值。bi_boot_params也是bd_info中另一个主要元素。表示uboot给linux kernel启动时的传参的地址。
Uboot给linux内核传参过程:uboot实现将准备好的传参(字符串,就是bootargs)放在内存的一个地址处(就是bi_boot_params),然后uboot就启动了内核(uboot在启动内核时真正是通过寄存器r0,r1,r2来直接传递参数),其中有一个寄存器就是bi_boot_params参数值。
内核启动后,从寄存器中读取bi_boot_params,就知道了uboot给内核传递的参数保存在内存的什么地方,然后自己去内存的那个地方去找bootargs。
计算可得到启动参数存放的地址是0x3000_0100。也就是uboot会将启动参数保存在0x3000_0100开始的地址处。启动内核的时候,内核会从这个地址去读取需要的启动参数。
在uboot其他地方使用内存时,要注意不要对这块内存操作,以免破坏启动参数。
3. interrupt_init
在cpu\s5pc11x\interrupts.c程序中。
看函数名似乎和中断初始化有关,实际上不是。实际上这个函数是用来初始化定时器的。实际初始化的是pwm timer4。
210共有5个PWM timer,其中timer0-timer3可以由输出管脚,但是timer4没有输出管脚。
这个timer4用来做计时功能。会用到两个寄存器,TCNTB4、TCNTO4。TCNTB4决定定时时长,TCNTO4表示当前计数值。通过读取TCNTO4寄存器的值是否减到0,就可知道计时时间是否已到。
使用timer4来定时,因为没有中断支持,所以CPU要使用轮询方式来不断查看TCNTO寄存器才能知道定时时间是否到达。
Uboot中定时就是使用timer4来实现定时的,所以uboot中定时时,是不能做其他事(典型下,就是bootdelay,bootdelay中定时时并且检查用户输入时用轮询方式实现的)。
定义pwm timers指针timers,使用S5PC11X_GetBase_TIMERS()获取pwm timer的基地址,这个函数最终返回ELFIN_TIMER_BASE的值,也就是PWM timer的基地址。
而S5PC11X_TIMERS是一个结构体,包括了PWM的所有寄存器。
对于__attribute__((packed))
设置PWM的time4的分频值是16。所以此时timer4的时钟是PCLK/16。
这部分是计算定时10ms,timer4应该定时的值。使用get_PCLK()获取PCLK的时钟频率。
读取CLK_DIV0寄存器的值,得到PCLK对HCLK的分频值。然后再获取HCLK,就可以知道PCLK的频率。HCLK的获取也是同样的道理。
获取到PCLK的频率后,除以16得到timer4的计时时钟。根据该时钟及定时时长,就可以计算得到应该计数的值。
装载TCNTB4寄存器的值为刚刚计算得到的值。然后对TCON寄存器进行设置。更新TCNTB4值,然后开启timer4,并启动auto-reload模式。
这个interrupt_init函数,其实就是给pwm timer4设置为定时10ms。并开启该定时器。
4. env_init
和环境变量有关的初始化函数。common\env_movi.c程序中。
这个函数在很多文件有定义。原因是uboot支持各种不同的启动介质(如norflash,nandflash,inand,sd卡……),从什么地方启动,就将环境变量env放到哪里。而各种介质存取操作env的方法是不一样的。因此uboot支持了各种不同介质中env的操作方法,所以有好多个env_xx.c文件。xx就是启动介质。
实际使用的是哪一个要根据自己开发板使用的存储介质来决定(这些env_xx.c同时只有1个会起作用,其他是不作用的,通过x210_sd.h中配置的宏来决定谁被包含使用),对于九鼎开发板,使用的是env_movi.c程序中的env_init。
这个函数只是对内存里维护的那一份uboot的env做了基本的初始化及判定,判断env中有没有能用的环境变量。因为此时还没有进行环境变量从SD卡到DDR中的relocate,因此当前环境变量时不能使用的。
在start_armboot函数中调用env_relocate后才进行环境变量从SD卡到DDR中的重定位。重定位之后需要环境变量时才可从DDR中去读取,重定位之前只能从SD卡中去读取环境变量。
5. init_baudrate
初始化串口通信的波特率,在lib_arm\Board.c程序中。其实就是修改gd和gd下的bd的波特率变量。
getenv_r函数获取环境变量的值。
该函数接收3个参数,第一个参数是环境变量的名字,第二个是保存环境变量的值(字符串表示),第三个是保存值字符串的长度。
返回读取环境变量值的字符串的长度。
使用getenv_r读取baudrated值,读出来的值是以字符串形式保存在tmp字符数组中,然后使用simple_strtoul函数将字符串波特率转化为数字波特率值。然后将值赋值给gd的baudrate变量和gd下的bd下的baudrate变量。
如果读取不成功,就将x210_sd.h中的CONFIG_BAUDRATE的值作为波特率。因此环境变量的优先级是很高的。
6. serial_init
串口初始化,在cpu\s5pc11x\Serial.c程序中。
调用serial_setbrg,该函数就在当前程序中。
这个函数就用for循环实现延时。
7. console_init_f
对于函数中有_r,说明该函数是第一阶段初始化。_r表明函数是第二阶段初始化。有时候初始化函数不能一次一起完成,中间会有些其他操作,因此将完成的一个模块的初始化分成了两部分。
这里,就是对控制台的第一阶段初始化。在common/console.c程序中。
仅仅对gd的have_console变量设置为1。表示使用控制台。
8. display_banner
打印uboot的标牌。在lib_arm\board.c程序中。
关键就是第一行的打印。其他的内容都是DUBUG用的。在include/Common.h中定义了debug。
而Board.c中,最开始就取消定义了DEBUG宏。所以debug宏就是空。
所以这个函数就是打印了uboot的标牌version_string。
U_BOOT_VERSION在uboot中找不到定义,这个字符串实际上时在makefile中定义的。在主makefile中,会设置变量U_BOOT_VERSION,然后将该变量写入到一个文件中。当make后,会在include目录下生成一个文件version_autogenerated.h,这个文件中就定义了U_BOOT_VERSION宏。所以程序中将该文件include进来后,就可以使用U_BOOT_VERSION宏了。
__DATA__ : 编译时的日期
__TIME__ : 编译时的时间
通过追踪printf的实现,发现最终调用的是puts函数。
puts函数中,会判断console是否初始化好,初始化好就调用fputs完成串口数据发送,没有初始化好,调用serial_puts串口输出。
在cpu/s5pc11x/Serial.c中。
在common/Console.c中,使用stdio_device中的puts进行输出。
控制台是通过串口输出,非控制台也是通过串口输出。但是两者应该是有区别。通过uboot代码分析,控制台就是一个用软件虚拟出来的设备,这个设备有一套专用的通信函数(发送、接收),控制台的通信函数最终会映射到硬件的通信函数中来实现。Uboot中实际上控制台的通信函数是直接映射到硬件串口的通信函数中的,也就是说uboot中有没有用控制台并没有什么区别。
在别的体系中,控制台的通信函数映射到硬件通信函数是可以用软件做一些中间优化,如缓冲机制。(操作系统中控制台都使用了缓冲机制,所以有时候printf了内容但是屏幕没有看到输出,就是因为被缓冲了。输出信息只是到了console的buffer中,buffer还没有被刷新到硬件输出设备上,尤其是在输出设备是LCD屏幕时)
9. print_cpuinfo
打印CPU的信息。cpu\s5pc11x\s5pc110\Speed.c程序中
Uboot启动过程中都是print_cpuinfo函数打印出来的
根据定义的时钟宏,设定速度为多少。官方推荐使用ARMCLK(1000M),HCLK_MSYS(200M),HCLK_DSYS(166),HCLK_PSYS(133M),uboot使用这个设置。
使用get_ARMCLK()函数获取ARMCLK时钟。
通过get_PLLCLK()函数,获取PLL的时钟。可以获取APLL,MPLL,EPLL的输出频率。
判断获取哪一个PLL的输出频率,从而从响应的寄存器去读取m,p,s参数。然后根据公式计算。
外部时钟FIN的时钟频率由宏CONFIG_SYS_CLK_FREQ定义,该宏在x210_sd.h中。
最后计算tmp = 100, 所以result_set = 1, 时钟设置正确。
然后根据判断CLK设置是否正确,打印OK还是FALL。利用同样的道理,读取其他时钟,然后打印。
10. checkboard
检查开发板,打印开发板的相关信息。 board/samsung/x210/X210.c
11. init_func_i2c
CONFIG_HARD_I2C: 使用IIC控制器
CONFIG_SOFT_I2C: 不使用IIC控制器,使用GPIO来模拟IIC
X210_sd.h中没有定义CONFIG_S3C64XX_I2C宏,所以CONFIG_HARD_I2C也没有定义。所以init_func_i2c并没有执行。
Uboot中没有使用IIC,以后需要使用IIC的话,可以修改宏,使之将init_func_i2c函数包含进来执行。
12. dram_init
初始化DDR软件部分。board/samsung/x210/x210.c,其实就是设置gd中的关于DRAM的全局变量参数。使gd->bd数据记录下当前开发板的DDR信息。以后程序中通过读取DRAM的全局变量,就知道外部DRAM的信息。
这里的初始化DDR和汇编阶段lowlevel_init中初始化DDR时不同的。那时是硬件的初始化,目的是让DDR可以工作。现在是软件结构中一些DDR相关的属性配置、地址设置的初始化、是纯软件层面。
软件层次初始化DDR的原因:对于uboot来说,是怎么知道开发板上有几片DDR内存,每一片的起始地址、长度这些信息呢?
在uboot中涉及中采用了一种简单直接有效的方式:在x210_sd.c中使用宏定义去配置板子的DDR信息,然后uboot只要读取这些信息即可。还有另外一种方法:就是uboot通过代码读取硬件信息来指导DDR配置,但是uboot没有这样。PC的BIOS采用的是这种方法)。
在x210_sd.c中,使用几个宏进行了定义。
定义了几个信息:板子使用了几片DDR,每一片DDR的起始地址、长度。这里的配置信息在uboot代码使用内存时就可以从这里提取使用。
13. display_dram_config
打印显示DRAM的配置信息。lib_arm/board.c
程序中没有定义宏DEBUG,执行#else部分的程序。Uboot就会打印DRAM的大小。
print_size函数,将输入参数1的进行匹配,按照GB,MB,KB的形式将大小转化为字符串并打印。
在uboot中,使用bdinfo命令,可以得到bd的信息。
可以看到有两个DRAM bank。每一块有起始地址,大小。
以上,就是所有的init_sequence数组中的初始化函数。也就是以下这部分代码。
init_sequence总结:
都是板级硬件的初始化以及gd、gd->bd中的数据结构的初始化。如网卡初始化、机器码(gd->bd->bi_arch_number),内核启动参数的地址(gd->bd->bi_boot_params)、PWM timer4初始化定时10ms,波特率设置(gd->bd->bi_baudrate和gd->baudrate),console第一阶段初始化(gd->have_console设置为1)、打印uboot启动信息、打印CPU相关设置信息、板级信息、DDR配置信息初始化(gd->bd->bi_dram)、打印DDR信息。