实现一个简单的Bootloader

实现一个简单的Bootloader

1、bootloader的目标:启动内核

简化版本:
在这里插入图片描述

2、最简单的bootloader的编写步骤

uboot第1阶段

1、初始化硬件:关看门狗、设置时钟、设置SDRAM、nand flash
2、如果booloader比较大,把它重定位到SDRAM、清除bss段
3、把内核从nand flash 读到sdram
4、跳到第2阶段

uboot第2阶段

1、设置要传给内核的参数
2、跳转执行内核

3、编写bootloader第1阶段

3.1、在start.S中

第一阶段主要做以下操作:

关看门狗

在这里插入图片描述
在这里插入图片描述
将看门狗的控制寄存器第0位设置位0即可关闭:

	ldr r0, =0x53000000
	mov r1, #0
	str r1, [r0]	//向

设置时钟

在这里插入图片描述
设置寄存器CLKDIVN0x05设置FCLK:HCLK:PCLK=1:4:8

ldr r0, =0x4c000014
    //mov r1, #0x03;		  // FCLK:HCLK:PCLK=1:2:4, HDIVN=1,PDIVN=1
	mov r1, #0x05;			  // FCLK:HCLK:PCLK=1:4:8
	str r1, [r0]

设置cpu为异步模式:

	/* 如果HDIVN非0,CPU的总线模式应该从“fast bus mode”变为“asynchronous bus mode” */
	mrc	p15, 0, r1, c1, c0, 0	/* 读出控制寄存器 */ 
	orr	r1, r1, #0xc0000000		/* 设置为“asynchronous bus mode” */
	mcr	p15, 0, r1, c1, c0, 0	/* 写入控制寄存器 */

设置MPLL为200MHz:
在这里插入图片描述

	ldr r0, =0x4c000004
	ldr r1, =S3C2440_MPLL_400MHZ
	str r1, [r0]

初始化SDRAM

	ldr r0, =MEM_CTL_BASE	 /* MEM_CTL_BASE为SDRAM控制器的地址 */
	adr r1, sdram_config     /* 标号sdram_config的当前地址 */
	add r3, r0, #(13*4)		 /* r3 = r0 + 13*4 */
1:
	ldr r2, [r1], #4	/* 从r1所指的地方取一个值,存到r2,r1再加4 */
	str r2, [r0], #4	/* 从r2所指的地方取一个值,存到r0,r0再加4 */
	cmp r0, r3			/* 比较r0和r3,不相等就跳回前面的标号 1: */
	bne 1b

。。。
。。。
sdram_config:
	.long 0x22011110	 //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 0x008C04F4	 // REFRESH
	.long 0x000000B1	 //BANKSIZE
	.long 0x00000030	 //MRSRB6
	.long 0x00000030	 //MRSRB7

其中adr r1, sdram_config /* 标号sdram_config的当前地址 */代码的adr指令:
在这里插入图片描述
就是取78的地方读值:
在这里插入图片描述
之后就是取78地址读值,既设置SDRAM控制寄存器:
在这里插入图片描述

重定位

把bootloader本身的代码从flash复制到它的链接地址去

	ldr sp, =0x34000000	/*  将栈设置再64M的地方,最高的内存 */
	
	bl nand_init	/* 先初始化nand flash */
	
	mov r0, #0	/* copy_code_to_sdram的第1个参数 src */
	ldr r1, =_start	/* copy_code_to_sdram的第2个参数 dest */
	ldr r2, =__bss_start
	sub r2, r2, r1	/* copy_code_to_sdram的第3个参数 len */
	
	bl copy_code_to_sdram

跳转到第二阶段

清除BSS段

bl clear_bss

跳转到第二阶段c函数

	ldr lr, =halt	/* 设置main函数的返回地址 */
	ldr pc, =main	/* 绝对跳转,到SDRAM中执行 */
halt:
	b halt

3.2、在init.c中

判断nand或nor启动

int isBootFromNorFlash(void)
{
	volatile int *p = (volatile int *)0;
	int val;

	val = *p;
	*p = 0x12345678;
	if (*p == 0x12345678) {
		/* 写成功, 是nand启动 */
		*p = val;
		return 0;
	} else {
		/* NOR不能像内存一样写 */
		return 1;
	}
}

