盡管C語(yǔ)言提供了許多低級(jí)處理的功能,但仍然保持著良好跨平臺(tái)的特性,以一個(gè)標(biāo)準(zhǔn)規(guī)格寫出的C語(yǔ)言程序可在許多電腦平臺(tái)上進(jìn)行編譯,甚至包含一些嵌入式處理器(單片機(jī)或稱MCU)以及超級(jí)電腦等作業(yè)平臺(tái)。
20世紀(jì)80年代,為了避免各開發(fā)廠商用的C語(yǔ)言語(yǔ)法產(chǎn)生差異,由美國(guó)國(guó)家標(biāo)準(zhǔn)局為C語(yǔ)言訂定了一套完整的國(guó)際標(biāo)準(zhǔn)語(yǔ)法,稱為ANSI C,作為C語(yǔ)言最初的標(biāo)準(zhǔn)。
C語(yǔ)言嵌入式系統(tǒng)編程注意事項(xiàng)
不同于一般形式的軟件編程,嵌入式系統(tǒng)編程建立在特定的硬件平臺(tái)上,勢(shì)必要求其編程語(yǔ)言具備較強(qiáng)的硬件直接操作能力。無(wú)疑,匯編語(yǔ)言具備這樣的特質(zhì)。但是,歸因于匯編語(yǔ)言開發(fā)過(guò)程的復(fù)雜性,它并不是嵌入式系統(tǒng)開發(fā)的一般選擇。而與之相比,C語(yǔ)言--一種“高級(jí)的低級(jí)”語(yǔ)言,則成為嵌入式系統(tǒng)開發(fā)的最佳選擇。筆者在嵌入式系統(tǒng)項(xiàng)目的開發(fā)過(guò)程中,一次又一次感受到C語(yǔ)言的精妙,沉醉于C語(yǔ)言給嵌入式開發(fā)帶來(lái)的便利。
大多數(shù)嵌入式系統(tǒng)的硬件平臺(tái)。它包括兩部分:
(1) 以通用處理器為中心的協(xié)議處理模塊,用于網(wǎng)絡(luò)控制協(xié)議的處理;
(2) 以數(shù)字信號(hào)處理器(DSP)為中心的信號(hào)處理模塊,用于調(diào)制、解調(diào)和數(shù)/模信號(hào)轉(zhuǎn)換。
本文的討論主要圍繞以通用處理器為中心的協(xié)議處理模塊進(jìn)行,因?yàn)樗嗟貭可娴骄唧w的C語(yǔ)言編程技巧。而DSP編程則重點(diǎn)關(guān)注具體的數(shù)字信號(hào)處理算法,主要涉及通信領(lǐng)域的知識(shí),不是本文的討論重點(diǎn)。
著眼于討論普遍的嵌入式系統(tǒng)C編程技巧,系統(tǒng)的協(xié)議處理模塊沒(méi)有選擇特別的CPU,而是選擇了眾所周知的CPU芯片--80186,每一位學(xué)習(xí)過(guò)《微機(jī)原理》的讀者都應(yīng)該對(duì)此芯片有一個(gè)基本的認(rèn)識(shí),且對(duì)其指令集比較熟悉。80186的字長(zhǎng)是16位,可以尋址到的內(nèi)存空間為1MB,只有實(shí)地址模式。C語(yǔ)言編譯生成的指針為32位(雙字),高16位為段地址,低16位為段內(nèi)編譯,一段最多64KB。
協(xié)議處理模塊中的FLASH和RAM幾乎是每個(gè)嵌入式系統(tǒng)的必備設(shè)備,前者用于存儲(chǔ)程序,后者則是程序運(yùn)行時(shí)指令及數(shù)據(jù)的存放位置。系統(tǒng)所選擇的FLASH和RAM的位寬都為16位,與CPU一致。
實(shí)時(shí)鐘芯片可以為系統(tǒng)定時(shí),給出當(dāng)前的年、月、日及具體時(shí)間(小時(shí)、分、秒及毫秒),可以設(shè)定其經(jīng)過(guò)一段時(shí)間即向CPU提出中斷或設(shè)定報(bào)警時(shí)間到來(lái)時(shí)向CPU提出中斷(類似鬧鐘功能)。
NVRAM(非易失去性RAM)具有掉電不丟失數(shù)據(jù)的特性,可以用于保存系統(tǒng)的設(shè)置信息,譬如網(wǎng)絡(luò)協(xié)議參數(shù)等。在系統(tǒng)掉電或重新啟動(dòng)后,仍然可以讀取先前的設(shè)置信息。其位寬為8位,比CPU字長(zhǎng)小。文章特意選擇一個(gè)與CPU字長(zhǎng)不一致的存儲(chǔ)芯片,為后文中一節(jié)的討論創(chuàng)造條件。
UART則完成CPU并行數(shù)據(jù)傳輸與RS-232串行數(shù)據(jù)傳輸?shù)霓D(zhuǎn)換,它可以在接收到[1~MAX_BUFFER]字節(jié)后向CPU提出中斷,MAX_BUFFER為UART芯片存儲(chǔ)接收到字節(jié)的最大緩沖區(qū)。
鍵盤控制器和顯示控制器則完成系統(tǒng)人機(jī)界面的控制。
以上提供的是一個(gè)較完備的嵌入式系統(tǒng)硬件架構(gòu),實(shí)際的系統(tǒng)可能包含更少的外設(shè)。之所以選擇一個(gè)完備的系統(tǒng),是為了后文更全面的討論嵌入式系統(tǒng)C語(yǔ)言編程技巧的方方面面,所有設(shè)備都會(huì)成為后文的分析目標(biāo)。
嵌入式系統(tǒng)需要良好的軟件開發(fā)環(huán)境的支持,由于嵌入式系統(tǒng)的目標(biāo)機(jī)資源受限,不可能在其上建立龐大、復(fù)雜的開發(fā)環(huán)境,因而其開發(fā)環(huán)境和目標(biāo)運(yùn)行環(huán)境相互分離。因此,嵌入式應(yīng)用軟件的開發(fā)方式一般是,在宿主機(jī)(Host)上建立開發(fā)環(huán)境,進(jìn)行應(yīng)用程序編碼和交叉編譯,然后宿主機(jī)同目標(biāo)機(jī)(Target)建立連接,將應(yīng)用程序下載到目標(biāo)機(jī)上進(jìn)行交叉調(diào)試,經(jīng)過(guò)調(diào)試和優(yōu)化,最后將應(yīng)用程序固化到目標(biāo)機(jī)中實(shí)際運(yùn)行。
CAD-UL是適用于x86處理器的嵌入式應(yīng)用軟件開發(fā)環(huán)境,它運(yùn)行在Windows操作系統(tǒng)之上,可生成x86處理器的目標(biāo)代碼并通過(guò)PC機(jī)的COM口(RS-232串口)或以太網(wǎng)口下載到目標(biāo)機(jī)上運(yùn)行。其駐留于目標(biāo)機(jī)FLASH存儲(chǔ)器中的monitor程序可以監(jiān)控宿主機(jī)Windows調(diào)試平臺(tái)上的用戶調(diào)試指令,獲取CPU寄存器的值及目標(biāo)機(jī)存儲(chǔ)空間、I/O空間的內(nèi)容。
后續(xù)章節(jié)將從軟件架構(gòu)、內(nèi)存操作、屏幕操作、鍵盤操作、性能優(yōu)化等多方面闡述C語(yǔ)言嵌入式系統(tǒng)的編程技巧。軟件架構(gòu)是一個(gè)宏觀概念,與具體硬件的聯(lián)系不大;內(nèi)存操作主要涉及系統(tǒng)中的FLASH、RAM和NVRAM芯片;屏幕操作則涉及顯示控制器和實(shí)時(shí)鐘;鍵盤操作主要涉及鍵盤控制器;性能優(yōu)化則給出一些具體的減小程序時(shí)間、空間消耗的技巧。
在我們的修煉旅途中將經(jīng)過(guò)25個(gè)關(guān)口,這些關(guān)口主分為兩類,一類是技巧型,有很強(qiáng)的適用性;一類則是常識(shí)型,在理論上有些意義。
So, let’s go.
C語(yǔ)言嵌入式系統(tǒng)編程注意事項(xiàng)之軟件架構(gòu)篇
模塊劃分的“劃”是規(guī)劃的意思,意指怎樣合理的將一個(gè)很大的軟件劃分為一系列功能獨(dú)立的部分合作完成系統(tǒng)的需求。
模塊劃分
模塊劃分的“劃”是規(guī)劃的意思,意指怎樣合理的將一個(gè)很大的軟件劃分為一系列功能獨(dú)立的部分合作完成系統(tǒng)的需求。C語(yǔ)言作為一種結(jié)構(gòu)化的程序設(shè)計(jì)語(yǔ)言,在模塊的劃分上主要依據(jù)功能(依功能進(jìn)行劃分在面向?qū)ο笤O(shè)計(jì)中成為一個(gè)錯(cuò)誤,牛頓定律遇到了相對(duì)論),C語(yǔ)言模塊化程序設(shè)計(jì)需理解如下概念:
(1) 模塊即是一個(gè).c文件和一個(gè).h文件的結(jié)合,頭文件(.h)中是對(duì)于該模塊接口的聲明;
(2) 某模塊提供給其它模塊調(diào)用的外部函數(shù)及數(shù)據(jù)需在.h中文件中冠以extern關(guān)鍵字聲明;
(3) 模塊內(nèi)的函數(shù)和全局變量需在.c文件開頭冠以staTIc關(guān)鍵字聲明;
(4) 永遠(yuǎn)不要在.h文件中定義變量!定義變量和聲明變量的區(qū)別在于定義會(huì)產(chǎn)生內(nèi)存分配的操作,是匯編階段的概念;而聲明則只是告訴包含該聲明的模塊在連接階段從其它模塊尋找外部函數(shù)和變量。如:
/*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文件 */
以上程序的結(jié)果是在模塊1、2、3中都定義了整型變量a,a在不同的模塊中對(duì)應(yīng)不同的地址單元,這個(gè)世界上從來(lái)不需要這樣的程序。正確的做法是:
/*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的話,對(duì)應(yīng)的是同一片內(nèi)存單元。
一個(gè)嵌入式系統(tǒng)通常包括兩類模塊:
(1)硬件驅(qū)動(dòng)模塊,一種特定硬件對(duì)應(yīng)一個(gè)模塊;
(2)軟件功能模塊,其模塊的劃分應(yīng)滿足低偶合、高內(nèi)聚的要求。
多任務(wù)還是單任務(wù)
所謂“單任務(wù)系統(tǒng)”是指該系統(tǒng)不能支持多任務(wù)并發(fā)操作,宏觀串行地執(zhí)行一個(gè)任務(wù)。而多任務(wù)系統(tǒng)則可以宏觀并行(微觀上可能串行)地“同時(shí)”執(zhí)行多個(gè)任務(wù)。
多任務(wù)的并發(fā)執(zhí)行通常依賴于一個(gè)多任務(wù)操作系統(tǒng)(OS),多任務(wù)OS的核心是系統(tǒng)調(diào)度器,它使用任務(wù)控制塊(TCB)來(lái)管理任務(wù)調(diào)度功能。TCB包括任務(wù)的當(dāng)前狀態(tài)、優(yōu)先級(jí)、要等待的事件或資源、任務(wù)程序碼的起始地址、初始堆棧指針等信息。調(diào)度器在任務(wù)被激活時(shí),要用到這些信息。此外,TCB還被用來(lái)存放任務(wù)的“上下文”(context)。任務(wù)的上下文就是當(dāng)一個(gè)執(zhí)行中的任務(wù)被停止時(shí),所要保存的所有信息。通常,上下文就是計(jì)算機(jī)當(dāng)前的狀態(tài),也即各個(gè)寄存器的內(nèi)容。當(dāng)發(fā)生任務(wù)切換時(shí),當(dāng)前運(yùn)行的任務(wù)的上下文被存入TCB,并將要被執(zhí)行的任務(wù)的上下文從它的TCB中取出,放入各個(gè)寄存器中。
嵌入式多任務(wù)OS的典型例子有Vxworks、ucLinux等。嵌入式OS并非遙不可及的神壇之物,我們可以用不到1000行代碼實(shí)現(xiàn)一個(gè)針對(duì)80186處理器的功能最簡(jiǎn)單的OS內(nèi)核,作者正準(zhǔn)備進(jìn)行此項(xiàng)工作,希望能將心得貢獻(xiàn)給大家。
究竟選擇多任務(wù)還是單任務(wù)方式,依賴于軟件的體系是否龐大。例如,絕大多數(shù)手機(jī)程序都是多任務(wù)的,但也有一些小靈通的協(xié)議棧是單任務(wù)的,沒(méi)有操作系統(tǒng),它們的主程序輪流調(diào)用各個(gè)軟件模塊的處理程序,模擬多任務(wù)環(huán)境。
單任務(wù)程序典型架構(gòu)
(1)從CPU復(fù)位時(shí)的指定地址開始執(zhí)行;
(2)跳轉(zhuǎn)至匯編代碼startup處執(zhí)行;
(3)跳轉(zhuǎn)至用戶主程序main執(zhí)行,在main中完成:
a.初試化各硬件設(shè)備;
b.初始化各軟件模塊;
c.進(jìn)入死循環(huán)(無(wú)限循環(huán)),調(diào)用各模塊的處理函數(shù)
用戶主程序和各模塊的處理函數(shù)都以C語(yǔ)言完成。用戶主程序最后都進(jìn)入了一個(gè)死循環(huán),其首選方案是:
while(1)
{
}
有的程序員這樣寫:
for(;;)
{
}
這個(gè)語(yǔ)法沒(méi)有確切表達(dá)代碼的含義,我們從for(;;)看不出什么,只有弄明白for(;;)在C語(yǔ)言中意味著無(wú)條件循環(huán)才明白其意。
下面是幾個(gè)“著名”的死循環(huán):
(1)操作系統(tǒng)是死循環(huán);
(2)WIN32程序是死循環(huán);
(3)嵌入式系統(tǒng)軟件是死循環(huán);
(4)多線程程序的線程處理函數(shù)是死循環(huán)。
你可能會(huì)辯駁,大聲說(shuō):“凡事都不是絕對(duì)的,2、3、4都可以不是死循環(huán)”。Yes,you are right,但是你得不到鮮花和掌聲。實(shí)際上,這是一個(gè)沒(méi)有太大意義的牛角尖,因?yàn)檫@個(gè)世界從來(lái)不需要一個(gè)處理完幾個(gè)消息就喊著要OS殺死它的WIN32程序,不需要一個(gè)剛開始RUN就自行了斷的嵌入式系統(tǒng),不需要莫名其妙啟動(dòng)一個(gè)做一點(diǎn)事就干掉自己的線程。有時(shí)候,過(guò)于嚴(yán)謹(jǐn)制造的不是便利而是麻煩。君不見,五層的TCP/IP協(xié)議棧超越嚴(yán)謹(jǐn)?shù)腎SO/OSI七層協(xié)議棧大行其道成為事實(shí)上的標(biāo)準(zhǔn)?
經(jīng)常有網(wǎng)友討論:
printf(“%d,%d”,++i,i++); /* 輸出是什么?*/
c = a+++b; /* c=? */
等類似問(wèn)題。面對(duì)這些問(wèn)題,我們只能發(fā)出由衷的感慨:世界上還有很多有意義的事情等著我們?nèi)ハ瘮z入的食物。
實(shí)際上,嵌入式系統(tǒng)要運(yùn)行到世界末日。
中斷服務(wù)程序
中斷是嵌入式系統(tǒng)中重要的組成部分,但是在標(biāo)準(zhǔn)C中不包含中斷。許多編譯開發(fā)商在標(biāo)準(zhǔn)C上增加了對(duì)中斷的支持,提供新的關(guān)鍵字用于標(biāo)示中斷服務(wù)程序(ISR),類似于__interrupt、#program interrupt等。當(dāng)一個(gè)函數(shù)被定義為ISR的時(shí)候,編譯器會(huì)自動(dòng)為該函數(shù)增加中斷服務(wù)程序所需要的中斷現(xiàn)場(chǎng)入棧和出棧代碼。
中斷服務(wù)程序需要滿足如下要求:
(1)不能返回值;
(2)不能向ISR傳遞參數(shù);
(3) ISR應(yīng)該盡可能的短小精悍;
(4) printf(char * lpFormatString,…)函數(shù)會(huì)帶來(lái)重入和性能問(wèn)題,不能在ISR中采用。
在某項(xiàng)目的開發(fā)中,我們?cè)O(shè)計(jì)了一個(gè)隊(duì)列,在中斷服務(wù)程序中,只是將中斷類型添加入該隊(duì)列中,在主程序的死循環(huán)中不斷掃描中斷隊(duì)列是否有中斷,有則取出隊(duì)列中的第一個(gè)中斷類型,進(jìn)行相應(yīng)處理。
/* 存放中斷的隊(duì)列 */
typedef struct tagIntQueue
{
int intType; /* 中斷類型 */
struct tagIntQueue *next;
}IntQueue;
IntQueue lpIntQueueHead;
__interrupt ISRexample ()
{
int intType;
intType = GetSystemType();
QueueAddTail(lpIntQueueHead, intType);/* 在隊(duì)列尾加入新的中斷 */
}
在主程序循環(huán)中判斷是否有中斷:
While(1)
{
If( !IsIntQueueEmpty() )
{
intType = GetFirsTInt();
switch(intType) /* 是不是很象WIN32程序的消息解析函數(shù)? */
{
/* 對(duì),我們的中斷類型解析很類似于消息驅(qū)動(dòng) */
case xxx: /* 我們稱其為“中斷驅(qū)動(dòng)”吧? */
…
break;
case xxx:
…
break;
…
}
}
}
按上述方法設(shè)計(jì)的中斷服務(wù)程序很小,實(shí)際的工作都交由主程序執(zhí)行了。
模塊劃分的“劃”是規(guī)劃的意思,意指怎樣合理的將一個(gè)很大的軟件劃分為一系列功能獨(dú)立的部分合作完成系統(tǒng)的需求
硬件驅(qū)動(dòng)模塊
一個(gè)硬件驅(qū)動(dòng)模塊通常應(yīng)包括如下函數(shù):
(1)中斷服務(wù)程序ISR
(2)硬件初始化
a.修改寄存器,設(shè)置硬件參數(shù)(如UART應(yīng)設(shè)置其波特率,AD/DA設(shè)備應(yīng)設(shè)置其采樣速率等);
b.將中斷服務(wù)程序入口地址寫入中斷向量表:
/* 設(shè)置中斷向量表 */
m_myPtr = make_far_pointer(0l); /* 返回void far型指針void far * */
m_myPtr += ITYPE_UART; /* ITYPE_UART: uart中斷服務(wù)程序 */
/* 相對(duì)于中斷向量表首地址的偏移 */
*m_myPtr = &UART _Isr; /* UART _Isr:UART的中斷服務(wù)程序 */
(3)設(shè)置CPU針對(duì)該硬件的控制線
a.如果控制線可作PIO(可編程I/O)和控制信號(hào)用,則設(shè)置CPU內(nèi)部對(duì)應(yīng)寄存器使其作為控制信號(hào);
b.設(shè)置CPU內(nèi)部的針對(duì)該設(shè)備的中斷屏蔽位,設(shè)置中斷方式(電平觸發(fā)還是邊緣觸發(fā))。
(4)提供一系列針對(duì)該設(shè)備的操作接口函數(shù)。例如,對(duì)于LCD,其驅(qū)動(dòng)模塊應(yīng)提供繪制像素、畫線、繪制矩陣、顯示字符點(diǎn)陣等函數(shù);而對(duì)于實(shí)時(shí)鐘,其驅(qū)動(dòng)模塊則需提供獲取時(shí)間、設(shè)置時(shí)間等函數(shù)。
C的面向?qū)ο蠡?/p>
在面向?qū)ο蟮恼Z(yǔ)言里面,出現(xiàn)了類的概念。類是對(duì)特定數(shù)據(jù)的特定操作的集合體。類包含了兩個(gè)范疇:數(shù)據(jù)和操作。而C語(yǔ)言中的struct僅僅是數(shù)據(jù)的集合,我們可以利用函數(shù)指針將struct模擬為一個(gè)包含數(shù)據(jù)和操作的“類”。下面的C程序模擬了一個(gè)最簡(jiǎn)單的“類”:
#ifndef C_Class
#define C_Class struct
#endif
C_Class A
{
C_Class A *A_this; /* this指針 */
void (*Foo)(C_Class A *A_this); /* 行為:函數(shù)指針 */
int a; /* 數(shù)據(jù) */
int b;
};
我們可以利用C語(yǔ)言模擬出面向?qū)ο蟮娜齻€(gè)特性:封裝、繼承和多態(tài),但是更多的時(shí)候,我們只是需要將數(shù)據(jù)與行為封裝以解決軟件結(jié)構(gòu)混亂的問(wèn)題。C模擬面向?qū)ο笏枷氲哪康牟辉谟谀M行為本身,而在于解決某些情況下使用C語(yǔ)言編程時(shí)程序整體框架結(jié)構(gòu)分散、數(shù)據(jù)和函數(shù)脫節(jié)的問(wèn)題。我們?cè)诤罄m(xù)章節(jié)會(huì)看到這樣的例子。
總結(jié)
本篇介紹了嵌入式系統(tǒng)編程軟件架構(gòu)方面的知識(shí),主要包括模塊劃分、多任務(wù)還是單任務(wù)選取、單任務(wù)程序典型架構(gòu)、中斷服務(wù)程序、硬件驅(qū)動(dòng)模塊設(shè)計(jì)等,從宏觀上給出了一個(gè)嵌入式系統(tǒng)軟件所包含的主要元素。
請(qǐng)記?。很浖Y(jié)構(gòu)是軟件的靈魂!結(jié)構(gòu)混亂的程序面目可憎,調(diào)試、測(cè)試、維護(hù)、升級(jí)都極度困難。
C語(yǔ)言嵌入式系統(tǒng)編程注意事項(xiàng)之內(nèi)存操作
在嵌入式系統(tǒng)的編程中,常常要求在特定的內(nèi)存單元讀寫內(nèi)容,匯編有對(duì)應(yīng)的MOV指令,而除C/C++以外的其它編程語(yǔ)言基本沒(méi)有直接訪問(wèn)絕對(duì)地址的能力
數(shù)據(jù)指針
在嵌入式系統(tǒng)的編程中,常常要求在特定的內(nèi)存單元讀寫內(nèi)容,匯編有對(duì)應(yīng)的MOV指令,而除C/C++以外的其它編程語(yǔ)言基本沒(méi)有直接訪問(wèn)絕對(duì)地址的能力。在嵌入式系統(tǒng)的實(shí)際調(diào)試中,多借助C語(yǔ)言指針?biāo)哂械膶?duì)絕對(duì)地址單元內(nèi)容的讀寫能力。以指針直接操作內(nèi)存多發(fā)生在如下幾種情況:
(1) 某I/O芯片被定位在CPU的存儲(chǔ)空間而非I/O空間,而且寄存器對(duì)應(yīng)于某特定地址;
(2) 兩個(gè)CPU之間以雙端口RAM通信,CPU需要在雙端口RAM的特定單元(稱為mail box)書寫內(nèi)容以在對(duì)方CPU產(chǎn)生中斷;
(3) 讀取在ROM或FLASH的特定單元所燒錄的漢字和英文字模。
譬如:
unsigned char *p = (unsigned char *)0xF000FF00;
*p=11;
以上程序的意義為在絕對(duì)地址0xF0000+0xFF00(80186使用16位段地址和16位偏移地址)寫入11。
在使用絕對(duì)地址指針時(shí),要注意指針自增自減操作的結(jié)果取決于指針指向的數(shù)據(jù)類別。上例中p++后的結(jié)果是p= 0xF000FF01,若p指向int,即:
int *p = (int *)0xF000FF00;
p++(或++p)的結(jié)果等同于:p = p+sizeof(int),而p-(或-p)的結(jié)果是p = p-sizeof(int)。
同理,若執(zhí)行:
long int *p = (long int *)0xF000FF00;
則p++(或++p)的結(jié)果等同于:p = p+sizeof(long int) ,而p-(或-p)的結(jié)果是p = p-sizeof(long int)。
記?。篊PU以字節(jié)為單位編址,而C語(yǔ)言指針以指向的數(shù)據(jù)類型長(zhǎng)度作自增和自減。理解這一點(diǎn)對(duì)于以指針直接操作內(nèi)存是相當(dāng)重要的。
函數(shù)指針
首先要理解以下三個(gè)問(wèn)題:
(1)C語(yǔ)言中函數(shù)名直接對(duì)應(yīng)于函數(shù)生成的指令代碼在內(nèi)存中的地址,因此函數(shù)名可以直接賦給指向函數(shù)的指針;
(2)調(diào)用函數(shù)實(shí)際上等同于“調(diào)轉(zhuǎn)指令+參數(shù)傳遞處理+回歸位置入?!保举|(zhì)上最核心的操作是將函數(shù)生成的目標(biāo)代碼的首地址賦給CPU的PC寄存器;
(3)因?yàn)楹瘮?shù)調(diào)用的本質(zhì)是跳轉(zhuǎn)到某一個(gè)地址單元的code去執(zhí)行,所以可以“調(diào)用”一個(gè)根本就不存在的函數(shù)實(shí)體,暈?請(qǐng)往下看:
請(qǐng)拿出你可以獲得的任何一本大學(xué)《微型計(jì)算機(jī)原理》教材,書中講到,186 CPU啟動(dòng)后跳轉(zhuǎn)至絕對(duì)地址0xFFFF0(對(duì)應(yīng)C語(yǔ)言指針是0xF000FFF0,0xF000為段地址,0xFFF0為段內(nèi)偏移)執(zhí)行,請(qǐng)看下面的代碼:
typedef void (*lp) ( ); /* 定義一個(gè)無(wú)參數(shù)、無(wú)返回類型的 */
/* 函數(shù)指針類型 */
lp lpReset = (lp)0xF000FFF0; /* 定義一個(gè)函數(shù)指針,指向*/
/* CPU啟動(dòng)后所執(zhí)行第一條指令的位置 */
lpReset(); /* 調(diào)用函數(shù) */
在以上的程序中,我們根本沒(méi)有看到任何一個(gè)函數(shù)實(shí)體,但是我們卻執(zhí)行了這樣的函數(shù)調(diào)用:lpReset(),它實(shí)際上起到了“軟重啟”的作用,跳轉(zhuǎn)到CPU啟動(dòng)后第一條要執(zhí)行的指令的位置。
記住:函數(shù)無(wú)它,唯指令集合耳;你可以調(diào)用一個(gè)沒(méi)有函數(shù)體的函數(shù),本質(zhì)上只是換一個(gè)地址開始執(zhí)行指令!
數(shù)組vs動(dòng)態(tài)申請(qǐng)
在嵌入式系統(tǒng)中動(dòng)態(tài)內(nèi)存申請(qǐng)存在比一般系統(tǒng)編程時(shí)更嚴(yán)格的要求,這是因?yàn)榍度胧较到y(tǒng)的內(nèi)存空間往往是十分有限的,不經(jīng)意的內(nèi)存泄露會(huì)很快導(dǎo)致系統(tǒng)的崩潰。
所以一定要保證你的malloc和free成對(duì)出現(xiàn),如果你寫出這樣的一段程序:
char * (void)
{
char *p;
p = (char *)malloc(…);
if(p==NULL)
…;
… /* 一系列針對(duì)p的操作 */
return p;
}
在某處調(diào)用(),用完中動(dòng)態(tài)申請(qǐng)的內(nèi)存后將其free,如下:
char *q = ();
…
free(q);
上述代碼明顯是不合理的,因?yàn)檫`反了malloc和free成對(duì)出現(xiàn)的原則,即“誰(shuí)申請(qǐng),就由誰(shuí)釋放”原則。不滿足這個(gè)原則,會(huì)導(dǎo)致代碼的耦合度增大,因?yàn)橛脩粼谡{(diào)用函數(shù)時(shí)需要知道其內(nèi)部細(xì)節(jié)!
正確的做法是在調(diào)用處申請(qǐng)內(nèi)存,并傳入函數(shù),如下:
char *p=malloc(…);
if(p==NULL)
…;
(p);
…
free(p);
p=NULL;
而函數(shù)則接收參數(shù)p,如下:
void (char *p)
{
… /* 一系列針對(duì)p的操作 */
}
基本上,動(dòng)態(tài)申請(qǐng)內(nèi)存方式可以用較大的數(shù)組替換。對(duì)于編程新手,筆者推薦你盡量采用數(shù)組!嵌入式系統(tǒng)可以以博大的胸襟接收瑕疵,而無(wú)法“海納”錯(cuò)誤。畢竟,以最笨的方式苦練神功的郭靖勝過(guò)機(jī)智聰明卻范政治錯(cuò)誤走反革命道路的楊康。
給出原則:
(1)盡可能的選用數(shù)組,數(shù)組不能越界訪問(wèn)(真理越過(guò)一步就是謬誤,數(shù)組越過(guò)界限就光榮地成全了一個(gè)混亂的嵌入式系統(tǒng));
(2)如果使用動(dòng)態(tài)申請(qǐng),則申請(qǐng)后一定要判斷是否申請(qǐng)成功了,并且malloc和free應(yīng)成對(duì)出現(xiàn)!
在嵌入式系統(tǒng)的編程中,常常要求在特定的內(nèi)存單元讀寫內(nèi)容,匯編有對(duì)應(yīng)的MOV指令,而除C/C++以外的其它編程語(yǔ)言基本沒(méi)有直接訪問(wèn)絕對(duì)地址的能力
關(guān)鍵字const
const意味著“只讀”。區(qū)別如下代碼的功能非常重要,也是老生長(zhǎng)嘆,如果你還不知道它們的區(qū)別,而且已經(jīng)在程序界摸爬滾打多年,那只能說(shuō)這是一個(gè)悲哀:
const int a;
int const a;
const int *a;
int * const a;
int const * a const;
(1) 關(guān)鍵字const的作用是為給讀你代碼的人傳達(dá)非常有用的信息。例如,在函數(shù)的形參前添加const關(guān)鍵字意味著這個(gè)參數(shù)在函數(shù)體內(nèi)不會(huì)被修改,屬于“輸入?yún)?shù)”。在有多個(gè)形參的時(shí)候,函數(shù)的調(diào)用者可以憑借參數(shù)前是否有const關(guān)鍵字,清晰的辨別哪些是輸入?yún)?shù),哪些是可能的輸出參數(shù)。
(2)合理地使用關(guān)鍵字const可以使編譯器很自然地保護(hù)那些不希望被改變的參數(shù),防止其被無(wú)意的代碼修改,這樣可以減少bug的出現(xiàn)。
const在C++語(yǔ)言中則包含了更豐富的含義,而在C語(yǔ)言中僅意味著:“只能讀的普通變量”,可以稱其為“不能改變的變量”(這個(gè)說(shuō)法似乎很拗口,但卻最準(zhǔn)確的表達(dá)了C語(yǔ)言中const的本質(zhì)),在編譯階段需要的常數(shù)仍然只能以#define宏定義!故在C語(yǔ)言中如下程序是非法的:
const int SIZE = 10;
char a[SIZE]; /* 非法:編譯階段不能用到變量 */
關(guān)鍵字volaTIle
C語(yǔ)言編譯器會(huì)對(duì)用戶書寫的代碼進(jìn)行優(yōu)化,譬如如下代碼:
int a,b,c;
a = inWord(0x100); /*讀取I/O空間0x100端口的內(nèi)容存入a變量*/
b = a;
a = inWord (0x100); /*再次讀取I/O空間0x100端口的內(nèi)容存入a變量*/
c = a;
很可能被編譯器優(yōu)化為:
int a,b,c;
a = inWord(0x100); /*讀取I/O空間0x100端口的內(nèi)容存入a變量*/
b = a;
c = a;
但是這樣的優(yōu)化結(jié)果可能導(dǎo)致錯(cuò)誤,如果I/O空間0x100端口的內(nèi)容在執(zhí)行第一次讀操作后被其它程序?qū)懭胄轮?,則其實(shí)第2次讀操作讀出的內(nèi)容與第一次不同,b和c的值應(yīng)該不同。在變量a的定義前加上volaTIle關(guān)鍵字可以防止編譯器的類似優(yōu)化,正確的做法是:
volatile int a;
volatile變量可能用于如下幾種情況:
(1) 并行設(shè)備的硬件寄存器(如:狀態(tài)寄存器,例中的代碼屬于此類);
(2) 一個(gè)中斷服務(wù)子程序中會(huì)訪問(wèn)到的非自動(dòng)變量(也就是全局變量);
(3) 多線程應(yīng)用中被幾個(gè)任務(wù)共享的變量。
CPU字長(zhǎng)與存儲(chǔ)器位寬不一致處理
在背景篇中提到,本文特意選擇了一個(gè)與CPU字長(zhǎng)不一致的存儲(chǔ)芯片,就是為了進(jìn)行本節(jié)的討論,解決CPU字長(zhǎng)與存儲(chǔ)器位寬不一致的情況。80186的字長(zhǎng)為16,而NVRAM的位寬為8,在這種情況下,我們需要為NVRAM提供讀寫字節(jié)、字的接口,如下:
typedef unsigned char BYTE;
typedef unsigned int WORD;
/* 函數(shù)功能:讀NVRAM中字節(jié)
* 參數(shù):wOffset,讀取位置相對(duì)NVRAM基地址的偏移
* 返回:讀取到的字節(jié)值
*/
extern BYTE ReadByteNVRAM(WORD wOffset)
{
LPBYTE lpAddr = (BYTE*)(NVRAM + wOffset * 2); /* 為什么偏移要×2? */
return *lpAddr;
}
/* 函數(shù)功能:讀NVRAM中字
* 參數(shù):wOffset,讀取位置相對(duì)NVRAM基地址的偏移
* 返回:讀取到的字
*/
extern WORD ReadWordNVRAM(WORD wOffset)
{
WORD wTmp = 0;
LPBYTE lpAddr;
/* 讀取高位字節(jié) */
lpAddr = (BYTE*)(NVRAM + wOffset * 2); /* 為什么偏移要×2? */
wTmp += (*lpAddr)*256;
/* 讀取低位字節(jié) */
lpAddr = (BYTE*)(NVRAM + (wOffset +1) * 2); /* 為什么偏移要×2? */
wTmp += *lpAddr;
return wTmp;
}
/* 函數(shù)功能:向NVRAM中寫一個(gè)字節(jié)
*參數(shù):wOffset,寫入位置相對(duì)NVRAM基地址的偏移
* byData,欲寫入的字節(jié)
*/
extern void WriteByteNVRAM(WORD wOffset, BYTE byData)
{
…
}
/* 函數(shù)功能:向NVRAM中寫一個(gè)字 */
*參數(shù):wOffset,寫入位置相對(duì)NVRAM基地址的偏移
* wData,欲寫入的字
*/
extern void WriteWordNVRAM(WORD wOffset, WORD wData)
{
…
}
子貢問(wèn)曰:Why偏移要乘以2?
子曰:16位80186與8位NVRAM之間互連只能以地址線A1對(duì)其A0,CPU本身的A0與NVRAM不連接。因此,NVRAM的地址只能是偶數(shù)地址,故每次以0x10為單位前進(jìn)!
子貢再問(wèn):So why 80186的地址線A0不與NVRAM的A0連接?
子曰:請(qǐng)看《IT論語(yǔ)》之《微機(jī)原理篇》,那里面講述了關(guān)于計(jì)算機(jī)組成的圣人之道。
總結(jié)
本篇主要講述了嵌入式系統(tǒng)C編程中內(nèi)存操作的相關(guān)技巧。掌握并深入理解關(guān)于數(shù)據(jù)指針、函數(shù)指針、動(dòng)態(tài)申請(qǐng)內(nèi)存、const及volatile關(guān)鍵字等的相關(guān)知識(shí),是一個(gè)優(yōu)秀的C語(yǔ)言程序設(shè)計(jì)師的基本要求。當(dāng)我們已經(jīng)牢固掌握了上述技巧后,我們就已經(jīng)學(xué)會(huì)了C語(yǔ)言的99%,因?yàn)镃語(yǔ)言最精華的內(nèi)涵皆在內(nèi)存操作中體現(xiàn)。
我們之所以在嵌入式系統(tǒng)中使用C語(yǔ)言進(jìn)行程序設(shè)計(jì),99%是因?yàn)槠鋸?qiáng)大的內(nèi)存操作能力!
如果你愛編程,請(qǐng)你愛C語(yǔ)言;
如果你愛C語(yǔ)言,請(qǐng)你愛指針;
如果你愛指針,請(qǐng)你愛指針的指針!
C語(yǔ)言嵌入式系統(tǒng)編程注意事項(xiàng)之屏幕操作
現(xiàn)在要解決的問(wèn)題是,嵌入式系統(tǒng)中經(jīng)常要使用的并非是完整的漢字庫(kù),往往只是需要提供數(shù)量有限的漢字供必要的顯示功能
漢字處理
現(xiàn)在要解決的問(wèn)題是,嵌入式系統(tǒng)中經(jīng)常要使用的并非是完整的漢字庫(kù),往往只是需要提供數(shù)量有限的漢字供必要的顯示功能。例如,一個(gè)微波爐的LCD上沒(méi)有必要提供顯示“電子郵件”的功能;一個(gè)提供漢字顯示功能的空調(diào)的LCD上不需要顯示一條“短消息”,諸如此類。但是一部手機(jī)、小靈通則通常需要包括較完整的漢字庫(kù)。
如果包括的漢字庫(kù)較完整,那么,由內(nèi)碼計(jì)算出漢字字模在庫(kù)中的偏移是十分簡(jiǎn)單的:漢字庫(kù)是按照區(qū)位的順序排列的,前一個(gè)字節(jié)為該漢字的區(qū)號(hào),后一個(gè)字節(jié)為該字的位號(hào)。每一個(gè)區(qū)記錄94個(gè)漢字,位號(hào)則為該字在該區(qū)中的位置。因此,漢字在漢字庫(kù)中的具體位置計(jì)算公式為:94*(區(qū)號(hào)-1)+位號(hào)-1。減1是因?yàn)閿?shù)組是以0為開始而區(qū)號(hào)位號(hào)是以1為開始的。只需乘上一個(gè)漢字字模占用的字節(jié)數(shù)即可,即:(94*(區(qū)號(hào)-1)+位號(hào)-1)*一個(gè)漢字字模占用字節(jié)數(shù),以16*16點(diǎn)陣字庫(kù)為例,計(jì)算公式則為:(94*(區(qū)號(hào)-1)+(位號(hào)-1))*32。漢字庫(kù)中從該位置起的32字節(jié)信息記錄了該字的字模信息。
對(duì)于包含較完整漢字庫(kù)的系統(tǒng)而言,我們可以以上述規(guī)則計(jì)算字模的位置。但是如果僅僅是提供少量漢字呢?譬如幾十至幾百個(gè)?最好的做法是:
定義宏:
# define EX_FONT_CHAR()
# define EX_FONT_UNICODE_VAL() (),
# define EX_FONT_ANSI_VAL() (),
定義結(jié)構(gòu)體:
typedef struct _wide_unicode_font16x16
{
WORD ; /* 內(nèi)碼 */
BYTE data[32]; /* 字模點(diǎn)陣 */
}Unicode;
#define CHINESE_CHAR_NUM … /* 漢字?jǐn)?shù)量 */
字模的存儲(chǔ)用數(shù)組:
Unicode chinese[CHINESE_CHAR_NUM] =
{
{
EX_FONT_CHAR(“業(yè)”)
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}
}
}
要顯示特定漢字的時(shí)候,只需要從數(shù)組中查找內(nèi)碼與要求漢字內(nèi)碼相同的即可獲得字模。如果前面的漢字在數(shù)組中以內(nèi)碼大小順序排列,那么可以以二分查找法更高效的查找到漢字的字模。
這是一種很有效的組織小漢字庫(kù)的方法,它可以保證程序有很好的結(jié)構(gòu)。
系統(tǒng)時(shí)間顯示
從NVRAM中可以讀取系統(tǒng)的時(shí)間,系統(tǒng)一般借助NVRAM產(chǎn)生的秒中斷每秒讀取一次當(dāng)前時(shí)間并在LCD上顯示。關(guān)于時(shí)間的顯示,有一個(gè)效率問(wèn)題。因?yàn)闀r(shí)間有其特殊性,那就是60秒才有一次分鐘的變化,60分鐘才有一次小時(shí)變化,如果我們每次都將讀取的時(shí)間在屏幕上完全重新刷新一次,則浪費(fèi)了大量的系統(tǒng)時(shí)間。
一個(gè)較好的辦法是我們?cè)跁r(shí)間顯示函數(shù)中以靜態(tài)變量分別存儲(chǔ)小時(shí)、分鐘、秒,只有在其內(nèi)容發(fā)生變化的時(shí)候才更新其顯示。
extern void DisplayTime(…)
{
static BYTE byHour,byMinute,bySecond;
BYTE byNewHour, byNewMinute, byNewSecond;
byNewHour = GetSysHour();
byNewMinute = GetSysMinute();
byNewSecond = GetSysSecond();
if(byNewHour!= byHour)
{
… /* 顯示小時(shí) */
byHour = byNewHour;
}
if(byNewMinute!= byMinute)
{
… /* 顯示分鐘 */
byMinute = byNewMinute;
}
if(byNewSecond!= bySecond)
{
… /* 顯示秒鐘 */
bySecond = byNewSecond;
}
}
這個(gè)例子也可以順便作為C語(yǔ)言中static關(guān)鍵字強(qiáng)大威力的證明。當(dāng)然,在C++語(yǔ)言里,static具有了更加強(qiáng)大的威力,它使得某些數(shù)據(jù)和函數(shù)脫離“對(duì)象”而成為“類”的一部分,正是它的這一特點(diǎn),成就了軟件的無(wú)數(shù)優(yōu)秀設(shè)計(jì)。
動(dòng)畫顯示
動(dòng)畫是無(wú)所謂有,無(wú)所謂無(wú)的,靜止的畫面走的路多了,也就成了動(dòng)畫。隨著時(shí)間的變更,在屏幕上顯示不同的靜止畫面,即是動(dòng)畫之本質(zhì)。所以,在一個(gè)嵌入式系統(tǒng)的LCD上欲顯示動(dòng)畫,必須借助定時(shí)器。沒(méi)有硬件或軟件定時(shí)器的世界是無(wú)法想像的:
(1) 沒(méi)有定時(shí)器,一個(gè)操作系統(tǒng)將無(wú)法進(jìn)行時(shí)間片的輪轉(zhuǎn),于是無(wú)法進(jìn)行多任務(wù)的調(diào)度,于是便不再成其為一個(gè)多任務(wù)操作系統(tǒng);
(2) 沒(méi)有定時(shí)器,一個(gè)多媒體播放軟件將無(wú)法運(yùn)作,因?yàn)樗恢篮螘r(shí)應(yīng)該切換到下一幀畫面;
(3) 沒(méi)有定時(shí)器,一個(gè)網(wǎng)絡(luò)協(xié)議將無(wú)法運(yùn)轉(zhuǎn),因?yàn)槠錈o(wú)法獲知何時(shí)包傳輸超時(shí)并重傳之,無(wú)法在特定的時(shí)間完成特定的任務(wù)。
因此,沒(méi)有定時(shí)器將意味著沒(méi)有操作系統(tǒng)、沒(méi)有網(wǎng)絡(luò)、沒(méi)有多媒體,這將是怎樣的黑暗?所以,合理并靈活地使用各種定時(shí)器,是對(duì)一個(gè)軟件人的最基本需求!
在80186為主芯片的嵌入式系統(tǒng)中,我們需要借助硬件定時(shí)器的中斷來(lái)作為軟件定時(shí)器,在中斷發(fā)生后變更畫面的顯示內(nèi)容。在時(shí)間顯示“xx:xx”中讓冒號(hào)交替有無(wú),每次秒中斷發(fā)生后,需調(diào)用ShowDot:
void ShowDot()
{
static BOOL bShowDot = TRUE; /* 再一次領(lǐng)略static關(guān)鍵字的威力 */
if(bShowDot)
{
showChar(’:’,xPos,yPos);
}
else
{
showChar(’ ’,xPos,yPos);
}
bShowDot = ! bShowDot;
}
菜單操作
無(wú)數(shù)人為之絞盡腦汁的問(wèn)題終于出現(xiàn)了,在這一節(jié)里,我們將看到,在C語(yǔ)言中哪怕用到一丁點(diǎn)的面向?qū)ο笏枷?,軟件結(jié)構(gòu)將會(huì)有何等的改觀!
要求以鍵盤上的“← →”鍵切換菜單焦點(diǎn),當(dāng)用戶在焦點(diǎn)處于某菜單時(shí),若敲擊鍵盤上的OK、CANCEL鍵則調(diào)用該焦點(diǎn)菜單對(duì)應(yīng)之處理函數(shù)。我曾經(jīng)傻傻地這樣做著:
/* 按下OK鍵 */
void onOkKey()
{
/* 判斷在什么焦點(diǎn)菜單上按下Ok鍵,調(diào)用相應(yīng)處理函數(shù) */
Switch(currentFocus)
{
case MENU1:
menu1OnOk();
break;
case MENU2:
menu2OnOk();
break;
…
}
}
/* 按下Cancel鍵 */
void onCancelKey()
{
/* 判斷在什么焦點(diǎn)菜單上按下Cancel鍵,調(diào)用相應(yīng)處理函數(shù) */
Switch(currentFocus)
{
case MENU1:
menu1OnCancel();
break;
case MENU2:
menu2OnCancel();
break;
…
}
}
終于有一天,我這樣做了:
/* 將菜單的屬性和操作“封裝”在一起 */
typedef struct tagSysMenu
{
char *text; /* 菜單的文本 */
BYTE xPos; /* 菜單在LCD上的x坐標(biāo) */
BYTE yPos; /* 菜單在LCD上的y坐標(biāo) */
void (*onOkFun)(); /* 在該菜單上按下ok鍵的處理函數(shù)指針 */
void (*onCancelFun)(); /* 在該菜單上按下cancel鍵的處理函數(shù)指針 */
}SysMenu, *LPSysMenu;
當(dāng)我定義菜單時(shí),只需要這樣:
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();
}
程序被大大簡(jiǎn)化了,也開始具有很好的可擴(kuò)展性!我們僅僅利用了面向?qū)ο笾械姆庋b思想,就讓程序結(jié)構(gòu)清晰,其結(jié)果是幾乎可以在無(wú)需修改程序的情況下在系統(tǒng)中添加更多的菜單,而系統(tǒng)的按鍵處理函數(shù)保持不變。
面向?qū)ο?,真神了?/p>
模擬MessageBox函數(shù)
MessageBox函數(shù),這個(gè)Windows編程中的超級(jí)猛料,不知道是多少入門者第一次用到的函數(shù)。還記得我們第一次在Windows中利用MessageBox輸出 “Hello,World!”對(duì)話框時(shí)新奇的感覺(jué)嗎?無(wú)法統(tǒng)計(jì),這個(gè)世界上究竟有多少程序員學(xué)習(xí)Windows編程是從MessageBox(“Hello,World!”,…)開始的。在我本科的學(xué)校,廣泛流傳著一個(gè)詞匯,叫做“’Hello,World’級(jí)程序員”,意指入門級(jí)程序員,但似乎“’Hello,World’級(jí)”這個(gè)說(shuō)法更搞笑而形象。
嵌入式系統(tǒng)中沒(méi)有給我們提供MessageBox,但是鑒于其功能強(qiáng)大,我們需要模擬之,一個(gè)模擬的MessageBox函數(shù)為:
/******************************************
/* 函數(shù)名稱: MessageBox
/* 功能說(shuō)明: 彈出式對(duì)話框,顯示提醒用戶的信息
/* 參數(shù)說(shuō)明: lpStr --- 提醒用戶的字符串輸出信息
/* TYPE --- 輸出格式(ID_OK = 0, ID_OKCANCEL = 1)
/* 返回值: 返回對(duì)話框接收的鍵值,只有兩種 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); /* 顯示字符串 */
/* 根據(jù)對(duì)話框類型決定是否顯示確定、取消 */
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是模式對(duì)話框,阻塞運(yùn)行,等待按鍵 */
while( (key != KEY_OK) || (key != KEY_CANCEL) )
{
key = getSysKey();
}
/* 返回按鍵類型 */
if(key== KEY_OK)
{
return ID_OK;
}
else
{
return ID_CANCEL;
}
}
上述函數(shù)與我們平素在VC++等中使用的MessageBox是何等的神似???實(shí)現(xiàn)這個(gè)函數(shù),你會(huì)看到它在嵌入式系統(tǒng)中的妙用是無(wú)窮的。
總結(jié)
本篇是本系列文章中技巧性最深的一篇,它提供了嵌入式系統(tǒng)屏幕顯示方面一些很巧妙的處理方法,靈活使用它們,我們將不再被LCD上凌亂不堪的顯示內(nèi)容所困擾。
屏幕乃嵌入式系統(tǒng)生存之重要輔助,面目可憎之顯示將另用戶逃之夭夭。屏幕編程若處理不好,將是軟件中最不系統(tǒng)、最混亂的部分,筆者曾深受其害。
C語(yǔ)言嵌入式系統(tǒng)編程注意事項(xiàng)之鍵盤操作
處理功能鍵
讓我們來(lái)看看WIN32編程中用到的“窗口”概念,當(dāng)消息(message)被發(fā)送給不同窗口的時(shí)候,該窗口的消息處理函數(shù)(是一個(gè)callback函數(shù))最終被調(diào)用,而在該窗口的消息處理函數(shù)中,又根據(jù)消息的類型調(diào)用了該窗口中的對(duì)應(yīng)處理函數(shù)。通過(guò)這種方式,WIN32有效的組織了不同的窗口,并處理不同窗口情況下的消息。
我們從中學(xué)習(xí)到的就是:
(1)將不同的畫面類比為WIN32中不同的窗口,將窗口中的各種元素(菜單、按鈕等)包含在窗口之中;
(2)給各個(gè)畫面提供一個(gè)功能鍵“消息”處理函數(shù),該函數(shù)接收按鍵信息為參數(shù);
(3)在各畫面的功能鍵“消息”處理函數(shù)中,判斷按鍵類型和當(dāng)前焦點(diǎn)元素,并調(diào)用對(duì)應(yīng)元素的按鍵處理函數(shù)。
/* 將窗口元素、消息處理函數(shù)封裝在窗口中 */
struct windows
{
BYTE currentFocus;
ELEMENT element[ELEMENT_NUM];
void (*messageFun) (BYTE key);
…
};
/* 消息處理函數(shù) */
void message(BYTE key)
{
BYTE i = 0;
/* 獲得焦點(diǎn)元素 */
while ( (element .ID!= currentFocus)&& (i 《 ELEMENT_NUM) )
{
i++;
}
/* “消息映射” */
if(i 《 ELEMENT_NUM)
{
switch(key)
{
case OK:
element.OnOk();
break;
…
}
}
}
在窗口的消息處理函數(shù)中調(diào)用相應(yīng)元素按鍵函數(shù)的過(guò)程類似于“消息映射”,這是我們從WIN32編程中學(xué)習(xí)到的。編程到了一個(gè)境界,很多東西都是相通的了。其它地方的思想可以拿過(guò)來(lái)為我所用,是為編程中的“拿來(lái)主義”。
在這個(gè)例子中,如果我們還想玩得更大一點(diǎn),我們可以借鑒MFC中處理MESSAGE_MAP的方法,我們也可以學(xué)習(xí)MFC定義幾個(gè)精妙的宏來(lái)實(shí)現(xiàn)“消息映射”。
處理數(shù)字鍵
用戶輸入數(shù)字時(shí)是一位一位輸入的,每一位的輸入都對(duì)應(yīng)著屏幕上的一個(gè)顯示位置(x坐標(biāo),y坐標(biāo))。此外,程序還需要記錄該位置輸入的值,所以有效組織用戶數(shù)字輸入的最佳方式是定義一個(gè)結(jié)構(gòu)體,將坐標(biāo)和數(shù)值捆綁在一起:
/* 用戶數(shù)字輸入結(jié)構(gòu)體 */
typedef struct tagInputNum
{
BYTE byNum; /* 接收用戶輸入賦值 */
BYTE xPos; /* 數(shù)字輸入在屏幕上的顯示位置x坐標(biāo) */
BYTE yPos; /* 數(shù)字輸入在屏幕上的顯示位置y坐標(biāo) */
}InputNum, *LPInputNum;
那么接收用戶輸入就可以定義一個(gè)結(jié)構(gòu)體數(shù)組,用數(shù)組中的各位組成一個(gè)完整的數(shù)字:
InputNum inputElement[NUM_LENGTH]; /* 接收用戶數(shù)字輸入的數(shù)組 */
/* 數(shù)字按鍵處理函數(shù) */
extern void onNumKey(BYTE num)
{
if(num==0|| num==1) /* 只接收二進(jìn)制輸入 */
{
/* 在屏幕上顯示用戶輸入 */
DrawText(inputElement[currentElementInputPlace].xPos, inputElement[currentElementInputPlace].yPos, “%1d”, num);
/* 將輸入賦值給數(shù)組元素 */
inputElement[currentElementInputPlace].byNum = num;
/* 焦點(diǎn)及光標(biāo)右移 */
moveToRight();
}
}
將數(shù)字每一位輸入的坐標(biāo)和輸入值捆綁后,在數(shù)字鍵處理函數(shù)中就可以較有結(jié)構(gòu)的組織程序,使程序顯得很緊湊。
整理用戶輸入
繼續(xù)第2節(jié)的例子,在第2節(jié)的onNumKey函數(shù)中,只是獲取了數(shù)字的每一位,因而我們需要將其轉(zhuǎn)化為有效數(shù)據(jù),譬如要轉(zhuǎn)化為有效的XXX數(shù)據(jù),其方法是:
/* 從2進(jìn)制數(shù)據(jù)位轉(zhuǎn)化為有效數(shù)據(jù):XXX */
void convertToXXX()
{
BYTE i;
XXX = 0;
for (i = 0; i 《 NUM_LENGTH; i++)
{
XXX += inputElement.byNum*power(2, NUM_LENGTH - i - 1);
}
}
反之,我們也可能需要在屏幕上顯示那些有效的數(shù)據(jù)位,因?yàn)槲覀円残枰軌蚍聪蜣D(zhuǎn)化:
/* 從有效數(shù)據(jù)轉(zhuǎn)化為2進(jìn)制數(shù)據(jù)位:XXX */
void convertFromXXX()
{
BYTE i;
XXX = 0;
for (i = 0; i 《 NUM_LENGTH; i++)
{
inputElement.byNum = XXX / power(2, NUM_LENGTH - i - 1) % 2;
}
}
當(dāng)然在上面的例子中,因?yàn)閿?shù)據(jù)是2進(jìn)制的,用power函數(shù)不是很好的選擇,直接用“《《 》》”移位操作效率更高,我們僅是為了說(shuō)明問(wèn)題的方便。試想,如果用戶輸入是十進(jìn)制的,power函數(shù)或許是唯一的選擇了。
總結(jié)
本篇給出了鍵盤操作所涉及的各個(gè)方面:功能鍵處理、數(shù)字鍵處理及用戶輸入整理,基本上提供了一個(gè)全套的按鍵處理方案。對(duì)于功能鍵處理方法,將LCD屏幕與Windows窗口進(jìn)行類比,提出了較新穎地解決屏幕、鍵盤繁雜交互問(wèn)題的方案。
計(jì)算機(jī)學(xué)的許多知識(shí)都具有相通性,因而,不斷追趕時(shí)髦技術(shù)而忽略基本功的做法是徒勞無(wú)意的。我們最多需要“精通”三種語(yǔ)言(精通,一個(gè)在如今的求職簡(jiǎn)歷里泛濫成災(zāi)的詞語(yǔ)),最佳拍檔是匯編、C、C++(或JAVA),很顯然,如果你“精通”了這三種語(yǔ)言,其它語(yǔ)言你應(yīng)該是可以很快“熟悉”的,否則你就沒(méi)有“精通”它們。
C語(yǔ)言嵌入式系統(tǒng)編程注意事項(xiàng)之性能優(yōu)化
在C語(yǔ)言中,宏是產(chǎn)生內(nèi)嵌代碼的唯一方法。對(duì)于嵌入式系統(tǒng)而言,為了能達(dá)到性能要求,宏是一種很好的代替函數(shù)的方法
使用宏定義
在C語(yǔ)言中,宏是產(chǎn)生內(nèi)嵌代碼的唯一方法。對(duì)于嵌入式系統(tǒng)而言,為了能達(dá)到性能要求,宏是一種很好的代替函數(shù)的方法。
寫一個(gè)“標(biāo)準(zhǔn)”宏MIN ,這個(gè)宏輸入兩個(gè)參數(shù)并返回較小的一個(gè):
錯(cuò)誤做法:
#define MIN(A,B) ( A 《= B ? A : B )
正確做法:
#define MIN(A,B) ((A)《= (B) ? (A) : (B) )
對(duì)于宏,我們需要知道三點(diǎn):
(1)宏定義“像”函數(shù);
(2)宏定義不是函數(shù),因而需要括上所有“參數(shù)”;
(3)宏定義可能產(chǎn)生副作用。
下面的代碼:
least = MIN(*p++, b);
將被替換為:
( (*p++) 《= (b) ?(*p++):(b) )
發(fā)生的事情無(wú)法預(yù)料。
因而不要給宏定義傳入有副作用的“參數(shù)”。
使用寄存器變量
當(dāng)對(duì)一個(gè)變量頻繁被讀寫時(shí),需要反復(fù)訪問(wèn)內(nèi)存,從而花費(fèi)大量的存取時(shí)間。為此,C語(yǔ)言提供了一種變量,即寄存器變量。這種變量存放在CPU的寄存器中,使用時(shí),不需要訪問(wèn)內(nèi)存,而直接從寄存器中讀寫,從而提高效率。寄存器變量的說(shuō)明符是register。對(duì)于循環(huán)次數(shù)較多的循環(huán)控制變量及循環(huán)體內(nèi)反復(fù)使用的變量均可定義為寄存器變量,而循環(huán)計(jì)數(shù)是應(yīng)用寄存器變量的最好候選者。
(1) 只有局部自動(dòng)變量和形參才可以定義為寄存器變量。因?yàn)榧拇嫫髯兞繉儆趧?dòng)態(tài)存儲(chǔ)方式,凡需要采用靜態(tài)存儲(chǔ)方式的量都不能定義為寄存器變量,包括:模塊間全局變量、模塊內(nèi)全局變量、局部static變量;
(2) register是一個(gè)“建議”型關(guān)鍵字,意指程序建議該變量放在寄存器中,但最終該變量可能因?yàn)闂l件不滿足并未成為寄存器變量,而是被放在了存儲(chǔ)器中,但編譯器中并不報(bào)錯(cuò)(在C++語(yǔ)言中有另一個(gè)“建議”型關(guān)鍵字:inline)。
下面是一個(gè)采用寄存器變量的例子:
/* 求1+2+3+….+n的值 */
WORD Addition(BYTE n)
{
register i,s=0;
for(i=1;i《=n;i++)
{
s=s+i;
}
return s;
}
本程序循環(huán)n次,i和s都被頻繁使用,因此可定義為寄存器變量。
內(nèi)嵌匯編
程序中對(duì)時(shí)間要求苛刻的部分可以用內(nèi)嵌匯編來(lái)重寫,以帶來(lái)速度上的顯著提高。但是,開發(fā)和測(cè)試匯編代碼是一件辛苦的工作,它將花費(fèi)更長(zhǎng)的時(shí)間,因而要慎重選擇要用匯編的部分。
在程序中,存在一個(gè)80-20原則,即20%的程序消耗了80%的運(yùn)行時(shí)間,因而我們要改進(jìn)效率,最主要是考慮改進(jìn)那20%的代碼。
嵌入式C程序中主要使用在線匯編,即在C程序中直接插入_asm{ }內(nèi)嵌匯編語(yǔ)句:
/* 把兩個(gè)輸入?yún)?shù)的值相加,結(jié)果存放到另外一個(gè)全局變量中 */
int result;
void Add(long a, long *b)
{
_asm
{
MOV AX, a
MOV BX, b
ADD AX, [BX]
MOV result, AX
}
}
利用硬件特性
首先要明白CPU對(duì)各種存儲(chǔ)器的訪問(wèn)速度,基本上是:
CPU內(nèi)部RAM》外部同步RAM》外部異步RAM》FLASH/ROM
對(duì)于程序代碼,已經(jīng)被燒錄在FLASH或ROM中,我們可以讓CPU直接從其中讀取代碼執(zhí)行,但通常這不是一個(gè)好辦法,我們最好在系統(tǒng)啟動(dòng)后將FLASH或ROM中的目標(biāo)代碼拷貝入RAM中后再執(zhí)行以提高取指令速度;
對(duì)于UART等設(shè)備,其內(nèi)部有一定容量的接收BUFFER,我們應(yīng)盡量在BUFFER被占滿后再向CPU提出中斷。例如計(jì)算機(jī)終端在向目標(biāo)機(jī)通過(guò)RS-232傳遞數(shù)據(jù)時(shí),不宜設(shè)置UART只接收到一個(gè)BYTE就向CPU提中斷,從而無(wú)謂浪費(fèi)中斷處理時(shí)間;
如果對(duì)某設(shè)備能采取DMA方式讀取,就采用DMA讀取,DMA讀取方式在讀取目標(biāo)中包含的存儲(chǔ)信息較大時(shí)效率較高,其數(shù)據(jù)傳輸?shù)幕締挝皇菈K,而所傳輸?shù)臄?shù)據(jù)是從設(shè)備直接送入內(nèi)存的(或者相反)。DMA方式較之中斷驅(qū)動(dòng)方式,減少了CPU 對(duì)外設(shè)的干預(yù),進(jìn)一步提高了CPU與外設(shè)的并行操作程度。
活用位操作
使用C語(yǔ)言的位操作可以減少除法和取模的運(yùn)算。在計(jì)算機(jī)程序中數(shù)據(jù)的位是可以操作的最小數(shù)據(jù)單位,理論上可以用“位運(yùn)算”來(lái)完成所有的運(yùn)算和操作,因而,靈活的位操作可以有效地提高程序運(yùn)行的效率。舉例如下:
/* 方法1 */
int i,j;
i = 879 / 16;
j = 562 % 32;
/* 方法2 */
int i,j;
i = 879 》》 4;
j = 562 - (562 》》 5 《《 5);
對(duì)于以2的指數(shù)次方為“*”、“/”或“%”因子的數(shù)學(xué)運(yùn)算,轉(zhuǎn)化為移位運(yùn)算“《《 》》”通??梢蕴岣咚惴ㄐ?。因?yàn)槌顺\(yùn)算指令周期通常比移位運(yùn)算大。
C語(yǔ)言位運(yùn)算除了可以提高運(yùn)算效率外,在嵌入式系統(tǒng)的編程中,它的另一個(gè)最典型的應(yīng)用,而且十分廣泛地正在被使用著的是位間的與(&)、或(|)、非(~)操作,這跟嵌入式系統(tǒng)的編程特點(diǎn)有很大關(guān)系。我們通常要對(duì)硬件寄存器進(jìn)行位設(shè)置,譬如,我們通過(guò)將AM186ER型80186處理器的中斷屏蔽控制寄存器的第低6位設(shè)置為0(開中斷2),最通用的做法是:
#define INT_I2_MASK 0x0040
wTemp = inword(INT_MASK);
outword(INT_MASK, wTemp &~INT_I2_MASK);
而將該位設(shè)置為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 */
}
上述方法在嵌入式系統(tǒng)的編程中是非常常見的,我們需要牢固掌握。
總結(jié)
在性能優(yōu)化方面永遠(yuǎn)注意80-20準(zhǔn)備,不要優(yōu)化程序中開銷不大的那80%,這是勞而無(wú)功的。
宏定義是C語(yǔ)言中實(shí)現(xiàn)類似函數(shù)功能而又不具函數(shù)調(diào)用和返回開銷的較好方法,但宏在本質(zhì)上不是函數(shù),因而要防止宏展開后出現(xiàn)不可預(yù)料的結(jié)果,對(duì)宏的定義和使用要慎而處之。很遺憾,標(biāo)準(zhǔn)C至今沒(méi)有包括C++中inline函數(shù)的功能,inline函數(shù)兼具無(wú)調(diào)用開銷和安全的優(yōu)點(diǎn)。
使用寄存器變量、內(nèi)嵌匯編和活用位操作也是提高程序效率的有效方法。
除了編程上的技巧外,為提高系統(tǒng)的運(yùn)行效率,我們通常也需要最大可能地利用各種硬件設(shè)備自身的特點(diǎn)來(lái)減小其運(yùn)轉(zhuǎn)開銷,例如減小中斷次數(shù)、利用DMA傳輸方式等。
-
嵌入式系統(tǒng)
+關(guān)注
關(guān)注
41文章
3551瀏覽量
129104 -
C語(yǔ)言
+關(guān)注
關(guān)注
180文章
7594瀏覽量
135861
原文標(biāo)題:C語(yǔ)言在嵌入式系統(tǒng)編程時(shí)的注意事項(xiàng)
文章出處:【微信號(hào):mcu168,微信公眾號(hào):硬件攻城獅】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論