引言
随着电子行业日新月异的发展,ARM处理器凭借自身高性能、低成本、低功耗等特点,广泛应用于消费电子、数字家电、工业生产等领域中[1]。CortexA8为Cortex系列中的“A”系列,采用ARM v7架构,主要面向搭载操作系统、高性能的应用领域。Bootloader是系统上电后执行的第一段代码,其功能类似于电脑的BIOS[2]。简单来说,Bootloader的工作主要有两点:其一是初始化底层硬件资源,为操作系统启动提供必要的环境;其二是从存储设备中读取操作系统镜像并启动[34]。但为了方便后期开发,往往要为Bootloader添加其他的功能,如支持串口打印调试信息、NFS网络下载、根文件系统烧写等[5]。本文以uboot2013.01为源码包,设计了一个基于S5PV210平台,功能齐全且高效稳定的Bootloader。
1 硬件平台
1.1 S5PV210简介
S5PV210是三星公司推出的一款基于CortexA8架构的高性能应用处理器。该处理器支持ARM v7指令集,具有32位内部总线结构,主频最高可达1 GHz,另外还支持挂接LPDDR1、LPDDR2和DDR2类型的RAM,Flash方面可选择NAND Flash或NOR Flash等。不仅如此,该处理器还提供了包括串口、LCD、I2C、SPI、USB、HDMI等丰富的外部接口资源。目前S5PV210已经广泛应用于智能手机和平板电脑之中。
1.2 S5PV210的启动方式
S5PV210支持多种启动方式,启动流程如图1所示。
图1 S5PV210的启动流程
S5PV210的启动过程由BL0、BL1和BL2(BL为Bootloader的简称)3部分代码实现,其中BL0在出厂时已经被固化到64 KB的iROM中。S5PV210上电后首先执行BL0,该段代码主要负责一些简单的初始化工作,如关看门狗、初始化ICache等;然后BL0会根据硬件设置判断使用何种方式启动,并将BL1从启动设备(OneNand、Nand Flash、USB、UART、SD card等)拷贝到Internal SRAM的BL1区中;最后BL0会比较一个校验值,如果相等则跳转到BL1中继续执行,否则转入其他启动方式。该校验值存在于BL1的头部,大小为16字节。
开发人员一般会单独编写一个工具为BL1编译出的二进制文件添加头部。其校验值(checksum)的计算方法如下:
a = Buf + SPL_HEADER_SIZE;
for(i = 0, checksum = 0; i < IMG_SIZE - SPL_HEADER_SIZE; i++) {
checksum += (0x000000FF) & *a++;
}
由于BL1的大小被限制为16 KB,所以一般情况下BL1负责的工作也不多。BL1被执行后首先初始化系统时钟、内存、串口等,然后将BL2代码拷贝到Internal SRAM的BL2区中并跳转执行。实际上SRAM的BL2区的大小只有80 KB,但很多情况下BL2代码的大小远远超过80 KB,所以将BL2代码拷贝到SRAM中意义不大。更好的做法是直接将BL2拷贝到容量更大的内存中,不过在拷贝之前一定要先初始化好系统时钟和内存。
BL2是整个Bootloader的主体部分,因此它需要完成更多的初始化工作,例如初始化网卡、Flash等,之后BL2读取操作系统镜像到内存中运行。一般把操作系统镜像放到Flash上,也可以放到SD卡上,根据具体的开发平台而定。
2 软件平台
2.1 UBoot介绍
UBoot(全称Universal Bootloader)是Bootloader的一种,其他常见的Bootloader还有RedBoot、vivi等。与它们相比,UBoot具有代码开源、可靠性和扩展性较高等优点。另外,对于类似于Nand Flash、Nor Flash、网卡、LCD等硬件资源,UBoot已经抽象出与硬件平台无关的代码作为设备驱动源码提供给开发者。对于与硬件平台相关的代码,UBoot将其定义成宏并保留在配置文件中,开发者往往只需要修改这些宏的值就能成功使用这些硬件资源,让移植工作变得十分简单。
2.2 UBoot中几个比较重要的源文件
UBoot的源码包含有上千个源文件,但经过分析之后,可以找出如下几个比较重要的源文件(括号中为该源文件所在源码包的位置):
(1) start.S(arch\\arm\\cpu\\armv7\\start.S)
通过分析UBoot的链接文件可以发现,start.S是UBoot上电后执行的第一个源文件。该源文件包括定义了异常向量入口、相关的全局变量、禁用L2缓存、关闭MMU等,之后跳转到lowlevel_init()函数中继续执行。
(2) lowlevel_init.S(board\\samsung\\smdkv210\\lowlevel_init.S)
该源文件用汇编代码编写,其中只定义了一个函数lowlevel_init()。该函数实现对平台硬件资源的一系列初始化过程,包括关看门狗、初始化系统时钟、内存和串口。
(3) mem_setup.S(board\\samsung\\smdkv210)
该源文件包含对内存进行初始化的汇编源码。
(4) board.c(arch\\arm\\lib\\board.c)
该源文件是用C语言编写的,主要实现了UBoot第二阶段启动过程,包括初始化环境变量、串口控制台、Flash和打印调试信息等,最后调用main_loop()函数。
(5) smdkv210.h(include\\configs\\Smdkv210.h)
S5PV210平台的配置文件,该源文件定义了一些与CPU或者外设相关的参数,这些参数都是用宏来定义的。
2.3 UBoot启动的一般流程
2.3.1 第一阶段初始化
UBoot的启动过程分为两个阶段:第一阶段主要由汇编代码实现,负责对CPU及底层硬件资源的初始化;第二阶段用C语言实现,负责使能Flash、网卡和引导操作系统等。第一阶段启动流程如图2所示。
图2 UBoot第一阶段启动流程
上电后,UBoot首先会设置CPU为管理模式、禁用L1缓存、关闭MMU和清除Caches,之后调用底层初始化函数lowlevel_init()。该函数实现略——编者注。
.globl lowlevel_init
lowlevel_init:
push{lr}
#if defined(CONFIG_SPL_BUILD)
/*初始化时钟 */
blsystem_clock_init
/*初始化内存 */
blmem_ctrl_asm_init
/*初始化串口 */
bluart_asm_init
#endif
pop{pc}
上述代码中system_clock_init(), mem_ctrl_asm_init(),uart_asm_init()这三个函数需要开发者结合具体硬件环境进行修改和实现。
初始化完成之后,UBoot首先调用一个拷贝函数将BL2拷贝到内存地址为0x 3FF0 0000处,然后跳转到该位置执行BL2。在UBoot中,BL1和BL2是基于相同的一些源文件编译生成的。开发者在编写代码时需要使用预编译宏CONFIG_SPL_BUILD来实现BL1和BL2不同的功能。其拷贝函数实现略——编者注。
void copy_code_2_sdram_and_run(void)
{
unsigned long ch;
void (*u_boot)(void);
ch = *(volatile unsigned int *)(0xD0037488);/*根据该地址的值判断传输通道 */
/* copy_bl2()函数不需要开发者去实现,S5PV210在出厂时已经固化在了0xD0037F98地址处 */
copy_sd_mmc_to_mem copy_bl2 = (copy_sd_mmc_to_mem) (*(unsigned int *) (0xD0037F98));
unsigned int ret;
if (ch == 0xEB000000) { /* CONFIG_SYS_TEXT_BASE = 0x3FF00000 */
ret = copy_bl2(0, 49, 1024,(unsigned int *)CONFIG_SYS_TEXT_BASE, 0);
} else if (ch == 0xEB200000) {
ret = copy_bl2(2, 49, 1024,(unsigned int *)CONFIG_SYS_TEXT_BASE, 0);
} else {
return;
}
u_boot = (void *)CONFIG_SYS_TEXT_BASE;
(*u_boot)(); /*跳转到该地址执行 */
}
值得注意的是以上代码中,copy_bl2()函数不需要开发者去实现,S5PV210在出厂时已经将该函数固化在了0xD0037F98地址处。其函数原型如下:
u32 (*copy_sd_mmc_to_mem)(u32 channel, u32 start_block, u16 block_size, u32 *trg, u32 init);/*
参数介绍:
channel:通道数2或0,该值通过读取0xD0037488地址上的值判断。
start_block:从第几个扇区开始拷贝,一个扇区为512byte。
block_size:拷贝多少个扇区,这里拷贝512K
trg:目的地址:0x3FF00000,即离内存顶部1M的位置
init:是否需要初始化SD卡,写0即可。*/
2.3.2 第二阶段初始化
进入第二阶段后,UBoot首先声明一个gd_t结构体类型的指针指向内存地址(0x 4000 0000~GD_SIZE)处。0x 4000 0000为内存结束地址,GD_SIZE为结构体gd_t的大小,这样相当于在内存最顶端分配了一段空间用于存放一个临时结构体gd_t。该结构体在global_data.h中被定义,UBoot用它来存储所有的全局变量。之后UBoot会调用board_init_f()和board_init_r()两个函数进一步对底板进行初始化。
(1) board_init_f()
进入board_init_f()之后,UBoot首先设置之前分配的临时结构体,然后开始划分内存空间,其内存分配状态如图3所示。
图3 UBoot内存分配状态
从图3中可以看到,gd指针指向的临时结构体存放在内存的最顶部。BL2代码存放在内存地址0x 3ff0 0000处,即距离内存顶部1 MB空间的位置,接下来依次分配malloc空间、bd_t结构体空间和gd_t结构体空间,并且重新设置栈,最后将临时结构体拷贝到ID指针所指向的位置。board_init_f()实现过程略——编者注。
unsigned int board_init_f(ulong bootflag) {
memset((void *)gd, 0, sizeof(gd_t));
...
设置gd结构体;
...
addr = CONFIG_SYS_TEXT_BASE;/* CONFIG_SYS_TEXT_BASE = 0x3ff00000000 */
addr_sp = addr - TOTAL_MALLOC_LEN;
addr_sp -= sizeof (bd_t);
bd = (bd_t *) addr_sp;
gd->bd = bd;
addr_sp -= sizeof (gd_t);
id = (gd_t *) addr_sp;
...
memcpy(id, (void *)gd, sizeof(gd_t));
base_sp = addr_sp;
return (unsigned int)id;
}
(2) board_init_r()
board_init_r()负责对其他硬件资源进行初始化,如网卡、Flash、MMC、中断等,最后调用main_loop(),等待用户输入命令。
3 设计实现
3.1 支持Nand Flash读写
Nand Flash是嵌入式系统中重要的存储设备,存储对象包括Bootloader、操作系统内核、环境变量、根文件系统等,所以使能Nand Flash读写是UBoot移植过程中必须完成的一个步骤。UBoot中Nand Flash初始化函数调用关系为:board_init_r()->nand_init()->nand_init_chip()->board_nand_init()。
board_nand_init()完成两件事:①对S5PV210关于Nand Flash控制器的相关寄存器进行设置。②对nand_chip结构体进行设置。需要设置的成员项有IO_ADDR_R和IO_ADDR_W,这两个成员都指向地址0x B0E0 0010,即Nand Flash控制器的数据寄存器的地址。此外还需要实现以下3个成员函数:
① void (*select_chip)(struct mtd_info *mtd, int chip);
该函数实现Nand Flash设备选中或取消选中。
② void (*cmd_ctrl)(struct mtd_info *mtd, int dat, unsigned int ctrl);
该函数实现对Nand Flash发送命令或者地址。
③ int (*dev_ready)(struct mtd_info *mtd);
该函数实现检测Nand Flash设备状态。最后将成员ecc.mode设置为NAND_ECC_SOFT,即ECC软件校验。
配置文件中相应的宏定义略——编者注。
3.2 支持网卡
支持NFS或TFTP网络下载会极大地方便从Linux服务器上下载文件或镜像到硬件平台上,所以使能网卡在UBoot移植过程中就显得非常重要。以网卡DM9000为例,UBoot已经抽象出一套完整的关于DM9000的驱动代码(其源码路径为drivers\\net\\dm9000x.c),用户只需要根据具体的硬件电路配置相应的宏即可。UBoot中DM9000网卡初始化函数的调用关系为:board_init_r()->eth_initialize()->board_eth_init()->dm9000_initialize()。
配置文件中相应的宏定义略——编者注。
3.3 支持环境变量的保存和修改
为了方便用户配置,UBoot将一部分变量,如串口波特率、IP地址、内核参数、启动命令等存在Flash或SD卡上,这部分数据称为环境变量。每次上电启动时,UBoot会检查Flash或SD卡上是否存放有环境变量。如果有则将其读取出来并使用,如果没有就使用默认的环境变量。默认的环境变量定义在env_default.h中,用户也可以随时修改或保存环境变量到Flash或SD卡中。
环境变量的移植非常简单。以Nand Flash为例,开发人员在smdkv210.h源文件中只需要添加如下的宏定义:
#define CONFIG_ENV_IS_IN_NAND
/* 告诉Makefile环境变量保存在Nand Flash中 */
#define CONFIG_ENV_OFFSET 0x80000/* 环境变量保存的Nand Flash中的偏移地址 */
#define CONFIG_ENV_SIZE 0x20000/*环境变量的大小 */
#define CONFIG_ENV_OVERWRITE
/*规定环境变量和覆盖 */
4 测试结果
4.1 测试UBoot启动
测试平台的软硬件环境:硬件平台为友善之臂的smart210开发板;Linux服务器版本为ubuntu9.0;UBoot版本为201301;GCC交叉编译工具链版本为4.3.2。
首先将修改后的源码包放到装有交叉编译工具链的Linux服务器上,输入命令make即可以编译生成两个二进制文件。一个是smdkv210spl.bin,即BL1代码,存放在spl目录下;另一个是uboot.bin,即BL2代码,存放在顶层目录下。然后插入SD卡,执行write2sd.sh脚本就可以把BL1和BL2分别烧写到SD卡的第1扇区和第49扇区。write2sd.sh脚本实现如下:
#!/bin/sh
sudo dd iflag=dsync oflag=dsync if=spl/smdkv210-spl.bin of=/dev/sdc seek=1
sudo dd iflag=dsync oflag=dsync if=u-boot.bin of=/dev/sdc seek=49
烧写完成之后将SD卡插入到smart210开发板的SD卡槽中,连接好串口和网线,上电启动。通过串口工具(本次测试使用的是SecureCRT)看到输出信息,即表示该UBoot已经成功运行在开发板上,测试UBoot启动图略——编者注。
4.2 测试Nand Flash和网卡
在等待命令状态下输入“nfs 20000000 192.168.1.123:/work/nfs_root/uImage”,该命令表示从IP为192.168.1.123的Linux服务器上,通过NFS下载该服务器上“/work/nfs_root”目录下的uImage,并存放在内存地址为0x 2000 0000处,uImage为适配于S5PV210开发板的Linux内核镜像。等待一段时间后看到“Bytes transferred = 2127008 (2074a0 hex)”字样,表示下载成功。
下载成功之后输入命令“nand erase.part kernel”,该命令表示擦除Nand Flash上的kernel分区。接着输入命令“nand write 2000 0000 kernel”,该命令表示拷贝内存地址为0x 2000 0000的内容,烧写到Nand Flash的kernel分区中。内存地址0x 2000 0000存放的内容正是之前从Linux服务器上下载下来的uImage。kernel保存在环境变量mtdparts中,kernel规定了该分区的起始地址和大小。执行完这两条命令后看到“4194304 bytes written: OK”字样,表示系统内核已经成功烧写到Nand Flash中,证明了Nand Flash和网卡均可以正常使用。其实验结果略——编者注。
4.3 测试引导内核启动
烧写完内核之后执行boot命令就可以启动该内核。boot命令实际上是执行环境变量bootcmd中的一条语句,其内容为“nand read 2000 0000 kernel; bootm 20000000”,该语句表示从Nand Flash的kernel分区中读出内核并存放在内存地址为0x 2000 0000处,然后跳转在该地址执行内核镜像。启动过程中可以看到串口打印出的信息,证明了该UBoot已经成功支持引导Linux操作系统。测试引导内核启动图略——编者注。
结语
本文首先从硬件和软件两个方面分析了S5PV210的特性及启动方式,然后通过uboot2013.01源码包详细阐述了UBoot启动过程中的两个阶段,最后结合smart210开发板成功设计了一个基于SD卡启动、多功能的Bootloader。测试结果表明,该Bootloader支持Nand Flash读写、NFS网络下载、环境变量保存和修改等功能,为后期开发带来了极大的便利。