這篇文章儘量地說清楚從編譯程序到把代碼燒到板子裏,而後開始上電後運行程序。不少細節是根據Arduino來寫的,由於Arduino沒有外接的serial flash或者外接的SDRAM,因此相對來講簡單一些。但整體來講這個過程對於嵌入式設備都是十分類似的。php
當按下編譯按鈕後,編譯器compiler將每一個.c文件編譯成彙編語言,彙編語言是機器能夠識別的語言。拿項目舉例來講,LED.c通過編譯器生成LED.o。而後鏈接器linker把每一個彙編文件(*.o)鏈接在一塊兒,生成最終的編譯文件並經過avrdude或者programmer下載到arduino的Flash中開始運行。html
//c language static void Task4(void) { USART_Transmit("Task4\r\n", strlen("Task4\r\n")); }
翻譯成彙編語言是git
ldi r22, 0x07 ; 7 //"Task4\r\n" 字符串長度7的low byte 存入寄存器22 ldi r23, 0x00 ; 0 //"Task4\r\n" 字符串長度7的high byte 存入寄存器23 ldi r24, 0x1E ; 30 //"Task4\r\n" 的內存位置0x011E的low byte存入寄存器24,這裏強調一下由於計算機架構的關係,這個位置在map文件中被翻譯成0080011E,後面會在詳細說明。 ldi r25, 0x01 ; 1 //"Task4\r\n" 的內存位置0x011E的high byte存入寄存器24 call 0x1f4 ; 0x1f4 <USART_Transmit> //把當前的地址壓到stack裏,而後去地址0x1f4調用函數USART_Transmit,這個函數會利用r22,r23,r24和r25 ret //從stack裏面彈出返回地址,去這個返回地址開始運行接下來的命令
你們能夠參考Atmel官方對ret的解釋,能夠清楚的看到,ret命令把stack的東西彈出而後放入PC(Program Counter:裏面記錄着當前運行的代碼的位置)裏面。github
通過編譯器的編譯後,c文件中的代碼,和變量等會被存放到不一樣的區域section,參考GCC Sections。
這裏面比較重要的是text section, data section和bss section:web
舉個例子來講明,對於下面的c代碼:架構
uint16_t Data = 0x1234; uint32_t Result; uint32_t Power(void) { Result = Data * Data; USART_Transmit("done!", 5); return Result; }
通過編譯後咱們能夠獲得:
函數
symbol table裏面記錄了text 和data section的起始位置,以及每一個函數和變量相對於對應section的偏移。工具
Arduino所使用的MCU是Atmel328P,根據數據手冊,Atmel328P有2KB的SRAM和32KB的Flash,以及1KB的EEPROM。細節請參考Atmel328P的section 12.2,我截取了數據手冊中的一幅圖:
網站
細看2KB的Internal SRAM:ui
當編譯器把全部的.c文件都編譯好後,鏈接器linker就會過來把全部的.c文件集合起來生成一個總的文件。在Atmel中,最後生成的是.elf文件。鏈接器的做用是把全部的undefined variable都找到,把它們的地址都補全,而後把所用的相對位置都計算出絕對位置,好比前面例子中,symbol table裏的函數USART_Transmit是未知位置的,這是linker就會去其餘編譯文件中找這個函數的定義,並獲得地址。最後生成的總的文件相似於第一張圖的樣子,也是開始是symbol table而後是text section,data section和bss section。
鏈接器linker須要它的配置文件,被稱爲linker file。對於Atmel328P來講,這個文件叫avr51.x
,它在Atmel Studio的安裝文件夾裏,個人路徑是(C:\Program Files (x86)\Atmel\Studio\7.0\toolchain\avr8\avr8-gnu-toolchain\avr\lib\ldscripts\avr51.x
)。打開avr51.x。
MEMORY { text (rx) : ORIGIN = 0, LENGTH = __TEXT_REGION_LENGTH__ data (rw!x) : ORIGIN = 0x800100, LENGTH = __DATA_REGION_LENGTH__ eeprom (rw!x) : ORIGIN = 0x810000, LENGTH = __EEPROM_REGION_LENGTH__ fuse (rw!x) : ORIGIN = 0x820000, LENGTH = __FUSE_REGION_LENGTH__ lock (rw!x) : ORIGIN = 0x830000, LENGTH = __LOCK_REGION_LENGTH__ signature (rw!x) : ORIGIN = 0x840000, LENGTH = __SIGNATURE_REGION_LENGTH__ user_signatures (rw!x) : ORIGIN = 0x850000, LENGTH = __USER_SIGNATURE_REGION_LENGTH__ } .text: { *(.vectors) KEEP(*(.vectors)) *(.init0) /* Start here after reset. */ KEEP (*(.init0)) *(.init1) KEEP (*(.init1)) *(.init2) /* Clear __zero_reg__, set up stack pointer. */ KEEP (*(.init2)) *(.init3) KEEP (*(.init3)) *(.init4) /* Initialize data and BSS. */ KEEP (*(.init4)) *(.init5) KEEP (*(.init5)) ... }
當Atmel328P編譯完成後,除了會生成最後的編譯文件.elf外,還會生成.map和.lss文件,不少時候這兩個文件能夠幫咱們很好的debug程序,理解程序。
下面咱們來簡單分析一下兩個文件:
在IoT_Ethernet.lss,我挑了幾段我認爲比較有意思的地方和你們分享下:
Idx Name Size VMA LMA File off Algn 0 .data 00000064 00800100 00000676 0000070a 2**0 CONTENTS, ALLOC, LOAD, DATA 1 .text 00000676 00000000 00000000 00000094 2**1 CONTENTS, ALLOC, LOAD, READONLY, CODE 2 .bss 00000096 00800164 00800164 0000076e 2**0 ALLOC
VMA vs LMA:上表中記錄了每一個section的大小和地址,值得注意的是這裏有兩個地址,一個是VMA(Virtual Memory Address),另一個是LMA(Load Memory Address)。根據這個解釋,VMA是當板子經歷過初始化階段(startup)後,該section的地址;LMA是這個section的數據從哪裏加載。因此:
這段程序是在初始化data section。在這段asssembly code中,程序從Flash的地址0x676中複製0x64 bytes(十六進制0x64,十進制100)的數據到地址0x00800100。這裏注意兩條彙編,一個是LPM,一個是ST,從Atmel Assembly的官方網站,LPM: Load Program Memory,是從program memory拿出數據,這裏的program memory指的是Flash。另一個是ST: Store Indirect From Register to Data space using Index X,這裏的data section指的是SDRAM,SDRAM從0x00800000開始,因此仔細分析這些彙編會發現,這裏的st X+, r0,X寄存器(r26和r27合在一塊兒)的範圍是從0x100開始,但實際是把數據存到了0x00800100。
00000074 <__do_copy_data>: 74: 11 e0 ldi r17, 0x01 ; 1 76: a0 e0 ldi r26, 0x00 ; 0 78: b1 e0 ldi r27, 0x01 ; 1 7a: e6 e7 ldi r30, 0x76 ; 118 7c: f6 e0 ldi r31, 0x06 ; 6 7e: 02 c0 rjmp .+4 ; 0x84 <__do_copy_data+0x10> 80: 05 90 lpm r0, Z+ 82: 0d 92 st X+, r0 84: a4 36 cpi r26, 0x64 ; 100 86: b1 07 cpc r27, r17 88: d9 f7 brne .-10 ; 0x80 <__do_copy_data+0xc>
當中斷髮生時,程序會跟據具體是哪一個中斷向量(好比定時器中斷,外部中斷等)來這個中斷向量表中找到中斷ISR(Interrupt Service Routine)的地址。好比我目前的這個中斷向量表中,當Timer0的compare match interrupt發生的時候,程序會到0x59C執行ISR;當UART接收到數據的時候,程序會到0x222執行ISR。以下面的代碼是中斷向量表的一部分:
00000000 <__vectors>: 0: 0c 94 34 00 jmp 0x68 ; 0x68 <__ctors_end> 4: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt> 8: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__bad_interrupt> ... 38: 0c 94 ce 02 jmp 0x59c ; 0x59c <__vector_14> ... 48: 0c 94 11 01 jmp 0x222 ; 0x222 <__vector_18> ...
這個文件能夠理解爲是一個symbol table,裏面包括了項目全部symbol的信息。好比
.text.LED_GetStatus 0x00000428 0x20 LowLevel/LED.o 0x00000428 LED_GetStatus
由於咱們使用AvrDude經過USB下載代碼到板子上,而AvrDude只接收Intel Hex Format,生成能被Arduino Bootloader識別的數據。使用WinAVR能夠將.elf文件傳變HEX文件(Atmel Studio已經幫咱們作這一步了)。簡單地說Intel Hex Format每行在說往某個特定的地址寫特定的數據。Intel Hex Format參考連接。
提取出.hex文件中比較直觀的一行來講:
:10066800FFFF4765744C6564537461747573005378
Bootloader是在Flash裏面的一段代碼,用來把新的代碼(新的代碼從AvrDude發到Arduino的16U2芯片,而後16U2芯片經過uart發送到Atmel328P)經過USB寫到Flash的0x00地址。若是沒有BootLoader,咱們只能經過programmer來燒代碼到Arduino,下圖是Avr ISP MKII,把它插到Arduino的ICSP header上,就能夠在沒有bootloader的狀況下給Arduino下載代碼了。若是是新購買的Arduino,它裏面已經有Bootloader了,它使用的是optiboot,它是open source的,這裏是它的github repository。下面咱們更詳細的說下bootloader。
在以前的linker file裏面有Fuse的地址:fuse (rw!x) : ORIGIN = 0x820000, LENGTH = __FUSE_REGION_LENGTH__
。Fuse裏面的信息是配置芯片的關鍵信息,查看和修改它的信息須要用ISP,通常USB接口沒辦法訪問Fuse。
當AvrDude拿到Intel Hex Format(.hex)的代碼,它會轉變成Atmel STK500格式的代碼,由於optiboot能夠識別Atmel STK500 格式。當AvrDude要發送新的代碼給Atmel328P,這是Atmel328P會先reset,而後AvrDude會發送STK500格式的數據給optiboot,optiboot會處理這些數據而後把相應的代碼從Flash的0x0000地址開始寫入。 當代碼下載完成,就從新reset芯片,而且等待程序跳出optiboot,就能夠從0x0000開始執行新的代碼了!