天天看點

C語言嵌入式系統程式設計修煉之道——軟體架構篇

<a></a>

<b>作者:</b>宋寶華<b>  e-mail:</b>[email][email protected][/email]<b></b>

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

(1)    子產品即是一個.c檔案和一個.h檔案的結合,頭檔案(.h)中是對于該子產品接口的聲明;

(2)    某子產品提供給其它子產品調用的外部函數及資料需在.h中檔案中冠以extern關鍵字聲明;

(3)    子產品内的函數和全局變量需在.c檔案開頭冠以static關鍵字聲明;

(4)    永遠不要在.h檔案中定義變量!定義變量和聲明變量的差別在于定義會産生記憶體配置設定的操作,是<b>彙編階段</b>的概念;而聲明則隻是告訴包含該聲明的子產品在<b>連接配接階段</b>從其它子產品尋找外部函數和變量。如:

/*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在不同的子產品中對應不同的位址單元,這個世界上從來不需要這樣的程式。正确的做法是:

extern int a;               /* 在子產品1的.h檔案中聲明int a  */

#include “module1.h”        /* 在子產品1中包含子產品1的.h檔案 */

int a = 5;                 /* 在子產品1的.c檔案中定義int a  */

#include “module1.h”        /* 在子產品2中包含子產品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:

     …

     }

    }

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

一個硬體驅動子產品通常應包括如下函數:

(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 = &amp;UART _Isr;   /* UART _Isr:UART的中斷服務程式 */

(3)設定CPU針對該硬體的控制線

a.如果控制線可作PIO(可程式設計I/O)和控制信号用,則設定CPU内部對應寄存器使其作為控制信号;

b.設定CPU内部的針對該裝置的中斷屏蔽位,設定中斷方式(電平觸發還是邊緣觸發)。

(4)提供一系列針對該裝置的操作接口函數。例如,對于LCD,其驅動子產品應提供繪制像素、畫線、繪制矩陣、顯示字元點陣等函數;而對于實時鐘,其驅動子產品則需提供擷取時間、設定時間等函數。

在面向對象的語言裡面,出現了類的概念。類是對特定資料的特定操作的集合體。類包含了兩個範疇:資料和操作。而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語言程式設計時程式整體架構結構分散、資料和函數脫節的問題。我們在後續章節會看到這樣的例子。

本篇介紹了嵌入式系統程式設計軟體架構方面的知識,主要包括子產品劃分、多任務還是單任務選取、單任務程式典型架構、中斷服務程式、硬體驅動子產品設計等,從宏觀上給出了一個嵌入式系統軟體所包含的主要元素。

請記住:軟體結構是軟體的靈魂!結構混亂的程式面目可憎,調試、測試、維護、更新都極度困難。

一個高尚的程式員應該是寫出如藝術作品般程式的程式員。

 本文轉自 21cnbao 51CTO部落格,原文連結:http://blog.51cto.com/21cnbao/120790,如需轉載請自行聯系原作者

繼續閱讀