【参赛作品】DIY电脑遥控器
现在能用来在远距离无线操作电脑的设备种类很多,从无线键盘、鼠标到蓝牙、红外,不一而足。虽然红外的方式遥控功能有限,但是在诸如调整音量、播放音视频等方面可以一键到位,在方便性上有着其它几种控制方式不可取代的优越性,所以自己动手DIY一下,既可以过过DIY瘾,又可以用到实际中,一举两得。思路上,就是把遵循某种遥控协议(以NEC的居多,这个DIY就按照这个来)的编码解码后,获知是遥控器的哪一个按键动作,然后用它作为控制信号完成相应的电脑动作的过程。
在方案上,就我的了解,有如下几种:
RS232电平转换→(串口)→上位机软解+上位机软件
MCU软解+MCU用固件实现USB协议→(USB接口)→上位机软件
MCU软解+USB芯片→(USB接口)→上位机软件
带MCU的USB芯片(完成软解及USB协议)→(USB接口)→上位机软件
考虑到电脑的串口基本上闲置不做他用,还有后几种方案需要专用的芯片,成本比较高,因此选择了第一种串口的方案。其实不管用哪种,这个DIY的硬件电路都比较简单,需要下功夫的是相应的软件部分。下图是串口方案的硬件电路图。
图中,串口的RTS、DTR经整流稳压后,得到+5V电压,给红外接收和电平转换芯片MAX3232供电。红外信号经9014反相放大,再经MAX3232进行电平变换后,由RXD传输到电脑串口。串口设置为:波特率9600bps,8位数据位,无校验,1个停止位。
这里想着重说明一下为什么需要反相。
这是NEC协议中发射端逻辑1和逻辑0的编码格式。它用的是脉宽编码。接收端输出的信号和发射端是反相的,再经三极管反相,也就是电路图中的SIG信号,与发射端又变为同相的。想要上位机能实现软解,就需要从逻辑1和逻辑0经过脉宽编码后的时序特征再结合串口协议中规定的帧格式来入手。
上图是串口协议的帧格式。它以下降沿为起始。这样一来,在电脑看来,就认为逻辑1和逻辑0的560μs后的下降沿是一帧的开始了。对于9600bps的波特率、8位数据位、无校验、1个停止位的设置,一帧的时间约是1.04ms,这样若是逻辑1,因为低电平持续时间是2.25-0.56=1.69ms>1.04ms,串口就认为接收到的数据都是0;若是逻辑0,低电平持续时间是1.12-0.56=0.56ms<1.04ms,串口在一帧开始后的前0.56ms接收到的是低电平,这个时间对于9600bps波特率,约发送了5.376bit的数据,除去起始位再计入误差,串口认为接收到了4或5位的0,后面接收到的是遥控器发送的下一编码的前半部分!不管这个码是什么,根据协议,它的前半部分总是高电平,所以串口收到的全为1。
这样一来,红外编码的逻辑1,串口接收到的是0x00;逻辑0,接收到的是0xe0或0xf0(串口协议低位在前)。红外编码的起始部分可以按照上面思路分析,接收到的是0x00,此外,结束部分也会被认为接收到一个0x00。所以对于首发码一共接收到34字节数据,连发码是2字节。
前面理论部分比较多,下面进入DIY部分。
遥控器选用的是7键的超薄型,简洁至上,其实配和强大的软件,可以完成的事情还是不少的。
遥控接收器的外壳选用的是市场上比较容易买到的那种:
这是定做的PCB电路板:
电路比较简单,三下五除二就可以完成焊接工作了。
接下来是安装串口数据线,一端连PCB,一端连串口母头。PCB那端焊接后,再用热熔胶固定。
好了,最后把PCB装在壳子里,再拧紧后面的4颗螺丝即可。串口插头那边用502胶粘在外面的塑料壳上。
到此,硬件部分制作完毕。
[ 本帖最后由 pechika 于 2011-2-16 19:56 编辑 ]
软件部分
软件用的编译器是PureBasic,呵呵,这个在国内似乎用的人不多。我一路从VB、VC用过来的,发现这个兼有两者的优点,带的库函数又多,进行规模不大的小型开发非常适合。开发使用纯Win32 API,因此这个软件编译后体积很小,效率也高。不过因为所有的都需要自己来,代码量很可观,如图,最后联编时显示的代码快到1万行了。:(
软件在界面和功能方面下了不小功夫。为了界面美观,使用了PNG图片贴图实现绚丽的效果。功能上,可以模拟鼠标、键盘,还可以用来完成音量、关机休眠等常用的电脑操作,此外,最有特色的部分要算OSD菜单了,灵感来自于电视机遥控的菜单。有了OSD,可以拓展遥控功能,更直观地遥控电脑。
我用Inno Setup把软件打包成了安装文件,如图,很袖珍的,才1.51MB
安装完毕后,安装目录里的文件有这些:
把遥控接收器插到电脑后面的串口上,然后双击“AeroPrism”即可启动上位机软件。启动后,需要设置好串口号,我的是COM1。
然后就可以使用了。
通过切换遥控器模式实现不同的功能。比如快捷运行,只要把快捷方式放到“Shortcuts”文件夹下,通过OSD就可以快速运行电脑上的任何文件了。活用这个功能,在听歌、看影片、看照片的时候就会很方便了。
因为是软解,这套DIY还可以变通把红外遥控的码显示出来,但是只限于NEC协议。
上位机解码部分代码
;==========================================================; 红外码串口接收
;
;==========================================================
;串口设备的缓存推荐值(系统用,和软件中的是两回事)
#MAX_WRITE_BUFFER = 1024
#MAX_READ_BUFFER= 1024
#EVENT_CHAR = $ff ;用来产生事件的字符(ANSI only, 1 Byte)
#FRAME_DURATION = 108;遥控器每帧数据时间(ms)
#MARGIN_OF_ERROR = 10;误差(ms)
;在何时接收到什么键码
Structure WhenWhatKey
key_code.l ;键码
dwTimeFirst.l;接收到最初的32位码的时间
dwTime.l ;接收时间
fBurst.l ;是否连发码
EndStructure
Global m_hCommPort.l ;串口设备句柄
Global m_timeouts_orig.COMMTIMEOUTS ;保存系统默认的串口超时值
Global m_dwEventFlags.l ;用于 WaitCommEvent 的串口事件
Global m_hEventReadExit.l ;读线程退出内核事件
Global m_hReadThread.l ;读线程句柄
;函数声明
Declare Read_StatusThread(*value)
Declare KeyCodePump(*pBuffer, cbData.l)
Declare PortEventHandler(dwCommEvent.l)
;串口设置字串
DataSection
DCS:
Data.s "baud=9600 parity=N data=8 stop=1"
EndDataSection
; 试一下串口端口是否可用
Procedure.i IsPortAvailable(nPort.l)
Dim sPortString.c(8)
*p = @sPortString()
CopyMemoryString(@"COM", @*p)
CopyMemoryString(Str(nPort))
hComm.l = CreateFile_(sPortString(), #GENERIC_READ, 0, #Null, #OPEN_EXISTING, #FILE_ATTRIBUTE_NORMAL | #FILE_FLAG_OVERLAPPED, #Null)
If hComm = #INVALID_HANDLE_VALUE
ProcedureReturn #False
Else
CloseHandle_(hComm)
ProcedureReturn #True
EndIf
EndProcedure
Procedure ConfigurePort(hComm.l)
;设置 DCB
dcb.DCB
GetCommState_(hComm, @dcb)
sSettings.s
Restore DCS
Read.s sSettings
BuildCommDCB_(@sSettings, @dcb) ;波特率、奇偶校验、停止位
dcb\EvtChar = #EVENT_CHAR ;事件字符
dcb\fbits | (1 << 1) ;奇偶校验
SetCommState_(hComm, @dcb)
;设置超时参数
GetCommTimeouts_(hComm, @m_timeouts_orig) ;先保存原来的
ct.COMMTIMEOUTS
With ct
\ReadIntervalTimeout = 20
\ReadTotalTimeoutMultiplier = 0
\ReadTotalTimeoutConstant = 0
EndWith
SetCommTimeouts_(hComm, @ct)
;设置缓存大小
SetupComm_(hComm, #MAX_READ_BUFFER, #MAX_WRITE_BUFFER)
EndProcedure
Procedure SetPortEvent(dwEvent.l)
m_dwEventFlags = dwEvent
EndProcedure
Procedure StartupComm(nPort.l)
If GetFileType_(m_hCommPort) = #FILE_TYPE_CHAR
ProcedureReturn
EndIf
;打开串口设备
Dim sPortString.c(8)
*p = @sPortString()
CopyMemoryString(@"COM", @*p)
CopyMemoryString(Str(nPort))
m_hCommPort = CreateFile_(sPortString(), #GENERIC_READ, 0, #Null, #OPEN_EXISTING, #FILE_ATTRIBUTE_NORMAL | #FILE_FLAG_OVERLAPPED, #Null)
If m_hCommPort = #INVALID_HANDLE_VALUE
ProcedureReturn
EndIf
;配置串口参数
ConfigurePort(m_hCommPort)
;设置 WaitCommEvent 的串口事件
SetPortEvent(#EV_ERR)
;建立线程退出事件
m_hEventReadExit = CreateEvent_(#Null, 0, 0, #Null)
;启动读线程
m_hReadThread = ThreadID(CreateThread(@Read_StatusThread(), 0))
SetThreadPriority_(m_hReadThread, #THREAD_PRIORITY_HIGHEST)
EndProcedure
Procedure BreakdownComm()
If GetFileType_(m_hCommPort) <> #FILE_TYPE_CHAR
ProcedureReturn
EndIf
;结束读线程
SetEvent_(m_hEventReadExit)
WaitForSingleObject_(m_hReadThread, #INFINITE)
CloseHandle_(m_hEventReadExit)
CloseHandle_(m_hReadThread)
;还原超时参数
SetCommTimeouts_(m_hCommPort, @m_timeouts_orig)
CloseHandle_(m_hCommPort)
m_hCommPort = #Null
EndProcedure
; 改变串口设备
Procedure AlterComm(nPort.l)
;打开新串口
Dim sPortString.c(8)
*p = @sPortString()
CopyMemoryString(@"COM", @*p)
CopyMemoryString(Str(nPort))
hComm.l = CreateFile_(sPortString(), #GENERIC_READ, 0, #Null, #OPEN_EXISTING, #FILE_ATTRIBUTE_NORMAL | #FILE_FLAG_OVERLAPPED, #Null)
If hComm = #INVALID_HANDLE_VALUE
ProcedureReturn
EndIf
;关闭旧串口
If GetFileType_(m_hCommPort) = #FILE_TYPE_CHAR
;结束读线程
SetEvent_(m_hEventReadExit)
WaitForSingleObject_(m_hReadThread, #INFINITE)
CloseHandle_(m_hReadThread)
;还原超时参数
SetCommTimeouts_(m_hCommPort, @m_timeouts_orig)
;关闭内核文件对象
CloseHandle_(m_hCommPort)
m_hCommPort = #Null
Else ;之前没有串口设备打开,需要建立相关同步对象
;建立线程退出事件
m_hEventReadExit = CreateEvent_(#Null, 0, 0, #Null)
EndIf
m_hCommPort = hComm
;配置新串口
ConfigurePort(m_hCommPort)
;设置 WaitCommEvent 的串口事件
SetPortEvent(#EV_ERR)
;启动读线程
m_hReadThread = ThreadID(CreateThread(@Read_StatusThread(), 0))
SetThreadPriority_(m_hReadThread, #THREAD_PRIORITY_HIGHEST)
EndProcedure
; 串口读与串口事件线程
Procedure Read_StatusThread(*value)
#READ_STATUS_CHECK_TIMEOUT = #INFINITE
#BYTES_OF_READ_BUFFER = 40
Define.l cbRead, cbDummy
dwWaitRst.l
Define.l dwCommEvent, dwStoredEvtFlags = ~0
Define.i fThreadDone = #False, fWaitingOnRead = #False, fWaitingOnStat = #False
; 建立接收缓存
Dim buffer.a(#BYTES_OF_READ_BUFFER - 1)
; 建立内核对象
Define.OVERLAPPED osReader, osStatus
osReader\hEvent = CreateEvent_(#Null, 1, 0, #Null)
osStatus\hEvent = CreateEvent_(#Null, 1, 0, #Null)
; 需要侦测下面几个事件:
; Read event
; Status event
; Thread exit event
;
Dim hArray.l(2)
hArray(0) = osReader\hEvent
hArray(1) = osStatus\hEvent
hArray(2) = m_hEventReadExit
While Not fThreadDone
; 若无读操作处于等待中,新建一个读操作
If Not fWaitingOnRead
If Not ReadFile_(m_hCommPort, buffer(), #BYTES_OF_READ_BUFFER, @cbRead, @osReader)
If GetLastError_() <> #ERROR_IO_PENDING
; 出错了
EndIf
fWaitingOnRead = #True
Else ; 读操作立即完成
If cbRead <> 0
KeyCodePump(buffer(), cbRead)
EndIf
EndIf
EndIf
;更新 WaitCommEvent 的事件
If dwStoredEvtFlags <> m_dwEventFlags
dwStoredEvtFlags = m_dwEventFlags
SetCommMask_(m_hCommPort, dwStoredEvtFlags)
EndIf
; 若无串口事件处于等待中,建立串口事件等待
If Not fWaitingOnStat
If Not WaitCommEvent_(m_hCommPort, @dwCommEvent, @osStatus)
If GetLastError_() <> #ERROR_IO_PENDING
; 出错了
Else
fWaitingOnStat = #True
EndIf
Else ; WaitCommEvent 立即返回
PortEventHandler(dwCommEvent)
EndIf
EndIf
;等待未决操作完成
If fWaitingOnStat And fWaitingOnRead
dwWaitRst = WaitForMultipleObjects_(3, hArray(), 0, #READ_STATUS_CHECK_TIMEOUT)
Select dwWaitRst
Case #WAIT_OBJECT_0 ; 读操作结束
If Not GetOverlappedResult_(m_hCommPort, @osReader, @cbRead, 0)
If GetLastError_() = #ERROR_OPERATION_ABORTED
; 读操作放弃
Else
; 出错了
EndIf
Else ;读操作成功完成
If cbRead <> 0
KeyCodePump(buffer(), cbRead)
EndIf
EndIf
fWaitingOnRead = #False
Case #WAIT_OBJECT_0 + 1 ;有串口事件
If Not GetOverlappedResult_(m_hCommPort, @osStatus, @cbDummy, 0)
If GetLastError_() = #ERROR_OPERATION_ABORTED
; WaitCommEvent 放弃
Else
; 出错了
EndIf
Else; 成功
PortEventHandler(dwCommEvent) ;WaitCommEvent 仍会填充 dwCommEvent
EndIf
fWaitingOnStat = #False
Case #WAIT_OBJECT_0 + 2 ; 线程退出
GetOverlappedResult_(m_hCommPort, @osReader, @cbRead, #False)
PurgeComm_(m_hCommPort, #PURGE_RXABORT | #PURGE_RXCLEAR) ;停止异步读取(线程退出后,接收缓存就不存在了)
fThreadDone = #True
Case #WAIT_TIMEOUT ; 超时
;
; 用这个机会检查串口状态、清除错误等
;
EndSelect
EndIf
Wend
; 释放内核对象
CloseHandle_(osReader\hEvent)
CloseHandle_(osStatus\hEvent)
EndProcedure
;响应串口事件
Procedure PortEventHandler(dwCommEvent.l)
Define.l fCTS, fDSR, fERR, fRING, fRLSD, fBREAK, fRXCHAR, fRXFLAG, fTXEMPTY
fCTS = #EV_CTS & dwCommEvent
fDSR = #EV_DSR & dwCommEvent
fERR = #EV_ERR & dwCommEvent
fRING = #EV_RING & dwCommEvent
fRLSD = #EV_RLSD & dwCommEvent
fBREAK = #EV_BREAK & dwCommEvent
fRXCHAR = #EV_RXCHAR & dwCommEvent
fRXFLAG = #EV_RXFLAG & dwCommEvent
fTXEMPTY = #EV_TXEMPTY & dwCommEvent
If fERR ;有错误发生
dwErrs.l
cs.COMSTAT
ClearCommError_(m_hCommPort, @dwErrs, @cs)
EndIf
EndProcedure
;把收到的数据取出,检验合法性,投入队列
Procedure KeyCodePump(*pBuffer, cbData.l)
Static s_previous_key.WhenWhatKey ;之前接收到的键码
Static Dim s_buffer.a(33) ;按键数据缓存
Static s_offset.l ;偏移
ks.KeyInstruction
;获得当前时间
dwTime.l = timeGetTime_()
;把收到的数据放到缓存中
If s_offset + cbData <= 34
CopyMemory(*pBuffer, @s_buffer() + s_offset, cbData)
s_offset + cbData
Else;之前接收过乱码?
If cbData = 34;丢弃之前的,本次的算作一次接收
CopyMemory(*pBuffer, @s_buffer(), 34)
s_offset = 34
Else
s_offset = 0
FillMemory(@s_previous_key, SizeOf(WhenWhatKey), 0)
ProcedureReturn
EndIf
EndIf
Select s_offset
Case 2;连发码
If PeekU(s_buffer()) = 0 And dwTime - s_previous_key\dwTime <= #FRAME_DURATION + #MARGIN_OF_ERROR ;有效的连发码
s_previous_key\dwTime = dwTime
s_previous_key\fBurst = 1
If dwTime - s_previous_key\dwTimeFirst >= g_kb_delay
ks\key_code = s_previous_key\key_code
ks\fStatus = #KS_BURST
EnterCriticalSection_(@g_csQKI)
EnterQueue(@g_queueKeyIns, @ks)
LeaveCriticalSection_(@g_csQKI)
SetEvent_(g_hEventKeySignal)
EndIf
EndIf
s_offset = 0
Case 34;键码
Dim key_code.a(3);a(3) -- ID7 ~ ID0
;a(2) -- ID7 ~ ID0 (NOT)
;a(1) -- FUN7 ~ FUN0
;a(0) -- FUN7 ~ FUN0 (NOT)
;红外码低位在前发送
Define.i i, j, k
If s_buffer(0) = 0 And s_buffer(33) = 0
k = 1
For i = 3 To 0 Step -1
Select s_buffer(k)
Case $0 ;逻辑1
key_code(i) | $80
Case $e0, $f0;逻辑0
key_code(i) | $0
Default ;按0处理
key_code(i) | $0
EndSelect
k + 1
For j = 1 To 7
key_code(i) >> 1
Select s_buffer(k)
Case $0 ;逻辑1
key_code(i) | $80
Case $e0, $f0;逻辑0
key_code(i) | $0
Default ;按0处理
key_code(i) | $0
EndSelect
k + 1
Next j
Next i
If key_code(0) ! key_code(1) = $ff ;And key_code(2) ! key_code(3) = $ff;有效键码
If s_previous_key\fBurst = 0 And s_previous_key\key_code = PeekL(key_code()) And dwTime - s_previous_key\dwTime <= g_dbclick_time;认为是双击
ks\fStatus = #KS_DBCLICK
Else
s_previous_key\key_code = PeekL(key_code())
s_previous_key\fBurst = 0
ks\fStatus = #KS_PRESSED
EndIf
s_previous_key\dwTime = dwTime
s_previous_key\dwTimeFirst = dwTime
ks\key_code = PeekL(key_code())
EnterCriticalSection_(@g_csQKI)
EnterQueue(@g_queueKeyIns, @ks)
LeaveCriticalSection_(@g_csQKI)
SetEvent_(g_hEventKeySignal)
PostThreadMessage_(g_idMainThread, #TM_IRKEYCODE, ks\key_code, 0)
EndIf
EndIf
s_offset = 0
EndSelect
EndProcedure 不错,实用。 高手~~~小弟刚接触单片机~:) 电脑高手,:victory: 软硬兼施的人才不多!:victory: 很好的主意--ideas for life
[ 本帖最后由 hawkins 于 2011-2-17 10:08 编辑 ] 很羡慕,但看不懂:L 高手,所有都自己干
网上有简单的 软硬兼施,楼长的评价十分中肯啊,高手! 用在HTPC上应该很方便吧,省了鼠标键盘了 电脑单片高手 软件高手,我不会写上位机,要是能写上位机就能做很多好玩的东西
页:
[1]