之前介绍了 uboot 启动第一阶段,现在介绍启动的第二阶段

启动阶段的工作

启动第一阶段的主要工作是对处理器的内部资源(如时钟、串口)、内存(ddr)初始化,并进行 uboot 的重定位,并跳转到启动第二阶段

启动第二阶段的主要工作则是对处理器的外部资源(iNand、网卡芯片...)、uboot环境(uboot命令、环境变量..)等初始化,并等待命令输入

工作流程

正常情况下,在 uboot 的初始化工作完毕后,会启动内核,在启动内核后结束 uboot 程序。

但是用户可以阻止 uboot 的结束,进入 uboot 的命令行模式,就是一个 uboot 中的死循环;uboot 在死循环中不断接受命令、解析命令、执行命令

start_armboot

如果说启动第一阶段主要工作是 lowlevel_init 完成的,那么启动第二阶段的主要工作是 start_armboot 完成的

相关变量介绍

init_fnc_ptr

它是一个 init_fnc_t 类型的函数指针数组,通过 typedef int (init_fnc_t) (void); 定义

gd

1
#define DECLARE_GLOBAL_DATA_PTR     register volatile gd_t *gd asm ("r8")

gd 这个变量通过 DECLARE_GLOBAL_DATA_PTR 定义,这个变量的作用是用来存储 uboot 需要使用到的全局变量,这样可以减少全局变量的数量,方便他人阅读代码。具体的作用可以从结构体成员中得知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef	struct	global_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
void **jt; /* jump table */
} gd_t;

gd 变量的结构体成员的功能基本可以从名字和注释中得知,不过对第一个成员 bd 的描述很少,这里再从 bd_t 中获取信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;

从 bd_t 的定义可以看出, bd 这个成员变量的主要作用是用来保存一些和板级相关的信息,如波特率、ip 地址等

因为 gd 这个变量需要被频繁访问,所以使用了 volatile ,避免编译器做出不适当的优化;另外使用 register asm ("r8") 加速访问

内存排布

上面提到的 gd bd 指针变量只是声明,此时还不能使用,需要给他们分配内存空间

分配内存的原则就是够用、紧凑,所以首先需要做的是为 gd bd 分配基地址

在 uboot 中计算 gd 和 bd 的基地址的方式如下

1
2
3
4
5
6
gd_base = CFG_UBOOT_BASE + CFG_UBOOT_SIZE - CFG_MALLOC_LEN - CFG_STACK_SIZE - sizeof(gd_t);
#ifdef CONFIG_USE_IRQ
gd_base -= (CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ);
#endif
...
gd->bd = (bd_t*)((char*)gd - sizeof(bd_t));

其中 CFG_UBOOT_BASE=0x33e00000 CFG_UBOOT_SIZE=2MB CFG_MALLOC_LEN=912KB CFG_STACK_SIZE=512KB gd_t=36字节 bd_t≈44字节

从而可以得到内存分配如图

部分外设初始化工作

1
2
3
4
5
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
if ((*init_fnc_ptr)() != 0) {
hang ();
}
}

这里初始化工作是通过一个 for 循环完成的,使用先前定义的 init_fnc_ptr 变量去指向一个预先定义的函数指针数组 init_sequence ,在 init_sequence 中存储了用来初始化外设的函数指针。通过 for 循环依次执行每一个初始化函数,一旦返回的值不为0,就停止程序的运行并报错

init_sequence 的定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
init_fnc_t *init_sequence[] = {
cpu_init, /* basic cpu dependent setup */
board_init, /* basic board dependent setup */
interrupt_init, /* set up exceptions */
env_init, /* initialize environment */
init_baudrate, /* initialze baudrate settings */
serial_init, /* serial communications setup */
console_init_f, /* stage 1 init of console */
display_banner, /* say that we are here */
#if defined(CONFIG_DISPLAY_CPUINFO)
print_cpuinfo, /* display cpu info (and speed) */
#endif
#if defined(CONFIG_DISPLAY_BOARDINFO)
checkboard, /* display board info */
#endif
#if defined(CONFIG_HARD_I2C) || defined(CONFIG_SOFT_I2C)
init_func_i2c,
#endif
dram_init, /* configure available RAM banks */
display_dram_config,
NULL,
};

cpu_init

cpu_init 只有当允许了中断的情况下才会做中断栈的设置工作,因为我们 uboot 中没有允许中断,所以这里相当于什么都没做

board_init

这里进行了网卡的初始化,本人使用的开发板使用的网卡芯片是 dm9000 ,所以使用了 dm9000_pre_init 进行预初始化操作

