一 问题的由来
关于VC++串口重叠IO通信,一直有些细节不清楚, 刚好要做一个串口通信类,调试时遇到问题了, 在使用重叠IO方式打开串口后,使用重叠方式读取出口数据时发现read函数总是直接返回TRUE,但获得的字节数却是0,代码如下:
int i =0;
DWORD dwRead=0;
char buf[1024];
//使用重叠IO操作, 直到超时返回
OVERLAPPED Overlapped;
memset(&Overlapped, 0, sizeof(Overlapped));
Overlapped.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); //初始化无信号状态
BOOL bRet = ReadFile(m_hComm, buf, sizeof(buf), &dwRead, &Overlapped);
DWORD Err = ::GetLastError();
调试断点状态结果 :
bRet : TRUE;
dwRead : 0;
Err :0;
// 一切都很正常, 好像没什么不对
==================================
疑问: 我使用的USB转232, TX和RX是使用跳线短接的, 在read之前, 先执行了write函数,读到的数据长度为0,这没有道理啊?
于是在Read之前加了几行代码, 查询串口缓冲区内数据:
COMSTAT ComStat;
DWORD dwErrorFlags;
ClearCommError(m_hComm, &dwErrorFlags, &ComStat);
得到ComStat.cbInQue=11, 这11个字节刚好是我发送的字节数, 说明串口缓冲区里面的数据时有的
但是, 读取到的数据长度仍然是0,调用均OK, 这令人有点崩溃了!
二 解决问题的线索
我是使用重叠方式打开串口的,这个结果肯定和重叠IO有关系,于是一阵狂搜, 在这里找到了提示:
https://wangbaiyuan.cn/c-serial-communication-write-reading.html
其中提到:
在用重叠方式读写串口时,虽然ReadFile和WriteFile在完成操作以前就可能返回,但超时仍然是起作用的。
在这种情况下,超时规定的是操作的完成时间,而不是ReadFile和WriteFile的返回时间(重叠IO将立即返回)
如果读间隔超时被设置成MAXDWORD并且读时间系数和读时间常量都为0,那么在读一次输入缓冲区的内容后读操作就立即返回,而不管是否读入了要求的字符 --------------这句话是重点所在了
在根据这个提示,串口打开后, 先做如下的读写超时设置:
COMMTIMEOUTS TimeOuts;
//读总超时=ReadTotalTimeoutMultiplier×N+ReadTotalTimeoutConstant
TimeOuts.ReadIntervalTimeout = 0/*MAXDWORD*/; //如果ReadIntervalTimeout为0,那么就不使用读间隔超时
//如果读间隔超时被设置成MAXDWORD并且读时间系数和读时间常量都为0,那么在读一次输入缓冲区的内容后读操作就立即返回,而不管是否读入了要求的字符。
TimeOuts.ReadTotalTimeoutMultiplier = 0; //如果ReadTotalTimeoutMultiplier 和 ReadTotalTimeoutConstant 都为0,则不使用读总超时
TimeOuts.ReadTotalTimeoutConstant = 0;
//在读一次输入缓冲区的内容后读操作就立即返回,
//而不管是否读入了要求的字符。
//设定写超时---------------------------------------------如果所有写超时参数均为0,那么就不使用写超时
TimeOuts.WriteTotalTimeoutMultiplier = 100;
TimeOuts.WriteTotalTimeoutConstant = 500;
SetCommTimeouts(m_hComm, &TimeOuts); //设置超时
》》》 修改后执行程序:
BOOL bRet = ReadFile(m_hComm, buf, sizeof(buf), &dwRead, &Overlapped);
bRet的返回不在是TRUE了, 而是FALSE; 而 ::GetLastError() 的结是ERROR_IO_PENDING,这正是串口重叠IO读操作的正确姿势啊!
======= 但问题有来了, 明明ComStat.cbInQue=11, 但是
=======在重叠IO超时后,使用::GetOverlappedResult(m_hComm, &Overlapped, &dwRead, FALSE);
=======获得的返回数据长度仍然为0, 代码如下:
if(::GetLastError() == ERROR_IO_PENDING){
DWORD dwRet =0;
if(0 == ovt_sec) ovt_sec=1; //因为使用了异步IO, 必须设置相应的延时,否则read数据始终=0
dwRet = WaitForSingleObject(Overlapped.hEvent, ovt_sec*1000);
::GetOverlappedResult(m_hComm, &Overlapped, &dwRead, FALSE);
//dwRet 返回的值是 WAIT_TIMEOUT
if (WAIT_OBJECT_0 != dwRet){
ClearCommError(m_hComm, &dwErrorFlags, &ComStat);
if (ComStat.cbInQue)
ReadFile(m_hComm, buf, ComStat.cbInQue, &dwRead, &Overlapped);
NetPrintf("\t 读串口[%d]超时(%dms), 返回%d字节!\n", m_comPort, ovt_sec * 1000, dwRead);
}
}
》》》 那我读一个字节试试:
BOOL bRet = ReadFile(m_hComm, buf, sizeof(buf), &dwRead, &Overlapped);
改成:
BOOL bRet = ReadFile(m_hComm, buf, 1, &dwRead, &Overlapped);
这次的结果变成:
bRet返回TRUE, dwRead 返回1 ---------- 读数据正常了
增加,读取的字节数,一直到11 (注意ClearCommError查询到缓冲区里面的数据正是11), ReadFile都是返回TRUE
当读取的字节数增加到12时, ReadFile都是返回TRUE, 进入if(::GetLastError() == ERROR_IO_PENDING)代码块;
问题来了:
IO_PENDING超时后,::GetOverlappedResult(m_hComm, &Overlapped, &dwRead, FALSE); 得到dwRead=0
诡异的是: 再次ClearCommError查询缓冲区内数据, 结果为0了, 说明数据被读走了,
如果缓冲区内数据量是未知的(串口硬件可能还在继续接收数据),或者数据长度是个不确定长度的值,我怎么知道我我通过重叠IO读取了多少数据?
三 问题的解决
以上说明我对串口重叠IO操作并没有深刻地理解, 有哪个地方出现了偏差,完全是在按照自己的想法在使用重叠IO,而且直接从网上抄了一段代码,为经过充分验证。
如果,我只是ClearCommError查询缓冲区内数据的长度来读取缓冲区, 则ReadFile返回TRUE, 重叠IO根本就用不上啊!
如果我读取未知数量的数据(ReadFile的读取期望长度参数值通常会比较大), 但是我的不到到底读取了多少字节数据啊!
选择相信微软, 肯定是自己哪里还有问题。再回顾一下:
在用重叠方式读写串口时,虽然ReadFile和WriteFile在完成操作以前就可能返回,但超时仍然是起作用的。
在这种情况下,超时规定的是操作的完成时间,而不是和WriteFile的返回时间(重叠IO将立即返回)
如果: ReadFile的返回时间比超时规定的是操作的完成时间要早, 会发生什么呢?做个试验吧:
》》》修改读超时时间为1秒
COMMTIMEOUTS TimeOuts;
TimeOuts.ReadIntervalTimeout = 0/*MAXDWORD*/;
TimeOuts.ReadTotalTimeoutMultiplier = 0;
TimeOuts.ReadTotalTimeoutConstant = 1000;
WaitForSingleObject(Overlapped.hEvent, 2*1000);
奇迹发生了:
WaitForSingleObject返回WAIT_OBJECT_0, 而不是超时WAIT_TIMEOUT
::GetOverlappedResult(m_hComm, &Overlapped, &dwRead, FALSE);
dwRead 也获得了返回的字节数11
四 总结 (个人理解,未找到文档做依据)
以上充分说明, 在对串口的重叠IO操作过程中,读写超时参数 COMMTIMEOUTS TimeOuts;仍然在发挥着重要作用
WaitForSingleObject是否获得信号, 实际上是ReadFile函数是否完成的信号。
如果WaitForSingleObject返回超时, 说明ReadFile函数的超时还没有结束。
所以如果我需要读取不定长度的数据, 最好把ReadFile超时设置的小于WaitForSingleObject超时,这样,读超时结束后,WaitForSingleObjec可以获得信号, 并通过 ::GetOverlappedResult(m_hComm, &Overlapped, &dwRead, FALSE);获得实际读取到的数据量。
也可以每次只读取一个字节,这样,只要有一个字节到来, WaitForSingleObjec就可以获得信号, 然后根据剩余的超时时间决定是否发起下一轮的ReadFile调用。
== 对于写超时, 因为串口是个低速设备, 当用于大量数据的发送时,可能写超时才会发生作用, 一般的应用中, 由于串口write缓冲区可能会大于写入的字节数,所以一般不会出现超时(witeFile一般会立即返回)。
我有个未经验证的猜想,如果我把串口缓冲区设置的小于单次发送的数据量,这个时候,写超时机制应该就会发挥作用了吧?
如果我们想读写可靠,要根据不同的速率和应用实际情况设置合适的值。
和同步模式和异步模式、不同的通信协议无关。