重定位

void copy_code_to_sdram(unsigned char *src, unsigned char *dest, unsigned int len)
{	
	int i = 0;
	
	if (isBootFromNorFlash()) { /* 如果是NOR启动 */
		while (i < len) {
			dest[i] = src[i];
			i++;
		}
	} else {	/* 如果是NAND启动 */		nand_read((unsigned int)src, dest, len);
	}
}

清除bss段

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

初始化nand flash

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);	
}

选中nand芯片

void nand_select(void)
{
    /* NFCONT的第1为为0,表示允许片选;为1禁止片选 
     * NFCONT的第0为为0,nand不工作;为1表示nand flash controller工作,在nand init中设置过了
     */
	NFCONT &= ~(1<<1);	
}

取消nand片选

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

发命令

void nand_cmd(unsigned char cmd)
{
	volatile int i;
	NFCMMD = cmd;
    /* 发出命令之后等待一会,立刻发可能会导致发不过去 */
	for (i = 0; i < 10; i++);   
}

发地址

void nand_addr(unsigned int addr)
{
	unsigned int col  = addr % 2048;    /* 列地址Column */
	unsigned int page = addr / 2048;    /* 页地址Row */
	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++);	
}

nand中发页地址

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++);	
}

nand中发列地址

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)
{
    /* NFSTAT的第0位
     * 为0:表示busy
     * 为1:表示ready
     */
	while (!(NFSTAT & 1));
}

读nand的数据

unsigned char nand_data(void)
{
	return NFDATA;
}

nand判断坏块

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;
}

nand重定位

void nand_read(unsigned int addr, unsigned char *buf, unsigned int len)
{
	int col = addr % 2048;  /* 第一次可能不是从第0列开始读 */
	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中根据发过来的地址需要寻找数据,
         * 放到Pages registers上,需要时间 
         * 根据NFSTAT寄存器来判断状态
         */
		nand_wait_ready();  
		/* 6. 读数据 */
        /* 这里读nand中Pages registers上2048既前2K数据,后面的OOB区不需要读 */
		for (; (col < 2048) && (i < len); col++) {
			buf[i] = nand_data();
			i++;
			addr++;
		}
		col = 0;
		/* 7. 取消选中 */		
		nand_deselect();
	}
}

3.3、第一阶段中重定位、nand、nor相关操作说明

对于重定位时,是从nand还是nor启动:

  • nand启动时,硬件将nand的前4K代码复制到片内SRAM中,重定位时前4K代码将nand所有代码复制到SDRAM中;
  • nor启动时,直接将nor代码复制到SDRAM中

在这里插入图片描述
nand flash的结构:
1页中有2K Bytes共2048个数据,还有64 Byts的OOB区,nand有位反转的缺陷,使用ECC进行校验:

  • 写数据时,写1页数据,生成ECC,将ECC写入OOB区
  • 读数据时,读1页数据,算出ECC,与OOB区里的ECC进行比较,使用特定算法进行校验

在这里插入图片描述
从上图可以看出,对于nand读写数据时,nand会将1页的数据先放在Page Register中,在对其进行寻址读写操作。
在这里插入图片描述
我们对nand读写数据时,需要发出页地址Row Address和列地址Column Address
在这里插入图片描述
nand读操作相关:2440板子上nand flash型号:
在这里插入图片描述

  • nand手册中写时序twp >= 15ns;2440中HCLK * (TWRPH0 + 1) >= 15,那么TWRPH0 >= 0.5,取1即可
  • 发出ALE/CLE之后,过多长时间能发出读写信号,2440中为TACLS,nand中为 tCLS - tWP
    nand手册中两者都为15ns,那么TACLS取为0
  • WE信号变为高电平,CLE/ALE还需要维持多长时间,nand中为tCLH >= 5ns;2440中HCLK x (TWRPH1 + 1) >= 5,TWRPH1 = 0

在这里插入图片描述
读操作:发出0命令、发出5个周期的地址、发出30命令,读地址
在这里插入图片描述
这些写完,第一阶段也结束了。就可以跳转到第二阶段运行了。

4、编写bootloader第2阶段

第2阶段从main函数开始,做三件事情:

4.1、从nand flash将内核读入SDRAM中

