在上一篇博客里比较深入地分析了uboot是如何启动内核(uboot博大精深,原谅我用“深入”这个词),所以这一篇博客里来看下如何实现一个bootloader。显然我们自己的写的bootloader不可能有uboot那样强大,我们的目的就是实现bootloader的最核心功能——加载并启动内核
关闭看门狗
.text
.global _start
_start:
ldr r0, =0x53000000 //ldr的寻址的参数如果带有等于号,说明这是一条伪汇编指令
mov r1, #0 //立即数寻址需要带上#做标识
str r1, [r0]
设置时钟
在这里插入代码片
初始化SDRAM
是时候SDRAM,实际上是配置和SDRAM相关的13个寄存器,而这13个相关的寄存器在内存上是连续的,所以可以采用如下循环的方式来操作,之前我们用C语言时,是采用的数组的方式来赋值的
#define MEM_CTL_BASE 0x48000000
ldr r0, =MEM_CTL_BASE
adr r1, sdram_config /*将sdram_config的当前地址赋值给r1*/
add r3, r0, #(4*13)
1:
ldr r2, [r1], #4 /*从对应的内存中读值,然后让r1的值加4,这样就可以让r1指向下一个数值所在的内存了*/
str r2, [r0], #4 /*因为寄存器是连续的,所以可以用这种方式来寻址寄存器*/
cmp r0, r3
bne 1b
sdram_config:
.long 0x22111110 //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 0x008e07a3 //REFRESH,HCLK=12MHz:0x008e07a3,HCLK=100MHz:0x008e04f4
.long 0x000000b2 //BANKSIZE
.long 0x00000030 //MRSRB6
.long 0x00000030 //MRSRB7
重定位代码 之前的博客中我们是用汇编语言写的,并且把重定位的地址写在了链接脚本中,在启动文件中可以直接从链接脚本中获取信息,有点像设备树。这次就用C语言来写,在这之前需要设置栈,同是也需要将code的所在地址和目标地址以及需要复制代码的长度作为参数传递给C函数,汇编向C函数传递参数需要遵循ATPCS规则,这里参数只有3个,我们可以将参数放在寄存器中
重定位 是将代码从我们烧写的地址复制到我们在链接脚本指定的地址里,由于这里使用的重定位c函数需要传递参数,所以在汇编阶段把三个传参按照顺序传给r0, r1, r2三个寄存器,其中第一个参数是代码源的地址,因为我们会将其烧写到0地址,所以为0;传参2是代码重定位的目标地址,是在链接脚本中指定;而第三个参数是需要复制多长的代码,应该是我们链接脚本的中指定的末尾到起始地址
ldr sp, =0x34000000
mov r0, #0x0
ldr r1, =_start
ldr r2, =_bss_start /*需要复制代码的长度需要根据链接脚本*/
sub r2, r2, r1
bl copy_code_for_sdram
bl clear_bss
boot.lds 链接脚本
SECTIONS {
. = 0x33f80000;
.text : {*(.text)}
. = ALIGN(4);
.rodata : {*(.rodata)}
. = ALIGN(4);
.data : {*(.data)}
. = ALIGN(4);
__bss_start = .;
.bss : {*(.bss) *(COMMON)}
__bss_end = .;
}
int copy_code_for_sdram(unsigned char *src, unsigned char *dest, unsigned int len)
{
int i;
if (isBootFromNorFlash())
{
/* 从 NOR Flash启动 */
for (i = 0; i < len; i++)
{
dest[i] = src[i];
}
return 0;
}
else
{
/* 初始化NAND Flash */
nand_init();
/* 从 NAND Flash启动 */
nand_read(start_addr, buf, (size + NAND_BLOCK_MASK_LP)&~(NAND_BLOCK_MASK_LP));
return 0;
}
}
清除bss段
void clear_bss(void)
{
extern int _bss_start, _bss_end;
int *p = &_bss_start;
for(; p < &_bss_end, p++)
{
*p = 0;
}
}
执行main函数,不能用bl main相对跳转,而用ldr pc addr绝对跳转的方式
ldr lr, =halt /*pc是指当前正在运行的地址,而lr则是将要传给pc的指令,当main函数返回时候,就会返回到lr中的地址*/
ldr pc, =main
halt:
b halt
在这里启动文件中,最为复杂的部分是nand flash的操作,已经写了一篇专门介绍nand flash的读操作的博客
#define TXD0READY (1<<2)
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);
}
void nand_select(void)
{
NFCONT &= ~(1<<1);
}
void nand_deselect(void)
{
NFCONT |= (1<<1);
}
void nand_cmd(unsigned char cmd)
{
volatile int i;
NFCMD = cmd;
for (i = 0; i < 10; i++);
}
void nand_addr(unsigned int addr)
{
unsigned int col = addr % 2048;
unsigned int page = addr / 2048;
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++);
}
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++);
}
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)
{
while (!(NFSTAT & 1));
}
unsigned char nand_data(void)
{
return NFDATA;
}
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;
}
void nand_read(unsigned int addr, unsigned char *buf, unsigned int len)
{
int col = addr / 2048;
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_wait_ready();
/* 6. 读数据 */
for (; (col < 2048) && (i < len); col++)
{
buf[i] = nand_data();
i++;
addr++;
}
col = 0;
/* 7. 取消选中 */
nand_deselect();
}
}
再来写main函数,main函数主要做的事是将内核读取出来,并复制到指定的区域,同时在启动文件和内核约定好的内存区域里,将内核启动需要的参数保存在其中;然后调用kernel来启动内核。
1、从flash中读取内核到内存中
用上上面实现的nand_read函数就可以了,关键是要知道内核放在flash上的哪里,在这里我们是将内核放在了flash上的kernel分区里,内核的目标地址是0x30008000,读取长度可以稍微选大一点,我们这里kernel区域大小为0x200000byte
2、设置参数
依然是需要参考u-boot的内容,我把需要参照的代码列在下面。
有两个疑问?这些参数放在哪里呢?或者说是在哪里指定了这些参数的位置呢?以及怎么让内核启动时可以寻址到这些参数呢?
这些参数的位置可以根据setup_start_tag函数来判断,在启动函数thekernel中,我们会把参数的起始地址作为传递参数告知内核,实际上的操作是把这个参数传递给寄存器r2.
第二个疑问,这些参数是干什么用的呢?具体这些参数做什么用,在下一篇关于内核的博客我会详细描述,但是我们可以看到在setup_memory_tags和setup_commandline_tag两个函数中,前者设置了内存的起始地址和内存的大小,后者则是保存了bootrags。
3、跳转执行
因为跳转是u-boot的最后一步,内核启动的第一步,当执行到这一步时,已经没有任何软件基础了,我们只能用硬件来操作,执行跳转命令的是一个函数指针,可以参照u-boot定义这个函数指针来定义,然后将我们内核的起始地址转化为thekernel型函数指针,然后执行这一函数指针。
void do_bootm_linux (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[],
ulong addr, ulong *len_ptr, int verify)
{
...
#if defined (CONFIG_SETUP_MEMORY_TAGS) || \
defined (CONFIG_CMDLINE_TAG) || \
defined (CONFIG_INITRD_TAG) || \
defined (CONFIG_SERIAL_TAG) || \
defined (CONFIG_REVISION_TAG) || \
defined (CONFIG_LCD) || \
defined (CONFIG_VFD)
setup_start_tag (bd);
#ifdef CONFIG_SERIAL_TAG
setup_serial_tag (¶ms);
#endif
#ifdef CONFIG_REVISION_TAG
setup_revision_tag (¶ms);
#endif
#ifdef CONFIG_SETUP_MEMORY_TAGS
setup_memory_tags (bd);
#endif
#ifdef CONFIG_CMDLINE_TAG
setup_commandline_tag (bd, commandline);
#endif
#ifdef CONFIG_INITRD_TAG
if (initrd_start && initrd_end)
setup_initrd_tag (bd, initrd_start, initrd_end);
#endif
#if defined (CONFIG_VFD) || defined (CONFIG_LCD)
setup_videolfb_tag ((gd_t *) gd);
#endif
setup_end_tag (bd);
#endif
...
}
static void setup_start_tag (bd_t *bd)
{
params = (struct tag *) bd->bi_boot_params;
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);
}
static void setup_memory_tags (bd_t *bd)
{
int i;
for (i = 0; i < CONFIG_NR_DRAM_BANKS; i++) {
params->hdr.tag = ATAG_MEM;
params->hdr.size = tag_size (tag_mem32);
params->u.mem.start = bd->bi_dram[i].start;
params->u.mem.size = bd->bi_dram[i].size;
params = tag_next (params);
}
}
static void setup_commandline_tag (bd_t *bd, char *commandline)
{
char *p;
if (!commandline)
return;
/* eat leading white space */
for (p = commandline; *p == ' '; p++);
/* skip non-existent command lines so the kernel will still
* use its default command line.
*/
if (*p == '\0')
return;
params->hdr.tag = ATAG_CMDLINE;
params->hdr.size =
(sizeof (struct tag_header) + strlen (p) + 1 + 4) >> 2;
strcpy (params->u.cmdline.cmdline, p);
params = tag_next (params);
}
static void setup_end_tag (bd_t *bd)
{
params->hdr.tag = ATAG_NONE;
params->hdr.size = 0;
}
main.c
#include "setup.h"
extern void uart0_init(void);
extern void nand_read(unsigned int addr, unsigned char *buf, unsigned int len);
extern void puts(char *str);
static 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);
}
static void setup_memory_tags (void)
{
/*这里我们只设置一个内存区域,所以不需要循环*/
params->hdr.tag = ATAG_MEM;
params->hdr.size = tag_size (tag_mem32);
params->u.mem.start = 0x300000000;
params->u.mem.size = 0x4000000;
params = tag_next (params);
}
int strlen(char *str)
{
int i = 0;
while(str[i])
i++;
return i;
}
void strcpy(char *dest, char *src)
{
int i = 0;
while(src[i])
{
*dest[i] = *src[i];
i++;
}
}
static 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);
}
static 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);
/*从flash中读出内核到内存中*/
nand_read(0x60000+64,(unsigned char *)0x30008000, 0x200000);
/*设置参数*/
setup_start_tag();
setup_memory_tags();
setup_commandline_tag("noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0");
setup_end_tag();
/*跳转的kernel函数指针,执行启动内核*/
theKernel = (void (*)(int, int, unit))0x30008000;
theKernel(0, 362, 0x30000100);
/*如果上面一步执行成功,bootloader就执行完毕了,所以当下面程序被执行,说明有问题*/
puts("error!\n\r");
return -1;
}
Makefile
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
在修改了语法错误后,生成了bin文件,将文件烧写后,启动开发板,发现没有任何反应;
1、检查是否是汇编代码start.S文件的问题
对于硬件初始化的代码,我们只能用硬件的方式来测试,可以对当前代码进行简单修改,然后来看下是否可以实现点灯
2、对于c代码中的问题,最好的方式就是串口输出,可以在main函数中加入输出信息,看执行到哪一步发生了问题。
main.c
int main(void)
{
void (*theKernel)(int zero, int arch, unsigned int params);
volatile unsigned int *p = (volatile unsigned int *)0x30008000;
uart0_init();
/* 1. 从NAND FLASH里把内核读入内存 */
puts("Copy kernel from nand\n\r");
nand_read(0x60000+64, (unsigned char *)0x30008000, 0x200000);
/* 设置参数 */
puts("Set boot params\n\r");
setup_start_tag();
setup_memory_tags();
setup_commandline_tag("noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0");
setup_end_tag();
/*跳转的kernel函数指针,执行启动内核*/
puts("Boot kernel\n\r");
theKernel = (void (*)(int, int, unsigned int))0x30008000;
theKernel(0, 362, 0x30000100);
/*
* mov r0, #0
* ldr r1, =362
* ldr r2, =0x30000100
* mov pc, #0x30008000
*/
puts("Error!\n\r");
/* 如果一切正常, 不会执行到这里 */
return -1;
}
结果发现这三条信息都在串口上打印出来了
Copy kernel from nand
Set boot params
Boot kernel
然后就输出乱码。那这个时候问题出现在哪里呢?从打印信息来看,程序可以执行到boot kernel这里来,是否代表着在此之前的代码都没有问题呢?显然不是,如果nand_read函数有问题,加载内核到内存上有问题,仅仅只会影响main函数的内核的启动,而不会影响这之前代码的运行。对于参数设置setup tags也一样,即使参数设置有问题 ,仅仅只会对内核启动有影响
所以在这里串口打印,仅仅让我们知道问题出现在main函数中三步执行中
如是我们来一个个排除
1、是不是nand_read加载内核到内存中有问题?好像不是,因为我们设置nand flash启动开发板,有串口信息输出,nand flash启动重定位代码时,将nand flash中的代码定位到内存中正是用nand_read函数,说明nand_read函数没有问题
2、是不是参数设置出了问题
因为参数的设置基本上是参考的u-boot,反复做了检查,cmdline是和内核参数设置是一样的结果发现没有问题
3、是不是启动除了问题
和启动有关的只有4行代码,好像也没有发现出问题
问题其实出现在nand_read函数中,一个非常低级的错误
void nand_read(unsigned int addr, unsigned char *buf, unsigned int len)
{
int col = addr / 2048;
addr / 2048这获得是页地址,就是确认在哪一页;如果是确认在哪一列就取膜运算。
那为什么nand flash启动没有问题呢,因为nand启动中我们经常将代码烧写到0地址,0是小于2048的,所以取模运算和取余数运算是一样的结果。
成功启动内核