第六章 系統軟體設計-嵌入式電腦語音溝通 系統和替代性輸入裝置軟體
本章將介紹自訂的嵌入式電腦語音溝通系統和替代性輸入裝置軟體程式的 設計概念及相關功能的說明,包括程式設計流程,軟體版面設計,輸入法設計和 聲音檔設計。圖 6-1 為系統軟體架構圖。
程 式 設 計 流 程
軟 體 版 面 設 計
輸 入 法 設 計
聲 音 檔 設 計 系統軟體
嵌入式電腦語音溝通 系統和替代性輸入裝置 系統初始化 軟體設計概念
圖 6-1 系統軟體設計之嵌入式電腦語音溝通系統和替代性輸入裝置架構圖
接著介紹本系統的程式設計流程,如圖 6-2:
否
是
進入起始點函式(WinMain)
印表機埠初始化 (ParPortInit)
註冊視窗類別
接收到 WM_LBUTTONDOWN 訊息,
呼叫(DoLButtonDownMain)函式。計算 出觸控面板之 x-y 座標並呼叫
(InvalidateRect)函式,負責通知 (DoPaintMain)函式將畫面重畫。
MainMessages table 將不斷被掃 瞄。SetTimer 函式將於每 0.01s 執行 WM_TIMER 訊息而呼叫 (DoTimerMain)函式,主要檢查 CheckKeybrd 副程式,測試電腦 端是否有信號傳送至系統。
按鍵是否被 按下 程式開始
執行
主視窗訊息被傳送到(MainWndProc)函式。訊 息不斷被掃瞄。若有,則呼叫相對應的副程式 或函式,因此它是整個程式的中心。
自動接收到 WM_SIZE 訊息,執行 (DoSizeMain)函式,將矩陣之值初始化。
自動接收到 WM_PAINT 訊息,呼叫(DoPaintMain) 函式。進行圖形的繪製。語音輸出和鍵盤按鍵碼 資料的送出也透過此函式的觸發而被執行。
自動接收到 WM_CREATE 訊息,
呼叫(DoCreateMain)函式,
執行 SetTimer(hWnd,1,10,NULL)
圖 6-2 系統程式主要流程圖
一、程式設計流程
首先從 WinMain 函式入口,它會分別傳入四個變數,為 HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow。
hInstance 為讓 Windows API(Application Programming Interface)辨識此指定程式 有別於其他應用程式的特定情況,hPrevInstance 則是保留讓舊式 Win16 API(Windows 3.1 等)使用,因此在 Win32 作業系統(包含 Windows CE),
hPrevInstance 的值多為 0,也可被忽略,lpCmdLine 主要是指到一具有 command line 文字的 Unicode 字串,其目的主要是容易分辨出此執行程式是在 Windows CE 下,因此在其他 Windows 版本下,該字串皆為 ASCII 字串。此變數也可稱為 command-line parameter,而最後一個變數 nCmdShow 有三種允許的狀況:
SW_SHOW、SW_HIDE、SW_SHOWNOACTIVATE(Show without activate),主要 是明確指定程式的主要視窗畫面的開始狀況[29]。
當視窗初始值設定完畢後,進入到 ParPortInit 副程式。此副程式主要是設定 印表機埠的初始值,因為系統印表機埠的 I/O 位子為 278H,因此我們將印表機 埠位子變數設定為 278H,而印表機埠的狀態地址(Status Address)則在 278H+1 的 位子。設定完畢後,我們將進入到 InitInstance(hInstance, lpCmdLine, nCmdShow) 函式。此函式將會呼叫 RegisterClass 註冊一視窗類別,其中第二個變數
WNDPROC lpfnWndProc 為最主要的屬性,它指定了視窗的 window procedure 地
址。當視窗類別記錄完畢,主視窗就可被產生了,此時 InitInstance 將結束。
回到 WinMain 後,將進入 GetMessage 執行訊號迴圈。它將會在應用程式的 訊息儜列裡拿接下來的訊息。若沒有訊息,則會等待,直到取得新訊息為止。
TranslateMessage 函式是將透過鍵盤或觸控面板所傳回的訊息轉換成字串訊息,
而 DispatchMessage 函式則告訴 Windows 將訊息轉送到合適的視窗。這時訊息將 會被傳送到 MainWndProc 進行處理。
第一個自動被處理的函式是 DoSizeMain,接著 DoCreateMain 函式也會被呼 叫,它主要是執行 SetTimer(hWnd,1,10,NULL)函式,其中第三個變數值 10 表示 每 0.01s 執行第四個變數指到的函式,而第四個變數 NULL 表示預設值指到(訊 息 WM_TIMER)DoTimerMain 函式,此函式只執行 CheckKeybrd 副程式,其功能 為測試電腦端是否有信息需傳送至系統。表 6-1 為 CheckKeybrd 副程式的程式片 段。
void CheckKeybrd() {
byte cpu_cmpdata, Test_DataFromCPU=0;
cpu_cmpdata = CheckStatusPort(0x28); //change 0010 1000 bit3 為 clock if (cpu_cmpdata == 0x08) //0000 1000
{ //cpu_clock=1, cpu_data=0
DataFromCPU = ReceiveDataFromCPU();
switch(DataFromCPU) {
case 0xF6:
Send_KbCode(wordData[1676].linkArr[0]->makecodelist[0]); //KeyACK break;
case 0xFF: //System reset
CPU_ClockTesting();//送按鍵之前測 clock 是否等於 1
Send_KbCode(wordData[1676].linkArr[0]->makecodelist[0]); //KeyACK CPU_ClockTesting();//送按鍵之前測 clock 是否等於 1
LongDelay(1000);
Send_KbCode(wordData[1678].linkArr[0]->makecodelist[0]); //KeyNormal LongDelay(2000);
break;
. . }//switch
}//if }
表 6-1 測試電腦端信號傳送之程式片段
印表機的 278h、279h 及 27Ah 分別為其資料埠(Data Port)、狀態埠(Status Port) 及控制埠(Control Port)。本系統的程式裡只用了資料埠和狀態埠。因此在執行是
CheckStatusPort 副程式如表 6-2 時,透過_inp 可以讀到 PrtStatAddr(Status Port) 位置之值,和變數 cpu_data(0x28)作 AND 的動作。0x28 主要是讓 clock 和 data 信號為 1,再和收到之資料做比較,接著將比較之資料送回到 CheckKeybrd 副程 式。
接著 if (cpu_cmpdata == 0x08)就判斷讀入之時脈信號是否為 1,若是,則表 示電腦端有資料需傳送到系統端。此時就進入到 ReceiveDataFromCPU 副程式,
如表 6-3。
byte CheckStatusPort(byte cpu_data) {
byte cpu_ReadData;
cpu_ReadData = _inp(PrtStatAddr); //從 279h 讀資料回來 cpu_ReadData &= cpu_data; //把讀回來的資料跟 data 做 AND return cpu_ReadData;
}
表 6-2 從電腦端讀入時脈和資料信號
byte ReceiveDataFromCPU(){
int r;
byte cpu_cmpdata, MaskKey;
ReceiveData = 0x00;
for(r=0;r<8;r++){
cpu_cmpdata = CheckStatusPort(0x28);
if ((cpu_cmpdata & 0x20) != 0){ //data=1, clcok 不一定要 1,0010 x000 MaskKey = 0x01;
MaskKey = MaskKey << r;
} else
MaskKey = 0;
ReceiveData += MaskKey;
KeyboardClock();
}//for r
//Parity bit ignored KeyboardClock();
ParPortOut(0);
KeyboardClock();
ParPortOut(1);
return ReceiveData;
}
表 6-3 從電腦端接受資料
for 迴圈裡將 8 次收到的資料存入 ReceiveData 變數,時脈的產生主要是靠 KeyboardClock 副程式。這時,根據表 6-1 就進入到判斷從電腦端接受的資料為 什麼 command。電腦端所提供之 command 有 KeyACK( 0xFA)、KeyRESET(0xF
F)、KeyNormal(0xAA)等,不同的 command 需回傳的資料也不一樣,不過都是 透過 Send_KbCode 副程式傳送。
接著自動執行的函式則是 DoPaintMain。執行完畢後,將會進入 MainMessage
table 等待按鍵的觸發。若有合適的訊息則進入該副程式,若無合適訊息則每 0.01s 執行 DoTimerMain 函式,否則將進入 DefWindowProc,它是 Windows 所提供進 行預設動作的副程式。
當觸控螢幕被按下後,將會進入到 MainMessage table 中的
DoLButtonDownMain 函式。它的功能除了計算出被按下的版面位址外,也會以 InvalidateRect 函式去呼叫 DoPaintMain 進行前景(foreground)重畫(refresh)的動 作。
在 DoPaintMain 裡我們可以發現有幾個特別的函式是 Windows CE 程式設計 軟體在畫視窗畫面時需要呼叫到的,例如 GetClientRect、BeginPaint 和 EndPaint。
當一視窗要被畫出時,首先要確定的是視窗的大小。在標準 Windows 程式裡,
標準視窗可分為兩部份,第一部份為非使用者區(nonclient area),另一部份則為
使用者區(client area)。視窗的邊框(borders)、捲軸條(scroll bars)和各種其他控制
(controls)組成了非使用者區而這部份是由視窗管理員(window manager)負責完
成。至於使用者區則是位在非使用者區裡面的中央空間,這部分則是應用程式應 該負責完成的部份。因此我們呼叫了 GetClientRect 函式,它將回傳一 RECT 結 構的資料,其中包括區域的左、右、上、下界線。接著我們需要 BeginPaint,藉 著回傳 hdc(Handle to a device context)我們才可利用它對視窗進行繪圖的動作。所 謂 device context,它是一個 physical display device(例如螢幕或印表機)的代表,
因為我們無法直接對硬體進行控制,因此需透過制定的裝置內容進行。
至於 DoPaintMain 結束後所執行的繪圖指令,則透過 Drawing 此副程式去呼 叫,例如呼叫 Draw_Main 副程式畫出首頁,呼叫 Play_Chinese_Voice 副程式播放 音樂,呼叫 sendkey_process 副程式進行圖形按鍵的輸出等。
表 6-4 的程式片段能正確的判斷圖形按鍵該落在首頁或分頁的版面,其主要 以 Page 這個被初始化為-1 值的整數變數進行版面的判斷。第一行 First_Time 變 數是判斷第一次系統啟動軟體時該執行 Draw_Main 副程式以畫出首頁版面,剩 餘的 Draw_Page 和 Clr_Screen 則是畫出其他分頁的副程式和清除整個螢幕的副 程式。最後倒數的三行則是該圖形按鍵不為畫面切換按鍵,而是該送資料之按 鍵,此時就會進入到 Play_Chinese_Voice 副程式。
每個按紐的資料結構設計主要分成首頁的圖形按鍵設計和其餘分頁的圖形 按鍵設計。不過每個圖形按鍵的位置判斷則是靠表 6-5 的 Mouse_final 這個變數。
有了它,我們就能確定該圖形按鍵在版面上的正確位置。因此,Page 變數配合
上 Mouse_final 變數,就能正確的找到圖形按鍵的相對位置。
表 6-4 的 Draw_Main 和 Draw_Page 副程式之繪圖功能非常類似,但由於 Draw_Main 不需傳入版面的所在位置值,而 Draw_Page 是負責畫出其他除了首
if (First_Time == true) //第一次畫首頁, 不用 button#
Draw_Main(hdc,0);
else //
{
if ((Mouse_final < 49) && (Page == -1))//在首頁按下後要判斷跳到哪個盤面 {//除了從首頁切換盤面到非首頁, 還有是在非首頁發聲
Clr_Screen(hdc,hWnd);
Draw_Page(hdc,MainPage_Order[Mouse_final-1] ,0);
Page = MainPage_Order[Mouse_final-1] ; }
else
if (Mouse_final == 54)//首頁 {
Clr_Screen(hdc,hWnd);
Draw_Main(hdc,0);
Page = -1;
} else
if (Mouse_final == 55) //上一頁 {
if (Page == -1) Page = -1;
else
if (Page == 0) //要換到首頁的顯示 {
Clr_Screen(hdc,hWnd);
Draw_Main(hdc,0);
Page = -1;
} else {
Page = Page - 1;
Draw_Page(hdc,Page,0);
} } else
if (Mouse_final == 56)//下一頁 {
if (Page < 31) //還有畫面 若有 4 個畫面, (Page < 3 ) {
Page = Page + 1;
Clr_Screen(hdc,hWnd);
Draw_Page(hdc,Page,0);
} } else
if ((Mouse_final < 54) && (Page != -1))
Play_Chinese_Voice(hInst,Mouse_final,Page);
}
表 6-4 版面切換之程式判斷
Mouse_x = pt.x/100 ; Mouse_y = pt.y/80 ;
Mouse_final = (Mouse_y * 8) + (Mouse_x + 1);
表 6-5 觸控按鍵座標轉換方式
頁的畫面,因此將其兩個副程式分開宣告。因此,若該頁為首頁,則透過呼叫表
6-6 的 Main_table 矩陣資料以繪出對應圖形,其中 bitmapindex[50]字元矩陣變數 表示可存放之檔案名稱長度為 50,Main_table[41]為首頁之圖形按鍵數,表示共 有 41 種情境類別。若該頁為分頁,則進入到 Play_Chinese_Voice 副程式以便進 行聲音檔的輸出和按鍵碼的傳送。
但我們發現,在 eMbedded Visual C++裡,顯示一張圖形的方式並不如桌上 型的 Visual C++那麼容易。表 6-7 宣告了在嵌入式系統進行繪圖時需要用到的內 建函式。
表 6-7 繪圖函式宣告表
struct Bitmap_Info {
TCHAR bitmapindex[50];
};
struct Bitmap_Info Main_table[41]=
{
TEXT("Hard Disk\\res\\m_jialirenwu.BMP"), //家裡人物 TEXT("Hard Disk\\res\\m_jialijiaju.BMP"), //家裡家具
. . . }
表 6-6 首頁之圖形按鍵矩陣
HBITMAP hbm; //handle to a bitmap HBITMAP hbmT;
HDC hdcMem = CreateCompatibleDC(NULL);
BITMAP bm;
TCHAR *TmpBitmap;
TmpBitmap =Main_BPNList[Page_Number].List[Button_Count++].bitmapindex;
hbm = SHLoadDIBitmap(TmpBitmap);
hbmT = SelectBitmap(hdcMem,hbm);
GetObject(hbm,sizeof(bm),&bm);
BitBlt(hdc,nX,nY,bm.bmWidth,bm.bmHeight,hdcMem,0,0,SRCCOPY);
SelectBitmap(hdcMem,hbmT);
DeleteDC(hdcMem);
首先宣告了兩個以 HBITMAP 結構為主的變數,HBITMAP 為 bitmap 的
handle。CreateCompatibleDC(NULL)主要是產生一個與目前螢幕相容的記憶裝置 內容(memory device context )並放入 hdcMem 這個 DC 的 handle,因為 bitmap 在 被選擇的過程中,它所要求的顏色必須符合裝置(螢幕)可用的顏色,唯有在這個 動作完成以後,bitmap 才能呈現於螢幕上。一旦透過函式建立了記憶裝置內容 後,就可利用 SHLoadDIBitmap(TmpBitmap)將指定的 bitmap 檔案載入,並將
bitmap 的 handle 回傳到 hbm 變數裡。接著 hbmT = SelectBitmap(hdcMem,hbm) 的 SelectBitmap 將會把新的 bitmap handle 放到記憶 DC 裡。而
GetObject(hbm,sizeof(bm),&bm)主要是希望取得 hbm 所指的 bitmap 的資料,&bm 指到一存放 bitmap 資料的緩衝區。到目前為止,bitmap 仍然還未顯示在螢幕上,
必須透過 BitBlt,它會將 hdcMem 所指到的 bitmap 彩色資料(color data)轉換至 hdc 上。當 bitmap 顯示完畢後,我們必須執行 SelectBitmap(hdcMem,hbmT),因為它 會將原來的 bitmap 選擇狀態儲存到記憶裝置內容(memory DC)。最後的步驟則是 執行 DeleteDC(hdcMem)將記憶裝置內容(memory DC)釋放出來。
接著將進入到 Play_Chinese_Voice 副程式。圖形按鍵之位置和頁數
(Mouse_final 和 Page)變數也會同時被傳入以進行判斷。表 6-8 提供的音樂儲存之
資料結構和表 6-9 之音樂矩陣結合,加上圖形按鍵位置和頁數的轉換(temp =
(48*Page) + Mouse_final),就可正確的讀到相對應的聲音檔和按鍵碼。
Wave_Number 表示某圖所具有的總共音個數,waveindex 則儲存了聲音檔的 存放地方和檔名。
表 6-9 之 IDR_List 矩陣變數共有 1580 筆資料,矩陣的第一個變數 2 表此圖 形按鍵共有兩個音,其檔名為@@o1f 和@l@2f。當確定了被按下圖形按鍵的聲 音檔後,就可呼叫內建的 PlaySound 函式將指定的聲音檔播放出來。接著進入到 傳送按鍵從系統至電腦端的 sendkey_process 副程式,temp 變數也將會被傳入至 sendkey_process 副程式。
struct Wave_Node_List IDR_List[1580] = {
{2,TEXT("Hard Disk\\Wave\\@@o1f"),TEXT("Hard Disk\\Wave\\@l@2f")}, //阿姨 {2,TEXT("Hard Disk\\Wave\\0@o4f"),TEXT("Hard Disk\\Wave\\0@o3f")}, //爸爸
. . . }
表 6-9 音樂儲存之矩陣
struct Wave_Node_List {
int Wave_Number;
TCHAR waveindex1[23];
TCHAR waveindex2[23];
TCHAR waveindex3[23];
TCHAR waveindex4[23];
TCHAR waveindex5[23];
TCHAR waveindex6[23];
TCHAR waveindex7[23];
TCHAR waveindex8[23];
};
表 6-8 音樂儲存之資料結構
進入 sendkey_process 副程式之後,我們需要先確定該圖形按鍵的長度,接 著透過鍵盤時脈將資料傳送至電腦端(或從電腦端接收資料),表 6-10 和 6-11 解 釋了按鍵之資料結構。
表 6-10 是每個鍵盤按鍵(例如 a、b、ㄅ)都具有各自獨立的按鍵碼(makecode 和 breakcode),而 Code_Struct 這個資料結構定義了四個以 byte(8 bits)大小變數儲 存的鍵盤按鍵碼資料,例如 a 的 makelen 儲存的是 1,因為 a 的 makecode 長度為
1,而 makecodelist 是指到一個儲存 a 的 makecode 的指標,該指標會對應到相對 的 16 進位鍵盤按鍵碼(1CH 為 a 的 makecode),如表 6-11。
表 6-12 單一圖形按鍵碼之資料結構 表 6-10 鍵盤按鍵(單鍵)之資料結構
typedef struct KB_STRUCT {
byte makelen; // make code 的長度 byte *makecodelist; // make code 的內容 byte breaklen; // break code 的長度 byte *breakcodelist; // break code 的內容 }Code_Struct;
typedef struct KB_Word_STRUCT{
int num_of_word;
Code_Struct *linkArr[MAX_MAKE_NUM];
}Word_Struct;
Word_Struct wordData[KEY_NUM];
表 6-11 鍵盤按鍵(單鍵)之矩陣
Code_Struct codedata[CODE_NUM] = {
{ 1, &makecode[0], 2, &breakcode[0] }, //000 1,ㄅ,s_!
{ 1, &makecode[1], 2, &breakcode[2] }, //001 q,ㄆ
..
.
}
Word_Struct wordData[KEY_NUM]=
{
{ 7, &codedata[24], &codedata[37], &codedata[0], &codedata[21], &codedata[39], &codedata[39], &codedata[48]},//阿姨 { 7, &codedata[0], &codedata[24], &codedata[41], &codedata[4], &codedata[54], &codedata[0], &codedata[48]},//爸爸
. . . }
表 6-12 的資料結構包含了一個圖形具有的文字個數,和其多個按鍵碼資料 的連結。接著系統將呼叫 Send_KbCode 副程式進行按鍵碼傳送,這時將會在
wordData 矩陣裡找到對應圖形的鍵盤按鍵碼,如表 6-13。其中 KEY_NUM 為系 統所有圖形數,預設為 2000 張圖,資料的第一欄 7(num_of_word)為該圖形所包 含的鍵盤按鍵碼個數。
表 6-13 圖形之鍵盤按鍵碼
當系統確定了按鍵碼個數後,則進入到 codedata 指標矩陣,其第一個和第三
欄位表該按鍵碼之 makecode 和 breakcode 的長度,*makecodelist 和*breakcodelist
則分別指到兩個放了鍵盤按鍵碼資料的 16 進位矩陣。圖 6-3 就是系統圖形之鍵
盤按鍵碼的資料結構分析圖。
鍵盤按鍵碼的傳輸透過資料線和時脈線,在此我們將展示程式如何完成控制 印表機埠以傳送資料的部份。首先我們判斷從 makecode 或 breakcode 矩陣所取 得的十六進位資料其 8 個 bits 為 0 或 1,我們以表 6-14 的程式表示之。
byte makecode[MAKE_NUM] = {
0x16, //000,000/099. 1,ㄅ,s_!
0x15, //001,001/099. q,ㄆ .
. . }
byte breakcode[BREAK_NUM] = {
0xF0, 0x16, //000,000/192. 1,ㄅ,s_!
0xF0, 0x15, //001,002/192. q,ㄆ .
. . }
Code_Struct codedata[CODE_NUM] = {
{ 1, &makecode[0], 2, &breakcode[0]}, //000 1,ㄅ,s_!
{ 1, &makecode[1], 2, &breakcode[2]}, //001 q,ㄆ .
. . }
Word_Struct wordData[KEY_NUM]=
{
{ 7, &codedata[24], &codedata[37], &codedata[0], &codedata[21], &codedata[39], &codedata[39], &codedata[48]},//阿姨 { 7, &codedata[0], &codedata[24], &codedata[41], &codedata[4], &codedata[54],&codedata[0], &codedata[48]},//爸爸
. . . }
Send_KbCode(wordData[key_position].linkArr[i]->makecodelist[k]);
typedef struct KB_STRUCT { byte makelen;
byte *makecodelist;
byte breaklen;
byte *breakcodelist;
}Code_Struct;
typedef struct KB_Word_STRUCT{
int num_of_word;
Code_Struct *linkArr[MAX_MAKE_NUM];
}Word_Struct;
圖 6-3 系統圖形之鍵盤按鍵碼的資料結構分析圖
當 for 迴圈結束後,矩陣 databit[8]裡每個 byte 所儲存的值則會是 senddata 裡 8 個 bits 之相對值,而 paritybit 主要是計算 senddata 資料裡為 1 的個數是否為 偶數。前置資料處理完畢後,接著就開始傳送資料至印表機埠上。我們利用表 6-15 之 ParPortOut 副程式將資料送出。
在這裡應該特別注意的是,由於作業系統的發展逐漸嚴謹,它不允許我們直 接存取 I/O 埠(印表機埠)之資料,因此在利用_outp 此內建的傳送資料至 I/O 埠之 函式之前,我們必須將 #include <conio.h> 宣告在程式的起始端,並將
inpout32.dll 存放到 C:\Windows\system32 下,否則函式如_inp 或_outp 將無法運 行。至於 inpout32.dll 則是提供於平行埠(parallel port)的標準介面。
我們利用印表機埠的第 2 和 3 腳作為資料和時脈送出的 I/O 埠,此二腳為印 表機資料埠的第 1 和第 2 位元。由於送資料時,時脈線必須為 1,因此才將 OutData
表 6-15 傳送資料至印表機埠之程式 表 6-14 鍵盤按鍵碼位元判斷之程式
byte mask=1, databit[8],paritybit;
for(i=0;i<8;i++){
if((senddata & (mask << i)) >0)//該 bit 為 1 {
databit[i] = 1;
bitsum += 1;
} else
databit[i] = 0; //該 bit 為 0 }
paritybit = (bitsum % 2 == 0)? 1 : 0;