4. 超时检测原理:基于时间戳的丢包判断、滑动窗口算法

超时检测,说白了就是地面站怎么知道「这个包丢了」。

我刚开始做地面站的时候,觉得这事很简单——发一个包,等回复,等不到就是丢了。后来发现,现实远比这复杂。网络抖动、链路拥塞、飞控忙不过来,都会导致延迟。你设个固定超时,要么太短误判,要么太长反应迟钝。

这一节,我们就来拆解两个核心机制:基于时间戳的丢包判断和滑动窗口算法。它们共同构成了地面站重连决策的「眼睛」和「尺子」。

4.1 基于时间戳的丢包判断

每个 MAVLink 消息发出时,地面站都会记录一个时间戳。这个时间戳不是消息里的 time_boot_ms,而是本地系统时钟的 send_time。

为什么不用飞控的时间?因为时钟不同步。飞控的 time_boot_ms 是上电以来的毫秒数,地面站的时间是 UTC 或本地时间。两者没有可比性。所以,我们只用自己的时钟。

判断逻辑其实就一句话:

核心规则:如果某个消息的 send_time 距离当前时间超过 ACK_TIMEOUT_MS,且仍未收到对应 ACK,则判定该消息丢失。

但这里有个坑——不是所有消息都需要 ACK。MAVLink 协议里,只有 COMMAND_ACK 或 MISSION_ACK 这类带序列号的交互才需要超时检测。心跳包、遥测数据这些流式消息,用的是另一种机制(后面章节会讲)。

我曾经在一个项目中,把心跳包也纳入了超时检测,结果地面站频繁误报「连接断开」。后来才发现,心跳包的发送间隔是 1 秒,而我的超时设了 500 毫秒——这不就自己打自己脸吗?

4.2 滑动窗口算法

单个包的丢包判断很简单,但实际场景中,网络是动态的。今天延迟 50ms,明天可能 200ms。固定超时值根本扛不住。

滑动窗口算法就是用来解决这个问题的。它维护一个最近 N 个包的 RTT(往返时间)样本,然后动态计算超时阈值。

我习惯用这样的数据结构:

struct SlidingWindow {
    uint64_t timestamps[WINDOW_SIZE];  // 每个包的发送时间戳
    uint32_t rtt_samples[WINDOW_SIZE]; // 对应的 RTT 值
    uint8_t head;                      // 当前写入位置
    uint8_t count;                     // 有效样本数
    uint32_t smoothed_rtt;             // 平滑后的 RTT
    uint32_t rtt_variance;             // RTT 方差
    uint32_t timeout;                  // 最终超时值
};

算法流程是这样的:

  1. 每发送一个需要 ACK 的消息,把当前时间戳写入 timestamps[head]。
  2. 收到 ACK 时,计算 now - timestamps[head],得到 RTT 样本,存入 rtt_samples[head]。
  3. 更新 smoothed_rtt 和 rtt_variance:
smoothed_rtt = (7 * smoothed_rtt + rtt_samples[head]) / 8;
rtt_variance = (3 * rtt_variance + abs(rtt_samples[head] - smoothed_rtt)) / 4;
timeout = smoothed_rtt + 4 * rtt_variance;

嗯,这里要注意:timeout 不是简单的平均值,而是「平均值 + 4 倍方差」。为什么要加方差?因为网络抖动越大,超时值就应该越大,避免误判。

经验值:我一般把 WINDOW_SIZE 设为 16 或 32。太小了抗抖动能力差,太大了反应迟钝。另外,timeout 要设一个下限(比如 100ms)和一个上限(比如 5s),防止极端情况。

4.3 丢包判断与滑动窗口的协同

这两个机制不是孤立的。它们配合起来,才能做出准确的丢包判断。

我画了一张流程图,展示它们的协作关系:

超时检测与滑动窗口协同流程 发送 MAVLink 消息 记录 send_time 到滑动窗口 等待 ACK 或超时 收到 ACK? 计算 RTT,更新滑动窗口 是 判定丢包,触发重传 否 更新 timeout 值 继续发送下一条消息

从图中可以看到,每次收到 ACK 后,滑动窗口都会更新 timeout 值。这个值反过来又影响下一次的丢包判断。说白了,这是一个自适应闭环。

4.4 避坑指南

我在实际项目中踩过不少坑,这里列几个最常见的:

问题 现象 原因 解决方案
超时值过小 频繁误判丢包,重传风暴 滑动窗口未考虑方差 使用 smoothed_rtt + 4 * rtt_variance
超时值过大 丢包后反应迟钝,重连慢 窗口样本太多,更新慢 缩小 WINDOW_SIZE 到 16 或 8
时间戳溢出 RTT 计算为负数 32 位毫秒计数器 49 天溢出 使用 64 位时间戳,或处理回绕
ACK 乱序 RTT 样本异常大或小 网络层或飞控端乱序 按序列号匹配,丢弃乱序 ACK

我曾经在一个无人机集群项目中,因为没处理时间戳回绕,导致地面站误以为所有包都超时,触发了全局重连。那场面,8 架飞机同时断连,差点炸机。从那以后,我所有时间戳相关代码都加了回绕检测。

4.5 小结

超时检测是地面站重连机制的第一道防线。基于时间戳的丢包判断,配合滑动窗口算法,能自适应网络变化,避免误判和延迟。

记住几个关键点:

  • 只用本地时间戳,别用飞控时间
  • 滑动窗口要同时考虑均值和方差
  • 设好超时的上下限,防止极端情况
  • 处理时间戳回绕和 ACK 乱序