1. 程式人生 > >C語言在嵌入式系統程式設計時的注意事項1

C語言在嵌入式系統程式設計時的注意事項1

C語言是一門通用計算機程式語言,應用廣泛。C語言的設計目標是提供一種能以簡易的方式編譯、處理低階儲存器、產生少量的機器碼以及不需要任何執行環境支援便能執行的程式語言。

儘管C語言提供了許多低階處理的功能,但仍然保持著良好跨平臺的特性,以一個標準規格寫出的C語言程式可在許多電腦平臺上進行編譯,甚至包含一些嵌入式處理器(微控制器或稱MCU)以及超級電腦等作業平臺。

20世紀80年代,為了避免各開發廠商用的C語言語法產生差異,由美國國家標準局為C語言訂定了一套完整的國際標準語法,稱為ANSI C,作為C語言最初的標準。  

C語言嵌入式系統程式設計注意事項

不同於一般形式的軟體程式設計,嵌入式系統程式設計建立在特定的硬體平臺上,勢必要求其程式語言具備較強的硬體直接操作能力。無疑,組合語言具備這樣的特質。但是,歸因於組合語言開發過程的複雜性,它並不是嵌入式系統開發的一般選擇。而與之相比,

C語言--一種“高階的低階”語言,則成為嵌入式系統開發的最佳選擇。筆者在嵌入式系統專案的開發過程中,一次又一次感受到C語言的精妙,沉醉於C語言給嵌入式開發帶來的便利。

大多數嵌入式系統的硬體平臺。它包括兩部分:

1) 以通用處理器為中心的協議處理模組,用於網路控制協議的處理;

2) 以數字訊號處理器(DSP)為中心的訊號處理模組,用於調製、解調和數/模訊號轉換。

本文的討論主要圍繞以通用處理器為中心的協議處理模組進行,因為它更多地牽涉到具體的C語言程式設計技巧。而DSP程式設計則重點關注具體的數字訊號處理演算法,主要涉及通訊領域的知識,不是本文的討論重點。

著眼於討論普遍的嵌入式系統

C程式設計技巧,系統的協議處理模組沒有選擇特別的CPU,嵌入式系統學習加意義氣嗚嗚吧久林就易,而是選擇了眾所周知的CPU晶片--80186,每一位學習過《微機原理》的讀者都應該對此晶片有一個基本的認識,且對其指令集比較熟悉。80186的字長是16位,可以定址到的記憶體空間為1MB,只有實地址模式。C語言編譯生成的指標為32位(雙字),高16位為段地址,低16位為段內編譯,一段最多64KB。

協議處理模組中的FLASH和RAM幾乎是每個嵌入式系統的必備裝置,前者用於儲存程式,後者則是程式執行時指令及資料的存放位置。系統所選擇的FLASH和RAM的位寬都為16位,與CPU一致。

實時鐘晶片可以為系統定時,給出當前的年、月、日及具體時間(小時、分、秒及毫秒),可以設定其經過一段時間即向

CPU提出中斷或設定報警時間到來時向CPU提出中斷(類似鬧鐘功能)。

NVRAM(非易失去性RAM)具有掉電不丟失資料的特性,可以用於儲存系統的設定資訊,譬如網路協議引數等。在系統掉電或重新啟動後,仍然可以讀取先前的設定資訊。其位寬為8位,比CPU字長小。文章特意選擇一個與CPU字長不一致的儲存晶片,為後文中一節的討論創造條件。

UART則完成CPU並行資料傳輸與RS-232序列資料傳輸的轉換,它可以在接收到[1~MAX_BUFFER]位元組後向CPU提出中斷,MAX_BUFFER為UART晶片儲存接收到位元組的最大緩衝區。

鍵盤控制器和顯示控制器則完成系統人機介面的控制。

以上提供的是一個較完備的嵌入式系統硬體架構,實際的系統可能包含更少的外設。之所以選擇一個完備的系統,是為了後文更全面的討論嵌入式系統C語言程式設計技巧的方方面面,所有裝置都會成為後文的分析目標。

嵌入式系統需要良好的軟體開發環境的支援,由於嵌入式系統的目標機資源受限,不可能在其上建立龐大、複雜的開發環境,因而其開發環境和目標執行環境相互分離。因此,嵌入式應用軟體的開發方式一般是,在宿主機(Host)上建立開發環境,進行應用程式編碼和交叉編譯,然後宿主機同目標機(Target)建立連線,將應用程式下載到目標機上進行交叉除錯,經過除錯和優化,最後將應用程式固化到目標機中實際執行。

