Bootloader编写简明教程

更新时间:2023-06-04 23:25:12 阅读: 评论:0

Bootloader编写简明教程
第⼀部分:基本功能流程
CPU上电后会从IO空间的某地址取第⼀条指令。但此时:PLL没有启动,CPU⼯作频率为外部输⼊晶振频率,⾮常低;CPU⼯作模式、中断设置等不确定;存储空间的各个BANK(包括内存)都没有驱动,内存不能使⽤。在这种情况下必须在第⼀条指令处做⼀些初始化⼯作,这段初始化程序与操作系统独⽴分开,称之为bootloader。
实际上,很少有必要⾃⼰写⼀个Bootloader,因为U-Boot已经强⼤到能够满⾜各种需要。但是强⼤必然复杂,⼀个初学者想要分析U-Boot的源代码,还是有些难度的。出于学习的⽬的,我写了这个史上最简单的启动加载器,它只包含最基本的功能,却囊括了⼀个嵌⼊式Bootloader应该有的核⼼和精华。我把这个启动加载器命名为S-Boot,是Simple Bootloader的缩写,亦可进⼀步简称为SB。
使⽤的实验环境为OK2440开发板,板上处理器为S3C2440A,有64M内存,Nand存储器为
K9F1208,64M。⽹⼝芯⽚为CS8900A。我们要实现的功能是:从串⼝下载Linux内核映像到RAM;从⽹⼝下载Linux内核映像到RAM;从RAM启动内核挂载NFS根⽂件系统。
1. 第⼀阶段的汇编代码:start.S
⼀个嵌⼊式Bootloader最初始部分的代码⼏乎必须是⽤汇编语⾔写成的,因为开发板刚上电后没有准备好C程序运⾏环境,⽐如堆栈指针SP没有指到正确的位置。汇编代码应该完成最原始的硬件设备初始化,并准备好C运⾏环境,这样后⾯的功能就可以⽤C语⾔来写了。
英语试卷对我们的S-Boot来说,上电后的起始运⾏代码是 start/start.S。
.text
lick my boobs
.global _start
w o_start:
b Ret ; 0x00: 发⽣复位异常时从地址零处开始运⾏
b HandleUndef ; 0x04: 未定义指令中⽌模式的向量地址
b HandleSWI ; 0x08: 管理模式的向量地址,通过SWI指令进⼊此模式
b HandlePrefetchAbort ; 0x0C: 指令预取终⽌导致的异常的向量地址
b HandleDataAbort ; 0x10: 数据访问终⽌导致的异常的向量地址
b HandleNotUd ; 0x14: 保留
b HandleIRQ ; 0x18: 中断模式的向量地址
b HandleFIQ ; 0x1C: 快中断模式的向量地址
这⾥,汇编指⽰符.text表明以下内容属于代码段,.global _start指明_start是全局可访问的符号。按照ARM920T的规定,从地址0x00到0x1C放置异常向量表,向量表每个条⽬占四个字节,正好可以放置⼀条跳转指令,跳转到相应异常的服务程序中去。在S-Boot中没有使⽤中断,所以除Ret异常外,其它异常的服务程序都可简单地写个死循环。Ret 异常是系统上电后⾃动触发的,所以我们的代码都写在Ret的服务程序⾥⾯。
实际上,异常向量表不⼀定⾮要位于地址0x00处,CP15协处理器中的c1寄存器的第13位⽤来控制异常向量表的起始地址。该位为0时,异常向量表位于低地址0x00处;该位为1时,异常向量表位于⾼地址 0xFFFF0000处。我们没有必要改变这个位的值,使⽤默认的低地址就⾏了。
Ret:
mrs r0,cpsr ;t 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模式。
MMU、ICache、DCache的打开和关闭都是由CP15协处理器的c1寄存器控制的。实际上在复位之后这三者都是⾃动关闭的,所以省略了关闭它们的代码。
S3C2440A的PSR寄存器(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 意思是把r0的Bit[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模式。
ldr r0, =0x53000000
mov r1, #0x0
str r1, [r0] ;disable watch dog
禁⽤看门狗更简单,因为WTCON寄存器的地址为0x53000000,直接向该寄存器写0即可。到⽬前为⽌,CPU⼯作在外接晶振12MHz频率之下。使⽤以下代码设置PLL,提升⼯作频率。
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 0, must asynchronous bus mode
orr r0,r0,#0xC0000000 @e 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=400MHz,HCLK=100MHz,PCLK=50MHz。
; SDRAM Init
mov r1, #0x48000000 ; MEM_CTL_BASE
adrl r2, mem_cfg_val
add r3, r1, #52
1:
ldr r4, [r2], #4 ; 读取设置值,并让r2加4
str r4, [r1], #4 ; 将此值写⼊寄存器,并让r1加4
cmp r1, r3 ; 判断是否设置完所有13个寄存器
bne 1b ; 若没有写成,继续
设置存储控制器。
the heatldr sp, =0x32FFF000 ; 设置堆栈
bl nand_init ; 初始化NAND Flash
;nand_read_ll函数需要3个参数:
ldr r0, =0x33000000 ; ⽬标地址=0x30000000,SDRAM的起始地址
mov r1, #0 ;2. 源地址 =0,S-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:
b halt_loop
这⾥把所有的代码从Nand拷贝到RAM中,然后跳转到main函数去执⾏。此后程序便在RAM 中运⾏了。
但是到⽬前为⽌,前⾯的程序都是在SteppingStone⾥运⾏的。所谓SteppingStone,是指在S3C2440A的内部的4KB的RAM缓存,它总是映射到地址0x00处。硬件加电后会⾃动将Nand Flash中的前4KB的数据拷贝到Stepping Stone中,然后从地址0x00处开始运⾏。
如果代码⾜够⼩(⼩于4KB)的话,那只在SteppingStone中运⾏,加载Linux内核到内存即可。但通常代码肯定会⼤于4KB。所以Bootloader⼀般分为两部分,Stage1的代码在SteppingStone中运⾏,它会把Stage2的代码拷贝到RAM中,并跳转到RAM中执⾏;Stage2的代码在RAM中执⾏,它可以完成加载内核及其它任何复杂的功能。因为Stage2的起始位置不好确定,为了⽅便,我们把所有的代码都拷贝到RAM中了。
C 函数nand_read有三个参数,第⼀个参数为⽬的地起始地址,第⼆个参数为源起始地址,第三个参数为要复制的数据长度,以字节为单位。根据ATPCS 函数调⽤规则,三个参数分别⽤寄存器r0,r1,r2来传递。我们在内存的0x33000000处存放Bootloader,复制长度根据编译⽣成的S- Boot.bin映像⽂件⼤⼩,向上取512字节的整数倍。
这⾥先来规划⼀下内存空间的分配。RAM的地址范围是从0x30000000到0x34000000共
64MByte。把S-Boot和Kernel放在⾼地址处,S-Boot从 0x33000000开始,预留8MByte
的空间,内核从0x33800000开始,可供使⽤的空间也是8MByte。因栈空间是向下⽣长的,我们在 S-Boot下⾯预留4096Byte的空闲区域,然后向下为栈空间,故栈指针SP初始化为0x32FFF000。其实留不留空闲区域是⽆所谓的,这⾥只是为了把⼆者更明显地区分开。我们只设置SVC模式下的SP,不使⽤CPU的其它⼯作模式,所以也没必要设置其它模式下的栈指针。另外,程序中不使⽤动态内存分配,故⽽也不必分配堆空间。
2. nand读操作
在编译连接时,我们把上述 start.S 代码放在⽣成的⼆进制映像⽂件的最开始位置,因⽽也被烧写到 Nand Flash 的最起始位置,因⽽会被⾃动拷贝到 SteppingStone ⾥运⾏。start.S 要完成的任务之⼀,是把S-Boot的所有代码从Nand Flash拷贝到内存中,这⾥需要对NAND的读操作,因此对NAND的初始化和读操作要在第⼀阶段写好。
以开发板上使⽤的K9F1208为例,每个页(page)为512Byte数据和16Byte校验,每个块(Block)为32个页,即16KByte数据和512Byte校验。
Nand Flash只⽤8根线与CPU的DATA0-7连接,位宽为8位,不管是数据、地址或控制字都通过这8根线传递,如果读写数据的话每次只能传输⼀个字节数据。Nand Flash的操作通过NFCONF、NFCMD、NFADDR、NFDATA、NFSTAT和NFECC六个寄存器来完成。在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 Flash:S3C2440的NFCONF寄存器⽤来设置时序参数TACLS、TWRPH0、TWRPH1,设置数据位宽;还有⼀些只读位。TACLS、 TWRPH0、TWRPH1这三个参数控制的是Nand Flash 信号线CLE/ALE与写控制信号nWE的时序关系。
写英语作文
注意,寄存器值转换成实际的时钟周期值时,TACLS不需加1,⽽TWRPH0和TWRPH1需要加1。⽐如NFCONF寄存器中设置
TACLS=1,TWRPH0=3,TWRPH1=0,意思是时序图中 TACLS=1个HCLK时钟,TWRPH0=4个HCLK时钟,TWRPH1=1个HCLK时钟。
void nand_init(void)
{
//时间参数设为:TACLS=0 TWRPH0=3 TWRPH1=0
NFCONF = 0x300;
/* 使能NAND Flash控制器, 初始化ECC, 禁⽌⽚选 */
ladykillersNFCONT = (1<<4)|(1<<1)|(1<<0);
/* 复位NAND Flash */
面板英文NFCONT &= ~(1<<1); //发出⽚选信号
NFCMMD = 0xFF; //复位命令
s3c2440_wait_idle();//循环查询NFSTAT位0,直到它等于1
NFCONT |= 0x2; //取消⽚选信号
}
读操作:读操作也是以页(512Byte)为单位进⾏的。在初始上电时,器件进⼊缺省的“读⽅式1模式”。在这⼀模式下,页读操作通过将0x00写⼊指令寄存器,接着写⼊3个地址(1个列地址和2个⾏地址)来启动。⼀旦页读指令被器件锁存,下⾯的页读操作就不需要再重复写⼊页读指令了。写⼊页读指令和地址后,处理器可以通过对信号线R//B的分析来判断页读操作是否完成。如果信号为低电平,表⽰器件正忙;如果信号为⾼电平,表⽰器件内部操作完成,要读取的数据被送⼊了数据寄存器。外部控制器可以再以50ns为周期的连续/RE脉冲信号的控制下,从IO⼝依次读出数据。连续页读操作中,输出的数据是从指定的列地址开始,直到该页最后⼀个列地址的数据为⽌。
for(i=start_addr; i < (start_addr + size);)
{
NFCMMD = 0; //发出READ0命令
s3c2440_write_addr(i); //Write Address
s3c2440_wait_idle(); //循环查询NFSTAT位0,直到它等于1
for(j=0; j < NAND_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中就有:
#define ATAG_CORE 0x54410001
#define ATAG_MEM 0x54410002
#define ATAG_CMDLINE 0x54410009
#define ATAG_NONE 0x00000000
这些都是TAG的类型,注意这些整数跟地址没有关系,只是⼀个⽤来识别标记类型的符号⽽已。
每个Tag都⽤结构体表⽰,包含TagHeader 头结构体以及随后的参数值数据结构。如ATAG_CORE:
struct Atag {
struct TagHeader stHdr;
struct TagCore stCore;
};
其中包含两个结构体。第⼀个结构体TagHeader含两个整型变量,⽤以表⽰本结构体的长度、标记类型;nSzie赋值为头部TagHeader和数据TagCore的⼤⼩之和,注意是以字(即4字节)为单位;ulTag 就赋值为先前定义的宏ATAG_CORE。第⼆个结构体就是实际的数据了。
struct TagHeader {
UINT32 nSize;
UINT32 ulTag;
};
struct TagCore {
UINT32 ulFlags;
UINT32 nPageSize;
UINT32 ulRootDev;
};
由于每个Tag都由⼀个TagHeader加⼀个数据部分组成,因此通常的做法是使⽤Struct和Union相结合来定义:
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/tup.h 头⽂件找到,我们把这些定义放在Bootloader的头⽂件atag.h中。
启动参数标记列表以标记 ATAG_CORE 开始,以标记 ATAG_NONE 结束。每个标记由标识被传递参数的 tag_header 结构以及随后的参数值数据结构来组成。数据结构 tag 和
tag_header 定义在 Linux 内核源码的include/asm/tup.h 头⽂件中,在我们的S-Boot 中对应的头⽂件为 atag.h。
在嵌⼊式 Linux 系统中,通常需要由 Boot Loader 设置的常见启动参数有:ATAG_CORE、ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_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⽤来描述系统的物理内存地址空间,定义如下:
struct atag_mem {
UINT32 nSize;/* size of the area */
UINT32 ulStart;/* physical start address */
};
其中nSzie表⽰内存的总⼤⼩,ulStart为内存的起始物理地址,⼆者结合告诉内核系统可⽤的物理内存空间是哪些。Struct TagCmdline结构体的定义就更简单了,只是⼀个字符数组,初始长度为1,如下所⽰:
struct TagCmdline {
char cCmdLine[1];/* this is the minimum size */
};
出国留学签证实际上命令⾏参数不可能只有⼀个字节,我们通常使⽤strcpy函数把命令⾏参数拷贝到cCmdLine地址处,在结尾附加⼀个字符串结束符’\0’,然后⽤strlen函数获得cCmdLine 数组的实际长度(包括字符串结束符)。常见的命令⾏参数如:root=/dev/mtdblock2
采纳
init=/linuxrc console=ttySAC0,115200 mem=65536。我们知道的是,Bootloader以标记列表的形式向内核传递的参数,⼤概有10种不同类型的Tag,⽽命令⾏参数只是其中的⼀种。其它需要设置的Tag包括ATAG_RAMDISK、ATAG_INITRD等,此处不再详细介绍。
abloy
在我们的S-Boot中设置了ATAG_CORE,ATAG_MEM,ATAG_CMDLINE,ATAG_NONE 四项。其中CmdLine 使⽤的是:
const char *CmdLine = "root=/dev/nfs
nfsroot=192.168.1.249:/home/hongwang/mkrootfs/rootfs

本文发布于:2023-06-04 23:25:12,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/fan/90/134183.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:地址   设置   参数
相关文章
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图