如果要做嵌入式Linux,我們首先要在板子上燒寫的往往不是kernel,而是u-boot,這時需要燒寫工具幫忙。當u-boot燒寫成功后,我們就可以用u-boot附帶的網絡功能來燒寫kernel了。每當板子上電時,u-boot一般會被加載到內存的前半段,如果我們的kernel之前就已經被燒寫到開發板了,那么u-boot會加載kernel到內存的后半段並跳轉到kernel的起始地址處執行(或者直接跳轉到kernel的起始地址處執行,如果kernel可以直接在flash上執行的話。)
如上圖所示,綠色部分為u-boot,紅色部分為kernel。
把loader(指u-boot)和kernel分離究竟有什么好處呢?
舉個極端的例子:沒有grub的話,我們就沒辦法做windows和linux雙系統了。這就是最大的好處。
然而對於嵌入式,我倒是說不出什么上得了台面的理由,根據個人喜好,我倒是有3點理由:
1、不用再求助燒寫工具了;
2、方便使用GNU交叉編譯工具;
3、擺脫Windows+linux虛擬機的工作平台。
現在,我的筆記本就可以輕松一下了,只需單開fedora/ubuntu就能工作啦!
以下是源碼和工程的下載鏈接:
注:僅可使用在stm32f10x系列
接下來,我們將分為三部分敘述:
1、系統概述;
2、kernel;
3、“my-boot”;
4、先燒寫"my-boot“,然后用"my-boot”加載kernel——操作示例;
1、系統概述
接下來我們將建立兩個工程,一個是用來編譯kernel,一個用來編譯loader(姑且命名為“my-boot”)。首先,我們先把“my-boot”和kernel都編譯好,並通過燒寫工具把“my-boot”燒寫進stm32的flash中。然后,我們就可以重啟stm32,並使之運行“my-boot”。“my-boot”等待接收燒寫kernel的起始命令,當我們通過串口向“my-boot”發送了燒寫起始命令后,“my-boot”將把串口設置為DMA模式,並等待我們發送kernel的bin文件。接着,我們再通過串口傳送kernel的bin文件。傳送結束后,kernel也就被寫入stm32的RAM中了,同時“my-boot”把串口切換回通常的窗口通信模式。此時,芯片的控制權依舊被掌控在“my-boot”手中,不過,如果我們再向串口發送一條啟動kernel的指令,那么stm32將跳轉到kernel代碼處執行。至此,我們的目標達成。
2、kernel
我們的kernel很簡單,只有一個源文件,其功能就是不停的閃led。程序參考了博客http://www.cnblogs.com/sky1991/archive/2012/10/13/2722640.html的“例子一”,並加以修改與簡化,代碼給出如下:
1 ;RCC寄存器地址映像 2 RCC_BASE EQU 0x40021000 3 RCC_CR EQU (RCC_BASE + 0x00) 4 RCC_CFGR EQU (RCC_BASE + 0x04) 5 RCC_CIR EQU (RCC_BASE + 0x08) 6 RCC_APB2RSTR EQU (RCC_BASE + 0x0C) 7 RCC_APB1RSTR EQU (RCC_BASE + 0x10) 8 RCC_AHBENR EQU (RCC_BASE + 0x14) 9 RCC_APB2ENR EQU (RCC_BASE + 0x18) 10 RCC_APB1ENR EQU (RCC_BASE + 0x1C) 11 RCC_BDCR EQU (RCC_BASE + 0x20) 12 RCC_CSR EQU (RCC_BASE + 0x24) 13 ;GPIO寄存器地址映像 14 GPIOA_BASE EQU 0x40010800 15 GPIOA_CRL EQU (GPIOA_BASE + 0x00) 16 GPIOA_CRH EQU (GPIOA_BASE + 0x04) 17 GPIOA_IDR EQU (GPIOA_BASE + 0x08) 18 GPIOA_ODR EQU (GPIOA_BASE + 0x0C) 19 GPIOA_BSRR EQU (GPIOA_BASE + 0x10) 20 GPIOA_BRR EQU (GPIOA_BASE + 0x14) 21 GPIOA_LCKR EQU (GPIOA_BASE + 0x18) 22 23 SETENA0 EQU 0xE000E100 24 SETENA1 EQU 0xE000E104 25 26 ;;FLASH緩沖寄存器地址映像 27 FLASH_ACR EQU 0x40022000 28 29 ;----------------- 30 MSP_TOP EQU 0x20005000 ;主堆棧起始值 31 PSP_TOP EQU 0x20004E00 ;進程堆棧起始值 32 33 DelayTime EQU 13000000 ; to choose a better number to fit your cpu 34 CLRPEND0 EQU 0xE000E280 35 36 ;常數定義--------- 37 Bit0 EQU 0x00000001 38 Bit1 EQU 0x00000002 39 Bit2 EQU 0x00000004 40 Bit3 EQU 0x00000008 41 Bit4 EQU 0x00000010 42 Bit5 EQU 0x00000020 43 Bit6 EQU 0x00000040 44 Bit7 EQU 0x00000080 45 Bit8 EQU 0x00000100 46 Bit9 EQU 0x00000200 47 Bit10 EQU 0x00000400 48 Bit11 EQU 0x00000800 49 Bit12 EQU 0x00001000 50 Bit13 EQU 0x00002000 51 Bit14 EQU 0x00004000 52 Bit15 EQU 0x00008000 53 Bit16 EQU 0x00010000 54 Bit17 EQU 0x00020000 55 Bit18 EQU 0x00040000 56 Bit19 EQU 0x00080000 57 Bit20 EQU 0x00100000 58 Bit21 EQU 0x00200000 59 Bit22 EQU 0x00400000 60 Bit23 EQU 0x00800000 61 Bit24 EQU 0x01000000 62 Bit25 EQU 0x02000000 63 Bit26 EQU 0x04000000 64 Bit27 EQU 0x08000000 65 Bit28 EQU 0x10000000 66 Bit29 EQU 0x20000000 67 Bit30 EQU 0x40000000 68 Bit31 EQU 0x80000000 69 70 71 ;向量表********************************************************************************* 72 AREA RESET, DATA, READONLY 73 74 DCD MSP_TOP ;初始化主堆棧 75 DCD Start ;復位向量 76 DCD NMI_Handler ;NMI Handler 77 DCD HardFault_Handler ;Hard Fault Handler 78 ;*************************************************************************************** 79 AREA |.text|, CODE, READONLY 80 ;主程序開始 81 ENTRY ;指示程序從這里開始執行 82 Start 83 CPSID I ;關中斷 84 ldr r0, =MSP_TOP 85 msr msp, r0 ;重設MSP 86 mov r0, #0 87 msr control, r0 ;切換MSP,並進入特權級 88 89 mov r0, #0 90 mov r1, #0 91 mov r2, #0 92 mov r3, #0 93 mov lr, #0 94 95 ldr r0, =CLRPEND0 96 ldr r1, [r0] 97 orr r1, #0xFFFFFFFF 98 str r1, [r0] 99 100 101 ;時鍾系統設置 102 ;啟動外部8M晶振 103 104 ldr r0,=RCC_CR 105 ldr r1,[r0] 106 orr r1,#Bit16 107 str r1,[r0] 108 ClkOk 109 ldr r1,[r0] 110 ands r1,#Bit17 111 beq ClkOk 112 ldr r1,[r0] 113 orr r1,#Bit17 114 str r1,[r0] 115 ;FLASH緩沖器 116 ldr r0,=FLASH_ACR 117 mov r1,#0x00000032 118 str r1,[r0] 119 ;設置PLL鎖相環倍率為7,HSE輸入不分頻 120 ldr r0,=RCC_CFGR 121 ldr r1,[r0] 122 orr r1,#Bit18 | Bit19 | Bit20 | Bit16 | Bit14 123 orr r1,#Bit10 124 str r1,[r0] 125 ;啟動PLL鎖相環 126 ldr r0,=RCC_CR 127 ldr r1,[r0] 128 orr r1,#Bit24 129 str r1,[r0] 130 PllOk 131 ldr r1,[r0] 132 ands r1,#Bit25 133 beq PllOk 134 ;選擇PLL時鍾作為系統時鍾 135 ldr r0,=RCC_CFGR 136 ldr r1,[r0] 137 orr r1,#Bit18 | Bit19 | Bit20 | Bit16 | Bit14 138 orr r1,#Bit10 139 orr r1,#Bit1 140 str r1,[r0] 141 ;其它RCC相關設置 142 ldr r0,=RCC_APB2ENR 143 mov r1,#Bit2 144 str r1,[r0] 145 ;IO端口設置 146 ldr r0,=GPIOA_CRH 147 ldr r1,[r0] 148 orr r1,#Bit0 | Bit1 ;PA.8輸出模式,最大速度50MHz 149 and r1,#~Bit2 & ~Bit3 ;PA.8通用推挽輸出模式 150 str r1,[r0] 151 152 mov r5, #0 ; led flag 153 154 ;CPSIE I 155 ;主循環================================================================================= 156 main 157 bl Delay 158 bl LedFlas 159 b main 160 ;子程序********************************************************************************** 161 LedFlas 162 push {r0-r3} 163 cmp r5,#1 164 beq ONLED 165 166 mov r5, #1 167 ;PA.8輸出1 168 ldr r0,=GPIOA_BRR 169 ldr r1,[r0] 170 orr r1,#Bit8 171 str r1,[r0] 172 b LedEx 173 ONLED 174 mov r5, #0 175 ;PA.8輸出0 176 ldr r0,=GPIOA_BSRR 177 ldr r1,[r0] 178 orr r1,#Bit8 179 str r1,[r0] 180 LedEx 181 pop {r0-r3} 182 bx lr 183 184 Delay 185 push {r0-r3} 186 187 ldr r0, =DelayTime 188 Loop CBZ r0, LoopExit 189 sub r0, #1 190 b Loop 191 LoopExit 192 pop {r0-r3} 193 bx lr 194 ;異常程序******************************************************************************* 195 NMI_Handler 196 ;xxxxxxxxxxxxxxxxxx 197 bx lr 198 ;----------------------------- 199 HardFault_Handler 200 ;xxxxxxxxxxxxxxxxxx 201 bx lr 202 ;*************************************************************************************** 203 ALIGN ;通過用零或空指令NOP填充,來使當前位置與一個指定的邊界對齊 204 ;----------------------------- 205 END
(1)主循環程序:
main
bl Delay // 延時
bl LedFlas // 翻轉led
b main // 跳轉會main開頭(即“延時”)
(2)延時程序:
Delay
push {r0-r3}
ldr r0, =DelayTime // r0 = DelayTime;
Loop
CBZ r0, LoopExit // if(r0 != 0) {
sub r0, #1 // r0 -= 1;
b Loop // goto Loop; }
LoopExit
pop {r0-r3}
bx lr
該延時程序是“C51式”的延時,就是純粹的讓CPU空跑n個周期,這里是“DelayTime=13000000“。“13000000”是隨便設的一個數,只是為了讓眼睛和耐性都能接受,時鍾頻率變化之后,這個數字可以自行的、隨性的去進行調整。
(3)LED翻轉程序:
LedFlas
push {r0-r3}
cmp r5,#1 // if(r5 == 1)
beq ONLED // goto ONLED;
mov r5, #1 // r5 = 1;
;PA.8輸出1
ldr r0,=GPIOA_BRR
ldr r1,[r0]
orr r1,#Bit8
str r1,[r0]
b LedEx
ONLED
mov r5, #0 // r5 = 0;
;PA.8輸出0
ldr r0,=GPIOA_BSRR
ldr r1,[r0]
orr r1,#Bit8
str r1,[r0]
LedEx
pop {r0-r3}
bx lr
該LED翻轉程序以“r5”寄存器為標志,“r5”為0或1時,分別使PA.8輸出不同的電平(此處PA.8對應開發板上一個紅色LED)。
注:
一般MDK會生成hex文件,但不生成bin文件,所以我們還要給MDK加一些設置:
先找到fromelf.exe文件(一般在你的MDK安裝目錄里的bin目錄里),然后如下圖輸入,
如:
C:\Keil_v5\ARM\ARMCC\bin\fromelf.exe --bin --output kernel.bin kernel.axf
重新編譯之后,於是我們就得到kernel的bin文件了,即kernel.bin,留着備用。
此處的kernel是可以獨立運行的,所以不妨將該程序通過燒寫工具燒寫進開發板驗證一下。
Note:
如果要用arm-gcc的kernel,首先,你的Linux必須得有arm-gcc編譯工具。可使用目錄中提供的腳本build.sh直接編譯。此處我用的是“arm-none-eabi-as”等,如果是arm-linux-eabi-as等,需要簡單修改腳本中的“PREFIX”變量。
3、“my-boot”
我們知道,在kernel和loader之間,真正的主角是kernel,loader只是一個輔助工具罷了。然而,作為loader的"my-boot"在這里卻比kernel復雜許多。
“my-boot”以一步步學習操作系統(1)中的代碼為基礎,並將之整理了一下,把各個源文件分類到了不同的目錄。
如圖,除了obj目錄是存放編譯時所用的中間文件和hex文件外,其余4個目錄都存放源碼。
(1)arch目錄:其中的源碼均是和CPU架構相關,如中斷代碼、串口初始化、啟動代碼等;
(2)include目錄:所有的頭文件都在這里;
(3)kernel目錄:包含主函數、任務調度、延時相關的源碼;
(4)lib目錄:stm32f10x庫函數源碼及“printf”重定向至串口的輔助代碼(printf_to_serial)。
主程序一共建立3個任務:Task1, TaskBH, TaskDMA_Print。
1 int main(void) 2 { 3 memset(SRAM_Buffer, 0, PAGE_SIZE); 4 OSInit(); 5 6 OSTaskCreate(Task1, (void*)0, (OS_STK*)&Task1Stk[TASK_STACK_SIZE-1]); 7 OSTaskCreate(TaskBH, (void*)0, (OS_STK*)&TaskBHStk[TASK_STACK_SIZE-1]); 8 OSTaskCreate(TaskDMA_Print, (void*)0, (OS_STK*)&TaskDMA_PrintStk[TASK_STACK_SIZE-1]); 9 10 OSStart(); 11 }
Task1:和kernel的功能一樣,也是不斷的閃led(最好是不同於kernel所使用的led),用來指示程序依舊正常運行,其功能很單純;
TaskBH:接受串口發送過來的相關命令,並向串口打印信息以提示命令發送成功。特別是當收到“startos”指令后,會置位變量“GotoKernelFlag”,以致后續代碼將跳轉到kernel運行,該任務是三個任務中最復雜的一個;
TaskDMA_Print:打印RAM中的kernel代碼。
其實以上三個任務的負擔並不重,身上擔子最重的時串口中斷程序:
串口通信遵循一個自定義的協議,協議內容如下:
將以下串口中斷程序與TaskBH結合着看,串口接受三種命令:
第一種:BURN命令:
如:
"BURN 0x08004000"
協議信息16進制表示為
57 41 4e 15 00 75 42 55 52 4e 20 30 78 30 38 30 30 34 30 30 30
該命令就是在通知開發板:“我要發送kernel了呦,趕緊准備接駕。”
這時,串口中斷程序會啟動串口的DMA模式,並開啟DMA中斷。
關於命令中的地址“0x08004000”,該值是設計為以后燒寫flash做准備的,但現在我們只將kernel寫入SRAM,所以現在還沒有特別的作用,任意值都可以。
這個命令發送之后就要小心了,緊跟着必須向串口發送kernel的bin文件。發送結束后,DMA中斷會被觸發,並且會調用“LED1TURN()”去翻轉另一個LED(不同於Task1的LED),用以指示kernel已經被寫入RAM。
Note:看了以下代碼后,其實對於"BURN”這個命令來說,校驗和是形同虛設的,為了圖方便就偷了個懶……
1 volatile void IRQ_Usart1(void) 2 { 3 4 RecvBuffer[Index] = serial_1; 5 6 // Magic handling 7 // Byte order: 0 1 2 8 if(!MagicGotten) { 9 if(0 == Index && 'W' == RecvBuffer[Index]) { 10 Index++; 11 }else if(1 == Index && 'A' == RecvBuffer[Index]) { 12 Index++; 13 }else if(2 == Index && 'N' == RecvBuffer[Index]) { 14 Index++; 15 MagicGotten = TRUE; 16 }else { 17 Index = 0; 18 } 19 return; 20 } 21 22 // Size handling 23 // byte order: 3 4 24 if(!SizeGotten) { 25 Index++; 26 if(5 == Index) { 27 SizeGotten = TRUE; 28 MsgSize = RecvBuffer[3] + (RecvBuffer[4] << 8); 29 } 30 if(SizeGotten && MsgSize > BUFSIZ) { 31 MagicGotten = FALSE; 32 SizeGotten = FALSE; 33 Index = 0; 34 } 35 return; 36 } 37 38 // Checksum handling 39 // byte order: 5 40 if(!ChecksumGotten) { 41 Index++; 42 if(6 == Index) { 43 ChecksumGotten = TRUE; 44 }else { 45 MagicGotten = FALSE; 46 SizeGotten = FALSE; 47 Index = 0; 48 } 49 return; 50 } 51 52 // Data handling: 53 // byte order: 6... 54 Index++; 55 if(Index >= MsgSize) { 56 MagicGotten = FALSE; 57 SizeGotten = FALSE; 58 ChecksumGotten = FALSE; 59 Index = 0; 60 MsgGotten = TRUE; 61 if(0 == strncmp((char *)RecvBuffer + 6, "BURN", 4)) { 62 USART_Cmd(USART1, DISABLE); 63 USART_ITConfig(USART1, USART_IT_RXNE, DISABLE); 64 USART_DMACmd(USART1,USART_DMAReq_Rx,ENABLE); 65 DMA1_Channel5->CNDTR = PAGE_SIZE;//re-load 66 DMA_Cmd(DMA1_Channel5, ENABLE);//re-open DMA 67 USART_Cmd(USART1, ENABLE); 68 LED1TURN(); 69 } 70 } 71 }
第二種:“startos”
協議信息16進制表示為
57 41 4e 0d 00 f8 73 74 61 72 74 6f 73
開發板接收到該命令后,TaskBH會將變量“GotoKernelFlag”設為1。之后,當SysTick中斷程序(如下)再次執行時,將會調用“ModifyPC()”(這里的“PC”不是指“Personal Computer”,而是指PC指令寄存器哦)。這個函數很難懂。如果能理解這個函數,那么loader加載kernel的原理也就等於理解了80%了。我們不妨來試着啃一啃這塊硬骨頭!
1 volatile void IRQ_SysTick(void) 2 { 3 OS_ENTER_CRITICAL(); 4 if(GotoKernelFlag) ModifyPC(); 5 if((--TaskTimeSlice) == 0){ 6 TaskTimeSlice = TASK_TIME_SLICE; 7 OSTaskSchedule(); 8 } 9 TimeMS++; 10 11 OS_EXIT_CRITICAL(); 12 }
“ModifyPC()”是嵌入C語言式的匯編代碼。其作用就是:
修改 PSP中存儲的、“當前被SysTick中斷的任務”的 PC指針,使之等於kernel代碼的起始地址。當該任務再一次被調度時,由於PC被換成了kernel代碼的起始地址,所以就進入了kernel。
於是,兩個問題出現了:
(1)kernel的起始地址是什么?
(2)被SysTick中斷的任務的PC又在哪?
或許有人會認為:“kernel在DMA傳送時,被放進‘SRAM_Buffer’這個緩沖區了,那么kernel的起始地址不就是‘SRAM_Buffer’嗎?”(一開始我也是這么想的……)
可惜,真正的“起始地址”要比SRAM_Buffer在靠后一點點。
不妨在MDK5下,在kernel工程里打開Debug,接着再用二進制編輯器打開kernel.bin,這樣就能看出蹊蹺了。
stm32燒寫程序時,是將代碼燒至起始地址為0x08000000的flash中,並在開機運行時也是直接從flash啟動。
看到沒有,我們開機時的第一條命令是“CPSID I”,對應的指令地址為0x08000010,機器碼為“B672”。
再用二進制文件打開kernel.bin后,發現果然是“B672”(二進制文件為“小端法”表示,所以是“72 B6”)。
所以,我們的kernel代碼的起始地址,確切來說是第一條命令“CPSID I”的地址為“SRAM_Buffer + 0x10”。
Note:
“既然代碼燒寫進地址為0x08000000起始的地方,那么第一條指令為什么確實0x08000010呢?”
意味0x08000010 - 0x08000000 = 0x10 = 16 = 4*4,也即代碼開頭的“4個DCD”,每個DCD4字節。
第二個問題,“被SysTick中斷的任務”的PC到底在哪兒呢?
首先我們要知道,任務使用的是PSP(可參考“PendSV_Handler”的匯編代碼)。確認了這點之后,我們就可以繼續往下講了。
根據《Cortex-M3權威指南》--“chap09中斷的具體行為”--“入棧”,當SysTick中斷發生時,PSP會將發生如下圖的變化。
也就是說,當SysTick中斷發生時,CPU會自動將被中斷任務的R0-R3,R12,LR,PC,xPSR這8個寄存器裝載進PSP的后續存儲空間,並且PSP最后將指向被中斷任務R0寄存器的存儲地址。
那么被中斷任務的PC寄存器的存儲地址就找到啦:PSP+24!如果該任務再次被調度執行,其第一條指令就是地址“PSP+24”存儲的內容,如果我“偷偷的”把這個存儲內容換成kernel代碼的起始地址(確切來說,是第一條指令所在的地址),那么當該任務再次被調度時,原來的任務搖身一變,就成了kernel。
那么,ModifyPC()函數的代碼就比較容易理解了。
PCModifyPC偽代碼可寫為:
ModifyPC()
PSP.PC = SRAM_Buffer+0x10
1 __asm void ModifyPC(void) { 2 IMPORT SRAM_Buffer 3 MRS R0, PSP 4 LDR R1, =SRAM_Buffer 5 ADD R1, #0x10 6 STR R1, [R0, #24] 7 BX LR 8 align 4 9 }
第三種:任意字符串
如:“ls”
協議信息16進制表示為
57 41 4e 08 00 31 6c 73
該命令將對TaskDMA_Print的行為產生影響(代碼如下)。
不難看出,只有當“ReadDMAFlag不為0時,該任務才會打印緩沖區SRAM_Buffer的內容。而在TaskBH中,上述命令會使變量“ReadDMAFlag”在0,1之間翻轉,所以該命令也就起到控制打印“SRAM_Buffer”內容的作用。
1 void TaskDMA_Print(void *p_arg) 2 { 3 int i = 0; 4 while(1) { 5 delayMs(2000); 6 if(!ReadDMAFlag) continue; 7 printf("########DMA##########START\r\n"); 8 for(i = 0; i < PAGE_SIZE; i++) { 9 printf("%x ", SRAM_Buffer[i]); 10 } 11 printf("########DMA##########END\r\n"); 12 13 } 14 }
“my-boot"中幾點注意事項:
(1)宏定義PAGE_SIZE
該宏在hardware.h中定義如下:
#define PAGE_SIZE 284
“284”?這個數字怎么這么莫名其妙?其實它表示的是kernel的大小(如下圖),同時它也決定了緩沖區SRAM_Buffer的大小。
如果我編譯了一個新的kernel,大小不再是284字節了怎么辦?
實在對不住!“my-boot”中的這個宏也要改成相應的數字。當然,這確實是個不合理的地方,但現在為使代碼盡可能簡潔,所以就未做完善這方面的工作了,暫且辛苦一下。
(2)預設宏定義:USE_STDPERIPH_DRIVER
為了使用stm32的函數庫,且避免編譯出錯,故定義該宏。具體內容可查詢“stm32f10x.h”第8296行附近的代碼。
(3)庫函數文件:
如果stm32的型號不是stm32f10x系列的,需要自備相應的函數庫。
4、先燒寫"my-boot“,然后用"my-boot”加載kernel——操作示例
(1)將“my-boot”燒進stm32開發板
(2)向stm32開發板發送燒寫命令:
BURN 0x08004000
16進制表示為
57 41 4e 15 00 75 42 55 52 4e 20 30 78 30 38 30 30 34 30 30 30
命令發送之后,串口工具會打印信息“Addr: 8004000”。而且還有一個變化,那就是另一個LED燈亮了/滅了(如果存在第二個led的話)。
注意,是16進制發送。
(3)發送kernel.bin
這時我們會發現,剛剛亮了/滅了的LED現在又滅了/亮了(如果存在第二個led的話)。
(4)打印剛剛燒進SRAM中的kernel命令(可選):
ls
16進制表示為
57 41 4e 08 00 31 6c 73
該命令發送一次,就會打印一次“###ls###”,並且跟后會打印SRAM中的內容。如果該命令只發送一次,那么SRAM中的打印將每隔2秒打印一次,直到再一次發送該命令為止。
所以圖中有2個“###ls###”,第二個就是終止打印的。
(5)啟動kernel:
startos
16進制表示為
57 41 4e 0d 00 f8 73 74 61 72 74 6f 73
這時你將會看到開發板在運行kernel的程序啦!