CAD-UL是適用於x86處理器的嵌入式應用軟體開發環境,它執行在Windows作業系統之上,可生成x86處理器的目的碼並通過PC機的COM口(RS-232串列埠)或乙太網口下載到目標機上執行。其駐留於目標機FLASH儲存器中的monitor程式可以監控宿主機Windows除錯平臺上的使用者除錯指令,獲取CPU暫存器的值及目標機儲存空間、I/O空間的內容。

後續章節將從軟體架構、記憶體操作、螢幕操作、鍵盤操作、效能優化等多方面闡述C語言嵌入式系統的程式設計技巧。軟體架構是一個巨集觀概念,與具體硬體的聯絡不大;記憶體操作主要涉及系統中的FLASH、RAM和NVRAM晶片;螢幕操作則涉及顯示控制器和實時鐘;鍵盤操作主要涉及鍵盤控制器;效能優化則給出一些具體的減小程式時間、空間消耗的技巧。

在我們的修煉旅途中將經過25個關口,這些關口主分為兩類,一類是技巧型,有很強的適用性;一類則是常識型,在理論上有些意義。

So, let’s go.

C語言嵌入式系統程式設計注意事項之軟體架構篇

模組劃分的“劃”是規劃的意思,意指怎樣合理的將一個很大的軟體劃分為一系列功能獨立的部分合作完成系統的需求。

模組劃分

模組劃分的“劃”是規劃的意思,意指怎樣合理的將一個很大的軟體劃分為一系列功能獨立的部分合作完成系統的需求。C語言作為一種結構化的程式設計語言,在模組的劃分上主要依據功能(依功能進行劃分在面向物件設計中成為一個錯誤,牛頓定律遇到了相對論),C語言模組化程式設計需理解如下概念:

1) 模組即是一個.c檔案和一個.h檔案的結合,標頭檔案(.h)中是對於該模組介面的宣告;

2) 某模組提供給其它模組呼叫的外部函式及資料需在.h中檔案中冠以extern關鍵字宣告;

3) 模組內的函式和全域性變數需在.c檔案開頭冠以staTIc關鍵字宣告;

4) 永遠不要在.h檔案中定義變數!定義變數和宣告變數的區別在於定義會產生記憶體分配的操作,是彙編階段的概念;而宣告則只是告訴包含該宣告的模組在連線階段從其它模組尋找外部函式和變數。如:

/*module1.h*/

int a = 5; /* 在模組1的.h檔案中定義int a */

/*module1 .c*/

#include “module1.h” /* 在模組1中包含模組1的.h檔案 */

/*module2 .c*/

  #i nclude “module1.h” /* 在模組2中包含模組1的.h檔案 */

/*module3 .c*/

  #i nclude “module1.h” /* 在模組3中包含模組1的.h檔案 */

以上程式的結果是在模組1、2、3中都定義了整型變數a,a在不同的模組中對應不同的地址單元,這個世界上從來不需要這樣的程式。正確的做法是:

/*module1.h*/

extern int a; /* 在模組1的.h檔案中宣告int a */

/*module1 .c*/

  #i nclude “module1.h” /* 在模組1中包含模組1的.h檔案 */

int a = 5; /* 在模組1的.c檔案中定義int a */

/*module2 .c*/

  #i nclude “module1.h” /* 在模組2中包含模組1的.h檔案 */

/*module3 .c*/

  #i nclude “module1.h” /* 在模組3中包含模組1的.h檔案 */

這樣如果模組1、2、3操作a的話,對應的是同一片記憶體單元。

一個嵌入式系統通常包括兩類模組:

1)硬體驅動模組,一種特定硬體對應一個模組;

2)軟體功能模組,其模組的劃分應滿足低偶合、高內聚的要求。

多工還是單任務

所謂“單任務系統”是指該系統不能支援多工併發操作,巨集觀序列地執行一個任務。而多工系統則可以巨集觀並行(微觀上可能序列)地“同時”執行多個任務。

多工的併發執行通常依賴於一個多工作業系統(OS),多工OS的核心是系統排程器,它使用任務控制塊(TCB)來管理任務排程功能。TCB包括任務的當前狀態、優先順序、要等待的事件或資源、任務程式碼的起始地址、初始堆疊指標等資訊。排程器在任務被啟用時,要用到這些資訊。此外,TCB還被用來存放任務的“上下文”(context)。任務的上下文就是當一個執行中的任務被停止時,所要儲存的所有資訊。通常,上下文就是計算機當前的狀態,也即各個暫存器的內容。當發生任務切換時,當前執行的任務的上下文被存入TCB,並將要被執行的任務的上下文從它的TCB中取出,放入各個暫存器中。

