圖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語言 作爲一種結構化的程序設計 語言,在模塊的劃分上主要依據功能(依功能進行劃分在面向對象 設計中成爲一個錯誤,牛頓定律遇到了相對論 ),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語言編程時程序整體框架結構分散、數據和函數脫節的問題。我們在後續章節會看到這樣的例子。
總結
本篇介紹了嵌入式系統編程軟件架構方面的知識,主要包括模塊劃分、多任務還是單任務選取、單任務程序典型架構、中斷服務程序、硬件驅動模塊設計等,從宏觀上給出了一個嵌入式系統軟件所包含的主要元素。
請記住:軟件結構是軟件的靈魂!結構混亂的程序面目可憎,調試、測試、維護、升級都極度困難。
在嵌入式系統的編程中,常常要求在特定的內存單元讀寫內容,彙編 有對應的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應成對出現!
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爲單位前進!
圖1 CPU與NVRAM地址線連接 |
子貢再問:So why 80186的地址線A0不與NVRAM的A0連接?
子曰:請看《IT論語》之《微機原理篇》,那裏面講述了關於計算機組成的聖人之道。
總結
本篇主要講述了嵌入式系統C編程中內存操作的相關技巧。掌握並深入理解關於數據指針、函數指針、動態申請內存、const及volatile關鍵字等的 相關知識,是一個優秀的C語言程序設計師的基本要求。當我們已經牢固掌握了上述技巧後,我們就已經學會了C語言的99%,因爲C語言最精華的內涵皆在內存 操作中體現。
我們之所以在嵌入式系統中使用C語言進行程序設計,99%是因爲其強大的內存操作能力!
如果你愛編程,請你愛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[3 2]; /* 字模點陣 */ }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語言中哪怕用到一丁點的面向對象思想,軟件結構將會有何等的改觀!
筆者曾經是個笨蛋,被菜單搞暈了,給出這樣的一個系統:
圖1 菜單範例 |
要求以鍵盤上的"← →"鍵切換菜單焦點,當用戶在焦點處於某菜單時,若敲擊鍵盤上的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函數,這個Windows編程中的超級猛料,不知道是多少入門者第一次用到的函數。還記得我們第一次在Windows中利用 MessageBox輸出 "Hello,World!"對話框時新奇的感覺嗎?無法統計,這個世界上究竟有多少程序員學習Windows編程是從 MessageBox("Hello,World!",…)開始的。在我本科的學校,廣泛流傳着一個詞彙,叫做"’Hello,World’級程序員", 意指入門級程序員,但似乎"’Hello,World’級"這個說法更搞笑而形象。
圖2 經典的Hello,World! |
圖2給出了兩種永恆經典的Hello,World對話框,一種只具有"確定",一種則包含"確定"、"取消"。是的,MessageBox的確有,而且也應該有兩類!這完全是由特定的應用需求決定的。
嵌入式系統中沒有給我們提供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上凌亂不堪的顯示內容所困擾。
屏幕乃嵌入式系統生存之重要輔助,面目可憎之顯示將另用戶逃之夭夭。屏幕編程若處理不好,將是軟件中最不繫統、最混亂的部分,筆者曾深受其害。