鸿 网 互 联 www.68idc.cn

【摘】自己动手编写嵌入式Bootloader

来源:互联网 作者:佚名 时间:2015-10-09 05:50
自己 动手 编写 嵌入 式Bootloader---基本功能流程 http://www.sina.com.cn 2009 年 08 月 31 日 13:55 IT168.com 文本 Tag : 嵌入 式开发 【 IT168 技术文档 】 CPU 上电后会从 IO 空间的某地址取第一条指令。但此时: PLL 没有启动, CPU 工作频率为外部

自己动手编写嵌入式Bootloader---基本功能流程

http://www.sina.com.cn  20090831日 13:55  IT168.com

文本Tag: 嵌入式开发

  【IT168 技术文档CPU上电后会从IO空间的某地址取第一条指令。但此时:PLL没有启动,CPU工作频率为外部输入晶振频率,非常低;CPU工作模式、中断设置等不确定;存储空间的各个BANK(包括内存)都没有驱动,内存不能使用。在这种情况下必须在第一条指令处做一些初始化工作,这段初始化程序与操作系统独立分开,称之为bootloader

  实际上,很少有必要自己写一个Bootloader,因为U-Boot已经强大到能够满足各种需要。但是强大必然复杂,一个初学者想要分析U-Boot的源代码,还是有些难度的。出于学习的目的,我写了这个史上最简单的启动加载器,它只包含最基本的功能,却囊括了一个嵌入Bootloader应该有的核心和精华。我把这个启动加载器命名为S-Boot, Simple Bootloader的缩写,亦可进一步简称为SB

  使用的实验环境为OK2440开发板,板上处理器为S3C2440A,有64M内存,Nand存储器为K9F120864M。网口芯片为CS8900A。我们要实现的功能是:从串口下载Linux内核映像到RAM;从网口下载Linux内核映像到RAM;RAM启动内核挂载NFS根文件系统。

  1. 第一阶段的汇编代码:start.S

  一个嵌入Bootloader最初始部分的代码几乎必须是用汇编语言写成的,因为开发板刚上电后没有准备好C程序运行环境,比如堆栈指针SP没有指到正确的位置。汇编代码应该完成最原始的硬件设备初始化,并准备好C运行环境,这样后面的功能就可以用C语言来写了。

  对我们的S-Boot来说,上电后的起始运行代码是 start/start.S



Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

.text
.global _start
_start:
Reset 0x00发生复位异常时从地址零处开始运行
    HandleUndef 0x04未定义指令中止模式的向量地址
    HandleSWI   0x08管理模式的向量地址,通过SWI指令进入此模式
    HandlePrefetchAbort 0x0C指令预取终止导致的异常的向量地址
    HandleDataAbort     0x10数据访问终止导致的异常的向量地址
    HandleNotUsed       0x14保留
    HandleIRQ           0x18中断模式的向量地址
    HandleFIQ           0x1C快中断模式的向量地址  这里,汇编指示符.text表明以下内容属于代码段,.global _start指明_start是全局可访问的符号(Give the symbol external linkage)。按照ARM920T的规定,从地址0x000x1C放置异常向量表,向量表每个条目占四个字节,正好可以放置一条跳转指令,跳转到相应异常的服务程序中去。在S-Boot中没有使用中断,所以除Reset异常外,其它异常的服务程序都可简单地写个死循环。Reset异常是系统上电后自动触发的,所以我们的代码都写在Reset的服务程序里面。

  实际上,异常向量表不一定非要位于地址0x00处,CP15协处理器中的c1寄存器的第13位用来控制异常向量表的起始地址。该位为0时,异常向量表位于低地址0x00;该位为1时,异常向量表位于高地址 0xFFFF0000处。我们没有必要改变这个位的值,使用默认的低地址就行了。



Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