嵌入式多工OS的典型例子有Vxworks、ucLinux等。嵌入式OS並非遙不可及的神壇之物,我們可以用不到1000行程式碼實現一個針對80186處理器的功能最簡單的OS核心,作者正準備進行此項工作,希望能將心得貢獻給大家。

究竟選擇多工還是單任務方式,依賴於軟體的體系是否龐大。例如,絕大多數手機程式都是多工的,但也有一些小靈通的協議棧是單任務的,沒有作業系統,它們的主程式輪流呼叫各個軟體模組的處理程式,模擬多工環境。

單任務程式典型架構

1)從CPU復位時的指定地址開始執行;

2)跳轉至彙編程式碼startup處執行;

3)跳轉至使用者主程式main執行,在main中完成:

       a.初試化各硬體裝置;

b.初始化各軟體模組;

c.進入死迴圈(無限迴圈),呼叫各模組的處理函式

使用者主程式和各模組的處理函式都以C語言完成。使用者主程式最後都進入了一個死迴圈,其首選方案是:

while(1)

{

}

有的程式設計師這樣寫:

for(;;)

{

}

這個語法沒有確切表達程式碼的含義,我們從for(;;)看不出什麼,只有弄明白for(;;)在C語言中意味著無條件迴圈才明白其意。

下面是幾個“著名”的死迴圈:

1)作業系統是死迴圈;

2)WIN32程式是死迴圈;

3)嵌入式系統軟體是死迴圈;

4)多執行緒程式的執行緒處理函式是死迴圈。

你可能會辯駁,大聲說:“凡事都不是絕對的,2、3、4都可以不是死迴圈”。Yes,you are right,但是你得不到鮮花和掌聲。實際上,這是一個沒有太大意義的牛角尖,因為這個世界從來不需要一個處理完幾個訊息就喊著要OS殺死它的WIN32程式,不需要一個剛開始RUN就自行了斷的嵌入式系統,不需要莫名其妙啟動一個做一點事就幹掉自己的執行緒。有時候,過於嚴謹製造的不是便利而是麻煩。君不見,五層的TCP/IP協議棧超越嚴謹的ISO/OSI七層協議棧大行其道成為事實上的標準?

經常有網友討論:

printf(“%d,%d”,++i,i++); /* 輸出是什麼?*/

c = a+++b; /* c=? */

等類似問題。面對這些問題,我們只能發出由衷的感慨:世界上還有很多有意義的事情等著我們去消化攝入的食物。

實際上,嵌入式系統要執行到世界末日。

中斷服務程式

中斷是嵌入式系統中重要的組成部分,但是在標準C中不包含中斷。許多編譯開發商在標準C上增加了對中斷的支援,提供新的關鍵字用於標示中斷服務程式(ISR),類似於__interrupt、#program interrupt等。當一個函式被定義為ISR的時候,編譯器會自動為該函式增加中斷服務程式所需要的中斷現場入棧和出棧程式碼。

中斷服務程式需要滿足如下要求:

1)不能返回值;

2)不能向ISR傳遞引數;

3) ISR應該儘可能的短小精悍;

4) printf(char * lpFormatString,…)函式會帶來重入和效能問題,不能在ISR中採用。

在某專案的開發中,我們設計了一個佇列,在中斷服務程式中,只是將中斷型別新增入該佇列中,在主程式的死迴圈中不斷掃描中斷佇列是否有中斷,有則取出佇列中的第一個中斷型別,進行相應處理。

/* 存放中斷的佇列 */

typedef struct tagIntQueue

{

int intType; /* 中斷型別 */

struct tagIntQueue *next;

}IntQueue;

IntQueue lpIntQueueHead;

__interrupt ISRexample ()

{

int intType;

intType = GetSystemType();

QueueAddTail(lpIntQueueHead, intType);/* 在佇列尾加入新的中斷 */

}

在主程式迴圈中判斷是否有中斷:

While(1)

{

If( !IsIntQueueEmpty() )

{

intType = GetFirsTInt();

switch(intType) /* 是不是很象WIN32程式的訊息解析函式? */

{

/* 對,我們的中斷型別解析很類似於訊息驅動 */

case xxx: /* 我們稱其為“中斷驅動”吧? */

break;

case xxx:

break;

}

}

}

按上述方法設計的中斷服務程式很小,實際的工作都交由主程式執行了。

模組劃分的“劃”是規劃的意思,意指怎樣合理的將一個很大的軟體劃分為一系列功能獨立的部分合作完成系統的需求