bootloader的实现

在上一篇博客里比较深入地分析了uboot是如何启动内核(uboot博大精深,原谅我用“深入”这个词),所以这一篇博客里来看下如何实现一个bootloader。显然我们自己的写的bootloader不可能有uboot那样强大,我们的目的就是实现bootloader的最核心功能——加载并启动内核

关闭看门狗

.text
.global _start
_start:
	ldr r0, =0x53000000 //ldr的寻址的参数如果带有等于号,说明这是一条伪汇编指令
	mov r1, #0 			//立即数寻址需要带上#做标识
	str r1, [r0]

设置时钟

在这里插入代码片

初始化SDRAM
是时候SDRAM,实际上是配置和SDRAM相关的13个寄存器,而这13个相关的寄存器在内存上是连续的,所以可以采用如下循环的方式来操作,之前我们用C语言时,是采用的数组的方式来赋值的

#define MEM_CTL_BASE 0x48000000
	ldr r0, =MEM_CTL_BASE
	adr r1, sdram_config /*将sdram_config的当前地址赋值给r1*/
	add r3, r0, #(4*13)
1:	
	ldr r2, [r1], #4 /*从对应的内存中读值,然后让r1的值加4,这样就可以让r1指向下一个数值所在的内存了*/
	str r2, [r0], #4 /*因为寄存器是连续的,所以可以用这种方式来寻址寄存器*/
	cmp r0, r3
	bne 1b
	
sdram_config:
	.long 0x22111110		//BWSCON
	.long 0x00000700		//BANKCON0
	.long 0x00000700		//BANKCON1
	.long 0x00000700		//BANKCON2
	.long 0x00000700		//BANKCON3	
	.long 0x00000700		//BANKCON4
	.long 0x00000700		//BANKCON5
	.long 0x00018005		//BANKCON6
	.long 0x00018005		//BANKCON7
	.long 0x008e07a3		//REFRESH,HCLK=12MHz:0x008e07a3,HCLK=100MHz:0x008e04f4
	.long 0x000000b2		//BANKSIZE
	.long 0x00000030		//MRSRB6
	.long 0x00000030		//MRSRB7

重定位代码 之前的博客中我们是用汇编语言写的,并且把重定位的地址写在了链接脚本中,在启动文件中可以直接从链接脚本中获取信息,有点像设备树。这次就用C语言来写,在这之前需要设置栈,同是也需要将code的所在地址和目标地址以及需要复制代码的长度作为参数传递给C函数,汇编向C函数传递参数需要遵循ATPCS规则,这里参数只有3个,我们可以将参数放在寄存器中
重定位 是将代码从我们烧写的地址复制到我们在链接脚本指定的地址里,由于这里使用的重定位c函数需要传递参数,所以在汇编阶段把三个传参按照顺序传给r0, r1, r2三个寄存器,其中第一个参数是代码源的地址,因为我们会将其烧写到0地址,所以为0;传参2是代码重定位的目标地址,是在链接脚本中指定;而第三个参数是需要复制多长的代码,应该是我们链接脚本的中指定的末尾到起始地址

	ldr sp, =0x34000000
	mov r0, #0x0  
	ldr r1, =_start
	ldr r2, =_bss_start			/*需要复制代码的长度需要根据链接脚本*/
	sub r2, r2, r1
	bl copy_code_for_sdram
	bl clear_bss

boot.lds 链接脚本

SECTIONS {
	. = 0x33f80000;
	.text : {*(.text)}
	
	. = ALIGN(4);
	.rodata : {*(.rodata)}
	
	. = ALIGN(4);
	.data : {*(.data)}
	
	. = ALIGN(4);
	__bss_start = .;
	.bss : {*(.bss) *(COMMON)}
	__bss_end = .;
}
int copy_code_for_sdram(unsigned char *src, unsigned char *dest, unsigned int len)
{
    int i;
    
    if (isBootFromNorFlash())
    {
	   /* 从 NOR Flash启动 */
        for (i = 0; i < len; i++)
        {
            dest[i] = src[i];
        }
        return 0;
    }
    else
    {
        /* 初始化NAND Flash */
		nand_init();
        /* 从 NAND Flash启动 */
        nand_read(start_addr, buf, (size + NAND_BLOCK_MASK_LP)&~(NAND_BLOCK_MASK_LP));
		return 0;
    }
}

