上一页 下一页 返回

12.3 串行通信与重叠I/O

  Win 32系统为串行通信提供了全新的服务。传统的OpenComm、ReadComm、WriteComm、CloseComm等函数已经过时,WM_COMMNOTIFY消息也消失了。取而代之的是文件I/O函数提供的打开和关闭通信资源句柄及读写操作的基本接口。

  新的文件I/O函数(CreateFile、ReadFile、WriteFile等)支持重叠式输入输出,这使得线程可以从费时的I/O操作中解放出来,从而极大地提高了程序的运行效率。

12.3.1 串行口的打开和关闭

  Win 32系统把文件的概念进行了扩展。无论是文件、通信设备、命名管道、邮件槽、磁盘、还是控制台,都是用API函数CreateFile来打开或创建的。该函数的声明为:

HANDLE CreateFile(

LPCTSTR lpFileName, // 文件名

DWORD dwDesiredAccess, // 访问模式

DWORD dwShareMode, // 共享模式

LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 通常为NULL

DWORD dwCreationDistribution, // 创建方式

DWORD dwFlagsAndAttributes, // 文件属性和标志

HANDLE hTemplateFile // 临时文件的句柄,通常为NULL

);

  如果调用成功,那么该函数返回文件的句柄,如果调用失败,则函数返回INVALID_HANDLE_VALUE。

  如果想要用重叠I/O方式(参见12.3.3)打开COM2口,则一般应象清单12.4那样调用CreateFile函数。注意在打开一个通信端口时,应该以独占方式打开,另外要指定GENERIC_READ、GENERIC_WRITE、OPEN_EXISTING和FILE_ATTRIBUTE_NORMAL等属性。如果要打开重叠I/O,则应该指定 FILE_FLAG_OVERLAPPED属性。

 

清单12.4

HANDLE hCom;

DWORD dwError;

hCom=CreateFile(“COM2”, // 文件名

GENERIC_READ | GENERIC_WRITE, // 允许读和写

0, // 独占方式

NULL,

OPEN_EXISTING, //打开而不是创建

FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, // 重叠方式

NULL

);

if(hCom = = INVALID_HANDLE_VALUE)

{

dwError=GetLastError( );

. . . // 处理错误

}

当不再使用文件句柄时,应该调用CloseHandle函数关闭之。

12.3.2 串行口的初始化

  在打开通信设备句柄后,常常需要对串行口进行一些初始化工作。这需要通过一个DCB结构来进行。DCB结构包含了诸如波特率、每个字符的数据位数、奇偶校验和停止位数等信息。在查询或配置置串行口的属性时,都要用DCB结构来作为缓冲区。

  调用GetCommState函数可以获得串口的配置,该函数把当前配置填充到一个DCB结构中。一般在用CreateFile打开串行口后,可以调用GetCommState函数来获取串行口的初始配置。要修改串行口的配置,应该先修改DCB结构,然后再调用SetCommState函数用指定的DCB结构来设置串行口。

  除了在DCB中的设置外,程序一般还需要设置I/O缓冲区的大小和超时。Windows用I/O缓冲区来暂存串行口输入和输出的数据,如果通信的速率较高,则应该设置较大的缓冲区。调用SetupComm函数可以设置串行口的输入和输出缓冲区的大小。

  在用ReadFile和WriteFile读写串行口时,需要考虑超时问题。如果在指定的时间内没有读出或写入指定数量的字符,那么ReadFile或WriteFile的操作就会结束。要查询当前的超时设置应调用GetCommTimeouts函数,该函数会填充一个COMMTIMEOUTS结构。调用SetCommTimeouts可以用某一个COMMTIMEOUTS结构的内容来设置超时。

  有两种超时:间隔超时和总超时。间隔超时是指在接收时两个字符之间的最大时延,总超时是指读写操作总共花费的最大时间。写操作只支持总超时,而读操作两种超时均支持。用COMMTIMEOUTS结构可以规定读/写操作的超时,该结构的定义为:

typedef struct _COMMTIMEOUTS {

DWORD ReadIntervalTimeout; // 读间隔超时

DWORD ReadTotalTimeoutMultiplier; // 读时间系数

DWORD ReadTotalTimeoutConstant; // 读时间常量

DWORD WriteTotalTimeoutMultiplier; // 写时间系数

DWORD WriteTotalTimeoutConstant; // 写时间常量

} COMMTIMEOUTS,*LPCOMMTIMEOUTS;

  COMMTIMEOUTS结构的成员都以毫秒为单位。总超时的计算公式是:

总超时=时间系数×要求读/写的字符数 + 时间常量

  例如,如果要读入10个字符,那么读操作的总超时的计算公式为:

读总超时=ReadTotalTimeoutMultiplier×10 + ReadTotalTimeoutConstant

  可以看出,间隔超时和总超时的设置是不相关的,这可以方便通信程序灵活地设置各种超时。

  如果所有写超时参数均为0,那么就不使用写超时。如果ReadIntervalTimeout为0,那么就不使用读间隔超时,如果ReadTotalTimeoutMultiplier和ReadTotalTimeoutConstant都为0,则不使用读总超时。如果读间隔超时被设置成MAXDWORD并且两个读总超时为0,那么在读一次输入缓冲区中的内容后读操作就立即完成,而不管是否读入了要求的字符。

  在用重叠方式读写串行口时,虽然ReadFile和WriteFile在完成操作以前就可能返回,但超时仍然是起作用的。在这种情况下,超时规定的是操作的完成时间,而不是ReadFile和WriteFile的返回时间。

清单12.5列出了一段简单的串行口初始化代码。

 

清单12.5 打开并初始化串行口

HANDLE hCom;

DWORD dwError;

DCB dcb;

COMMTIMEOUTS TimeOuts;

hCom=CreateFile(“COM2”, // 文件名

GENERIC_READ | GENERIC_WRITE, // 允许读和写

0, // 独占方式

NULL,

OPEN_EXISTING, //打开而不是创建

FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, // 重叠方式

NULL

);

if(hCom = = INVALID_HANDLE_VALUE)

{

dwError=GetLastError( );

. . . // 处理错误

}

 

SetupComm( hCom, 1024, 1024 ) //缓冲区的大小为1024

 

TimeOuts. ReadIntervalTimeout=1000;

TimeOuts.ReadTotalTimeoutMultiplier=500;

TimeOuts.ReadTotalTimeoutConstant=5000;

TimeOuts.WriteTotalTimeoutMultiplier=500;

TimeOuts.WriteTotalTimeoutConstant=5000;

SetCommTimeouts(hCom, &TimeOuts); // 设置超时

 

GetCommState(hCom, &dcb);

dcb.BaudRate=2400; // 波特率为2400

dcb.ByteSize=8; // 每个字符有8位

dcb.Parity=NOPARITY; //无校验

dcb.StopBits=ONESTOPBIT; //一个停止位

SetCommState(hCom, &dcb);

 

12.3.3 重叠I/O

  在用ReadFile和WriteFile读写串行口时,既可以同步执行,也可以重叠(异步)执行。在同步执行时,函数直到操作完成后才返回。这意味着在同步执行时线程会被阻塞,从而导致效率下降。在重叠执行时,即使操作还未完成,调用的函数也会立即返回。费时的I/O操作在后台进行,这样线程就可以干别的事情。例如,线程可以在不同的句柄上同时执行I/O操作,甚至可以在同一句柄上同时进行读写操作。“重叠”一词的含义就在于此。

  ReadFile函数只要在串行口输入缓冲区中读入指定数量的字符,就算完成操作。而WriteFile函数不但要把指定数量的字符拷入到输出缓冲中,而且要等这些字符从串行口送出去后才算完成操作。

  ReadFile和WriteFile函数是否为执行重叠操作是由CreateFile函数决定的。如果在调用CreateFile创建句柄时指定了FILE_FLAG_OVERLAPPED标志,那么调用ReadFile和WriteFile对该句柄进行的读写操作就是重叠的,如果未指定重叠标志,则读写操作是同步的。

  函数ReadFile和WriteFile的参数和返回值很相似。这里仅列出ReadFile函数的声明:

