close
本文的討論主要圍繞以通用處理器為中心的協議處理模塊進行,因為它更多地牽涉到具體的C語言編程技巧
不同於一般形式的軟件編程,嵌入式系統編程建立在特定的硬件平臺上,勢必要求其編程語言具備較強的硬件直接操作能力。無疑,匯編語言具備這樣的特質。但是,歸因於匯編語言開發過程的復雜性,它並不是嵌入式系統開發的一般選擇。而與之相比,C語言--一種"高級的低級"語言,則成為嵌入式系統開發的最佳選擇。筆者在嵌入式系統項目的開發過程中,一次又一次感受到C語言的精妙,沈醉於C語言給嵌入式開發帶來的便利。

圖1給出了本文的討論所基於的硬件平臺,實際上,這也是大多數嵌入式系統的硬件平臺。它包括兩部分:

(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串口)或以太網口下載到目標機上運行,如圖2。其駐留於目標機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;
}
}
}

按上述方法設計的中斷服務程序很小,實際的工作都交由主程序執行了。
模塊劃分的"劃"是規劃的意思,意指怎樣合理的將一個很大的軟件劃分為一系列功能獨立的部分合作完成系統的需求
硬件驅動模塊

一個硬件驅動模塊通常應包括如下函數:

(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語言編程時程序整體框架結構分散、數據和函數脫節的問題。我們在後續章節會看到這樣的例子。

總結

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

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


C語言嵌入式系統編程註意事項之內存操作

在嵌入式系統的編程中,常常要求在特定的內存單元讀寫內容,匯編有對應的MOV指令,而除C/C++以外的其它編程語言基本沒有直接訪問絕對地址的能力
數據指針

在嵌入式系統的編程中,常常要求在特定的內存單元讀寫內容,匯編有對應的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語言指針以指向的數據類型長度作自增和自減。理解這一點對於以指針直接操作內存是相當重要的。

函數指針

首先要理解以下三個問題:

(1)C語言中函數名直接對應於函數生成的指令代碼在內存中的地址,因此函數名可以直接賦給指向函數的指針;

(2)調用函數實際上等同於"調轉指令+參數傳遞處理+回歸位置入棧",本質上最核心的操作是將函數生成的目標代碼的首地址賦給CPU的PC寄存器;

(3)因為函數調用的本質是跳轉到某一個地址單元的code去執行,所以可以"調用"一個根本就不存在的函數實體,暈?請往下看:

請拿出你可以獲得的任何一本大學《微型計算機原理》教材,書中講到,186 CPU啟動後跳轉至絕對地址0xFFFF0(對應C語言指針是0xF000FFF0,0xF000為段地址,0xFFF0為段內偏移)執行,請看下面的代碼:

typedef void (*lp) ( ); /* 定義一個無參數、無返回類型的 */
/* 函數指針類型 */
lp lpReset = (lp)0xF000FFF0; /* 定義一個函數指針,指向*/
/* CPU啟動後所執行第一條指令的位置 */
lpReset(); /* 調用函數 */

在以上的程序中,我們根本沒有看到任何一個函數實體,但是我們卻執行了這樣的函數調用:lpReset(),它實際上起到了"軟重啟"的作用,跳轉到CPU啟動後第一條要執行的指令的位置。

記住:函數無它,唯指令集合耳;你可以調用一個沒有函數體的函數,本質上只是換一個地址開始執行指令!

數組vs.動態申請

在嵌入式系統中動態內存申請存在比一般系統編程時更嚴格的要求,這是因為嵌入式系統的內存空間往往是十分有限的,不經意的內存泄露會很快導致系統的崩潰。

所以一定要保證你的malloc和free成對出現,如果你寫出這樣的一段程序:

char * (void)
{
char *p;
p = (char *)malloc(…);
if(p==NULL)
…;
… /* 一系列針對p的操作 */
return p;
}

在某處調用(),用完中動態申請的內存後將其free,如下:

char *q = ();
free(q);

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

正確的做法是在調用處申請內存,並傳入函數,如下:

char *p=malloc(…);
if(p==NULL)
…;
(p);
free(p);
p=NULL;

而函數則接收參數p,如下:

void (char *p)
{
… /* 一系列針對p的操作 */
}

基本上,動態申請內存方式可以用較大的數組替換。對於編程新手,筆者推薦你盡量采用數組!嵌入式系統可以以博大的胸襟接收瑕疵,而無法"海納"錯誤。畢竟,以最笨的方式苦練神功的郭靖勝過機智聰明卻範政治錯誤走反革命道路的楊康。

給出原則:

(1)盡可能的選用數組,數組不能越界訪問(真理越過一步就是謬誤,數組越過界限就光榮地成全了一個混亂的嵌入式系統);

(2)如果使用動態申請,則申請後一定要判斷是否申請成功了,並且malloc和free應成對出現!


在嵌入式系統的編程中,常常要求在特定的內存單元讀寫內容,匯編有對應的MOV指令,而除C/C++以外的其它編程語言基本沒有直接訪問絕對地址的能力
關鍵字const

const意味著"只讀"。區別如下代碼的功能非常重要,也是老生長嘆,如果你還不知道它們的區別,而且已經在程序界摸爬滾打多年,那只能說這是一個悲哀:

const int a;
int const a;
const int *a;
int * const a;
int const * a const;

(1) 關鍵字const的作用是為給讀你代碼的人傳達非常有用的信息。例如,在函數的形參前添加const關鍵字意味著這個參數在函數體內不會被修改,屬於"輸入參數"。在有多個形參的時候,函數的調用者可以憑借參數前是否有const關鍵字,清晰的辨別哪些是輸入參數,哪些是可能的輸出參數。

(2)合理地使用關鍵字const可以使編譯器很自然地保護那些不希望被改變的參數,防止其被無意的代碼修改,這樣可以減少bug的出現。

const在C++語言中則包含了更豐富的含義,而在C語言中僅意味著:"只能讀的普通變量",可以稱其為"不能改變的變量"(這個說法似乎很拗口,但卻最準確的表達了C語言中const的本質),在編譯階段需要的常數仍然只能以#define宏定義!故在C語言中如下程序是非法的:

const int SIZE = 10;
char a[SIZE]; /* 非法:編譯階段不能用到變量 */

關鍵字volatile

C語言編譯器會對用戶書寫的代碼進行優化,譬如如下代碼:

int a,b,c;
a = inWord(0x100); /*讀取I/O空間0x100端口的內容存入a變量*/
b = a;
a = inWord (0x100); /*再次讀取I/O空間0x100端口的內容存入a變量*/
c = a;

很可能被編譯器優化為:

int a,b,c;
a = inWord(0x100); /*讀取I/O空間0x100端口的內容存入a變量*/
b = a;
c = a;

但是這樣的優化結果可能導致錯誤,如果I/O空間0x100端口的內容在執行第一次讀操作後被其它程序寫入新值,則其實第2次讀操作讀出的內容與第一次不同,b和c的值應該不同。在變量a的定義前加上volatile關鍵字可以防止編譯器的類似優化,正確的做法是:

volatile int a;

volatile變量可能用於如下幾種情況:

(1) 並行設備的硬件寄存器(如:狀態寄存器,例中的代碼屬於此類);

(2) 一個中斷服務子程序中會訪問到的非自動變量(也就是全局變量);

(3) 多線程應用中被幾個任務共享的變量。

CPU字長與存儲器位寬不一致處理

在背景篇中提到,本文特意選擇了一個與CPU字長不一致的存儲芯片,就是為了進行本節的討論,解決CPU字長與存儲器位寬不一致的情況。80186的字長為16,而NVRAM的位寬為8,在這種情況下,我們需要為NVRAM提供讀寫字節、字的接口,如下:

typedef unsigned char BYTE;
typedef unsigned int WORD;
/* 函數功能:讀NVRAM中字節
* 參數:wOffset,讀取位置相對NVRAM基地址的偏移
* 返回:讀取到的字節值
*/
extern BYTE ReadByteNVRAM(WORD wOffset)
{
LPBYTE lpAddr = (BYTE*)(NVRAM + wOffset * 2); /* 為什麼偏移要×2? */

return *lpAddr;
}

/* 函數功能:讀NVRAM中字
* 參數:wOffset,讀取位置相對NVRAM基地址的偏移
* 返回:讀取到的字
*/
extern WORD ReadWordNVRAM(WORD wOffset)
{
WORD wTmp = 0;
LPBYTE lpAddr;
/* 讀取高位字節 */
lpAddr = (BYTE*)(NVRAM + wOffset * 2); /* 為什麼偏移要×2? */
wTmp += (*lpAddr)*256;
/* 讀取低位字節 */
lpAddr = (BYTE*)(NVRAM + (wOffset +1) * 2); /* 為什麼偏移要×2? */
wTmp += *lpAddr;
return wTmp;
}

/* 函數功能:向NVRAM中寫一個字節
*參數:wOffset,寫入位置相對NVRAM基地址的偏移
* byData,欲寫入的字節
*/
extern void WriteByteNVRAM(WORD wOffset, BYTE byData)
{
}

/* 函數功能:向NVRAM中寫一個字 */
*參數:wOffset,寫入位置相對NVRAM基地址的偏移
* wData,欲寫入的字
*/
extern void WriteWordNVRAM(WORD wOffset, WORD wData)
{
}

子貢問曰:Why偏移要乘以2?

子曰:請看圖1,16位80186與8位NVRAM之間互連只能以地址線A1對其A0,CPU本身的A0與NVRAM不連接。因此,NVRAM的地址只能是偶數地址,故每次以0x10為單位前進!
子貢再問:So why 80186的地址線A0不與NVRAM的A0連接?

子曰:請看《IT論語》之《微機原理篇》,那裏面講述了關於計算機組成的聖人之道。

總結

本篇主要講述了嵌入式系統C編程中內存操作的相關技巧。掌握並深入理解關於數據指針、函數指針、動態申請內存、const及volatile關鍵字等的相關知識,是一個優秀的C語言程序設計師的基本要求。當我們已經牢固掌握了上述技巧後,我們就已經學會了C語言的99%,因為C語言最精華的內涵皆在內存操作中體現。

我們之所以在嵌入式系統中使用C語言進行程序設計,99%是因為其強大的內存操作能力!

如果你愛編程,請你愛C語言;

如果你愛C語言,請你愛指針;

如果你愛指針,請你愛指針的指針!

C語言嵌入式系統編程註意事項之屏幕操作

現在要解決的問題是,嵌入式系統中經常要使用的並非是完整的漢字庫,往往只是需要提供數量有限的漢字供必要的顯示功能
漢字處理

現在要解決的問題是,嵌入式系統中經常要使用的並非是完整的漢字庫,往往只是需要提供數量有限的漢字供必要的顯示功能。例如,一個微波爐的LCD上沒有必要提供顯示"電子郵件"的功能;一個提供漢字顯示功能的空調的LCD上不需要顯示一條"短消息",諸如此類。但是一部手機、小靈通則通常需要包括較完整的漢字庫。

如果包括的漢字庫較完整,那麼,由內碼計算出漢字字模在庫中的偏移是十分簡單的:漢字庫是按照區位的順序排列的,前一個字節為該漢字的區號,後一個字節為該字的位號。每一個區記錄94個漢字,位號則為該字在該區中的位置。因此,漢字在漢字庫中的具體位置計算公式為:94*(區號-1)+位號-1。減1是因為數組是以0為開始而區號位號是以1為開始的。只需乘上一個漢字字模占用的字節數即可,即:(94*(區號-1)+位號-1)*一個漢字字模占用字節數,以16*16點陣字庫為例,計算公式則為:(94*(區號-1)+(位號-1))*32。漢字庫中從該位置起的32字節信息記錄了該字的字模信息。

對於包含較完整漢字庫的系統而言,我們可以以上述規則計算字模的位置。但是如果僅僅是提供少量漢字呢?譬如幾十至幾百個?最好的做法是:

定義宏:

# define EX_FONT_CHAR()
# define EX_FONT_UNICODE_VAL() (),
# define EX_FONT_ANSI_VAL() (),

定義結構體:

typedef struct _wide_unicode_font16x16
{
WORD ; /* 內碼 */
BYTE data[32]; /* 字模點陣 */
}Unicode;
#define CHINESE_CHAR_NUM … /* 漢字數量 */

字模的存儲用數組:

Unicode chinese[CHINESE_CHAR_NUM] =
{
{
EX_FONT_CHAR("業")
EX_FONT_UNICODE_VAL(0x4e1a)
{0x04, 0x40, 0x04, 0x40, 0x04, 0x40, 0x04, 0x44, 0x44, 0x46, 0x24, 0x4c, 0x24, 0x48, 0x14, 0x50, 0x1c, 0x50, 0x14, 0x60, 0x04, 0x40, 0x04, 0x40, 0x04, 0x44, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00}
},
{
EX_FONT_CHAR("中")
EX_FONT_UNICODE_VAL(0x4e2d)
{0x01, 0x00, 0x01, 0x00, 0x21, 0x08, 0x3f, 0xfc, 0x21, 0x08, 0x21, 0x08, 0x21, 0x08, 0x21, 0x08, 0x21, 0x08,
0x3f, 0xf8, 0x21, 0x08, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00}
},
{
EX_FONT_CHAR("雲")
EX_FONT_UNICODE_VAL(0x4e91)
{0x00, 0x00, 0x00, 0x30, 0x3f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0xff, 0xfe, 0x03, 0x00, 0x07, 0x00,

0x06, 0x40, 0x0c, 0x20, 0x18, 0x10, 0x31, 0xf8, 0x7f, 0x0c, 0x20, 0x08, 0x00, 0x00}
},
{
EX_FONT_CHAR("件")
EX_FONT_UNICODE_VAL(0x4ef6)
{0x10, 0x40, 0x1a, 0x40, 0x13, 0x40, 0x32, 0x40, 0x23, 0xfc, 0x64, 0x40, 0xa4, 0x40, 0x28, 0x40, 0x2f, 0xfe,

0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40}
}
}

要顯示特定漢字的時候,只需要從數組中查找內碼與要求漢字內碼相同的即可獲得字模。如果前面的漢字在數組中以內碼大小順序排列,那麼可以以二分查找法更高效的查找到漢字的字模。

這是一種很有效的組織小漢字庫的方法,它可以保證程序有很好的結構。

系統時間顯示

從NVRAM中可以讀取系統的時間,系統一般借助NVRAM產生的秒中斷每秒讀取一次當前時間並在LCD上顯示。關於時間的顯示,有一個效率問題。因為時間有其特殊性,那就是60秒才有一次分鐘的變化,60分鐘才有一次小時變化,如果我們每次都將讀取的時間在屏幕上完全重新刷新一次,則浪費了大量的系統時間。

一個較好的辦法是我們在時間顯示函數中以靜態變量分別存儲小時、分鐘、秒,只有在其內容發生變化的時候才更新其顯示。

extern void DisplayTime(…)
{
static BYTE byHour,byMinute,bySecond;
BYTE byNewHour, byNewMinute, byNewSecond;
byNewHour = GetSysHour();
byNewMinute = GetSysMinute();
byNewSecond = GetSysSecond();

if(byNewHour!= byHour)
{
… /* 顯示小時 */
byHour = byNewHour;
}
if(byNewMinute!= byMinute)
{
… /* 顯示分鐘 */
byMinute = byNewMinute;
}
if(byNewSecond!= bySecond)
{
… /* 顯示秒鐘 */
bySecond = byNewSecond;
}
}

這個例子也可以順便作為C語言中static關鍵字強大威力的證明。當然,在C++語言裏,static具有了更加強大的威力,它使得某些數據和函數脫離"對象"而成為"類"的一部分,正是它的這一特點,成就了軟件的無數優秀設計。


動畫顯示

動畫是無所謂有,無所謂無的,靜止的畫面走的路多了,也就成了動畫。隨著時間的變更,在屏幕上顯示不同的靜止畫面,即是動畫之本質。所以,在一個嵌入式系統的LCD上欲顯示動畫,必須借助定時器。沒有硬件或軟件定時器的世界是無法想像的:

(1) 沒有定時器,一個操作系統將無法進行時間片的輪轉,於是無法進行多任務的調度,於是便不再成其為一個多任務操作系統;

(2) 沒有定時器,一個多媒體播放軟件將無法運作,因為它不知道何時應該切換到下一幀畫面;

(3) 沒有定時器,一個網絡協議將無法運轉,因為其無法獲知何時包傳輸超時並重傳之,無法在特定的時間完成特定的任務。

因此,沒有定時器將意味著沒有操作系統、沒有網絡、沒有多媒體,這將是怎樣的黑暗?所以,合理並靈活地使用各種定時器,是對一個軟件人的最基本需求!

在80186為主芯片的嵌入式系統中,我們需要借助硬件定時器的中斷來作為軟件定時器,在中斷發生後變更畫面的顯示內容。在時間顯示"xx:xx"中讓冒號交替有無,每次秒中斷發生後,需調用ShowDot:

void ShowDot()
{
static BOOL bShowDot = TRUE; /* 再一次領略static關鍵字的威力 */
if(bShowDot)
{
showChar(’:’,xPos,yPos);
}
else
{
showChar(’ ’,xPos,yPos);
}
bShowDot = ! bShowDot;
}

菜單操作

無數人為之絞盡腦汁的問題終於出現了,在這一節裏,我們將看到,在C語言中哪怕用到一丁點的面向對象思想,軟件結構將會有何等的改觀!

要求以鍵盤上的"← →"鍵切換菜單焦點,當用戶在焦點處於某菜單時,若敲擊鍵盤上的OK、CANCEL鍵則調用該焦點菜單對應之處理函數。我曾經傻傻地這樣做著:

/* 按下OK鍵 */
void onOkKey()
{
/* 判斷在什麼焦點菜單上按下Ok鍵,調用相應處理函數 */
Switch(currentFocus)
{
case MENU1:
menu1OnOk();
break;
case MENU2:
menu2OnOk();
break;
}
}
/* 按下Cancel鍵 */
void onCancelKey()
{
/* 判斷在什麼焦點菜單上按下Cancel鍵,調用相應處理函數 */
Switch(currentFocus)
{
case MENU1:
menu1OnCancel();
break;
case MENU2:
menu2OnCancel();
break;
}
}

終於有一天,我這樣做了:

/* 將菜單的屬性和操作"封裝"在一起 */
typedef struct tagSysMenu
{
char *text; /* 菜單的文本 */
BYTE xPos; /* 菜單在LCD上的x坐標 */
BYTE yPos; /* 菜單在LCD上的y坐標 */
void (*onOkFun)(); /* 在該菜單上按下ok鍵的處理函數指針 */
void (*onCancelFun)(); /* 在該菜單上按下cancel鍵的處理函數指針 */
}SysMenu, *LPSysMenu;

當我定義菜單時,只需要這樣:

static SysMenu menu[MENU_NUM] =
{
{
"menu1", 0, 48, menu1OnOk, menu1OnCancel
}
,
{
" menu2", 7, 48, menu2OnOk, menu2OnCancel
}
,
{
" menu3", 7, 48, menu3OnOk, menu3OnCancel
}
,
{
" menu4", 7, 48, menu4OnOk, menu4OnCancel
}
};

OK鍵和CANCEL鍵的處理變成:

/* 按下OK鍵 */
void onOkKey()
{
menu[currentFocusMenu].onOkFun();
}
/* 按下Cancel鍵 */
void onCancelKey()
{
menu[currentFocusMenu].onCancelFun();
}

程序被大大簡化了,也開始具有很好的可擴展性!我們僅僅利用了面向對象中的封裝思想,就讓程序結構清晰,其結果是幾乎可以在無需修改程序的情況下在系統中添加更多的菜單,而系統的按鍵處理函數保持不變。

面向對象,真神了!

模擬MessageBox函數

MessageBox函數,這個Windows編程中的超級猛料,不知道是多少入門者第一次用到的函數。還記得我們第一次在Windows中利用MessageBox輸出 "Hello,World!"對話框時新奇的感覺嗎?無法統計,這個世界上究竟有多少程序員學習Windows編程是從MessageBox("Hello,World!",…)開始的。在我本科的學校,廣泛流傳著一個詞匯,叫做"’Hello,World’級程序員",意指入門級程序員,但似乎"’Hello,World’級"這個說法更搞笑而形象。

嵌入式系統中沒有給我們提供MessageBox,但是鑒於其功能強大,我們需要模擬之,一個模擬的MessageBox函數為:

/******************************************
/* 函數名稱: MessageBox
/* 功能說明: 彈出式對話框,顯示提醒用戶的信息
/* 參數說明: lpStr --- 提醒用戶的字符串輸出信息
/* TYPE --- 輸出格式(ID_OK = 0, ID_OKCANCEL = 1)
/* 返回值: 返回對話框接收的鍵值,只有兩種 KEY_OK, KEY_CANCEL
/******************************************
typedef enum TYPE { ID_OK,ID_OKCANCEL }MSG_TYPE;
extern BYTE MessageBox(LPBYTE lpStr, BYTE TYPE)
{
BYTE key = -1;

ClearScreen(); /* 清除屏幕 */
DisplayString(xPos,yPos,lpStr,TRUE); /* 顯示字符串 */
/* 根據對話框類型決定是否顯示確定、取消 */
switch (TYPE)
{
case ID_OK:
DisplayString(13,yPos+High+1, " 確定 ", 0);
break;
case ID_OKCANCEL:
DisplayString(8, yPos+High+1, " 確定 ", 0);
DisplayString(17,yPos+High+1, " 取消 ", 0);
break;
default:
break;
}
DrawRect(0, 0, 239, yPos+High+16+4); /* 繪制外框 */
/* MessageBox是模式對話框,阻塞運行,等待按鍵 */
while( (key != KEY_OK) || (key != KEY_CANCEL) )
{
key = getSysKey();
}
/* 返回按鍵類型 */
if(key== KEY_OK)
{
return ID_OK;
}
else
{
return ID_CANCEL;
}
}