网卡的驱动是不需要修改的,关键是移植,dm9000_pre_init 需要做的事就是移植相关,就像 stm32 一样,使用 GPIO 前,需要进行初始化的工作,这里就是做这些的工作

除了网卡的初始化,这里还在 bd 中添加了机器码和参数内存地址

机器码是用来软件和开发板之间配对用的,避免将软件下载到错误的开发板导致损失。机器码是唯一的,需要向 uboot 进行申请,当然学习 uboot 的时候可以随意填写,只要 uboot 中配置的机器码和 linux 内核中的机器码一致即可

这里的参数地址中存放的是需要想内核中传送的字符串参数(bootargs)的地址, uboot 启动的时候是通过 r0 r1 r2 来传递参数,其中一个寄存器放的就是 bd->bi_boot_params 中的值

interrupt_init

这里是对定时器4做的相关初始化,进行了10ms的定时

env_init

这里是环境变量的初始化,通过 x210_sd.h 中定义的宏 CFG_ENV_IS_IN_AUTO 分析可以知道,env_init 应该使用的是 env_auto.c 中定义的 env_init

init_baudrate

首先是从 uboot 环境变量中读取波特率,如果环境变量中没有定义波特率就使用 x210_sd.h 定义的波特率

uboot 环境变量可以在 uboot 中使用 print 查看

serial_init

串口初始化,主要工作在汇编中完成了,这里没有做什么事

console_init_f

控制台初始化第一阶段,一般第一阶段后缀为 _f,第二阶段后缀为 _r

display_banner

用来输出 uboot 的 logo

1
2
const char version_string[] =
U_BOOT_VERSION" (" __DATE__ " - " __TIME__ ")"CONFIG_IDENT_STRING;

uboot 启动时输出的 version_string 是定义在主 Makefile 中的,在编译时自动生成的 include/version_autogenerated.h 就会包含 U_BOOT_VERSION 相关信息

print_cpuinfo

这里可以输出 CPU 的相关信息

1
2
3
4
5
6
CPU:  S5PV210@1000MHz(OK)
APLL = 1000MHz, HclkMsys = 200MHz, PclkMsys = 100MHz
MPLL = 667MHz, EPLL = 96MHz
HclkDsys = 166MHz, PclkDsys = 83MHz
HclkPsys = 133MHz, PclkPsys = 66MHz
SCLKA2M = 200MHz

dram_init

这里做的是 ddr 软件方面的初始化

因为嵌入式设备是定制性的,不像 PC 机都是标准化的,可以自动获取 ddr 的片数、大小

因此在 uboot 中,需要人为添加 ddr 的相关配置信息,相当于是将 ddr 的配置信息写入 gd->bd->bi_dram 这个数组中

display_dram_config

打印 dram 信息

堆管理器初始化

1
mem_malloc_init (CFG_UBOOT_BASE + CFG_UBOOT_SIZE - CFG_MALLOC_LEN - CFG_STACK_SIZE);

这样堆的地址如下图

开发板专有初始化

为了保证 uboot 的通用性,三星使用了宏来判断当前的开发板的型号,并做对应的初始化

这里的初始化是针对 MMC 的,通过 Makefile 中的脚本,配置开发板是 sd 还是 onenand 启动;然后对 MMC 的初始化根据宏做相应的处理

环境变量重定位

uboot 的环境变量需要重定位到 ddr 中才能使用

在 env_relocate 中,先是通过宏判读环境变量是否嵌入在代码段中;本人的开发板的环境变量在 SD 卡中,所以直接跳过,直接分配一个缓存准备用来存放环境变量

uboot 第一次启动时,因为没有烧录 env 分区,gd->env_valid 应当为0,这时候会使用 uboot 的默认系统变量,并写入到 SD 卡的 env 分区中

控制台初始化

console_init_r 做的是控制台的第二阶段的初始化,主要是进行控制台的软件方面的配置

重定位了 stdin stdout stderr

网卡芯片初始化

eth_initialize 对网卡芯片本身进行初始化

开机logo显示

x210_preboot_init 进行开发板启动前的初始化,开启 lcd 显示开机 logo

logo 一般是使用软件制作的数组

通过sd卡烧录系统

通过 check_menu_update_from_sd 会判断开发部的 left 按键是否按下,开发板会在按键按下的时候,进入更新模式,读取 SD 卡中的镜像烧录系统

使用 SD 卡烧录比 fastboot 的方式简单、高效,一般用于量产的场景

死循环

主要是有三个功能

  • 命令解析器
  • 开机倒计时
  • 命令补全

总结

以下来自朱老师总结的 uboot 的工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 主循环