从零开始写一个简单的bootloader(1)

前言

        之前学习嵌入式裸板程序也有一定的时间了,而一个bootloader则是裸板程序的一个集大成者,能包含很多的知识点,所以编写一个bootloader能巩固之前的所学。废话少讲,下面就开始正式的编写。

目标

      相信搞嵌入式的都知道bootloader是什么东西,这里我就不作详细介绍,简单说一下我编写的bootloader要做什么工作。bootloader的终极目标就是为了要引导内核,并启动内核,为了达到这个目标我们需要做这么几个工作:

  1. 初始化硬件:关看门狗(否则几秒钟后就会复位整个单板)、设置系统时钟、初始化SDRAM、初始化NAND flash(因为我的内核是烧写在NAND flash的,要初始化才能读)
  2. 如果bootloader比较大,我们还需要把它重定位到SDRAM中(为什么这么做后面会讲)
  3. 把内核从flash中读到SDRAM
  4. 设置要传递给内核的参数
  5. 跳转执行内核

     下面的程序我们就会按照这几个步骤进行编写。

Bootloader的编写

我会先给出完整的代码,再分步按照上面的顺序,对每一步做出详细的解释,废话不多讲,请看代码:

#define MEM_CTL_BASE 0x48000000

.text
.global _start
_start:

/* 1.关看门狗 */
	ldr r0, =0x53000000  /*当值比较大的时候,不能直接使用mov,只能用ldr*/
	mov r1, #0
	str r1, [r0]  /*关看门狗就是把零值写入到固定的地址*/

/* 2.设置时钟 */
	/*设置MPLL, FCLK:HCLK:PCLK = 400MHz : 100MHz : 50MHz*/
	/*为了保险先初始化一下locktime*/
	ldr r0, =0x4C000000
	ldr r1, =0xFFFFFFFF
	str r1, [r0]

	/*设置CLKDVIN:
	  *HDIVN[2:1] :    10  -  HCLK=FCLK/4
	  *PDVIN[0]    :     1   -   PCLK=HCLK/2
	  */
	ldr r0, =0x4C000014
	ldr r1, =0x5
	str r1, [r0]

	/*设置CPU为异步模式*/
	mrc p15,0,r0,c1,c0,0
	orr r0,r0,#0xc0000000  //R1_nF:OR:R1_iA 
	mcr p15,0,r0,c1,c0,0

	/*
	  *Mpll = (2 * m * Fin) / (p * 2S) 
	  *m = (MDIV + 8), p = (PDIV + 2), s = SDIV
	  *当Fin=12MHz,若要 Mpll=400MHz,则有MDIV=92, PDIV=SDIV=1
	  */
	ldr r0, =0x4C000004
	ldr r1, =(92<<12) | (1<<4) | (1<<0)
	str r1, [r0]

	/*一旦设置了PLL,就会锁定locktime直到PLL输出稳定
	  *然后CPU工作于新的频率
	  */

	/*启动ICACHE*/
	mrc p15, 0, r0, c1, c0, 0
	orr r0, r0, #(1<<12)
	mcr p15, 0, r0, c1, c0, 0

/* 3.初始化SDRAM */
	ldr r0, =MEM_CTL_BASE
	adr r1, sdram_config   /*sdram_config的当前地址*/
	add r3, r0, #(13*4)
1:
	ldr r2, [r1], #4 /*让r1地址的值读到r2,让后r1加4,也就是指向下一个地址*/
	str r2, [r0], #4 /*让r2的值写入到r0地址的寄存器,r0加上4,指向下一个地址*/
	cmp r0, r3 /*不断的循环把sdram_config里面的值写入到BWSCON开始的寄存器里面*/
	bne 1b /*b的含义代表调到这行代码前面的1,如果是1f就代表下面的1*/

/* 4.重定位: 把bootloader本身的代码从flash复制到链接地址去 */
	/*查看芯片手册第196页可知,我们的内存为64MB,
	  *基地址为0x30000000,所以sp指向最高地址就可以了,
	  *因为栈是向下增长,0x30000000加上64MB等于0x34000000
	  */
	ldr sp, =0x34000000 //设置栈
	bl nand_init //不管是nor还是nand启动,都需要初始化nand flash,因为内核存在nand flash上
	/*设置copy_code_to_sdram的三个参数*/
	mov r0, #0  //src的值就是0地址
	ldr r1, =_start  //目的地址就是连接地址,就是脚本开始的_start变量
	ldr r2, =__bss_start
	sub r2, r2, r1  //bss段开始地址减去链接地址就是二进制文件的长度了

	bl copy_code_to_sdram

	/*清除bss段,也就是赋为零值*/
	bl clean_bss

/* 5.执行main函数 */
	ldr lr, =halt //如果main函数有返回值,则跳入halt循环,避免单板调到未知的地方
	ldr pc, =main
halt:
	bl halt


sdram_config:
	.long 0x22000000 //BWSCON
	.long 0x00000700 //BANKCON0
	.long 0x00000700 //BANKCON1
	.long 0x00000700 //BANKCON2
	.long 0x00000700 //BANKCON3
	.long 0x00000700 //BANKCON4
	.long 0x00000700 //BANKCON5
	.long 0x00018001 //BANKCON6
	.long 0x00018001 //BANKCON7
	.long 0x008404f5 //REFRESH
	.long 0x000000b1 //BANKSIZE
	.long 0x00000020 //MRSRB6
	.long 0x00000020 //MRSRB7

(1)关看门狗

关看门狗这步没什么讲的,按照芯片手册,给特定的寄存器写0值

(2)设置时钟

