C語言嵌入式系統程式設計修煉之軟體架構篇
阿新 • • 發佈:2019-02-15
轉載來自下面的網頁:http://dev.yesky.com/131/2017631_2.shtml
模組劃分
模組劃分的"劃"是規劃的意思,意指怎樣合理的將一個很大的軟體劃分為一系列功能獨立的部分合作完成系統的需求。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*/ #include "module1.h" /* 在模組2中包含模組1的.h檔案 */ /*module3 .c*/ #include "module1.h" /* 在模組3中包含模組1的.h檔案 */ |
以上程式的結果是在模組1、2、3中都定義了整型變數a,a在不同的模組中對應不同的地址單元,這個世界上從來不需要這樣的程式。正確的做法是:
/*module1.h*/ extern int a; /* 在模組1的.h檔案中宣告int a */ /*module1 .c*/ #include "module1.h" /* 在模組1中包含模組1的.h檔案 */ int a = 5; /* 在模組1的.c檔案中定義int a */ /*module2 .c*/ #include "module1.h" /* 在模組2中包含模組1的.h檔案 */ /*module3 .c*/ #include "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; … } } } |
按上述方法設計的中斷服務程式很小,實際的工作都交由主程式執行了。
硬體驅動模組
一個硬體驅動模組通常應包括如下函式:
(1)中斷服務程式ISR
(2)硬體初始化
a.修改暫存器,設定硬體引數(如UART應設定其波特率,AD/DA裝置應設定其取樣速率等);
b.將中斷服務程式入口地址寫入中斷向量表:
/* 設定中斷向量表 */ m_myPtr = make_far_pointer(0l); /* 返回void far型指標void far * */ m_myPtr += ITYPE_UART; /* ITYPE_UART: uart中斷服務程式 */ /* 相對於中斷向量表首地址的偏移 */ *m_myPtr = &UART _Isr; /* UART _Isr:UART的中斷服務程式 */ |
(3)設定CPU針對該硬體的控制線
a.如果控制線可作PIO(可程式設計I/O)和控制訊號用,則設定CPU內部對應暫存器使其作為控制訊號;
b.設定CPU內部的針對該裝置的中斷遮蔽位,設定中斷方式(電平觸發還是邊緣觸發)。
(4)提供一系列針對該裝置的操作介面函式。例如,對於LCD,其驅動模組應提供繪製畫素、畫線、繪製矩陣、顯示字元點陣等函式;而對於實時鐘,其驅動模組則需提供獲取時間、設定時間等函式。
C的面向物件化
在面向物件的語言裡面,出現了類的概念。類是對特定資料的特定操作的集合體。類包含了兩個範疇:資料和操作。而C語言中的struct僅僅是資料的集合,我們可以利用函式指標將struct模擬為一個包含資料和操作的"類"。下面的C程式模擬了一個最簡單的"類":
#ifndef C_Class #define C_Class struct #endif C_Class A { C_Class A *A_this; /* this指標 */ void (*Foo)(C_Class A *A_this); /* 行為:函式指標 */ int a; /* 資料 */ int b; }; |
我們可以利用C語言模擬出面向物件的三個特性:封裝、繼承和多型,但是更多的時候,我們只是需要將資料與行為封裝以解決軟體結構混亂的問題。C模擬面向物件思想的目的不在於模擬行為本身,而在於解決某些情況下使用C語言程式設計時程式整體框架結構分散、資料和函式脫節的問題。我們在後續章節會看到這樣的例子。
總結
本篇介紹了嵌入式系統程式設計軟體架構方面的知識,主要包括模組劃分、多工還是單任務選取、單任務程式典型架構、中斷服務程式、硬體驅動模組設計等,從巨集觀上給出了一個嵌入式系統軟體所包含的主要元素。
請記住:軟體結構是軟體的靈魂!結構混亂的程式面目可憎,除錯、測試、維護、升級都極度困難。