清除bss段

void clear_bss(void)
{
	extern int _bss_start, _bss_end;
	int *p = &_bss_start;
	
	for(; p < &_bss_end, p++)
	{
		*p = 0;
	}
}

执行main函数,不能用bl main相对跳转,而用ldr pc addr绝对跳转的方式

	ldr lr, =halt /*pc是指当前正在运行的地址,而lr则是将要传给pc的指令,当main函数返回时候,就会返回到lr中的地址*/
	ldr pc, =main
halt:
	b halt	

在这里启动文件中,最为复杂的部分是nand flash的操作,已经写了一篇专门介绍nand flash的读操作的博客

#define TXD0READY   (1<<2)

void nand_init(void)
{
#define TACLS   0
#define TWRPH0  1
#define TWRPH1  0
	/* 设置时序 */
	NFCONF = (TACLS<<12)|(TWRPH0<<8)|(TWRPH1<<4);
	/* 使能NAND Flash控制器, 初始化ECC, 禁止片选 */
	NFCONT = (1<<4)|(1<<1)|(1<<0);	
}

void nand_select(void)
{
	NFCONT &= ~(1<<1);	
}

void nand_deselect(void)
{
	NFCONT |= (1<<1);	
}

void nand_cmd(unsigned char cmd)
{
	volatile int i;
	NFCMD = cmd;
	for (i = 0; i < 10; i++);
}

void nand_addr(unsigned int addr)
{
	unsigned int col  = addr % 2048;
	unsigned int page = addr / 2048;
	volatile int i;

	NFADDR = col & 0xff;
	for (i = 0; i < 10; i++);
	NFADDR = (col >> 8) & 0xff;
	for (i = 0; i < 10; i++);
	
	NFADDR  = page & 0xff;
	for (i = 0; i < 10; i++);
	NFADDR  = (page >> 8) & 0xff;
	for (i = 0; i < 10; i++);
	NFADDR  = (page >> 16) & 0xff;
	for (i = 0; i < 10; i++);	
}

void nand_page(unsigned int page)
{
	volatile int i;
	
	NFADDR  = page & 0xff;
	for (i = 0; i < 10; i++);
	NFADDR  = (page >> 8) & 0xff;
	for (i = 0; i < 10; i++);
	NFADDR  = (page >> 16) & 0xff;
	for (i = 0; i < 10; i++);	
}

void nand_col(unsigned int col)
{
	volatile int i;

	NFADDR = col & 0xff;
	for (i = 0; i < 10; i++);
	NFADDR = (col >> 8) & 0xff;
	for (i = 0; i < 10; i++);
}


void nand_wait_ready(void)
{
	while (!(NFSTAT & 1));
}

unsigned char nand_data(void)
{
	return NFDATA;
}

int nand_bad(unsigned int addr)
{
	unsigned int col  = 2048;
	unsigned int page = addr / 2048;
	unsigned char val;

	/* 1. 选中 */
	nand_select();
	
	/* 2. 发出读命令00h */
	nand_cmd(0x00);
	
	/* 3. 发出地址(分5步发出) */
	nand_col(col);
	nand_page(page);
	
	/* 4. 发出读命令30h */
	nand_cmd(0x30);
	
	/* 5. 判断状态 */
	nand_wait_ready();

	/* 6. 读数据 */
	val = nand_data();
	
	/* 7. 取消选中 */		
	nand_deselect();


	if (val != 0xff)
		return 1;  /* bad blcok */
	else
		return 0;
}


