嵌入式C語言基礎教程一

 

 
嵌入式系統C語言編程——背景
嵌入式系統C語言編程修煉之道——軟件架構                                                                     1.模塊劃分
2.多任務還是單任務
3.單任務程序典型架構
4.中斷服務程序
5.硬件驅動模塊
6.C的面向對象化...
總結... 10
嵌入式系統C語言編程道——內存操作                                                                                1.數據指針                                                                                                                                 2.函數指針                                                                                                                                3.數組vs.動態申請                                                                                                                     4.關鍵字const
5.關鍵字volatile.
6.CPU字長與存儲器位寬不一致處理
總結
嵌入式系統C語言編程——屏幕操作
1.漢字處理                                                                                                                                2.系統時間顯示                                                                                                                         3.動畫顯示
4.菜單操作                                                                                                                                 5.模擬MessageBox函數總結
嵌入式系統C語言編程——鍵盤操作篇                                                                                1.處理功能鍵                                                                                                                            2.處理數字鍵                                                                                                                            3.整理用戶輸入
總結
嵌入式系統C語言編程修煉之道——性能優化
1.使用宏定義
2.使用寄存器變量
3.內嵌彙編
4.利用硬件特性
5.活用位操作
總結
 
 
嵌入式系統C語言編程——背景篇
不同於一般形式的軟件編程,嵌入式系統編程建立在特定的硬件平臺上,勢必要求其編程語言具備較強的硬件直接操作能力。無疑,彙編語言具備這樣的特質。但是,歸因於彙編語言開發過程的複雜性,它並不是嵌入式系統開發的一般選擇。而與之相比,C語言——一種“高級的低級”語言,則成爲嵌入式系統開發的最佳選擇。筆者在嵌入式系統項目的開發過程中,一次又一次感受到C語言的精妙,沉醉於C語言給嵌入式開發帶來的便利。本文的目的在於進行“C語言嵌入式系統開發的內功心法”秀,一共包括25招。
圖1給出了本文的討論所基於的硬件平臺,實際上,這也是大多數嵌入式系統的硬件平臺。它包括兩部分:
(1)     以通用處理器爲中心的協議處理模塊,用於網絡控制協議的處理;
(2)     以數字信號處理器(DSP)爲中心的信號處理模塊,用於調製、解調和數/模信號轉換。
本文的討論主要圍繞以通用處理器爲中心的協議處理模塊進行,因爲它更多地牽涉到具體的C語言編程技巧。而DSP編程則重點關注具體的數字信號處理算法,主要涉及通信領域的知識,不是本文的討論重點。
着眼於討論普遍的嵌入式系統C編程技巧,系統的協議處理模塊沒有選擇特別的CPU,而是選擇了衆所周知的CPU芯片——80186,每一位學習過《微機原理》的讀者都應該對此芯片有一個基本的認識,且對其指令集比較熟悉。80186的字長是16位,可以尋址到的內存空間爲1MB,只有實地址模式。C語言編譯生成的指針爲32位(雙字),高16位爲段地址,低16位爲段內編譯,一段最多64KB。
圖1  系統硬件架構
協議處理模塊中的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串口)或以太網口下載到目標機上運行,如圖2。其駐留於目標機FLASH存儲器中的monitor程序可以監控宿主機Windows調試平臺上的用戶調試指令,獲取CPU寄存器的值及目標機存儲空間、I/O空間的內容。
圖2  交叉開發環境
後續章節將從軟件架構、內存操作、屏幕操作、鍵盤操作、性能優化等多方面闡述C語言嵌入式系統的編程技巧。軟件架構是一個宏觀概念,與具體硬件的聯繫不大;內存操作主要涉及系統中的FLASH、RAM和NVRAM芯片;屏幕操作則涉及顯示控制器和實時鐘;鍵盤操作主要涉及鍵盤控制器;性能優化則給出一些具體的減小程序時間、空間消耗的技巧。
本文即將講述的25個主題可分爲兩類,一類是編程技巧,有很強的適用性;一類則介紹嵌入式系統編程的一般常識,具有一定的理論意義。
So, let’s go.
 
嵌入式系統C語言編程——軟件架構篇
1.模塊劃分
模塊劃分的“劃”是規劃的意思,意指怎樣合理的將一個很大的軟件劃分爲一系列功能獨立的部分合作完成系統的需求。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)軟件功能模塊,其模塊的劃分應滿足低偶合、高內聚的要求。
2.多任務還是單任務
所謂“單任務系統”是指該系統不能支持多任務併發操作,宏觀串行地執行一個任務。而多任務系統則可以宏觀並行(微觀上可能串行)地“同時”執行多個任務。
多任務的併發執行通常依賴於一個多任務操作系統(OS),多任務OS的核心是系統調度器,它使用任務控制塊(TCB)來管理任務調度功能。TCB包括任務的當前狀態、優先級、要等待的事件或資源、任務程序碼的起始地址、初始堆棧指針等信息。調度器在任務被激活時,要用到這些信息。此外,TCB還被用來存放任務的“上下文”(context)。任務的上下文就是當一個執行中的任務被停止時,所要保存的所有信息。通常,上下文就是計算機當前的狀態,也即各個寄存器的內容。當發生任務切換時,當前運行的任務的上下文被存入TCB,並將要被執行的任務的上下文從它的TCB中取出,放入各個寄存器中。
嵌入式多任務OS的典型例子有Vxworks、ucLinux等。嵌入式OS並非遙不可及的神壇之物,我們可以用不到1000行代碼實現一個針對80186處理器的功能最簡單的OS內核,作者正準備進行此項工作,希望能將心得貢獻給大家。
究竟選擇多任務還是單任務方式,依賴於軟件的體系是否龐大。例如,絕大多數手機程序都是多任務的,但也有一些小靈通的協議棧是單任務的,沒有操作系統,它們的主程序輪流調用各個軟件模塊的處理程序,模擬多任務環境。
3.單任務程序典型架構
(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=? */
等類似問題。面對這些問題,我們只能發出由衷的感慨:世界上還有很多有意義的事情等着我們去消化攝入的食物。
實際上,嵌入式系統要運行到世界末日。
4.中斷服務程序
中斷是嵌入式系統中重要的組成部分,但是在標準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;
     …
     }
}
    }