上述函數與我們平素在VC++等中使用的MessageBox是何等的神似啊?實現這個函數,你會看到它在嵌入式系統中的妙用是無窮的。

總結

本篇是本系列文章中技巧性最深的一篇,它提供了嵌入式系統屏幕顯示方面一些很巧妙的處理方法,靈活使用它們,我們將不再被LCD上淩亂不堪的顯示內容所困擾。

屏幕乃嵌入式系統生存之重要輔助,面目可憎之顯示將另用戶逃之夭夭。屏幕編程若處理不好,將是軟件中最不系統、最混亂的部分,筆者曾深受其害。

C語言嵌入式系統編程註意事項之鍵盤操作

處理功能鍵
讓我們來看看WIN32編程中用到的"窗口"概念,當消息(message)被發送給不同窗口的時候,該窗口的消息處理函數(是一個callback函數)最終被調用,而在該窗口的消息處理函數中,又根據消息的類型調用了該窗口中的對應處理函數。通過這種方式,WIN32有效的組織了不同的窗口,並處理不同窗口情況下的消息。

我們從中學習到的就是:

(1)將不同的畫面類比為WIN32中不同的窗口,將窗口中的各種元素(菜單、按鈕等)包含在窗口之中;

(2)給各個畫面提供一個功能鍵"消息"處理函數,該函數接收按鍵信息為參數;

