嵌入式C语言04——BootLoader
嵌入式系统在上电时会运行一小段程序。一般来说,这段程序主要完成硬件初始化,代码搬运及必要的软件环境初始化,被称为BootLoader
对于ARM处理器而言,上电后做的第一件事就是到系统的“零地址”去取指并执行得到的这条指令。因此,系统启动的过程也就是到零地址取指并执行的过程。
系统上电或复位时通常都从地址0x00000000处开始执行,而在这个地址处安排的通常就是系统的BootLoader。
系统的零地址的具体位置和处理器有这非常密切的关系。处理器通过硬件引脚的设置可以选择通过NorFlash或者NandFlash进行启动。由于NandFlash启动机制比较复杂,这里主要通过NorFlash进行分析BootLoader。
BootLoader实现了系统的启动控制,最简单的BootLoader可以只使用一条B指令来实现。但是实际中,BootLoader至少要完成系统初始化和代码搬运的功能。较为完善的BootLoader除了系统初始化和搬运外,还包括其他比如与用户交互的功能。
在BootLoader中实现系统初始化和代码搬运考虑到效率,一般都是用汇编语言来实现。下例为SEP400的boot.s
首先是对使用到的寄存器进行定义
PMU_PLTR EQU 0x10001000 ;PLL的稳定时间寄存器
PMU_PMCR EQU 0x10001004 ;系统主时钟控制寄存器
PMU_PCSR EQU 0x1000100C ;内部模块时钟源供给寄存器
PMU_PMDR EQU 0x10001014 ;芯片工作模式寄存器
EMI_CSECONF EQU 0x11000010 ;CSE参数配置寄存器
EMI_SDCONF1 EQU 0x11000018 ;SDRAM时序配置寄存器1
EMI_SDCONF2 EQU 0x1100001C ;SDRAM时序配置寄存器
对硬件进行必要的初始化。首先对PMU进行初始化,使系统工作在所需要的时钟频率上。
下面代码使CPU的时钟通过PLL倍频提升到80MHZ
PMU_PLTR EQU 0x10001000 ;PLL的稳定时间寄存器
PMU_PMCR EQU 0x10001004 ;系统主时钟控制寄存器
PMU_PCSR EQU 0x1000100C ;内部模块时钟源供给寄存器
PMU_PMDR EQU 0x10001014 ;芯片工作模式寄存器
EMI_CSECONF EQU 0x11000010 ;CSE参数配置寄存器
EMI_SDCONF1 EQU 0x11000018 ;SDRAM时序配置寄存器1
EMI_SDCONF2 EQU 0x1100001C ;SDRAM时序配置寄存器
AREA BOOTLOADER, CORE, READONLY
ENTRY
ldr r4, PMU_PCSR ;打开所有模块时钟
ldr r5, 0x0001FFFF
str r5, [r4]
ldr r4, PMU_PLTR ;配置过渡时间
ldr r5, 0x0FFA0FFA
str r5, [r4]
ldr r4, PMU_PMCR ;配置系统时钟为80MHZ
ldr r5, 0x0000C00C
str r5, [r4]
ldr r4, PMU_PMDR ;由slow模式切换为Normal模式
ldr r5, 0x00000001
str r5, [r4]
ldr r4, PMU_PMCR ;配置系统时钟为80MHZ,确认配置
ldr r5, 0x0000c00c
str r5, [r4]
为了提高系统运行效率,需要将代码从NorFlash复试到SDRAM中,因此必须先进行SDRAM的初始化。
ldr r4, EMI_CSECONF ;CSE片选时序参数配置
ldr r5, 0x8ca6a6a1 ;
str r5, [r4]
ldr r4, EMI_SDCONF1 ;SDRAM参数配置1
ldr r5, 0x1e104177
str r5, [r4]
ldr r4, EMI_SDCONF2 ;SDRAM参数配置2
ldr r5, 0x80002860
str r5, [r4]
接下来代码由NorFlash复制到SDRAM,例子中采用逐字节的复试。
;复制代码到SDRAM
;源地址0x20001000,目的地址0x30000000,大小0x44000字节
ldr r3, 0x00000000
ldr r1, 0x30000000 ;SDRAM基址
ldr r2, 0x20001000 ;NorFlash基址
LOOP
ldrb r4,[r2],#1 ;事后增
strb r4,[r1],#1
add r3,r3,#1
cmp r3,#0x44000
bne LOOP
最后,控制PC指针跳转到SDRAM去执行刚才复制过去的代码:
ldr pc, 0x30000000
DEAD
b DEAD
END
UBOOT技术实现分析:
1. UBoot启动与系统初始化:stage1
系统上电后首先执行的是Start.s,这称为第一阶段。在进行必要的硬件初始化后,在Start.s的最后一个汇编语句中,程序跳转到UBoot的第一个C函数start_armboot进入第二阶段
在第一阶段中,首先设置各模式的堆栈指针,由于UBOOT中没有引入中断,所以只需要设置SVC模式的SP就可以了,其他模式各分配8字节甚至更少的堆栈控件。
完成堆栈初始化后,通过cpu_init_crit和lowlevel_init分别为CPU主频初始化,SDRAM控制器的初始化,使CPU工作在适合的时钟频率下,并未下面代码复制做好准备。
接下来根据当前地址判断是否需要代码复制。如果需要,则将NorFlash中的Stage2的代码复制到SDRAM中,以加速代码执行过程。
2. UBOOT启动与系统初始化:stage2
在Stage2中,将进一步完成其他硬件设备的初试化,完成后将循环调用main_loop来接受用户输入,并响应用户输入的指令。
在start_armboot中,使用函数指针完成系统各模块的初始化:
for(init_fnc_ptr = init_sequence;*init_fnc_ptr;++init_fnc_ptr){
if((*init_fnc_ptr)()!=0){
hang();
}
}
函数指针数组定义如下:
typedef int (init_fnc_t)(void);
init_fnc_t *init_sequence[] = {
cpu_init, //基本CPU相关初始化
board_init, //基本的板级初始化
interrupt_init, //中断初始化
env_init, //初始化环境变量
init_baudrate, //初始化波特率
serial_init, //串口通信初始化
console_init_f, //控制台初始化
display_banner, //显示标题
dram_init, //配置可用RAM
display_dram_config,
NULL
}
3. Linux操作系统引导与参数传递
BootLoader最重要的一个作用就是引导操作系统,UBoot也是的。
Linux操作系统内核的引导需要4个因素,也就是CPU的4个寄存器进行参与,分别是r0,r1,r2和PC。期中r0中一般为0,r1设置为机器号,r2中放置的是参数区(Tags)的地址,PC需要指向内核所在的地址。
在UBOOT中,利用C语言参数传递规范,通过一次函数调用就完成了Linux的内核引导
theKernel = (void(*)(int,int,uint))ntohl(hdr->ih_ep);
theKernel (0,bd->bi_arch_number,bd->bi_boot_params);
首先进行强制类型转换,将ntohl(hdr->ih_ep)的返回值(Linux内核入口地址)转换成为一个函数指针theKernel。