bootloader详解

22 篇文章 2 订阅

4.2               Bootloader的总体设计

4.2.1        阶段设计

在前面的章节中,已经介绍过bootloader的启动可以是分阶段的。在设计时,我们将bootloader分为两个阶段:阶段1和阶段2。分为两个阶段的原因是因为:(1)基于编程语言的考虑。阶段1用主要用汇编语言,它主要进行与CPU核以及存储设备密切相关的处理工作,进行一些必要的初始化工作,是一些依赖于CPU体系结构的代码,为了增加效率以及因为涉及到协处理器的设置,只能用汇编编写,这部分直接在FLASH中执行;阶段2用一般的C语言,来实现一般的流程以及对板级的一些驱动支持,这部分会被拷贝到RAM中执行。(2)代码具有更好的可读性与移植性:若对于相同的CPU以及存储设备,要增加外设支持,阶段1的代码可以维护不变,只对阶段2的代码进行修改;若要支持不同的CPU,则基础代码只需在阶段1中修改。

4.2.2        地址规划设计

当bootloader阶段设计好之后,需要考虑的是镜像存储的地址分配:总镜像保存在什么地方,阶段2对应的镜像会被拷贝到什么地方;内核镜像原先存放在什么地方,bootloader会把它又重新加载到什么地方;如何进行准确的地址规划以保证没有相互冲突等等,这些都是本节需要考虑的范畴。

PXA255的地址空间是统一寻址的,对于本课题的硬件系统,外接32M的FLASH,由片选信号CS0选择,因此映射到物理地址的0x00000000处;对于64M的SDRAM,则对应PXA255的SDRAM BANK0,映射到0xa0000000处。

PXA255系统复位后,从物理地址的0x00000000开始执行第一段代码,这个地址是由 CPU制造商预先安排的。而我们基于PXA255构建的系统将固态存储设备FLASH映射到这个地址上。当Bootloader放到FLASH的起始处后,系统加电或者复位后,CPU将首先执行bootloader程序。

本文所使用的内核镜像以及根文件系统镜像都被加载到SDRAM中运行,这样做是因为基于运行速度的考虑,尽管在嵌入式系统中内核镜像与根文件系统镜像也可以直接在ROM或FLASH这样的固态存储设备中直接运行。所以bootloader在启动时以及加载内核时通常要考虑这一点。下图为具体的存储布局图:

以上为SDRAM地址空间   内核镜像拷贝处

                                                              0xa0300000

 


                               镜像2被拷贝处

                                                              0xa0000000

 

                 

 

 

 

 

 

 

 

 

 


                               内核镜像存放处

           FLASH地址空间                                     0x000c0000

                                                               

                              (镜像2存放处)                 0x00040000

 


                               镜像1存放处

                                                               0x00000000

 图4.2

 

虽然bootloader最终生成一个可执行镜像,但是为了更能清楚的解释其实现流程,在此虚拟地将其与启动阶段相对应起来,分成两个镜像:镜像1和镜像2(事实上,在编译过程中是会形成这两个镜像,除了一个总的镜像镜像1,还有被拷贝至SDRAM中的镜像2)。在本课题中,将物理地址的0x00000000-0x00040000存放bootloader的镜像,内核镜像放在物理地址开始0x000c0000之后的1M空间内(内核镜像一般都小于1M大小);在前面的阶段设计中已经谈及镜像2在SDRAM中运行,这样bootloader的启动速度会大大加快,因此本课题将镜像2放在SDRAM的起始地址0xa0000000处运行;而内核镜像则规划至物理地址的0xa0300000处执行(放在这边是基于这样的考虑:linux内核会在SDRAM开始处存放一些全局数据结构,比如启动参数和内核页表等,所以预留一段空间)。

4.2.3        模式设计

对于普通用户来说只需要bootloader的启动加载模式,但是对于开发者来说,则需要下载模式,因为他们需要时时刻刻地进行一些镜像的更新。为了在两者之间做到兼顾,本课题既支持启动加载模式,也支持下载模式,具体思路为:在bootloader做完一些硬件初始化工作后,而在加载内核镜像之前,先在一定的时间内等待有没有用户有键盘输入,如果没有,则为启动加载模式,直接加载内核镜像进行启动;如果有,则进入命令行格式,这时开发者就可以根据自己的需要以及bootloader的支持情况,做一些其他的工作。模式的转换设计主要在阶段2中实现。

综合起来,整个bootloader的实现流程可以如下图所示:

                            基本硬件初始化

 


            阶段1

                           拷贝阶段2镜像至RAM

 


                             进入阶段2开始执行

 


                           扩展功能所需硬件初始化

                          

                           拷贝内核镜像至RAM中

 


            阶段2

                                  等待50ms  

                                看是否串口有

                                    输入

                                 是         否

                               进入下载模式       跳转至内核镜像

                               接收命令

 

 


4.3              Bootloader的具体实现

    从本节开始介绍本课题的bootloader的具体代码实现。

4.3.1        阶段1的代码实现

阶段1通常包括以下步骤(以执行的先后顺序):

l         一些基本硬件的初始化工作

l         为加载镜像2准备RAM空间(RAM足够的情况下可以省略)

l         把镜像2拷贝到RAM空间

l         跳转到镜像2的入口点(一般是C入口点)

下面结合相关具体代码描述相关的实现,其中涉及到的汇编代码可以参考ARM指令

集,涉及到的对PXA255协处理器的操作可以参考参考文献中相关的介绍。

 

本课题将阶段1中的这段汇编文件命名为rom_reset.s。

和一般的汇编一样,首先要做的是一些伪代码:

(1)定义ARM各模式的栈大小。

 

    .equ MonStackSz, 4096

    .equ FiqStackSz, 4096

    .equ IrqStackSz, 4096

    .equ AbtStackSz, 4096

    .equ UndStackSz, 4096

    .equ SysStackSz, 4096

(2)申明各模式的栈。

 

.global MonStack

    .global FiqStack

    .global IrqStack

    .global AbtStack

    .global UndStack

.global SysStack  

 

(3)将各模式的栈与栈大小结合起来,即为各栈分配栈大小。

.comm   MonStack, MonStackSz   

.comm   FiqStack, FiqStackSz   

.comm   IrqStack, IrqStackSz   

.comm   AbtStack, AbtStackSz   

.comm   UndStack, UndStackSz  

.comm   SysStack, SysStackSz   

 

(4)接着就是申明一些标号量(代码略)。

这些基本工作做完之后,开始进入真正的初始化工作,标识为正文段(.text段,见后

面连接章节)。

以reset标号标识,一开始处设置异常中断向量表,当是冷启动时,直接跳转至对应处进行启动:

 

