使用USB路径打开通信设备时如何为CreateFile()句柄的ReadFile()设置超时
我正在扩展热敏小票打印机的串口(COM)打印接口,使其无需虚拟串口即可支持USB接口。目前我已完成可用原型,可枚举已连接的USB设备,定位指定供应商ID(VID)和产品ID(PID)设备的USB路径,并通过CreateFile()打开设备连接。
现有串口代码使用封装了Windows API的函数库实现,我的方案是在同一套函数框架下新增适配USB连接的代码,无需修改原有核心逻辑。此前我已通过相同方案成功实现后厨打印机同时支持串口和WiFi/LAN连接的需求。
但现有函数库依赖ReadFile()的超时机制:若热敏打印机在合理时间内未响应状态请求,应用可标记设备故障,继续执行业务逻辑或切换至备用打印机。
通过USB路径调用CreateFile()打开通信设备获取的文件句柄,如何为其ReadFile()操作指定超时时间?
额外说明
相关代码为多线程架构,可同时对接多种串口通信设备(小票打印机、后厨打印机、称重设备等),且每个线程独占对应设备的访问权限(后厨打印线程仅访问后厨打印机串口、称重线程仅访问称重设备串口等)。
现有串口代码中,针对CreateFile()打开的串口连接,使用SetCommTimeouts()设置超时,但该函数对通过CreateFile()打开的USB连接无效,因此需要其他机制实现USB设备路径下的I/O超时判断。
现有代码片段
串口打开代码
// 参考微软官方串口开发规范:如何指定大于COM9的串口 // CreateFile() 可用于获取串口句柄,Win32 API说明中明确要求: // 共享模式必须为0,创建参数必须为OPEN_EXISTING,模板参数必须为NULL // // 文件名使用"COM1"到"COM9"时CreateFile()可正常执行 // 但使用"COM10"及以上编号时会返回INVALID_HANDLE_VALUE // // 如果端口名称为\\.\COM10,调用CreateFile()时正确的写法是"\\\\.\\COM10" // // 注意:该语法同样适用于COM1到COM9,部分硬件支持自定义端口名,该语法也可兼容这类自定义名称 wsprintf(wszPortName, TEXT("\\\\.\\COM%d"), usPortId); /* 打开串口 */ /* 避免CreateFile偶发调用失败 */ for (i = 0; i < 10; i++) { hHandle = CreateFile (wszPortName, /* 端口名称指针,对应PifOpenCom()入参 */ GENERIC_READ | GENERIC_WRITE, /* 读写访问权限 */ 0, /* 共享模式 */ NULL, /* 安全属性指针 */ OPEN_EXISTING,/* 串口打开方式 */ 0, /* 端口属性 */ NULL); /* 待复制属性的端口句柄 */ /* 打开端口失败则返回对应错误 */ if ( hHandle == INVALID_HANDLE_VALUE ) { /* 无法打开端口 */ dwError = GetLastError (); if (dwError == ERROR_FILE_NOT_FOUND || dwError == ERROR_INVALID_NAME || dwError == ERROR_ACCESS_DENIED) { LeaveCriticalSection(&g_SioCriticalSection); // COM端口不存在,大概率是USB设备对应的虚拟串口已拔出、断电,或串口/虚拟串口被其他应用占用 return PIF_ERROR_COM_ACCESS_DENIED; } PifLog (MODULE_PIF_OPENCOM, LOG_ERROR_PIFSIO_CODE_01); PifLog (MODULE_ERROR_NO(MODULE_PIF_OPENCOM), (USHORT)dwError); PifLog(MODULE_DATA_VALUE(FAULT_AT_PIFOPENCOM), usPortId); PifSleep(500); } else { break; } } if ( hHandle == INVALID_HANDLE_VALUE ) { /* 无法打开端口 */ wsprintf(wszDisplay, TEXT("CreateFile, COM%d, Last Error =%d\n"), usPortId, dwError); OutputDebugString(wszDisplay); LeaveCriticalSection(&g_SioCriticalSection); return PIF_ERROR_COM_ERRORS; } /* 清除错误并清空接收缓冲区 */ dwError = (DWORD)(~0); // 置位所有错误码位 ClearCommError(hHandle, &dwError, NULL); PurgeComm( hHandle, PURGE_TXABORT | PURGE_RXABORT | PURGE_TXCLEAR | PURGE_RXCLEAR ) ;
ReadFile封装实现代码
fResult = ReadFile(hHandle, pBuffer, (DWORD)usBytes, &dwBytesRead, NULL); if (PifSioCheckPowerDown(usPort, aPifSioTable) == TRUE) { return PIF_ERROR_COM_POWER_FAILURE; } if (fResult) { if (!dwBytesRead) return PIF_ERROR_COM_TIMEOUT; return (SHORT)dwBytesRead; } else { SHORT sErrorCode = 0; // PifSubGetErrorCode()返回的错误码,必须在GetLastError()之后调用 dwError = GetLastError(); PifLog (MODULE_PIF_READCOM, LOG_ERROR_PIFSIO_CODE_06); PifLog (MODULE_ERROR_NO(MODULE_PIF_READCOM), (USHORT)dwError); sErrorCode = PifSubGetErrorCode(hHandle); PifLog (MODULE_ERROR_NO(MODULE_PIF_READCOM), (USHORT)abs(sErrorCode)); PifLog (MODULE_DATA_VALUE(MODULE_PIF_READCOM), usPort); return (sErrorCode); }
由于USB设备句柄不支持串口专用的SetCommTimeouts()接口,可通过以下两种方案实现超时逻辑,完全兼容现有框架,无需修改上层业务代码:
方案1:重叠IO + 等待超时(推荐,侵入性最低)
仅需要修改USB设备打开逻辑和ReadFile封装逻辑:
- 调用
CreateFile()打开USB设备时,新增FILE_FLAG_OVERLAPPED属性标记 - 读取USB设备时传入OVERLAPPED结构体并绑定手动重置事件,用
WaitForSingleObject()设置等待超时时间 - 超时触发时调用
CancelIo()终止未完成的IO操作,直接返回现有框架定义的超时错误码即可
适配后的ReadFile封装示例
// 提前在设备上下文新增bIsUsbDevice标记,USB设备打开时赋值为TRUE,串口设备为FALSE if (bIsUsbDevice) { OVERLAPPED ov = {0}; ov.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); BOOL readRet = ReadFile(hHandle, pBuffer, (DWORD)usBytes, &dwBytesRead, &ov); if (!readRet && GetLastError() == ERROR_IO_PENDING) { // 超时时间可替换为你现有框架的超时配置值,单位为毫秒 DWORD waitRet = WaitForSingleObject(ov.hEvent, 500); if (waitRet == WAIT_TIMEOUT) { CancelIo(hHandle); CloseHandle(ov.hEvent); return PIF_ERROR_COM_TIMEOUT; } else if (waitRet == WAIT_OBJECT_0) { GetOverlappedResult(hHandle, &ov, &dwBytesRead, FALSE); } else { CloseHandle(ov.hEvent); return PIF_ERROR_COM_ERRORS; } } CloseHandle(ov.hEvent); } else { // 原有串口读取逻辑保持不变 fResult = ReadFile(hHandle, pBuffer, (DWORD)usBytes, &dwBytesRead, NULL); } // 后续原有逻辑保持不变 if (PifSioCheckPowerDown(usPort, aPifSioTable) == TRUE) { return PIF_ERROR_COM_POWER_FAILURE; } // ...其余原有判断逻辑完全复用
方案2:超时检测线程(适配不支持重叠IO的场景)
如果不想修改IO模式,可在调用ReadFile()前启动计时线程,超时未完成读取就调用CancelIoEx()终止IO操作,此时ReadFile()会返回失败,错误码为ERROR_OPERATION_ABORTED,将该错误码映射为现有框架的PIF_ERROR_COM_TIMEOUT即可。
内容的提问来源于stack exchange,提问作者Richard Chambers