Reset:
        mrs r0,cpsr       @set cpu to SVC32 mode
        bic r0,r0,#0x1F
        orr r0,r0,#0xD3
        msr cpsr,r0       @cpsr=11x10011, IRQ/FIQ disabled   代码最初始的任务是设置CPU工作在SVC32模式,关闭所有中断,禁用看门狗。实际上,即使不设置工作模式,CPU在复位之后将自动工作在管理模式。在整个S-Boot运行期间,我们没有使用中断,也没有改变CPU工作模式,它将一直工作在SVC32模式。

  MMUICacheDCache的打开和关闭都是由CP15协处理器的c1寄存器控制的。实际上在复位之后这三者都是自动关闭的,所以省略了关闭它们的代码。

  【摘】<strong>自己</strong><strong>动手</strong><strong>编写</strong><strong>嵌入</strong>式Bootloader

  S3C2440APSR寄存器(Program Status Reguster)中每个Bit位的含义如图1所示。Bit4~Bit0为模式位,用来设置CPU工作模式,现在只要知道 M[4:0] 10011 表示SVC32模式就行了。Bit5为状态位,T=0表示工作在ARM状态,T=1表示工作在Thumb状态,默认为0,不需要改变。Bit6为快速中断禁止位,F=1为禁止快速中断,F=0为使能快速中断。Bit7为中断禁止位,I=1为禁止中断,F=0为使能中断。其它Bit位暂时可以不必理会。

  mrs msr是在PSR寄存器和其它寄存器间传递数据的指令。如:mrs r0,cpsr cpsr的值传送到r0中, msr cpsr,r0 r0的值传送到cpsr中。bic是位清零(Bit Clear)指令,bic r0,r0,#0x1F 意思是把r0Bit[4:0]位清零(0x1F指示),然后把结果写入r0中。 orr是按位求或指令,orr r0,r0,#0xD3 表示把r0的 Bit7,Bit6,Bit4,Bit1,Bit0 置为1,其它位保持不变。

  执行完上述操作后,cpsr中的 I=1, F=1, T保持不变(默认为0)M[4:0]=10011,意思是禁止IRQ,禁止FIQ,工作在ARM状态,工作在SVC32模式。



Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

ldr r0, =0x53000000
        mov r1, #0x0
        str r1, [r0] @disable watch dog  禁用看门狗更简单,因为WTCON寄存器的地址为0x53000000,直接向该寄存器写0即可。

  到目前为止,CPU工作在外接晶振12MHz频率之下。使用以下代码设置PLL,提升工作频率。



Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

ldr r0, =0x4C000014 @CLKDIVN register

        mov r1, #0x05 @FCLK:HCLK:PCLK 1:4:8
        str r1, [r0]

        mrc p15,0,r0,c1,c0,0 @if HDIVN Not 0must asynchronous bus mode
        orr r0,r0,#0xC0000000 @see S3C2440A manual P7-9
        mcr p15,0,r0,c1,c0,0

        ldr r0, =0x4C000004 @MPLLCON register
        ldr r1, =0x0005C011 @((92<<12)|(1<<4)|(1))
        str r1, [r0] @FCLK is 400 MHz !  最后的结果是,FCLK=400MHzHCLK=100MHzPCLK=50MHz



Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

  SDRAM Init
        mov r1, #0x48000000 @MEM_CTL_BASE
        adrl r2, mem_cfg_val
        add r3, r1, #52
1:
        ldr r4, [r2], #4 读取设置值,并让r24
        str r4, [r1], #4 将此值写入寄存器,并让r14
        cmp r1, r3 判断是否设置完所有13个寄存器
        bne 1b 若没有写成,继续  设置存储控制器。



Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

ldr sp, =0x32FFF000 @设置堆栈
        bl nand_init @初始化NAND Flash 
                                            @nand_read_ll函数需要3个参数:
        ldr r0, =0x33000000 @1目标地址=0x30000000,这是SDRAM的起始地址
        mov r1, #0 @2源地址 =0S-Boot代码都存在NAND地址0开始处
        mov r2, #102400 @3复制长度=102400(bytes)
        bl nand_read @调用C函数nand_read


        ldr lr, =halt_loop @设置返回地址
        ldr pc, =main @b指令和bl指令只能前后跳转32M的范围,故使用向pc赋值的方法进行跳转

