摘要: 本文介绍了在Microsoft Visual C++6.0环境下对RS-232-C串行端口进行编程,以及对后台监控程序所普遍涉及到的无阻塞后台运行、数据的实时接收和处理等问题的解决方法。 一、 引言 在实验室和工业应用中,受信道成本限制,串口常常作为计算机与外部串行设备之间的首选数据传输通道,而且由于串行通信方便易行,许多设备和计算机都可以通过串口对外设进行控制、检测,串口通讯日益成为计算机和外设进行通讯、获取由外设采集到的监测数据的一个非常重要的手段。本文所描述的程序实例运行于Windows 9x操作系统下,可后台运行、实时接收、处理从端口传来的数据,并能通过向串口发送命令来控制外设的动作。为了避免在实时监控数据时引发程序阻塞,在本程序中引入了线程和端口中断响应等技术。 二、 程序设计思路 由于本程序要对串行端口进行实时监控,这就要求它是一个后台程序,在监控的同时可以在前台进行其他一些于之无关的操作。因而在实现时即要避免无时无刻都在反复读端口的效率低下的轮询方式,又不能因为来不及处理而将突然到达的监测数据丢失。只有采取端口中断的异步方式才能实现高效、安全的监控过程,只要一有数据到达端口,马上抛出中断请求,中断处理函数便会及时启动以处理到来的数据,从而避免了轮询间隙丢时数据的可能。而在大部分无数据到达的时间内不会有中断抛出,中断处理函数也不会执行,即仅仅在有数据到达的一瞬间进行工作,此效率不可谓不高。 综上所述,要实现上述要求,就要用到下列技术来解决所遇到的关键性问题:一是采用多线程来避免在进行文件操作等耗时操作时会引发阻塞现象的发生。同时为了防止多个线程同时对同一个变量进行操作引起时序上的差错,为了保持线程的同步,还采取了临界区加解锁的技术;二是对端口的数据读取方式采取中断响应模式,具体原因前面以讲的很清楚,在此不再赘述;三是使用了定时器,以满足实时监控类程序的实时显示功能,以便及时的将所接收到的动态数据及时的反映到屏 幕上。
三、 RS-232-C串行端口监控软件的程序实现 (一) 界面风格 由于是实时监控软件,那就既要监测从外设传来的实时数据,又要通过串口向外设发送一些具体的指令以控制外设完成预先设定的动作。为了方便向串口发送命令可以在工具条上再加一个类似于"Internet Explorer 浏览器"风格的对话条,可以在初建工程时指定"Internet Explorer ReBars"风格,也可以通过添加Microsoft Visual C++ 6.0自带的"DialogBar"组件来实现。而要及时将从外部读取的数据显示给管理人员,并且留有相当记录以备查阅,可以选择列表视图来实现。 (二) 串口的参数设置及打开 对RS-232-C串行端口进行参数配置是使用串口进行通讯的必要条件。而且由于场合不同、用途、功能的不同对串口也采取不同的配置方式,为了使本程序更灵活,适应面更广,采取将所有的可能参数都预先设置在几个组合框中,可以在程序运行后随时更改设置。自定义一个设置串口参数的数据结构:
typedef struct tagCOM_CONFIG { int nPort; file://端口号,从COM1到COM4 int nBaud; file://波特率,从1200bps到57600bps(对应的宏为CBR_1200到CBR_57600) int nData; file://数据位个数,7位或是8位 int nStop; file://停止位个数,可以是1位、1.5位、2位。 int nParity;//采取的校验方式,有无校验(NOPARITY)、 file://奇校验(ODDPARITY)和偶校验(EVENPARITY)等。 }COM_CONFIG;
当选择好适当的参数后就可以根据设置好的端口配置情况打开通讯端口了。与以往DOS下串行通信程序不同的是,Windows操作平台下不提倡应用程序直接控制硬件(包括端口),也不让使用中断(除非打入到Ring0系统级),而是通过Windows操作系统提供的设备驱动程序来进行数据传递。在Windows操作系统下串行口和其他通讯端口一样是作为文件来进行处理的,而不是直接对端口进行操作,对于串行通信,Win 32 提供了相应的文件I/O函数与通信函数,通过了解这些函数的使用,可以编制出符合不同需要的通信程序。与通信设备相关的结构有COMMCONFIG ,COMMPROP,COMMTIMEOUTS,COMSTAT,DCB,MODEMDEVCAPS,MODEMSETTINGS共7个,与通信有关的Windows API函数共有26个,具体说明可参考MSDN帮助文件。下面是打开串口的部分关键代码:
//以创建文件的形式打开文件,并将返回的端口句柄保存于句柄idComDev之中。 idComDev =CreateFile( g_szCom_Port[g_com_config.nPort], GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); …… file://cfg为COMMCONFIG结构的实例对象,获取当前通讯口的状态。 cfg.dcb.DCBlength = sizeof( DCB ) ; GetCommState( idComDev, &(cfg.dcb) ) ; file://设置发送、接收缓存大小 SetupComm( idComDev, 4096, 4096 ) ; // PurgeComm()是一个清除函数,它可以用来中止任何未决的后台读或写,并且可以冲掉I/O file://缓冲区.其中:PURGE_TXABORT 用于中止后台写操作;PRUGE_RXABORT用于中止后台 file://读操作 ;PRUGE_TXCLEAR用于清除发送缓冲区;PRUGE_RXCLEAR用于清除接收缓冲区 PurgeComm(idComDev,PURGE_TXABORT|PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR); file://根据设置的参数填充DCB结构对象dcb的各个数据成员变量 dcb.DCBlength = sizeof( DCB ) ; GetCommState( idComDev, &dcb ) ; file://设置端口通讯参数 dcb.BaudRate =g_Com_Baud[g_com_config.nBaud]; dcb.ByteSize =g_Com_ByteSize[g_com_config.nData]; dcb.Parity =g_Com_Parity[g_com_config.nParity] ; dcb.StopBits =g_Com_StopBits[g_com_config.nStop]; file://硬件流控制 dcb.fDtrControl = DTR_CONTROL_DISABLE ; dcb.fOutxCtsFlow = FALSE ; dcb.fRtsControl = RTS_CONTROL_DISABLE ; file://软件流控制 dcb.fInX = dcb.fOutX = FALSE ; dcb.XonChar = (char)0xFF ; dcb.XoffChar = (char)0XFF ; dcb.XonLim = 100 ; dcb.XoffLim = 100 ; dcb.EvtChar=0x0d; dcb.fBinary = TRUE ; dcb.fParity = TRUE ; file://超时控制的设置。超时有两种:区间超时:(仅对从端口中读取数据有用)它指定在读取两个字符之间要经历的时间;总超时: 当读或写特定的字节数需要的总时间超过某一阈值时,超时触发。计算超时可以根据公式: file://ReadTotalTimeout = (ReadTotalTimeoutMultiplier * bytes_to_read)+ // ReadToTaltimeoutConstant file://WriteTotalTimeout = (WriteTotalTimeoutMuliplier * bytes_to_write)+ // WritetoTotalTimeoutConstant file://如果在设置超时时参数为0则为无限等待,即无超时。 CommTimeOuts.ReadIntervalTimeout =MAXDWORD; CommTimeOuts.ReadTotalTimeoutMultiplier =0; CommTimeOuts.ReadTotalTimeoutConstant = 0 ; CommTimeOuts.WriteTotalTimeoutMultiplier =2*9600/dcb.BaudRate ; CommTimeOuts.WriteTotalTimeoutConstant = 25 ; SetCommTimeouts(idComDev , &CommTimeOuts ) ; file://根据设置好的dcb结构设置好通讯口的状态,并开启用于侦听端口,监视从外设传来的数 file://据的线程COMReadThreadProc。 if (SetCommState( idComDev, &dcb )) { m_bComPortOpen=TRUE; g_hCom=idComDev; AfxBeginThread(COMReadThreadProc,NULL,THREAD_PRIORITY_NORMAL); return; }
(三) 侦听监视线程 当成功的打开端口之后通过执行线程开启函数AfxBeginThread(COMReadThreadProc,NULL,THREAD_PRIORITY_NORMAL);开启了一个用于侦听端口的工作线程COMReadThreadProc。其具体处理过程如下:
UINT COMReadThreadProc(LPVOID pParam) { …… file://设置读端口线程执行标志的标识 g_comthread.SetReadThreadKillFlag(FALSE); while(1) { file://读取端口开启状态的标识 if(TRUE==g_comthread.GetCloseCOMFlag()) { g_comthread.SetReadThreadKillFlag(TRUE); return 0;//正常关闭 } file://读端口操作 dwNeedRead=500; file://从端口读取数据到缓存中 if(!ReadFile(g_hCom,buf,dwNeedRead,&dwActRead,NULL)) { ClearCommError(g_hCom,&dwErrorMask,&comstat); PurgeComm(g_hCom,PURGE_RXCLEAR); continue; } file://读字符加入到全局缓冲 g_comreadbuf.Add(buf,dwActRead); Sleep(1); } …… return 0; }
其中用到的g_comthread和g_comreadbuf分别是线程类CCOMThread和读端口类COMReadBuf的实例对象。这两个类里都用类CCriticalSectionm_Lock;实现了临界区技术,用以保持线程间的同步。CCOMReadBuf类的两个函数GetOneByte(……)、Add(……)分别用于从端口读取一个字符和向缓冲区添加读取的字符。其主要实现代码如下:
BOOL CCOMReadBuf::GetOneByte(BYTE *cb) { m_Lock.Lock(); if(m_nHead==m_nTail) { m_Lock.Unlock(); return FALSE;//空 } *cb=m_readbuf[m_nTail]; if(m_nTail < m_nBufSize-1) m_nTail++; else m_nTail=0; m_Lock.Unlock(); return TRUE;//空 } void CCOMReadBuf::Add(BYTE buf[],int nBytes) { int nt,i; m_Lock.Lock(); for(i=0;i BR> { nt=(m_nHead-m_nTail); if(nt<0) nt+=m_nBufSize; if(nt+1==m_nBufSize) break;//缓冲区满 m_readbuf[m_nHead]=buf[i]; if(m_nHead < m_nBufSize-1) m_nHead++; else m_nHead=0; } m_Lock.Unlock(); }
(四) 控制命令的发送 控制命令可以从对话条上的编辑框获取,然后就可以通过写文件形式从端口发送出去,这部分实现起来较简单,也牵扯不到线程等技术。主要的代码主要有:
…… file://从对话条获取命令行 nRead=m_wndDlgBar.GetDlgItemText(IDC_EDIT_SEND,buf,500); file://向端口发送命令 if(nRead>0) { buf[nRead]=0x0d; buf[nRead+1]=0x00; ::WriteFile(g_hCom,buf,nRead+1,&dwActWrite,NULL); } ……
(五) 监测信息的显示 本程序选择了列表视图作为数据的显示途径。为了能及时的将接收到的数据反馈给监控者,在视类中通过定时器完成定时刷新的功能,可以在视类的OnCreate() 函数里用SetTimer(……)函数在程序开始执行时打开定时器,在OnDestroy()里用KillTimer(……)函数在程序退出前先关闭定时器。在定时器消息 WM_TIMER的响应函数里完成向列表控件添加最新接收到的信息。主要语句有:
…… file://获取列表视相关的列表空间的句柄 CListCtrl &ListCtrl=GetListCtrl(); file://列表有两列:收到字符的时间和对应的信息 CTime t = CTime::GetCurrentTime(); CString szTemp; szTemp.Format("%02d:%02d:%02d",t.GetHour(),t.GetMinute(),t.GetSecond()); file://向列表添加信息 int nIndex=ListCtrl.InsertItem(0, szTemp); if(-1!=nIndex) { m_Buf[m_nCurPoint]=0; ListCtrl.SetItemText(nIndex,1,LPTSTR(m_Buf)); } ……
四、 调试与检测
现在程序已经写完,可以编译运行。我们最好先检验一下机器串口是否能正常工作,可用DOS下的Comdebug程序检查。在确认串口工作正常后,如果条件允许最好同另一台计算机或外设相连,进行检测,如笔者用的是一台高频段数传电台。如果只有一台计算机也可以进行简单的测试:将计算机串口的第2脚和第3脚短接,即自己发送、接收数据。如果接有外设,当有采集到的数据送到端口时就会在列表中将时间和信息内容记录下来,也可以在对话条中输入命令来控制外设的工作状态,完全具备实时监控软件所需的功能。
小结:
串行通讯在通讯领域被广泛应用,标准的RS-232-C接口已成为计算机、外设、交换机和许多通讯设备的标准接口。计算机与计算机、计算机与外设等都可以通过RS-232-C接口进行方便的连接,以实现监视、控制外设和传输数据等目的。对于其他类型的串口通讯程序本文所介绍的方法也是值得借鉴的。本程序由
Microsoft Visual C++ 6.0编译、在Windows 98下运行通过。