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 秒 |
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);
}
为什么要指数退避?你想想看,如果网络已经拥塞了,你还拼命重传,只会让情况更糟。指数退避给网络一个喘息的机会。
4.5 四种方法的对比与选择
这四种方法不是互斥的,而是互补的。我一般这样组合使用:
- 心跳检测:用时间戳对比 + 序列号分析,判断链路是否存活
- 指令传输:用 ACK 确认 + 超时重传,保证关键指令可靠到达
- 数据流(如遥测):只用序列号分析,丢了就丢了,不重传
说白了,没有一种方法能解决所有问题。你得根据场景选。实时性要求高的,少重传;可靠性要求高的,多确认。
- 序列号分析:检测顺序错乱和丢包数量
- 时间戳对比:区分延迟和丢包
- ACK 确认:双向握手,保证指令执行
- 超时重传:指数退避,有限重试
嗯,以上就是丢包检测的四种核心方法。每种方法都有自己的适用场景,没有银弹。我个人的经验是:先搞清楚你的系统对实时性和可靠性的要求,再决定用哪几种方法组合。别一上来就全上,那样只会把系统搞复杂。