4、数据包转发机制:监听模式、数据包捕获、转发策略与重传

好,咱们接着聊。上一节我们把同步的底层时钟和时序理清了,这一节要解决一个更实际的问题——数据包怎么在左右耳之间跑起来

说白了,TWS耳机最核心的痛点就是:手机只发一个音频流,但左右耳要同时听到。那中间这个“转发”环节,就成了整个系统的命门。我当年做第一个TWS项目时,就因为这个转发机制没想透,导致左耳老是断断续续,被测试同事追着骂了整整两周……嗯,后来我花了大力气把这块啃透了,今天就把这些经验掰开揉碎讲给你。

4.1 监听模式:耳朵要“偷听”

先问一个问题:手机连上TWS耳机后,它到底跟谁在通信?

答案是——手机只跟“主耳”建立蓝牙连接。副耳呢?它其实是在“偷听”。

这个模式叫监听模式(Sniff Mode)。主耳收到手机发来的音频数据包后,会立刻转发给副耳。但副耳并不是傻等着,它也在空中监听手机和主耳的通信。

具体怎么做的?

  • 主耳角色:接收手机数据 → 解析 → 转发给副耳
  • 副耳角色:监听手机→主耳的通信,同时接收主耳转发的数据
  • 监听窗口:副耳会在特定时隙打开射频接收器,捕获空中数据包

关键点:监听模式不是一直开着耳朵听,那样太费电了。副耳只在约定的时隙内“醒来”监听,其他时间休眠。这个时隙的精度,直接决定了同步质量。

我在项目中遇到过一个问题:副耳监听时隙跟主耳转发时隙没对齐,导致副耳经常漏包。后来发现是蓝牙时钟漂移导致的,解决办法是每10个连接事件做一次时钟校准。这个坑,你们以后大概率也会踩到。

4.2 数据包捕获:抓包的艺术

监听模式打开了,接下来就是怎么把数据包“抓”下来。

数据包捕获(Packet Capture)听起来高大上,其实就三步:

  1. 识别包类型:是音频数据包?还是控制包?还是空包?
  2. 提取有效载荷:把音频数据从蓝牙协议栈里剥出来
  3. 时间戳标记:记录这个包到达的时刻,用于后续同步

这里有个细节很多人会忽略——捕获时机。副耳捕获手机→主耳的数据包时,其实已经比主耳晚了一点点(空中传播延迟+处理延迟)。这个延迟虽然只有几微秒,但在高精度同步场景下,必须补偿。

我的建议:在捕获数据包时,同时记录本地时钟计数器的值。这样后续做重传或同步时,可以精确知道每个包的“年龄”。

代码层面,捕获逻辑大概长这样:

// 伪代码:数据包捕获流程
void packet_capture(uint8_t *raw_data, uint32_t len) {
    // 1. 检查CRC,丢弃损坏包
    if (crc_check(raw_data, len) != CRC_OK) {
        return;  // 坏包直接扔掉
    }
    
    // 2. 解析蓝牙头部,获取连接句柄和包类型
    bt_hdr_t *hdr = (bt_hdr_t *)raw_data;
    if (hdr->type != AUDIO_DATA_TYPE) {
        return;  // 不是音频包,不处理
    }
    
    // 3. 提取音频载荷
    audio_payload_t *payload = extract_payload(raw_data + BT_HEADER_SIZE);
    
    // 4. 打时间戳
    uint32_t timestamp = get_local_tick();
    
    // 5. 存入捕获缓冲区
    capture_buffer_push(payload, timestamp);
}

你想想看,这个捕获缓冲区其实是个环形队列。满了怎么办?要么覆盖旧数据,要么丢弃新数据。我一般建议用覆盖策略——因为音频是流式的,旧数据迟早要丢掉,留着反而浪费内存。

4.3 转发策略:主耳怎么“传话”

数据包捕获到了,接下来就是转发。转发策略决定了副耳能不能稳定收到数据。

常见的转发策略有三种:

策略名称 原理 优点 缺点
立即转发 主耳收到手机数据后,立刻在下一个可用时隙转发 延迟最低 容易跟手机数据冲突
定时转发 主耳在固定的转发时隙发送数据 时序可控,冲突少 延迟稍高
按需转发 副耳主动请求重传时,主耳才转发 节省带宽 实现复杂,延迟不稳定

我个人习惯用定时转发+按需重传的组合策略。为什么?因为TWS耳机对延迟敏感,但又不能太浪费带宽。定时转发保证了基础的数据流,按需重传则处理丢包情况。

注意:转发时一定要考虑蓝牙的“主从角色切换”。有些芯片方案支持主耳和副耳动态切换角色,这时候转发策略也要跟着变。我曾经在一个项目中没处理好这个切换,导致副耳升为主耳后,转发链路断了,整个耳机直接哑巴了……

4.4 重传机制:丢了怎么办?

无线通信嘛,丢包是常态。TWS耳机里,重传机制是保证音质连续性的最后一道防线。

重传分两种场景:

  • 手机→主耳丢包:主耳没收到数据,需要向手机请求重传
  • 主耳→副耳丢包:副耳没收到转发数据,需要向主耳请求重传

这里有个核心问题——重传请求怎么发?

蓝牙协议里,重传请求是通过NACK(Negative Acknowledgement)包实现的。副耳发现某个序列号的数据包没收到,就在下一个可用的时隙里发一个NACK给主耳。

但NACK不能乱发,否则会引发“重传风暴”。我见过一个项目,因为NACK处理不当,导致主耳一直在重传旧数据,新数据反而传不过去,音频卡成PPT。

重传策略的核心原则

  • 每个数据包最多重传2-3次,超过就放弃
  • 重传优先级低于新数据——先保证流畅,再保证完整
  • 使用序列号(Sequence Number)标记每个包,避免重复处理

代码实现上,重传逻辑大概是这样:

// 伪代码:重传处理
void handle_retransmission(uint16_t seq_num) {
    // 检查重传次数
    if (retransmit_count[seq_num] >= MAX_RETRANSMIT) {
        // 超过最大重传次数,放弃
        drop_packet(seq_num);
        return;
    }
    
    // 从重传缓冲区取出数据
    packet_t *pkt = retransmit_buffer_get(seq_num);
    if (pkt == NULL) {
        // 缓冲区已被覆盖,无法重传
        return;
    }
    
    // 发送重传数据
    send_packet(pkt);
    retransmit_count[seq_num]++;
}

你可能会问:重传缓冲区要多大?这个取决于你的音频码率和延迟要求。我一般按50ms的音频数据量来算。比如48kHz采样率、16bit量化、双声道,50ms的数据量大概是 48000×2×2×0.05 = 9600字节。留点余量,设个12KB的缓冲区基本够用。

4.5 实战中的坑与避坑指南

讲到这里,我把自己踩过的几个坑列出来,你们遇到类似情况可以少走弯路。

  • 我曾经因为监听时隙没对齐,导致副耳漏包率高达30%。解决办法是在每个连接事件开始时,主耳发一个同步标记包,副耳用这个包校准自己的监听窗口。
  • 我曾经因为重传缓冲区太小,导致音频频繁断流。后来把缓冲区从4KB扩大到12KB,问题就解决了。嗯,有时候硬件资源不能省。
  • 我曾经因为NACK包跟音频数据包冲突,导致重传延迟飙升。解决办法是把NACK包放在专门的“控制时隙”里发送,跟数据时隙错开。

一个小技巧:调试转发机制时,可以在主耳和副耳上各打一个GPIO,用示波器抓这两个GPIO的波形。主耳转发数据时拉高,副耳收到数据时拉高。看两个波形的延迟和抖动,就能直观判断转发质量。

好了,这一节的内容就到这。数据包转发机制听起来复杂,其实核心就三件事:监听要准、捕获要快、重传要稳。下一节我们会讲音频数据的编解码与压缩,到时候你会看到,转发过来的数据是怎么变成你耳朵里听到的声音的。