时钟的设置也不难,也就是根据芯片手册,设置一些寄存器的值。可以参考我的这篇文章:S3C2440芯片的时钟体系结构

(3)启动ICACHE

上面的汇编代码中有个启动ICACHE的设置,这个作用主要是为了加快指令的执行,从而加快bootloader的启动

(4)初始化SDRAM

       SDRAM的初始化其实也是根据自己的需求,参考芯片手册,去设置一些寄存器的值,至于每个寄存器设置的值的含义,可以参考我这篇文章:S3C2440芯片的SDRAM初始化设置

        这里需要解释一下的就是MEM_CTL_BASE(0x48000000)的值就是寄存器BWSCON的地址

(5)重定位

这里我先解释一下为什么需要重定位:

        我们板子中有nor flash、SDRAM和nand flash,还有一个4k的片内内存SRAM。

        CPU能直接访问的地方有:nor flash、SDRAM、SRAM和各种控制器(包括NAND flash控制器)。所以当我们的程序烧写到SDRAM或者NOR flash的时候,程序能直接运行。但是如果烧写到NAND flash,芯片会把程序的头4K先拷贝到SRAM中执行,如果NAND flash中的程序小于4K的话,程序还能正常运行,如果大于4K,那大于4K的这部分就运行不了。

        所以我们就引入了重定位,NAND flash的代码中的前4K的代码中需要把整个代码拷贝到SDRAM去执行。

        另外,对于NOR FLASH来说,虽然能在上面执行代码,但是我们却无法写NOR FLASH,所以一旦程序中有需要写的变量,比如全局变量和静态变量,我们在无法在NOR FLASH上直接修改它们的值。因此,我们还是需要将代码重定位到SDRAM中去执行。

       这里重定位用到了链接脚本的知识,下面我先给出这次bootloader的链接脚本:

SECTIONS {
	. = 0x33f80000;

	__code_start = .;
	
	. = ALIGN(4);
	.text   :
	{
		*(.text)
	}

	. = ALIGN(4);
	.rodata :
	{
		*(.rodata)
	}

	. = ALIGN(4);
	.data   :
	{
		*(.data)
	}

	. = ALIGN(4);
	__bss_start = .;
	.bss    :
	{
		*(.bss) *(.COMMON)
	}
	__bss_end = .;
}

         一开始的值0x33f80000,表示的是链接的地址,也就是代码运行时地址的起始地址,这个地址也可以是别的值,只要它在我们的内存地址中,且加上bootloader的大小后不会超出内存的最大地址即可。

         由一开始给出汇编代码可以看出,我们把所有从地址0开始的代码都拷贝到地址0x33f80000开始的内存中:

	mov r0, #0  //src的值就是0地址
	ldr r1, =_start  //目的地址就是连接地址,就是脚本开始的_start变量
	ldr r2, =__bss_start
	sub r2, r2, r1  //bss段开始地址减去链接地址就是二进制文件的长度了

	bl copy_code_to_sdram

简单解释一下汇编代码的含义:

1、把地址0作为第一个参数(如果是NAND启动,就是NAND FLASH零地址的位置,如果是NOR启动,就是NOR FLASH 零地址的位置),为拷贝代码的源地址;

2、_start(为代码一开始的地址,也就是链接脚本中定义的0x33f80000)作为第二个参数,为代码拷贝的目的地址;

3、而要拷贝的代码有多长呢?这里就要用到有关ELF文件中BSS段的知识。我们都知道编译出来的bin文件是不包含BSS段的,BSS段存放的是未初始化的全局变量和静态变量,所以我们可以把它想象为初始化为0值,如果bin文件中存放一堆0值的变量是很浪费空间的。至于更加深层的原因可以参考这篇文章:bss段不占据磁盘空间的理解。这里我们就知道,拷贝的代码长度为BSS段开始的地址减去代码的起始地址,也就是__bss_start - _start 。

现在,我们来看一下copy_code_to_sdram函数怎么对代码进行重定位,也就是代码的拷贝

/*知识背景:
  *对于nand flash: 开机启动的时候,从0地址开始的前4k内容会被拷贝到
  *芯片的片内0地址开始的RAM里面,并在RAM的0地址开始执行,所以
  *我们可以读写0地址开始的内容。
  *而对于nor flash : 是能直接在nor flash读的,但是不能写,开机启动
  *是在nor flash的0地址处开始执行,所以我们能读但是写不了。
  */
int isBootFromNorFlash(void)
{
	volatile int *p = (volatile int *)0;
	int val;

	val = *p;
	*p = 0x12345678;
	if(*p == 0x12345678)
	{
		/*nand flash启动*/
		*p = val;
		return 0;
	}
	else
	{
		/*nor flash启动*/
		return 1;
	}
}

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

上面的代码都有注释,从代码来看也比较直观。先判断是NOR启动还是NAND启动,如果是NOR启动就很简单,直接对内容进行赋值。如果是NAND启动,则篇幅会比较长,涉及到NAND FLASH的读操作。为了保持整个讲解流程的简洁,就不在这里展开,放到我的这篇文章里面:NAND FLASH的读操作及原理

(6)清除BSS段

清除BSS段其实就是直接赋零0,不多讲,直接上代码

void clean_bss(void)
{
	extern int __bss_start, __bss_end;
	int *p = &__bss_start;

	for (; p<&__bss_end; p++)
		*p = 0;
}

后续

        到这里,我们的汇编代码部分就大概梳理了一遍,最后会跳转到main函数里面去执行,里面会做一些串口的初始化、传递给内核的参数的一些设置、把内核读到内存、以及跳转去执行内核部分。

猜你喜欢

转载自blog.csdn.net/lee_jimmy/article/details/82079342