void nand_read(unsigned int addr, unsigned char *buf, unsigned int len)
{
	int col = addr / 2048;
	int i = 0;
		
	while (i < len)
	{

		if (!(addr & 0x1FFFF) && nand_bad(addr)) /* 一个block只判断一次 */
		{
			addr += (128*1024);  /* 跳过当前block */
			continue;
		}

		/* 1. 选中 */
		nand_select();
		
		
		/* 2. 发出读命令00h */
		nand_cmd(0x00);

		/* 3. 发出地址(分5步发出) */
		nand_addr(addr);

		/* 4. 发出读命令30h */
		nand_cmd(0x30);

		/* 5. 判断状态 */
		nand_wait_ready();

		/* 6. 读数据 */
		for (; (col < 2048) && (i < len); col++)
		{
			buf[i] = nand_data();
			i++;
			addr++;
		}
		
		col = 0;


		/* 7. 取消选中 */		
		nand_deselect();
		
	}
}

再来写main函数,main函数主要做的事是将内核读取出来,并复制到指定的区域,同时在启动文件和内核约定好的内存区域里,将内核启动需要的参数保存在其中;然后调用kernel来启动内核。
1、从flash中读取内核到内存中
用上上面实现的nand_read函数就可以了,关键是要知道内核放在flash上的哪里,在这里我们是将内核放在了flash上的kernel分区里,内核的目标地址是0x30008000,读取长度可以稍微选大一点,我们这里kernel区域大小为0x200000byte
2、设置参数
依然是需要参考u-boot的内容,我把需要参照的代码列在下面。
有两个疑问?这些参数放在哪里呢?或者说是在哪里指定了这些参数的位置呢?以及怎么让内核启动时可以寻址到这些参数呢?
这些参数的位置可以根据setup_start_tag函数来判断,在启动函数thekernel中,我们会把参数的起始地址作为传递参数告知内核,实际上的操作是把这个参数传递给寄存器r2.
第二个疑问,这些参数是干什么用的呢?具体这些参数做什么用,在下一篇关于内核的博客我会详细描述,但是我们可以看到在setup_memory_tags和setup_commandline_tag两个函数中,前者设置了内存的起始地址和内存的大小,后者则是保存了bootrags。

3、跳转执行
因为跳转是u-boot的最后一步,内核启动的第一步,当执行到这一步时,已经没有任何软件基础了,我们只能用硬件来操作,执行跳转命令的是一个函数指针,可以参照u-boot定义这个函数指针来定义,然后将我们内核的起始地址转化为thekernel型函数指针,然后执行这一函数指针。

void do_bootm_linux (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[],
		     ulong addr, ulong *len_ptr, int verify)
{
	...
	#if defined (CONFIG_SETUP_MEMORY_TAGS) || \
	    defined (CONFIG_CMDLINE_TAG) || \
	    defined (CONFIG_INITRD_TAG) || \
	    defined (CONFIG_SERIAL_TAG) || \
	    defined (CONFIG_REVISION_TAG) || \
	    defined (CONFIG_LCD) || \
	    defined (CONFIG_VFD)
		setup_start_tag (bd);
	#ifdef CONFIG_SERIAL_TAG
		setup_serial_tag (&params);
	#endif
	#ifdef CONFIG_REVISION_TAG
		setup_revision_tag (&params);
	#endif
	#ifdef CONFIG_SETUP_MEMORY_TAGS
		setup_memory_tags (bd);
	#endif
	#ifdef CONFIG_CMDLINE_TAG
		setup_commandline_tag (bd, commandline);
	#endif
	#ifdef CONFIG_INITRD_TAG
		if (initrd_start && initrd_end)
			setup_initrd_tag (bd, initrd_start, initrd_end);
	#endif
	#if defined (CONFIG_VFD) || defined (CONFIG_LCD)
		setup_videolfb_tag ((gd_t *) gd);
	#endif
		setup_end_tag (bd);
	#endif
	...
}
static void setup_start_tag (bd_t *bd)
{
	params = (struct tag *) bd->bi_boot_params;

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

	params->u.core.flags = 0;
	params->u.core.pagesize = 0;
	params->u.core.rootdev = 0;

	params = tag_next (params);
}

