自制電腦紅外遙控接收器 PC軟解碼
網上有很多介紹紅外遙控接收器製作的文章,但其中大部分是用單片進行紅外解碼,然後再通過串列埠或USB把解碼後的按鍵資訊傳入到PC的。這樣的電路製作起來,不僅造價相對偏高,而且需要對單片程式設計,這會令大部分軟體開發愛好者望而卻步。
最近看到一篇僅需要7個簡單元器件的紅外接收器,只需拿起烙鐵,不需硬體程式設計就可以製作完成,原理圖如下:
由原理圖我們可知,紅外接收頭把接收的紅外訊號轉換為高低電平通過串列埠的DSR管腳傳入到PC,PC軟體通過對DSR高低電平訊號的時間曲線進行分析,從而獲得相對應的按鍵資訊。
紅外遙控器一般採用脈寬調製的序列碼,經38kHz的載頻把紅外訊號發射出去。其編碼資訊一般由三部分組成:引導碼、地址碼和資料碼。一般訊號長度大約
常態下,紅外接收頭的輸出(OUT)都是高電平,引導碼訊號首先會令紅外接收頭輸出一個大約10ms左右的低電平(不同遙控器有不同的時延),這可令接收裝置從容判定訊號的到來,而後面的地址碼和資料碼其電平高低變化就相對較快了,大概在幾十或幾百個微秒之間。
PC紅外遙控軟體一般選用Girder,在使用之前需要安裝“SFH-56 plugin for Girder”這個外掛(檔名"igor SFH-56P lug.dll"),否則不能正常處理我們這種電路的紅外接收器訊號。可悲的是我至今沒找到這個外掛,網上提供的很多連結都是壞的。
即使找到了這個外掛,要想在我們自己編寫的程式中使用也是困難的,因為
既然Girder能用軟體實現紅外解碼,我們為什麼不能呢?凡事都要開動大腦,積極行動才對,下面就是我自己焊接的一個紅外接收器(元器件是在中發買的,一共不到10元錢,還富裕好多電阻、電容!)
(圖下方的紅外遙控器的接收器是基於USB的,僅支援Vista以上版本,並且不支援個人開發,不過今天它終於發揮了它應有的作用。當然用電視或VCD遙控器也是可以的)
硬體有了,但程式該從何編起呢?
1、由於接收到的紅外訊號在微秒級別中變化,對系統實時性要求較高,所以具備垃圾回收功能,實時性沒有保證的C#,似乎完不成這種訊號的接收功能,所以我們選擇的是VC,由它實現高優先順序的執行緒去進行訊號接收。
2、由於紅外遙控訊號是脈寬調製的序列碼,所以我們需要採集訊號的寬度,顯然採用一般的時鐘函式來獲取時間間隔是不可行的,因為精度太低,所以我們需用採用多媒體時鐘和高精度計時的API函式。
3、一般我們按鍵持續時間為幾秒鐘,並且由於按鍵發出前有一個10ms左右的引導訊號,所以我們的程式很容易判斷出訊號起始點,這樣我們一次僅需要接收一定量的原始資料就可以完成初步訊號採集工作。
4、對於我們的紅外接收程式來說並不需要實際解碼出紅外訊號到底包含了那些具體的資訊,只要其能夠區分出紅外遙控上的各個按鍵就行。
5、由於紅接收器是通過串列埠RTS管腳供電,且通過DSR傳遞紅外訊號的,所以我們的程式即使不接收資料,也要開啟串列埠,不過僅需要處理RTS和DSR管腳的訊號即可。
好了,動手去做,下面是用VC實現的一個DLL,其功能就是接收並記錄紅外訊號的持續時間。核心程式碼如下:
DWORD WINAPI ThreadProc(LPVOID pParam)
{
LARGE_INTEGER litmp;
LONGLONG QPart1,QPart2;
double dfFreq;
int iTime=0;//微秒
// 獲得計數器的時鐘頻率
QueryPerformanceFrequency(&litmp);
dfFreq = (double)1000000.0/litmp.QuadPart;
DWORD ModemState,oldModemState=MS_DSR_ON;
//EV_BREAK or EV_CTS or EV_DSR or EV_ERR or EV_RING or EV_RLSD or EV_RXCHAR or EV_RXFLAG or EV_TXEMPTY
//SetCommMask(HSC_COM_Handle,EV_DSR);
//DWORD EvtMask,dwError;
//COMSTAT cs;
while(HSC_Thread_RunFlag)
{
//等待DSR訊號發生變化
//WaitCommEvent(HSC_COM_Handle,&EvtMask,&HSC_Ovread);
//ClearCommError(HSC_COM_Handle,&dwError,&cs);
//獲得DSR的狀態
GetCommModemStatus(HSC_COM_Handle,&ModemState);
ModemState = (ModemState & MS_DSR_ON);
if(ModemState == oldModemState)continue;
oldModemState=ModemState;
//清計數
InterlockedExchange(&HSC_NUM,0);
//開始接收資料
if(HSC_State == 0 && ModemState == 0)
{
QueryPerformanceCounter(&litmp);
QPart1 = litmp.QuadPart;
HSC_State=1;
//復位計數
InterlockedExchange(&HSC_NUM,0);
InterlockedExchange(&HSC_Index,0);
//開啟定時器
HSC_TimerID = timeSetEvent(10,HSC_Accuracy,MMTimer,NULL,TIME_PERIODIC);
continue;
}
//接收資料狀態
if(HSC_State == 1)
{
QueryPerformanceCounter(&litmp);
QPart2 = litmp.QuadPart;
//--
if(ModemState == 0)
{
iTime = (int)((QPart2-QPart1)*dfFreq);
}
else
{
iTime = (int)((QPart1-QPart2)*dfFreq);
}
if(HSC_Index < HSC_BufferSize)
*(HSC_Buffer+HSC_Index) = iTime;
InterlockedIncrement(&HSC_Index);
//--
QPart1=QPart2;
}
}
return STILL_ACTIVE;
}
如果採用WaitCommEvent函式,你會發現CPU使用時間會很低,不過它會讓接收程式無法正常退出,所以只好註釋掉該函數了,此時你會發現CPU使用時間會很高。
原始資料一旦採集完畢,剩下的就由C#程式大顯身手吧。
C#中DLL的介面函式如下:
const string DllPath = @"YFHSCollect.dll";
[DllImport(DllPath)]
public static extern Int32 HSCStart(Int32 COM, Int32 delay, Int32 BufferSize);
[DllImport(DllPath)]
public static extern Int32 HSCEnd();
[DllImport(DllPath)]
public static extern Int32 HSCData(int[] intData);
我封裝了一個類,一旦有按鍵資訊,就會觸發一個Click事件。此外程式還具備自學習功能,並且可以把學習後的結果序列化到一個XML檔案中去,這樣下次再按鍵就可以識別出鍵名了。
主程式中測試程式碼如下:
public partial class frmMain : Form
{
YFHWCollect hw =null;
int[] hwData = null;
public frmMain()
{
InitializeComponent();
hw = new YFHWCollect(this, 1);
hw.Click += new YFHWCollect.HWEventHandler(hw_Click);
}
void hw_Click(object sender, HWEventArgs e)
{
string strInfo = "";
for (int i = 0; i < e.lstData.Count; i++)
{
for (int j = 0; j < e.lstData[i].Length; j++)
{
strInfo += e.lstData[i][j].ToString() + " ";
}
strInfo += "/r/n";
}
txtInfo.Text = strInfo;
lblKeyName.Text = e.KeyName+ " (" + (e.Interval /10).ToString() + "ms)";
hwData = e.Data;
picBar.Refresh();
}
private void btnCommand_Click(object sender, EventArgs e)
{
if (btnCommand.Text == "開始")
{
btnCommand.Text = "停止";
hw.Start();
}
else
{
btnCommand.Text = "開始";
hw.End();
}
}
private void btnStudy_Click(object sender, EventArgs e)
{
hw.Study(txtKeyName.Text);
}
private void picBar_Paint(object sender, PaintEventArgs e)
{
int width = picBar.Width, height = picBar.Height;
e.Graphics.DrawLine(new Pen(Color.Gray), 0, height / 2, width, height / 2);
if (hwData != null)
{
float Len=0;
foreach(int l in hwData)
{
Len+=l;
}
float dx = width / Len,DX=0;
Pen p = new Pen(Color.Green);
float Y=0, Y1=height/4,Y2=(float)(height*3.0/4.0);
float X=0;
for(int i=0;i<hwData.Length;i++)
{
Y = ((i % 2)==0 ? Y2:Y1);
DX = hwData[i] * dx;
e.Graphics.DrawLine(p, X, Y, X + DX, Y);
X += DX;
e.Graphics.DrawLine(p, X, Y1, X, Y2);
}
}
}
}
測試程式執行結果如下:
(上面顯示的資料為高電平和低電平的持續時間(低高低高…),單位為1/10毫秒)
注意事項:
1、紅外遙控器按鍵偶數次和奇數次的編碼是不同的,程式需要學習兩次,才能正常識別按鍵資訊。
2、普通的USB轉串列埠由於僅連線了2、3、5管腳,所以不能正常使用,對比較好的USB轉串列埠(比如Moxa的三百多一根),雖然所有的管腳都引出了,但是由於是通過USB轉換的,所以響應時間很是問題,我就因為這個差一點功虧一簣,幸好把程式又在PC機跑了一遍。
//獲得DSR的狀態
GetCommModemStatus(HSC_COM_Handle,&ModemState);
上面的指令如果採用的是USB轉串列埠,執行時間會是7ms左右,而用主機板自帶串列埠僅是幾個微秒,相差實在太大了。所以上面的紅外接收器程式在沒有自帶串列埠的筆記本上是無法正常工作的。