按上述方法設計的中斷服務程序很小,實際的工作都交由主程序執行了。
5.硬件驅動模塊
一個硬件驅動模塊通常應包括如下函數:
(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,其驅動模塊應提供繪製像素、畫線、繪製矩陣、顯示字符點陣等函數;而對於實時鐘,其驅動模塊則需提供獲取時間、設置時間等函數。
6.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語言編程時程序整體框架結構分散、數據和函數脫節的問題。我們在後續章節會看到這樣的例子。
總結
本篇介紹了嵌入式系統編程軟件架構方面的知識,主要包括模塊劃分、多任務還是單任務選取、單任務程序典型架構、中斷服務程序、硬件驅動模塊設計等,從宏觀上給出了一個嵌入式系統軟件所包含的主要元素。
請記住:軟件結構是軟件的靈魂!結構混亂的程序面目可憎,調試、測試、維護、升級都極度困難。
一個高尚的程序員應該是寫出如藝術作品般程序的程序員。
 
C語言嵌入式系統編程修煉之道——內存操作篇
1.數據指針
在嵌入式系統的編程中,常常要求在特定的內存單元讀寫內容,彙編有對應的MOV指令,而除C/C++以外的其它編程語言基本沒有直接訪問絕對地址的能力。在嵌入式系統的實際調試中,多借助C語言指針所具有的對絕對地址單元內容的讀寫能力。以指針直接操作內存多發生在如下幾種情況:
(1)   某I/O芯片被定位在CPU的存儲空間而非I/O空間,而且寄存器對應於某特定地址;
(2)   兩個CPU之間以雙端口RAM通信,CPU需要在雙端口RAM的特定單元(稱爲mail box)書寫內容以在對方CPU產生中斷;
(3)   讀取在ROM或FLASH的特定單元所燒錄的漢字和英文字模。
譬如:
unsigned char *p = (unsigned char *)0xF000FF00;
*p=11;
以上程序的意義爲在絕對地址0xF0000+0xFF00(80186使用16位段地址和16位偏移地址)寫入11。
在使用絕對地址指針時,要注意指針自增自減操作的結果取決於指針指向的數據類別。上例中p++後的結果是p= 0xF000FF01,若p指向int,即:
int *p = (int *)0xF000FF00;
p++(或++p)的結果等同於:p = p+sizeof(int),而p—(或—p)的結果是p = p-sizeof(int)。
同理,若執行:
long int *p = (long int *)0xF000FF00;
則p++(或++p)的結果等同於:p = p+sizeof(long int) ,而p—(或—p)的結果是p = p-sizeof(long int)。
記住:CPU以字節爲單位編址,而C語言指針以指向的數據類型長度作自增和自減。理解這一點對於以指針直接操作內存是相當重要的。
2.函數指針
首先要理解以下三個問題:
(1)C語言中函數名直接對應於函數生成的指令代碼在內存中的地址,因此函數名可以直接賦給指向函數的指針;
(2)調用函數實際上等同於“調轉指令+參數傳遞處理+迴歸位置入棧”,本質上最核心的操作是將函數生成的目標代碼的首地址賦給CPU的PC寄存器;
(3)因爲函數調用的本質是跳轉到某一個地址單元的code去執行,所以可以“調用”一個根本就不存在的函數實體,暈?請往下看:
請拿出你可以獲得的任何一本大學《微型計算機原理》教材,書中講到,186 CPU啓動後跳轉至絕對地址0xFFFF0(對應C語言指針是0xF000FFF0,0xF000爲段地址,0xFFF0爲段內偏移)執行,請看下面的代碼:
typedef  void  (*lpFunction) ( );    /* 定義一個無參數、無返回類型的 */
/* 函數指針類型 */
lpFunction lpReset =(lpFunction)0xF000FFF0;    /* 定義一個函數指針,指向*/
/* CPU啓動後所執行第一條指令的位置 */
lpReset();                           /* 調用函數 */
在以上的程序中,我們根本沒有看到任何一個函數實體,但是我們卻執行了這樣的函數調用:lpReset(),它實際上起到了“軟重啓”的作用,跳轉到CPU啓動後第一條要執行的指令的位置。
記住:函數無它,唯指令集合耳;你可以調用一個沒有函數體的函數,本質上只是換一個地址開始執行指令!
3.數組vs.動態申請
在嵌入式系統中動態內存申請存在比一般系統編程時更嚴格的要求,這是因爲嵌入式系統的內存空間往往是十分有限的,不經意的內存泄露會很快導致系統的崩潰。
所以一定要保證你的malloc和free成對出現,如果你寫出這樣的一段程序:
char * function(void)
{
  char *p;
  p = (char *)malloc(…);
  if(p==NULL)
…;
  …                         /* 一系列針對p的操作 */
return p;
}
在某處調用function(),用完function中動態申請的內存後將其free,如下:
char *q = function();

free(q);
上述代碼明顯是不合理的,因爲違反了malloc和free成對出現的原則,即“誰申請,就由誰釋放”原則。不滿足這個原則,會導致代碼的耦合度增大,因爲用戶在調用function函數時需要知道其內部細節!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章