(3)在各畫面的功能鍵"消息"處理函數中,判斷按鍵類型和當前焦點元素,並調用對應元素的按鍵處理函數。

/* 將窗口元素、消息處理函數封裝在窗口中 */
struct windows
{
BYTE currentFocus;
ELEMENT element[ELEMENT_NUM];
void (*messageFun) (BYTE key);
};
/* 消息處理函數 */
void message(BYTE key)
{
BYTE i = 0;
/* 獲得焦點元素 */
while ( (element .ID!= currentFocus)&& (i < ELEMENT_NUM) )
{
i++;
}
/* "消息映射" */
if(i < ELEMENT_NUM)
{
switch(key)
{
case OK:
element.OnOk();
break;
}
}
}

在窗口的消息處理函數中調用相應元素按鍵函數的過程類似於"消息映射",這是我們從WIN32編程中學習到的。編程到了一個境界,很多東西都是相通的了。其它地方的思想可以拿過來為我所用,是為編程中的"拿來主義"。

在這個例子中,如果我們還想玩得更大一點,我們可以借鑒MFC中處理MESSAGE_MAP的方法,我們也可以學習MFC定義幾個精妙的宏來實現"消息映射"。
處理數字鍵

用戶輸入數字時是一位一位輸入的,每一位的輸入都對應著屏幕上的一個顯示位置(x坐標,y坐標)。此外,程序還需要記錄該位置輸入的值,所以有效組織用戶數字輸入的最佳方式是定義一個結構體,將坐標和數值捆綁在一起:

