使用当前pyserial 在Windwos 下做的项目,移交到客户案场后, 客户发现程序会卡住,经过最终调查,是卡在了pyserial 的write 方法中了。
具体现象:
程序卡在write 方法中,长时间无法返回,即使设置了write_timeout 也无法返回。
分析
下来我们将Send 方法的代码切出来:
def write(self, data):
"""Output the given byte string over the serial port."""
if not self.is_open:
raise PortNotOpenError()
#~ if not isinstance(data, (bytes, bytearray)):
#~ raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data)))
# convert data (needed in case of memoryview instance: Py 3.1 io lib), ctypes doesn't like memoryview
data = to_bytes(data)
if data:
#~ win32event.ResetEvent(self._overlapped_write.hEvent)
n = win32.DWORD()
success = win32.WriteFile(self._port_handle, data, len(data), ctypes.byref(n), self._overlapped_write)
if self._write_timeout != 0: # if blocking (None) or w/ write timeout (>0)
if not success and win32.GetLastError() not in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING):
raise SerialException("WriteFile failed ({!r})".format(ctypes.WinError()))
# Wait for the write to complete.
#~ win32.WaitForSingleObject(self._overlapped_write.hEvent, win32.INFINITE)
win32.GetOverlappedResult(self._port_handle, self._overlapped_write, ctypes.byref(n), True)
if win32.GetLastError() == win32.ERROR_OPERATION_ABORTED:
return n.value # canceled IO is no error
if n.value != len(data):
raise SerialTimeoutException('Write timeout')
return n.value
else:
errorcode = win32.ERROR_SUCCESS if success else win32.GetLastError()
if errorcode in (win32.ERROR_INVALID_USER_BUFFER, win32.ERROR_NOT_ENOUGH_MEMORY,
win32.ERROR_OPERATION_ABORTED):
return 0
elif errorcode in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING):
# no info on true length provided by OS function in async mode
return len(data)
else:
raise SerialException("WriteFile failed ({!r})".format(ctypes.WinError()))
else:
return 0
这里有一个 Windows 串口编程的“坑”:即使你设置了 write_timeout,某些情况下,WriteFile + GetOverlappedResult() 仍可能卡住不返回。下面我从操作系统底层机制解释这个行为。
在 PySerial 的 Windows 实现中,串口写操作等价于如下 WinAPI 调用流程:
OVERLAPPED ov = {...}; // 重叠结构体(异步IO用)
DWORD written = 0;
BOOL success = WriteFile(hSerial, buffer, len, &written, &ov);
if (!success && GetLastError() == ERROR_IO_PENDING) {
// 等待异步写完成
GetOverlappedResult(hSerial, &ov, &written, TRUE); // TRUE = 阻塞等待
}
问题发生点:GetOverlappedResult(…, TRUE)
这个调用是阻塞等待写完成。理论上,当写操作超过 WriteTotalTimeoutConstant + len * WriteTotalTimeoutMultiplier 时,系统应返回 SerialTimeoutException,但实际并不总是这样。
原因 | 解释 |
---|---|
✅ 驱动未正确处理超时 | 某些串口驱动(尤其是 USB 转串口)忽略了 SetCommTimeouts 中的写超时设置。 |
✅ 缓冲区已满 | 写缓冲区写不进去,但驱动仍让 WriteFile 挂起等待,超时机制失效。 |
✅ GetOverlappedResult 不检查超时 | 它本身不带超时参数,除非你用 WaitForSingleObject 另设超时,pyserial 没这么做。 |
✅ 虚拟串口/硬件死锁 | 某些虚拟串口设备(如蓝牙串口、USB转串口)会因设备掉线导致永远不返回。 |
举个例子:
ser.write_timeout = 2.0
ser.write(b'X' * 102400) # 写一个很大的包
如果目标串口设备已断开/没响应:
- WriteFile() 会返回 ERROR_IO_PENDING
- 然后 GetOverlappedResult(…, TRUE) 就会 永久阻塞
- 此时 write_timeout 设置毫无作用
如果串口写缓冲区已满,驱动并不会立即失败, 而是等待硬件清空缓冲区(例如对端设备接受), 如果硬件永远不读(比如死机了),那它就永远挂住了。
WriteFile() 是异步的,但 GetOverlappedResult(…, TRUE) 是阻塞的。它等的是 overlapped 事件完成,但串口驱动只有在写成功/失败时才触发事件。
当使用 OVERLAPPED 结构进行串口异步写时,WriteFile() 返回后,并不会立刻知道是否成功,而是通过一个事件句柄 hEvent 在未来某个时间点通知“写操作完成”。
所以我们给出最终解决方案如下:
def write(self, data):
"""Output the given byte string over the serial port."""
if not self.is_open:
raise PortNotOpenError()
#~ if not isinstance(data, (bytes, bytearray)):
#~ raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data)))
# convert data (needed in case of memoryview instance: Py 3.1 io lib), ctypes doesn't like memoryview
data = to_bytes(data)
if data:
#~ win32event.ResetEvent(self._overlapped_write.hEvent)
n = win32.DWORD()
success = win32.WriteFile(self._port_handle, data, len(data), ctypes.byref(n), self._overlapped_write)
if self._write_timeout != 0: # if blocking (None) or w/ write timeout (>0)
if not success and win32.GetLastError() not in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING):
raise SerialException("WriteFile failed ({!r})".format(ctypes.WinError()))
# Wait for the write to complete.
WAIT_TIMEOUT = 0x00000102
if self._write_timeout is None:
timeout_ms = win32.INFINITE
else:
timeout_ms = int(self._write_timeout * 1000)
rc = win32.WaitForSingleObject(self._overlapped_write.hEvent, timeout_ms)
if rc == WAIT_TIMEOUT:
self.cancel_write()
raise SerialTimeoutException('Write timeout due to device blocking.')
win32.GetOverlappedResult(self._port_handle, self._overlapped_write, ctypes.byref(n), False)
if win32.GetLastError() == win32.ERROR_OPERATION_ABORTED:
return n.value # canceled IO is no error
if n.value != len(data):
raise SerialTimeoutException('Write timeout')
return n.value
else:
errorcode = win32.ERROR_SUCCESS if success else win32.GetLastError()
if errorcode in (win32.ERROR_INVALID_USER_BUFFER, win32.ERROR_NOT_ENOUGH_MEMORY,
win32.ERROR_OPERATION_ABORTED):
return 0
elif errorcode in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING):
# no info on true length provided by OS function in async mode
return len(data)
else:
raise SerialException("WriteFile failed ({!r})".format(ctypes.WinError()))
else:
return 0
使用WaitForSingleObject:
- 等待 overlapped 写事件完成
- 最多等待 timeout_in_ms 毫秒
- 返回值 rc 用于判断是成功、超时,还是其他错误
- 如果超时,调用cancel_write() 掉取消一个挂起的 I/O 操作(如 ReadFile、WriteFile 等)(特别是在使用 重叠(Overlapped)I/O 时非常有用。)
如此,就完美的解决掉pyserial 在windows 下卡住的现象了。