串口通信协议
一、串口API
1. 打开串口
使用CreateFile函数可以打开串口。通常有两种方式可以打开,一种是同步方式(NonOverlapped),另外一种异步方式(Overlapped)。
HANDLE hComm;
hComm = CreateFile( gszPort, //串口名
GENERIC_READ|GENERIC_WRITE //读写
0, //注意:串口为不可共享设备,本参数须为0
0,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED, //异步方式
0);
if(hComm == INVALID_HANDLE_VALUE) //打开串口失败处理
······
2. 配置串口
DCB(Device Control Block)结构定义了串口通信设备的控制设置,有3种方式可以初始化DCB。
通过GetCommState函数得到DCB的初始值:
DCB dcb;
memset(&dcb, 0, sizeof(dcb));
if(!GetCommState(hComm, &dcb)) …… //错误处理
else …… //已准备就绪
用BuildCommDCB函数初始化DCB结构:
DCB dcb;
memset(&dcb, 0, sizeof(dcb));
dcb.DCBlength = sizeof(dcb);
if(!BuildCommDCB(“9600,n,8,1”, &dcb)) …… //参数配置错误
else …… //已准备就绪
用SetCommState函数手动设置DCB初值:
DCB dcb;
memset(&dcb, 0, sizeof(dcb));
if(!GetCommState(hComm, &dcb)) return FALSE;
dcb.BaudRate = CBR_9600;
3. 流控设备
流控制有如下两种设置:
硬件流控制:硬件流控有两种,DTE/DSR方式和RTS/CTS方式。这与DCB结构的初始化有关系,建议采用标准流行的流控方式,采用硬件流控时,DTE、DSR、RTS、CTS的逻辑位直接影响到数据的读写及收发数据的缓冲区控制。
软件流控制:串口通信中采用特殊字符XON和XOFF作为控制串口数据的收发。
注意:在不设置流控制方式或软件流控的情况下,基本上不会出现什么问题,但在硬件流控下,规范的RTS_CONTROL_HANDSHAKE流控方式的含义本来是当缓冲区快满的时候RTS会自动OFF通知对方暂停发送,当缓冲区重新空出来的时候,RTS会自动ON,但很多时候当RTS变OFF以后即使已经清空了缓冲区,RTS也不会自动的ON,造成对方停在那里不发送了。所以,如果要用硬件流控制的话,还要在接收后最好加上检测缓冲区大小的判断,具体做法是使用ClearCommError后返回COMSTAT.cbInQue,当缓冲区已经空出来的时候,要使用invoke(EscapeCommFunction,hComm,SETRTS)重新将RTS设置为ON。
4. 串口读写操作
串口读写有两种方式:同步方式(NonOverlapped)和异步方式(Overlapped)。同步方式指必须完成了读写操作,函数才返回,这可能会使程序无响应,因为如果在读写时发生了错误,永远不返回就会出错,可能线程将停在原地。而异步方式则灵活的多,一旦读写不成功,就将读写挂起,函数直接返回,可以通过GetLastError函数得知读写未成功的原因,所以串口读写常常采用异步方式操作。
ReadFile()函数用于完成读操作,异步方式的读操作为:
DWORD dwRead;
BOOL fWaitingOnRead = FALSE;
OVERLAPPED osReader;
memset(&osReader, 0, sizeof(osReader));
osReader.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if(osReader.hEvent == NULL) …… //错误处理
if(!fWaitingOnRead)
{
if(!ReadFile(hComm, lpBuf, READ_BUF_SIZE, &dwRead, &osReader)) //读串口
{
if(GetLastError() != ERROR_IO_PENDING) …… //报告错误
else fWaitingOnRead = TRUE;
}
}
else
{
//读取完成,不必在调用GetOverlappedResults函数
HandleASuccessfulRead(lpBuf, dwRead);
}
//如果读操作被挂起,可以调用WaitForSingleObject()函数或
//WaitForMuntilpleObjects()等待读操作完成或者超时发生,
//再调用GetOverlappedResult()得到想要的信息。
if(fWaitingOnRead)
{
dwRes = WaitForSingleObject(osReader.hEvent, READ_TIMEOUT);
switch(dwRes)
{
case WAIT_OBJECT_0: //完成读操作
if(!GetOverlappedResult(hComm, &osReader, &dwRead, FALSE)) …… //错误
else …… //全部读取成功
HandleASuccessfulRead(lpBuf, dwRead);
fWaitintOnRead = FALSE;
break;
case WAIT_TIMEOUT: //操作尚未完成
……. //处理其他任务
break;
default:
…… //出现错误
break;
}
}
注意上述代码在处理多线程串口在windows系列下存在一些问题,修改完成后代码参考1.4节。
5. 关闭串口
程序结束或需要释放串口资源时,必须正确关闭串口。调用CloseHandle函数关闭串口的句柄即可,
CloseHandle(hComm);
值得注意的是,在关闭串口前必须保证读写串口线程已经退出,否则会引起误操作,一般采用的办法是使用事件驱动机制,启动一事件,通知串口读写线程强制退出。
6. 其他问题
串口通信中其他必须处理的问题主要有如下几个:
检测通信事件:用SetCommMask()设置想要得到的通信事件的掩码,再调用WaitCommEvent()检测通信事件的发生。可设置事件标志有EV_BREAK \ EV_VTS \ EV_DSR \ EV_ERR \ EV_RING \ EV_RLSD \ EV_RXCHAR \ EV_RXFLAG \ EV_TXEMPTY。
处理通信超时:在通信中,超时是一个很重要的考虑因素,因为数据接收过程中由于某种原因突然中断或停止,如果不采取超时控制机制,将会使得I/O线程被挂起或无限阻塞。超时设置分两步,首先设置COMMTIMEOUTS结构的5个变量,然后调用SetcommTimeouts()设置超时值,对于使用异步方式读写的操作,如果操作挂起后,异步成功完成了读写,WaitForSingleObject()或WaitForMultipleObjects()将返回WAIT_OBJECT_0,GetOverlappedResult()返回TRUE。其实还可以用GetCommTimeouts()得到系统初始值。
错误处理和通信状态:在串口通信中,可以会产生很多的错误,使用ClearCommError()可以检测错误并且清除错误条件。
WaitCommEvent()返回时,只是指出了如CTS等等状态有变化。但要了解具体变化情况必须使用GetCommModemStatus()获得串口线路状态更详细的信息。
二、串口操作方式
1. 同步方式
同步(NonOverlapped)方式是比较简单的一种方式,编写代码长度明显少于异步(Overlapped)方式。同步方式中,读串口的函数试图在串口的接收缓冲区中读取规定数据的数据,直到规定数据的数据全部被读出或设定超时时间已到时才返回。例如:
COMMTIMEOUTS timeOver;
memset(&timeOver, 0, sizeof(timeOver));
DWORD timeMultiplier, timeConstant;
……
timeOver.ReadTotalTimeoutMultiplier = timeMultiplier;
timeOver.ReadTotalTimeoutConstant = timeConstant;
SetCommTimeouts(hComm, &timeOver);
……
ReadFile(hComm, inBuffer, nWantRead, &nRealRead, NULL); //NULL指采用同步文件读写
如果所规定的待读取数据的数目nWantRead较大且设定的超时时间较长,而接收缓冲区中数据较少,则可能引起线程阻塞。解决这一问题的方法是检查COSTAT结构的cbInQue成员,该成员的大小即为接收缓冲区中处于等待状态的实际个数。如果令nWantRead的值等于COMSTAT.cbInQue,就能很好的防止线程阻塞。
2. 异步方式
在异步方式中,利用Windows的多线程结构,可以让串口的读写操作在后台进行,而应用程序的其他部分在前台执行。例如:
OVERLAPPED wrOverlapped;
COMMTIMEOUTS timeOVer;
memset(&timeOver, 0, sizeof(timeOver));
DWORD timeMultiplier, timeConstant;
…… //给timeMultiplier, timeConstant赋值
timeOver.ReadTotalTimeoutMultiplier = timeMultiplier;
timeOver.ReadTotalTimeoutConstant = timeConstant;
SetCommTimeouts(hComm, &timeOver);
wrOverlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
……
ReadFile(hComm, inBuffer, nWantRead, &nRealRead, &wrOverlapped);
GetOverlappedResult(hComm, &wrOverlapped, &nRealRead,TRUE);
……
ResetEvent(wrOverlapped.hEvent);
上面代码中的ReadFile由于采用了异步方式,所以只返回数据是否已经开始读入的状态,并不返回实际的读入数据,即ReadFile中的nRealRead无效。实际读入的数据由GetOverlappedResult返回的,该函数的最后一个参数值为TRUE,表示它等待异步操作结束后才返回到应用程序,此时,GetOverlappedResult与WaitForSingleObject函数无效。
3. 查询方式
即一个进程中的某一线程定时地查询串口的接收缓冲区,如果缓冲区中有数据,就读取数据;若缓冲区没有数据,该线程将继续执行,因此会占用大量的CPU时间,它实际上是同步方式的一种派生。例如:
COMMTIMEOUTS timeOver,
Memset(&timeOver, 0, sizeof(timeOver));
timeOver.ReadIntervalTimeout = MAXWORD;
SetCommTimeouts(hComm, &timeOver);
……ReadFile(hComm, inBuffer, nWantRead, &nRealRead, NULL);
除了COMMTIMEOUTS结构的变量timeOver设置不同外,查询方式与同步方式在程序代码方面很类似,但二者的工作方式却差别很大。尽管ReadFile采用的也是同步文件读写方式,但由于timeOver的区间超过时间设置为MAXWORD,所以ReadFile每次将读出接收队列中的所有处于等待状态的数据,一次最多可读出nWantRead个字节的数据。
4. 事件驱动方式
若对端口数据的响应时间要求较严格,可采用事件驱动方式。事件驱动方式通过设置事件通知,当所希望的事件发生时,Windows发出该事件已经发生的通知。Windows定义了9中串口通信事件,常用的有以下3中:
EV_RXCHAR:接收到一个字节,并放入输入缓冲区。
EV_TXEMPTY:输出缓冲区中的最后一个字符,发送出去。
EV_RXFLAG:接收到事件字符(DCB结构中的EvtChar成员),放入输入缓冲区。
在用SetCommMask()制定了有用的事件后,应用程序可调用WaitCommEvent()来等待事件的发生。SetCommMask可使WaitCommEvent()中止。例如:
COMSTAT comStat;
DWORD dwEvent;
SetCommMask(hComm, EV_RXCHAR);
……
if(WaitCommEvent(hComm, &dwEvent, NULL))
if((dwEvent & EV_RXCHAR) && comstat.cbInQue)
ReadFile(hComm, inBuffer, comstat.cbInQue, &nRealRead, NULL);
5. 总结
一般要求情况下,查询方式是一种最直接的读串口的方式。但定时查询存在一个致命的弱点,即查询是定时发生的,可能发生的过早或过晚。在数据变化较快的情况下,特别是主控计算机的串口通过扩展板扩展多个时,需定时对所有串口轮流查询,容易发生数据的丢失。虽然定时间隔越小,数据的实时性越高,但系统的资源也被占用越多。
Windows中提出文件读写的异步方式,主要是针对文件IO相对较慢的速度而进行的改进,它利用了系统的多线程结构,虽然在Windows中没有实现任何对文件IO的异步操作,但它却能对串口进行异步操作。采用异步方式,可以提高系统整体性能,在对系统强壮性要求高的场合,建议采用这种方式。
事件驱动方式是一种高效的串口读方式。这种方式实时性较高,特别对扩展了多个串口的情况,并不要求像查询方式那样定时地对所有串口轮询,而像中断方式那样,只有当设定的事件发生时,应用程序得到windows操作系统发出的消息后,才进行相应处理,以免数据丢失。