之前学习嵌入式裸板程序也有一定的时间了,而一个bootloader则是裸板程序的一个集大成者,能包含很多的知识点,所以编写一个bootloader能巩固之前的所学。废话少讲,下面就开始正式的编写。
相信搞嵌入式的都知道bootloader是什么东西,这里我就不作详细介绍,简单说一下我编写的bootloader要做什么工作。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
关看门狗这步没什么讲的,按照芯片手册,给特定的寄存器写0值
时钟的设置也不难,也就是根据芯片手册,设置一些寄存器的值。可以参考我的这篇文章:S3C2440芯片的时钟体系结构
上面的汇编代码中有个启动ICACHE的设置,这个作用主要是为了加快指令的执行,从而加快bootloader的启动
SDRAM的初始化其实也是根据自己的需求,参考芯片手册,去设置一些寄存器的值,至于每个寄存器设置的值的含义,可以参考我这篇文章:S3C2440芯片的SDRAM初始化设置
这里需要解释一下的就是MEM_CTL_BASE(0x48000000)的值就是寄存器BWSCON的地址
这里我先解释一下为什么需要重定位:
我们板子中有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的读操作及原理。
清除BSS段其实就是直接赋零0,不多讲,直接上代码
void clean_bss(void)
{
extern int __bss_start, __bss_end;
int *p = &__bss_start;
for (; p<&__bss_end; p++)
*p = 0;
}
到这里,我们的汇编代码部分就大概梳理了一遍,最后会跳转到main函数里面去执行,里面会做一些串口的初始化、传递给内核的参数的一些设置、把内核读到内存、以及跳转去执行内核部分。