WinCE下用C++實現掌上型電腦遙控TV
1. 簡介
你是否曾想過通過你的掌上型電腦上的IR連接埠控制你的TV、Hi-Fi或者其它視頻?本文將介紹怎樣使用掌上型電腦中的IR連接埠來編程式控制制一台TV。
2. 背景
我近些日子丟失了我的老式索尼TV的遙控器。這本身沒有什麼問題,因為我買了個新的遙控器作為代替。然而,當電視失去了它的設定的顏色時,我遇到了問題,因為它只能顯示黑白色了,而新的遙控器沒有顏色調整按鈕。我決定在我的老式的Jornada 525掌上型電腦上寫一個程式使用IR連接埠把正確的代碼發送給TV。
共有三個主要協議可以用於發送IR代碼到裝置上。索尼TV使用 ’Pulse Coded’ 方法,它需要發送一個包含頭(header)位的以空格隔開的’1’位和’0’位的資料流。這些位被調製成一種40KHz的載波訊號。其中,頭長度為2200 μs,’1’位為110 μs,’0’位為550 μs,而空格是550μs的沉默(silence)。大多數索尼裝置使用12位元據,它被分離成6位的地址(裝置類型)和6位命令。因此資料看起來象這個樣子:hxxxxxxyyyyyy,其中h是頭位,xxxxxx是6位的命令(msb first),yyyyyy是6位的地址。對此我不再細述,因為網上有很多資源描述這種協議,並列舉了針對不同裝置的代碼。一些新的索尼裝置使用19位代碼,我相信另外的製造商也使用和我描述的相同的格式。還有可能為使用’Space Coded’或’Shift Coded’協議的裝置寫出相似的類。
我曾使用嵌入式C++寫過一個類CirPulse,它封裝了從一台運行Windows CE 3.0的Jornada 525 PC上控制索尼及其相匹配裝置的功能。估計它能夠與其它相匹配裝置和作業系統一起工作,但是你需要實驗才行!
3. 實現過程分析
這個CIrPulse類暴露了幾個函數,它們使得發送IR代碼儘可能容易。在聲明CIrPulse類時,你應該調用一次FindIrPort(),它返回一個描述IrDA連接埠的連接埠號碼的UINT,這通過搜尋註冊表得到。這個連接埠號碼用於後面的調用來開啟IrDA連接埠進行串列通訊。
UINT CIrPulse::FindIrPort() { // 查詢註冊表中的IR連接埠號碼 HKEY hKey = NULL; if(RegOpenKeyEx(HKEY_LOCAL_MACHINE,_T("Comm//IrDA"),0, 0, &hKey) == ERROR_SUCCESS) { DWORD dwType = 0; DWORD dwData = 0; DWORD dwSize = sizeof(dwData); if (RegQueryValueEx(hKey, _T("Port"), NULL, &dwType, (LPBYTE) &dwData, &dwSize) == ERROR_SUCCESS) { if (dwType == REG_DWORD && dwSize == sizeof(dwData)) { RegCloseKey(hKey); return (UINT) dwData; } } RegCloseKey(hKey); } return 0; } |
得到連接埠號碼後,你可以調用Open(UINT)函數,把通過調用FindIrPort()得到的連接埠號碼傳遞過去。這開啟該連接埠並設定串口參數,如果成功返回true。該連接埠被設定為115200傳輸速率,8個資料位元,2個停止位和同位位元。關於如何產生載波以及為什麼我使用這些設定將在本文後面介紹。
BOOL CIrPulse::Open(UINT uiPort) { ASSERT(uiPort > 0 && uiPort <= 255); Close(); //開啟IRDA連接埠 CString strPort; strPort.Format(_T("COM%d:"), uiPort); m_irPort = CreateFile((LPCTSTR) strPort, GENERIC_READ | GENERIC_WRITE,0, NULL, OPEN_EXISTING, 0, NULL); if (m_irPort == INVALID_HANDLE_VALUE) { return FALSE; } //設定輸入和輸出緩衝區的大小 VERIFY(SetupComm(m_irPort, 2048, 2048)); //清除讀和寫緩衝區 VERIFY(PurgeComm(m_irPort,PURGE_TXABORT|PURGE_RXABORT| PURGE_TXCLEAR|PURGE_RXCLEAR)); //重新初始化所有的IRDA連接埠設定 DCB dcb; dcb.DCBlength = sizeof(DCB); VERIFY(GetCommState(m_irPort, &dcb)); dcb.BaudRate = CBR_115200; dcb.fBinary = TRUE; dcb.fParity = TRUE; dcb.fOutxCtsFlow = FALSE; dcb.fOutxDsrFlow = FALSE; dcb.fDtrControl = DTR_CONTROL_DISABLE; dcb.fDsrSensitivity = FALSE; dcb.fTXContinueOnXoff = FALSE; dcb.fOutX = FALSE; dcb.fInX = FALSE; dcb.fErrorChar = FALSE; dcb.fNull = FALSE; dcb.fRtsControl = RTS_CONTROL_DISABLE; dcb.fAbortOnError = FALSE; dcb.ByteSize = 8; dcb.Parity = EVENPARITY; dcb.StopBits = TWOSTOPBITS; VERIFY(SetCommState(m_irPort, &dcb)); //為所有的讀和寫操作設定逾時值 COMMTIMEOUTS timeouts; VERIFY(GetCommTimeouts(m_irPort, &timeouts)); timeouts.ReadIntervalTimeout = MAXDWORD; timeouts.ReadTotalTimeoutMultiplier = 0; timeouts.ReadTotalTimeoutConstant = 0; timeouts.WriteTotalTimeoutMultiplier = 0; timeouts.WriteTotalTimeoutConstant = 0; VERIFY(SetCommTimeouts(m_irPort, &timeouts)); DWORD dwEvent=EV_TXEMPTY; SetCommMask(m_irPort,dwEvent); return TRUE; } |
調用函數SetCodeSize(DWORD)來設定要傳送的位元(如12位)。這可以在任何時候完成且只需要做一次。它一直保持有效,直到後面的調用改變它為止。
最後調用SendCode(long),傳遞實際要發送的代碼。
BOOL CIrPulse::SendCode(DWORD lValue) { DWORD dwCount; int i=0; ASSERT(iDataLength>0); //清除傳送緩衝區 VERIFY(PurgeComm(m_irPort,PURGE_TXABORT| PURGE_RXABORT |PURGE_TXCLEAR | PURGE_RXCLEAR)); //每次按鍵設定代碼6次 for(int x=0;x<6;x++) { MakeStream(lValue); //發送代碼 dwCount=GetTickCount(); while(GetTickCount()<dwCount+26) //延遲26ms i++; } return true; } |
注意這個函數調用另外一個函數MakeStream(long)6次,每兩次調用之間停頓26毫秒。我發現該代碼必鬚髮送好幾次才能使接收裝置響應,大概是為防止假行為的緣故吧。26毫秒對於接收裝置登記該代碼是必需的,在下一個代碼出現之前。
這個函數MakeStream(long)把位元組流寫入IrPort,並根據是否有起始位(1或者0)來確保發送正確的資料包長度。包含資料位元組(0xdb)的緩衝區是以一個ByteArray形式存在的。
函數Close()用於在連接埠使用後,自然地關閉IrPort。
這個函數在我的ornada上運行良好。請看下面的討論以進一步確定你要做的可能性改變。
BOOL CIrPulse::MakeStream(DWORD lValue) { DWORD dwStreamLength; //建立開始脈衝 dwStreamLength=iHPulse/charWidth; ASSERT(Write((const char *)bPulseStream.GetData(), dwStreamLength)==dwStreamLength); // ************************************ // ***** 在下一個脈衝到來前延遲一段時間 // ************************************ //迴圈作業碼中的位來發送脈衝 for(int i=0;i<iDataLength;i++) { if(lValue & 1) { //建立一個脈衝1 dwStreamLength=i1Pulse/charWidth; ASSERT(Write((const char *)bPulseStream.GetData(), dwStreamLength)==dwStreamLength); // ********************************* // ***在下一個脈衝到來前延遲一段時間 // ********************************* } else { //建立一個脈衝 0 dwStreamLength=i0Pulse/charWidth; ASSERT(Write((const char *)bPulseStream.GetData(), dwStreamLength)==dwStreamLength); // ******************************** // **在下一個脈衝到來前延遲一段時間 // ******************************** } lValue >>= 1; } return TRUE; } |
我在所附原始碼中包含了一個簡單的應用程式,它使用CIrPulse來建立一台索尼TV的遠距離遙控。它具有基本的頻道選擇、音量調整和開/關機的功能。
4. 特別注意
因為該CIrPort類使用一個序列埠串連到該IR連接埠,所以必鬚生成一個40KHz的載波訊號,這通過從該序列埠發送恰當的字元來實現。幸好,如果我們發送字元0xdb,以115200傳輸速率,用8個資料位元,2個停止位和同位,這樣就能產生一種極接近38.4KHz的載波訊號。我們所有的索尼裝置接收這種資料是沒有問題的。
最大的問題是,如何?間隔每次脈衝的沉默周期。不可能由序列埠來產生該沉默周期,因為就算你發送一個0x0字元,由於存在起始和停止位,你仍然在該IR上得到脈衝。我通過發送不同的字元進行實驗,依據的前提是如果你不以40KHz的頻率發送一個載波訊號,這有可能使裝置誤把這個當作一個沉默。這樣做的優點是你可以產生一個包含完整的代碼的byteArray,以確保準確計時。但是結果並不一致,所以我拒絕使用這個方法,為的是實現在兩次從序列埠發出成組的0xdb字元之間支援暫停。因為需要的延遲是以550μs的順序;到目前為止,我還沒有找到取得獨立於處理器速度的暫停方法。在我的Jornada上,是完全不必產生一個延遲的,因為每次調用Write函數看上去都使用了合適的時限。不管怎樣,我擔心的是,你可能胡亂產生一個可以使你的掌上型電腦能工作的一個延遲。