reset:

       b coldstart

     b undefined_instruction

    b software_interrupt

    b abort_prefetch

    b abort_data

    b not_used

    b interrupt_request

    b fast_interrupt_request

 

系统正常启动,都属于冷启动,程序直接跳转至coldstart标号处执行。接着完成的功能

包括:

1.  使能各协处理器,PXA255中很多特殊功能都需要借助于协处理器,比如存储管理。

 

ldr r0, =0x2001                                 

mcr        p15,0,r0,c15,c1,0

2.  关闭MMU,内部指令/数据cache以及写缓冲区,ARM体系bootloader中都无需MMU的功能,所有的地址都直接使用物理地址;cache也都关闭,原因可参看上一章相关内容 。

 

ldr r0, =0x00000078

mcr       p15,0,r0,c1,c0,0

 

3.  清空TLB,Caches以及写缓冲区,当系统冷启动时所有的保留数据都以无效处理,因此都要清空,况且cache都已经关闭。

       ldr    r0, =0x00000000

       mcr     p15,0,r0,c8,c7,0               

       mcr     p15,0,r0,c7,c7,0               

       mcr        p15,0,r0,c7,c10,4         

 

4.  开系统各存储空间域的访问权限;ARM体系中系统的存储空间分为最多的16个域,每个域对应一定的内存区域,该区域具有相同的访问控制属性。MMU中寄存器C3用于控制与域相关属性的配置。

 

mvn    r0, #0                   

mcr    p15,0,r0,c3,c0,0 

以上涉及的是相关存储方面通用的处理,接着就要开始具体的硬件初始化的工作了,与

具体的CPU以及具体的存储芯片大小有关。

1.  屏蔽所有的中断:为中断提供服务通常是操作系统设备驱动程序的责任,因此在bootloader的执行全过程中可以不必相应任何中断,中断屏蔽是通过写CPU提供的中断屏蔽寄存器来完成的。

ldr r1, =(INT_BASE | INT_ICMR)   /* 通过中断控制器基地址与相关偏移量获得中断屏蔽寄存器的地址*/

    ldr r2, =0x0                  

str r2, [r1]                 

 

2.  设置CPU的速度与时钟频率:系统复位后对于CPU的速度与时钟频率都有一个初始的默认值,可以直接使用该值,到内核启动初始化的时候再设置;也可以在此处直接设置。

    ldr r1, =CLK_BASE                    

    ldr r2, =0x00000141

