文章目录
实现一个简单的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] //向
设置时钟
设置寄存器CLKDIVN
为0x05
设置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
;
需要先实现strlen
和strcpy
函数;
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");