上半個月在學習bootloader,突然找到了一個非常好的vboot,vboot只有最基本的內核引導功能(基於s3c2440,從nand flash啟動),對其深入研究后,發現對bootloader有了比較全面的理解,雖然沒有像uboot那么多功能,但vboot已經實現了bootloader最核心的功能,其他像什么網絡功能、燒寫功能等等也只是一些裸機驅動而已。學習bootloader需要有匯編的基礎,如果有單片機編程經驗的話那更是“如魚得水”了。
先看vboot的整體架構,下面是vboot包含的所有文件:
很簡單是吧,其中核心的文件是head.S、main.c和nand.c,vboot.bin已經是編譯出來的二進制文件,用於燒寫在nand flash里。先看mem.lds文件,這是一個鏈接腳本,從那里可以找到程序的入口:
1 SECTIONS { 2 . = 000000; 3 .myhead ALIGN(0): {*(.text.FirstSector)} 4 .text ALIGN(512): { *(.text) } 5 .bss ALIGN(4) : { *(.bss*) *(COMMON) } 6 .data ALIGN(4) : { *(.data*) *(.rodata*) } 7 }
比較簡單,程序入口位於text.FirstSector這個段里(因為程序是從nand flash的0地址開始執行的),它在head.S文件里定義:
1 .section .text.FirstSector 2 .globl first_sector 3 4 first_sector: 5 @ 0x00: Reset 6 b Reset 7 8 @ 0x04: Undefined instruction exception 9 UndefEntryPoint: 10 b UndefEntryPoint 11 12 @ 0x08: Software interrupt exception 13 SWIEntryPoint: 14 b SWIEntryPoint 15 16 @ 0x0c: Prefetch Abort (Instruction Fetch Memory Abort) 17 PrefetchAbortEnteryPoint: 18 b PrefetchAbortEnteryPoint 19 20 @ 0x10: Data Access Memory Abort 21 DataAbortEntryPoint: 22 b DataAbortEntryPoint 23 24 @ 0x14: Not used 25 NotUsedEntryPoint: 26 b NotUsedEntryPoint 27 28 @ 0x18: IRQ(Interrupt Request) exception 29 IRQEntryPoint: 30 b IRQHandle 31 32 @ 0x1c: FIQ(Fast Interrupt Request) exception 33 FIQEntryPoint: 34 b FIQEntryPoint 35 36 @0x20: Fixed address global value. will be replaced by downloader. 37 38 .long ZBOOT_MAGIC 39 .byte OS_TYPE, HAS_NAND_BIOS, (LOGO_POS & 0xFF), ((LOGO_POS >>8) &0xFF) 40 .long OS_START 41 .long OS_LENGTH 42 .long OS_RAM_START 43 .string LINUX_CMD_LINE
第5~34行的作用是安裝異常向量表,在這里除了復位,其他異常都沒有定義具體的執行代碼。
1 .section .text 2 Reset: 3 @ 關閉看門狗 4 mov r1, #0x53000000 5 mov r2, #0x0 6 str r2, [r1] 7 8 @ 關閉中斷 9 mov r1, #INT_CTL_BASE 10 mov r2, #0xffffffff 11 str r2, [r1, #oINTMSK] 12 ldr r2, =0x7ff 13 str r2, [r1, #oINTSUBMSK] 14 15 @ 初始化系統時鍾 16 mov r1, #CLK_CTL_BASE 17 mvn r2, #0xff000000 18 str r2, [r1, #oLOCKTIME] @設置LOCKTIME寄存器 19 20 mov r1, #CLK_CTL_BASE 21 ldr r2, clkdivn_value 22 str r2, [r1, #oCLKDIVN] @設置分頻寄存器 23 24 mrc p15, 0, r1, c1, c0, 0 @ read ctrl register 25 orr r1, r1, #0xc0000000 @ Asynchronous 異步總線模式 26 mcr p15, 0, r1, c1, c0, 0 @ write ctrl register 27 28 mov r1, #CLK_CTL_BASE 29 ldr r2, =S3C2440_UPLL_48MHZ_Fin12MHz 30 str r2, [r1, #oUPLLCON] 31 32 nop 33 nop 34 nop 35 nop 36 nop 37 nop 38 nop 39 nop 40 nop 41 42 ldr sp, DW_STACK_START @ setup stack pointer 43 44 ldr r2, mpll_value_USER @ clock user set 12MHz 45 str r2, [r1, #oMPLLCON] 46 bl memsetup 47 48 @ set GPIO for UART 49 mov r1, #GPIO_CTL_BASE 50 add r1, r1, #oGPIO_H 51 ldr r2, gpio_con_uart 52 str r2, [r1, #oGPIO_CON] 53 ldr r2, gpio_up_uart 54 str r2, [r1, #oGPIO_UP] 55 bl InitUART 56 57 58 @ get read to call C functions 59 mov fp, #0 @ no previous frame, so fp=0 60 mov a2, #0 @ set argv to NULL 61 62 bl Main 63 64 1: b 1b @
第4~6行,關閉看門狗,以免系統不斷復位;第9~13行,關閉中斷;第16~18行,設置系統時鍾穩定(鎖定)時間;第20~22行,設置時鍾分頻比為1:4:8(FCLK:HCLK:PCLK);第24~26行,設置為異步總線模式(因為FCLK已經不等於HCLK);第28~30,行,設置UPLL為48MHZ,用於USB通信;第42行,設置棧指針,為下面調用c程序做准備;第44~45行,設置FCLK為400MHZ,那么HCLK=100MHZ,PCLK=50MHZ;第46行,跳到內存初始化程序:
1 memsetup: 2 @ initialise the static memory 3 4 @ set memory control registers 5 mov r1, #MEM_CTL_BASE 6 adrl r2, mem_cfg_val 7 add r3, r1, #52 @13*4 8 1: ldr r4, [r2], #4 9 str r4, [r1], #4 10 cmp r1, r3 11 bne 1b 12 mov pc, lr
2440總共有13個設置內存的寄存器,因此第7行的立即數是52(13*4);第8~11行,通過循環設置13個寄存器的值。返回到memsetup下面的代碼:
1 @ set GPIO for UART 2 mov r1, #GPIO_CTL_BASE 3 add r1, r1, #oGPIO_H 4 ldr r2, gpio_con_uart 5 str r2, [r1, #oGPIO_CON] 6 ldr r2, gpio_up_uart 7 str r2, [r1, #oGPIO_UP] 8 bl InitUART 9 10 11 @ get read to call C functions 12 mov fp, #0 @ no previous frame, so fp=0 13 mov a2, #0 @ set argv to NULL 14 15 bl Main 16 17 1: b 1b @
第2~8行,用於初始化串口(115200bps,8N1);第12~13行,設置兩個arm寄存器;第15行,跳到Main函數執行。在main.c文件里:
1 void Main(void) 2 { 3 MMU_EnableICache(); 4 MMU_EnableDCache(); 5 6 Port_Init(); 7 NandInit(); 8 9 if (g_page_type == PAGE_UNKNOWN) { 10 Uart_SendString("\r\nunsupport NAND\r\n"); 11 for(;;); 12 } 13 14 GetParameters(); 15 16 Uart_SendString("loading Image of Linux from Nand Flash...\n\r"); 17 ReadImageFromNand(); 18 }
第3~4行,使能Dcache和Icache:
static inline void MMU_EnableICache(void) { asm ( "mrc p15,0,r0,c1,c0,0\n" "orr r0,r0,#(1<<12)\n" "mcr p15,0,r0,c1,c0,0\n" ); } static inline void MMU_EnableDCache(void) { asm ( "mrc p15,0,r0,c1,c0,0\n" "orr r0,r0,#(1<<2)\n" "mcr p15,0,r0,c1,c0,0\n" ); }
第6行,初始化一些IO口(沒用到);第7行,初始化nand flash控制器,在nand.c文件里定義:
void NandInit(void) { NFCONF = (TACLS << 12) | (TWRPH0 << 8) | (TWRPH1 << 4) | (0 << 0); NFCONT = (0 << 13) | (0 << 12) | (0 << 10) | (0 << 9) | (0 << 8) | (0 << 6) | (0 << 5) | (1 << 4) | (1 << 1) | (1 << 0); NFSTAT = 0; NandReset(); NandCheckId(); }
設置具體nand flash芯片的時序參數、頁的大小和位寬等,初始化之后,就可以讀寫nand flash了。回到Main函數的第14行調用的GetParameters()函數的定義:
static inline void GetParameters(void) { U32 Buf[2048]; g_os_type = OS_LINUX; //內核在flash中的起始地址 g_os_start = 0x50000; //內核映像的大小 g_os_length = 0x300000; //內核被拷貝到內存的起始地址 g_os_ram_start = 0x30008000; // vivi LINUX CMD LINE //從flash的參數分區中讀命令行參數 NandReadOneSector((U8 *)Buf, 0x40000); if (Buf[0] == 0x49564956 && Buf[1] == 0x4C444D43) { memcpy(g_linux_cmd_line, (char *)&(Buf[2]), sizeof g_linux_cmd_line); } }
設置了內核映像在nand flash的起始地址和大小,還有設置內核映像被拷貝到ram的起始地址,命令行參數是通過BIOS(nor flash里的supervivi)寫到nand flash的0x40000地址處,通過NandReadOneSector()把它讀出來,其中Buf[0]、Buf[1]這兩個值是“暗藏值”,是對應於具體的BIOS的,是由BIOS寫進去的,位於命令行參數的第一和第二個字,因為BIOS的代碼不不開源的,無法修改,所以移植vboot的時候只要是用這個BIOS來燒寫vboot就不用修改兩個值(不用太糾結,我曾糾結了很久)。從memcpy()函數也可以知道,Buf[0]和Buf[1]這兩個值是用來識別具體的BIOS的,沒用於命令行參數。現在看NandReadOneSector()函數:
1 int NandReadOneSector(U8 * buffer, U32 addr) 2 { 3 int ret; 4 5 switch(g_page_type) { 6 case PAGE512: 7 ret = NandReadOneSectorP512(buffer, addr); 8 break; 9 case PAGE2048: 10 ret = NandReadOneSectorP2048(buffer, addr); 11 break; 12 default: 13 for(;;); 14 } 15 return ret; 16 }
因為我板子(GT2440)上的nand flash是64M的,頁的大小為512字節,所以看第7行的調用:
static inline int NandReadOneSectorP512(U8 * buffer, U32 addr) { U32 sector; sector = addr >> 9; NandReset(); #if 0 NF_RSTECC(); NF_MECC_UnLock(); #endif NF_nFCE_L(); NF_CLEAR_RB(); NF_CMD(0x00); NF_ADDR(0x00); NF_ADDR(sector & 0xff); NF_ADDR((sector >> 8) & 0xff); NF_ADDR((sector >> 16) & 0xff); delay(); NF_DETECT_RB(); ReadPage512(buffer, &NFDATA); #if 0 NF_MECC_Lock(); #endif NF_nFCE_H(); return 1; }
該函數里前面那些是設置讀操作,設置讀起始地址,核心是調用ReadPage512()函數,它由匯編實現,在head.S里:
1 .globl ReadPage512 2 3 ReadPage512: 4 stmfd sp!, {r2-r7} @ 將r2~r7寄存器的值壓棧 5 mov r2, #0x200 @ 512個字節 6 7 1: 8 ldr r4, [r1] 9 ldr r5, [r1] 10 ldr r6, [r1] 11 ldr r7, [r1] 12 stmia r0!, {r4-r7} 13 ldr r4, [r1] 14 ldr r5, [r1] 15 ldr r6, [r1] 16 ldr r7, [r1] 17 stmia r0!, {r4-r7} 18 ldr r4, [r1] 19 ldr r5, [r1] 20 ldr r6, [r1] 21 ldr r7, [r1] 22 stmia r0!, {r4-r7} 23 ldr r4, [r1] 24 ldr r5, [r1] 25 ldr r6, [r1] 26 ldr r7, [r1] 27 stmia r0!, {r4-r7} 28 subs r2, r2, #64 @ 一次循環讀64個字節 29 bne 1b; 30 ldmfd sp!, {r2-r7} @ 恢復r2~r7寄存器的值 31 mov pc,lr @ 返回
挺好懂的,不多解析。再回到Main()函數的17行(最后一個函數調用)調用ReadImageFromNand():
1 void ReadImageFromNand(void) 2 { 3 unsigned int Length; 4 U8 *RAM; 5 unsigned BlockNum; 6 unsigned pos; 7 8 Length = g_os_length; 9 //內核的大小(單位:塊) 10 Length = (Length + BLOCK_SIZE - 1) >> (BYTE_SECTOR_SHIFT + SECTOR_BLOCK_SHIFT) << (BYTE_SECTOR_SHIFT + SECTOR_BLOCK_SHIFT); // align to Block Size 11 //內核在flash中的第幾塊 12 BlockNum = g_os_start >> (BYTE_SECTOR_SHIFT + SECTOR_BLOCK_SHIFT); 13 //要拷貝到的起始地址 14 RAM = (U8 *) g_os_ram_start; 15 for (pos = 0; pos < Length; pos += BLOCK_SIZE) { 16 unsigned int i; 17 // skip badblock 18 //壞塊檢測 19 for (;;) { 20 if (NandIsGoodBlock 21 (BlockNum << 22 (BYTE_SECTOR_SHIFT + SECTOR_BLOCK_SHIFT))) { 23 break; 24 } 25 BlockNum++; //try next 26 } 27 for (i = 0; i < BLOCK_SIZE; i += SECTOR_SIZE) { 28 int ret = 29 NandReadOneSector(RAM, 30 (BlockNum << 31 (BYTE_SECTOR_SHIFT + 32 SECTOR_BLOCK_SHIFT)) + i); 33 RAM += SECTOR_SIZE; 34 ret = 0; 35 36 } 37 38 BlockNum++; 39 } 40 41 CallLinux(); 42 }
主要是從nand flash里把內核映像一塊一塊地讀到ram里,每讀一塊之前先進行壞塊檢測,如果是壞塊就跳過,繼續讀下一塊(這里的壞塊檢測是一個比較粗略的檢測方法),直到把整個內核映像讀到ram里面。這里內核映像的大小設置為3M(實際上不到3M),因此讀也是讀3M大小到ram里面。最后該函數的第41行調用CallLinux():
1 static void CallLinux(void) 2 { 3 struct param_struct { 4 union { 5 struct { 6 unsigned long page_size; /* 0 */ 7 unsigned long nr_pages; /* 4 */ 8 unsigned long ramdisk_size; /* 8 */ 9 unsigned long flags; /* 12 */ 10 unsigned long rootdev; /* 16 */ 11 unsigned long video_num_cols; /* 20 */ 12 unsigned long video_num_rows; /* 24 */ 13 unsigned long video_x; /* 28 */ 14 unsigned long video_y; /* 32 */ 15 unsigned long memc_control_reg; /* 36 */ 16 unsigned char sounddefault; /* 40 */ 17 unsigned char adfsdrives; /* 41 */ 18 unsigned char bytes_per_char_h; /* 42 */ 19 unsigned char bytes_per_char_v; /* 43 */ 20 unsigned long pages_in_bank[4]; /* 44 */ 21 unsigned long pages_in_vram; /* 60 */ 22 unsigned long initrd_start; /* 64 */ 23 unsigned long initrd_size; /* 68 */ 24 unsigned long rd_start; /* 72 */ 25 unsigned long system_rev; /* 76 */ 26 unsigned long system_serial_low; /* 80 */ 27 unsigned long system_serial_high; /* 84 */ 28 unsigned long mem_fclk_21285; /* 88 */ 29 } s; 30 char unused[256]; 31 } u1; 32 union { 33 char paths[8][128]; 34 struct { 35 unsigned long magic; 36 char n[1024 - sizeof(unsigned long)]; 37 } s; 38 } u2; 39 char commandline[1024]; 40 }; 41 //啟動參數在內存的起始地址 42 struct param_struct *p = (struct param_struct *)0x30000100; 43 memset(p, 0, sizeof(*p)); 44 memcpy(p->commandline, g_linux_cmd_line, sizeof(g_linux_cmd_line)); 45 //內存頁的大小4K 46 p->u1.s.page_size = 4 * 1024; 47 //內存總共有多少頁 48 p->u1.s.nr_pages = 64 * 1024 * 1024 / (4 * 1024); 49 50 { 51 unsigned int *pp = (unsigned int *)(0x30008024); 52 if (pp[0] == 0x016f2818) { //zImage的魔數,在內核中定義 53 //Uart_SendString("\n\rOk\n\r"); 54 } else { 55 Uart_SendString("\n\rWrong Linux Kernel\n\r"); 56 for (;;) ; 57 } 58 59 } 60 asm ( 61 "mov r5, %2\n" 62 "mov r0, %0\n" 63 "mov r1, %1\n" 64 "mov ip, #0\n" 65 "mov pc, r5\n" 66 "nop\n" "nop\n": /* no outpus */ 67 :"r"(0), "r"(782), "r"(g_os_ram_start) 68 ); 69 }
首先定義了一個struct param_struct結構體變量,從這里就可以看出,vboot用的是舊的方式(新的是用tag方式),struct param_struct與內核里定義的一樣。第41~59行,看注釋可以明白,第60~67行,是內核的一些約定:
R0 = 0
R1 = 機器ID
.....
最后第65行,設置pc為內核映像在內存中的起始地址,直接跳到內核映像的入口,從而開始內核代碼的執行......
總結:
vboot是一個十分精簡的bootloader,從nand flash啟動,目前只支持2440 Linux,只有引導內核的功能,它的編譯后的二進制文件不會超過4K(這是由2440從nand flash啟動所限制的),編譯vboot只需要在代碼目錄下執行make,便可生成vboot.bin文件,通過BIOS將它燒寫到nand flash里。強烈推薦想學習ARM bootloader的同學從vboot開始入手。