实际工作中利用串口完成通信任务的时候非常之多。已有一些文章介绍串口编程的文章在计算机杂志上发表。但总的感觉说来不太全面,特别是介绍32位下编程的更少,且很不详细。笔者在实际工作中积累了较多经验,结合硬件、软件,重点提及比较新的技术,及需要注意的要点作一番探讨。希望对各位需要编写串口通信程序的朋友有一些帮助。
一.串行通信的基本原理
串行端口的本质功能是作为CPU和串行设备间的编码转换器。当数据从 CPU经过串行端口发送出去时,字节数据转换为串行的位。在接收数据时,串行的位被转换为字节数据。
在Windows环境(Windows NT、Win98、Windows2000)下,串口是系统资源的一部分。
应用程序要使用串口进行通信,必须在使用之前向操作系统提出资源申请要求(打开串口),通信完成后必须释放资源(关闭串口)。
串口通信程序的流程如下图:
二.串口信号线的接法
一个完整的RS-232C接口有22根线,采用标准的25芯插头座(或者9芯插头座)。25芯和9芯的主要信号线相同。以下的介绍是以25芯的RS-232C为例。
①主要信号线定义:
2脚:发送数据TXD; 3脚:接收数据RXD; 4脚:请求发送RTS; 5脚:清除发送CTS;
6脚:数据设备就绪DSR;20脚:数据终端就绪DTR; 8脚:数据载波检测DCD;
1脚:保护地; 7脚:信号地。
②电气特性:
数据传输速率最大可到20K bps,最大距离仅15m.
注:看了微软的MSDN 6.0,其Windows API中关于串行通讯设备(不一定都是串口RS-232C或RS-422或RS-449)速率的设置,最大可支持到RS_256000,即256K bps! 也不知道到底是什么串行通讯设备?但不管怎样,一般主机和单片机的串口通讯大多都在9600 bps,可以满足通讯需求。
③接口的典型应用:
大多数计算机应用系统与智能单元之间只需使用3到5根信号线即可工作。这时,除了TXD、RXD以外,还需使用RTS、CTS、DCD、DTR、DSR等信号线。(当然,在程序中也需要对相应的信号线进行设置。)
图 最简单的RS232-C信号线接法
以上接法,在设计程序时,直接进行数据的接收和发送就可以了,不需要 对信号线的状态进行判断或设置。(如果应用的场合需要使用握手信号等,需要对相应的信号线的状态进行监测或设置。)
三.16位串口应用程序的简单回顾
16位串口应用程序中,使用的16位的Windows API通信函数:
① OpenComm() 打开串口资源,并指定输入、输出缓冲区的大小(以字节计);
CloseComm() 关闭串口;
例:
int idComDev; idComDev = OpenComm("COM1", 1024, 128); CloseComm(idComDev);
char *m_pRecieve; int count; ReadComm(idComDev,m_pRecieve,count); Char wr[30]; int count2; WriteComm(idComDev,wr,count2);
DWORD style=WS_VISIBLE|WS_CHILD; if (!m_ComPort.Create(NULL,style,CRect(0,0,0,0),this,ID_COMMCTRL)){ TRACE0("Failed to create OLE Communications Control\n"); return -1; // fail to create }
m_ComPort.SetCommPort(1); //选择COM? m_ComPort. SetInBufferSize(1024); //设置输入缓冲区的大小,Bytes m_ComPort. SetOutBufferSize(512); //设置输入缓冲区的大小,Bytes// if(!m_ComPort.GetPortOpen()) //打开串口 m_ComPort.SetPortOpen(TRUE); m_ComPort.SetInputMode(1); //设置输入方式为二进制方式 m_ComPort.SetSettings("9600,n,8,1"); //设置波特率等参数 m_ComPort.SetRThreshold(1); //为1表示有一个字符引发一个事件 m_ComPort.SetInputLen(0);
void CMainFrame::OnCommMscomm(){ VARIANT vResponse; int k; if(m_commCtrl.GetCommEvent()==2) { k=m_commCtrl.GetInBufferCount(); //接收到的字符数目 if(k>0) { vResponse=m_commCtrl.GetInput(); //read SaveData(k,(unsigned char*) vResponse.parray->pvData); } // 接收到字符,MSComm控件发送事件 } 。。。。。 // 处理其他MSComm控件 } void CMainFrame::OnCommSend() { 。。。。。。。。 // 准备需要发送的命令,放在TxData[]中 CByteArray array; array.RemoveAll(); array.SetSize(Count); for(i=0;i<Count;i++) array.SetAt(i, TxData[i]); m_ComPort.SetOutput(COleVariant(array)); // 发送数据 }请大家认真关注第⑷、⑸中内容,在实际工作中是重点、难点所在。
hCom =CreateFile( "COM2", GENERIC_READ | GENERIC_WRITE, // 允许读写 0, // 此项必须为0 NULL, // no security attrs OPEN_EXISTING, //设置产生方式 FILE_FLAG_OVERLAPPED, // 我们准备使用异步通信 NULL );
UINT CommWatchProc(HWND hSendWnd){ DWORD dwEvtMask=0 ; SetCommMask( hCom, EV_RXCHAR|EV_TXEMPTY );//有哪些串口事件需要监视? WaitCommEvent( hCom, &dwEvtMask, os );// 等待串口通信事件的发生 //检测返回的dwEvtMask,知道发生了什么串口事件: if ((dwEvtMask & EV_RXCHAR) == EV_RXCHAR){ // 缓冲区中有数据到达 COMSTAT ComStat ; DWORD dwLength; ClearCommError(hCom, &dwErrorFlags, &ComStat ); dwLength = ComStat.cbInQue ; //输入缓冲区有多少数据 if (dwLength > 0) { BOOL fReadStat ; fReadStat = ReadFile( hCom, lpBuffer,dwLength, &dwBytesRead, &READ_OS( npTTYInfo ) ); //读数据注:我们在CreareFile()时使用了FILE_FLAG_OVERLAPPED,现在ReadFile()也必须使用LPOVERLAPPED结构.否则,函数会不正确地报告读操作已完成了.
if (!fReadStat){ if (GetLastError() == ERROR_IO_PENDING){ while(!GetOverlappedResult(hCom, &READ_OS( npTTYInfo ), & dwBytesRead, TRUE )){ dwError = GetLastError(); if(dwError == ERROR_IO_INCOMPLETE) continue; //缓冲区数据没有读完,继续 …… …… ::PostMessage((HWND)hSendWnd,WM_NOTIFYPROCESS,0,0);//通知主线程,串口收到数据 }
所谓的非阻塞通信,也即异步通信。是指在进行需要花费大量时间的数据读写操作(不仅仅是指串行通信操作)时,一旦调用ReadFile()、WriteFile(), 就能立即返回,而让实际的读写操作在后台运行;相反,如使用阻塞通信,则必须在读或写操作全部完成后才能返回。由于操作可能需要任意长的时间才能完成,于是问题就出现了。
非常阻塞操作还允许读、写操作能同时进行(即重叠操作?),在实际工作中非常有用。
要使用非阻塞通信,首先在CreateFile()时必须使用FILE_FLAG_OVERLAPPED;然后在 ReadFile()时lpOverlapped参数一定不能为NULL,接着检查函数调用的返回值,调用GetLastError(),看是否返回ERROR_IO_PENDING。如是,最后调用GetOverlappedResult()返回重叠操作(overlapped operation)的结果;WriteFile()的使用类似。
⑸.在主线程中发送下行命令。
BOOL fWriteStat ; char szBuffer[count]; …………//准备好发送的数据,放在szBuffer[]中 fWriteStat = WriteFile(hCom, szBuffer, dwBytesToWrite, &dwBytesWritten, &WRITE_OS( npTTYInfo ) ); //写数据
使用LPOVERLAPPED结构,WriteFile()立即返回,不必等待写操作完成,实现非阻塞 通信.此时, WriteFile()返回FALSE, GetLastError()返回ERROR_IO_PENDING.
int err=GetLastError(); if (!fWriteStat) { if(GetLastError() == ERROR_IO_PENDING){ while(!GetOverlappedResult(hCom, &WRITE_OS( npTTYInfo ), &dwBytesWritten, TRUE )) { dwError = GetLastError(); if(dwError == ERROR_IO_INCOMPLETE){ // normal result if not finished dwBytesSent += dwBytesWritten; continue; } ......................
综上,我们使用了多线程技术,在辅助线程中监视串口,有数据到达时依靠事件驱动,读入数据并向主线程报告(发送数据在主线程中,相对说来,下行命令的数据总是少得多);并且,WaitCommEvent()、ReadFile()、WriteFile()都使用了非阻塞通信技术,依靠重叠(overlapped)读写操作,让串口读写操作在后台运行。
依托vc6.0丰富的功能,结合我们提及的技术,写出有强大控制能力的串口通信应用程序。就个人而言,我更偏爱API技术,因为控制手段要灵活的多,功能也要强大得多。