/* 用戶數字輸入結構體 */
typedef struct tagInputNum
{
BYTE byNum; /* 接收用戶輸入賦值 */
BYTE xPos; /* 數字輸入在屏幕上的顯示位置x坐標 */
BYTE yPos; /* 數字輸入在屏幕上的顯示位置y坐標 */
}InputNum, *LPInputNum;

那麼接收用戶輸入就可以定義一個結構體數組,用數組中的各位組成一個完整的數字:

InputNum inputElement[NUM_LENGTH]; /* 接收用戶數字輸入的數組 */
/* 數字按鍵處理函數 */
extern void onNumKey(BYTE num)
{
if(num==0|| num==1) /* 只接收二進制輸入 */
{
/* 在屏幕上顯示用戶輸入 */
DrawText(inputElement[currentElementInputPlace].xPos, inputElement[currentElementInputPlace].yPos, "%1d", num);
/* 將輸入賦值給數組元素 */
inputElement[currentElementInputPlace].byNum = num;
/* 焦點及光標右移 */
moveToRight();
}
}

將數字每一位輸入的坐標和輸入值捆綁後,在數字鍵處理函數中就可以較有結構的組織程序,使程序顯得很緊湊。

整理用戶輸入

繼續第2節的例子,在第2節的onNumKey函數中,只是獲取了數字的每一位,因而我們需要將其轉化為有效數據,譬如要轉化為有效的XXX數據,其方法是:

/* 從2進制數據位轉化為有效數據:XXX */
void convertToXXX()
{
BYTE i;
XXX = 0;
for (i = 0; i < NUM_LENGTH; i++)
{
XXX += inputElement.byNum*power(2, NUM_LENGTH - i - 1);
}
}

反之,我們也可能需要在屏幕上顯示那些有效的數據位,因為我們也需要能夠反向轉化:

/* 從有效數據轉化為2進制數據位:XXX */
void convertFromXXX()
{
BYTE i;
XXX = 0;
for (i = 0; i < NUM_LENGTH; i++)
{
inputElement.byNum = XXX / power(2, NUM_LENGTH - i - 1) % 2;
}
}

當然在上面的例子中,因為數據是2進制的,用power函數不是很好的選擇,直接用"<< >>"移位操作效率更高,我們僅是為了說明問題的方便。試想,如果用戶輸入是十進制的,power函數或許是唯一的選擇了。

總結

本篇給出了鍵盤操作所涉及的各個方面:功能鍵處理、數字鍵處理及用戶輸入整理,基本上提供了一個全套的按鍵處理方案。對於功能鍵處理方法,將LCD屏幕與Windows窗口進行類比,提出了較新穎地解決屏幕、鍵盤繁雜交互問題的方案。

計算機學的許多知識都具有相通性,因而,不斷追趕時髦技術而忽略基本功的做法是徒勞無意的。我們最多需要"精通"三種語言(精通,一個在如今的求職簡歷裏泛濫成災的詞語),最佳拍檔是匯編、C、C++(或JAVA),很顯然,如果你"精通"了這三種語言,其它語言你應該是可以很快"熟悉"的,否則你就沒有"精通"它們。