static void setup_memory_tags (bd_t *bd)
{
	int i;

	for (i = 0; i < CONFIG_NR_DRAM_BANKS; i++) {
		params->hdr.tag = ATAG_MEM;
		params->hdr.size = tag_size (tag_mem32);

		params->u.mem.start = bd->bi_dram[i].start;
		params->u.mem.size = bd->bi_dram[i].size;

		params = tag_next (params);
	}
}

static void setup_commandline_tag (bd_t *bd, char *commandline)
{
	char *p;

	if (!commandline)
		return;

	/* eat leading white space */
	for (p = commandline; *p == ' '; p++);

	/* skip non-existent command lines so the kernel will still
	 * use its default command line.
	 */
	if (*p == '\0')
		return;

	params->hdr.tag = ATAG_CMDLINE;
	params->hdr.size =
		(sizeof (struct tag_header) + strlen (p) + 1 + 4) >> 2;

	strcpy (params->u.cmdline.cmdline, p);

	params = tag_next (params);
}

static void setup_end_tag (bd_t *bd)
{
	params->hdr.tag = ATAG_NONE;
	params->hdr.size = 0;
}

main.c

#include "setup.h"


extern void uart0_init(void);
extern void nand_read(unsigned int addr, unsigned char *buf, unsigned int len);
extern void puts(char *str);


static void setup_start_tag (void)
{
	params = (struct tag *)0x30000100;

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

	params->u.core.flags = 0;
	params->u.core.pagesize = 0;
	params->u.core.rootdev = 0;

	params = tag_next (params);
}

static void setup_memory_tags (void)
{
	/*这里我们只设置一个内存区域,所以不需要循环*/
	params->hdr.tag = ATAG_MEM;
	params->hdr.size = tag_size (tag_mem32);

	params->u.mem.start = 0x300000000;
	params->u.mem.size = 0x4000000;

	params = tag_next (params);
}

int strlen(char *str)
{
	int i = 0;
	while(str[i])
		i++;

	return i;
}

void strcpy(char *dest, char *src)
{
	int i = 0;

	while(src[i])
	{
		*dest[i] = *src[i]; 
		i++;
	}
}

static void setup_commandline_tag (char *cmdline)
{
	int len = strlen(cmdline) + 1;
	params->hdr.tag = ATAG_CMDLINE;
	params->hdr.size =
		(sizeof (struct tag_header) + len + 3) >> 2;

	strcpy (params->u.cmdline.cmdline, cmdline);

	params = tag_next (params);
}

static void setup_end_tag (void)
{
	params->hdr.tag = ATAG_NONE;
	params->hdr.size = 0;
}



int main(void)
{
	void (*theKernel)(int zero, int arch, unsigned int params);
	/*从flash中读出内核到内存中*/
	nand_read(0x60000+64,(unsigned char *)0x30008000, 0x200000);

	/*设置参数*/
	setup_start_tag();
	setup_memory_tags();
	setup_commandline_tag("noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0");
	setup_end_tag();
	

	/*跳转的kernel函数指针,执行启动内核*/
	theKernel = (void (*)(int, int, unit))0x30008000;
	theKernel(0, 362, 0x30000100);

	/*如果上面一步执行成功,bootloader就执行完毕了,所以当下面程序被执行,说明有问题*/
	puts("error!\n\r");

	return -1;
}

Makefile

CC      = arm-linux-gcc
LD      = arm-linux-ld
AR      = arm-linux-ar
OBJCOPY = arm-linux-objcopy
OBJDUMP = arm-linux-objdump