nand_read(0x60000+64, (unsigned char *)0x30008000, 0x200000);

参数一:从哪里读内核

开发板上内核是放在地址0x00060000处,共0x00200000大小
在这里插入图片描述
烧写内核使用的映像是uImage:
在这里插入图片描述
那么64K头部存在0x60000处,真正的内核存在0x60000 + 64k处。

参数二:内核读到哪里

内核存到0x30008000处:
在这里插入图片描述

参数三:读多大

读内核长度定为0x20000000(2M)大小。

4.2、为内核设置启动参数

设置参数之前先定义标记结构体tag

#include "setup.h"
static struct tag *params;

设置setup_start_tag

params结构体指向0x30000100处;
设置tag_header结构体hdr
设置tag_core联合体core
params结构体指向下一个tag

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);
}

设置setup_memory_tags

设置tag_header结构体hdr
设置tag_mem32联合体mem
params结构体指向下一个tag

void setup_memory_tags(void)
{
	params->hdr.tag = ATAG_MEM;
	params->hdr.size = tag_size (tag_mem32);
	
	params->u.mem.start = 0x30000000;   //内存SDRAM起始地址
	params->u.mem.size  = 64*1024*1024;	/* 大小64MB */
	
	params = tag_next (params);
}

设置setup_commandline_tag

设置tag_header结构体hdr
设置tag_cmdline联合体cmdline
需要先实现strlenstrcpy函数;

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

void strcpy(char *dest, char *src)
{
	while ((*dest++ = *src++) != '\0');
}

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);
}

设置setup_end_tag

设置tag_header结构体hdr
在这里插入图片描述

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

4.3、跳转到内核入口执行内核

定义函数指针

void (*theKernel)(int zero, int arch, unsigned int params);

函数指针指向内核地址

theKernel = (void (*)(int, int, unsigned int))0x30008000;

跳转执行内核

theKernel(0, 362, 0x30000100);
/* 在汇编中相当于下列操作
 *  mov r0, #0
 *  ldr r1, =362 //机器ID
 *  ldr r2, =0x30000100	//标记列表的位置(参数的位置)
 *  mov pc, #0x30008000 
 */

4.4、设置串口

帮内核设置串口: 内核启动的开始部分会从串口打印一些信息,但是内核一开始没有初始化串口:
在init.c中实现:uart0_init();

在init.c文件中

/* GPIO */
#define GPHCON              (*(volatile unsigned long *)0x56000070)
#define GPHUP               (*(volatile unsigned long *)0x56000078)

/* UART registers*/
#define ULCON0              (*(volatile unsigned long *)0x50000000)
#define UCON0               (*(volatile unsigned long *)0x50000004)
#define UFCON0              (*(volatile unsigned long *)0x50000008)
#define UMCON0              (*(volatile unsigned long *)0x5000000c)
#define UTRSTAT0            (*(volatile unsigned long *)0x50000010)
#define UTXH0               (*(volatile unsigned char *)0x50000020)
#define URXH0               (*(volatile unsigned char *)0x50000024)
#define UBRDIV0             (*(volatile unsigned long *)0x50000028)
#define TXD0READY   (1<<2)

#define PCLK            50000000    // init.c中的clock_init函数设置PCLK为50MHz
#define UART_CLK        PCLK        //  UART0的时钟源设为PCLK
#define UART_BAUD_RATE  115200      // 波特率
#define UART_BRD        ((UART_CLK  / (UART_BAUD_RATE * 16)) - 1)
/*
 * 初始化UART0
 * 115200,8N1,无流控
 */
void uart0_init(void)
{
    GPHCON  |= 0xa0;    // GPH2,GPH3用作TXD0,RXD0
    GPHUP   = 0x0c;     // GPH2,GPH3内部上拉

    ULCON0  = 0x03;     // 8N1(8个数据位,无较验,1个停止位)
    UCON0   = 0x05;     // 查询方式,UART时钟源为PCLK
    UFCON0  = 0x00;     // 不使用FIFO
    UMCON0  = 0x00;     // 不使用流控
    UBRDIV0 = UART_BRD; // 波特率为115200
}

/* 发送一个字符 */
void putc(unsigned char c)
{
    /* 等待,直到发送缓冲区中的数据已经全部发送出去 */
    while (!(UTRSTAT0 & TXD0READY));
    
    /* 向UTXH0寄存器中写入数据,UART即自动将它发送出去 */
    UTXH0 = c;
}

