pechika 发表于 2011-2-16 17:44:29

【参赛作品】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 编辑 ]

pechika 发表于 2011-2-16 18:20:39

软件部分

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

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


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



我用Inno Setup把软件打包成了安装文件,如图,很袖珍的,才1.51MB

安装完毕后,安装目录里的文件有这些:


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

然后就可以使用了。

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


因为是软解,这套DIY还可以变通把红外遥控的码显示出来,但是只限于NEC协议。

pechika 发表于 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

社建电子 发表于 2011-2-16 19:11:49

不错,实用。

硝化甘油 发表于 2011-2-16 19:17:56

高手~~~小弟刚接触单片机~:)

风雨123 发表于 2011-2-16 19:54:49

电脑高手,:victory:

wdele 发表于 2011-2-16 22:26:03

软硬兼施的人才不多!:victory:

hawkins 发表于 2011-2-17 10:06:26

很好的主意--ideas for life

[ 本帖最后由 hawkins 于 2011-2-17 10:08 编辑 ]

独孤飞戈 发表于 2011-3-10 13:56:18

很羡慕,但看不懂:L

收听者 发表于 2011-3-16 22:15:40

高手,所有都自己干
网上有简单的

randy01 发表于 2011-3-17 11:42:29

软硬兼施,楼长的评价十分中肯啊,高手!

walter 发表于 2011-4-22 15:54:29

用在HTPC上应该很方便吧,省了鼠标键盘了

boywc 发表于 2011-4-22 19:23:59

电脑单片高手

chenqw8 发表于 2013-3-22 14:32:41

软件高手,我不会写上位机,要是能写上位机就能做很多好玩的东西
页: [1]
查看完整版本: 【参赛作品】DIY电脑遥控器