一、硬件初始化和制作鏈接腳本lds
1.1、目標
第一階段:
- 關看門狗
- 設置時鍾
- 初始化SDRAM(初始化寄存器以及清除bss段)
- 重定位(將nand/nor中的代碼COPY到鏈接地址上,需要初始化nandflash,讀flash)
- 執行main
進入第二階段:
- 寫main函數,在main()中設置要傳給內核的參數,然后跳轉內核基地址處
- 制作uboot.lds鏈接腳本
1.2、創建工程並編寫start.S
1)創建個名為"my_bootloader"根目錄,方便編寫uboot
2)新建my_bootloader/si目錄,創建source insight工程
3)新建my_bootloader/start.S (后綴名必須是大寫的S,或者后面編譯會報錯)
編寫start.S (第一階段初始硬件文件):
/* 看門狗寄存器 */
#define WTCON 0x53000000
/* 2.時鍾寄存器 */
#define CLKDIVN 0x4C000014 //設置FCLK,HCLK,PCLK比例
#define MPLLCON 0x4C000004 //設置FCLK頻率
#define S3C2440_MPLL_200MHZ ((0x5c<<12)|(0x01<<4)|(0x02)) //設置FCLK=200MHZ
#define S3C2440_MPLL_400MHZ ((0x5c<<12)|(0x01<<4)|(0x01)) //設置FCLK=400MHZ
/* 3.bank寄存器 */
#define BWSCON 0x48000000
.text /*設置代碼段*/
.global _start /*定義全局變量,要被連接腳本用到*/
_start: /*_start跳轉到這里實現硬件初始化*/
/* 1.關看門狗 */
ldr r0,=WTCON
mov r1,#0
str r1,[r0]
/* 2.設置時鍾(必須設為異步總線模式) */
ldr r0,=CLKDIVN
mov r1,#3 /*FCLK:HCLK:PCLK=1:2:4*/
str r1,[r0]
mrc p15, 0, r1, c1, c0 /* 讀出控制寄存器 */
orr r1, r1, #0xc0000000 /* 設置為“asynchronous bus mode” */
mcr p15, 0, r1, c1, c0, 0 /* 寫入控制寄存器 */
ldr r0,=MPLLCON
ldr r1,=S3C2440_MPLL_200MHZ //使用復雜的數不能用mov,需要用ldr
str r1,[r0]
/* 3.初始化SDRAM */
ldr r0,=BWSCON //r0=SDRAM寄存器基地址
adr r1,SDRAM_CONFIG //使用adr相對跳轉,因為SDRAM未初始化
add r2,r0,#(13*4) //r2等於 SDRAM尾地址+4
0:
ldr r3,[r1],#4 //r3=r1里的 內容, &r1+=4;
str r3,[r0],#4 //*r0=r3,&r0+=4
cmp r0,r2
bne 0b //(ne:no equal b:bank) 若r0!=r2,跳轉到后面的0處
/* 4.重定位 */
ldr sp,=0x34000000 //64Msdram,所以設置棧SP=0x34000000,避免堆棧溢出
mov r0,#0 //r0->src
ldr r1,=_start //r1->dest
ldr r2,=__bss_start //r2->len->__bss_start-_start
sub r2,r2,r1
bl copy_code_to_sdram //復制代碼到SDRAM連接地址dest上
bl clear_bss //清除bss段
/* 5.執行main */
ldr lr,=halt //執行一個子程序調用返回時,需要先將返回地址賦給lr鏈接寄存器
ldr pc,=main //初始化SDRAM后,可以使用pc 絕對跳轉到SDRAM地址上
mov pc,lr //若main函數跳出后,使PC等於lr鏈接寄存器,避免程序跑飛
halt: //死循環,避免跑飛
b halt
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
1.3、編寫my_bootloader/init.c
新建my_bootloader/init.c,用於重定位,bss段清除,初始化NandFlash等
在重定位函數中的nand驅動在(http://www.cnblogs.com/lifexy/p/7097695.html)這篇帖子有詳細介紹,這里就不描述.
1)編寫copy_code_tosdram() 重定位函數
void copy_code_to_sdram(unsigned char *src,unsigned char *dest,unsigned int len) //復制代碼到SDRAM連接地址dest上
{
unsigned int i;
/*判斷nor啟動還是nand啟動*/
if(is_boot_from_norflash()) //nor啟動,直接復制
{
for(i=0;i<len;i++)
{
dest[i]=src[i];
}
}
else
{
nand_init();
nand_read((unsigned int)src,dest,len);
}
}
2)編寫isBootFramNorFlash()函數,來判斷nand啟動還是nor啟動
/*判斷nor啟動還是nand啟動*/
unsigned char is_boot_from_norflash(void)
{
volatile unsigned int *p=(volatile unsigned int *)0;
unsigned int i;
i=*p;
*p=0x12345678;
if(*p==0x12345678) //nand
{
*p=i;
return 0;
}
else //nor
{
*p=i;
return 1;
}
}
3)編寫clear_bss()函數
void clear_bss(void) //清除BSS段
{
extern int __bss_start,__bss_end;
int *p=&__bss_start;
for( ;p<&__bss_end;p++)
{
*p=0;
}
}
1.4、編寫鏈接腳本uboot.lds
參考硬件實驗里的uart.lds和u-boot-1.1.6里的u-boot.lds
SECTIONS {
. = 0x33f80000; //0地址里存放0X33F80000
. = ALIGN(4);
.text : { *(.text) }
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(COMMON) }
__bss_end = .;
}
其中0x33f80000就是鏈接地址首地址,共512K空間存放bootloader
定義__bss_start
和__bss_end
符號,是用來程序開始之前將這些未定義的變量清0,節省內存且bss_start-0x33f80000就等於代碼的大小(copy_code_tosdram函數中len值)
二、啟動內核和制作Makefile
2.1、目標
1)添加頭文件setup.h和serial.h
2)寫main函數
幫內核設置串口0, (內核啟動會打印出啟動信息)
把內核讀入到SDRAM
設置參數(參考u-boot-1.1.6 /lib_arm/armlinux.C中do_bootm_linux()函數)
跳轉運行內核(參考u-boot-1.1.6/lib_arm/armlinux.C中do_bootm_linux()函數)
3)寫tag參數函數
setup_start_tag (void);
setup_memory_tags(void);
setup_commandline_tag (char *cmdline);
setup_end_tag (void);
4)寫makefile文件
2.2、添加頭文件setup.h和serial.h
1)將lcd裸板程序中串口uart0初始化文件serial.c復制到my_bootloader目錄中.並修改serial.c
2)因為TAG結構體定義是存在u-boot-1.1.6/include/asm-arm/setup.h中,所以設置TAG參數需要用到這個文件,將setup.h復制到my_bootloader目錄中.
3)修改setup.h文件
刪除以下不需要的代碼
#define __tag __attribute__((unused, __section__(".taglist")))
#define __tagtable(tag, fn) \
static struct tagtable __tagtable_##fn __tag = { tag, fn }
#define tag_member_present(tag,member) \
((unsigned long)(&((struct tag *)0L)->member + 1) \
<= (tag)->hdr.size * 4)
添加以下代碼
#define u32 unsigned long
#define u16 unsigned int
#define u8 unsigned char
2.3、編輯my_bootloader/boot.c
新建my_bootloader/boot.c,用於存放main函數(main:由start.S跳轉過來的).
main函數代碼如下:
void main(void)
{
void (*theKernel)(int zero, int arch, unsigned int params);
/*定義一個函數指針theKernel,其中第一個參數zero:0 */
/* arch:機器ID ,由於芯片類型很多,內核為了辨別芯片而定義的機器ID,其中2440芯片的ID號是362,*/
/* params :tag參數位置,這里我們的tag起始地址=0x30000100*/
/*1 初 始 化 串 口 0 , 使 內 核 能 打 印 信 息 */
uart0_init(); //調用serial.h頭文件里的uart0_init()
puts(“uart0 init OK\r\n”); //打印uart0初始化
/*2從 nand flash 里 把 內 核 復 制 到 SDRAM 中 */
puts(“copy kernel from nand\r\n”); //打印內核復制
nand_read((0x60000+64),0X30008000,0X200000); //燒寫2MB,多燒寫點避免出錯
/*
0x60000+64:表示內核在nand(存儲)地址上位置,
0X30008000:內核在sdram(運行)地址上位置
0X200000:內核長度2MB
因為Flash上存的內核格式是:uImage(64B頭部(header) + 真正的內核 )
在uboot界面中輸入mtd命令可以看到:
kernel分區位於 nand的0X00060000~0x00260000
所以在nand中真正的內核地址=0x60000+64,
在uboot界面中輸入boot命令可以看到:
Data Size: 1848656 Bytes =1.8 MB
Load Address: 30008000
所以內核目的地址=0X30008000
長度=1.8MB
*/
/*3 設 置 T A G 參 數 */
puts(“set boot params\r\n”); //打印設置參數信息
setup_start_tag (void); //在0X30000100地址保存start_tag數據,
setup_memory_tags (void); //保存memory_tag數據,讓內核知道內存多大
setup_commandline_tag (“boottargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0”);
/*保存命令行bootargs參數,讓內核知道根文件系統位置在/dev/mtdblock3,指定開機運行第一個腳本/linuxrc,指定打印串口0*/
setup_end_tag (void); //初始化tag結構體結束
/* 4 跳 轉 執 行 */
puts(“boot kernel\r\n”); //打印啟動內核
theKernel = (void (*)(int, int, unsigend int))0x30008000;
// 設置theKernel地址=0x30008000,用於后面啟動內核
theKernel(0,362,0x300000100); //362:機器ID, 0x300000100: params(tag)地址
/*傳遞參數跳轉執行到0x30008000啟動內核, */
/*相當於: mov r0,#0 */
/*ldr r1,=362 */
/*ldr r2,= 0x300000100 */
/*mov pc,#0x30008000 */
puts(“kernel ERROR\r\n”); //打印內核啟動出錯
}
2.4、創建TAG參數函數
設置tag參數函數代碼如下
#include “setup.h”
static struct tag *params; //定義個tag結構體變量params指針
setup_start_tag (void) //開始tag
{
params = (struct tag *) 0x30000100; //tag起始地址等於0X30000100
params->hdr.tag = ATAG_CORE; //頭部常量tag=0x54410001
params->hdr.size = tag_size (tag_core); //size=5,
params->u.core.flags = 0;
params->u.core.pagesize = 0;
params->u.core.rootdev = 0;
params = tag_next (params); //parmas=( struct tag *)((u32 *)parmas+ params->hdr.size)
}
// setup_start_tag (bd)保存tag參數如下:
setup_memory_tags (void) //內存tag
{
int i;
params->hdr.tag = ATAG_MEM; //頭部常量tag=0x54410002
params->hdr.size = tag_size (tag_mem32); //size=4
params->u.mem.start = 0x30000000; //SDRAM起始地址
params->u.mem.size = 0x4000000; //SDRAM內存大小64M
params = tag_next (params); //指向下個tag
}
// setup_memory_tag s(bd)保存tag參數如下:
int strlen(char *str) //uboot不依賴任何庫,所以需要自己寫strlen函數
{
int i=0;
while(str[i])
{
i++;
}
return i;
}
void strcpy(char *dest, char *src)
{
while((*dest++=*src++)!=’\0’&&*dest!=’\0’);
}
setup_commandline_tag (char *cmdline) //命令行tag
/**cmdline :指向命令行參數*/
/*一般為:“boottargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0” */
{
int len=strlen(cmdline)+1; //計算cmdline長度,並加上結束符
params->hdr.tag = ATAG_CMDLINE; //頭部常量tag=0x54410009
params->hdr.size =(sizeof (struct tag_header) +len+3) >> 2; /*size=(字符串長度+頭部長度) >>2 */
/*“+3”表示:按4字節對齊,比如當總長度=(1,2,3,4)時,size=(總長度+3)>>2=1,實現4字節對齊 */
strcpy (params->u.cmdline.cmdline, cmdline); //復制形參字符串到params->u.cmdline.cmdline
params = tag_next (params); //執行下個tag
}
setup_end_tag (void) //結束tag
{
params->hdr.tag = 0;
params->hdr.size = 0;
}
2.5、寫makefile文件
首先將lcd裸板程序里的makefile復制到my_bootloader目錄中,並修改.
備注:在makefile中‘=’與‘:=’的區別:
‘=’:無關位置的等於(比如:”x=a y=$(x) x=b”,那么y的值永遠等於最后的值,等於 b ,而不是a)
‘:=’:有關位置的等於(比如:”x:=a y:=$(x) x:=b”,那么y的值取決於當時位置的值,等於 a ,而不是b)
CC = arm-linux-gcc //定義CC變量=arm-linux-gcc,簡化書寫,編譯命令,(*.C,*.S)文件生成*.O文件
LD = arm-linux-ld //連接命令,將多個*.O文件生成 boot.elf
AR = arm-linux-ar //庫管理命令,這里沒有用到
OBJCOPY = arm-linux-objcopy //復制/格式轉換命令, boot.elf生成boot.dis
OBJDUMP = arm-linux-objdump //反匯編命令,boot.bin生成boot.dis
CFLAGS := -Wall -O2 //GCC編譯參數,-Wall:顯示所有錯誤和警告, -O2:采用2級編譯優化
CPPFLAGS := -nostdinc -fno-builtin
//添加頭文件參數,-nostdinc忽略缺省目錄, -fno-builtin不連接系統標准啟動文件和標准庫文件(表示不用自帶的strlen()等庫函數)
objs := start.o init.o boot.o //定義objs變量,包含生成boot.bin目標文件需要的依賴文件
boot.bin: $(objs) //執行生成目標文件,首先是先滿足objs所有依賴文件都擁有,才執行
${LD} -Tuboot.lds -o boot_elf $^
${OBJCOPY} -O binary -S boot_elf $@
${OBJDUMP} -D -m arm boot_elf > boot.dis
%.o:%.c //%通配符。生成xxx.o文件先要找到xxx.c文件
${CC} $(CPPFLAGS) $(CFLAGS) -c -o $@ $< //-c編譯不連接。$@表示目標文件 $<表示第一個依賴文件
%.o:%.S
${CC} $(CPPFLAGS) $(CFLAGS) -c -o $@ $<
clean:
rm -f *.bin *.elf *.dis *.o
三、編譯測試以及改進
3.1、編譯測試
1)將寫好的uboot復制到linux下面
2)make編譯,然后將錯誤的地方修改,生成boot.bin(編譯出錯的解決方案:http://www.cnblogs.com/lifexy/p/7326172.html)
3)通過make生成的反匯編來查看代碼是否正確
4)通過oflash燒寫到板子nand flash上
5)查看串口是否數據打印
發現串口無數據,發現兩處錯誤:
5.1)在init.C中define定義 沒有加大括號,沒有定義volatile型
例如:
#define NFCONF *((unsigned long *)0x4E000000)
需要改為: #define NFCONF (*((volatile unsigned long *)0x4E000000) )
5.2)在init.C中nand_read_addr()函數出錯,代碼如下:
void nand_read_addr(unsigned int addr)
{
volatile int i;
NFADDR=( addr>>0)&0xff; //A7~A0
for(i=0;i<10;i++);
NFADDR=( addr>>8)&0x0f; //A11~A8
for(i=0;i<10;i++);
NFADDR=( addr>>12)&0xff; //A19~A12
for(i=0;i<10;i++);
NFADDR=( addr>>20)&0xff; //A27~A20
for(i=0;i<10;i++);
NFADDR=( addr>>28)&0xff; //A28
for(i=0;i<10;i++);
}
上面之所以錯是因為nand flash是2048B一頁,這里的寫的一頁是A11~A0,其值=4096,已經屬於兩頁大小了
應該改為:
void nand_read_addr(unsigned int addr)
{
unsigned int col = addr % 2048;
unsigned int page = addr / 2048;
volatile int i;
NFADDR=(col>>0)&0xff; //A7~A0
for(i=0;i<10;i++);
NFADDR=(col>>8)&0x0f; //A10~A8
for(i=0;i<10;i++);
NFADDR=(page>>0)&0xff; //A18~A11
for(i=0;i<10;i++);
NFADDR=(page>>8)&0xff; //A26~A19
for(i=0;i<10;i++);
NFADDR=(page>>16)&0xff; //A27
for(i=0;i<10;i++);
}
3.2、優化改進(加快內核啟動時間)
3.2.1、提高CPU頻率
將CPU頻率設為最大值400MHZ(內核啟動時間7S變為6S,因為HCLK和PCLK頻率沒有改變)
然后分頻系數FCLK:HCLK:PCLK需要設置為1:4:8
因為HCLK最高133MHZ,這里需要設置為100MHZ
PCLK最高50MHZ,這里需要設置為50HZ
如下圖所示,得出 CLKDIVN寄存器需要等於0X5即可
通過下圖看出,提高CPU頻率需要設置MPLLCON中MDIV、PDIV、SDIV
通過下圖得出,取400MHZ時,設置MDIV為0X5C,PDIV為0x1,SDIV為0x1
所以改進代碼如下:
#define S3C2440_MPLL_400MHZ ((0x5c<<12)|(0x01<<4)|(0x01)) //設置FCLK=400MHZ
/* 2.設置時鍾(必須設為異步總線模式) */
ldr r0,=CLKDIVN
mov r1,#5 /*FCLK:HCLK:PCLK=1:4:8*/
str r1,[r0]
mrc p15, 0, r1, c1, c0 /* 讀出控制寄存器 */
orr r1, r1, #0xc0000000 /* 設置為“asynchronous bus mode” */
mcr p15, 0, r1, c1, c0, 0 /* 寫入控制寄存器 */
ldr r0,=MPLLCON
ldr r1,= S3C2440_MPLL_400MHZ //使用復雜的數不能用mov,需要用ldr
str r1,[r0]
3.2.2、開啟ICAHE(內核啟動時間6S變為1.5S)
CAHE介紹:
通過高速緩存存儲器來加快對內存的數據訪問,在CAHE中有ICAHE(指令緩存)和DCAHE(數據緩存)
ICAHE: 指令緩存,用來存放執行這些數據的指令
DCAHE:用來存放數據,需要開啟MMU才能開啟DCAHE
在沒開啟ICAHE之前,CPU讀取SDRAM地址數據時,每次都需要先訪問一次地址值,在讀數據.
當開了ICAHE后,第一次讀取SDRAM地址數據時,ICAHE發現緩存里沒有這個地址數據,然后將SDRAM中需要讀取的那部分一大塊內存數據都復制在緩存中,后面陸續讀取數據就不會再訪問SDRAM了,直到CPU沒有找到地址數據后ICAHE再從SDRAM中重新復制
通過CP15協處理器來開啟ICAHE
(CP15協處理器操作用法: http://www.cnblogs.com/lifexy/p/7203786.html)
從上面鏈接中可以找到ICAHE控制位在CP15的寄存器C1中位12(如下圖), 然后通過MRS和MSR向該位12置1,開啟ICAHE
所以代碼如下(放在SDRAM初始化之前)
mrc p15, 0, r0, c1, c0, 0 //將 CP15 的寄存器 C1 的值讀到 r0 中
orr r0, r0, #(1<<12) //將r0中位12置1
mcr p15,0, r0,c1,c0,0 //開啟ICAHE