CFLAGS 		:= -Wall -O2
CPPFLAGS   	:= -nostdinc -nostdlib -fno-builtin

objs := start.o init.o boot.o

boot.bin: $(objs)
	${LD} -Tboot.lds -o boot.elf $^
	${OBJCOPY} -O binary -S boot.elf $@
	${OBJDUMP} -D -m arm boot.elf > boot.dis
	
%.o:%.c
	${CC} $(CPPFLAGS) $(CFLAGS) -c -o $@ $<

%.o:%.S
	${CC} $(CPPFLAGS) $(CFLAGS) -c -o $@ $<

clean:
	rm -f *.o *.bin *.elf *.dis

在修改了语法错误后,生成了bin文件,将文件烧写后,启动开发板,发现没有任何反应;
1、检查是否是汇编代码start.S文件的问题
对于硬件初始化的代码,我们只能用硬件的方式来测试,可以对当前代码进行简单修改,然后来看下是否可以实现点灯
2、对于c代码中的问题,最好的方式就是串口输出,可以在main函数中加入输出信息,看执行到哪一步发生了问题。
main.c

int main(void)
{
	void (*theKernel)(int zero, int arch, unsigned int params);
	volatile unsigned int *p = (volatile unsigned int *)0x30008000;

	uart0_init();
	
	/* 1. 从NAND FLASH里把内核读入内存 */
	puts("Copy kernel from nand\n\r");
	nand_read(0x60000+64, (unsigned char *)0x30008000, 0x200000);

	/* 设置参数 */
	puts("Set boot params\n\r");
	setup_start_tag();
	setup_memory_tags();
	setup_commandline_tag("noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0");
	setup_end_tag();

	/*跳转的kernel函数指针,执行启动内核*/
	puts("Boot kernel\n\r");
	theKernel = (void (*)(int, int, unsigned int))0x30008000;
	theKernel(0, 362, 0x30000100);  
	/* 
	 *  mov r0, #0
	 *  ldr r1, =362
	 *  ldr r2, =0x30000100
	 *  mov pc, #0x30008000 
	 */

	puts("Error!\n\r");
	/* 如果一切正常, 不会执行到这里 */

	return -1;
}

结果发现这三条信息都在串口上打印出来了
Copy kernel from nand
Set boot params
Boot kernel
然后就输出乱码。那这个时候问题出现在哪里呢?从打印信息来看,程序可以执行到boot kernel这里来,是否代表着在此之前的代码都没有问题呢?显然不是,如果nand_read函数有问题,加载内核到内存上有问题,仅仅只会影响main函数的内核的启动,而不会影响这之前代码的运行。对于参数设置setup tags也一样,即使参数设置有问题 ,仅仅只会对内核启动有影响
所以在这里串口打印,仅仅让我们知道问题出现在main函数中三步执行中
如是我们来一个个排除
1、是不是nand_read加载内核到内存中有问题?好像不是,因为我们设置nand flash启动开发板,有串口信息输出,nand flash启动重定位代码时,将nand flash中的代码定位到内存中正是用nand_read函数,说明nand_read函数没有问题
2、是不是参数设置出了问题
因为参数的设置基本上是参考的u-boot,反复做了检查,cmdline是和内核参数设置是一样的结果发现没有问题
3、是不是启动除了问题
和启动有关的只有4行代码,好像也没有发现出问题

问题其实出现在nand_read函数中,一个非常低级的错误

void nand_read(unsigned int addr, unsigned char *buf, unsigned int len)
{
	int col = addr / 2048;

addr / 2048这获得是页地址,就是确认在哪一页;如果是确认在哪一列就取膜运算。
那为什么nand flash启动没有问题呢,因为nand启动中我们经常将代码烧写到0地址,0是小于2048的,所以取模运算和取余数运算是一样的结果。

成功启动内核

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值