halt_loop:
        halt_loop  这里把所有的代码从Nand拷贝到RAM中,然后跳转到main函数去执行。此后程序便在RAM中运行了。但是到目前为止,前面的程序都是在SteppingStone里运行的。所谓SteppingStone,是指在S3C2440A的内部的4KBRAM缓存,它总是映射到地址0x00处。硬件加电后会自动将Nand Flash中的前4KB的数据拷贝到Stepping Stone中,然后从地址0x00处开始运行。

  如果代码足够小(小于4KB)的话,那只在SteppingStone中运行,加载Linux内核到内存即可。但通常代码肯定会大于4KB。所以Bootloader一般分为两部分,Stage1的代码在SteppingStone中运行,它会把Stage2的代码拷贝到RAM中,并跳转到RAM中执行;Stage2的代码在RAM中执行,它可以完成加载内核及其它任何复杂的功能。因为Stage2的起始位置不好确定,为了方便,我们把所有的代码都拷贝到RAM中了。

  函数nand_read有三个参数,第一个参数为目的地起始地址,第二个参数为源起始地址,第三个参数为要复制的数据长度,以字节为单位。根据ATPCS 函数调用规则,三个参数分别用寄存器r0r1r2来传递。我们在内存的0x33000000处存放Bootloader,复制长度根据编译生成的S- Boot.bin映像文件大小,向上取512字节的整数倍。

  这里先来规划一下内存空间的分配。RAM的地址范围是从0x300000000x3400000064MByte。把S-BootKernel放在高地址处,S-Boot从 0x33000000开始,预留8MByte的空间,内核从0x33800000开始,可供使用的空间也是8MByte。因栈空间是向下生长的,我们在 S-Boot下面预留4096Byte的空闲区域,然后向下为栈空间,故栈指针SP初始化为 0x32FFF000。其实留不留空闲区域是无所谓的,这里只是为了把二者更明显地区分开。我们只设置SVC模式下的SP,不使用CPU的其它工作模式,所以也没必要设置其它模式下的栈指针。另外,程序中不使用动态内存分配,故而也不必分配堆空间。

  【摘】<strong>自己</strong><strong>动手</strong><strong>编写</strong><strong>嵌入</strong>式Bootloader

内容导航 

  2. nand读操作

  在编译连接时,我们把上述 start.S 代码放在生成的二进制映像文件的最开始位置,因而也被烧写到 Nand Flash 的最起始位置,因而会被自动拷贝到 SteppingStone 里运行。start.S 要完成的任务之一,是把S-Boot的所有代码从Nand Flash拷贝到内存中,这里需要对NAND的读操作,因此对NAND的初始化和读操作要在第一阶段写好。

  以开发板上使用的K9F1208为例,每个页(page)512Byte数据和16Byte校验,每个块(Block)32个页,即16KByte数据和512Byte校验。

【摘】<strong>自己</strong><strong>动手</strong><strong>编写</strong><strong>嵌入</strong>式Bootloader  Nand Flash只用8根线与CPUDATA0-7连接,位宽为8位,不管是数据、地址或控制字都通过这8根线传递,如果读写数据的话每次只能传输一个字节数据。Nand Flash的操作通过NFCONFNFCMDNFADDRNFDATANFSTATNFECC六个寄存器来完成。在S3C2440A数据手册第218页可以看到读写Nand Flash的操作时序:1. 通过NFCONF寄存器配置Nand Flash;2.Nand Flash命令到NFCMD寄存器;3.Nand Flash地址到 NFADDR寄存器;4. 在读写数据时,通过NFSTAT寄存器获得Nand Flash的状态信息。应该在读操作前或写操作后检查R/nB信号(Ready/Busy信号)

  初始化NAND FlashS3C2440NFCONF寄存器用来设置时序参数TACLSTWRPH0TWRPH1,设置数据位宽;还有一些只读位。TACLS、 TWRPH0TWRPH1这三个参数控制的是Nand Flash信号线CLE/ALE与写控制信号nWE的时序关系。

  注意,寄存器值转换成实际的时钟周期值时,TACLS不需加1,而TWRPH0TWRPH1需要加1。比如NFCONF寄存器中设置 TACLS=1TWRPH0=3TWRPH1=0,意思是时序图中 TACLS=1HCLK时钟,TWRPH0=4HCLK时钟,TWRPH1=1HCLK时钟。



Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

