2019年(1)
分类: LINUX
2019-05-15 15:28:19
原文地址:自己动手编写嵌入式Bootloader之(1) 作者:pursuitxh
.text .global _start _start: b Reset @ 0x00: 发生复位异常时从地址零处开始运行 b HandleUndef @ 0x04: 未定义指令中止模式的向量地址 b HandleSWI @ 0x08: 管理模式的向量地址,通过SWI指令进入此模式 b HandlePrefetchAbort @ 0x0C: 指令预取终止导致的异常的向量地址 b HandleDataAbort @ 0x10: 数据访问终止导致的异常的向量地址 b HandleNotUsed @ 0x14: 保留 b HandleIRQ @ 0x18: 中断模式的向量地址 b HandleFIQ @ 0x1C: 快中断模式的向量地址 |
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 |
ldr r0, =0x53000000 mov r1, #0x0 str r1, [r0] @disable watch dog |
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 @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 ! |
@ 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 @ 若没有写成,继续 |
ldr sp, =0x32FFF000 @设置堆栈 bl nand_init @初始化NAND Flash @nand_read_ll函数需要3个参数: ldr r0, =0x33000000 @1. 目标地址=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 |
void nand_init(void) { //时间参数设为:TACLS=0 TWRPH0=3 TWRPH1=0 NFCONF = 0x300; /* 使能NAND Flash控制器, 初始化ECC, 禁止片选 */ NFCONT = (1<<4)|(1<<1)|(1<<0); /* 复位NAND Flash */ NFCONT &= ~(1<<1); //发出片选信号 NFCMMD = 0xFF; //复位命令 s3c2440_wait_idle();//循环查询NFSTAT位0,直到它等于1 NFCONT |= 0x2; //取消片选信号 } |
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++; } } |
#define ATAG_CORE 0x54410001 |
这些都是TAG的类型,注意这些整数跟地址没有关系,只是一个用来识别标记类型的符号而已。
每个Tag都用结构体表示,包含TagHeader 头结构体以及随后的参数值数据结构。如 ATAG_CORE:
struct Atag {
struct TagHeader stHdr;
struct TagCore stCore;
};
其中包含两个结构体。第一个结构体TagHeader含两个整型变量,用以表示本结构体的长度、标记类型;nSzie赋值为头部TagHeader和数据TagCore的大小之和,注意是以字(即4字节)为单位;ulTag 就赋值为先前定义的宏ATAG_CORE。第二个结构体就是实际的数据了。
struct TagHeader { struct TagCore { |
由于每个Tag都由一个TagHeader加一个数据部分组成,因此通常的做法是使用Struct和Union相结合来定义:
struct Atag { |
其中涉及到的所有数据结构均可在 Linux 内核源码的include/asm/setup.h 头文件找到,我们把这些定义放在Bootloader的头文件atag.h中。
启动参数标记列表以标记 ATAG_CORE 开始,以标记 ATAG_NONE 结束。每个标记由标识被传递参数的 tag_header 结构以及随后的参数值数据结构来组成。数据结构 tag 和 tag_header 定义在 Linux 内核源码的include/asm/setup.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 { |
其中nSzie表示内存的总大小,ulStart为内存的起始物理地址,二者结合告诉内核系统可用的物理内存空间是哪些。Struct TagCmdline结构体的定义就更简单了,只是一个字符数组,初始长度为1,如下所示:
struct TagCmdline { |
实际上命令行参数不可能只有一个字节,我们通常使用strcpy函数把命令行参数拷贝到cCmdLine地址处,在结尾附加一个字符串结束符’\0’,然后用strlen函数获得cCmdLine数组的实际长度(包括字符串结束符)。常见的命令行参数如:root=/dev/mtdblock2 init=/linuxrc console=ttySAC0,115200 mem=65536。我们知道的是,Bootloader以标记列表的形式向内核传递的参数,大概有10种不同类型的Tag,而命令行参数只是其中的一种。其它需要设置的Tag包括ATAG_RAMDISK、ATAG_INITRD等,此处不再详细介绍。
在我们的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 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/rootfs是NFS服务器地址及要挂载的目录。
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不旋转。
4. boot kernel zImagezImage 二进制文件包含两部分内容,起始部分是解压缩程序,后面是压缩的内核。解压缩程序是最先运行的,内核中文件是: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=0,r1=机器ID,r2=TAG首地址,然后跳到arch/arm/boot/compressed/head.S文件中的第一条指令处。
既然可以把TAG首地址传递给内核,那么TAG LIST就可以放在RAM中的任何位置了,只要不与其它有用内容冲突即可。但是事实却并不是想象的这样。实验发现,第三个参数传递进去的TAG首地址似乎没有起到作用,因为启动时总是找不到正确的启动参数。后来发现内核有个默认的TAG首地址0x30000100,它总是到0x30000100去寻找启动参数,而不理会我们传进来的第三个参数。所以,S-Boot中把TAG首地址就设置为0x30000100。
综上所述,包含最基本功能的S-Boot运行流程已经很清楚了。下图对此作了一个总结。