應用程序開發的一個重要方面就是代碼的維護,但也卻是被更快的產品上市要求所忽 略。這對於某一些程序來說,也許並不嚴重。這是因為這類產品的生命周期很短,或者這 類產品一旦部署就再也不會動它了。
然而,嵌入式軟件的生命周期卻往往長達數 年,這就意味著前期的一些失誤會導致後期的大量損失。
嵌入式軟件就意味著有 較長的生命周期,在設計和實現的時候就一定要考慮維護的問題。下面的技巧雖然不能保 證完整,但的確能夠點出一些常見的問題。記住他們並不要成為那些擁有痛苦記憶的一員 。
技巧#1 避免匯編代碼
當然,在一些低端PIC單片機上你沒得選擇,而在 一些高端ARM處理器上你也可能不需要,但在此兩個極端之間,還有很多平台使用匯編來 提高性能,減少代碼。然而,簡單的使用匯編來也可能把你的項目打回幾個月前。
匯編可以直接訪問機器功能,但性能的提升會被真正理解程序在做什麼這件事所 替代。這也就是為什麼高級語言,例如C和Java,被設計出來的原因。
當調試的時 候,每一段匯編代碼都可疑,而高級語言的異常安全特性就顯得容易的多了。如果必須使 用匯編,嘗試逐句注釋。C和Java中,注釋可能會把代碼弄亂,但在匯編語言中,注釋可 就會節約很多時間並不會讓人覺得挫敗了。
注釋也可以針對程序塊,但確保程序 塊不要超過5~6條語句。理想情況下,算法可以用偽碼的形式寫在注釋中。(參見技巧 #8)
技巧#2 避免注釋變更
這是一條通用編程技巧,但在嵌入式編程中,保 證代碼和其注釋關聯尤其重要。當代碼更新的時候,這中錯誤非常容易發生,並且導致代 碼很難理解。下面的例子就展示了隨時間流逝,注釋是多麼容易被變更。
(譯者 注:為了理解方便,代碼中的注釋不做翻譯)
// This function adds two numbers and returns the result
#if __DEBUG
void printNumber(int num) {
printf("Output: %dn", num);
}
#endif
// This function multiplies two numbers and returns the result
int multiply(int a, int b) {
return a*b;
}
int add(int a, int b) {
#if __DEBUG
// Debugging output
printNumber(a+b);
#endif
return a+b;
}
可以看到函數Add的注釋和代碼之間插入了 printNumber函數。後來的人發現這個Add函數並挨著它加入了multiply函數,這樣就使得 Add函數和它的注釋文檔斷開了。為了避免這個問題,將注釋可以寫在函數內部,或者用 線條來前後隔開注釋。
技巧#3 不要過早的優化
程序設計中的一條大罪就 是過早優化。然而,這條戒律卻常常被時間限制,邋遢的代碼或者過於熱心的程序員所破 壞。任何你所編寫的程序應當盡可能從簡單著手,並嚴格按照設計功能實現。如果要求性 能,那也應當以簡單實現為優先。
只有完成了完整代碼塊(可能是一個程序或者 一個大系統的組件)的測試,再回頭去做優化。無計劃的,隨意性的代碼優化很容易造成 維護的噩夢。優化後的代碼往往難於理解,並可能無法達到優化的目的。最好是能夠使用 分析工具(例如gprof或者intel的Vtune)來查找瓶頸,針對瓶頸來優化可能會達到意外 的效果。
技巧#4 中斷服務程序要盡可能的簡單
同時考慮到性能和維護, 中斷服務程序必須要盡可能的簡單。與生俱來的異步特性使得中斷服務程序的調試要比通 常的程序難了許多。讓其做的事情盡可能少對於維護程序來說就尤為重要。把ISR中的數 據處理移到主程序中,這樣ISR可以解放出來去僅僅抓取數據(例如從硬件設備)並把數 據放到臨時buffer中以備用。可以用一個標志來通知主程序數據待處理。
技巧#5 把調式代碼從源文件中移除
在開發過程中,你可能會加入大量的調式代碼,例如 冗長的文本輸出,斷言,LED閃爍等等。當項目結束,就應當把這些代碼清除掉,尤其是 那些隨意加入的調式代碼。
清掃代碼是一個高尚的工作,但去掉調試信息也可能 帶來的問題。維護代碼的兄弟將可能會加入產生這些代碼,如果代碼已經有了,就會使得 維護變得容易。如果這些代碼需要在產品構建的時候去掉,使用條件編譯或者把調試代碼 放進中介模塊或者庫中,並保證不會鏈接到產品中去。最初的開發應當考慮到編寫文檔和 清理調式代碼的時間;這些額外的時間將會物有所值。
技巧#6 給提供調用封裝接 口
盡可能將底層I/O接口與上層應用邏輯通過接口來分離,寫到一起會讓程序維護 變得異常困難。把程序的所有功能放到幾個大函數中會讓代碼變得難於理解並更難調試。 這對於硬件接口來說更加中藥。你可能會直接訪問硬件寄存器或者I/O,或者平台提供的 API,但更建議封裝自己的接口。
通常你無法控制硬件行為,如果你還不得不修改 平台,在程序中寫有硬件相關的代碼(API或者直接操作,無論哪種方式)將使得代碼更難 以移植。
如果你封裝了自己的接口,可能是一些宏來定義硬件API,你的代碼就可 以保值一致,所有需要移植的僅僅是那些集中起來的接口而已。
技巧#7 根據需要 分裂函數
嵌入式軟件與PC軟件不同之處在於代碼與使用的硬件有很大關系。將函 數單元盡可能分割成為最小塊並不建議-在一個范圍內保證大概少於6~6個函數調用為宜 。硬件功能單元與軟件代碼塊要對應。
過分分割程序將會將調用關系復雜,使得 調式和理解變得困難。
技巧#8 編寫文檔
根據代碼維護文檔,最好也有對 應的硬件副本。當為程序寫文檔的時候,盡量把設計和程序原型寫到代碼中。如果必須要 分離開來,作為注釋的源文件放到項目中。(譯者注:這裡沒有將link譯作鏈接是因為個 人判斷。注釋不會被連接器鏈接到程序中的,因此就簡單譯作了“放”這個詞 )
如果你擁有版本管理系統(例如CVS或者Microsoft Source Safe),把文檔和代 碼放到同一目錄下。如果沒有放到一起,文檔那個很容易被遺失(譯者注:這一點個人不 敢苟同,還是有很多方法來維護文檔的,將代碼和文檔放到一起會讓項目文件變得混亂)
最好將所有文檔和源碼刻到CD(或者你願意的可移動存儲設備)上並放到安全位 置。你的繼任者將會非常感謝你的。
技巧#9 不要投機取巧
就像預先優化 一樣,取巧的代碼可能會導致大麻煩。C和C++在嵌入式世界中居於統治地位,有大把的解 決問題的方法。模板,繼承,goto,三元運算符(?),這個列表可以很長。
真 正聰明的程序員會想出極端缜密和優雅的方法來使用這些工具來解決問題。但問題是通常 只有這個程序員才理解這些方法(並也很可能忘記它是如何工作的)
唯一解決方 法就是避免取巧和使用那與理解的語言特性。例如:不要依賴C語言的短路賦值語句(譯 者注:||或者&&運算)和三元運算符來控制程序(使用if語言).
技巧 #10 將所有定義放在一個地方
這個簡單;如果有大量的常量定義或者條件定義, 把他們集中放置。可以是一個單獨文件或者源代碼路徑。如果你把定義深深隱藏在代碼中 ,它會讓你吃苦頭的。
Timothy Stapko是Digi International 的首席程序員,專 注於嵌入式產品中的Rabbit。Stapko用於8年以上開發經驗,是《實用嵌入式系統安全》 的作者。