1. 程式人生 > >一位久經沙場的嵌入式er站在初學者角度談談嵌入式開發與學習的一些問題

一位久經沙場的嵌入式er站在初學者角度談談嵌入式開發與學習的一些問題

一位久經沙場的嵌入式er站在初學者角度談談嵌入式開發與學習的一些問題

在剛剛涉足嵌入式開發的時候,總想找到這樣一本書,它可以解決我一些這樣那樣的疑惑。但是遺憾的是,到現在也沒有這樣一本書面世,而且我想永遠也不可能面世了。因為我的疑惑太多太雜了。這些疑惑在教科書中又難以尋找到答案。C 教程注重講C 的語法,編譯原理注重講語法,語義的分析。每一門教科書都是有它的注重,所以那些交叉的問題便成了三不管。市場上的那些自稱為《XX 寶典》、《XX 聖經》的書卻總是說一些可能連作者自己也沒搞清楚的問題。於是我想,我想了解的也許是大家都想了解的吧,那麼把我學到的一點東西寫出來,大家也許就可以少花點時間在上面,留出寶貴的腦力資源去做更有意義的事。

語言選擇,C 還是其他

剛剛涉及嵌入式開發者總是先閱讀一些指導型別文章,然後就開始對開發語言的選擇躊躇不決。是C 還是C++?還是好像更熱門的JAVA?不用猶豫,至少目前看來C 還是你的選擇。嵌入式開發的本質是訂製開發,硬體平臺林林總總,處理能力高下不同,如果想保護你學習精力投資的話,C 是最好的優績股C++的優點在於它的程式碼重用,但是效率比C低很多,最重要的是,並非所有晶片的編譯器都能支援C++JAVA 就更不用提及,在一個虛擬平臺上開發的優點是不用關心具體的硬體細節,但這不是一個嵌入式開發者的作風,換一種說法,這種開發不能稱之為嵌入式開發。

C被稱為高階語言中的低階語言,低階語言中的高階語言,這是因為其一方面有高階語言所具有的接近於人類思想的語言體系,另一方面同時支援地址與位操作。可以方便的與硬體打交道。嵌入式開發必然要操作

IO、硬體地址,沒有位操作和指標你又如何方便做到?

嵌入式開發一般流程

嵌入式開發的流程與高層開發大體類似,編碼——編譯、連結——執行。中間當然可以有聯機除錯,重新編碼等遞迴過程。但有一些不同之處。

首先,開發平臺不同。受嵌入式平臺處理能力所限,嵌入式開發一般都採用交叉編譯環境開發。所謂交叉編譯就是在A 平臺上編譯B 平臺上執行的目標程式。在A 平臺上執行的B 平臺程式編譯器就被稱為交叉編譯器。一個初入門者,建立一套這樣的編譯環境也許就要花掉幾天的時間。

其次,除錯方式不同。我們在Windows 或者Linux 上開發的程式可以馬上執行察看執行結果,也可以利用IDE 來除錯執行過程,但是嵌入式開發者卻至少需要作一系列工作才能達到這種地步。

目前最流行的是採用JTAG 方式連線到目標系統上,將編譯成功的程式碼下載執行,高階的偵錯程式幾乎可以像VC 環境一樣任意的除錯程式。再者,開發者所瞭解層次結構不同。高層軟體開發者把工作的重點放在對應用需求的理解和實現上。

嵌入式開發者對整個過程細節必須比高層開發者有更深的認識。最大不同之處在於有作業系統支援的程式不需要你關心程式的執行地址以及程式連結後各個程式塊最後的位置。像WindowsLinux 這類需要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 相提並論。

程式在這些儲存器上執行會降低執行速率。大多數的方案是在此處存放一個BootLoaderBootLoader 所完成的功能可多可少,一個基本的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))

由於不需要呼叫子函式,巨集提高了執行效率,但是浪費了程式空間,這是由於凡是用到此巨集的地方,都要替換為一句其代替的語句。開發者需要根據系統需求取捨時間與空間。