void nand_init(void)
{
    //时间参数设为:TACLS=0 TWRPH0=3 TWRPH1=0
    NFCONF 0x300;
    
    NFCONT (1<<4)|(1<<1)|(1<<0);
    
    NFCONT &= ~(1<<1); //发出片选信号
    NFCMMD 0xFF//复位命令
    s3c2440_wait_idle();//循环查询NFSTAT0,直到它等于1
    NFCONT |= 0x2//取消片选信号
}   读操作:读操作也是以页(512Byte)为单位进行的。在初始上电时,器件进入缺省的读方式1模式。在这一模式下,页读操作通过将0x00写入指令寄存器,接着写入3个地址(1个列地址和2个行地址)来启动。一旦页读指令被器件锁存,下面的页读操作就不需要再重复写入页读指令了。写入页读指令和地址后,处理器可以通过对信号线R//B的分析来判断页读操作是否完成。如果信号为低电平,表示器件正忙;如果信号为高电平,表示器件内部操作完成,要读取的数据被送入了数据寄存器。外部控制器可以再以50ns为周期的连续/RE脉冲信号的控制下,从IO口依次读出数据。连续页读操作中,输出的数据是从指定的列地址开始,直到该页最后一个列地址的数据为止。



Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

for(i=start_addr; (start_addr size);)
     {
         NFCMMD 0//发出READ0命令
         s3c2440_write_addr(i); //Write Address
         s3c2440_wait_idle(); //循环查询NFSTAT0,直到它等于1
         for(j=0NAND_SECTOR_SIZE; j++, i++)
         {
             *buf (unsigned char)NFDATA;
             buf++;
         }
     }  缺点:没有使用ECC校验和纠错;没有使用坏块检查;

 

内容导航 

  3. main 函数

  串口初始化,以便能够向用户输出一些信息;网口初始化,以便能够从主机下载内核映像;输出一些菜单,以便用户选择执行所需要的功能。比如,用户可以选择从串口或网口下载内核映像到RAM中某个地址,然后运行这个内核。关于下载内核映像的实现,在后文会详细介绍。这里只看当内核映像已经存在于RAM中时,怎样才能把这个内核启动起来。

  4. 启动参数的传递

  启动Linux内核之前需要设置好一些必要的启动参数,这些参数以TAG列表的形式传递给内核。所谓TAG列表,就是多个TAG在内存空间中按顺序排列。每个TAG,其实都是一个结构体,每个结构体中又包含了一个头部结构体和一个内容结构体称。头部结构体指明了本TAG的类型、占用空间大小;所谓TAG的类型,就是一个宏定义,用一个确定的整数来识别该标记。内容结构体包含了该TAG的具体内容。

  下面以具体的例子做说明。

  在atag.h中就有:



Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

#define ATAG_CORE 0x54410001
#define ATAG_MEM 0x54410002
#define ATAG_CMDLINE 0x54410009
#define ATAG_NONE 0x00000000  这些都是TAG的类型,注意这些整数跟地址没有关系,只是一个用来识别标记类型的符号而已。

  每个Tag都用结构体表示,包含TagHeader 头结构体以及随后的参数值数据结构。如 ATAG_CORE



Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

struct Atag {

             struct TagHeader  stHdr;

             struct TagCore stCore;

};  其中包含两个结构体。第一个结构体TagHeader含两个整型变量,用以表示本结构体的长度、标记类型;nSzie赋值为头部TagHeader和数据TagCore的大小之和,注意是以字(4字节)为单位;ulTag 就赋值为先前定义的宏ATAG_CORE。第二个结构体就是实际的数据了。



Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

