題外話:
最近一直在學習u-boot的源代碼,從代碼量到代碼風格,都讓我認識到什么才是真正的程序。以往我所學到的C語言知識和u-boot的源代碼相比,實在不值一提。說到底,機器都是0和1控制的。感覺這很像我們中國《易經》里的一句話:“太極生兩儀,兩儀生四象。”兩儀指的就是陰陽、天地,對立而又相互依存的一切,它們生成了天地萬物。簡單的0和1就構成了我們現在所用的操作系統,各種軟件。硬件也是由高低電平控制,0和1就是萬物。
剛剛在讀一本科幻小說,里面提到一種叫做“腦域”的新興技術,意思是類比於物聯網,將人類的大腦互相聯網形成腦聯網,俗稱“腦域”。研究腦域的專家們住在最好的實驗室大樓里研究技術。實驗室大樓十分豪華,一切設備都是最先進的,一切生活用品、娛樂用品一應俱全。這些專家們就在這最好的環境下研究腦域技術。但是一個研究水稻的人說:“你們自以為研究腦域是在幫助這個社會,實際上這種技術也不過曇花一現罷了。我知道你們每年的產值高達幾十個億,但這又有什么用呢?你們以為自己在為社會造福,把脖子伸出窗外看看吧,在你們豪華的大樓下就是普通的市井生活,他們的生活環境臟亂差,人們的生活水平沒有得到提高。人們的素質水平也沒有得到提高。你們的研究就像陽春白雪,似乎很有用,但不能拯救這個社會。真正能幫這個社會的,只有那些最基本的東西,看似最無用的東西。人人都能夠得到,人人都能夠受益,才會真正讓人們進步。”
我思考了一下剛剛那句話,想我是不是正在研究的也是和人們生活完全不相關的東西。在宿舍里,在辦公室里,研究着新興技術,自以為是在造福社會,實際上做的事情卻絲毫不會影響人們的生活?
我想為開源世界貢獻自己的力量,開發嵌入式系統,更好的控制硬件,制造出人人都能買的起的可穿戴設備,而不是什么黑科技。所以我覺得是有意義的,從這門技術的發展方向來看。我大膽預測一下吧,未來的可穿戴設備可能會發展成類似於一枚戒指的東東,一定是便於攜帶的,便於使用的形式存在。在尊重版權的情況下開源代碼減少軟件開發成本,用材料和結構減少硬件成本。讓人人都買的起可穿戴設備,就像每個人都買的起手表,這才是真正的讓人們受益。讓大家都感到方便,感受到新科技的美好。
科技在進步,這是必然趨勢。不要害怕這種進步,而是要提高自身的道德素養。道德必須凌駕於科技之上。
1.從我們的start_armboot開始講起
u-boot整體由匯編段和C語言段外加連接腳本組成。關於匯編段請看我之前的博客《u-boot源碼匯編段簡要分析》,好,讓我們進入start_armboot函數。
這里首先涉及到的是一個叫做gd_t的結構體指針。給它分配了一塊內存。位於CFG_GBL_DATA_SIZE。我們把它打開來看:
從作者的注釋我們可以知道,這個結構體的作用是針對某些初始化較早的內存空間。在設置存儲控制器之前,系統初始化的時候進行一些全局變量的定義。看里面的內容,涉及到了環境地址變量、環境檢測變量、幀緩沖區的基地址、展示模式、CPU時鍾、總線時鍾、RAM大小、復位狀態寄存器等等。回到start_armboot函數,我們接着往下看:
在這里涉及到了init_sequence結構體,打開來看:
這個結構體里面包含了cpu初始化、單板初始化、中斷初始化、環境初始化、波特率初始化、串行通信、CPUINFO、BOARDINFO、RAM塊等等一系列初始化函數。回到start_armboot函數接着往下看:
在這里紅框圈起來的是對Flash的初始化,flash_init()是對Nor Flash初始化,nand_init()是對Nand Flash初始化。中間是對LCD或者VFD進行初始化,並分配內存。接下來是一個 env_relocate ()函數,這個函數是對環境變量進行初始化。所謂的環境變量就是我們在用u-boot的時候從哪里讀出內核,從哪里啟動內核等一系列信息,如下圖:
其中bootcmd就表示你把內核讀到哪里。這里明顯是把內核讀到0x30007FC0地址上,然后bootm 0x0007FC0,也就是啟動該地址上的內核。這里還有波特率、IP地址、bootargs等u-boot環境變量。
最后我們進入了一個循環:main_loop();這是最重要的一個函數。
2.main_loop()函數
char *s; s = getenv ("bootcmd"); # ifndef CFG_HUSH_PARSER { printf("Booting Linux ...\n"); run_command (s, 0); } len = readline (CFG_PROMPT); rc = run_command (lastcommand, flag);
u-boot代碼量很大,對於各種硬件初始化函數特別多,所以這里博主把最關鍵的語句摘出來。方便大家分析。這里的getenv是獲取環境變量,u-boot在啟動時會首先獲取環境變量,所謂的環境變量我們上面通過u-boot打印出的信息就可以知道。這里的環境變量有兩種,如果u-boot獲取到了FLASH上的環境變量則應用此環境變量啟動,否則使用默認的環境變量啟動。如果在u-boot啟動期間我們沒有按下空格鍵,則在串口打印出“Booting Linux...”,並執行函數run_command啟動內核。如果我們按下了空格鍵,則進入下面的分支語句,進入u-boot界面,readline函數獲取我們輸入的命令,run_command執行命令。所以run_command這個函數作用很大。
if ((argc = parse_line (finaltoken, argv)) == 0) { rc = -1; /* no command at all */ continue; } /* Look up command in command table */ if ((cmdtp = find_cmd(argv[0])) == NULL) { printf ("Unknown command '%s' - try 'help'\n", argv[0]); rc = -1; /* give up after bad command */ continue; } /* found - check max args */ if (argc > cmdtp->maxargs) { printf ("Usage:\n%s\n", cmdtp->usage); rc = -1; continue; }
在run_command函數中,我們能夠看到,首先,parse_line函數是對我們輸入的命令進行解析。一般我們在u-boot輸入的命令大概是這樣的:“命令,參數0,參數1,...”,所以由parse_line函數進行解析:argv[0]="命令字符串",argv[1]="參數1",argv[2]="參數2",...等等。而find_cmd則相當於一個switch,它把我們輸入的命令與在u-boot中定義的各種命令相比對,若吻合則執行相應的函數操作。若遍歷所有的命令名均沒有我們輸入的命令,則報錯。在u-boot中,我們不是通過簡單的switch進行命令的選擇和函數的執行,而是將每個命令定義成結構體。我們進入find_cmd進行分析:
/* * Some commands allow length modifiers (like "cp.b"); * compare command name only until first dot. */ len = ((p = strchr(cmd, '.')) == NULL) ? strlen (cmd) : (p - cmd); for (cmdtp = &__u_boot_cmd_start; cmdtp != &__u_boot_cmd_end; cmdtp++) { if (strncmp (cmd, cmdtp->name, len) == 0) { if (len == strlen (cmdtp->name)) return cmdtp; /* full match */ cmdtp_temp = cmdtp; /* abbreviated command ? */ n_found++; } } if (n_found == 1) { /* exactly one match */ return cmdtp_temp; }
這是find_cmd函數的主要代碼,這里根據作者的注釋我們可以知道,和我們剛剛講的一樣,就是進行命令名的比對。strncmp大家都很熟悉這個字符串比較函數,若比對成功則返回這個命令的結構體指針,否則繼續遍歷,若最終沒有找到則報錯。這里的__u_boot_cmd_start和__u_boot_cmd_end是在u-boot.lds連接腳本里定義。
__u_boot_cmd_start = .;
.u_boot_cmd : { *(.u_boot_cmd) }
__u_boot_cmd_end = .;
對於這里的u_boot_cmd大家可能不知道它是干什么的,在哪里定義的。通過查找,我們可以發現在Command.h中有一段相關的定義,是一個結構體:
#define Struct_Section __attribute__ ((unused,section (".u_boot_cmd")))
現在我們還是不知道它是干嘛的,在此搜索bootm,在Cmd_bootm.c中,我們看到了這樣的一個定義:
U_BOOT_CMD( bootm, CFG_MAXARGS, 1, do_bootm, "bootm - boot application image from memory\n", "[addr [arg ...]]\n - boot application image stored in memory\n" "\tpassing arguments 'arg ...'; when booting a Linux kernel,\n" "\t'arg' can be the address of an initrd image\n" #ifdef CONFIG_OF_FLAT_TREE "\tWhen booting a Linux kernel which requires a flat device-tree\n" "\ta third argument is required which is the address of the of the\n" "\tdevice-tree blob. To boot that kernel without an initrd image,\n" "\tuse a '-' for the second argument. If you do not pass a third\n" "\ta bd_info struct will be passed instead\n" #endif );
這里其實就定義了u_boot_cmd的這條指令的具體參數。不過此刻我們還是不明白這些參數的具體含義,再次查找u_boot_cmd,在Command.h中我們終於找到了對於bootm這條指令的宏定義:
#define U_BOOT_CMD(name,maxargs,rep,cmd,usage,help) \
cmd_tbl_t __u_boot_cmd_##name Struct_Section = {#name, maxargs, rep, cmd, usage, help}
現在開始,我們解析bootm這條命令:
name:命令名,這里將##name替換成bootm。
Struct_Section:結構體段,根據上面我們找到的宏定義,替換成__attribute__ ((unused,section (".u_boot_cmd")))。
#name:命令名,替換成"bootm"。
maxargs:最大參數,替換成CFG_MAXARGS。
rep:是否可重復,替換成1(可重復)。
cmd:命令,替換成do_bootm。
這里的usage表示短命令,help表長命令。對應U_BOOT_CMD中的字符串。經過一系列替換,這條語句最后變成了:
#define U_BOOT_CMD(name,maxargs,rep,cmd,usage,help) \
cmd_tbl_t __u_boot_cmd_bootm __attribute__ ((unused,section (".u_boot_cmd"))) = {"bootm", CFG_MAXARGS, 1, do_bootm, usage, help}
其實,經過剛才的分析,現在我們可以自己寫u-boot命令了。這里是博主自己寫的u-boot命令:
命令名是"CCJ",執行這條命令顯示的信息為:"Hi! I am CrazyCatJack!"並對輸入的命令及參數進行輸出。還有長信息和短信息如圖所示。
#include <common.h> #include <watchdog.h> #include <command.h> #include <image.h> #include <malloc.h> #include <zlib.h> #include <bzlib.h> #include <environment.h> #include <asm/byteorder.h> int do_CCJ (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[]) { int i; printf (" Hi!I am CrazyCatJack! %d\n",argc); for(i=0;i<argc;i++) { printf("%s\n",argv[i]); } return 0; } U_BOOT_CMD( CCJ, CFG_MAXARGS, 1, do_CCJ, "CCJ -CCJ short information.\n", "CCJ,long information...........................................................\n" );
這里是博主寫的源代碼,大家感興趣的可以自己分析,自己寫一個u-boot命令試試。
3.讀出內核啟動內核
經過上述一系列准備,現在終於可以讀出內核,並啟動它了。真是相當的不容易。真的感覺佩服u-boot的作者。還記得我們讀出內核和啟動內核的命令嗎?
bootcmd=nand read.jffs2 0x30007FC0 kernel; bootm 0x30007FC0
類比於PC上的硬盤,我們硬盤上都有各種各樣的分區,而我們的FLASH則是將這種分區寫到了代碼里,換言之,FLASH里的分區是不能動態更改的。通過u-boot打印的信息,我們了解到kernel的分區,我的kernel位於0x00060000,而且有2M的大小。
#define MTDPARTS_DEFAULT "mtdparts=nandflash0:256k@0(bootloader)," \ "128k(params)," \ "2m(kernel)," \ "-(root)"
這是在具體我們板子型號的頭文件中對FLASH的分區。256K裝bootloader,128K裝各種環境變量參數,2M的kernel,最后是root。在Cmd_nand.c中的do_nand函數將會對nand進行一系列細致的初始化。現在我們已經分析完第一條指令,讀出內核,下一步是啟動內核。我們的內核實際上是由一個頭部+內核組成。這里的頭部主要包含了內核的加載地址和入口地址:
typedef struct image_header { uint32_t ih_magic; /* Image Header Magic Number */ uint32_t ih_hcrc; /* Image Header CRC Checksum */ uint32_t ih_time; /* Image Creation Timestamp */ uint32_t ih_size; /* Image Data Size */ uint32_t ih_load; /* Data Load Address */ uint32_t ih_ep; /* Entry Point Address */ uint32_t ih_dcrc; /* Image Data CRC Checksum */ uint8_t ih_os; /* Operating System */ uint8_t ih_arch; /* CPU architecture */ uint8_t ih_type; /* Image Type */ uint8_t ih_comp; /* Compression Type */ uint8_t ih_name[IH_NMLEN]; /* Image Name */ } image_header_t;
這就是我們內核的頭部,其中最重要的就是加載地址ih_load:Image文件存放地址。還有入口地址ih_ep:內核真正開始執行的地址。
在我們啟動內核的時候,如果內核被放到了入口地址上,則無需移動內核文件,否則需執行搬運函數memmove (&header, (char *)addr, sizeof(image_header_t));將內核轉移到入口地址上。
接下來是執行do_bootm_linux函數,它主要的作用就是初始化TAG並啟動內核。所謂的TAG就是u-boot需要將一些參數傳遞給內核,但由於二者文件格式不同,所以不能夠直接進行通信。最直接的方法就是創建一個二者都能夠讀取的文件格式,約定好讀取規則。在TAG中u-boot向即將啟動的內核傳遞了初始化相關的信息。
#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 /* we assume that the kernel is in place */ printf ("\nStarting kernel ...\n\n"); #ifdef CONFIG_USB_DEVICE { extern void udc_disconnect (void); //udc_disconnect (); // cancled by www.100ask.net } #endif cleanup_before_linux (); theKernel (0, bd->bi_arch_number, bd->bi_boot_params);
其中的TAG就是u-boot傳遞的各項參數,最后執行theKernel (0, bd->bi_arch_number, bd->bi_boot_params);內核啟動完成!u-boot到此結束!接下來就是內核相關的操作了。
u-boot源碼分析到此結束。^_^
版權聲明:本博客為CrazyCatJack原創,未經允許禁止轉載。
博主尊重u-boot的作者DENX Software Engineering的版權,感謝他們制作出u-boot,並開源給全世界的人們學習、使用。
本博客旨在分享學習成果,幫助大家理解u-boot源代碼,讓u-boot被更多的人理解並使用。若能受到啟發,創造出更好的事物博主就沒白寫了^_^
CCJ
2016-11-25 17:53:21