一次看似简单的“重试两次”,为何第二次永远收不到数据?
本文把问题从表面现象一直追到硬件协议层,给出可落地的最终方案。
1 问题现象
在 Windows 平台下,我们的代码大致如下(精简后):
// 最外层:业务重试 2 次
for (int i = 0; i < 2; ++i) {
PurgeComm(hComm, PURGE_RXCLEAR | PURGE_TXCLEAR);
SendCommData(hComm, szCmd, 30); // 30 字节
if (RecvCommData(hComm, szRecv, 18)) // 想收 18 字节
return SUCCESS;
Sleep(100);
}
第 1 轮:
发送 → 接收 → 很快返回 18 字节,但内容因干扰而“错误”。
逻辑判失败 → 进入第 2 轮。第 2 轮:
同样步骤,却永远超时(ReadFile 一直读不到 0 字节)。
2 逐层排查
排查项 | 结果 | 结论 |
---|---|---|
线路波形 | TX 两次都成功发出 30 字节;RX 第 2 次无任何波形 | 设备未回数据 |
串口缓存 | 每轮都 PurgeComm |
旧数据已清空 |
超时参数 | 单轮 ReadFile 最多 150 ms + 18×100 ms = 1.95 s,足够长 | 不是时间不够 |
波特率 / 时钟 | 已确认正确 | 排除硬件速率问题 |
设备说明书 | “一帧指令一次应答,重复指令不响应” | 根因已找到 |
3 根本原因
设备协议本身只支持“单次应答”。
- 收到第 1 帧 → 立即回 18 字节。
- 再收第 2 帧(内容完全相同)→ 直接丢弃,不发任何数据。
因此:
- 第 1 轮即使因干扰拿到“错误帧”,也把唯一一次应答“用掉”。
- 第 2 轮再发,设备不回 → FIFO 永远空 → ReadFile 超时。
4 最终解决方案
方案 | 说明 | 代码示例 |
---|---|---|
A. 一次性收对 | 把单轮超时拉大,确保首轮就能把完整帧收齐。出现错误直接报错,不再重发同一帧。 | ReadTotalTimeoutConstant = 1000; ReadTotalTimeoutMultiplier = 0; |
B. 协议级重试 | 若必须重试,修改设备固件或协议:带序号/ACK,使设备能区分“重发”。 | 由硬件/固件团队完成 |
C. 上层策略 | 把重试逻辑改为“断电复位→重新上电→再发指令”,让设备重新初始化。 | 电源控制脚或继电器 |
如果硬件无法改动,方案 A 是唯一可行且零成本的落地做法。
5 推荐参数组合(方案 A)
COMMTIMEOUTS cto = {0};
cto.ReadIntervalTimeout = 20; // 字节间 20 ms 容错
cto.ReadTotalTimeoutMultiplier = 0; // 不字节累加
cto.ReadTotalTimeoutConstant = 1000; // 整帧 1 s 足够
SetCommTimeouts(hComm, &cto);
// 去掉内部循环重试
BOOL RecvCommData(HANDLE hComm, char* buf, DWORD want)
{
DWORD got = 0;
return ReadFile(hComm, buf, want, &got, NULL) && got == want;
}
6 一句话总结
第二次超时不是 Windows 的问题,而是设备协议“只回一次”。
要么第一轮就保证正确,要么改协议/硬件;否则永远重发无效。