串口重叠操作返回错误码87及Arduino通信异常问题排查
Arduino Nano串口重叠I/O通信问题解决
问题概述
- 测试场景:Arduino Nano每秒通过串口发送字符
p,Windows端使用**重叠I/O(overlapped I/O)**模式,通过WaitCommEvent监听串口事件 - 遇到的问题:
- 移除
WaitCommEvent前的Sleep调用后,无法接收并显示Arduino发送的字符 SetCommMask必须放在循环内,否则WaitCommEvent返回错误码87(ERROR_INVALID_PARAMETER)
- 移除
用户原代码
BOOL Status; // Status // Defining COM port id const wchar_t PortNo[20] = L"\\\\.\\COM4"; //Open the serial com port HANDLE hComm; // Handle to the Serial port printf("Opening %ls\n", PortNo); hComm = CreateFile(PortNo, //friendly name GENERIC_READ | GENERIC_WRITE, // Read/Write Access 0, // No Sharing, ports cant be shared NULL, // No Security OPEN_EXISTING, // Open existing port only FILE_FLAG_OVERLAPPED, // Overlapped I/O NULL); // Null for Comm Devices if (hComm == INVALID_HANDLE_VALUE) { printf_s("\n Port %ls can't be opened\n\n", PortNo); } //Setting the Parameters for the SerialPort DCB dcbSerialParams = { 0 }; // Initializing DCB structure dcbSerialParams.DCBlength = sizeof(dcbSerialParams); Status = GetCommState(hComm, &dcbSerialParams); //retreives the current settings dcbSerialParams.BaudRate = CBR_9600; //BaudRate = 9600 dcbSerialParams.ByteSize = 8; //ByteSize = 8 dcbSerialParams.StopBits = ONESTOPBIT; //StopBits = 1 dcbSerialParams.Parity = NOPARITY; //Parity = None Status = SetCommState(hComm, &dcbSerialParams); EXIT_ON_ERROR(Status, __LINE__); //Setting Timeouts COMMTIMEOUTS timeouts = { 0 }; //Initializing timeouts structure timeouts.ReadIntervalTimeout = 50; timeouts.ReadTotalTimeoutConstant = 50; timeouts.ReadTotalTimeoutMultiplier = 10; timeouts.WriteTotalTimeoutConstant = 50; timeouts.WriteTotalTimeoutMultiplier = 10; if (SetCommTimeouts(hComm, &timeouts) == FALSE) { printf_s("\nError to Setting Time outs"); CloseHandle(hComm); } DWORD BytesWritten = 0; // No of bytes written to the port DWORD dwEventMask; // Event mask to trigger DWORD NoBytesRead; // Bytes read by ReadFile() int index = 0; OVERLAPPED o = { 0 }; o.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); if (o.hEvent == NULL) return FALSE; // Error creating overlapped event handle. assert(o.hEvent); DWORD dwRes; while (1) { Status = SetCommMask(hComm, EV_RXCHAR); if (Status == FALSE) { printf_s("\nError to in Setting CommMask\n\n"); CloseHandle(hComm); } //Setting WaitComm() Event char ReadData[100] = { 0 }; //temporary Character Status = WaitCommEvent(hComm, &dwEventMask, &o); //Wait for the character to be received if (GetLastError() != ERROR_IO_PENDING) { printf("IO_NOT_PENDING %d\n", GetLastError()); } DWORD lpModemStat; dwRes = WaitForSingleObject(o.hEvent, INFINITE); switch (dwRes) { case WAIT_OBJECT_0: if (dwEventMask & EV_RXCHAR) { Status = ReadFile(hComm, &ReadData, sizeof(ReadData), &NoBytesRead, &o); ResetEvent(o.hEvent); for (int i = 0; i < 100; i++) { std::cout << ReadData[i]; } } break; case WAIT_TIMEOUT: break; default: break; } }
问题原因分析
1. SetCommMask必须放循环内的原因
Windows串口API中,WaitCommEvent在完成异步操作后会自动清除当前设置的通信掩码。如果不在每次调用WaitCommEvent前重新调用SetCommMask设置EV_RXCHAR,后续的WaitCommEvent会因为掩码无效而返回错误码87(参数不正确),这是API的既定行为。
2. 移除Sleep后无法接收字符的原因
- 重叠操作的结果未正确获取:原代码中,
WaitCommEvent返回ERROR_IO_PENDING后,直接通过WaitForSingleObject等待事件,但未调用GetOverlappedResult获取异步操作的最终结果,导致dwEventMask的值可能未被正确更新,后续判断EV_RXCHAR事件可能失效。 - OVERLAPPED结构体复用冲突:原代码中
WaitCommEvent和ReadFile复用同一个OVERLAPPED结构体,事件对象的状态会被两个异步操作干扰,导致信号触发逻辑混乱。 - 读取结果处理错误:原代码固定输出100个字符,但实际每次只收到1个字符,多余的空字符输出会掩盖有效数据,且未正确判断
ReadFile的异步操作结果。
修正后的代码
#include <windows.h> #include <stdio.h> #include <iostream> #include <assert.h> #define EXIT_ON_ERROR(expr, line) if(!(expr)) { printf("Error at line %d\n", line); CloseHandle(hComm); return FALSE; } int main() { BOOL Status; const wchar_t PortNo[20] = L"\\\\.\\COM4"; HANDLE hComm = CreateFile(PortNo, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); if (hComm == INVALID_HANDLE_VALUE) { printf_s("\n Port %ls can't be opened\n\n", PortNo); return FALSE; } // 配置串口参数 DCB dcbSerialParams = { 0 }; dcbSerialParams.DCBlength = sizeof(dcbSerialParams); Status = GetCommState(hComm, &dcbSerialParams); EXIT_ON_ERROR(Status, __LINE__); dcbSerialParams.BaudRate = CBR_9600; dcbSerialParams.ByteSize = 8; dcbSerialParams.StopBits = ONESTOPBIT; dcbSerialParams.Parity = NOPARITY; Status = SetCommState(hComm, &dcbSerialParams); EXIT_ON_ERROR(Status, __LINE__); // 配置超时 COMMTIMEOUTS timeouts = { 0 }; timeouts.ReadIntervalTimeout = 50; timeouts.ReadTotalTimeoutConstant = 50; timeouts.ReadTotalTimeoutMultiplier = 10; timeouts.WriteTotalTimeoutConstant = 50; timeouts.WriteTotalTimeoutMultiplier = 10; if (!SetCommTimeouts(hComm, &timeouts)) { printf_s("\nError Setting Timeouts\n"); CloseHandle(hComm); return FALSE; } // 为WaitCommEvent和ReadFile分别创建OVERLAPPED结构体,避免冲突 OVERLAPPED ovWait = { 0 }; ovWait.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); EXIT_ON_ERROR(ovWait.hEvent != NULL, __LINE__); OVERLAPPED ovRead = { 0 }; ovRead.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); EXIT_ON_ERROR(ovRead.hEvent != NULL, __LINE__); DWORD dwEventMask; DWORD NoBytesRead; char ReadData[100]; while (1) { // 每次WaitCommEvent前必须重新设置掩码 Status = SetCommMask(hComm, EV_RXCHAR); if (!Status) { printf_s("\nError Setting CommMask\n"); break; } // 异步等待接收事件 Status = WaitCommEvent(hComm, &dwEventMask, &ovWait); if (!Status) { if (GetLastError() != ERROR_IO_PENDING) { printf("WaitCommEvent Error: %d\n", GetLastError()); break; } } // 等待事件触发 DWORD dwRes = WaitForSingleObject(ovWait.hEvent, INFINITE); if (dwRes != WAIT_OBJECT_0) { printf("WaitForSingleObject Error: %d\n", GetLastError()); break; } // 获取异步操作的最终结果,更新dwEventMask if (!GetOverlappedResult(hComm, &ovWait, NULL, FALSE)) { printf("GetOverlappedResult (WaitCommEvent) Error: %d\n", GetLastError()); break; } ResetEvent(ovWait.hEvent); // 处理接收事件 if (dwEventMask & EV_RXCHAR) { memset(ReadData, 0, sizeof(ReadData)); // 异步读取串口数据 Status = ReadFile(hComm, ReadData, sizeof(ReadData)-1, &NoBytesRead, &ovRead); if (!Status) { if (GetLastError() == ERROR_IO_PENDING) { WaitForSingleObject(ovRead.hEvent, INFINITE); if (!GetOverlappedResult(hComm, &ovRead, &NoBytesRead, FALSE)) { printf("GetOverlappedResult (ReadFile) Error: %d\n", GetLastError()); continue; } } else { printf("ReadFile Error: %d\n", GetLastError()); continue; } } ResetEvent(ovRead.hEvent); // 只输出实际读取到的字节数 if (NoBytesRead > 0) { std::cout << "Received: "; for (DWORD i = 0; i < NoBytesRead; i++) { std::cout << ReadData[i]; } std::cout << std::endl; } } } // 清理资源 CloseHandle(ovWait.hEvent); CloseHandle(ovRead.hEvent); CloseHandle(hComm); return TRUE; }
关键修改点
- 分离OVERLAPPED结构体:为
WaitCommEvent和ReadFile分别创建独立的OVERLAPPED对象,避免事件信号互相干扰。 - 正确获取异步结果:在
WaitForSingleObject后调用GetOverlappedResult,确保dwEventMask和读取字节数的正确性。 - 按需输出数据:根据
NoBytesRead的值输出实际收到的字符,避免空字符干扰。 - 规范资源清理:在程序退出时关闭所有创建的事件句柄和串口句柄。
内容的提问来源于stack exchange,提问作者l_KarmaKat
相关产品推荐
相关产品推荐