C語言嵌入式系統編程註意事項之性能優化

在C語言中,宏是產生內嵌代碼的唯一方法。對於嵌入式系統而言,為了能達到性能要求,宏是一種很好的代替函數的方法
使用宏定義

在C語言中,宏是產生內嵌代碼的唯一方法。對於嵌入式系統而言,為了能達到性能要求,宏是一種很好的代替函數的方法。

寫一個"標準"宏MIN ,這個宏輸入兩個參數並返回較小的一個:

錯誤做法:

#define MIN(A,B)  ( A <= B ? A : B )

正確做法:

#define MIN(A,B) ((A)<= (B) ? (A) : (B) )

對於宏,我們需要知道三點:

(1)宏定義"像"函數;

(2)宏定義不是函數,因而需要括上所有"參數";

(3)宏定義可能產生副作用。

下面的代碼:

least = MIN(*p++, b);

將被替換為:

( (*p++) <= (b) ?(*p++):(b) )

發生的事情無法預料。

因而不要給宏定義傳入有副作用的"參數"。

使用寄存器變量

當對一個變量頻繁被讀寫時,需要反復訪問內存,從而花費大量的存取時間。為此,C語言提供了一種變量,即寄存器變量。這種變量存放在CPU的寄存器中,使用時,不需要訪問內存,而直接從寄存器中讀寫,從而提高效率。寄存器變量的說明符是register。對於循環次數較多的循環控制變量及循環體內反復使用的變量均可定義為寄存器變量,而循環計數是應用寄存器變量的最好候選者。