BOOL ReadFile(

HANDLE hFile, // 文件句柄

LPVOID lpBuffer, // 读缓冲区

DWORD nNumberOfBytesToRead, // 要求读入的字节数

LPDWORD lpNumberOfBytesRead, // 实际读入的字节数

LPOVERLAPPED lpOverlapped // 指向一个OVERLAPPED结构

); //若返回TRUE则表明操作成功

 

  需要注意的是如果该函数因为超时而返回,那么返回值是TRUE。参数lpOverlapped在重叠操作时应该指向一个OVERLAPPED结构,如果该参数为NULL,那么函数将进行同步操作,而不管句柄是否是由FILE_FLAG_OVERLAPPED标志建立的。

  当ReadFile和WriteFile返回FALSE时,不一定就是操作失败,线程应该调用GetLastError函数分析返回的结果。例如,在重叠操作时如果操作还未完成函数就返回,那么函数就返回FALSE,而且GetLastError函数返回ERROR_IO_PENDING。

  在使用重叠I/O时,线程需要创建OVERLAPPED结构以供读写函数使用。OVERLAPPED结构最重要的成员是hEvent,hEvent是一个事件对象句柄,线程应该用CreateEvent函数为hEvent成员创建一个手工重置事件,hEvent成员将作为线程的同步对象使用。如果读写函数未完成操作就返回,就那么把hEvent成员设置成无信号的。操作完成后(包括超时),hEvent会变成有信号的。

  如果GetLastError函数返回ERROR_IO_PENDING,则说明重叠操作还为完成,线程可以等待操作完成。有两种等待办法:一种办法是用象WaitForSingleObject这样的等待函数来等待OVERLAPPED结构的hEvent成员,可以规定等待的时间,在等待函数返回后,调用GetOverlappedResult。另一种办法是调用GetOverlappedResult函数等待,如果指定该函数的bWait参数为TRUE,那么该函数将等待OVERLAPPED结构的hEvent 事件。GetOverlappedResult可以返回一个OVERLAPPED结构来报告包括实际传输字节在内的重叠操作结果。

  如果规定了读/写操作的超时,那么当超过规定时间后,hEvent成员会变成有信号的。因此,在超时发生后,WaitForSingleObject和GetOverlappedResult都会结束等待。WaitForSingleObject的dwMilliseconds参数会规定一个等待超时,该函数实际等待的时间是两个超时的最小值。注意GetOverlappedResult不能设置等待的时限,因此如果hEvent成员无信号,则该函数将一直等待下去。

  在调用ReadFile和WriteFile之前,线程应该调用ClearCommError函数清除错误标志。该函数负责报告指定的错误和设备的当前状态。

  调用PurgeComm函数可以终止正在进行的读写操作,该函数还会清除输入或输出缓冲区中的内容。

 

12.3.4 通信事件

  在Windows 95/NT中,WM_COMMNOTIFY消息已经取消,在串行口产生一个通信事件时,程序并不会收到通知消息。线程需要调用WaitCommEvent函数来监视发生在串行口中的各种事件,该函数的第二个参数返回一个事件屏蔽变量,用来指示事件的类型。线程可以用SetCommMask建立事件屏蔽以指定要监视的事件,表12.4列出了可以监视的事件。调用GetCommMask可以查询串行口当前的事件屏蔽。

 

表12.4 通信事件

事件屏蔽

含义

EV_BREAK

检测到一个输入中断

EV_CTS

CTS信号发生变化

EV_DSR

DSR信号发生变化

EV_ERR

发生行状态错误

EV_RING

检测到振铃信号

EV_RLSD

RLSD(CD)信号发生变化

EV_RXCHAR

输入缓冲区接收到新字符

EV_RXFLAG

输入缓冲区收到事件字符

EV_TXEMPTY

发送缓冲区为空

  WaitCommEvent即可以同步使用,也可以重叠使用。如果串口是用FILE_FLAG_OVERLAPPED标志打开的,那么WaitCommEvent就进行重叠操作,此时该函数需要一个OVERLAPPED结构。线程可以调用等待函数或GetOverlappedResult函数来等待重叠操作的完成。

  当指定范围内的某一事件发生后,线程就结束等待并把该事件的屏蔽码设置到事件屏蔽变量中。需要注意的是,WaitCommEvent只检测调用该函数后发生的事件。例如,如果在调用WaitCommEvent前在输入缓冲区中就有字符,则不会因为这些字符而产生EV_RXCHAR事件。

  如果检测到输入的硬件信号(如CTS、RTS和CD信号等)发生了变化,线程可以调用GetCommMaskStatus函数来查询它们的状态。而用EscapeCommFunction函数可以控制输出的硬件信号(如DTR和RTS信号)。