全部博文(69)
分类: LINUX
2011-03-31 13:13:47
主要参考:Li Hongwang大侠的代码
排版效果不是很好,可以下载附件中 word看^_^
这里,主要实现了以下三点:
1、 从nandflash启动内核;
2、 从sdram启动内核;
3、 利用kermit协议下载内核到sdram中去。
首先,从nandflash启动内核,这里有一个前提就是nandflash要分好区,并且nandflash要有内核映像zImage,但是到目前为止,lboot还没有实现nandflash的分区和下载内核到nandflash的功能,所以暂且借用supervivi首相将nandflash分好区,并下载内核映像zImage到内核分区。如果以后时间充足的话,还要实现类似supervivi的格式化nandflash,为nandflash分区,下载东西到nandflash中,我想格式化nandflash的话就是再实现一个擦除nandflash的函数,前面的nand.c只实现了nand的读函数,而下载东西到nandflash中就是在实现一个nand写函数,而分区呢,个人觉得可以定义几个宏,分别代表每个区的启始地址,然后在实现比如往nandflash中烧写bootloader的时候就往nand写函数传入bootloader的起始地址,烧写内核映像的时候,就传入内核分区的起始地址就可以了,具体的等看看vivi的源码是怎么实现的,然后在米葫芦画瓢的实现。好了,现在假设在nand的0x00060000处已经存放的linux内核映像zImage,那么怎么实现从nand启动内核呢?其实很简单,简单的调用nandread函数将内核映像拷贝到sdram中的0x3380,0000地址处,拷贝完之后再跳到这个地址就可以启动内核了,代码实现如下:
/********** SDRAM and Nand Space Allocation ****************/
#define SDRAM_TOTAL_SIZE 0x04000000 //64M SDRAM
#define SDRAM_ADDR_START 0x30000000 //起始地址是0x3000,0000
#define SDRAM_LBOOT_START 0x33000000 //LBOOT的存放地址
#define SDRAM_TAGS_START 0x30000100 //tag列表的存放地址
#define SDRAM_KERNEL_START 0x33800000 //kernel的存放地址
#define NAND_KERNEL_START 0x00060000
#define NAND_KERNEL_SZIE 0x00500000
int boot_linux()
{
setup_core_tag( (void *)SDRAM_TAGS_START ); /* standard stCore ulTag 4k pagenSize */
setup_mem_tag(SDRAM_ADDR_START, SDRAM_TOTAL_SIZE); /* 64Mb at 0x30000000 */
setup_cmdline_tag( CmdLine );
setup_end_tag(); /* end of ulTags */
s3c2440_nand_init();
printf("copy Kernel from nandflash to SDRAM... ...\n\r");
s3c2440_nand_read((unsigned char *)SDRAM_KERNEL_START, NAND_KERNEL_START, NAND_KERNEL_SZIE);
printf("copy Kernel from nandflash to SDRAM has finished!\n\r");
printf("Now jump to the kernel entry!\n\r");
theKernel = (void (*)(int, int, unsigned int))SDRAM_KERNEL_START;
theKernel(0, S3C2440_MATHINE_TYPE, SDRAM_TAGS_START);
return 0;
}
说道这里不得不谈下内核的启动过程,我们知道bootloader的最终目的是做什么呢?就是要实现启动内核的功能,那么我们现在bootloader已经完成了硬件的初始化,同时也为内核的启动准备好了条件,那么现在要做的就是传递内核启动参数给内核,然后内核接收这些参数并启动,为什么要传递参数呢,linux内核是强大的,支持N种不同的开发板,所以你要告诉内核我是什么板子?比如我是mini2440开发板,那么linux内核就会调用mini2440相关的函数来实现在mini2440这块板子上运行,当然前提是你要为你的板子移植好linux内核。
那么bootloader和内核参数之间是怎么传递的呢?是通过tag list的形式传递的,网上很多讲这方面的内容,我只阐述一个大概的思想:bootloader和内核使用约定俗成的形式定义一系列的参数,然后bootloader把内核需要的信息放在这些参数里面,之后将这个参数放在一个地址处,然后将这个地址传给内核,内核就会到这个地址处来读这些参数,这样就完成内核参数的传递了。
参数的统一形式如下:
struct Atag {
struct TagHeader stHdr;
union {
struct TagCore stCore;
struct TagMem32 stMem;
struct TagCmdline stCmdLine;
}u;
};
struct TagHeader {
unsigned int nSize; //以字为单位即4个字节
unsigned int ulTag;
};
Atag由tag头部TagHeader和内容u来组成,TagHeader有两个参数:nSize代表整个Atag的大小,ulTag代表这个Atag是什么类型的,类型定义如下:
#define ATAG_NONE 0x00000000 //必须有,且放在最后一个位置,只有头部,没有内容
#define ATAG_CORE 0x54410001 //必须有,且放在第一个位置
#define ATAG_MEM 0x54410002 //代表内存
#define ATAG_CMDLINE 0x54410009 //代表命令行参数
数字没有什么具体的含义,就是一种标识,按照linux内核里面的定义值即可。
所以在跳转到内核之前,一定要先把这些参数的值设好,设置的函数如下:
static struct Atag *pCurTag; /* used to point at the current ulTag */
void (*theKernel)(int, int, unsigned int);
const char *CmdLine = "root=/dev/mtdblock3 console=ttySAC0,115200 mem=64M init=/linuxrc";
static void setup_core_tag(void *pStartAddr)
{
pCurTag = (struct Atag *)pStartAddr; /* start at given address */到时侯传递0x30000100,即参数列表在sdram中存放地址
pCurTag->stHdr.ulTag = ATAG_CORE; /* start with the stCore ulTag */类型
pCurTag->stHdr.nSize = TAG_SIZE(TagCore); /* nSize the ulTag */大小
pCurTag->u.stCore.ulFlags = 1; /* ensure read-only */内容
pCurTag->u.stCore.nPageSize = 4096; /* systems pagenSize (4k) */
pCurTag->u.stCore.ulRootDev = 0; /* zero (typicaly overidden from commandline )*/
pCurTag = TAG_NEXT(pCurTag); /* move pointer to next ulTag */
}
static void setup_mem_tag(unsigned int start, unsigned int len) //设置内存参数
{
pCurTag->stHdr.ulTag = ATAG_MEM; /* Memory ulTag */
pCurTag->stHdr.nSize = TAG_SIZE(TagMem32); /* nSize ulTag */
pCurTag->u.stMem.ulStart = start; /* Start of memory area (physical address) */
pCurTag->u.stMem.nSize = len; /* Length of area */
pCurTag = TAG_NEXT(pCurTag); /* move pointer to next ulTag */
}
static void setup_cmdline_tag(const char * line)
{
int linelen = strlen(line);
if(!linelen)
return; /* do not insert a ulTag for an empty commandline */
pCurTag->stHdr.ulTag = ATAG_CMDLINE; /* Commandline ulTag */
pCurTag->stHdr.nSize = (sizeof(struct TagHeader) + linelen + 1+4) >> 2;
strcpy(pCurTag->u.stCmdLine.cCmdLine,line); /* place commandline into ulTag */
pCurTag = TAG_NEXT(pCurTag); /* move pointer to next ulTag */
}
static void setup_end_tag(void)
{
pCurTag->stHdr.ulTag = ATAG_NONE; /* Empty ulTag ends list */
pCurTag->stHdr.nSize = 0; /* zero length */
}
这里参数是以链表的形式存放的,赋值主要设计两个宏:
#define TAG_NEXT(t) ((struct Atag *)((unsigned int *)(t) + (t)->stHdr.nSize))
#define TAG_SIZE(type) ((sizeof(struct TagHeader) + sizeof(struct type)) >> 2)//左移2的目的前面说过size是以字为单位即4个字节,左移2相当于除以4,这样就可以算出以4个字节为单位,参数所占的大小了。
还有一点要说明的是:
pCurTag->stHdr.nSize = (sizeof(struct TagHeader) + linelen + 1+4) >> 2;
这里为什么要+1+4,+1代表着加上’\0’,因为strlen并没有将‘\0’算上,+4为什么呢?比如我实际长度只有2,那么以4字节为单位的话,长度应该是1,你钥匙直接用2/4的话,那么长度为0了,加上4就能保证长度为1,主要就是这么一样意思。
所以在刚上来从nand启动时,我们首先要将参数设置好,然后在复制内核映像到SDRAM中,然后跳到内核入口执行内核映像,那么怎么跳到内核入口呢,其实就是跳到内核的存放地址即可:
void (*theKernel)(int, int, unsigned int);
theKernel = (void (*)(int, int, unsigned int))SDRAM_KERNEL_START;
theKernel(0, S3C2440_MATHINE_TYPE, SDRAM_TAGS_START)
这里首先将内核存放地址强制转换,然后赋给函数指针theKernel,然后传入机器码,内核参数列表地址即可。这样,就可以从nandflash启动内核了。
接下来:实现从SDRAM启动内核,思路是一样的,看实现代码:
int boot_linux_sdram()
{
setup_core_tag( (void *)SDRAM_TAGS_START ); /* standard stCore ulTag 4k pagenSize */
setup_mem_tag(SDRAM_ADDR_START, SDRAM_TOTAL_SIZE); /* 64Mb at 0x30000000 */
setup_cmdline_tag( CmdLine );
setup_end_tag(); /* end of ulTags */
theKernel = (void (*)(int, int, unsigned int))SDRAM_KERNEL_START;
theKernel(0, S3C2440_MATHINE_TYPE, SDRAM_TAGS_START);
return 0;
}
这里就要求我们首先在SDRAM_KERNEL_START处存放有内核映像,那么我们就得首先实现下载内核映像到SDRAM中的功能,这里利用的kermit协议来实现了,关于kermit协议的介绍,我会在附件中贴出,里面介绍的很详细。
要实现利用kermit协议使用串口下载内核映像到SDRAM中,我们要做的工作就是实现接收部份的代码,因为我们是从PC->开发板,发送部份的代码是由PC来实现的,如PC上的C-Kermit软件,而接收部份的代码是很简单的,主要实现发送ACK包就可以了,看下面的程序前,首先要把附件中kermit协议看了,里面的包格式,使用的编码方式等。发送ACK包是通过写串口和我们开发板上的串口联系起来的,看程序:
发送ACK包程序:
#define KERM_ACK_LEN 0x10
static void send_ack_packet(unsigned int seq) //发送ACK包
{
unsigned char buf[KERM_ACK_LEN];
int index = 0, check_sum = 0;
buf[index++] = MARK_START; //MARK段:SOH
buf[index++] = ENC_PRINT(3); //LEN段:0x3+0x20
buf[index++] = seq + KERM_KEY_SPACE; //SEQ段:第几个包
buf[index++] = KERM_TYPE_ACK; //TYPE段:ACK包
buf[index] = '\0';
index = 1; //CHECK段只计算LEN段和SEQ段
while (buf[index])
{
check_sum += buf[index]; //采用单字节算术校验和
index++;
}
buf[index++] = (KERM_KEY_SPACE + (0x3f & (check_sum + (0x03 & (check_sum >> 6))))) & 0xff;//在编码之前把第6、7两位提出加到低6位上
buf[index++] = KERM_KEY_TERM;
buf[index] = '\0';
#ifdef CONFIG_TOPHALF_KERMIT
UartPuts((const char *)buf);
#else
index = 0;
while (buf[index])
GtUartWriteByte(buf[index++]); //发送ACK包,使用串口发送
#endif
}
接收文件程序:
#define KERM_BUF_LEN 128
int GtSerialLoad(void *pLoadAddr) //接收kermit send发送过来的数据到pLoadAddr地址处,pLoadAddr就为内核在SDRAM
//中的存放地址
{
unsigned char buf[KERM_BUF_LEN];
unsigned char curr_char;
int index;
int check_sum, len, seq, type;
unsigned char *pAddr = (unsigned char *)pLoadAddr; //pAddr = 内核在SDRAM中的存放地址
do {
while(MARK_START != GtUartReadByte()); //没收到启始包,循环等待
index = 0;
while(KERM_KEY_TERM != (buf[index] = GtUartReadByte())) //接收一包数据
index++;
index = 0;
/* length decode */
len = buf[index++];
check_sum = len;
len -= KERM_KEY_SPACE; //解码,编码是+0X20,解码时-0x20
/* sequence decode */
seq = buf[index++];
check_sum += seq;
seq -= KERM_KEY_SPACE;
/* get package type */
type = buf[index++];
check_sum += type;
if (len) //fixme: handle extended length //根据协议要减去两个字节
len -= 2;
while (len > 1) {
curr_char = buf[index++];
check_sum += curr_char;
len--;
if (type != KERM_TYPE_DATA) //接收数据包
continue;
if (curr_char == KERM_KEY_SHARP) /* '#' */ //数据包中的控制字符
{
curr_char = buf[index++];
check_sum += curr_char;
len--;
if (0x40 == (curr_char & 0x60))
curr_char = curr_char & (~0x40);
else if (0x3f == (curr_char & 0x7f))
curr_char |= 0x40;
}
*pAddr++ = curr_char;
}
/* checksum */
curr_char = buf[index++]; //校验数据是否正确
if (curr_char != (KERM_KEY_SPACE + (0x3f & (check_sum + (0x03 & (check_sum >> 6))))))
#ifdef CONFIG_TOPHALF_KERMIT
goto Loop;
#else
goto Error;
#endif
/* terminator */
curr_char = buf[index++];
if (curr_char != KERM_KEY_TERM)
#ifdef CONFIG_TOPHALF_KERMIT
goto Loop;
#else
goto Error;
#endif
/* send ack package */
send_ack_packet(seq); /发送ACK包
}while(type != KERM_TYPE_BREAK); //收到'B'代表文件传送结束
return pAddr - (unsigned char *)pLoadAddr; //返回接收文件的大小
#ifdef CONFIG_TOPHALF_KERMIT
Loop:
while (1)
GtUartWriteByte('E');
#else
Error:
#endif
printf("Error\n\r");
return -1;
}
下载完之后,我们在执行int boot_linux_sdram() 就可以实现从SDRAM启动了。
最后讲下Makefile:Makefile实现的功能是:每个文件夹内都有自己的Makefile,根目录下的主Makefile会进入各个子目录并调用各自的Makefile。每个子目录下的Makefile把自己编译的代码链接成一个build-in.o文件, 主Makefile把各个子目录下的build-in.o链接成一个可执行文件。
这里学着Li Hongwang大侠的做法:
CC = arm-lwm-linux-gnueabi-gcc
LD = arm-lwm-linux-gnueabi-ld
AR = arm-lwm-linux-gnueabi-ar
OBJCOPY = arm-lwm-linux-gnueabi-objcopy
OBJDUMP = arm-lwm-linux-gnueabi-objdump
INCLUDEDIR := $(shell pwd)/include
CFLAGS := -Wall -O2
CPPFLAGS := -nostdinc -fno-builtin -I$(INCLUDEDIR)
BUILT_IN_OBJ = built-in.o
export CC LD AR OBJCOPY OBJDUMP INCLUDEDIR CFLAGS CPPFLAGS
export BUILT_IN_OBJ
SUBDIRS = start device lib app main
/*可以google下foreach的用法*/
SUBOBJS := $(foreach n, $(SUBDIRS), $(n)/$(BUILT_IN_OBJ))
all: lboot.bin lboot.dis
lboot.bin : lboot.elf
${OBJCOPY} -O binary -S $^ $@
lboot.dis : lboot.elf
${OBJDUMP} -D -m arm $^ > $@
lboot.elf : $(SUBOBJS)
${LD} -Tlboot.lds -o $@ $^
$(SUBOBJS) : $(SUBDIRS)
.PHONY : $(SUBDIRS)
$(SUBDIRS) :
@make -C $@ all
.PHONE : clean
clean:
rm -f lboot.dis lboot.bin lboot.elf *.o
@for subdir in $(SUBDIRS); do \
(make clean -C $$subdir); \
done
总结:
可以这么说,到目前为止,基本上都是参考Li Hongwang大侠的博客及代码做的,只是添加了自动识别norflash启动和nandflash启动,并修改了nandflash部份的代码,因为我的nandflash是128M,nandflash从128M是一个分界点,页的大小不一样了。
下面又要到关键的部份了,因为我的网卡是DM9000,^_^,在实现网口下载功能之前或者之后,我可能还会实现nandflash的格式化,nandflash的下载,nandflash的分区,之后我可能会根据我的开发板完成一些测试的功能,如测试LED,ADC,按键等。
好了,想起一位老师的一句话,既然你已经选择了读研,那么你肯定对你有着更高的期望,那么就让我们带着梦想,珍惜在校的每一天,继续努力吧。
lboot_bootlinux_v1.0.rar lboot_bootlinux_v2.0规范.rar kermit协议介绍.rar
效果图: