Uboot启动的第二阶段
一、第二阶段该做哪些以及Start_armboot
从第一阶段可知,第一阶段主要初始化了SOC内部的一些片内外设(看门狗、时钟等),然后初始化DDR并完成了重定位。那么第二阶段做啥?第二阶段主要初始化SOC外部的硬件(iNand、网卡芯片、LCD控制器等)和uboot本身的一些东西(uboot的命令、环境变量等....),初始化完必要的东西后进入uboot的命令准备接受命令。第二阶段归结何处?Uboot打印相应的信息后,倒数bootdelay秒后,如果用户没有按下回车键打断,则自动执行bootcmd对应的启动命令,uboot就死掉了。如果被打断,uboot则一直在命令行下工作,uboot的命令行就是一个死循环,重复:接收命令、解释命令、执行命令。
Start_armboot这个函数构成整个uboot的第二阶段启动。
二、Start_armboot的解析
1. 开头定义 init_fnc_t **init_fnc_ptr;
init_fnc_t的定义 typedef int (init_fnc_t) (void); 由此可知,这是一个函数类型。init_fnc_ptr是一个二重函数指针。
二重函数指针的作用:用来指向一重指针和指向指针数组。所以这里的init_fnc_ptr用来指向函数指针数组。
2.DECLARE_GLOBAL_DATA_PTR宏定义
#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r8")
定义了一个指针类型的全局变量gd,占4字节。用volatile修饰表示可变的,用register修饰表示这个变量要尽量放到寄存器中。asm ("r8")是GCC支持的一种语法,意思是把gd放到寄存器r8中。
为什么要定义register? gd是uboot中很重要的一个全局变量(结构体),这个变量经常被访问,以此放到register中来提升效率。
typedefstructglobal_data{
bd_t *bd;
unsigned long flags;
unsigned long baudrate;
unsigned long have_console; /* serial_init() was called */
unsigned long reloc_off; /* Relocation Offset */
unsigned long env_addr; /* Address of Environment struct */
unsigned long env_valid; /* Checksum of Environment valid? */
unsigned long fb_base; /* base address of frame buffer */
#ifdef CONFIG_VFD
unsigned char vfd_type; /* display type */
#endif
#if 0
unsigned long cpu_clk; /* CPU clock in Hz! */
unsigned long bus_clk;
phys_size_t ram_size; /* RAM size */
unsigned long reset_status; /* reset status register at boot */
#endif
void **jt; /* jump table */
} gd_t;
typedef struct bd_info {
int bi_baudrate; /* serial console baudrate */
unsigned long bi_ip_addr; /* IP Address */
unsigned char bi_enetaddr[6]; /* Ethernet adress */
struct environment_s *bi_env;
ulong bi_arch_number; /* unique id for this board */
ulong bi_boot_params; /* where this board expects params */
struct /* RAM configuration */
{
ulong start;
ulong size;
} bi_dram[CONFIG_NR_DRAM_BANKS];
#ifdef CONFIG_HAS_ETH1
/* second onboard ethernet port */
unsigned char bi_enet1addr[6];
#endif
} bd_t;
gd_t定义在include/asm-arm/global_data.h中。
bd_t定义在uboot/include/asm-arm/U-boot.h中。
bd_t是开发板信息的结构体,是一些硬件相关的参数,如波特率、ip地址、机器码、DDR内存分布。
3.为什么要内存分布?
DECLARE_GLOBAL_DATA_PTR只能定义了一个指针,也就是说gd里面的成员并没有被分配内存,所以需要分配内存给gd和bd。由于当前内存没有操作系统管理,所以大量的DDR存放着随意使用,只要使用内存地址直接访问即可。gd = (gd_t*)gd_base;
但是后面的操作的还需要内存块,所以这里最好不随意使用,应该紧凑排布。
gd_base = CFG_UBOOT_BASE + CFG_UBOOT_SIZE - CFG_MALLOC_LEN - CFG_STACK_SIZE - sizeof(gd_t);
内存的排布:
uboot区 CFG_UBOOT_SIZE uboot的实际长度
堆区 CFG_MALLOC_LEN 912kb
栈区 CFG_STACK_SIZE 512KB
gd sizeof(gd_t) 36字节
bd sizeof(bd_t) 44字节左右
内存间隔 为了防止高版本的gcc的优化造成错误
4.SoC外部初始化
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
if ((*init_fnc_ptr)() != 0) {
hang ();
}
}
init_fnc_ptr前面已经提过了,它是一个二重指针,用来指向函数指针数组。init_sequence是啥?init_fnc_t *init_sequence[] = {......} init_sequence就是一个函数指针数组,数组中存着许多的函数指针(init_fnc_t函数类型的指针)
cpu_init 里面为空的函数
board_init在uboot/board/samsung/x210/x210.c目录下,dm9000_pre_init()就是网卡初始化。
gd->bd->bi_arch_number = MACH_TYPE:开发板的机器码。这里的MACH_TYPE是自己定的。这个机器码作为uboot给linux内核的传参的一部分传给Linux内核,内核启动过程中会对比这个机器码,和自己本身的机器码对比,如果相等就启动,如果不相等则不启动。
gd->bd->bi_boot_params = (PHYS_SDRAM_1+0x100); bi_boot_params表示uboot给linux内核启动时传参的内存地址。Uboot给Linux内核传参:uboot事先将准备好的传参(字符串,就是bootargs)放在内存的地址处,就是bi_boot_params,uboot在内核启动时通过r0,r1,r2来直接传递参数的,其中有一个寄存器中就是bi_boot_params。内核启动后从寄存器中读取bi_boot_params就知道了uboot传递的参数到底在内存的哪里。然后自己去内存的那个地方去找bootargs。
X210中bi_boot_params的值为0x30000100。
interrupt_init 用来初始化定时器Timer4,x210共有5个PWM定时器。其中Timer0-timer3都有对应的PWM信号输出引脚。而Timer4没有引脚,无法输出PWM波形。Timer4在设计的时候就不是用来输出PWM波形的(没有引脚,没有TCMPB寄存器),这个定时器被设计用来做计时。TCNTB4固定次数,TCNTO4计数。
使用Timer4来定时,因为没有中断支持,CPU只能使用轮询方式来不断查看TCNTO寄存器才能知道定时时间到了没。
env_init 和环境变量有关的初始化。uboot支持各种不同的启动介质,如norflash、nandflash、inand、sd卡......一般从哪里启动就会把环境变量env放到哪里,而各种介质存取操作env的方法都是不一样的。因此uboot支持了各种不同介质中env的操作方法。所以有好多个env_xx开头的c文件。实际使用的是哪一个要根据自己开发板使用的存储介质来定,对于x210,看env_movi.c中的函数。等到env_relocate()函数执行时,才将SD中的环境变量读取到DDR,所以在重定位之前,如果用到环境变量,就到SD卡里读取。
#else /* ENV_IS_EMBEDDED */
gd->env_addr = (ulong)&default_environment[0];
gd->env_valid = 1;
是默认使用系统的环境变量。
init_baudrate 用来初始化串口通信波特率。getenv_r函数是用来读取环境变量baudrate的值(字符串类型),然后用simple_strtoul函数将字符串转成数字格式的波特率。
int i = getenv_r ("baudrate", tmp, sizeof (tmp));
gd->bd->bi_baudrate = gd->baudrate = (i > 0)
? (int) simple_strtoul (tmp, NULL, 10)
: CONFIG_BAUDRATE;
baudrate初始化时的规则是:先去环境变量中读取"baudrate"这个环境变量的值。如果读取成功则使用这个值作为环境变量,记录在gd->baudrate和gd->bd->bi_baudrate中;如果读取不成功则使用x210_sd.h中的的CONFIG_BAUDRATE的值作为波特率。
serial_init (uboot/cpu/s5pc11x/serial.c)串口初始化。其实什么也没做,因为在汇编阶段已经被初始化过了。
console_init_f (uboot/common/console.c)
int console_init_f (void)
{
gd->have_console = 1;
#ifdef CONFIG_SILENT_CONSOLE
if (getenv("silent") != NULL)
gd->flags |= GD_FLG_SILENT;
#endif
return (0);
}
console_init_f是console(控制台)的第一阶段初始化。_f表示是第一阶段初始化,_r表示第二阶段初始化。有时候初始化函数不能一次一起完成,中间必须要夹杂一些代码,因此将完整的一个模块的初始化分成了2个阶段。
display_banner
(1)display_banner用来串口输出显示uboot的logo
(2)display_banner中使用printf函数向串口输出了version_string这个字符串。那么上面的分析表示console_init_f并没有初始化好console怎么就可以printf了呢?
(3)通过追踪printf的实现,发现printf->puts,而puts函数中会判断当前uboot中console有没有被初始化好。如果console初始化好了则调用fputs完成串口发送(这条线才是控制台);如果console尚未初始化好则会调用serial_puts(再调用serial_putc直接操作串口寄存器进行内容发送)。
(4)控制台也是通过串口输出,非控制台也是通过串口输出。究竟什么是控制台?和不用控制台的区别?实际上分析代码会发现,控制台就是一个用软件虚拟出来的设备,这个设备有一套专用的通信函数(发送、接收···),控制台的通信函数最终会映射到硬件的通信函数中来实现。uboot中实际上控制台的通信函数是直接映射到硬件串口的通信函数中的,也就是说uboot中用没用控制器其实并没有本质差别
(5)但是在别的体系中,控制台的通信函数映射到硬件通信函数时可以用软件来做一些中间优化,譬如说缓冲机制。(操作系统中的控制台都使用了缓冲机制,所以有时候我们printf了内容但是屏幕上并没有看到输出信息,就是因为被缓冲了。我们输出的信息只是到了console的buffer中,buffer还没有被刷新到硬件输出设备上,尤其是在输出设备是LCD屏幕时)
(6)U_BOOT_VERSION在uboot源代码中找不到定义,这个变量实际上是在makefile中定义的,然后在编译时生成的include/version_autogenerated.h中用一个宏定义来实现的。
print_cpuinfo
printf("\nCPU:S5PV210@%ldMHz(%s)\n",get_ARMCLK()/1000000, ((result_set == 1) ? "OK" : "FAIL"));
输出CPU: S5PV210@1000MHz(OK)
ulong get_ARMCLK(void)
{
ulong div,apll_ratio;
div = CLK_DIV0_REG;
apll_ratio = ((div>>0) & 0x7);
return ((get_PLLCLK(APLL)) / (apll_ratio + 1));
}
printf(" APLL = %ldMHz, HclkMsys = %ldMHz, PclkMsys = %ldMHz\n",
get_FCLK()/1000000,get_HCLK()/1000000, get_PCLK()/1000000);
输出:APLL = 1000MHz, HclkMsys = 200MHz, PclkMsys = 100MHz
printf(" MPLL = %ldMHz, EPLL = %ldMHz\n",
get_MPLL_CLK()/1000000, get_PLLCLK(EPLL)/1000000);
printf(" HclkDsys = %ldMHz, PclkDsys = %ldMHz\n",
get_HCLKD()/1000000, get_PCLKD()/1000000);
printf(" HclkPsys = %ldMHz, PclkPsys = %ldMHz\n",
get_HCLKP()/1000000, get_PCLKP()/1000000);
printf(" SCLKA2M = %ldMHz\n", get_SCLKA2M()/1000000);
MPLL = 667MHz, EPLL = 96MHz
HclkDsys = 166MHz, PclkDsys = 83MHz
HclkPsys = 133MHz, PclkPsys = 66MHz
SCLKA2M = 200MHz
Checkboard 检查开发板(哪个开发板)。printf("\nBoard: X210\n");
init_func_i2c
#if defined(CONFIG_HARD_I2C) || defined(CONFIG_SOFT_I2C)
static int init_func_i2c (void)
{
puts ("I2C: ");
i2c_init (CFG_I2C_SPEED, CFG_I2C_SLAVE);
puts ("ready\n");
return (0);
}
#ifdef CONFIG_S3C64XX_I2C
#define CONFIG_HARD_I2C 1 由于此条件的限制,所以此函数在实际中并未执行。如果要用IIC,在x210_sd.h中配置相应的宏。
实验烧写过程:
第一步:进入sd_fusing目录下
第二步:make clean
第三步:make
第四步:插入sd卡,ls /dev/sd*得到SD卡在ubuntu中的设备号(一般是/dev/sdb,注意SD卡要连接到虚拟机ubuntu中,不要接到windows中)
第五步:./sd_fusing.sh /dev/sdb完成烧录(注意不是sd_fusing2.sh)
dram_init
int dram_init(void)
{
DECLARE_GLOBAL_DATA_PTR;
gd->bd->bi_dram[0].start = PHYS_SDRAM_1;
gd->bd->bi_dram[0].size = PHYS_SDRAM_1_SIZE;
#if defined(PHYS_SDRAM_2)
gd->bd->bi_dram[1].start = PHYS_SDRAM_2;
gd->bd->bi_dram[1].size = PHYS_SDRAM_2_SIZE;
#endif
#if defined(PHYS_SDRAM_3)
gd->bd->bi_dram[2].start = PHYS_SDRAM_3;
gd->bd->bi_dram[2].size = PHYS_SDRAM_3_SIZE;
#endif
return 0;
}
在汇编阶段和第二阶段的前面,都已经初始并分配栈了,从代码上看,其实就是初始化gd->bd->bi_dram这个结构体数组。
display_dram_config
如何在uboot运行中得知uboot的DDR配置信息?uboot中有一个命令叫bdinfo,这个命令可以打印出gd->bd中记录的所有硬件相关的全局变量的值
size = flash_init ();
display_flash_config (size);
但是实际上X210中是没有Norflash的。所以着两行代码是可以去掉的(我也不知道为什么没去掉?猜测原因有可能是去掉着两行代码会导致别的地方工作不正常。
mem_malloc_init
mem_malloc_init (CFG_UBOOT_BASE + CFG_UBOOT_SIZE - CFG_MALLOC_LEN - CFG_STACK_SIZE);
(1)mem_malloc_init函数用来初始化uboot的堆管理器。
(2)uboot中自己维护了一段堆内存,肯定自己就有一套代码来管理这个堆内存。有了这些东西uboot中你也可以malloc、free这套机制来申请内存和释放内存。我们在DDR内存中给堆预留了896KB的内存。
mmc_initialize(gd->bd)
这是开发板特有的初始化。(uboot/drivers/mmc/mmc.c)
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控制器。
env_relocate
(1)env_relocate是环境变量的重定位,完成从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的代码是在env_relocate_spec内部的movi_read_env完成的。
void env_relocate_spec (void)
{
#if !defined(ENV_IS_EMBEDDED)
uint *magic = (uint*)(PHYS_SDRAM_1);
if ((0x24564236 != magic[0]) || (0x20764316 != magic[1]))
movi_read_env(virt_to_phys((ulong)env_ptr));
if (crc32(0, env_ptr->data, ENV_SIZE) != env_ptr->crc)
return use_default();
#endif /* ! ENV_IS_EMBEDDED */
}
serial_initialize() 串口控制器初始化。
/* IP Address */
gd->bd->bi_ip_addr = getenv_IPaddr ("ipaddr");
/* MAC Address */
{
int i;
ulong reg;
char *s, *e;
char tmp[64];
i = getenv_r ("ethaddr", tmp, sizeof (tmp));
s = (i > 0) ? tmp : NULL;
for (reg = 0; reg < 6; ++reg) {
gd->bd->bi_enetaddr[reg] = s ? simple_strtoul (s, &e, 16) : 0;
if (s)
s = (*e) ? e + 1 : e;
}
开发板的IP地址是在gd->bd中维护的,来源于环境变量ipaddr。getenv函数用来获取字符串格式的IP地址,然后用string_to_ip将字符串格式的IP地址转成字符串格式的点分十进制格式。
devices_init 设备初始化
jumptable_init
jumptable跳转表,本身是一个函数指针数组。跳转表只是被赋值从未被引用,因此跳转表在uboot中根本就没使用。
console_init_r
console_init_f是控制台的第一阶段初始化,console_init_r是第二阶段初始化。实际上第一阶段初始化并没有实质性工作,第二阶段初始化才进行了实质性工作。uboot的console实际上并没有干有意义的转化,它就是直接调用的串口通信的函数。
enable_interrupts
#ifdef CONFIG_USE_IRQ
/* enable IRQ interrupts */
void enable_interrupts(void)
{
unsigned long temp;
__asm__ __volatile__("mrs %0, cpsr\n" "bic %0, %0, #0x80\n" "msr cpsr_c, %0":"=r"(temp)
::"memory");
}
(1)看名字应该是中断初始化代码。这里指的是CPSR中总中断标志位的使能。
(2)因为我们uboot中没有使用中断,因此没有定义CONFIG_USE_IRQ宏。
loadaddr、bootfile两个环境变量
这两个环境变量都是内核启动有关的
/* Initialize from environment */
if ((s = getenv ("loadaddr")) != NULL) {
load_addr = simple_strtoul (s, NULL, 16);
}
#if defined(CONFIG_CMD_NET)
if ((s = getenv ("bootfile")) != NULL) {
copy_filename (BootFile, s, sizeof (BootFile));
}
board_late_init 一些较后的初始化
eth_initialize(gd->bd)
网卡芯片本身的一些初始化。对于X210(DM9000)来说,这个函数是空的。X210的网卡初始化在board_init函数中,网卡芯片的初始化在驱动中。
extern int x210_preboot_init(void);
x210_preboot_init();
x210开发板在启动起来之前的一些初始化,以及LCD屏幕上的logo显示。
/* check menukey to update from sd */
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卡进行系统烧录部署。
main_loop
(1)解析器
(2)开机倒数自动执行
(3)命令补全
uboot启动2阶段总结
(1)第二阶段主要是对开发板级别的硬件、软件数据结构进行初始化。
(2)
init_sequence
cpu_init 空的
board_init 网卡、机器码、内存传参地址
dm9000_pre_init 网卡
gd->bd->bi_arch_number 机器码
gd->bd->bi_boot_params 内存传参地址
interrupt_init 定时器
env_init
init_baudrate gd数据结构中波特率
serial_init 空的
console_init_f 空的
display_banner 打印启动信息
print_cpuinfo 打印CPU时钟设置信息
checkboard 检验开发板名字
dram_init gd数据结构中DDR信息
display_dram_config 打印DDR配置信息表
mem_malloc_init 初始化uboot自己维护的堆管理器的内存
mmc_initialize inand/SD卡的SoC控制器和卡的初始化
env_relocate 环境变量重定位
gd->bd->bi_ip_addr gd数据结构赋值
gd->bd->bi_enetaddr gd数据结构赋值
devices_init 空的
jumptable_init 不用关注的
console_init_r 真正的控制台初始化
enable_interrupts 空的
loadaddr、bootfile 环境变量读出初始化全局变量
board_late_init 空的
eth_initialize 空的
x210_preboot_init LCD初始化和显示logo
check_menu_update_from_sd 检查自动更新
main_loop 主循环
启动过程特征总结
(1)第一阶段为汇编阶段、第二阶段为C阶段
(2)第一阶段在SRAM中、第二阶段在DRAM中
(3)第一阶段注重SoC内部、第二阶段注重SoC外部Board内部
移植时的注意点
(1)x210_sd.h头文件中的宏定义
(2)特定硬件的初始化函数位置(譬如网卡)