在剛剛涉足嵌入式開發的時候,總想找到這樣一本書,它能夠解決我一些這樣那樣的疑惑。可是遺憾的是,到如今也沒有這樣一本書面世,並且我想永遠也不可能面世了。由於個人疑惑太多太雜了。這些疑惑在教科書中又難以尋找到答案。C 教程注重講C 的語法,編譯原理注重講語法,語義的分析。每一門教科書都是有它的注重,因此那些交叉的問題便成了三無論。市場上的那些自稱爲《XX 寶典》、《XX 聖經》的書卻老是說一些可能連做者本身也沒搞清楚的問題。因而我想,我想了解的也許是你們都想了解的吧,那麼把我學到的一點東西寫出來,你們也許就能夠少花點時間在上面,留出寶貴的腦力資源去作更有意義的事。編程
語言選擇,C 仍是其餘網絡
剛剛涉及嵌入式開發者老是先閱讀一些指導類型文章,而後就開始對開發語言的選擇躊躇不決。是C 仍是C++?仍是好像更熱門的JAVA?不用猶豫,至少目前看來C 仍是你的選擇。嵌入式開發的本質是訂製開發,硬件平臺林林總總,處理能力高下不一樣,若是想保護你學習精力投資的話,C 是最好的「優績股」。C++的優勢在於它的代碼重用,可是效率比C低不少,最重要的是,並不是全部芯片的編譯器都能支持C++。JAVA 就更不用說起,在一個虛擬平臺上開發的優勢是不用關心具體的硬件細節,但這不是一個嵌入式開發者的做風,換一種說法,這種開發不能稱之爲嵌入式開發。函數
C被稱爲高級語言中的低級語言,低級語言中的高級語言,這是由於其一方面有高級語言所具備的接近於人類思想的語言體系,另外一方面同時支持地址與位操做。能夠方便的與硬件打交道。嵌入式開發必然要操做IO、硬件地址,沒有位操做和指針你又如何方便作到?工具
嵌入式開發通常流程學習
嵌入式開發的流程與高層開發大致相似,編碼——編譯、連接——運行。中間固然能夠有聯機調試,從新編碼等遞歸過程。但有一些不一樣之處。編碼
首先,開發平臺不一樣。受嵌入式平臺處理能力所限,嵌入式開發通常都採用交叉編譯環境開發。所謂交叉編譯就是在A 平臺上編譯B 平臺上運行的目標程序。在A 平臺上運行的B 平臺程序編譯器就被稱爲交叉編譯器。一個初入門者,創建一套這樣的編譯環境也許就要花掉幾天的時間。spa
其次,調試方式不一樣。咱們在Windows 或者Linux 上開發的程序能夠立刻運行察看運行結果,也能夠利用IDE 來調試運行過程,可是嵌入式開發者卻至少須要做一系列工做才能達到這種地步。操作系統
目前最流行的是採用JTAG 方式鏈接到目標系統上,將編譯成功的代碼下載運行,高級的調試器幾乎能夠像VC 環境同樣任意的調試程序。再者,開發者所瞭解層次結構不一樣。高層軟件開發者把工做的重點放在對應用需求的理解和實現上。設計
嵌入式開發者對整個過程細節必須比高層開發者有更深的認識。最大不一樣之處在於有操做系統支持的程序不須要你關心程序的運行地址以及程序連接後各個程序塊最後的位置。像Windows,Linux 這類須要MMU 支持的操做系統,其程序都是放置在虛擬地址空間的一個固定的內存地址。無論程序在真正RAM 空間的地址位置在哪裏,最後都由MMU映射到虛擬地址空間的一個固定的地址。指針
爲何程序的運行與存放的地址要相關呢?學過彙編原理,或者看過最後編譯成機器碼程序的人就知道,程序中的變量、函數最後都在機器碼中體現爲地址,程序的跳轉,子程序的調用,以及變量調用最後都是CPU 經過直接提取其地址來實現的。嵌入式學習企鵝意義氣嗚嗚吧久零就易。編譯時指定的TEXT_BASE 就是全部一切地址的參考值。若是你指定的地址與最後程序放置的地址不一致顯然不能正常運行。
但也有例外,不過不尋常的用法固然要付出不尋常的努力。有兩種方法能夠解決這個問題。
一種方法是在程序的最起始編寫與地址無關的代碼,最後將後面的程序自搬移到你真正指定的TEXT_BASE 而後跳轉到你將要運行的代碼處。
另外一種方法是,TEXT_BASE 指定爲你程序的存放地址,而後將程序搬移到真正運行的地址,有一個變量將後者的地址記錄下來做爲參考值,在之後的符號表地址都以此值做爲參考與偏移值合成爲其真正的地址。
聽起來很拗口,實現起來也很難,在後面的內容中有更好的解決辦法——用一個BootLoader 支持。另外,一個完整的程序必然至少有三個段TEXT (正文,也就是最後用程序編譯後的機器指令)段、BSS(未初始變量)段DATA(初始化變量)段。前面講到的TEXT_BASE 只是TEXT 段的基址,對於另外的BSS 段和DATA 段,若是最後的整個程序放在RAM 中,那麼三個段能夠連續放置,可是,若是程序是放置在ROM 或者FLASH 這種只讀存儲器中,那麼你還須要指定你的其餘段的地址,由於代碼在運行中是不改變的,然後二者卻不一樣。這些工做都是在連接的時候完成,編譯器必然爲你提供了一些手段讓你完成這些工做。
仍是那句話,有操做系統支持的編程屏蔽了這些細節,讓你徹底不用考慮這些頭痛的問題。可是嵌入式開發者沒有那麼幸運,他們老是在一個冷冰冰的芯片上從頭作起。CPU 上電覆位老是從一個固定的地址去找程序,開始其繁忙的工做。對於咱們的PC 來講這個地址就是咱們的BIOS 程序,對於嵌入式系統,通常沒有BIOS 支持,RAM 不能在掉電狀況下保留你的程序,因此必須將程序存放在ROM 或FLASH中,可是通常來說,這些存儲器的寬度和速度都沒法與RAM 相提並論。
程序在這些存儲器上運行會下降運行速率。大多數的方案是在此處存放一個BootLoader,BootLoader 所完成的功能可多可少,一個基本的BootLoader 只完成一些系統初始化並將用戶程序搬移到必定地址,而後跳轉到用戶程序即交出CPU 控制權,功能強大的BootLoad 還能夠支持網絡、串口下載,甚至調試功能。但不要期望有一個像PC BIOS 那樣通用的BootLoader 供你使用,至少你須要做一些移植工做使其符合你的系統,這個移植工做也是你開發的一個部分,做爲嵌入式開發個入門者來說,移植或者編寫一個BootLoader 會使你受益不淺。
沒有BootLoader 行不行?固然能夠,要麼你就犧牲效率直接從ROM 中運行,要麼你就本身編寫程序搬移代碼去RAM 運行,最主要的是,開發過程當中你要有好的調試工具支持在線調試,不然你就得在改動哪怕一個變量的狀況下都要去從新燒片驗證。繼續程序入口的話題,無論過程如何,程序最後在執行時都是變成了機器指令,一個純的執行程序就是這些機器指令的集合。像咱們在操做系統上的可運行程序都不是純的執行程序,而是帶有格式的.嵌入式學習更多內容請加企鵝意義氣嗚嗚吧久零就易。通常除了包含上面提到的幾個段之外,還有程序的長度,校驗以及程序入口——就是從哪兒開始執行用戶程序。
爲何有了程序地址還須要有程序的入口呢?這是由於你要真正開始執行的代碼並不是必定放置在一個文件的最開始,就算放在最開始,除非你去控制連接,不然在多文件的狀況下,編譯器也不必定將你的這段程序放置在最後程序的最頂端。像咱們通常有操做系統支持的程序,只需在你的代碼中有一個main 做爲程序入口——注意這個main 只是大多數編譯器約成定俗的入口,除非你利用了別人的初始化庫,不然程序入口能夠自行設定——便可。顯然,帶有格式的這種執行文件使用更加靈活,但須要BootLoader 的支持。有關執行文件格式的內容能夠看看ELF 文件格式。
編譯預處理
首先看看文件包含,從咱們的第一個C 程序Hello World! 開始,咱們就使用頭文件包含,可是另人驚奇的是,不少人在作了很長時間的開發之後仍然對文件的包含沒有正確的認識或者是概念不清,有更多的人卻把頭文件和與之相關聯的庫混淆。
爲了照顧這些初學者,這裏羅嗦一下,其實文件包含的本質就是把一個大的文件截成幾個小文件便於管理和閱讀,若是你包含了那個文件,那麼你把這個文件的全部內容原封不動的複製到你包含其的文件中,效果是徹底同樣的,另外一方面,若是你編譯了一些中間代碼,如庫文件,能夠經過提供頭文件來告知調用者你的庫包含的函數和調用格式,可是真正的代碼已經變成了目標代碼以庫文件形式存在了。至於包含文件的後綴如.h 只是告訴使用者,這是一個頭文件,你用任何別的名字,編譯器都通常不會在乎。
那些對頭文件和庫還混淆的朋友應該恍然大悟了吧,其實頭文件只能保證你的程序編譯不出現語法錯誤,可是直到最後連接的時候纔會真正使用到庫,那些只把一個頭文件拷貝來就想擁有一個庫的人不再要犯這樣的錯誤了。若是你的工程中源程序數目繁多令你以爲管理困難,把他們所有包含在一個文件中也何嘗不可。
另外一個初學者經常遇到的問題就是因爲重複包含引發的困惑。若是一個文件中包含了另外一個文件兩次或兩次以上極可能引發重複定義的問題,可是沒有人蠢到會重複包含兩次同一個文件的,這種問題都是隱式的重複包含,好比A 文件中包含了B 文件和C 文件,B 文件中又包含了C 文件,這樣,A 文件實際上已經包含了C 文件兩次。不過一個好的頭文件巧妙的利用編譯預處理避免了這種狀況。在頭文件中你可能發現這樣的一些預處理:
#ifndef __TEST_H__
#define __TEST_H__
… …
#endif /* __TEST_H__ */
這三行編譯預處理前兩行通常位於文件最頂端,最後文件位於文件最末端,它的意思是,若是沒有定義__TEST_H__那麼就定義__TEST_H__同時下面的代碼一直到#endif 前參與編譯,反之不參與編譯。多麼巧妙的設計,有了這三行簡潔的預處理,這個文件即便被包含幾萬次也只能算一次。
咱們再來看看宏的使用。初學者在看別人代碼的時候老是想,爲何用那麼多宏呢?看得人一頭霧水,的確,有時候宏的使用會下降代碼的可讀性。但有時宏也能夠提升代碼的可讀性,看看下邊這兩段代碼:
1)
#define SCC_GSMRH_RSYN 0x00000001 /* receive sync timing */
#define SCC_GSMRH_RTSM 0x00000002 /* RTS* mode */
#define SCC_GSMRH_SYNL 0x0000000c /* sync length */
#define SCC_GSMRH_TXSY 0x00000010 /* transmitter/receiver sync*/
#define SCC_GSMRH_RFW 0x00000020 /* Rx FIFO width */
#define SCC_GSMRH_TFL 0x00000040 /* transmit FIFO length */
#define SCC_GSMRH_CTSS 0x00000080 /* CTS* sampling */
#define SCC_GSMRH_CDS 0x00000100 /* CD* sampling */
#define SCC_GSMRH_CTSP 0x00000200 /* CTS* pulse */
#define SCC_GSMRH_CDP 0x00000400 /* CD* pulse */
#define SCC_GSMRH_TTX 0x00000800 /* transparent transmitter */
#define SCC_GSMRH_TRX 0x00001000 /* transparent receiver */
#define SCC_GSMRH_REVD 0x00002000 /* reverse data */
#define SCC_GSMRH_TCRC 0x0000c000 /* transparent CRC */
#define SCC_GSMRH_GDE 0x00010000 /* glitch detect enable */
*(int *)0xff000a04 = SCC_GSMRH_REVD | SCC_GSMRH_TRX | SCC_GSMRH_TTX |
SCC_GSMRH_CDP | SCC_GSMRH_CTSP | SCC_GSMRH_CDS | SCC_GSMRH_CTSS;
2)
*(int *)0xff000a04 = 0x00003f80;
這是對某一個寄存器的賦值程序,二者完成的是徹底相同的工做。第一段代碼略顯冗長,第二段代碼很簡潔,可是若是你若是想改動此寄存器的設置的時候顯然更喜歡看到的是第一段代碼,由於它現有的值已經很清楚,要對那些位賦值只要用相應得宏定義便可,沒必要每次改變都拿筆再從新計算一次。這一點對於嵌入式開發者很重要,有時咱們調試一個設備的時候,一個關鍵寄存器的值也許會被咱們修改不少次,每一次都計算每一位所對應得值是一件很頭疼的事。
另外利用宏也能夠提升代碼的運行效率,子程序的調用須要壓棧出棧,這一過程若是過於頻繁會耗費掉大量的CPU 運算資源。因此一些代碼量小但運行頻繁的代碼若是採用帶參數宏來實現會提升代碼的運行效率,好比咱們經常用到的對外部IO 賦值的操做,你能夠寫一個相似下邊的函數來實現:
void outb(unsigned char val, unsigned int *addr)
{
*addr = val;
}
僅僅是一句語句的函數,卻要調用一個函數,若是不用函數呢,重複寫上面的語句又顯得羅嗦。不如用下面的宏實現。
#define outb(b, addr) (*(volatile unsigned char *)(addr) = (b))
因爲不須要調用子函數,宏提升了運行效率,可是浪費了程序空間,這是因爲凡是用到此宏的地方,都要替換爲一句其代替的語句。開發者須要根據系統需求取捨時間與空間。