扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
1 前言
伴随着微电子的发展,用于嵌入式设备的处理器速度越来越快,功能也越来越强大。三星公司生产的S3C44B0微处理器,采用的是ARM7TDMI内核。该内核因为有着功耗小、成本低等特点,因此非常适合作为移动手持终端的处理器核心。Linux操作系统因为它的开放性,使得它不断的被应用到各个领域。在嵌入式领域同样也出现了各种各样的Linux变体,最常用的是uClinux。也正是因为uClinux操作系统支持不带MMU单元的ARM处理器,因此该系统可以对S3C44B0微处理器有很好的支持。
在嵌入式系统开发中,第一个部分便是系统的引导。而系统的引导过程是通过BootLoader来完成的。BootLoader程序是与硬件紧密相关的一段代码,而且编写的时候比较复杂,它主要的功能是初始化微处理器以及周边的硬件资源,并且引导操作系统的启动。下面我将以S3C44B0微处理器来作为例子,对uClinux操作系统内核的引导过程进行一个剖析。
2 BootLoader程序概念
简单的说Boot Lodaer就是在操作系统内核运行之前运行的一段小程序,通过这段小程序,可以初始化硬件设备、建立系统的内存空间映射图,从而将系统的软硬件环境设置成一个适合的状态,以便为最终调用操作系统内核准备好正确的环境。最终,BootLoader把操作系统内核映象加载到RAM中,并将系统控制权传递给它。
2.1 典型的BootLoader程序框架
操作系统角度来说,Boot Loader的总目标就是正确的调用内核来执行。
由于Boot Loader的实现依赖于CPU的体系结构,因此大多数Boot Loader都分为Stage1和Stage2两大部分。依赖于CPU体系结构的代码,例如设备初始化代码等,通常都放在Stage1中,而且通常都用汇编语言来实现,以达到短小精悍的目的。而Stage2通常用C语言来实现,这样可以实现更加复杂的功能,而且代码会具有更好的可读性和可移植性。
Boot Loader的Stage1通常包括如下步骤:
1) 硬件设备初始化
2) 为加载Boot Loader的Stage2准备RAM空间
3) 复制Boot Loader的Stage2到RAM空间中
4) 设置好堆栈
5) 跳转到Stage2的C入口点
Boot Loader的Stage2通常包括如下步骤:
1) 始化本阶段要使用的硬件设备
2) 检测系统内存映射(Memory Map)
3) 将Kernel映象和根文件系统映象从FLASH上读取到RAM空间中
4) 为内核设置启动参数
5) 调用内核
2.2 系统内存组织
由于嵌入式设备具有很好的制定性,因此通常硬件环境会变的千差万别。就算是用户使用了相同的处理器芯片,但是也很有可能因为外围设备电路设计的不同,而存在差异。对于BootLoader程序来说,存储设备的与处理器的连接方式,与其息息相关。对于我们采用的S3C44B0微处理器来说,在系统加电之后,指令指针是指向0x00000000的,也就是说系统是从0x00000000开始之行。正是因为这个原因,通常这个地址空间我们会安排给FLASH存储器。这样我们可以将BootLoader启动代码以及我们之后将会要启动的uClinux操作系统映像烧写到Flash里。对于RAM地址空间,S3C44B0芯片将其设定为从0x0C000000到0x0FFFFFFF一共64MB的范围里。我们可以通过设定存储器控制寄存器来重新设定RAM的大小。例如我们试验采用的存储设备安排如下:
0x00000000 – 0x003FFFFF 4MB Flash
0x0C000000 – 0x0C7FFFFF 8MB RAM
通常来说对于系统的引导和操作系统的启动,可以完全都在Flash中进行,但是Flash存储器的速度相对于RAM来说会慢很多,因此出于速度上的考虑,我们通常会将启动代码和uClinux操作系统的内核映像文件拷贝到RAM中之行。
下面我将对典型的BootLoader程序框架进行分析。
2.3 Stage1阶段
该阶段的主要工作是完成对系统中断向量的设置,初始化微处理器内部寄存器,初始化堆栈,初始化RAM地址空间,并且将Stage2部分的C代码拷贝到RAM空间的指定地点,然后跳转到C代码入口点继续执行。对于这段代码来说,做的都是一些准备工作,因此为了提高效率,这段代码通常都是使用汇编语言来完成的。下面我将结合具体的代码来分析一下Stage1的启动过程。
1)设置中断向量
设置S3C44B0处理器定义的8种系统中断的中断向量地址。这八种系统中断分别是复位中断、未定义指令中断、软件中断、指令预取异常中断、数据异常中断、地址异常中断、IRQ中断和FIQ中断。这8个中断通常是通过无条件跳转的方式来实现的。具体的代码如下。
__entry :
b ResetHandler /* Reset vector */
b HandlerUndef /* Undefined instruction */
b HandlerSWI /* SWI */
b HandlerPabort /* Prefetch abort */
b HandlerDabort /* Data abort */
b . /* Address exception */
b HandlerIRQ /* IRQ */
b HandlerFIQ /* FIQ */
2)初始化微处理器内部寄存器
这段代码主要是要完成硬件部分的初始化,包括关闭中断响应、初始化微处理器通用端口、设置CPU频率等操作。不过需要注意的是,在进行硬件初始化之前需要将微处理器的运行状态转换到SVC模式下。
MRS a1,CPSR /*; Pickup current CPSR*/
BIC a1,a1,#MODE_MASK /*; Clear the mode bits*/
ORR a1,a1,#SUP_MODE /*; Set the supervisor mode bits*/
ORR a1,a1,#LOCKOUT /*; Insure IRQ and FIQ intr are locked out*/
MSR CPSR_cxsf,a1 /*; Setup the new CPSR*/
3)初始化系统RAM空间
这个部分的工作主要是为之后启动代码和内核映像的拷贝操作做准备,并且也为之后的C代码的执行初始化堆栈。这部分的工作主要可以分成两个部分来处理。首先,根据系统配置的存储器特性来初始化相关的存储器控制寄存器。在我们使用的S3C44B0处理器中,存储空间被分成了BANK0-BANK7一共8个块,分别由BANKCON0-BANKCON7控制各个块存储器的读写时钟和片选时钟等信号参数。具体代码如下:
ldr r0,=rBANKCON0
ldr r1,=0x700
str r1,[r0]
ldr r0,=rBANKCON1
ldr r1,=0x700 /* 0x7ffc */
str r1,[r0]
ldr r0,=rBANKCON2
ldr r1,=0x700 /* 0x7ffc */
str r1,[r0]
ldr r0,=rBANKCON3
ldr r1,=0x7568
str r1,[r0]
ldr r0,=rBANKCON4
ldr r1,=0x700 /* 0x7ffc */
str r1,[r0]
ldr r0,=rBANKCON5
ldr r1,=0x700 /* 0x7ffc */
str r1,[r0]
ldr r0,=rBANKCON6
ldr r1,=0x18008
str r1,[r0]
ldr r0,=rBANKCON7
ldr r1,=0x18000
str r1,[r0]
ldr r0,=rREFRESH
ldr r1,=0xac03e1
str r1,[r0]
ldr r0,=rBANKSIZE
ldr r1,=0x16
str r1,[r0]
ldr r0,=rMRSRB6
ldr r1,=0x020
str r1,[r0]
ldr r0,=rMRSRB7
ldr r1,=0x020
str r1,[r0]
初始化RAM空间的第二个部分就是初始化连接脚本文件中指定的需要清0的地址空间,将该断地址空间的内容清0。该部分地址空间主要是用来存放C语言代码中的全局变量等内容的。实现代码如下:
LDR a1,=Image_ZI_Base /* Pickup the start of the BSS area */
MOV a3,#0 /* Clear value in a3 */
LDR a2,=Image_ZI_Limit /* Pickup the end of the BSS area */
CMP a1,a2
BEQ move_data
clear_loop :
STR a3,[a1],#4 /* Clear a word, a1 += 4 */
CMP a1,a2 /* end of ZI ? */
BNE clear_loop
4)为Stage2的C语言代码的执行准备必要的堆栈
因为在Stage2阶段一般都是采用C语言代码来完成的,因此必须在使用C语言代码之前先建立起必要的堆栈信息。通常为了避免堆栈数据被执行代码破坏,通常都是放在RAM的高端地址,并且使得堆栈指针的增长方向是向下增长的。
5)将初始化代码拷贝到RAM中,并且跳转到RAM中执行。因为在我们采用的S3C44B0微处理器里对于FLASH和RAM地址空间是使用的统一编址的,因此我们可以直接使用一个简单循环来完成拷贝。
ldr r3, =0x10000 /* 64K Bytes */
ldr r2, =0xc700000
ldr r1, =0
next :
ldr r0,[r1],#4
str r0,[r2],#4
cmp r1,r3
bne next
6)跳转到C代码执行(即Stage2阶段)
这个过程是直接给指令指针赋值于跳转的C代码的入口地址,在我们的试验中该入口地址是Main。
LDR pc,=Main
2.4 Stage2阶段
该阶段的代码主要使用C语言来实现的。该阶段的工作主要是建立开发板与宿主机之间的通信,加载uClinux内核映像文件和配置内核启动参数,并且启动内核。
嵌入式设备与宿主机的通讯方式有多种,最常用的是使用串口方式进行数据交换。本试验采用的S3C44B0微处理器提供了两个UART口,因此我们可以任选其中一个来初始化并且使用它来与宿主机交互。对于串口的初始化主要是波特率、奇偶校验、停止位、数据位等内容。
对于串口的波特率和波特因子的计算采用如下公式
Iubrd =((int(mclk/16 / baud + 0.5) – 1)
mclk是频率、baud为波特率
2.4.1 检测内存
该部分的功能主要是检测系统在进行硬件初始化的时候是否发生了内存映射错误,即是否物理地址是否被映射到不存在的地址空间。通常是使用读写方式来检测的,即以内存页为单位,在每个页头进行读写操作,比较读写结果。因为S3C44B0处理器并不支持内存映射,因此我们在Stage2过程中并没有包含该部分功能函数。
2.4.2 加载uClinux内核映像
该过程其实只是一个从Flash的指定位置(该位置是uClinux烧写的起始地址)拷贝到RAM中指定的地址空间里。在拷贝之前必须要为uClinux的全局变量结构,即启动参数、内核页表、RAM的页目录等信息预留一定的空间。如果我们将FLASH和RAM看成连在一起的线性地址,则系统的空间分配会如下图:
...... Boot 初始化代码 uClinux 未用 中断向量表 初始化映像代码 启动参数 内核映像 未用 堆栈
2.4.3 配置内核启动参数
我们采用的uClinux是2.4.x内核版本,该版本的内核支持参数启动过程。在嵌入式系统中,启动参数的传入主要是依靠bootloader程序向标记列表(tagged list)的相关域中填写相应的值来完成的。
2.5 uClinux内核引导
当我们初始化完毕uClinux的启动参数之后,控制权就可以交给uClinux内核了,uClinux系统调用内核解压函数(decompress_kernel)来对上一个阶段拷贝的uClinux内核在RAM空间里进行解压(当然如果系统内核在建立的时候没有配置成压缩格式,则解压过程略去)。在解压完毕后,跳转到内核调用函数(call_kernel),该函数实际上执行的是start_kernel(),这个函数包含了有关处理器初始化、中断初始化、进程初始化等操作。最后,将控制权完全的交与uClinux操作系统来执行。
伪处理过程如下:
IF(启动参数正确)
CALL decmporess_kernel()
CALL call_kernel()
ELSE
启动失败
decompress_kernel()
{
解压内核映像
}
call_kernel()
{
...
start_kernel()
....
}
3 总结
本文是对S3C44B0的启动过程进行了一次分析,启动部分的代码可以说是嵌入式设备开发比较重要的部分。而且该部分的处理工作往往又比较麻烦,因此在这里我只是想起到抛砖引玉的作用。因为成文时间比较仓促,难免有错误,请大家批评指正。
4 参考文献
[1] 陈赜.ARM嵌入式教学实践教程.武汉.华中科技大学出版社.
[2] Krim YaChmour.构建嵌入式Linux系统.北京.中国电力出版社.
[3] 崙.32位嵌入式系统硬件设计与调试.机械工业出版社
[4] 毛德操,胡希明.Linux内核源代码情景分析.浙江大学出版社
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者