u-boot分析與使用—u-boot編譯體驗
- 硬件平臺:韋東山嵌入式Linxu開發板(S3C2440.v3)
- 軟件平臺:運行於VMware Workstation 12 Player下UbuntuLTS16.04_x64 系統
- 參考資料:《嵌入式Linux應用開發手冊》、《嵌入式Linux應用開發手冊第2版》
- 開發環境:Linux 2.6.22.6 內核、arm-linux-gcc-3.4.5-glibc-2.3.6工具鏈、u-boot-1.1.6
一、前言
在嵌入式Linux開發板中,u-boot的目的是爲了啓動內核,大致流程如下圖:
- 開發板一上電,啓動bootloader
- bootloader首先從Nand Flash上把內核加載到內存中,
- bootloader隨後初始化內存、時鐘等
- bootloader之後進行參數的設置,如啓動參數、內存參數、命令參數、結束參數
- bootloader最後跳轉啓動內核
下面就來從0開始編寫bootloader,實現上述功能。
二、編寫bootloader
- 在【5.1 u-boot分析與使用—u-boot編譯體驗】有提到過,bootloader可以類比成一個複雜的裸機程序。對於一個裸機程序,根據第一期的學習經驗,可以知道程序首先執行的是
.S
文件中的指令,所以第一個需要編寫的文件是start.S
- 整個程序的流程:先執行
start.S
中的程序,下面的第五步,進入到main()
內進行串口的初始化、傳遞給內核的參數的一些設置、把內核讀到內存、以及跳轉去執行內核部分。
1、編寫start.S
文件
實現的功能如下:
- 關看門狗
- 設置時鐘
- 初始化SDRAM
- 重定位 : 把bootloader本身的代碼從flash複製到它的鏈接地址去
- 執行
main()
1.1 關看門狗
查s3c2440可以往0x53000000
地址寫0可以關閉看門狗
ldr r0, =0x53000000
mov r1, #0
str r1, [r0]
1.2 設置時鐘
- 這裏的時鐘設置爲:FCLK:HCLK:PCLK=1:4:8,主要是一個優化措施:提高時鐘頻率
- 對於CPU總線模式設置爲
asynchronous bus mode
,這裏是手冊上規定的 - 啓動
ICACHE
:主要是一個優化措施:提高從Flash上讀出內核的速度
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]
/* 如果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 //寫入控制寄存器
/* MPLLCON = S3C2440_MPLL_200MHZ */
ldr r0, =0x4c000004
ldr r1, =S3C2440_MPLL_400MHZ
str r1, [r0]
/* 啓動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
1.3 初始化SDRAM
ldr r0, =MEM_CTL_BASE
adr r1, sdram_config //sdram_config的當前地址
add r3, r0, #(13*4) //r3等於r0 + 52,即sdram_config的末地址
1:
ldr r2, [r1], #4 //r2從r1的地址讀到一個值,後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
1.4 重定位 : 把bootloader本身的代碼從flash複製到它的鏈接地址去
對於copy_code_to_sdram()
與clean bss()
是通過C語言實現的。
爲什麼需要重定位呢?(具體可以看這篇博客【009 數據段重定位】)
問題:
- 首先:CPU能直接訪問的地方有:NOR FLASH、SDRAM、SRAM和各種控制器(包括NAND flash控制器)。所以當我們的程序燒寫到SDRAM或者NOR FALSH的時候,程序能直接運行。
- 如果燒寫到NAND FLASH,芯片會把程序的頭4K先拷貝到SRAM中執行,如果NAND flash中的程序小於4K的話,程序還能正常運行,如果大於4K,那大於4K的這部分就運行不了。
解決:
-
所以我們就引入了重定位,NAND FLASH的代碼中的前4K的代碼中需要把整個代碼拷貝到SDRAM去執行。
-
另外,對於NOR FLASH來說,我們無法簡單的去寫NOR FLASH,所以一旦程序中有需要寫的變量,比如全局變量和靜態變量,我們在無法在NOR FLASH上直接修改它們的值。因此,我們還是需要將NOR FLASH代碼重定位到SDRAM中去執行。
ldr sp, =0x34000000 //設置棧
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 clear_bss //清楚bss段
1.5 執行main()
對於main()
是通過C語言實現
ldr lr, =halt //返回地址死循環
ldr pc, =main
halt:
b halt
1.6 完整的程序
#define S3C2440_MPLL_200MHZ ((0x5c<<12)|(0x01<<4)|(0x02))
#define S3C2440_MPLL_400MHZ ((0x5c<<12)|(0x01<<4)|(0x01))
#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; // FCLK:HCLK:PCLK=1:2:4, HDIVN=1,PDIVN=1
mov r1, #0x05; // FCLK:HCLK:PCLK=1:4:8
str r1, [r0]
/* 如果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 //寫入控制寄存器
/* MPLLCON = S3C2440_MPLL_200MHZ */
ldr r0, =0x4c000004
ldr r1, =S3C2440_MPLL_400MHZ
str r1, [r0]
/* 啓動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 */
ldr r0, =MEM_CTL_BASE
adr r1, sdram_config //sdram_config的當前地址
add r3, r0, #(13*4) //r3等於r0 + 52,即sdram_config的末地址
1:
ldr r2, [r1], #4 //r2從r1的地址讀到一個值,後r1+4
str r2, [r0], #4 //把r2的值存到r0所指向的地方,後r0+4
cmp r0, r3 //比較r0與r3,不等則跳回1
bne 1b
/* 4. 重定位 : 把bootloader本身的代碼從flash複製到它的鏈接地址去 */
ldr sp, =0x34000000 //設置棧
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 clear_bss //清楚bss段
/* 5. 執行main */
ldr lr, =halt //返回地址死循環
ldr pc, =main
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
2、編寫鏈接腳本boot.lds
SECTIONS {
. = 0x33f80000;
.text : { *(.text) }
. = ALIGN(4);
.rodata : {*(.rodata*)}
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(COMMON) }
__bss_end = .;
}
3、編寫boot.c
主函數文件
實現的功能如下:
- 幫內核設置串口: 內核啓動的開始部分會從串口打印一些信息
- 從NAND FLASH裏把內核讀入內存
- 設置參數
- 跳轉執行
3.1 幫內核設置串口
uart0_init()
是在init.c
文件中實現的,這裏先把代碼貼出來
/*
* 初始化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
}
3.2 從NAND FLASH裏把內核讀入內存
這裏的nand_read()、puthex()、puts()
也是在init.c
文件中實現
/* 1、從NAND FLASH裏把內核讀入內存
* uImage = 64 + zImage,通過uboot執行mtd可以找到zImage燒寫地址
*/
puts("Copy kernel from nand\n\r");
nand_read(0x60000+64, (unsigned char *)0x30008000, 0x200000);
/* 調試代碼 */
puthex(0x1234ABCD);
puts("\n\r");
puthex(*p);
puts("\n\r");
3.3 設置參數
/* 2、設置參數 */
puts("Set boot params\n\r");
setup_start_tag(); //啓動參數
setup_memory_tags(); //內存參數
setup_commandline_tag("noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0,115200"); //命令行參數
setup_end_tag(); //結束參數
對於4個參數的設置,我畫了下面的圖以供理解:
-
根據
start_tag
的參數tag:0x30000100
,在地址爲0x30000100
的內存中進行入棧 -
在每設置完一個參數後,
params
指針移動到下一個tag的位置
,進行下一個參數的入棧
3.4 跳轉執行
最後則會根據傳入的參數,跳去執行theKernel()
函數
/* 3、跳轉執行 */
puts("Boot kernel\n\r");
theKernel = (void (*)(int, int, unsigned int))0x30008000;
/*
* mov r0, #0
* ldr r1, =362 —> 機器ID
* ldr r2, =0x30000100
* mov pc, #0x30008000
*/
theKernel(0, 362, 0x30000100);
/* 如果一切正常, 不會執行到這裏 */
puts("Error!\n\r");
3.5 完整的文件
#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);
extern void puthex(unsigned int val);
static struct tag *params;
/* 設置啓動參數 */
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;
/* 移動指針指向下一個tag */
params = tag_next (params);
}
/* 設置內存參數 */
void setup_memory_tags(void)
{
params->hdr.tag = ATAG_MEM;
/* ((sizeof(struct tag_header) + sizeof(struct type)) >> 2)
* struct tag_header { struct tag_mem32 {
* __u32 size; __u32 size;
* __u32 tag; __u32 start;
* }; };
* 字節: (8 + 8) >> 2 = 4
*/
params->hdr.size = tag_size (tag_mem32);
params->u.mem.start = 0x30000000; //內存起始地址
params->u.mem.size = 64*1024*1024; //內存大小64M
/* 移動指針指向下一個tag */
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);
/* 移動指針指向下一個tag */
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);
volatile unsigned int *p = (volatile unsigned int *)0x30008000;
/* 0、幫內核設置串口: 內核啓動的開始部分會從串口打印一些信息,但是內核一開始沒有初始化串口 */
uart0_init();
/* 1、從NAND FLASH裏把內核讀入內存
* uImage = 64 + zImage,通過uboot執行mtd可以找到zImage燒寫地址
*/
puts("Copy kernel from nand\n\r");
nand_read(0x60000+64, (unsigned char *)0x30008000, 0x200000);
puthex(0x1234ABCD);
puts("\n\r");
puthex(*p);
puts("\n\r");
/* 2、設置參數 */
puts("Set boot params\n\r");
setup_start_tag(); //啓動參數
setup_memory_tags(); //內存參數
setup_commandline_tag("noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0,115200"); //命令行參數
setup_end_tag(); //結束參數
/* 3、跳轉執行 */
puts("Boot kernel\n\r");
theKernel = (void (*)(int, int, unsigned int))0x30008000;
/*
* mov r0, #0
* ldr r1, =362 —> 機器ID
* ldr r2, =0x30000100
* mov pc, #0x30008000
*/
theKernel(0, 362, 0x30000100);
/* 如果一切正常, 不會執行到這裏 */
puts("Error!\n\r");
return -1;
}
4、編寫相關初始化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 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);
}
}
/* nor flash啓動,0地址是nor flash的0地址,可以像內存一樣讀,但是不能像內存一樣寫
* nand flash啓動,0地址是片內內存,內存可以寫
*/
int isBootFromNorFlash(void)
{
volatile int *p = (volatile int *)0; //舊值
int val;
val = *p; //緩衝舊值
*p = 0x12345; //往0地址寫入新值
/* 判斷新值是否寫成功 */
if (*p == 0x12345) {
/* 寫成功,nand啓動 */
*p = val;
return 0;
} else {
/* 寫失敗,nor啓動 */
return 1;
}
}
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;
NFCMMD = 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) {
/* 一個block只判斷一次 */
if (!(addr & 0x1FFFF) && nand_bad(addr)) {
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();
}
}
void copy_code_to_sdram(unsigned char *src, unsigned char *dest,
unsigned int length)
{
int i = 0;
/* 如果是NOR 啓動 */
if (isBootFromNorFlash()) {
while (i < length) {
dest[i] = src[i];
i++;
}
} else {
//nand_init();
nand_read((unsigned int)src, dest, length);
}
}
void clear_bss(void)
{
/* 引用鏈接腳本的變量方法:定義外部變量,取地址 */
extern int __bss_start, __bss_end;
int *p = &__bss_start;
for (; p < &__bss_end; p++)
*p = 0;
}
5、編寫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
三、編譯與測試
1、編譯
執行make
之後會生成boot.bin
文件
2、測試
把boot.bin
文件通過OpenJtag
分別燒寫到Nor Flash
與Nand Flash
中,可以看到開發板成功啓動bootloader
。