str r2, [r1, #CLK_CCCR]     

 

3.  SDRAM  的初始化以及使能,这个设置与具体的RAM大小有关,包括正确地设置系统的内存控制器以及各内存库控制寄存器。具体代码稍长,在这里相关代码略去。

 

    相关基本硬件初始化之后,开始设置模式。

1.  为ARM的各个操作模式设置称栈指针:在前面的伪代码中已经为各个操作模式分配相应的栈区,而且ARM各个模式下都有自己通用的栈顶指针SP,现在要做的是在不同模式下将该模式下的SP指向栈顶。每个模式的处理都类似,都是通过设置状态寄存器CPSR(可以参看前面章节的介绍)来完成,并且每个模式都要完成。以IRQ模式为例:

 

mrs r0, cpsr                         /* 保存CPSR的值 */

    bic r0, r0, #0x1f                     /* 清楚所有的模式位*/

    orr r0, r0, #0x12                    /* 将系统设置为IRQ模式*/

    msr CPSR_c, r0                    /*将改变后的状态值发会给CPSR*/

ldr sp, =(IrqStack + IrqStackSz - 4)    /* 设置SP*/

 

2.  将系统模式设置成监管模式(SVC),并设置SP。

    mrs r0, cpsr              

    bic r0, r0, #0x1f          

    orr r0, r0, #0x13          

    msr CPSR_c, r0             

ldr sp, =(MonStack + MonStackSz - 4)

 

到此时,用汇编编写的硬件基本初始化代码完成,对于阶段1来说,剩下的工作就是将镜像2拷贝到SDRAM的指定处,这个工作我们通过一小段C代码来完成,文件名为cstart.c,完成拷贝功能的函数设为cstart。则在汇编末尾可以通过跳转指令进入阶段1的这一小段C代码:

bl cstart

在编译时,本课题将镜像2存放在一个二进制数组umon[ ]之中,具体的编译实现在后面相关篇幅中会有介绍。这样的话,将镜像2拷贝到SDRAM中的实现就比较简单,通过以下代码代码就可以实现:

 

memcpy((char *)UMON_RAMBASE,(char *)umon,(int)sizeof(umon));

 

其中,UMON_RAMBASE即为0xa0000000。接着通过以下代码就可以将执行流程跳转至阶段2开始执行:

 

void (*entry)();

entry = (void(*)())UMON_RAMBASE;

entry();

4.3.2        阶段2的代码实现

本文在阶段2的处理,更多的是对bootloader的功能性的扩展,具体集中在对本文的硬件系统的支持,并在此基础上实现bootloader的两种操作模式。

 

4.3.2.1           基本功能的实现

       基本功能的实现包括加载内核镜像并转让控制权给内核,具体的实现步骤包括:

(1)       前面的存储布局中已经提过,本课题的内核镜像存放在地址0x000c0000处。根据linux内核压缩镜像的构成,离镜像起始的0x24处存放的是一个32位数来标识其身份,因此首先判断(0x000c0000+0x24)处的一个32位数是否等于0x016F2818(标识符)。

(2)       若检查一致,则将位于FLASH中的kernel镜像直接拷入SDRAM中,地址设为0xa0300000,如下:

 

 memcpy(ram_start, (uchar *)KERNEL_FLASH_START, KERNEL_MAX_SIZE)

其中,KERNEL_MAX_SIZE设为1M,ram_start为0xa0300000,KERNEL_FLASH_START为0x000c0000。

(3)       设置启动参数,现今的Linux内核都以链表(tagged list)的形式来传递参数,启动参数以标记ATAG_CORE开始,ATAG_NONE结束。任何bootloader想要传递给kernel的参数都可以添加在在这两个表头与表尾之间。相关的代码如下:

struct tag *params = (struct tag *)BOOT_PARAMS;

 

        params->hdr.size = tag_size(tag_core);

        params->hdr.tag = ATAG_CORE;

        params->u.core.flags = 0;

        params->u.core.pagesize = 0;

        params->u.core.rootdev = 0;

        params = tag_next(params);

 

          //如果有参数传递,在此添加

 

        params->hdr.size = 0;

        params->hdr.tag = ATAG_NONE;

 

其中,相关数据结构如下:

struct tag_header {

                  ulong size;

                  ulong tag;

};

struct tag {

                  struct tag_header hdr;

                  union {

                         struct tag_core              core;

                         struct tag_mem32  mem;

                         struct tag_videotext       videotext;

                         struct tag_ramdisk  ramdisk;

                         struct tag_initrd      initrd;

                         struct tag_serialnr   serialnr;

                         struct tag_revision  revision;

                         struct tag_videolfb  videolfb;

                         struct tag_cmdline  cmdline;

struct tag_acorn     acorn;

                         struct tag_memclk  memclk;

                         } u;

};

 

(4)       直接跳转至RAM中的内核镜像的第一条指令执行:

 linux_kernel = (void (*)(int, int, struct tag *))ram_start;

 linux_kernel(LINUX_ARG0, LINUX_ARG1, tagparams);

 

 其中,三个参数的函数如下:

 LINUX_ARG0固定为0;

 LINUX_ARG1为表征系统机器类型,要与内核中的一致;

 Tagparams为系统传给内核的参数列表的首物理地址。

 以汇编角度看,最终这三个参数会对应系统寄存器R0,R1和R2。

 

4.3.2.2           扩展功能的实现

本文对bootloader的设计决定了实现的bootloader不仅仅起到加载内核镜像这一基本功能,而是把bootloader看作是一个虚拟的小系统,让其对硬件板级系统有更多的支持以为系统开发者提供方便。

(1)          串口的支持

本文将串口的支持放在扩展功能的范畴内,事实上现在一般都已经存在一种共识:bootloader应该串口功能的支持,能够从串口终端正确地收到打印信息。

l         串口打印信息支持

向串口终端打印信息是一个非常重要而且有效地调试手段。

基于以上的原因,本课题没有通过外界的触发,而是直接在bootloader的阶段2代码中,初始化串口,串口初始化时将其配置成8位数据位、1停止位、无奇偶校验,波特率为115200。此串口使用PXA255内置的FF UART(全功能串口)。

另外在串口的支持代码中,增加相关接收与发送数据应用接口getchar( void )以及putchar(char c)。当要接收数据时,可以通过调用接收数据函数接收数据;对于发送数据,主要将数据转发给屏幕显示函数,使其能够在屏幕上显示相应的字符,以达到打印信息的功能。

 

(2)          下载模式的实现

有了串口的支持,就可以实现下载模式的实现。在基本功能实现的第四个步骤(也就是启动内核)之前,首先使得代码流程延迟一段时间,接着调用串口的接收函数看是否有输入(开发者对应的是键盘输入),如果有数据,则bootloader不加载运行内核镜像,转入等待命令状态;如果没有,则加载运行内核镜像。

   udelay(50000);                                        /* 等待用户输入*/

   if(getachar())

      return;                                            /* 返回之后进入命令行等待状态 */

   else{                                                 /* 否则运行内核*/

            linux_kernel = (void (*)(int, int, struct tag *))ram_start;

           

linux_kernel(LINUX_ARG0, LINUX_ARG1, tagparams);

              }

 

那么在等待命令状态后,bootloader又如何进行工作呢?

在本文中,定义了一个数据结构monCommand(如下),它主要用来描述一个命令,结构体中,name项指向具体的命令,第二项指向该命令对应的执行函数,最后一项则指向一些帮助文档说明,例如命令的用法等等。

struct      monCommand {

    char    *name;           

    int        (*func)();             

    char    **helptxt;       

       };

 

本文中静态地维护一个命令数组,数组成员都是monCommand结构的,将要支持的命令以及相关处理保存在该数组中。Bootloader进入命令行状态后,一直等待串口的输入(即命令),当终端有输入时,读取数据,然后上面提过的数组中查找看是否有注册过,如果有立即执行对应的执行函数,如果没有,则继续等待。

这样,以用户通过敲击键盘作为数据输入,bootloader通过串口接收到数据为触发,bootloader就进入等待命令状态,也同时进入下载模式;紧接着通过用户的命令,bootloader的执行完成相关的下载操作或者其它的处理。

 

(3)          通过串口使用XMODEM协议进行下载内核镜像

通过串口的接收功能可以进行内核镜像的下载,由于硬件串口以及相应软件上的支持使

得硬件以及链路层可以完全支持,关键的问题在于上层的通信协议以及要顾及到PC端的实现(下载内核镜像一般放在PC端)。由于bootloader中调试信息也是通过串口输出,如果上层协议处理不当,两者会有冲突。考虑到PC端都使用WINDOWS操作系统,它自带的超级终端(可以使用多种传输协议)非常方便,因此本课题在bootloader中使用XMODEM协议来作为串口传输的上一层协议,PC端就直接使用超级终端。

       下面简单介绍一下XMODEM协议:

XMODEM协议是一种使用拨号调制解调器的个人计算机通信中广泛使用的异步文件运输协议。这种协议以128字节块的形式传输数据,并且每个块都使用一个校验和过程来进行错误检测。如果接收方关于一个块的校验和与它在发送方的校验和相同时,接收方就向发送方发送一个认可字节。然而,这种对每个块都进行认可的策略将导致低性能,特别是具有很长传播延迟的卫星连接的情况时,问题更加严重。因此延伸出与XMODEM想似的协议。使用循环冗余校验的与XMODEM相应的一种协议称为XMODEM-CRC。还有一种是XMODEM-1K,它以1024字节一块来传输数据[14]

具体的数据包格式如表4.1[14]所示(按低字节开始):

 

<soh>表示为标准xmodem协议,每次传送128字节

或者

<stx>表示为1k xmodem协议,每次传送1024个字节

占1个字节

数据块号,第一个数据块为01h

占1个字节

数据块反号

占1个字节

数据块

占128或者1024个字节

校验位。使用求和和CRC校验

占1个或者2个字节

                                                   表4.1

 

定义:<soh> 01H,  <stx> 02H,  <eot> 04H , <ack> 06H  ,<nak> 15H , <can> 18H

具体的数据流如下:

               发送端                  接受端

                                                             <---        <nak>

<soh> 01 FE -data- <xx> --->

                                                             <---        <ack>

<soh> 02 FD -data- xx  ---> (未成功)

                                                             <---        <nak>

<soh> 02 FD -data- xx  --->

                                                             <---        <ack>

<soh> 03 FC -data- xx   --->

                                                           <---     <ack>

<eot>                    --->

                                                               <---        <ack>

                                                     图4.3

实现上,首先在命令数组注册命令“xmodem”,以及相应的执行函数Xmdoem,帮助函数xmodemhelp。当bootloader进入等待命令状态后一直等待接收串口数据,用户端键入“xmodem –d”,则串口接收到数据后,在表中查找到命令“xmodem”,立刻执行函数int Xmodem(int argc,char *argv[]),然后确认参数为“-d”,说明是下载功能,代码就利用xmodem协议通过串口与PC端利用超级终端通过串口进行数据传输。

为了防止冲突,最终传输的内核镜像仍然存放在物理地址的0xa0300000处,与设计中的一致。

 

(4)          通过USB接口进行镜像下载

通过串口进行内核镜像的下载,受到速度的限制,当开发者以后使用ramdisk(linux中的概念)时,通常要下载若干兆的数据,这时串口的速度劣势更加明显地表现出来了。本课题使用的硬件环境中,CPU PXA255芯片内置了一个USB 1.1设备端的控制模块,因此可以在bootloader中增加对USB传输的支持。

通过USB进行传输,硬件底层以及链路层都通过USB1.1协议,但是传输是双方面的,双方如何进行通信,如何进行同步,则成为关键问题。由于bootloader中增加该功能扩展,有着很强的目的性,就是为了下载内核镜像,因此在本课题中自己定义了一套简单的传输协议,包括通信流程以及具体的数据包格式。在本功能中,主机对应PC,从机对应本文的硬件系统。

 

本文中通信双方数据传输都使用数据包(packet)格式,具体格式如下:

数据包格式:

数据包头 Header

数据块 block

 

每个packet都有一个packet header,其定义如下:

 

typedef struct tagUSB_PACKET_HEADER

{

    UCHAR headerSig[HEADER_SIG_BYTES];

    UCHAR pktType;

    UCHAR Reserved;

    USHORT payloadSize;  

} USB_PACKET_HEADER, *PUSB_PACKET_HEADER;

 

其中headerSig为头标志,本课题设为“kITL”,payloadSize为后面数据块Block Data的长度,以字节为单位。

 

pktType为数据包类型,表明Header后面所跟的Block Data的类型。我们定义了三种类型:

l         Boot Request:此时pktType=0xBB,后面数据块Block Data为USB_BOOT_REQUEST类型,它是由从机向主机发送,请求开始传输数据,定义为:

typedef struct tagUSB_BOOT_REQUEST

{

                               char PlatformId[KITL_MAX_DEV_NAMELEN+1];

                               char DeviceName[KITL_MAX_DEV_NAMELEN+1];

                               short reserved;

} USB_BOOT_REQUEST, *PUSB_BOOT_REQUEST;

l         Boot ACK:此时pktType=0xDD,后面的数据块Block Data为USB_BOOT_ACK类型。它是主机接收到Boot Request请求后向从机发送的,表明主机已准备好向从机发送数据,定义为:

 

typedef struct tagUSB_BOOT_ACK

{

                                 DWORD fJumping;

} USB_BOOT_ACK, *PUSB_BOOT_ACK;

l         Data:此时pktType= 0xCC,后面的数据块Block Data为传输数据块,它是由主机发送给从机的。它的前四个字节为USB_BLOCK_HEADER,后面为真正的传输数据。

Block Data:

USB_BLOCK_HEADER

DATA

其中USB_BLOCK_HEADER定义如下:

 

typedef struct tagUSB_BLOCK_HEADER

{

    DWORD uBlockNum;                       /*本数据块编号。第一个数据块为1*/

} USB_BLOCK_HEADER, *PUSB_BLOCK_HEADER;

 

具体的通信过程为:

1.         客户端(即从机)调用USBSendBootRequest()函数向主机端发送Boot请求;

2.         主机端程序USBBoot一旦收到Boot请求,立即向客户端发送Boot ACK应答,表示主机端已准备好;

3.         客户端接受到Boot ACK后,进入数据传输阶段;

4.         客户端向主机端发送数据请求包(Data request),在包中指明所要数据包号(第一个数据包号为0);

5.         主机端程序接收到数据请求包(Data request),则向客户端发送数据包;重复过程4、5,直到数据传送结束。

 

在bootloader中增加USB传输支持,与串口有一点不同的是:串口的独特作用被默认为bootloader的基本功能,因此串口直接进行初始化,无需任何的触发;而为了bootloader最终生成的镜像文件不至于太大,只有在bootloader进入等待命令行状态后,用户敲入“USB”命令后,才会初始化USB硬件控制模块,当用户无需USB功能时,只要不给系统发送USB的命令,该模块就不会被使能,也就不会进行工作。

 

(5)          对FLASH擦写操作的支持

前面已经提及,通过串口或者USB更新内核镜像,仅仅做的是将内核镜像存放在SDRAM的具体地址处。SDRAM的一个特点是断电内容会消失,这样的话,如果将内核镜像下载完之后,系统重新启动,则系统使用的还是原先的镜像,更新镜像没有达到最终的目的。为了解决这个问题 ,本文在bootloader的实现过程中,增加了对FLASH的擦写操作支持。

在我们的硬件系统中,FLASH使用的是Intel的28f128系列。在对该芯片的支持中,必须在底层完成对FLASH的擦除以及写的处理函数供上层调用,具体实现与存储芯片的一套机制相关,与本课题相去甚远,在这不再赘述。在bootloader的阶段2中,先初始化该芯片。然后与串口处理一样,在命令数组中注册命令“flash”,处理函数“falshcmd”;当bootloadr进入等待命令状态后,用户键入以“flash”开始的一串命令(具体命令见后面章节),进入flashcmd函数,该函数会通过后面的参数,来判断开发者的意图,紧接着调用芯片底层的处理函数来完成具体的操作。

 

(6)          宏的应用控制

在前面的总体设计中,已经为bootloader生成的镜像分配了256K的存储空间。随着它扩展功能的增加,各个外设的支持,会使得最终生成的镜像文件越来越大,很可能随着以后实现的深入造成存储空间不够。更为重要的是,对于普通用户来说,这些扩展功能都是多余的,如果系统作为产品发布,根本不需要这些功能,用户也看不到这些功能。因此,需要提供一种方案,只有当开发者需要特定的功能时,该模块才特定的支持。

控制宏的加入就是为了解决这个问题。本文中,添加一个配置的控制文件config.h,在其中针对每个扩展功能,都有一个宏来控制,“1”表示该宏打开,需要对应的功能;“0”表示该宏关闭,不需要对应的功能。以USB为例,定义:

#define USB_SUPPORT          1

并且用它来控制软件上的USB模块的初始化以及所有有关USB操作的代码。当开发者不需要该功能时,将宏改为0,这样直接导致该模块不被初始化,相当于bootloader中未增加该扩展功能。其他模块都一样,除了串口,因为本课题中串口默认为必要的功能。

这样做的另一个好处是在没有必要的时候不启动模块,既能节省功耗,又不至于与以后内核初始化该模块造成冲突。

另外,对于本文所涉及到的地址,也全部使用宏来表示,这样做是为了更方便地进行移植。

 

(7)     一些转换工具的加入

在本文的bootloader的实现过程中,除了基本的代码之外,还另外需要一些转换工具,使得在链接生成镜像的时候,能完成本文所需要的一些特定功能。

幸运的是,互联网技术的发展,能够将一些重要有用的资源进行网上共享,本课题中所涉及到的一些工具均为网上资源下载所得,并且能够非常干脆地完成其功能。这些工具包括:

l         可执行的“elf”,用来从一个elf文件中获得相关信息或者压缩生成elf文件的各个段,具体的功能按其参数选择为准:

-B:将elf文件转换为bin文件

-c:将big-endian控制结构转换为little-endian控制结构

-f:显示elf文件的头

-M:显示elf映射信息(考虑文件的偏移)

-m:显示elf映射信息(不考虑文件的偏移)

-s:显示elf各段的头

l         可执行的“bin2array”,用来将一个bin文件转化成一个二进制的数组。

在实现中,专门创建一个tools的目录,来存放这些可执行的转化工具。

4.3.3        代码的编译

本文所用的编译器是GNU的gcc编译器,由于在本文实现的过程中编译工作都在PC端(X86平台),而最终编译的程序要在Xscale的平台上运行,为了解决这个问题,GNU有专门的针对ARM体系结构的交叉编译器arm-linux-gcc。

 

对于一个C文件,编译它(例如a.c),只要

a.o:        a.c a.h

       $(CC) $(CFLAGS) a.c

宏CC即为arm-linux-gcc,宏CFLAGS表示一些编译选项,这些选项都是根据本课题的实际情况进行配置的。这些选项为:

CFLAGS        = -c -Wno-format -O -fno-for-scope -mcpu=strongarm \

                       -msoft-float -nostdinc -fno-builtin -g \

                       -I. -I$(OBJ) -I$(COMMON) -I$(COMCSB) -I$(FLASHDIR) -o $@

 

       其中:

-c表示只建立obj档,留到后面才进行链接;

           -O:优化,会根据CPU的构架进行一些优化;

           -mcpu=strongarm:表明cpu类型为strong arm;

           -msoft-float:表明支持软件模拟的浮点格式;

-nostdinc:表明对于与一些标准头文件不在标准库目录下寻找,本课题中都有自己定义的头文件以及与一些库函数同名的函数;

-fno-builtin:对于一些编译器自带的一些内嵌函数,如fabs,labs,memcmp,memcpy, sin,sqrt,strcmp,strcpy,strlen等等,除非在程序里前面以双下划线表识,否则编译器是不认的,这样做是因为本课题中的这些函数都重新进行了定义。

-g:建立一些除错资讯,这样一些debug工具才能除错;

­-I:指定头文件.h的搜寻目录,后面即所跟这些目录。

 

对于一个汇编文件(.S),它的编译命令为:

obj/rom_reset.o: rom_reset.s  config.h

       $(CC) $(ASMFLAGS) rom_reset.s

 

ASMFLAGS为汇编编译选项,具体为:

 

ASMFLAGS   = -xassembler-with-cpp -mcpu=strongarm -c -g -o $@ \

                       -D ASSEMBLY_ONLY -I. -I$(COMMON) -I$(COMCSB)

-x:表示所编译文件的语言,-xassembler-with-cpp则就表示所编译的是汇编语言编写的程序。

当然,为了实现简单,我们只需使用一个Makefile文件,在文件里面,将所有需要编译的文件,其依赖文件、编译选项以及编译环境都设定好。Makefile文件是用于自动编译和链接的,一个工程有很多文件组成,每个文件的改变都会导致工程的重新链接,但不是所有的文件都需要重新编译。Makefile中记录有文件的信息,在make的时候决定在链接的时候需要重新编译哪些文件。Makefile的宗旨就是:让编译器知道要编译一个文件需要依赖其他的哪些文件。

4.3.4        目标文件的链接与转换

各种源文件(包括汇编程序、C语言程序以及C++语言程序)经过编译器编译后生成ELF格式的目标文件,这些目标文件吓相应的C/C++运行时库经过ARM连接器处理后,生成ELF(Executable and Linkable Format)格式的镜像文件。ELF时目前最常用的一种,它定义了一些变量以及信息使得动态连接更有弹性,现在常用的ELF格式的文件包括:

l         relocatable:这就是我们编译时产生的.o文件;

l         executable:这就是一般最后生成的可执行文件;

l         shared obj:这就是通常在/lib 或/usr/lib下那些可以动态连接的函数库;

l         core:这就是core dump时产生的文件;

    但是需要注意的是,这里所讲的ELF格式档是广义的二进制文件,而不是仅仅单指可执行文件。

 

4.3.4.1           镜像文件的组成

ARM的这种ELF镜像文件是一个层次性结构的文件,其中包含了域(region)、输出段(output section)和输入段(input section)。各部分的关系如下[6]

l         一个镜像文件由一个或多个域组成。

l         每个域包含一个或多个输出段。

l         每个输出段包含一个或多个输入段。

l         各输入段包含了目标文件中的代码和数据。

下图4.4[6]描述了相关构成:

 

 

 

 

 

 

 

 

 

 

                                                       输入段1.1.1

                                 输出段1.1

                                                       输入段1.1.2

                                 输出段1.2

             域1                                       输入段1.2.1

                                 输出段1.3

                                                       输入段1.3.1

                                                         

                                                       输入段1.3.2

 


              域2                                     输入段2.1.1

                                  输出段2.1            

                                                       输入段2.1.2

                                                       输入段2.1.3                                   

内存                输出段                  输入段

图4.4  ARM镜像文件的组成

 

下面具体介绍各组成部分。

输入段中包含了4类内容:代码、已经初始化的数据、为经过初始化的存储区域、内容初始化为零的存储区域。每个输入段有相应的属性,一般的,可以分为只读的(RO)、可读写的(RW)以及初始化为0的(ZI)。连接器根据各输入段的属性将这些输入段分组,再组成不同的输出段以及域。

一个输出段中包含了一系列具有相同的RO、RW、ZI属性的输入段。输出段的属性与其中包含的输入段的属性相同。在一个输出段内部,各输入段是按照一定的规则排序的。

一个域中包含1~3个输出段,其中各输出段的属性各不相同。各输出段的排列顺序是由其属性决定的,其中,RO属性的输出段排在最前面,其次是RW属性的输出段,最后是ZI属性的输出段。一个域通常映射到一个物理存储器上,如ROM和RAM。

镜像文件的各组成部分在存储系统中的地址有两种:一种是镜像文件位于存储器中时(也是该镜像文件开始运行之前)的地址,称为加载地址;一种是镜像文件运行时的地址,称为运行地址。之所以有这两种地址,是因为镜像文件在运行时,其中的有些域是可以移动到新的存储区域。比如:已经初始化的RW属性的数据所在的段在运行前可能保存在系统中的ROM中,在运行时,它被移动到RAM中。

我们可以举个例子,如图5.5中,RW段的加载时地址为0x6000(指该段所占的存储区域的起始地址),该地址位于ROM中;RW段的运行段的运行地址为0x8000(指该段所占的存储区域的起始地址),该地址位于RAM中

 

 

 

 

 

 

                                         0xffff

                                                    

           RAM                          0xa000        ZI段

 

                                         0x8000        RW段

 


                       RW段

                                         0x6000

            ROM

                        RO段                         RO段

                                         0x0000

 


                  加载时地址映射关系               运行时地址映射关系

                                                                      图4.5

 

通常一个镜像文件中包含若干的域,各域又可包含若干的输出段。因此,连接器就需要知道如下的信息,以决定如何生成相应的镜像文件。

l         分组信息:决定如何将各输入段组这称相应的输出段和域。

l         定位信息:决定各域在存储空间中的起始地址。

ARM镜像文件的入口点有两种类型:一种是镜像文件运行时的入口点,称为初始入口点,另一种是普通的入口点[6]

初始入口点是镜像文件运行时的入口点,每个镜像文件中只有一个唯一的初始入口点,它一般保存在ELF头文件中。

普通入口点是在汇编程序中用ENTRY伪操作定义的,它通常用于标识该段代码是通过异常中断处理程序进入的。一个映象文件中可以定义多个普通入口点。初始入口点可以是普通入口点,但也可以不是普通入口点。

如何让连接器知道相关的信息呢?这就要涉及到接下来的一节,配置文件的说明。

 

4.3.4.2           连接脚本文件的编写

在上一节中已经说过,GNU编译器生成地目标文件缺省为ELF格式。ELF文件由若干段(section)组成,如果不特别指明,由C源程序(本课题所用的基本语言)生成的目标代码中包含如下段:.text(正文段),包含程序的指令代码;.data(数据段),包含固定的数据,如常量、字符串;.bss(未初始化数据段)包含未初始化的变量、数组等。如果更加细致一点,还可以细分出.rodata(只读数据段),包含只读性质的数据;.got(偏移段),包含全局的偏移量表;以及一些遗留下来的.sdata段与.sbss等。连接器的任务就是将多个目标文件的.text、.data和.bss等段连接在一起,而连接脚本文件是告诉连接器从什么地方开始放置这些段。

Gcc等编译器内置有缺省的连接脚本。如果采用缺省脚本,则生成的目标代码需要操作系统才能加载运行。本课题中,此时还没有操作系统的概念,为了能使代码能在本课题的硬件系统上直接运行,需要编写自己连接脚本文件。一般地,具体的连接文件的格式与具体的连接器有关,并不存在通用的格式。具体格式要能够让具体的连接器所识别解析。

在前面的章节中提过,本课题的镜像可以包括镜像1(总镜像)和镜像2,镜像1中的阶段1部分在FLASH中直接运行,镜像2在SDRAM  中运行。

本文中镜像1的连接脚本文件(设为rom.lnk)如下:

MEMORY

{

       rom :       org = 0x00000000, len = 0x100000

       dram :     org = 0xa0100000, len = 0x100000

}

 

SECTIONS

{

       .text :

       {

              boot_base = .;

              obj/rom_reset.o(.text)

              *(.glue_7t)

              *(.glue_7)

       } >rom

 

       .data       :

       {

              *(.data)

       } >rom

 

       .rodata    :

       {

              *(.rodata)

       } >rom

       .got  :

       {

              *(.got)

       } >rom

 

       .bss :

       {

              bss_start = .;

              *(.bss) *(COMMON)

       } >dram

 

       .sbss       :

       {

              *(.sbss)

              bss_end = .;

       } >dram

}

在MEMORY标识的括号里说明的是各存储空间:包括rom(FALSH),偏移首地址为0x00000000,大小为1M;ram,偏移首地址为0xa0100000,大小也为1M。SECTIONS标识的括号里说明的是各个段的放置空间:.text放在当前的偏移首地址,只存放rom_reset.o中.text段,“>rom”标识是存放在rom中;.text段之后的是.data段,也放在rom中;接着是只读数据段.rodata;本课题还留有.got段,尽管没有使用到;未初始化数据段.bss 段放在ram的偏移首地址处,“>dram”标识是存放在ram中。

最终,本文中该镜像的具体的地址映射为:

 

     .text:     0x00000000……0x000003eb (1004 bytes)       allocated text

     .data:     0x000003ec..       …..0x00028993 (165288 bytes)    allocated writeable

     .bss:      0xa0100000…….0xa0105fff (24576 bytes)      allocated writeable

     .sbss:     0xa0106000…….0xa0106000 (0 bytes)           writeable

 

镜像2的连接脚本为:

MEMORY

{

       ram :       org = 0xa0000000, len = 0x100000

}

 

SECTIONS

{

       .text :

       {

              boot_base = .;

              obj/ram_reset.o(.text)

              *(.glue_7t)

              *(.glue_7)

       } >ram

       .data       :

       {

              *(.data)

       } >ram

 

       .rodata    :

       {

              *(.rodata)

       } >ram

 

       .got  :

       {

              *(.got)

       } >ram

       .bss :

       {

              bss_start = .;

              *(.bss) *(COMMON)

       } >ram

       .sbss       :

       {

              *(.sbss)

              bss_end = .;

       } >ram

}

 

其具体的含义与上一个脚本没有什么差别,唯一不同的是所有的存储空间为ram,而且其偏移初始地址也不一样。

镜像2在该连接文本的设置下的具体映射为:

     .text:            0xa0000000……0xa00109b3 (68020 bytes)       allocated text

     .data:           0xa00109b4……0xa0024a07 (82004 bytes)       allocated writeable

     .rodata:        0xa0024a08……0xa00285a7 (15264 bytes)        allocated

     .bss:            0xa00285b0……0xa0042453 (106148 bytes)     allocated writeable

     .sbss:           0xa0042454……0xa0042454 (0 bytes)               writeable

 

4.3.4.3           代码的连接以及转换

在这一节中,我们就可以回答前面所提过的问题:怎么阶段2的镜像会存放在数组umon[ ]中,使得阶段1可以将其拷贝到SDRAM中,以及本课题中是任何进行连接的。

首先看阶段2的部分,它最终生成.bin文件,连接语句如下:

$(AOUTRAM).bin: info $(OBJS2) $(OBJ)/libz.a makefile ram.lnk

       $(LD) -e coldstart -o $(AOUTRAM) -T ram.lnk $(OBJS2) $(LIBS)

       $(WIN_TOOLS)/elf -cm $(AOUTRAM)

       $(WIN_TOOLS)/elf -cB $(AOUTRAM).bin $(AOUTRAM)

       $(WIN_TOOLS)/bin2array -D "UMON_RAMBASE 0xa0000000" \

       -D "UMON_START 0xa0000000" -o $(OBJ)/umon.c -a umon $(AOUTRAM).bin

 

很明显地,第一行指明一些必须依赖的文件,其中$(OBJS2)是阶段2中所有源程序经编译后生成的目标文件(.o)的集合。第二行中,$(LD)就是本课题所使用的GNU连接工具arm-linux-ld,-e指明该镜像文件的入口,-o指明生成的文件名,­-T指明了连接脚本。正常地,前面的两行已经基本可以了,但是由于本课题的特殊性,还有很多后续工作要做。第三行,开始使用前面提过的可执行的转换工具elf,经生成的文件中的地址映射打印出来,并且使用little-endian控制构架;第四行完成的是将生成文件转换为可下载的.bin文件。最后执行的是通过可执行的转换工具bin2array,将刚刚转换的.bin文件再转换成二进制的数组,数组中则保存着镜像的二进制内容,数组名为umon,并保存在临时生成的文件umon.c中,(这就是本节一开始提出的问题的答案所在),在第一阶段只需将该数组里的内容拷贝至SDRAM中即可;-D则定义了一些本文需要使用到的宏。

 

至于本文最终生成的rom.bin,其连接执行语句如下:

$(AOUTROM).bin: $(AOUTRAM).bin $(OBJS1) rom.lnk

       $(LD) -e reset -o $(AOUTROM) -T rom.lnk $(OBJS1) $(LIBS)

       $(WIN_TOOLS)/elf -cm $(AOUTROM)

       $(WIN_TOOLS)/elf -cB $(AOUTROM).bin $(AOUTROM)

 

rom.bin的生成依赖于阶段2中生成的ram.bin($(AOUTRAM).bin),$(OBJS1)(阶段1中的汇编目标文件以及一个C文件编译后的目标文件)。其他的,与前面刚刚提过的一样。

4.4              代码组织结构

具体代码实现介绍完之后,本节简单介绍一下bootloader具体的代码组织构架。

为了实现理论上的通用性,本文对整个代码的组织结构进行了统一安排。可以为不同的ARM硬件系统单独创建文件夹,在该目录下均包含只与本硬件系统相关的代码,例如:编写其对应的Makefile,在Makefile中针对不同的硬件系统写对应的依赖关系,编译环境(包括编译工具、参数等)不需要改变;如有必要,编写其对应的连接脚本。而对于其他一些任何系统都需要使用的代码或者工具,则也专门创建文件夹。这样当要使用某个特定的系统,只需在本课题代码的基础上进行少量移植,并将代码保存在对应的目录下,编译时只需在该特定目录下通过Makefile进行编译即可。

具体的代码结构如下(例如存在SA1100处理器及其系统和Xscale处理器及其系统):

                                         

                                                 图4.6

在common目录下为一些基本公用的代码,是与硬件系统没有关系的,比如:各种FLASH的支持代码,一些网络协议的支持,一些重新定义的函数等。targets目录下为各个硬件系统,如上图,包括Xscale处理器的硬件系统以及Sa1100处理器的硬件系统,各自目录下包括各自的处理代码,这些代码是与具体的CPU以及硬件系统密切相关的,当然还包括Makefile以及连接脚本;如要增加其他的ARM硬件系统,只需在targets目录下再添加对应的目录即可。Tools目录下包含本课题所使用的各种转换工具。

 

4.5               使用操作

通过硬件复位后,系统正常启动,bootloader完成其基本的功能之后加载内核镜像,然后将控制权交给内核。如果要使用bootloader的扩展功能,当硬件复位后,敲击键盘任意键,bootloader接收到后,进入命令行状态,具体命令包括:

l         xmodem –d:通过XMODEM协议下载数据并显示数据大小;

l         usb –d:通过USB下载数据并显示数据大小;

l         通过擦写FLASH更新镜像:

(1)       更新bootloader自身镜像,镜像放在0x00000000开始的FLASH的第一个扇区内。命令如下:

flash opw

flash erase 0

flash opw

flash ewrite 0x00000000 0xa0300000 166272

解释如下:

――flash opw

由于bootloader所在的零扇区是写保护的,这个命令将写保护暂时打开。打开的时间到下一条命令运行完为止。

――flash erase 0

擦除零扇区。

――flash opw

再一次打开写保护。

――flash ewrite 0x00000000 0xa0300000 16672

将sdram中从0xa0300000地址开始的数据写到flash从0x00000000开始的空间中,数据量为166272,即用串口或者USB传输时显示的数字。

(2)       更新内核镜像,命令如下:

flash erase 3

flash erase 4

flash erase 5

flash erase 6

 

flash write 0x000c0000 0xa0300000 878976

解释如下:

――flash erase     3(4,5,6)

擦除内核镜像所在的3,4,5,6扇区(内核镜像存放在0x000c0000开始的1M存储区域内,该区域正好在这些扇区之内)。

――flash write 0x000c0000 0xa0300000 878976

将SDRAM中从0xa0300000地址开始的数据写到FALSH从0x000c0000开始的空间中,数据量为878976,即用串口或者USB传输时显示的数字。


第五章                 实验结果与测评

上一章bootloader的设计与实现,硬件系统中CPU采用了Intel Xscale系列的PXA255,前面已经提及,Xscale核心与ARMV5TE构架相兼容,所以以PXA255作为CPU来搭建的硬件系统是一个典型ARM系统。但是PXA255还是有其自身的一些特点,而这些特点是其他一些ARM处理器所没有的。所以,要如本文题目所讲,实现一个通用的ARM bootloader,即要适合所有的ARM处理器以及硬件系统,是不太可能的事情。本文真正所做的,就是基于PXA255搭建的这个ARM硬件系统,来设计和完成bootloader。特别的,本文在设计时,已经考虑了移植其他ARM 系统,因此将本文涉及的bootloader进行移植,并不是件困难的事。

本章内容主要是功能上的一些验证结果,并对该结果进行简单的说明,由于本课题性质上的限定,没有太多的数据。

5.1                 实验结果

具体的实验结果包括bootloader的基本功能的实现以及扩展功能的实现,在这里以实际使用时的一些使用画面来作为功能实现的证据。

5.1.1        基本功能的实现结果

本文中的操作系统使用的Linux操作系统,内核编译完成后生成的是一个压缩的镜像文件,当系统控制权交给操作系统后,也就是说当开始执行内核代码时,它首先做的是自解压的过程,并且如果用串口打印消息,可以看到“uncompressing linux ……………….done, booting the kernel……”,说明已经开始启动内核。对于bootloader来说,出现这样的消息,说明已经成功地加载和引导内核了,它的基本功能也就实现了,而且不存在各类镜像存放加载地址之间的冲突。对于本文的系统,当通过硬件复位重启后,可以看到此消息,验证了基本功能的实现,并且本文实现的bootloader已经在具体的项目中进行使用,没有出现差错。

下图为内核的启动信息:

 

 

 

 

 

 

 

 

 

 

 

      

                                            图6.1  内核启动信息

5.1.2        扩展功能的实现结果

本文bootloader扩展功能主要是实现了通过串口和USB进行数据传输,以及对FLASH的擦写操作。对串口读写数据的支持,包含了使用XOMDEM以及1K-XMODEM协议,而且只支持下载功能(即PC端到硬件设备端);类似的,使用USB进行数据传输,也是只支持下载功能;而对FLASH的擦写操作,支持了对单个BLOCK擦写或者多个BLOCK的连续擦写。

以下是一些使用时的界面。

                           

                                                 图6.2 使用XMODEM传输数据界面

 

                

                              图6. 3  使用USB传输数据时PC上的传输界面

5.2              程序性能

衡量程序的性能,通常考虑稳定性、执行速度以及它的移植性。由于本文的bootloader并非是大容量的代码,因此,其执行速度是基本可以忽略不计的,另外稳定性的问题,通过平常大量的使用,功能都能正常稳定的实现。因此程序的性能问题,主要考虑扩展功能的一些相关数据以及程序的移植性问题。

5.2.1        扩展功能的功能性测试数据

以下是使用本课题中bootloader扩展功能传输数据时的代表性能的一些相关数据(其中,串口波特率设置为115200):

功能

平均传输速度

稳定性

使用XMODEM协议,串口下载数据

  1—2k cps

较好

使用1K-XMODEM协议,串口下载数据

10k cps

较好

使用USB下载数据

  200k cps

较好

 

                                                     

 

 

表6.1

5.2.2        程序的可移植性

本文中,对bootloader的设计时,已经考虑到针对其它ARM系统bootloader的移植,特别是设计的bootloader采用了双阶段,更加有利于移植工作。

对于不同的CPU以及硬件系统进行移植,需要做的是:

(1)       在阶段1的汇编代码rom_reset.s中,根据自身CPU的一些指令(特别是一些独有的指令)以及硬件系统的具体配置(体现在SDRAM的配置上),针对上一章实现中的描述进行修改即可。

(2)       根据自身硬件系统存储空间的映射,进行地址规划,修改相关的地址。

这样在关闭配置文件中所有对扩展的支持后,就可以完成该系统bootloader的基本功能。如果还要支持扩展功能,则就要看具体的硬件电路以及CPU本身支持的内置硬件控制模块了,可能要重新进行代码的编写。

 

 

 

 

 

 

 

 

 


第六章     总结与展望

本文主要介绍的是基于ARM嵌入式系统通用bootloader的设计与实现。由于bootloader是与具体的硬件系统紧密相关的,所以在具体的实现上,主要以Intel Xscale 核心的PXA255为处理器构建的硬件系统为硬件平台,以Linux为操作系统来阐明一个bootloader的设计过程。而对于ARM系统的通用bootloader,本文则从理论上来阐述对于一个ARM系统,bootloader所要实现的功能以及在实现bootloader时的一些软硬件上的规定,并且结合实现的bootloder,进一步的说明如何通过已实现的bootloader来进行简单的移植以适合其它的ARM系统。

Bootloader是因嵌入式系统的蓬勃发展而应运而生的。一个嵌入式系统赋予bootloader的职能是引导与加载内核镜像,但是为了给更多的开发者提供便利的开发手段,bootloader越来越不局限于其基本功能,它不断地增加对硬件电路板具体功能模块的支持,甚至是支持一些简单的网络协议。现在的bootloader,更多的像一个小系统。

实现bootloader,就基于以上原因,在扩展功能上进行了更多的支持。除了bootloader基本的加载内核镜像的功能,还增加了对串口(使用XMODEM协议)的支持,USB1.1的支持以及对FLASH擦写的支持。

但是,一个真正的通用bootloader,应该有更多的扩展功能来适应更多的应用。由于时间的限制,实现的bootloader,扩展功能仅局限于上面所提及的。以后,应该能够有更多的支持,例如:简单的tftp协议;支持一些扩展存储卡(例如MMC卡,CF卡)的支持;以及对应的一些文件系统的支持(TFS,FAT等)。

  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
单片机bootloader是一段特定的程序代码,位于单片机的内部存储器中,用于引导其他程序的运行。它的主要作用是在单片机上电后首先运行,负责初始化硬件和软件环境,并将应用程序从外部存储器中加载到内部存储器中运行。 单片机的bootloader通常由单片机厂商提供或开发,具有以下几个功能: 1. 硬件初始化:bootloader负责初始化单片机的各个硬件模块,包括时钟、中断、IO口等,确保单片机正常工作。 2. 外部存储器访问:bootloader能够读取外部存储器,如闪存、EEPROM等,从中加载应用程序到内部存储器中,实现程序的更新或更换。 3. 应用程序验证:bootloader可以对外部存储器中的应用程序进行验证,确保其完整性和正确性,避免加载错误的程序导致单片机异常甚至损坏。 4. 应用程序跳转:当应用程序加载完成后,bootloader会将控制权转交给应用程序,让其开始正常运行。这通常通过设置程序计数器(PC)来实现。 5. 通信接口支持:bootloader通常会提供一些通信接口,如串口、CAN总线等,以便与外部设备进行通信,例如PC机进行升级或调试。 单片机bootloader的设计与应用相当重要,它能够方便地对单片机进行在线升级、故障恢复和软件调试。同时,它也要求在尽可能小的存储空间内实现各种功能,并保证安全性和稳定性。因此,bootloader的设计需要仔细考虑各种情况和要求,以满足不同应用场景下的需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值