(1) 只有局部自動變量和形參才可以定義為寄存器變量。因為寄存器變量屬於動態存儲方式,凡需要采用靜態存儲方式的量都不能定義為寄存器變量,包括:模塊間全局變量、模塊內全局變量、局部static變量;

(2) register是一個"建議"型關鍵字,意指程序建議該變量放在寄存器中,但最終該變量可能因為條件不滿足並未成為寄存器變量,而是被放在了存儲器中,但編譯器中並不報錯(在C++語言中有另一個"建議"型關鍵字:inline)。

下面是一個采用寄存器變量的例子:

/* 求1+2+3+….+n的值 */
WORD Addition(BYTE n)
{
register i,s=0;
for(i=1;i<=n;i++)
{
s=s+i;
}
return s;
}

本程序循環n次,i和s都被頻繁使用,因此可定義為寄存器變量。

內嵌匯編

程序中對時間要求苛刻的部分可以用內嵌匯編來重寫,以帶來速度上的顯著提高。但是,開發和測試匯編代碼是一件辛苦的工作,它將花費更長的時間,因而要慎重選擇要用匯編的部分。

在程序中,存在一個80-20原則,即20%的程序消耗了80%的運行時間,因而我們要改進效率,最主要是考慮改進那20%的代碼。

嵌入式C程序中主要使用在線匯編,即在C程序中直接插入_asm{ }內嵌匯編語句:

