从烧录程序到设备加载运行

前言

    在最近的一个项目中,突然想起以前学习嵌入式系统启动流程,所以借此机会复习一下从烧录程序到设备加载代码运行的过程,加深印象。

一.程序的结构

一个程序一般分为3段:text段,data段,bss段

text段:就是放程序代码的,编译时确定,只读,

data段:存放在编译阶段(而非运行时)就能确定的数据,可读可写

就是通常所说的静态存储区,赋了初值的全局变量和静态变量存放在这个区域,常量也存放在这个区域

bss段:定义而没有赋初值的全局变量和静态变量,放在这个区域

我们编译完成后生成烧录文件,一般单片机的HEX文件,还有些其他类型的文件,内容基本上都是上述三个段。接下来用烧录工具把生成的文件烧录到机器中的ROM或者FLASH。

二.设备启动

    以一般的单片机或者嵌入式Linux产品为例,CPU芯片中一般还有两个片内的ROM和RAM,ROM中存着一段芯片厂家出厂就写好的指令。机器启动时,CPU会运行ROM中的指令,把Flash中bootloader的第一部分,加载到CPU的RAM中,这个阶段做的事是:

1.硬件设备初始化:CPU的工作模式、关看门狗、设置时钟、关MMU、关CACHE等 (代码在cpu/arm920t/start.S中reset:)
2.为加载Bootloader的第二阶段代码准备RAM空间:初始化内存芯片SDRAM,使得外接 的SDRAM可用 (代码在board/smdk2410/lowlevel_Init.S中的lowlevel.Init中,是在start.S中调用的)
3.复制Bootloader的第二阶段代码在RAM空间中:这里将整个U-boot的代码(包括第一、 第二阶段)都复制到SDRAM中 (代码在cpu/arm920t/start.S中实现)
4.设置好堆栈:堆栈的灵活性很大,只要让sp寄存器指向一段没有使用的内存即可 (代码在cpu/arm920t/start.S中实现)
5.清除BSS段之后,跳转到第二阶段的C代码入口点:ldrpc,_start_armboot _start_armboot:.wordstart_armboot (代码在cpu/arm920t/start.S中实现,被调用的函数start_armboot在lib_arm/board.c中)

第二阶段:

6.初始化本阶段要使用的硬件设备:如设置系统时钟、初始化串口。注意board_init函数 还保存了机器类型的ID (如代码在board/smdk2410/smdk2410.c中的board_init、在cpu/arm920t/s3c24x0/srial.c中的serial.init)

7.检测系统内存映射:确定板上使用了多少内存、它们的地址空间是什么,检测到的参数 在向内核传递参数的时候用到 (代码在board/smdk2410/smdk2410.c中的dram_init)
8.将内核映像和根文件系统映像从Flash上读到RAM空间中。内核的复制和启动,这里 是通过命令bootm、bootp、nboot来完成的,这些命令实际上是调用相应的函数,先将映像从各种媒介中读出,存放在指定的位置 (u-boot中的命令都是通过U_BOOT_CMD宏来定义的, U_BOOT_CMD(name,maxargs,repeatable,command,"usage","help"))
9. 为内核{设置}启动参数:U-Boot也是通过【标记列表】向内核传递参数。一般而言只设置内存标记-取值函数setup_memory_tags和命令行标记-取值setup_commandline_tag就可以了。这一步很简单,仅仅是配置对应的两个宏就可以了(这两个函数在lib_arm/armlinux.c中定义)
10.调用内核。对于ARM架构的CPU,都是通过lib_arm/armlinux.c中的do_bootm_linux函 数来启动内核。在这个函数中,先设置标记列表,最后通过theKernel(0,bd->bi_arch_number,bd->bi_boot_params);调用内核。 theKernel指向内核存放的地址(对于ARM架构的CPU,通常是0x30008000),bd->bi_arch_number就是前面board_init函数设置的机器类型ID,而bd_bi_boot_params就是标记列表的开始地址(lib_arm/armlinux.c)

注意:
1、上面的有些步奏可能有些不是必需的、可以调换顺序,比如在S3C2410/S3C2440的开发板中使用的U-Boot中,就是将CPU的速度和时钟频率的设置放到第二阶段;
2、Flash上的内核映像有可能是经过压缩的,在读到RAM之后,还要进行解压,当然,对于有自解压的功能的内核,不需要Bootloadr来解压
3、将根文件系统映像复制到RAM中也不是必须的,这取决于是什么类型的根文件系统,以及内核访问它的方法
4、甚至,将第二阶段的代码复制到RAM空间也不是必需的,对于NORFlash等存储设备,完全可以在上面直接执行代码,只不过相比在RAM中执行效率大为降低


对于一般的单片机,可能没有那么复杂,简单的来说,这两个阶段就是,初始化外部的RAM,初始化其他的硬件设备,把FLash中的text, data,bss三段加载到RAM中,加载的过程如下:

(1)为全局变量分配地址空间---如果全局变量已赋初值,则将初始值从ROM中拷贝到RAM中,如果没有赋初值,则这个全局变量所对应的地址下的初值为0或者是不确定的。当然,如果已经指定了变量的地址空间,则直接定位到对应的地址就行,那么这里分配地址及定位地址的任务由“连接器”完成。

(2)设置堆栈段的长度及地址---用C语言开发的单片机程序里面,普遍都没有涉及到堆栈段长度的设置,但这不意味着不用设置。堆栈段主要是用来在中断处理时起“保存现场”及“现场还原”的作用,其重要性不言而喻。而这么重要的内容,也包含在了编译器预设的内容里面,确实省事,可并不一定省心。
(3)分配数据段data,常量段const,代码段code的起始地址——代码段与常量段的地址可以不管,它们都是固定在ROM里面的,无论它们怎么排列,都不会对程序产生影响。但是数据段的地址就必须得关心。数据段的数据时要从ROM拷贝到RAM中去的,而在RAM中,既有数据段data,也有堆栈段stack,还有通用的工作寄存器组。通常,工作寄存器组的地址是固定的,这就要求在绝对定址数据段时,不能使数据段覆盖所有的工作寄存器组的地址。必须引起严重关注。
注:这里所说的“第一行代码处”,并不一定是你自己写的程序代码,绝大部分都是编译器代劳的,或者是编译器自带的demo程序文件。因为,你自己写的程序(C语言程序)里面,并不包含这些内容。高级一点的单片机,这些内容,都是在startup的文件里面。
4、普通的flashMCU是在上电时或复位时,PC指针里面的存放的是“0000”,表示CPU从ROM的0000地址开始执行指令,在该地址处放一条跳转指令,使程序跳转到_main函数中,然后根据不同的指令,一条一条的执行,当中断发生时(中断数量也很有限,2~5个中断),按照系统分配的中断向量表地址,在中断向量里面,放置一条跳转到中断服务程序的指令,如此如此,整个程序就跑起来了。决定CPU这样做,是这种ROM结构所造成的。
注:特别的,如下

1--I/O口寄存器:也是可以被改变的量,它被安排在一个特别的RAM地址,为系统所访问,而不能将其他变量定义在这些位置。

2--中断向量表:中断向量表是被固定在MCU内部的ROM地址中,不同的地址对应不同的中断。每次中断产生时,直接调用对应的中断服务子程序,将程序的入口地址放在中断向量表中。

总结有如下几段:

1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。

3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放

4、文字常量区—常量字符串就是放在这里的。 程序结束后由系统释放

5、程序代码区—存放函数体的二进制代码。

如果哪位有缘人看到觉得有帮助,就随手端个赞呗~
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章