串口通信“第二次总超时”的复盘

发布于:2025-08-13 ⋅ 阅读:(20) ⋅ 点赞:(0)

一次看似简单的“重试两次”,为何第二次永远收不到数据?
本文把问题从表面现象一直追到硬件协议层,给出可落地的最终方案。


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 的问题,而是设备协议“只回一次”。
要么第一轮就保证正确,要么改协议/硬件;否则永远重发无效。