使用WinAPI实现未知缓冲区大小的串口通信最优策略是什么
核心问题原因
串口是面向字节流的通信协议,本身不存在硬件层面的"消息结束"标识,和你熟悉的TCP recv() 逻辑不同:TCP连接关闭时会发送FIN包,recv() 收到FIN就会返回0代表流结束,但串口只要端口保持打开,没有主动关闭连接的信号,ReadFile 就会一直等待直到收到指定长度的字节,这就是你遇到阻塞的原因。
你提到的Arduino Serial.available() 本质是读取当前串口接收缓冲区中已经就绪的字节数,不是检测流结束,Windows API里也有对应功能,就是你代码中已经用到的 COMSTAT 结构体的 cbInQue 字段。
最优实现建议
1. 首先约定串口通信的帧规则
这是所有不定长串口通信的通用前提,推荐根据你的使用场景二选一:
- 特殊分隔符帧:Wemos发送消息时,末尾统一加固定分隔符(比如
\r\n或者自定义的特殊字符),PC端读到分隔符就判定为一帧结束,适合传输字符串类消息 - 长度头帧:每帧前2个字节固定存后续消息体的长度,PC端先读2字节拿到长度,再读对应长度的内容,适合传输二进制数据
2. 调整读取逻辑,替换固定长度读取
不管是同步还是重叠模式,都不要一次性读取固定长度的字节,改成"分块读取+缓存拼接+帧检测"的逻辑:
- 触发
EV_RXCHAR事件后,先调用ClearCommError(hCOM, &errors, &status),拿到status.cbInQue(当前接收缓冲区已就绪的字节数) - 读取
cbInQue个字节,追加到类成员的接收缓存中(不要用局部动态数组,避免内存泄漏) - 检查接收缓存中是否存在符合约定的完整帧:如果有,取出完整帧处理,剩余未读完的字节留在缓存中等待下一次读取;如果没有,继续等待下一次
EV_RXCHAR事件
3. 现有代码的问题修复
- 非重叠模式中,你忽略了
ReadFile返回的nBytesRead实际读取长度,直接循环打印length个字符,会输出未初始化的垃圾值 - 两个实现中
new申请的message数组都没有释放,存在内存泄漏 - 重叠模式中
CreateEvent创建的事件句柄没有调用CloseHandle释放,会造成句柄泄漏
参考实现(同步模式示例)
// 类成员变量定义:std::string recv_buf; 用来缓存未处理的接收数据 void Serial::ReadSerialPort() { DWORD nBytesRead; DWORD dwEvtMask; DWORD errors; BOOL bResult; COMSTAT status; char temp_buf[256]; // 临时读缓存,大小可根据实际场景调整 SetCommMask(hCOM, EV_RXCHAR | EV_ERR); while (WaitCommEvent(hCOM, &dwEvtMask, 0)) { if (dwEvtMask & EV_ERR) { std::cout << "error: " << dwEvtMask << std::endl; continue; } if (dwEvtMask & EV_RXCHAR) { ClearCommError(hCOM, &errors, &status); // 只读当前已就绪的字节数,不会阻塞 DWORD to_read = min(status.cbInQue, sizeof(temp_buf)); bResult = ReadFile(hCOM, temp_buf, to_read, &nBytesRead, 0); if (bResult && nBytesRead > 0) { // 追加到全局接收缓存 recv_buf.append(temp_buf, nBytesRead); // 检测是否有完整帧(这里以\n为分隔符举例) size_t pos; while ((pos = recv_buf.find('\n')) != std::string::npos) { std::string message = recv_buf.substr(0, pos); // 处理完整消息,这里直接打印 std::cout << "收到消息:" << message << std::endl; // 移除已处理的帧,保留剩余内容 recv_buf.erase(0, pos + 1); } } } } }
内容的提问来源于stack exchange,提问作者Caio Peres
相关产品推荐
相关产品推荐

