矿石收音机论坛

 找回密码
 加入会员

QQ登录

只需一步,快速开始

搜索
查看: 10451|回复: 13

【参赛作品】DIY电脑遥控器

 关闭 [复制链接]
发表于 2011-2-16 17:44:29 | 显示全部楼层 |阅读模式
现在能用来在远距离无线操作电脑的设备种类很多,从无线键盘、鼠标到蓝牙、红外,不一而足。虽然红外的方式遥控功能有限,但是在诸如调整音量、播放音视频等方面可以一键到位,在方便性上有着其它几种控制方式不可取代的优越性,所以自己动手DIY一下,既可以过过DIY瘾,又可以用到实际中,一举两得。

思路上,就是把遵循某种遥控协议(以NEC的居多,这个DIY就按照这个来)的编码解码后,获知是遥控器的哪一个按键动作,然后用它作为控制信号完成相应的电脑动作的过程。

在方案上,就我的了解,有如下几种:
RS232电平转换→(串口)→上位机软解+上位机软件
MCU软解+MCU用固件实现USB协议→(USB接口)→上位机软件
MCU软解+USB芯片→(USB接口)→上位机软件
带MCU的USB芯片(完成软解及USB协议)→(USB接口)→上位机软件

考虑到电脑的串口基本上闲置不做他用,还有后几种方案需要专用的芯片,成本比较高,因此选择了第一种串口的方案。其实不管用哪种,这个DIY的硬件电路都比较简单,需要下功夫的是相应的软件部分。下图是串口方案的硬件电路图。
circuit.gif
图中,串口的RTS、DTR经整流稳压后,得到+5V电压,给红外接收和电平转换芯片MAX3232供电。红外信号经9014反相放大,再经MAX3232进行电平变换后,由RXD传输到电脑串口。串口设置为:波特率9600bps,8位数据位,无校验,1个停止位。

这里想着重说明一下为什么需要反相。
protocol.gif
这是NEC协议中发射端逻辑1和逻辑0的编码格式。它用的是脉宽编码。接收端输出的信号和发射端是反相的,再经三极管反相,也就是电路图中的SIG信号,与发射端又变为同相的。想要上位机能实现软解,就需要从逻辑1和逻辑0经过脉宽编码后的时序特征再结合串口协议中规定的帧格式来入手。
frame.gif
上图是串口协议的帧格式。它以下降沿为起始。这样一来,在电脑看来,就认为逻辑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键的超薄型,简洁至上,其实配和强大的软件,可以完成的事情还是不少的。
DSC00014.JPG

遥控接收器的外壳选用的是市场上比较容易买到的那种:
DSC00013.JPG

这是定做的PCB电路板:
DSC0001.JPG

电路比较简单,三下五除二就可以完成焊接工作了。
DSC00011.JPG
DSC00012.JPG

接下来是安装串口数据线,一端连PCB,一端连串口母头。PCB那端焊接后,再用热熔胶固定。
DSC00015.JPG
DSC00016.JPG
DSC00017.JPG

好了,最后把PCB装在壳子里,再拧紧后面的4颗螺丝即可。串口插头那边用502胶粘在外面的塑料壳上。
DSC00018.JPG
DSC00019.JPG

到此,硬件部分制作完毕。

[ 本帖最后由 pechika 于 2011-2-16 19:56 编辑 ]
 楼主| 发表于 2011-2-16 18:20:39 | 显示全部楼层

软件部分

软件用的编译器是PureBasic,呵呵,这个在国内似乎用的人不多。我一路从VB、VC用过来的,发现这个兼有两者的优点,带的库函数又多,进行规模不大的小型开发非常适合。

开发使用纯Win32 API,因此这个软件编译后体积很小,效率也高。不过因为所有的都需要自己来,代码量很可观,如图,最后联编时显示的代码快到1万行了。
compiler.gif

软件在界面和功能方面下了不小功夫。为了界面美观,使用了PNG图片贴图实现绚丽的效果。功能上,可以模拟鼠标、键盘,还可以用来完成音量、关机休眠等常用的电脑操作,此外,最有特色的部分要算OSD菜单了,灵感来自于电视机遥控的菜单。有了OSD,可以拓展遥控功能,更直观地遥控电脑。
snap5.jpg
snap6.jpg

我用Inno Setup把软件打包成了安装文件,如图,很袖珍的,才1.51MB
setup.jpg
安装完毕后,安装目录里的文件有这些:
folder.jpg

把遥控接收器插到电脑后面的串口上,然后双击“AeroPrism”即可启动上位机软件。启动后,需要设置好串口号,我的是COM1。
com11.jpg
然后就可以使用了。

通过切换遥控器模式实现不同的功能。比如快捷运行,只要把快捷方式放到“Shortcuts”文件夹下,通过OSD就可以快速运行电脑上的任何文件了。活用这个功能,在听歌、看影片、看照片的时候就会很方便了。
shortcut.jpg

因为是软解,这套DIY还可以变通把红外遥控的码显示出来,但是只限于NEC协议。
code.jpg
回复 支持 反对

使用道具 举报

 楼主| 发表于 2011-2-16 18:53:48 | 显示全部楼层

上位机解码部分代码

;==========================================================
;                     红外码串口接收
;
;==========================================================
;串口设备的缓存推荐值(系统用,和软件中的是两回事)
#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

评分

1

查看全部评分

回复 支持 反对

使用道具 举报

发表于 2011-2-16 19:11:49 | 显示全部楼层
不错,实用。
回复 支持 反对

使用道具 举报

     
发表于 2011-2-16 19:17:56 | 显示全部楼层
高手~~~小弟刚接触单片机~
回复 支持 反对

使用道具 举报

发表于 2011-2-16 19:54:49 | 显示全部楼层
电脑高手,
回复 支持 反对

使用道具 举报

     
发表于 2011-2-16 22:26:03 | 显示全部楼层
软硬兼施的人才不多!
回复 支持 反对

使用道具 举报

     
发表于 2011-2-17 10:06:26 | 显示全部楼层
很好的主意--ideas for life

[ 本帖最后由 hawkins 于 2011-2-17 10:08 编辑 ]
回复 支持 反对

使用道具 举报

发表于 2011-3-10 13:56:18 | 显示全部楼层
很羡慕,但看不懂
回复 支持 反对

使用道具 举报

发表于 2011-3-16 22:15:40 | 显示全部楼层
高手,所有都自己干
网上有简单的
回复 支持 反对

使用道具 举报

发表于 2011-3-17 11:42:29 | 显示全部楼层
软硬兼施,楼长的评价十分中肯啊,高手!
回复 支持 反对

使用道具 举报

     
发表于 2011-4-22 15:54:29 | 显示全部楼层
用在HTPC上应该很方便吧,省了鼠标键盘了
回复 支持 反对

使用道具 举报

     
发表于 2011-4-22 19:23:59 | 显示全部楼层
电脑单片高手
回复 支持 反对

使用道具 举报

     
发表于 2013-3-22 14:32:41 | 显示全部楼层
软件高手,我不会写上位机,要是能写上位机就能做很多好玩的东西
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 加入会员

本版积分规则

小黑屋|手机版|矿石收音机 ( 蒙ICP备05000029号-1 )

蒙公网安备 15040402000005号

GMT+8, 2024-4-25 18:29

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表