翻译自:ms-help://MS.MSDNQTR.v80.chs/MS.MSDN.v80/MS.WIN32COM.v10.en/dnfiles/html/msdn_serial.htm
老外写的文章,虽比较全面,但很啰嗦,不如看各个函数的文档来得快。为方便以后查阅,列出本文涉及的主要函数如下:
CreateFile、ReadFile、WriteFile、GetOverlappedResult、WaitForSingleObject
SetCommMask、WaitCommEvent
ClearCommError、GetCommModemStatus、EscapeCommFunction
GetCommState、BuildCommDCB、SetCommState、SetCommTimeouts。
0 简介
本文仅关注在Windows NT和95间兼容的API。Windows 95支持Telephony API(TAPI),但Windows NT 3.x不支持TAPI,所以本文不讨论它。
本文的示例程序MTTTY(Multithreaded TTY)使用了三个线程:一个进行内存管理的界面线程;控制所有写入操作的写入者线程;读取数据和处理端口状态改变的读取/状态线程。示例采用了一些不同的堆来进行内存管理;还大量使用了同步方法来进行线程间通信。
1 打开端口
使用CreateFile函数打开端口。打开端口时有两种方法:重叠的和非重叠的。下列代码片段以重叠方式打开端口:
HANDLE hComm; hComm = CreateFile( gszPort, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, 0); if (hComm == INVALID_HANDLE_VALUE) // error opening port; abort |
去掉参数中的FILE_FLAG_OVERLAPPED就是非重叠操作方式了。用CreateFile打开通信端口时,有下列限制:
- fdwShareMode 必须是0。通信端口不能像文件那样被共享。要共享通信端口,需要使用句柄继承或者复制操作。
- fdwCreate 必须指定 OPEN_EXISTING 标志。
- hTemplateFile 参数必须是 NULL。
端口名通常是COM1、COM2、COM3和COM4。Win32 API不提供确定系统中有哪些端口可用的机制。Windows NT和Windows 95跟踪系统已安装端口的方法是不同的,所以不太可能提供兼容的确定可用端口的方法。某些系统可能有多于4个端口,而传统的通信端口最大个数是4。硬件厂商和串口驱动编写者可以自由地为端口命名。所以,程序最好可以让用户指定要使用的端口名字。如果端口不存在,则试图打开端口时会返回ERROR_FILE_NOT_FOUND错误,这时应该提示用户端口不可用。
2 读写操作
通信端口的读写操作与文件I/O操作非常相似,它们使用同样的函数。Win32的I/O操作可分为两种:重叠(overlapped)的和非重叠的(nonoverlapped)。平台SDK文档分别使用异步(asynchronous)和同步(synchronous)来表示这两种I/O方式。
很多开发者都熟悉非重叠I/O,因为它就是传统的I/O方式:函数返回时,所请求的操作已经完成。然而在重叠I/O的情况下,系统则可能在操作还没有完成的情形下立即返回,随后才通知调用者操作完成。程序可以在发起I/O请求和请求被完成之间进行一些后台工作。
2.1 非重叠I/O
非重叠I/O的工作方式很简单:I/O操作进行时,调用线程被阻塞;操作完成后,函数返回,调用线程可以继续执行。在多线程应用中,这种I/O方式很有用:一个线程阻塞在某I/O操作上时,其他线程可以继续工作。应用程序应该保证对端口的串行访问。某个线程阻塞在等待某I/O操作上时,其他线程后续的通信API调用也都将阻塞。比如说,一个线程在等待ReadFile调用返回时,另一个线程的WriteFile函数调用将阻塞。
在选择使用非重叠还是重叠方式时,可移植性是要考虑的因素之一。有时候重叠操作并不是好的选择,因为很多操作系统不支持它;然而很多操作系统都支持某种形式的多线程。所以从兼容性方面考虑,多线程非重叠I/O可能是最好的选择。
2.2 重叠I/O
重叠I/O不像非重叠I/O那样简单易懂,但却灵活高效。使用重叠方式打开的端口允许多个线程同时进行I/O操作,并且在操作进行期间可以进行其他的工作。此外,重叠操作的行为方式还允许单个线程提交多个不同的请求,然后在操作进行期间进行其他后台工作。
在单线程和多线程应用中,都必须在提交I/O请求和处理操作结果间进行一些同步操作。线程可能需要在操作结果可用前阻塞;当然也可以进行其他工作。如果没有其他需要进行的工作,则重叠I/O的优点是更好的用户响应性能。
MTTTY使用了重叠I/O。它创建用于读取数据和监测端口状态的线程,并且还定时进行一些后台工作;此外它还另外创建一个线程用于写入数据。
重叠I/O操作分为两个部分:创建I/O操作和检测操作完成。创建I/O操作涉及到建立OVERLAPPED结构体、创建用于同步的手动复位事件、调用恰当的函数(ReadFile或者WriteFile)。I/O操作可能立即完成,也可能不能立即完成,不能认为一个重叠I/O操作请求总是生成一个重叠操作。如果操作立即完成,程序应该可以继续进行通常的处理。检测操作完成涉及到等待事件句柄、检查操作完成结果、处理数据。与重叠I/O相关的工作更多的原因是有更多的失败点。非重叠操作中,简单地通过函数返回值判断操作是否失败;而重叠操作中,则可能在创建操作请求时失败,或者操作阻塞期间失败,也可能是操作超时,或者是等待操作完成信号超时。
2.2.1 读操作
下面的代码片段展示了提交重叠的读操作请求的方法。注意,如果ReadFile返回TRUE,调用了一个函数处理数据。代码定义了fWaitingOnRead标志,它表示是否有重叠的读取操作存在,用于阻止在一个操作进行中时提交另一个读取操作请求。
DWORD dwRead; BOOL fWaitingOnRead = FALSE; OVERLAPPED osReader = {0}; // Create the overlapped event. Must be closed before exiting // to avoid a handle leak. osReader.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); if (osReader.hEvent == NULL) // Error creating overlapped event; abort. if (!fWaitingOnRead) { // Issue read operation. if (!ReadFile(hComm, lpBuf, READ_BUF_SIZE, &dwRead, &osReader)) { if (GetLastError() != ERROR_IO_PENDING) // read not delayed? // Error in communications; report it. else fWaitingOnRead = TRUE; } else { // read completed immediately HandleASuccessfulRead(lpBuf, dwRead); } } |
OVERLAPPED结构体的事件句柄被传递给WaitForSingleObject以等待事件授信,操作完成。注意,事件受信表示操作完成,而不是操作成功完成。应该用GetOverlappedResult来取得操作结果,它返回TRUE表示操作成功完成;FALSE表示有错误发生,用GetLastError可以取得具体的错误码。也可以用GetOverlappedResult来检测操作完成:GetOverlappedResult返回FALSE,GetLastError返回ERROR_IO_INCOMPLETE表示操作进行中。如果对bWait参数传入TRUE,则效果就是重叠操作变成了非重叠的,直到操作完成,函数才返回。下面的代码片段展示了一种检测重叠读取操作完成的方法。注意fWaitingOnRead标志的使用,它是检测代码的控制入口,只有在某操作进行中时,才应该调用检测代码。
#define READ_TIMEOUT 500 // milliseconds DWORD dwRes; if (fWaitingOnRead) { dwRes = WaitForSingleObject(osReader.hEvent, READ_TIMEOUT); switch(dwRes) { // Read completed. case WAIT_OBJECT_0: if (!GetOverlappedResult(hComm, &osReader, &dwRead, FALSE)) // Error in communications; report it. else // Read completed successfully. HandleASuccessfulRead(lpBuf, dwRead); // Reset flag so that another opertion can be issued. fWaitingOnRead = FALSE; break; case WAIT_TIMEOUT: // Operation isn't complete yet. fWaitingOnRead flag isn't // changed since I'll loop back around, and I don't want // to issue another read until the first one finishes. // // This is a good time to do some background work. break; default: // Error in the WaitForSingleObject; abort. // This indicates a problem with the OVERLAPPED structure's // event handle. break; } } |
2.2.2 写入操作
写入操作跟读取操作非常相似。下面的代码片段展示了如何提交写入操作,并等待操作完成。
BOOL WriteABuffer(char * lpBuf, DWORD dwToWrite) { OVERLAPPED osWrite = {0}; DWORD dwWritten; DWORD dwRes; BOOL fRes; // Create this write operation's OVERLAPPED structure's hEvent. osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); if (osWrite.hEvent == NULL) // error creating overlapped event handle return FALSE; // Issue write. if (!WriteFile(hComm, lpBuf, dwToWrite, &dwWritten, &osWrite)) { if (GetLastError() != ERROR_IO_PENDING) { // WriteFile failed, but isn't delayed. Report error and abort. fRes = FALSE; } else // Write is pending. dwRes = WaitForSingleObject(osWrite.hEvent, INFINITE); switch(dwRes) { // OVERLAPPED structure's event has been signaled. case WAIT_OBJECT_0: if (!GetOverlappedResult(hComm, &osWrite, &dwWritten, FALSE)) fRes = FALSE; else // Write operation completed successfully. fRes = TRUE; break; default: // An error has occurred in WaitForSingleObject. // This usually indicates a problem with the // OVERLAPPED structure's event handle. fRes = FALSE; break; } } } else // WriteFile completed immediately. fRes = TRUE; CloseHandle(osWrite.hEvent); return fRes; } |
注意,上面的代码使用WaitForSingleObject时,超时值是INFINITE,这使得函数无限等待直到操作完成。这可能让调用线程似乎是被挂起了;而实际上只是写入操作需要较长的时间,或者流控制阻塞了传输操作。下文将讨论的状态检查可以检测到这种情况,但它也不会让WaitForSingleObject返回。有三种方法可以克服此问题:
-
把代码放在单独的线程中。这样写入线程在等待写操作完成时,其他线程可以进行任何所需的操作。MTTTY就是这么做的。
-
使用COMMTIMEOUTS使得写操作在经过一个超时值指定的时间后完成。本文后面的“通信超时”节将详细讨论它。MTTTY也可以使用这种方法。
-
修改WaitForSingleObject调用,使用超时值。这样会更麻烦:如果原来的操作仍在进行中,程序提交另一个操作请求,则需要分配新的OVERLAPPED结构和重叠事件。这种记录跟踪保持是很困难的,尤其是与“工作队列”相比较时。MTTTY使用了工作队列。
上面代码中的WaitForSingleObject使用了INFINITE作为超时值,其效果等同于使用TRUE作为GetOverlappedResult的fWait参数。下面是等效的更简洁的代码:
BOOL WriteABuffer(char * lpBuf, DWORD dwToWrite) { OVERLAPPED osWrite = {0}; DWORD dwWritten; BOOL fRes; // Create this writes OVERLAPPED structure hEvent. osWrite.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); if (osWrite.hEvent == NULL) // Error creating overlapped event handle. return FALSE; // Issue write. if (!WriteFile(hComm, lpBuf, dwToWrite, &dwWritten, &osWrite)) { if (GetLastError() != ERROR_IO_PENDING) { // WriteFile failed, but it isn't delayed. Report error and abort. fRes = FALSE; } else { // Write is pending. if (!GetOverlappedResult(hComm, &osWrite, &dwWritten, TRUE)) fRes = FALSE; else // Write operation completed successfully. fRes = TRUE; } } else // WriteFile completed immediately. fRes = TRUE; CloseHandle(osWrite.hEvent); return fRes; } |
GetOverlappedResult并不总是等待重叠操作完成的最好方法。比如说,如果应用需要同时等待另一个事件句柄,则第一个代码片段模型比第二个更好,因为可以很容易地用WaitForMultipleObjects替换WaitForSingleObject,来等待更多的句柄。MTTTY就是这么做的。
在前一个重叠操作完成前重用OVERLAPPED结构是重叠I/O编程中常现的一个错误。如果要在前一个重叠操作完成前提交新的重叠操作请求,则需要分配新的OVERLAPPED结构,其hEvent字段也应该包含新的手动复位事件句柄。只有在重叠操作完成后,OVERLAPPED结构和其事件句柄才可以被重用。
串口通信中使用OVERLAPPED结构时只需要修改hEvent字段,其他字段只需要初始化为零。
3 串口状态
有两种获取通信端口状态的方法。第一种方法是设置事件掩码,当指定事件发生时应用程序会收到通知。SetCommMask函数用于设置事件掩码,WaitCommEvent用于等待指定的事件发生。它们与16位Windows中的SetCommEventMask和EnableCommNotification类似,只是它们不发送WM_COMMNOTIFY消息。第二种方法是不时地调用另一些状态函数来获取通信端口的状态。当然,轮询是低效的,不建议使用。
3.1 通信事件
通信事件在使用通信端口时可能随时发生。接收通信事件需要两个步骤:
用SetCommMask设定需要接收通知的事件 用WaitCommEvent提交状态检查请求,请求可以是重叠的或者非重叠的,与读写操作一样。下面是使用SetCommMask的示例:
DWORD dwStoredFlags;
dwStoredFlags = EV_BREAK | EV_CTS | EV_DSR | EV_ERR | EV_RING |
EV_RLSD | EV_RXCHAR | EV_RXFLAG | EV_TXEMPTY ;
if (!SetCommMask(hComm, dwStoredFlags))
// error setting communications mask
下表描述了每种事件类型。
事件标志
描述
EV_BREAK
检测到输入中的break
EV_CTS
CTS(Clear To Send)信号状态改变。要取得CTS线路状态,应使用GetCommModemStatus函数。
EV_DSR
DSR(Data Set Ready)信号状态改变。要取得DSR线路状态,应使用GetCommModemStatus函数。
EV_ERR
某线路状态错误发生。线路状态错误包括CE_FRAME、CE_OVERRUN和CE_RXPARITY。要取得具体错误种类,需调用ClearCommError函数。
EV_RING
检测到振铃指示
EV_RLSD
RLSD(Receive Line Signal Detect)信号状态改变。要取得RLSD线路状态,需调用GetCommModemStatus函数。注意,RLSD通常被称作CD(carrier detect)。
EV_RXCHAR
接收到一个字符并且已放入输入缓冲区。请参考下面的“警告”节对此标志的详细讨论。
EV_RXFLAG
接收到一个事件字符并且已放入输入缓冲区。事件字符由下文讨论的DCB结构EvtChar字段指定。下面的“警告”节也讨论了这个标志。
EV_TXEMPTY
输出缓冲区中最后一个字符被发送到串口设备了。如果使用硬件缓冲区,此标志仅表示所有数据已经发送到硬件了。如果不与设备驱动交互,是无法确定硬件缓冲区空的。
指定事件掩码后,使用WaitCommEvent函数检测事件发生。如果以非重叠方式打开端口,则WaitCommEvent不需要OVERLAPPED结构体,函数阻塞调用线程直到某事件发生。如果没有事件发生,调用线程将无限阻塞。
下面的代码片段展示了如何在以非重叠方式打开的端口上等待EV_RING事件。
DWORD dwCommEvent;
if (!SetCommMask(hComm, EV_RING))
// Error setting communications mask
return FALSE;
if (!WaitCommEvent(hComm, &dwCommEvent, NULL))
// An error occurred waiting for the event.
return FALSE;
else
// Event has occurred.
return TRUE;
如果没有事件发生,上面的代码将无限阻塞调用线程。解决方法是以重叠方式打开端口,用下面的方式等待状态事件:
#define STATUS_CHECK_TIMEOUT 500 // Milliseconds
DWORD dwRes;
DWORD dwCommEvent;
DWORD dwStoredFlags;
BOOL fWaitingOnStat = FALSE;
OVERLAPPED osStatus = {0};
dwStoredFlags = EV_BREAK | EV_CTS | EV_DSR | EV_ERR | EV_RING |\
EV_RLSD | EV_RXCHAR | EV_RXFLAG | EV_TXEMPTY ;
if (!SetCommMask(comHandle, dwStoredFlags))
// error setting communications mask; abort
return 0;
osStatus.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (osStatus.hEvent == NULL)
// error creating event; abort
return 0;
for ( ; ; ) {
// Issue a status event check if one hasn't been issued already.
if (!fWaitingOnStat) {
if (!WaitCommEvent(hComm, &dwCommEvent, &osStatus)) {
if (GetLastError() == ERROR_IO_PENDING)
bWaitingOnStatusHandle = TRUE;
else
// error in WaitCommEvent; abort
break;
}
else
// WaitCommEvent returned immediately.
// Deal with status event as appropriate.
ReportStatusEvent(dwCommEvent);
}
// Check on overlapped operation.
if (fWaitingOnStat) {
// Wait a little while for an event to occur.
dwRes = WaitForSingleObject(osStatus.hEvent, STATUS_CHECK_TIMEOUT);
switch(dwRes)
{
// Event occurred.
case WAIT_OBJECT_0:
if (!GetOverlappedResult(hComm, &osStatus, &dwOvRes, FALSE))
// An error occurred in the overlapped operation;
// call GetLastError to find out what it was
// and abort if it is fatal.
else
// Status event is stored in the event flag
// specified in the original WaitCommEvent call.
// Deal with the status event as appropriate.
ReportStatusEvent(dwCommEvent);
// Set fWaitingOnStat flag to indicate that a new
// WaitCommEvent is to be issued.
fWaitingOnStat = FALSE;
break;
case WAIT_TIMEOUT:
// Operation isn't complete yet. fWaitingOnStatusHandle flag
// isn't changed since I'll loop back around and I don't want
// to issue another WaitCommEvent until the first one finishes.
//
// This is a good time to do some background work.
DoBackgroundWork();
break;
default:
// Error in the WaitForSingleObject; abort
// This indicates a problem with the OVERLAPPED structure's
// event handle.
CloseHandle(osStatus.hEvent);
return 0;
}
}
}
CloseHandle(osStatus.hEvent);
上面的代码片段与重叠读取操作的代码非常相似。实际上,MTTTY使用WaitForMultipleObjects在同一个线程中等待读取完成或者状态改变事件发生。 SetCommMask和WaitCommEvent有两种很有意思的边际效应。第一,如果以非重叠方式打开通信端口,WaitCommEvent将阻塞直到某事件发生。如果其他线程调用SetCommMask设置新的事件掩码,则线程将阻塞在SetCommMask调用上,原因是第一个线程的WaitCommEvent调用仍在执行中。SetCommMask将一直阻塞调用线程,直到第一个线程的WaitCommEvent调用返回。这种边际效应对于以非重叠方式打开的端口是通用的。如果某线程阻塞在任何通信函数上,则第二个线程对任何通信函数的调用都将阻塞,直到第一个线程的函数调用返回。第二种边际效应是关于以重叠方式打开的端口的。如果使用SetCommMask设置新的事件掩码,则未决的WaitCommEvent调用将成功完成,导致调用完成的事件掩码将是NULL
3.2 警告
使用EV_RXCHAR标志可以在每个字节到达端口时通知线程。与ReadFile配合使用,可以让程序在数据到达接收缓冲区后立即被读取;这与提交读取操作请求,然后等待数据到达是不同的。这对于以非重叠方式打开的端口特别有用,因为程序在数据到达时被EV_RXCHAR事件通知,而不需要轮询操作。这样可以得到下列伪代码:
DWORD dwCommEvent;
DWORD dwRead;
char chRead;
if (!SetCommMask(hComm, EV_RXCHAR))
// Error setting communications event mask.
for ( ; ; ) {
if (WaitCommEvent(hComm, &dwCommEvent, NULL)) {
if (ReadFile(hComm, &chRead, 1, &dwRead, NULL))
// A byte has been read; process it.
else
// An error occurred in the ReadFile call.
break;
}
else
// Error in WaitCommEvent.
break;
}
上面的代码等待EV_RXCHAR事件发生,然后读取接收到的一个字节,随后继续循环,等待下一个EV_RXCHAR事件。当一个或者两个字节快速连续到达时,这段代码工作得很好。收到一个字节导致EV_RXCHAR发生,代码读取该字节。如果在下一次调用WaitCommEvent之前没有其他字节到达,一切都好:下一个字节的到达将使WaitCommEvent收到EV_RXCHAR事件。如果在下一次调用WaitCommEvent之前,另一个字节到达,一切也都很好:第一个字节仍然正常读取;第二个字节的到达导致内部设定EV_RXCHAR标志;当代码再次调用WaitCommEvent时,仍然可以收到EV_RXCHAR事件,然后通过ReadFile读取第二个字节。当三个或者更多个字节连续快速到达时,上面的代码就有问题了。第一个字节使得EV_RXCHAR事件发生;第二个字节使得内部设定EV_RXCHAR标志,下一次调用WaitCommEvent时,这个标志指示EV_RXCHAR事件发生。现在,第三个字节到达通信端口,系统试图在内部设置EV_RXCHAR标志;但是第二个字节到达时EV_RXCHAR标志已经被设置,所以第三个字节的到达是不被注意的。代码最终正常读取第一个字节;此后调用WaitCommEvnet时,EV_RXCHAR事件(由第二个字节到达引发)使得第二个字节被读取,第三个字节保持在接收缓冲区中。此时,系统和代码就失去同步了。当第四个字节到达导致EV_RXCHAR发生时,代码读取的是第三个字节。这种情况将一直持续下去。
似乎只要增加读取操作请求的字节数就可以解决这个问题:不是请求一个字节,而是请求两个、十个或者其他数目的字节。此方法的问题是,超过请求的字节数两个或者更多个字节快速连续到达时,代码还是会失败。就是说,如果请求两个字节,那么,四个字节快速连续到达会导致问题;如果请求10个字节,那么12个字节快速连续到达会导致问题。
真正的解决方法是从端口读取数据,直到没有数据可读取,下面的代码展示了这一方法。另一种可能的方法是调用ClearCommError确定缓冲区中有多少个字节,然后一次读取所有数据。这种方法要求更复杂的缓冲区管理,但是在大量数据一次到达时可以减少读取操作次数。
DWORD dwCommEvent;
DWORD dwRead;
char chRead;
if (!SetCommMask(hComm, EV_RXCHAR))
// Error setting communications event mask
for ( ; ; ) {
if (WaitCommEvent(hComm, &dwCommEvent, NULL)) {
do {
if (ReadFile(hComm, &chRead, 1, &dwRead, NULL))
// A byte has been read; process it.
else
// An error occurred in the ReadFile call.
break;
} while (dwRead);
}
else
// Error in WaitCommEvent
break;
}如果没有正确的通信超时值,上面的代码也不能正常工作。后文的“通信超时”节将讨论通信超时,它会影响到ReadFile的行为,使得ReadFile不等待字节到达就返回。上述关于EV_RXCHAR的警告也适用于EV_RXFLAG。如果标志字符连续快速到达,可能每个字符都不会触发EV_RXFLAG。同样,最好的解决方案是读取数据直到没有可读数据了。上述警告同样也适用于与字符接收无关的其他事件。如果其他事件连续快速发生,则某些通知可能丢失。比如说,如果CTS信号线电平开始是高,然后变为低,变为高,再变为低,则会发生EV_CTS事件;如果CTS线电平改变发生得太快,则无法保证WaitCommEvent最终可以检测到多少个EV_CTS事件。因此,不能用WaitCommEvent来保持信号线的状态。线路状态将在本文随后的“Modem状态”节讲述。
4 错误处理和通信状态
调用SetCommMask时可以指定EV_ERR这个事件标。EV_ERR事件表示通信端口存在错误条件,然而端口发生的某些错误不会导致EV_ERR的发生。通信端口相关的错误将导致所有I/O操作被挂起,直到移除了错误条件为止。ClearCommError用于检测错误和清除错误条件。ClearCommError也可以提供通信状态,以指示传输为何终止;它还可以指示收发缓冲区中各有多少个字节。传输终止的原因可能是存在错误,或者因为流控制。本文随后将讨论流控制。下列代码展示了ClearCommError的使用:
COMSTAT comStat;
DWORD dwErrors;
BOOL fOOP, fOVERRUN, fPTO, fRXOVER, fRXPARITY, fTXFULL;
BOOL fBREAK, fDNS, fFRAME, fIOE, fMODE;
// Get and clear current errors on the port.
if (!ClearCommError(hComm, &dwErrors, &comStat))
// Report error in ClearCommError.
return;
// Get error flags.
fDNS = dwErrors & CE_DNS;
fIOE = dwErrors & CE_IOE;
fOOP = dwErrors & CE_OOP;
fPTO = dwErrors & CE_PTO;
fMODE = dwErrors & CE_MODE;
fBREAK = dwErrors & CE_BREAK;
fFRAME = dwErrors & CE_FRAME;
fRXOVER = dwErrors & CE_RXOVER;
fTXFULL = dwErrors & CE_TXFULL;
fOVERRUN = dwErrors & CE_OVERRUN;
fRXPARITY = dwErrors & CE_RXPARITY;
// COMSTAT structure contains information regarding
// communications status.
if (comStat.fCtsHold)
// Tx waiting for CTS signal
if (comStat.fDsrHold)
// Tx waiting for DSR signal
if (comStat.fRlsdHold)
// Tx waiting for RLSD signal
if (comStat.fXoffHold)
// Tx waiting, XOFF char rec'd
if (comStat.fXoffSent)
// Tx waiting, XOFF char sent
if (comStat.fEof)
// EOF character received
if (comStat.fTxim)
// Character waiting for Tx; char queued with TransmitCommChar
if (comStat.cbInQue)
// comStat.cbInQue bytes have been received, but not read
if (comStat.cbOutQue)
// comStat.cbOutQue bytes are awaiting transfer4.1 Modem状态(线路状态)
对SetCommMask的调用可能包含EV_CTS、EV_DSR、EV_RING、EV_RLSD等标志,这些标志指示串口信号线电平的改变,但仅仅指示发生了改变,不能指示信号线的实际状态。GetCommModemStatus函数可以获取这些状态线的实际状态,它返回一个比特掩码用以表示每个信号线的状态。下面的代码展示了GetCommModemStatus的使用:
DWORD dwModemStatus;
BOOL fCTS, fDSR, fRING, fRLSD;
if (!GetCommModemStatus(hComm, &dwModemStatus))
// Error in GetCommModemStatus;
return;
fCTS = MS_CTS_ON & dwModemStatus;
fDSR = MS_DSR_ON & dwModemStatus;
fRING = MS_RING_ON & dwModemStatus;
fRLSD = MS_RLSD_ON & dwModemStatus;
// Do something with the flags.4.2 扩展函数
某些时候可能要用应用程序来代替串口通信驱动程序对控制线进行控制,比如说,当应用要实现自己的流控制时。此时应用必须负责RTS和DTR信号线的状态改变。EscapeCommFunction可以让通信驱动程序进行这些扩展操作。它还可以让驱动程序执行一些其他功能,如设置和清除BREAK条件。关于此函数的更多信息,请参考平台SDK文档,Win32 SDK知识库和MSDN。
5 串口设置
5.1 DCB设置
设备控制块(Device Control Block,DCB)的设置是串口编程中最重要的部分,很多通常的错误都跟没有正确设置DCB结构有关。GetCommState()函数可以获取当前正在使用的DCB结构;BuildCommDCB()函数可以填充DCB结构的波特率、校验类型、停止位数、数据位数字段;SetCommState()用于设置新的DCB结构。DCB设置的一般方法如下所示:
DCB dcb;
FillMemory(&dcb, sizeof(dcb), 0);
if (!GetCommState(hComm, &dcb)) // get current DCB
// Error in GetCommState
return FALSE;// Update DCB rate.
dcb.BaudRate = CBR_9600 ;// Set new state.
if (!SetCommState(hComm, &dcb))
// Error in SetCommState. Possibly a problem with the communications
// port handle or a problem with the DCB structure itself.
6 流控制
流控制可以在通信的某一方忙或者由于其他原因不能进行通信时暂停通信。通常有两种流控制:硬件流控制和软件流控制。串行通信中一个通常的问题是写操作实际上并没有把数据写入到设备中。通常,这是流控制的效果。此时,DCB结构的下列字段可能是TRUE:fOutxCtsFlow、fOutxDsrFlow或者fOutX。另一个确定流控制启用的方法是调用ClearCommError()并检查COMSTAT结构体,它可以反映传输因为流控制而暂停。
在详细讨论流控制前,最好了解下相关术语。串行通信发生在两个设备间,通常是PC和调制解调器或者打印机。PC称作数据终端设备(Data Terminal Equipment,DTE),有时也称为主机(host);调制解调器,打印机,或者其他外设称作数据通信设备(Data Communications Equipment,DCE),有时也称为设备(device)。
6.1 硬件流控制
硬件流控制使用串行线路中控制线的电平来控制收发。DTE和DCE必须就通信会话中使用的流控制类型进行协商。设置DCB结构体以启用流控制只是配置了DTE。此外还需要配置DCE以保证DTE和DCE使用相同类型的流控制,然而Win32没有提供设置DCE流控制的机制。通常要使用设备上的DIP开关,或者向设备发生命令来进行流控制配置。下表描述了控制线、流控制方向和线路对DTE与DCE的影响。
Line and Direction Effect on DTE/DCE CTS
(Clear To Send)
Output flow controlDCE sets the line high to indicate that it can receive data. DCE sets the line low to indicate that it cannot receive data. If the fOutxCtsFlow member of the DCB is TRUE, then the DTE will not send data if this line is low. It will resume sending if the line is high.
If the fOutxCtsFlow member of the DCB is FALSE, then the state of the line does not affect transmission.
DSR
(Data Set Ready)
Output flow controlDCE sets the line high to indicate that it can receive data. DCE sets the line low to indicate that it cannot receive data. If the fOutxDsrFlow member of the DCB is TRUE, then the DTE will not send data if this line is low. It will resume sending if the line is high.
If the fOutxDsrFlow member of the DCB is FALSE, then the state of the line does not affect transmission.
DSR
(Data Set Ready)
Input flow controlIf the DSR line is low, then data that arrives at the port is ignored. If the DSR line is high, data that arrives at the port is received. This behavior occurs if the fDsrSensitivity member of the DCB is set to TRUE. If it is FALSE, then the state of the line does not affect reception.
RTS
(Ready To Send)
Input flow controlThe RTS line is controlled by the DTE. If the fRtsControl member of the DCB is set to RTS_CONTROL_HANDSHAKE, the following flow control is used: If the input buffer has enough room to receive data (at least half the buffer is empty), the driver sets the RTS line high. If the input buffer has little room for incoming data (less than a quarter of the buffer is empty), the driver sets the RTS line low.
If the fRtsControl member of the DCB is set to RTS_CONTROL_TOGGLE, the driver sets the RTS line high when data is available for sending. The driver sets the line low when no data is available for sending. Windows 95 ignores this value and treats it the same as RTS_CONTROL_ENABLE.
If the fRtsControl member of the DCB is set to RTS_CONTROL_ENABLE or RTS_CONTROL_DISABLE, the application is free to change the state of the line as it needs. Note that in this case, the state of the line does not affect reception.
The DCE will suspend transmission when the line goes low. The DCE will resume transmission when the line goes high.
DTR
(Data Terminal Ready)
Input flow controlThe DTR line is controlled by the DTE. If the fDtrControl member of the DCB is set to DTR_CONTROL_HANDSHAKE, the following flow control is used: If the input buffer has enough room to receive data (at least half the buffer is empty), the driver sets the DTR line high. If the input buffer has little room for incoming data (less than a quarter of the buffer is empty), the driver sets the DTR line low.
If the fDtrControl member of the DCB is set to DTR_CONTROL_ENABLE or DTR_CONTROL_DISABLE, the application is free to change the state of the line as it needs. In this case, the state of the line does not affect reception.
The DCE will suspend transmission when the line goes low. The DCE will resume transmission when the line goes high.
可以简单地认为CE_RXOVER错误发生时,就需要流控制了。CE_RXOVER错误表示接收缓冲区溢出,数据丢失。如果数据到达端口的速度比它被读取的数据快,则可能发生CE_RXOVER错误。增加输入缓冲区大小可能减小错误发生的频率,但不能完全解决问题。这时需要输入流控制。驱动程序检测到输入缓冲区快满的时候,会拉低输入流控制线电平,使得DCE停止传输,让DTE有足够的时间从输入缓冲区读取数据。当输入缓冲区有足够的空闲区域时,流控制线电平被设置为高,DCE继发送数据。
CE_OVERRUN是一种类似的错误,它表示数据在通信硬件和驱动程序完全接收原数据前,新的数据到达。当传输速度对于通信硬件或者CPU而言太快时可能发生CE_OVERRUN错误;操作系统没有时间为通信硬件服务时也可能发生这个错误。解决此问题的方法是降低传输速度,替换硬件,或者提升CPU速度。有时候第三方硬件驱动程序不能有效使用CPU资源也可能导致此错误。流控制可以减低CE_OVERRUN发生的频度,但不能完全解决问题。
6.2 软件流控制
软件流控制使用通信流中的数据来控制收发操作。因为软件流控制使用XOFF和XON这两个特殊字符,所以不能应用于二进制传输。软件流控制对基于文本的通信,或者不使用XOFF和XON的传输有效。要启用软件流控制,需设置DCB结构的fOutX和fInX字段为TRUE:fOutX控制输出流控制;fInX控制输入流控制。程序可以动态指定流控制字符,DCB结构的XoffChar字段指示输入和输出流控制使用的XOFF字符,XonChar则指定XON字符。对于输入流控制,XoffLim字段指示发送XOFF字符前输入缓冲区允许的最小可用空间大小;如果输入缓冲区可用空间大小小于这个值,则会发送XOFF字符。XonLim字段指示在发送XON字符前输入缓冲区中最小的数据字节数;如果输入缓冲区中的数据量小于此值,则会发送XON字符。下面描述了使用XOFF/XON流控制时DTE的行为
Table 4. Software flow-control behavior
Flow-control character Behavior XOFF received by DTE DTE transmission is suspended until XON is received. DTE reception continues. The fOutX member of the DCB controls this behavior. XON received by DTE If DTE transmission is suspended because of a previous XOFF character being received, DTE transmission is resumed. The fOutX member of the DCB controls this behavior. XOFF sent from DTE XOFF is automatically sent by the DTE when the receive buffer approaches full. The actual limit is dictated by the XoffLim member of the DCB. The fInXmember of the DCB controls this behavior. DTE transmission is controlled by the fTXContinueOnXoff member of the DCB as described below. XON sent from the DTE XON is automatically sent by the DTE when the receive buffer approaches empty. The actual limit is dictated by the XonLim member of the DCB. The fInXmember of the DCB controls this behavior.
如果输入控制启用了软件流控制,则DCB的fTXContinueOnXoff字段有效,它控制是否在系统自动发送XOFF字符后暂停传输。如果fTXContinueOnXoff为TRUE,则在接收缓冲区满,发送了XOFF字符后继续传输;否则暂停传输直到系统自动发送XON字符。使用软件流控制的DCE设备会在接收到XOFF字符后暂停发送。某些设备会在DTE发送XON字符后恢复发送,然而,有些DCE设备会在接收到任何字符后恢复发送。如果DTE在自动发送XOFF后继续传输,DCE会继续发送,使得XOFF失效。Win32 API没有提供让DTE与这些设备行为相同的机制。DCB结构没有提供字段以指示在接收到任何字符后恢复被暂停的传输。只有XON字符可以恢复传输。接收到XON和XOFF字符会让未决的读取操作返回零字节而完成,但应用程序不会读取到XON和XOFF字符,因为它们不在输入缓冲区中。很多程序,包括Windows中的超级终端,都可以让用户选择流控制类型:硬件流控制,软件流控制,或者不使用流控制。实际上,可以自由设置DCB结构中影响流控制的各个字段,来进行各种流控制配置,需要遵循的限制只是便于最终用户使用,当然也要考虑设备是否支持所有类型的流控制。
本文摘录积小流,成江海的博客。