void puts(char *str)
{
	int i = 0;
	while (str[i]) {
		putc(str[i]);
		i++;
	}
}

void puthex(unsigned int val)
{
	/* 0x1234abcd */
	int i;
	int j;
	
	puts("0x");

	for (i = 0; i < 8; i++) {
		j = (val >> ((7-i)*4)) & 0xf;
		if ((j >= 0) && (j <= 9))
			putc('0' + j);
		else
			putc('A' + j - 0xa);	
	}	
}

在boot.c

	/* 0. 帮内核设置串口: 
	 * 内核启动的开始部分会从串口打印一些信息,
	 * 但是内核一开始没有初始化串口 
	 */
	uart0_init();
	/* 1. 从NAND FLASH里把内核读入内存 */
	...
	...

4.5、实现Makefile文件、lds链接文件

Makefile

/* 执行make时,生成第一个目标boot.bin
 * boot.bin依赖于$(objs)
 * $(objs)中.o文件依赖于.S文件;.o文件依赖于.c文件
 * 就是用对应的命令编译.S、.c文件
 *
 * 编译完之后使用链接命令${OBJCOPY} -O binary -S boot.elf $@
 * 将其链接为elf文件
 *
 * 使用${OBJCOPY} -O binary -S boot.elf $@
 * 将其转换为二进制文件
 * 
 * 使用${OBJDUMP} -D -m arm boot.elf > boot.dis
 * 反汇编
 */

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

其中:build-in表示内核自己实现的strcpy函数与自己实现的不一样,编译时加上-nostdinc-fno-builtin即可
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

lds链接文件

SECTIONS {
    . = 0x33f80000;
    .text : { *(.text) }
    
    . = ALIGN(4);	//向4取整
    .rodata : {*(.rodata*)} 
    
    . = ALIGN(4);
    .data : { *(.data) }
    
    . = ALIGN(4);
    __bss_start = .;
    .bss : { *(.bss)  *(COMMON) }
    __bss_end = .;
}

5、改进

提高频率

提高CPU频率,200MHz 升为 400MHz
MPLL:
在这里插入图片描述
FCLK:HCLK:PCLK=1:4:8
在这里插入图片描述

在start.S中:
#define S3C2440_MPLL_400MHZ     ((0x5c<<12)|(0x01<<4)|(0x01))

/* 2. 设置时钟 */
	ldr r0, =0x4c000014
	//	mov r1, #0x03;		  // FCLK:HCLK:PCLK=1:2:4, HDIVN=1,PDIVN=1
	mov r1, #0x05;			  // FCLK:HCLK:PCLK=1:4:8
	。。。
	。。。
	
	/* MPLLCON = S3C2440_MPLL_200MHZ */
	ldr r0, =0x4c000004
	ldr r1, =S3C2440_MPLL_400MHZ
	。。。
	。。。

设置ICACHE

不启用ICACHE的话,cpu每次都是到SDRAM中取指令然后执行;
启用ICACHE的话,指令会将SDRAM附近的指令存到ICACHE中,CPU想从ICACHE中找指令,速度就会很快。

DCACHE时数据缓存,需要启用MMU,暂时不可用。
在这里插入图片描述

	/* 启动ICACHE */
	mrc p15, 0, r0, c1, c0, 0	@ read control reg
	orr r0, r0, #(1<<12)
	mcr	p15, 0, r0, c1, c0, 0   @ write it back
/* 3. 初始化SDRAM */
	...
	...

6、测试uboot

使用oflash烧写uboot
oflash 0 1 0 0 0 .\boot.bin	//nand flash
oflash 0 1 1 0 0 .\boot.bin	//nor flash

可以在main函数中给内核传递参数:
1、从nand上的内核和根文件系统启动
setup_commandline_tag("noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0,115200");
2、从网络文件系统启动
setup_commandline_tag("set bootargs noinitrd root=/dev/nfs nfsroot=192.168.2.16:/work/nfs_root/first_fs ip=192.168.2.55:192.168.2.16:192.168.2.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC0,115200");

7、完整代码

girhub仓库

  • 4
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

「已注销」

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值