/* 把兩個輸入參數的值相加,結果存放到另外一個全局變量中 */
int result;
void Add(long a, long *b)
{
_asm
{
MOV AX, a
MOV BX, b
ADD AX, [BX]
MOV result, AX
}
}

利用硬件特性

首先要明白CPU對各種存儲器的訪問速度,基本上是:

CPU內部RAM > 外部同步RAM > 外部異步RAM > FLASH/ROM

對於程序代碼,已經被燒錄在FLASH或ROM中,我們可以讓CPU直接從其中讀取代碼執行,但通常這不是一個好辦法,我們最好在系統啟動後將FLASH或ROM中的目標代碼拷貝入RAM中後再執行以提高取指令速度;

對於UART等設備,其內部有一定容量的接收BUFFER,我們應盡量在BUFFER被占滿後再向CPU提出中斷。例如計算機終端在向目標機通過RS-232傳遞數據時,不宜設置UART只接收到一個BYTE就向CPU提中斷,從而無謂浪費中斷處理時間;

如果對某設備能采取DMA方式讀取,就采用DMA讀取,DMA讀取方式在讀取目標中包含的存儲信息較大時效率較高,其數據傳輸的基本單位是塊,而所傳輸的數據是從設備直接送入內存的(或者相反)。DMA方式較之中斷驅動方式,減少了CPU 對外設的幹預,進一步提高了CPU與外設的並行操作程度。

活用位操作

使用C語言的位操作可以減少除法和取模的運算。在計算機程序中數據的位是可以操作的最小數據單位,理論上可以用"位運算"來完成所有的運算和操作,因而,靈活的位操作可以有效地提高程序運行的效率。舉例如下:

/* 方法1 */
int i,j;
i = 879 / 16;
j = 562 % 32;
/* 方法2 */
int i,j;
i = 879 >> 4;
j = 562 - (562 >> 5 << 5);

對於以2的指數次方為"*"、"/"或"%"因子的數學運算,轉化為移位運算"<< >>"通常可以提高算法效率。因為乘除運算指令周期通常比移位運算大。

C語言位運算除了可以提高運算效率外,在嵌入式系統的編程中,它的另一個最典型的應用,而且十分廣泛地正在被使用著的是位間的與(&)、或(|)、非(~)操作,這跟嵌入式系統的編程特點有很大關系。我們通常要對硬件寄存器進行位設置,譬如,我們通過將AM186ER型80186處理器的中斷屏蔽控制寄存器的第低6位設置為0(開中斷2),最通用的做法是:

#define INT_I2_MASK 0x0040
wTemp = inword(INT_MASK);
outword(INT_MASK, wTemp &~INT_I2_MASK);

而將該位設置為1的做法是:

#define INT_I2_MASK 0x0040
wTemp = inword(INT_MASK);
outword(INT_MASK, wTemp | INT_I2_MASK);

判斷該位是否為1的做法是:

#define INT_I2_MASK 0x0040
wTemp = inword(INT_MASK);
if(wTemp & INT_I2_MASK)
{
… /* 該位為1 */
}

上述方法在嵌入式系統的編程中是非常常見的,我們需要牢固掌握。

總結

在性能優化方面永遠註意80-20準備,不要優化程序中開銷不大的那80%,這是勞而無功的。

宏定義是C語言中實現類似函數功能而又不具函數調用和返回開銷的較好方法,但宏在本質上不是函數,因而要防止宏展開後出現不可預料的結果,對宏的定義和使用要慎而處之。很遺憾,標準C至今沒有包括C++中inline函數的功能,inline函數兼具無調用開銷和安全的優點。

使用寄存器變量、內嵌匯編和活用位操作也是提高程序效率的有效方法。

除了編程上的技巧外,為提高系統的運行效率,我們通常也需要最大可能地利用各種硬件設備自身的特點來減小其運轉開銷,例如減小中斷次數、利用DMA傳輸方式等。

arrow
arrow
    全站熱搜

    主要步驟 發表在 痞客邦 留言(0) 人氣()