串口异步读写
最近尝试写了一个串口读写的程序,学习并复习了一些知识点。本文首先讲异步读写注意点,然后讲串口的注意点。因为有些问题没有深入研究下去,所以本文也仅仅当做一个笔记。
1. 文件指针
我们使用ReadFile和WriteFile来进行读写,这两个API是用来读写文件的,在同步读写中,有一个文件指针的概念,但是在异步读写中,系统会忽略文件指针。
OVERLAPPED的结构如下:
typedef struct _OVERLAPPED { ULONG_PTR Internal; ULONG_PTR InternalHigh; DWORD Offset; DWORD OffsetHigh; HANDLE hEvent; } OVERLAPPED, *LPOVERLAPPED;
|
上述结构的5个成员,其中三个成员Offset、OffsetHigh和hEvent必须在调用ReadFile和WriteFile之前初始化,hEvent是一个通知事件,而Offset和OffsetHigh就是用来代替同步读写中文件指针的——用来指定本次读写的开始的位置。
非文件设备会忽略Offset和OffsetHigh——我们必须将这两个成员都初始化为0,否则I/O请求会失败,这时调用GetLastError会返回ERROR_INVALID_PARAMETER。
Internal和InternalHigh原本设计时是内部使用的,但是后来用途变了:Internal用作返回状态值;InternalHigh返回读取字符数。这些在后面有进一步说明。
2. 异步读写中实际读写的字符数
以ReadFile为例,函数定义如下:
BOOL WINAPI ReadFile( _In_ HANDLE hFile, _Out_ LPVOID lpBuffer, _In_ DWORD nNumberOfBytesToRead, _Out_opt_ LPDWORD lpNumberOfBytesRead, _Inout_opt_ LPOVERLAPPED lpOverlapped );
|
在同步读时,lpNumberOfBytesRead返回实际读取的数据长度,但是在异步读时,lpOverlapped不为NULL,此时lpNumberOfBytesRead可以设为NULL值。
ReadFile sets this value to zero before doing any work or error checking. Use NULL for this parameter if this is an asynchronous operation to avoid potentially erroneous results. |
返回数据的长度在lpOverlapped->InternalHigh中。
3. 触发事件内核对象
异步读写时,ReadFile和WriteFile中必须设置lpOverlapped值(不能置为NULL)。但是可以将lpOverlapped->hEvent置为NULL。当异步读写完成时,Windows系统会触发文件内核对象hFile,如果lpOverlapped->hEvent不是NULL值,Windows也会触发该事件。
因为hFile可以被多个异步读写同时共享,所以如果在hFile上的等待可能会被误触发(其他读写完成了,系统触发了hFile)。当只有一个读或者写时,可以使用hFile等待。其它情况应该使用lpOverlapped->hEvent事件等待。
但是有一点要明确,Windows会触发hFile和lpOverlapped->hEvent(如果有的话)。
4. lpOverlapped
当这个OVERLAPPED结构的地址通过ReadFile/WriteFile传给系统后,系统会一直使用这个数据块,直到读写完成。
完成时,系统会将读写数据长度放在InternalHigh中,将返回状态值放在Internal中。所以千万不要在读写结束前将这个OVERLAPPED内存块销毁或者挪作他用。
5. GetOverlappedResult
这个函数原型如下:
BOOL WINAPI GetOverlappedResult( _In_ HANDLE hFile, _In_ LPOVERLAPPED lpOverlapped, _Out_ LPDWORD lpNumberOfBytesTransferred, _In_ BOOL bWait );
|
这个函数的源代码可以看我以前的文章。从源码看以看出这个如下几点:
1) hFile不一定是必须的。只有在bWait为TRUE且lpOverlapped->hEvent为0时才会使用hFile.
2) lpOverlapped就是前面调用ReadFile、WriteFile时所使用的地址值,不能是其他的。
3) 这个函数的功能其实很简单:将lpOverlapped->InternalHigh赋给lpNumberOfByesTransferred;将lpOverlapped->Internal赋给返回值。
所以,当读写完成时,这个函数基本没有什么用。也因此,《Windows核心编程》基本就不用这个函数了。
HANDLE hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...); BYTE bBuffer[100]; OVERLAPPED o = { 0 }; o.Offset = 345; BOOL bReadDone = ReadFile(hFile, bBuffer, 100, NULL, &o); DWORD dwError = GetLastError(); if (!bReadDone && (dwError == ERROR_IO_PENDING)) { // 异步I/O,等待返回 WaitForSingleObject(hFile, INFINITE);//有时此处也会发生错误。 bReadDone = TRUE; } //返回了,不管是同步也是异步。 if (bReadDone) { // o.Internal包含错误值 // o.InternalHigh包含传输字节数 // bBuffer包含读取的数据 } else { // 发生错误,见dwError }
|
6. 串口通讯
应用程序实际上是直接和驱动程序打交道的。所以虽然是串口I/O操作,很多函数都是很快返回的。这可能有一定的好处,但是也引发一些不利的地方,比如不能了解PC串口上是否真正连接到设备上。就是说对一个空的COM口,大部分COMM函数都能成功返回的。
7. CreateFile打开串口设备
CreateFile函数原型如下:
HANDLE WINAPI CreateFile( _In_ LPCTSTR lpFileName, _In_ DWORD dwDesiredAccess, _In_ DWORD dwShareMode, _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes, _In_ DWORD dwCreationDisposition, _In_ DWORD dwFlagsAndAttributes, _In_opt_ HANDLE hTemplateFile ); |
lpFileName是指定要打开设备的名称。COM1至COM9口可以直接lpFileName指定为“COM1”..“COM9”。但是超过COM10这种方法就没有用了,必须使用“\\.\COM10”了。所以为了统一可以都使用“\\.\COMx”方式来指定名称。
8. SetCommTimeouts
串口通讯中使用该函数来设置超时(GetCommTimeouts用来获取超时设置)。超时设置会影响并决定何时完成读写(返回ReadFile/WriteFile或者WaitFor…)。
函数设置了一个COMMTIMEOUTS结构:
typedef struct _COMMTIMEOUTS { DWORD ReadIntervalTimeout; DWORD ReadTotalTimeoutMultiplier; DWORD ReadTotalTimeoutConstant; DWORD WriteTotalTimeoutMultiplier; DWORD WriteTotalTimeoutConstant; } COMMTIMEOUTS, *LPCOMMTIMEOUTS;
|
设置了超时后,只要满足条件,ReadFile/WriteFile都会正常返回(hEvent和hFile被触发)。
在WaitForSingleObject中:
DWORD WINAPI WaitForSingleObject( _In_ HANDLE hEvent, _In_ DWORD dwMilliseconds );
|
这里也有一个dwMilliseconds超时值,这和SetCommTimeouts设置的超时是不同的:
1) CommTimeouts超时,hHandle会被触发。WaitForSingleObject返回WAIT_OBJECT_0,ol.InternalHigh返回读写数据字节数。
2) dwMilliseconds超时,WaitForSingleObject返回WAIT_TIMEOUT。