文章主题
bootloader 是什么?如果你看到了这篇文章,肯定已经知道答案了,所以这里就不赘述了。这篇文章主要是根据韦东山老师的视频,从零开始写一个最简单的 bootloader,每一行代码都是手动输入。虽然直接看一遍视频,也能够理解其中的步骤或者原理,但是根据视频敲一遍之后,印象才是最深刻的。
内容导航
测试平台
开发环境:电脑 MAC Pro,安装2个虚拟机 Windows7 和Ubuntu14.04。
开发板:JZ2440-V3,这块板子上已经集成了 USB转串口芯片。
烧录工具:OpenJTAG,连接开发板的 JTAG 接口即可。
测试工具:串口,直接连接 MicroUSB 口到 PC 的 USB 口,串口工具 SecureCRT。
文件目录结构
start.S:第一个启动的程序,完成的功能是:
init.c:串口的初始化和发送数据,NandFlash 的初始化和读取数据,BSS 段初始化。
setup.h:主要是定义了向内核传递参数时所需要的 tag 结构体。
boot.c:main 函数,设置 bootloader 向内核传递的启动参数。
boot.lds:连接脚本。
Makefile:make 编译指令。
代码详解
我比较赞成“代码即注释”这样的编程风格,只要看代码中的注释,就能明白其中的逻辑。所以,这里只贴出代码,不再进行进一步解释说明了。
1、start.S
#define S3C2440_MPLL_200MHZ ((0x5c<<12) | (0x01<<4) | (0x02))
#define MEM_CTL_BASE 0x48000000
.text
.global _start
_start:
/*
1.
关看门狗
*/
ldr r0,
=0x53000000
mov r1,
#0
str r1,
[r0]
/*
2.
设置时钟
*/
ldr r0,
=0x4c000014
mov r1,
#0x03 /* FLCK:HCLK:PCLK = 1:2:4, HDIVN=1, PDIVN=1*/
str r1,
[r0]
/*
如果 HDIVN 非0, CPU 的总线模式应该从
"fast bus mode"
改为
"asynchronous bus mod"*/
mrc p15,
0, r1, c1, c0,
0
/*
读出控制寄存器
*/
orr r1, r1,
#0xc0000000 /* 设置为 "asynchronous bus mod" */
mcr p15,
0, r1, c1, c0,
0
/*
写入控制寄存器
*/
/*
设置 MPLLCON */
ldr r0,
=0x4c000004
ldr r1,
=S3C2440_MPLL_200MHZ
str r1,
[r0]
/*
3.
初始化SDRAM */
ldr r0,
=MEM_CTL_BASE
adr r1, sdram_config
add r3, r0,
#(13*4)
1:
ldr r2,
[r1],
#4
str r2,
[r0],
#4
cmp r0, r3
bne 1b
/* b表示向前跳转到标号1的地方*/
/*
4.
重定位:把bootloader本身的代码,从flash复制到它的链接地址上去
*/
ldr sp,
=0x34000000
/*
把堆栈设置为 SDRAM 的最顶端,因为是向下增长的
*/
bl nand_init /*
初始化 NAND Flash
*/
mov r0,
#0 /* 第一个参数:源地址 */
ldr r1,
=_start /*
第二个参数:目标地址,即链接地址*/
ldr r2,
=__bss_start
sub r2, r2, r1 /*
第三个参数:代码长度*/
bl copy_code_to_sdram /*
调用代码拷贝函数
*/
bl clean_bss /*
调用 bss 段清零函数
*/
/*
5.
执行 main */
ldr lr,
=halt
ldr pc,
=main
halt:
b halt
/* SDRAM 13
个控制器的值
*/
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
2、init.c
/*
Nand
Flash
相关寄存器
*/
#define NFCONF (*((volatile unsigned long *)0x4E000000))
#define NFCONT (*((volatile unsigned long *)0x4E000004))
#define NFCMMD (*((volatile unsigned char *)0x4E000008))
#define NFADDR (*((volatile unsigned char *)0x4E00000C))
#define NFDATA (*((volatile unsigned char *)0x4E000010))
#define NFSTAT (*((volatile unsigned char *)0x4E000020))
/*
串口 GPIO 配置*/
#define GPHCON (*((volatile unsigned char *)0x56000070))
#define GPHUP (*((volatile unsigned char *)0x56000078))
/*
串口寄存器
*/
#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 PCLK 50000000 // init.c
#define UART_CLK PCLK // UART0 的时钟源设置为 PCLK
#define UART_BAUD_RATE 115200 // 波特率
#define UART_BRD ((UART_CLK / (UART_BAUD_RATE * 16)) - 1)
#define TXD0READY (1<<2)
#define RXD0READY (1)
extern void nand_read(unsigned int addr, unsigned char*buf, unsigned int len);
/* UART0 初始化
*/
void uart0_init(void)
{
GPHCON |=
0xa0;
// GPH2,GPH3 用作 TXD0,RXD0
GPHUP =
0x0c;
// GPH2,GPH3内部上拉
ULCON0 =
0x03;
//
8N1
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;
}
unsigned char getc(void)
{
/*
等待,直到接受缓冲区有数据
*/
while
(!(UTRSTAT0 & RXD0READY));
/*
直接读取 URXH0 寄存器,即可获得接收到的数据
*/
return URXH0;
}
void puts(char *str)
{
int i =
0;
while(str[i])
{
putc(str[i]);
i++;
}
}
/*
*
Check
if boot from Nor
Flash
*
Return:
1-Yes,
0-No
*/
int isbootFromNorFlash(void)
{
volatile int *p =
(volatile int *)0;
int val;
val =
*p;
*p =
0x12345678;
if
(*p ==
0x12345678)
{
/* write data success, boot form Nand
Flash
*/
*p = val;
return
0;
}
else
{
/* write data failed, boot from Nor
Flash
*/
return
1;
}
}
/*
*
Init
Nand
Flash
Device
*
Return:
None
*/
void nand_init(void)
{
/*
NandFlash
的读写需要满足一定的时序,
首先查找 NAND FLASH 手册,确定每个信号需要持续的时间,以及两个信号之间需要等待的时间。
然后查找
2440
手册,确定这些时间要设置的是哪个寄存器的哪些bit位,然后通过公式计算出应该设置的值*/
#define TACLS 0
#define TWRPH0 3
#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)
{
unsigned int i;
NFCMMD = cmd;
for
(i =
0;i <
10; i++);
}
void nand_addr(unsigned int addr)
{
volatile unsigned int i;
unsigned int col = addr %
2048;
unsigned int page = addr /
2048;
/*
发出列地址
*/
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_wait_ready(void)
{
while
(!(NFSTAT &
1));
}
char nand_data(void)
{
return NFDATA;
}
/*
*
Read data from Nand
Flash
*
Return:
None
*/
void nand_read(unsigned int addr, unsigned char*buf, unsigned int len)
{
unsigned int i =
0;
int col = addr%2048;
/*
1.
选中
*/
nand_select();
while
(i < len)
{
/*
2.
发出读命令
0x00
*/
nand_cmd(0x00);
/*
3.
发出地址(分5步发出)
*/
nand_addr(addr);
/*
4.
发出读命令
0x30
*/
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();
}
/*
*
Copy code from NorFlash or NandFlash to SDRAM
*
Return:
None
*/
void copy_code_to_sdram(unsigned char *src, unsigned char *dest, unsigned int len)
{
unsigned int i =
0;
if
(isbootFromNorFlash())
{
while
(i < len)
{
dest[i]
= src[i];
i++;
}
}
else
{
nand_read((unsigned int)src, dest, len);
}
}
/*
*
Clear BSS Section
*
Return:
None
*/
void clean_bss(void)
{
extern int __bss_start, __bss_end;
int *p =
&__bss_start;
for
(; p <
&__bss_end; p++)
*p =
0;
}
3、setup.h
主要是 tag 数据结构,这里就不贴全部代码了,如果需要可以直接从 uboot 的源码里找到这个文件。
struct tag {
struct tag_header hdr;
union {
struct tag_core core;
struct tag_mem32 mem;
struct tag_videotext videotext;
struct tag_ramdisk ramdisk;
struct tag_initrd initrd;
struct tag_serialnr serialnr;
struct tag_revision revision;
struct tag_videolfb videolfb;
struct tag_cmdline cmdline;
/*
*
Acorn specific
*/
struct tag_acorn acorn;
/*
* DC21285 specific
*/
struct tag_memclk memclk;
} u;
};
4、boot.c
#include "setup.h"
static struct tag *params;
void setup_start_tag(void)
{
/* uboot与kernel约定好:在地址
0x30000100
的地方开始存放参数
*/
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);
}
void setup_memory_tag(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;
// SDRAM 容量
params = tag_next(params);
}
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);
}
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);
/*
帮内核设置串口:
内核启动时,会从串口打印一些信息,但是内核一开始没有初始化串口
*/
uart0_init();
puts("Sewain>> Init UART0 success \r\n");
/*
1.
从 NAD FLASH 里把内核读入内存。
开发板中内核 uImage =
Header(64B)
+ zImage(真正的内核)。
参数1:uImage 的地址位于
0x00060000,
所以从这个地址之后的
64
字节,才是真正的内核文件。
参数2:内核在编译时就固定了加载地址和运行地址都为
0x30008000(是距离 SDRAM 底部32K
的位置),所以第二个参数是写入这个地址。
参数3:内核文件大概是
1.8M,这里直接读取
2M
的数据
*/
puts("Sewain>> Copy kernel from nand flash \r\n");
nand_read(0x00060000
+
64,
(unsigned char *)0x30008000,
0x00200000);
/*
2.
设置参数
*/
puts("Sewain>> Set boot params \r\n");
setup_start_tag();
setup_memory_tag();
setup_commandline_tag("noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0");
setup_end_tag();
/*
3.
跳转执行
*/
puts("Sewain>> Boot kernel \r\n");
theKernel=
(void (*)(int, int, unsigned int))0x30008000;
theKernel(0,
362,
0X3000100);
/*
相当于: mov pc,
#0x30008000 */
/*
如果一切正常,不应该执行到这里
*/
puts("Sewain>> Error! You should NOT come here!! \r\n");
return
-1;
}
5、boot.lds
这里的 0x33f80000 是位于开发板上 SDRAM 上的空间,距离底部 15M+512K 的地址,意思就是编译出来的程序的加载地址是 0x33f80000。
SECTIONS {
.
=
0x33f80000;
.text :
{
*(.text)
}
.
= ALIGN(4);
.rodata :
{
*(.rodata*)
}
.
= ALIGN(4);
.data :
{
*(.data)
}
.
= ALIGN(4);
__bss_start =
.;
.bss :
{
*(.bss)
*(COMMON)
}
__bss_end =
.;
}
6、Makefile
CC = arm-linux-gcc
LD = arm-linux-ld
AR = arm-linux-ar
OBJCOPY = arm-linux-objcopy
OBJDUMP = arm-linux-objdump
INCLUDEDIR := $(shell pwd)/include
CFLAGS :=
-Wall
-O2
CPPFLAGS :=
-nostdinc -nostdlib -fno-builtin
objs := start.o boot.o init.o
boot.bin: $(objs)
${OBJDUMP}
-D -m arm boot.elf > boot.dis
%.o:
%.c
%.o:
%.S
clean:
rm -f *.o *.bin *.elf *.dis
一些操作指令和流程
1、编译
通过 SecureCRT 把编辑好的代码复制到 Ubuntu 中,然后执行 make clean & make 编译,得到三个文件:boot.elf, boot.bin, boot.dis。
2. 烧写程序
把 boot.bin 复制到 Win7 中,然后通过命令行执行烧写工具 oflash boot.bin,根据提示符选择烧录到 Nand Flash 中。这期间会让你多次选择一些指令,看一下就明白了。
3. 验证
把开发板上的启动开关选择为 NAND,然后用串口工具 SecureCRT 连接到开发板,上电,可以看到一些信息,就是在 main 函数中写的那些打印信息。
总结
如果你需要上面的源码包,请联系我,非常乐意分享。
当然了,还是强烈建议你去看一下韦东山老师的视频教程,自己动手操作一遍之后,理解会更深刻。比如:如何配系统时钟、如何初始化串口、NandFlash 的存储和读写机制是怎么处理的等等。