[讀書筆記2]《C語言嵌入式系統程式設計修煉》
第3章 螢幕操作
3.1 漢字處理
現在要解決的問題是,嵌入式系統中經常要使用的並非是完整的漢字型檔,往往只是需要提供數量有限的漢字供必要的顯示功能。例如,一個微波爐的LCD上沒有必要提供顯示"電子郵件"的功能;一個提供漢字顯示功能的空調的LCD上不需要顯示一條"短訊息",諸如此類。但是一部手機、小靈通則通常需要包括較完整的漢字型檔。
如果包括的漢字型檔較完整,那麼,由內碼計算出漢字字模在庫中的偏移是十分簡單的:漢字型檔是按照區位的順序排列的,前一個位元組為該漢字的區號,後一個位元組為該字的位號。每一個區記錄94個漢字,位號則為該字在該區中的位置。因此,漢字在漢字型檔中的具體位置計算公式為:94(區號-1)+位號-1。減1是因為陣列是以0為開始而區號位號是以1為開始的。只需乘上一個漢字字模佔用的位元組數即可,即:(94
對於包含較完整漢字型檔的系統而言,我們可以以上述規則計算字模的位置。但是如果僅僅是提供少量漢字呢?譬如幾十至幾百個?最好的做法是:
定義巨集:
# 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} } }
要顯示特定漢字的時候,只需要從陣列中查詢內碼與要求漢字內碼相同的即可獲得字模。如果前面的漢字在陣列中以內碼大小順序排列,那麼可以以二分查詢法更高效的查詢到漢字的字模。這是一種很有效的組織小漢字型檔的方法,它可以保證程式有很好的結構。
<br >
3.2 系統時間顯示
從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具有了更加強大的威力,它使得某些資料和函式脫離"物件"而成為"類"的一部分,正是它的這一特點,成就了軟體的無數優秀設計。
3.3 動畫顯示
動畫是無所謂有,無所謂無的,靜止的畫面走的路多了,也就成了動畫。隨著時間的變更,在螢幕上顯示不同的靜止畫面,即是動畫之本質。所以,在一個嵌入式系統的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 ; //使用!操作符,來改變 bShowDot 的值,從而達到閃爍效果
}
以前的類似閃爍程式是這樣寫的,並不是太好:
int i;
void showDot()
{
i++;
if(i%2)
showChar(":",xPos,yPos);
else
showChar(" ",xPos,yPos);
}
3.4 選單操作
無數人為之絞盡腦汁的問題終於出現了,在這一節裡,我們將看到,在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();
}
程式被大大簡化了,也開始具有很好的可擴充套件性!我們僅僅利用了面向物件中的封裝思想,就讓程式結構清晰,其結果是幾乎可以在無需修改程式的情況下在系統中新增更多的選單,而系統的按鍵處理函式保持不變。
3.5 模擬MessageBox函式
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是何等的神似啊?實現這個函式,你會看到它在嵌入式系統中的妙用是無窮的。
3.6 總結
本篇是本系列文章中技巧性最深的一篇,它提供了嵌入式系統螢幕顯示方面一些很巧妙的處理方法,靈活使用它們,我們將不再被LCD上凌亂不堪的顯示內容所困擾。
螢幕乃嵌入式系統生存之重要輔助,面目可憎之顯示將另使用者逃之夭夭。螢幕程式設計若處理不好,將是軟體中最不繫統、最混亂的部分,筆者曾深受其害。
第四章 鍵盤操作
4.1 處理功能鍵
功能鍵的問題在於,使用者介面並非固定的,使用者功能鍵的選擇將使螢幕畫面處於不同的顯示狀態下。例如主畫面如圖1:
圖1 主畫面
當用戶在設定XX上按下Enter鍵之後,畫面就切換到了設定XX的介面,如圖2:
圖2 切換到設定XX畫面
程式如何判斷使用者處於哪一畫面,並在該畫面的程式狀態下呼叫對應的功能鍵處理函式,而且保證良好的結構,是一個值得思考的問題。
讓我們來看看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定義幾個精妙的巨集來實現"訊息對映"。
4.2 處理數字鍵
使用者輸入數字時是一位一位輸入的,每一位的輸入都對應著螢幕上的一個顯示位置(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();
}
}
將數字每一位輸入的座標和輸入值捆綁後,在數字鍵處理函式中就可以較有結構的組織程式,使程式顯得很緊湊。
4.3 整理使用者輸入
繼續第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函式或許是唯一的選擇了。
4.4 總結
本篇給出了鍵盤操作所涉及的各個方面:功能鍵處理、數字鍵處理及使用者輸入整理,基本上提供了一個全套的按鍵處理方案。對於功能鍵處理方法,將LCD螢幕與Windows視窗進行類比,提出了較新穎地解決螢幕、鍵盤繁雜互動問題的方案。
計算機學的許多知識都具有相通性,因而,不斷追趕時髦技術而忽略基本功的做法是徒勞無意的。我們最多需要"精通"三種語言(精通,一個在如今的求職簡歷裡氾濫成災的詞語),最佳拍檔是彙編、C、C++(或JAVA),很顯然,如果你"精通"了這三種語言,其它語言你應該是可以很快"熟悉"的,否則你就沒有"精通"它們。