struct TagHeader {
UINT32 nSize;
UINT32 ulTag;
};


struct TagCore {
UINT32 ulFlags;
UINT32 nPageSize;
UINT32 ulRootDev;
};  由于每个Tag都由一个TagHeader加一个数据部分组成,因此通常的做法是使用StructUnion相结合来定义:



Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

struct Atag {
           struct TagHeader stHdr;
           union {
                  struct TagCore stCore;
                  struct TagMem32 stMem;
                  struct TagVideoText stVideoText;
                  struct TagRamDisk stRamDisk;
                  struct TagInitrd stInitRd;
                  struct TagSerialnr stSerialNr;
                  struct TagRevision stRevision;
                  struct TagVideolfb stVideoLfb;
                  struct TagCmdline stCmdLine;
           };
};  其中涉及到的所有数据结构均可在 Linux 内核源码的include/asm/setup.h 头文件找到,我们把这些定义放在Bootloader的头文件atag.h中。

【摘】<strong>自己</strong><strong>动手</strong><strong>编写</strong><strong>嵌入</strong>式Bootloader  启动参数标记列表以标记 ATAG_CORE 开始,以标记 ATAG_NONE 结束。每个标记由标识被传递参数的 tag_header 结构以及随后的参数值数据结构来组成。数据结构 tag 和 tag_header 定义在 Linux 内核源码的include/asm/setup.h 头文件中,在我们的S-Boot中对应的头文件为 atag.h

  在嵌入式 Linux 系统中,通常需要由 Boot Loader 设置的常见启动参数有:ATAG_COREATAG_MEMATAG_CMDLINEATAG_RAMDISKATAG_INITRD等。

  向内核传递参数的方法,先在内存中某个起始地址开始,连续存放多个Tag, 组成Tag列表。列表中的每个Tag包括头部TagHeader和数据结构体。按规定,第一个Tag必须是ATAG_CORE, 最末一个Tag必须是ATAG_NONE,而且中间必须包含至少一个ATAG_MEM。 注意的是末尾的ATAG_NONE只包括头部,没有数据内容。如图所示。

  在编程时先定义好起始地址,然后用一个指针,每设置完毕一个Tag的内容就向后移动相应的长度,然后设置下一个Tag内容,以保证各个Tag的连续存放。

  下面具体说明几个关键Tag的数据区域内容的设置。struct TagCore结构体已经在前面列出,它包含三个整型变量,ulFlags一般设为零,nPageSize表示分页内存管理中每一页的大小,一般为4096字节,ulRootDev是系统启动的设备号,设为零即可,因为通常在后面的命令行参数Cmdline中覆盖这个设置。Struct TagMem用来描述系统的物理内存地址空间,定义如下:



Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

struct atag_mem {
            UINT32 nSize; 
            UINT32 ulStart; 
};  其中nSzie表示内存的总大小,ulStart为内存的起始物理地址,二者结合告诉内核系统可用的物理内存空间是哪些。Struct TagCmdline结构体的定义就更简单了,只是一个字符数组,初始长度为1,如下所示:



Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

