4、丢包检测方法:序列号分析、时间戳对比、ACK确认机制、超时重传策略

丢包检测,说白了就是怎么知道数据丢了。

我刚开始做无人机飞控的时候,觉得这问题很简单——没收到回复就是丢了呗。后来发现,事情远没那么简单。你想想看,有时候是数据真的丢了,有时候是回复丢了,有时候只是网络延迟大了一点。怎么区分?这就是本章要聊的核心。

4.1 序列号分析——最基础的丢包检测手段

MAVLink 的每个消息都带有一个序列号,存放在 msgid 之后的 seq 字段里。这个字段是递增的,从 0 开始,每发一条消息就加 1。

接收端怎么用?很简单:

// 伪代码示例
uint16_t expected_seq = 0;

void on_receive(mavlink_message_t *msg) {
    if (msg->seq != expected_seq) {
        // 丢包了!
        uint16_t lost_count = msg->seq - expected_seq;
        printf("检测到丢包,丢失 %d 个包\n", lost_count);
        expected_seq = msg->seq + 1;
    } else {
        expected_seq++;
    }
}

我在项目中遇到过一个问题:序列号溢出。MAVLink 1.0 的序列号是 8 位的,最大 255。飞到 256 的时候就归零了。如果不处理溢出,接收端会以为丢了 255 个包。嗯,这里要注意,一定要用模运算来处理:

uint16_t diff = (msg->seq - expected_seq) & 0xFF;
if (diff != 0) {
    // 真的丢包了
}
我的习惯:序列号分析适合做实时丢包检测,但不适合做精确统计。因为如果连续丢包,你只能知道丢了多少个,不知道具体丢了哪些内容。

4.2 时间戳对比——判断延迟还是丢包

序列号只能告诉你「顺序不对」,但没法告诉你「是不是超时了」。这时候就需要时间戳。

MAVLink 的 HEARTBEAT 消息里自带 time_boot_ms 字段。接收端可以记录上一次收到心跳的时间,然后跟当前时间对比:

uint32_t last_heartbeat_time = 0;
uint32_t timeout_ms = 3000;  // 3秒超时

void check_heartbeat_timeout() {
    uint32_t now = get_system_time_ms();
    if (now - last_heartbeat_time > timeout_ms) {
        printf("心跳超时!可能丢包或链路断开\n");
        // 触发重连或报警
    }
}

我个人习惯把时间戳对比和序列号分析结合起来用。举个例子:

  • 序列号跳了,但时间戳间隔正常 → 大概率是丢包了
  • 序列号没跳,但时间戳间隔很大 → 可能是链路延迟,不一定是丢包
  • 序列号跳了,时间戳也超时了 → 链路可能真的断了
我曾经踩过的坑:时间戳的精度问题。有些飞控的 time_boot_ms 是 32 位的,大约 49 天溢出一次。如果你的系统连续运行超过 49 天,时间戳对比会出问题。解决办法是用相对时间差,而不是绝对值。

4.3 ACK确认机制——双向确认才靠谱

序列号和时间戳都是单向检测。但真正的可靠性,需要双向确认。这就是 ACK 机制。

MAVLink 2.0 引入了 COMMAND_ACK 消息。你发一条指令,对方必须回复一条 ACK,告诉你「我收到了,执行结果如何」。

典型的交互流程是这样的:

// 发送端
send_command(MAV_CMD_ARM_AUTHORIZATION);
start_ack_timer();

// 接收端(飞控)
on_command_received() {
    execute_command();
    send_ack(MAV_RESULT_ACCEPTED);
}

// 发送端收到 ACK
on_ack_received() {
    stop_ack_timer();
    printf("指令已确认执行\n");
}

这里有个关键点:ACK 本身也可能丢。所以发送端需要维护一个「待确认列表」,超时未收到 ACK 就重发。

消息类型 是否需要 ACK 典型超时时间
HEARTBEAT 不需要
COMMAND_LONG 需要 3-5 秒
MISSION_ITEM 需要 1-2 秒
PARAM_SET 需要 2-3 秒
我的建议:不是所有消息都需要 ACK。心跳消息如果也用 ACK,带宽会翻倍。只有关键指令(如解锁、切换模式、上传航点)才需要 ACK 确认。

4.4 超时重传策略——丢了怎么办

检测到丢包之后,下一步就是重传。但怎么重传?什么时候重传?重传几次?这里面有讲究。

我常用的策略是「指数退避 + 有限重传」:

#define MAX_RETRIES 3
#define BASE_TIMEOUT_MS 1000

int retry_count = 0;
int timeout_ms = BASE_TIMEOUT_MS;

void send_with_retry(mavlink_message_t *msg) {
    while (retry_count < MAX_RETRIES) {
        send_message(msg);
        if (wait_for_ack(timeout_ms)) {
            // 收到 ACK,成功
            retry_count = 0;
            timeout_ms = BASE_TIMEOUT_MS;
            return;
        }
        // 超时,重试
        retry_count++;
        timeout_ms *= 2;  // 指数退避
        printf("第 %d 次重试,超时时间 %d ms\n", retry_count, timeout_ms);
    }
    printf("重传 %d 次失败,放弃\n", MAX_RETRIES);
}

为什么要指数退避?你想想看,如果网络已经拥塞了,你还拼命重传,只会让情况更糟。指数退避给网络一个喘息的机会。

我曾经犯过的错:重传次数设得太多。有一次我设了 10 次重传,结果飞控卡在重传循环里,其他任务都停了。后来我改成最多 3 次,配合一个「全局重传队列」来异步处理,才解决问题。

4.5 四种方法的对比与选择

这四种方法不是互斥的,而是互补的。我一般这样组合使用:

  • 心跳检测:用时间戳对比 + 序列号分析,判断链路是否存活
  • 指令传输:用 ACK 确认 + 超时重传,保证关键指令可靠到达
  • 数据流(如遥测):只用序列号分析,丢了就丢了,不重传

说白了,没有一种方法能解决所有问题。你得根据场景选。实时性要求高的,少重传;可靠性要求高的,多确认。

核心要点:
  • 序列号分析:检测顺序错乱和丢包数量
  • 时间戳对比:区分延迟和丢包
  • ACK 确认:双向握手,保证指令执行
  • 超时重传:指数退避,有限重试
丢包检测方法体系 序列号分析 检测顺序错乱 时间戳对比 区分延迟与丢包 ACK确认机制 双向握手确认 超时重传策略 指数退避重试 组合使用,互补检测 心跳检测 时间戳 + 序列号 指令传输 ACK + 超时重传 数据流遥测 仅序列号,不重传 根据场景选择合适的方法组合

嗯,以上就是丢包检测的四种核心方法。每种方法都有自己的适用场景,没有银弹。我个人的经验是:先搞清楚你的系统对实时性和可靠性的要求,再决定用哪几种方法组合。别一上来就全上,那样只会把系统搞复杂。