文章主题
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 的存储和读写机制是怎么处理的等等。