struct TagCmdline {
           char cCmdLine[1]; 
}; 

  实际上命令行参数不可能只有一个字节,我们通常使用strcpy函数把命令行参数拷贝到cCmdLine地址处,在结尾附加一个字符串结束符’\0’,然后用strlen函数获得cCmdLine数组的实际长度(包括字符串结束符)。常见的命令行参数如:root=/dev/mtdblock2 init=/linuxrc console=ttySAC0,115200 mem=65536。我们知道的是,Bootloader以标记列表的形式向内核传递的参数,大概有10种不同类型的Tag,而命令行参数只是其中的一种。其它需要设置的Tag包括ATAG_RAMDISKATAG_INITRD等,此处不再详细介绍。

  在我们的S-Boot中设置了ATAG_COREATAG_MEMATAG_CMDLINEATAG_NONE 四项。其中CmdLine 使用的是:

  const char *CmdLine "root=/dev/nfs nfsroot=192.168.1.249:/home/hongwang/mkrootfs/rootfs ip=192.168.1.252:192.168.1.249:192.168.1.1:255.255.255.0:hwlee.net:eth0:off console=ttySAC0,115200 init=/linuxrc mem=65536K console=tty1 fbcon=rotate:2";

  这里root=/dev/nfs表示使用NFS做根文件系统,注意并不真的存在/dev/nfs这个设备,它只是一个符号而已,告诉内核使用NFS而不是使用真正的设备做根文件系统。

  nfsroot=[:][,]

  nfsroot=192.168.1.249:/home/hongwang/mkrootfs/rootfsNFS服务器地址及要挂载的目录。

  ip=::::::

  ip=192.168.1.252:192.168.1.249:192.168.1.1:255.255.255.0:hwlee.net:eth0:off

  只说明一下autoconf,这一个选项指明开发板使用的自动配置IP地址的方法,有时开发板可以设置成通过DHCP或者BOOTP等协议从服务器获取IP地址。off 或 none 表示不使用自动配置,使用指定的静态IP地址信息。

  console=ttySAC0,115200 串口控制台

  console=tty1 fbcon=rotate:2 液晶屏Framebuffer控制台,如果内核支持,可以在LCD屏幕上显示Linux内核启动过程,起点结束后在LCD屏幕上进入Shell控制台供用户操作。fbcon=rotate:2表示控制台旋转180度,若为1表示旋转90度,3旋转270度,0不旋转。

内容导航 

  5. boot kernel zImage

  zImage 二进制文件包含两部分内容,起始部分是解压缩程序,后面是压缩的内核。解压缩程序是最先运行的,内核中文件是:arch/arm/boot /compressed/head.S,它负责把压缩的内核解压到0x30008000处。因此zImage可以下载到RAM任意位置处,由解压缩程序负责搬移到正确的运行地址。

  所以 Bootloader启动Linux内核的方法就是直接跳转到内核的第一条指令处,也就是跳转到内存中存放内核映像的开始地址,内核映像具有自解压功能,会把自己释放到正确的运行地址。Tag列表怎样传给内核呢?使用的方法是把Tag列表的起始地址传给内核。首先,定义一个指向函数的指针:

  typedef void (*LINUX_KERNEL_ENTRY)(int, int, UINT32);

  LINUX_KERNEL_ENTRY pfExecKernel;

  这样pfExecKernel就是一个函数指针,函数具有三个整型变量。然后,让pfExecKernel指向内核映像的起始地址处,这里使用强制类型转换把地址转换成函数指针类型:

  pfExecKernel (LINUX_KERNEL_ENTRY)pKernelStartAddr;

  最后,以三个参数调用pfExecKernel函数:

  pfExecKernel(0, MACH_ID, ATAG_BASE);

  其中第一个参数默认为零,可以不必理会。第二个参数是机器ID号,不同的CPU有不同的号码与之对应,可以在内核源代码的linux/arch/arm/tools/mach-types 文件中查到,S3C2440 对应的MACH_ID 362。第三个参数ATAG_BASE就是上文讲到的Tag列表的首地址。

  这个函数调用的作用其实就是设置 r0=0r1=机器IDr2=TAG首地址,然后跳到arch/arm/boot/compressed/head.S文件中的第一条指令处。

  既然可以把TAG首地址传递给内核,那么TAG LIST就可以放在RAM中的任何位置了,只要不与其它有用内容冲突即可。但是事实却并不是想象的这样。实验发现,第三个参数传递进去的TAG首地址似乎没有起到作用,因为启动时总是找不到正确的启动参数。后来发现内核有个默认的TAG首地址0x30000100,它总是到0x30000100去寻找启动参数,而不理会我们传进来的第三个参数。所以,S-Boot中把TAG首地址就设置为0x30000100

内容导航 

 6. 小结

  综上所述,包含最基本功能的S-Boot运行流程已经很清楚了。下图对此作了一个总结。

【摘】<strong>自己</strong><strong>动手</strong><strong>编写</strong><strong>嵌入</strong>式Bootloader

 

网友评论
<