Sniff 省电模式参数调优——续航与音频稳定性平衡实操

Sniff 是经典蓝牙三个节能模式里用得最多的一个。TWS 耳机待机时能不能省电、A2DP 播放时会不会断音,都和 Sniff 参数直接相关。这篇拆解 Sniff 的三个核心参数,以及 TWS 场景下怎么调才平衡。

TL;DR

  • Sniff 模式让从设备周期性"醒来"监听,其余时间休眠。三个参数:Interval(醒的周期)、Attempt(醒多久)、Timeout(额外等多久)。
  • 参数调优的核心矛盾:Interval 越大越省电,但响应延迟越高;Attempt 越大越稳,但功耗越高。
  • TWS 待机场景 Interval 可以拉到 500ms-1s,A2DP 播放场景要压到 40ms 左右,HFP 通话不能用 Sniff。
  • Sniff Subrating 允许在不退出 Sniff 的情况下动态调整 Interval,实现"待机省电、有数据快速响应"。

目录

  1. [为什么要用 Sniff 模式](#为什么要用 Sniff 模式)
  2. [Sniff 的工作原理](#Sniff 的工作原理)
  3. 三个核心参数详解
  4. [Sniff Subrating 机制](#Sniff Subrating 机制)
  5. [HCI 命令与 LMP 交互](#HCI 命令与 LMP 交互)
  6. [TWS 场景参数调优](#TWS 场景参数调优)
  7. [抓包分析 Sniff](#抓包分析 Sniff)
  8. 常见问题排查

1. 为什么要用 Sniff 模式

经典蓝牙的 ACL 链路在 Active 状态下,从设备每个时隙都要监听主设备是否发数据------即使没有数据,RF 和基带也一直开着。这对于电池供电的设备(TWS 耳机、手表、传感器)是巨大的功耗浪费。

蓝牙定义了三个节能模式来降低功耗:

模式 功耗 响应延迟 适用场景
Hold 短时间挂起(如协商加密)
Sniff 可调 长时间连接但数据稀疏(TWS 待机、传感器)
Park 极低 已基本不用(BLE 取代)

Sniff 是实际项目中最常用的------它保持 ACL 链路不断开,但从设备大部分时间在休眠,只在约定的时隙醒来监听。TWS 耳机待机时能撑好几天,核心就是 Sniff 在工作。

为什么不用 Hold?Hold 是"约定一段时间不通信",到期后自动恢复 Active。它适合临时挂起(比如切换加密模式时),不适合长期待机------因为 Hold 期间完全不能收数据,到期恢复 Active 后又回到全时监听。

为什么不用 Park?Park 几乎完全休眠,只保留 beacon 监听,但响应延迟太大(秒级),而且 Park 模式下设备没有 AM_ADDR,不能直接收发数据,必须先 Unpark。现在 BLE 已经取代了 Park 的大多数场景,经典蓝牙里 Park 基本不用了。


2. Sniff 的工作原理

Sniff 模式下,主从双方约定一个 Sniff Interval (嗅探间隔)。每隔这个间隔,从设备"醒来"一段窗口(Sniff Attempt),监听主设备是否发数据。如果在这段窗口里收到数据,从设备保持唤醒继续收发;如果没收到,从设备重新休眠,等下一个 Interval。

复制代码
时间轴 →

主设备:  ──[发数据]──────────────────────────[发数据]──
从设备:  ▏醒▕─────────睡─────────▏醒▕─────────睡─────────▏醒▕
         │←Attempt→│←Interval-Attempt→│←Attempt→│←Interval-Attempt→│

Active 模式对比:
从设备:  ▏醒▕醒▕醒▕醒▕醒▕醒▕醒▕醒▕醒▕醒▕醒▕醒▕醒▕醒▕醒▕  ← 全时监听

关键点:主设备也必须在 Sniff 窗口才能给从设备发数据。Sniff 是双方约定的------主设备不能在从设备休眠时发数据,否则从设备收不到。这意味着 Sniff 参数影响的不只是从设备的功耗,还有链路的传输能力。


3. 三个核心参数详解

3.1 Sniff Interval(嗅探间隔)

从设备两次"醒来"之间的时间间隔。

  • 取值范围:0x0004 - 0x4000,单位 0.625ms(即 2.5ms - 10.24s)
  • 对功耗的影响:越大越省电(休眠时间越长)
  • 对延迟的影响:越大延迟越高(主设备要等到下一个窗口才能发数据)
  • 对吞吐的影响:越大有效带宽越低(传输机会少)

典型值:

场景 Interval 理由
TWS 待机 500ms - 1s 无数据传输,最大化省电
A2DP 播放 40ms - 60ms 匹配音频帧周期(SBC 约 20ms/帧),保证及时传输
HID 键盘 100ms - 200ms 按键响应延迟 < 200ms 用户可接受
传感器上报 1s - 4s 数据稀疏,延迟可接受

3.2 Sniff Attempt(嗅探尝试窗口)

从设备每次醒来后,持续监听的时长。

  • 取值范围:0x0001 - 0x7FFF,单位 0.625ms
  • 对功耗的影响:越大越耗电(醒着的时间长)
  • 对稳定性的影响:越大越稳(给链路更多时间重传/同步)

Attempt 不能为 0------至少要醒一个时隙才能判断主设备有没有发数据。Attempt 通常设置成能覆盖若干个跳频时隙(每个时隙 625μs),确保即使遇到跳频干扰也能收到。

典型值:

场景 Attempt 理由
TWS 待机 4 - 8 时隙(2.5ms - 5ms) 只需确认有无数据,短窗口够
A2DP 播放 16 - 32 时隙(10ms - 20ms) 要收完整个 SBC 帧包
弱信号环境 加大到 32 - 64 时隙 给重传留余量

3.3 Sniff Timeout(嗅探超时)

从设备在 Attempt 窗口内收到数据后,额外延长监听的时间。

  • 取值范围:0x0000 - 0x7FFF,单位 0.625ms
  • 作用:如果 Attempt 窗口里收到了数据,说明有通信需求,从设备继续醒着 Timeout 时长,看主设备是否还有后续数据
  • Timeout=0:Attempt 结束立即休眠,不管有没有收到数据

这个参数解决的是"突发数据"场景------主设备可能连续发多个包,如果每个包都要等下一个 Interval,延迟会累积。Timeout 让从设备在收到数据后"多醒一会儿",避免频繁休眠-唤醒。

典型值:

场景 Timeout 理由
TWS 待机 0 或 4 时隙 无突发数据,不需额外等待
A2DP 播放 16 - 32 时隙 连续媒体包,需要延长接收窗口
大数据传输 64+ 时阶 临时退出 Sniff 更好(见下一节)

3.4 三个参数的关系

复制代码
│←───────────────── Sniff Interval ─────────────────→│
│                                                      │
│  │← Attempt →│← Timeout →│                            │
│  │            (仅收到数据时)                           │
│  ▏醒▕─────────睡──────────────────────────────────────
│                                                      │
│  下一个 Interval 开始
│  │← Attempt →│← Timeout →│
│  ▏醒▕─────────睡──────────────────────────────────────

三个参数的工程意义:

  • Interval 决定省电程度(宏观)
  • Attempt 决定基础可用性(微观)
  • Timeout 决定突发处理能力(弹性)

调参的核心逻辑:先定 Interval(决定省电目标),再定 Attempt(保证基础通信),最后用 Timeout 优化突发场景。


4. Sniff Subrating 机制

4.1 问题:频繁进出 Sniff 的开销

TWS 耳机在"待机 - 来电 - 通话 - 挂断 - 待机"这种场景里,状态频繁切换。每次切换如果都要 Exit Sniff → Active → 重新 Enter Sniff,开销很大:

  • LMP_unsniff + LMP_sniff 两次握手
  • 状态机切换的时序约束
  • 重新协商参数的延迟

4.2 Subrating 的解法

Sniff Subrating(opcode 0x54)允许在不退出 Sniff 的情况下,动态调整有效 Sniff Interval。原理是引入一个"基础 Interval"和"子速率"概念:

  • 基础 Interval 保持较短(如 40ms),保证低延迟响应能力

  • 实际运行时通过 Subrating 把有效 Interval 拉长到 N 倍(如 N=16,有效 Interval=640ms)

  • 一旦有数据要传,立刻回到基础 Interval,无需重新协商

    无 Subrating:
    Interval=640ms ▏醒▕──────────────────────────────────────睡──────
    ↑ 来数据了,但要等 640ms 才能响应

    有 Subrating (base=40ms, subrate=16):
    基础周期: ▏醒▕────▏醒▕────▏醒▕──── ... 共 16 次
    ↑ 第 1 次就收到数据,立即保持 40ms 的活跃 Interval
    (无需 LMP 重新协商)

    无数据时:
    有效周期: ▏醒▕────────────────────────────睡────── (640ms)
    ↑ 每 16 个基础周期才真正醒一次

4.3 Subrating 参数

HCI_Sniff_Subrating 命令(Opcode 0x0C11)的参数:

参数 含义
Max_Latency 最大允许的有效 Interval(×0.625ms),即子速率后的最大间隔
Min_Remote_Timeout 对端的最小 Sniff Timeout
Min_Local_Timeout 本端的最小 Sniff Timeout

Max_Latency 是核心参数------它告诉控制器"有效 Interval 可以拉到多大",但基础 Interval 保持不变。控制器根据链路活动自动决定实际 Interval:有数据时用基础 Interval,无数据时逐步拉长到 Max_Latency。

4.4 适用场景

Subrating 非常适合"间歇性活动"的设备:

  • TWS 耳机:待机时拉长 Interval 省电,有来电/通知时立刻回到短 Interval 响应
  • HID 设备:无输入时长 Interval,按键时短 Interval 低延迟
  • 健康传感器:周期性上报数据,上报期间短 Interval,其余时间长 Interval

5. HCI 命令与 LMP 交互

5.1 进入 Sniff 的 HCI 命令

Host 通过 HCI_Sniff_Mode 命令(Opcode 0x0803)让 Controller 发起 Sniff:

c 复制代码
// HCI_Sniff_Mode 命令参数
struct hci_sniff_mode_cmd {
    uint16_t handle;            // ACL 连接句柄
    uint16_t sniff_interval;    // 嗅探间隔 (×0.625ms)
    uint16_t sniff_attempt;     // 嗅探尝试 (×0.625ms)
    uint16_t sniff_timeout;     // 嗅探超时 (×0.625ms)
};

// 示例:进入 Sniff,Interval=100ms, Attempt=5ms, Timeout=0
struct hci_sniff_mode_cmd cmd = {
    .handle         = acl_handle,
    .sniff_interval = 160,    // 100ms / 0.625ms = 160
    .sniff_attempt  = 8,      // 5ms / 0.625ms ≈ 8
    .sniff_timeout  = 0,      // 不额外等待
};
hci_send_cmd(HCI_SNIFF_MODE, &cmd, sizeof(cmd));

5.2 底层 LMP 交互

Controller 收到 HCI 命令后,发起 LMP 交互:
Host (B) Controller B Controller A Host (A) Host (B) Controller B Controller A Host (A) #mermaid-svg-QgEEjbaS0meGCu5p{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QgEEjbaS0meGCu5p .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QgEEjbaS0meGCu5p .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QgEEjbaS0meGCu5p .error-icon{fill:#552222;}#mermaid-svg-QgEEjbaS0meGCu5p .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QgEEjbaS0meGCu5p .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QgEEjbaS0meGCu5p .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QgEEjbaS0meGCu5p .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QgEEjbaS0meGCu5p .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QgEEjbaS0meGCu5p .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QgEEjbaS0meGCu5p .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QgEEjbaS0meGCu5p .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QgEEjbaS0meGCu5p .marker.cross{stroke:#333333;}#mermaid-svg-QgEEjbaS0meGCu5p svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QgEEjbaS0meGCu5p p{margin:0;}#mermaid-svg-QgEEjbaS0meGCu5p .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QgEEjbaS0meGCu5p text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-QgEEjbaS0meGCu5p .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-QgEEjbaS0meGCu5p .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-QgEEjbaS0meGCu5p .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-QgEEjbaS0meGCu5p .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-QgEEjbaS0meGCu5p #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-QgEEjbaS0meGCu5p .sequenceNumber{fill:white;}#mermaid-svg-QgEEjbaS0meGCu5p #sequencenumber{fill:#333;}#mermaid-svg-QgEEjbaS0meGCu5p #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-QgEEjbaS0meGCu5p .messageText{fill:#333;stroke:none;}#mermaid-svg-QgEEjbaS0meGCu5p .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QgEEjbaS0meGCu5p .labelText,#mermaid-svg-QgEEjbaS0meGCu5p .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-QgEEjbaS0meGCu5p .loopText,#mermaid-svg-QgEEjbaS0meGCu5p .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-QgEEjbaS0meGCu5p .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-QgEEjbaS0meGCu5p .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-QgEEjbaS0meGCu5p .noteText,#mermaid-svg-QgEEjbaS0meGCu5p .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-QgEEjbaS0meGCu5p .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QgEEjbaS0meGCu5p .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QgEEjbaS0meGCu5p .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QgEEjbaS0meGCu5p .actorPopupMenu{position:absolute;}#mermaid-svg-QgEEjbaS0meGCu5p .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-QgEEjbaS0meGCu5p .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QgEEjbaS0meGCu5p .actor-man circle,#mermaid-svg-QgEEjbaS0meGCu5p line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-QgEEjbaS0meGCu5p :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 双方进入 Sniff 模式 HCI_Sniff_Mode(Interval, Attempt, Timeout) LMP_sniff(D_Sniff, T_Sniff, D_Sniff_Attempt, T_Sniff_Attempt) HCI_Sniff_Mode_Request 事件(通知 Host B) HCI_Accept_Sniff_Mode_Request(或拒绝) LMP_accepted(LMP_sniff) HCI_Command_Complete (Sniff_Mode) HCI_Mode_Change 事件

注意:LMP_sniff 是 Master 发起的。如果 Slave 想进入 Sniff,它发 HCI 命令给自己的 Controller,Controller 会通过 LMP 通知 Master,由 Master 决定是否发起 Sniff 协商。

5.3 退出 Sniff

退出 Sniff 用 HCI_Exit_Sniff_Mode(Opcode 0x0804),底层走 LMP_unsniff:
Controller Host Controller Host #mermaid-svg-K1OkGhcGT4feiI3O{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-K1OkGhcGT4feiI3O .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-K1OkGhcGT4feiI3O .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-K1OkGhcGT4feiI3O .error-icon{fill:#552222;}#mermaid-svg-K1OkGhcGT4feiI3O .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-K1OkGhcGT4feiI3O .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-K1OkGhcGT4feiI3O .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-K1OkGhcGT4feiI3O .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-K1OkGhcGT4feiI3O .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-K1OkGhcGT4feiI3O .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-K1OkGhcGT4feiI3O .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-K1OkGhcGT4feiI3O .marker{fill:#333333;stroke:#333333;}#mermaid-svg-K1OkGhcGT4feiI3O .marker.cross{stroke:#333333;}#mermaid-svg-K1OkGhcGT4feiI3O svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-K1OkGhcGT4feiI3O p{margin:0;}#mermaid-svg-K1OkGhcGT4feiI3O .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-K1OkGhcGT4feiI3O text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-K1OkGhcGT4feiI3O .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-K1OkGhcGT4feiI3O .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-K1OkGhcGT4feiI3O .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-K1OkGhcGT4feiI3O .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-K1OkGhcGT4feiI3O #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-K1OkGhcGT4feiI3O .sequenceNumber{fill:white;}#mermaid-svg-K1OkGhcGT4feiI3O #sequencenumber{fill:#333;}#mermaid-svg-K1OkGhcGT4feiI3O #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-K1OkGhcGT4feiI3O .messageText{fill:#333;stroke:none;}#mermaid-svg-K1OkGhcGT4feiI3O .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-K1OkGhcGT4feiI3O .labelText,#mermaid-svg-K1OkGhcGT4feiI3O .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-K1OkGhcGT4feiI3O .loopText,#mermaid-svg-K1OkGhcGT4feiI3O .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-K1OkGhcGT4feiI3O .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-K1OkGhcGT4feiI3O .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-K1OkGhcGT4feiI3O .noteText,#mermaid-svg-K1OkGhcGT4feiI3O .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-K1OkGhcGT4feiI3O .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-K1OkGhcGT4feiI3O .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-K1OkGhcGT4feiI3O .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-K1OkGhcGT4feiI3O .actorPopupMenu{position:absolute;}#mermaid-svg-K1OkGhcGT4feiI3O .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-K1OkGhcGT4feiI3O .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-K1OkGhcGT4feiI3O .actor-man circle,#mermaid-svg-K1OkGhcGT4feiI3O line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-K1OkGhcGT4feiI3O :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 对端回 LMP_accepted HCI_Exit_Sniff_Mode(handle) 发送 LMP_unsniff HCI_Mode_Change 事件 (Mode=Active)

5.4 Sniff Subrating 的 HCI 命令

c 复制代码
// HCI_Sniff_Subrating 命令
struct hci_sniff_subrating_cmd {
    uint16_t handle;              // ACL 连接句柄
    uint16_t max_latency;         // 最大有效 Interval (×0.625ms)
    uint16_t min_remote_timeout;  // 对端最小 Timeout (×0.625ms)
    uint16_t min_local_timeout;   // 本端最小 Timeout (×0.625ms)
};

这个命令可以在 Sniff 模式下随时发,不触发状态切换。


6. TWS 场景参数调优

6.1 TWS 耳机的四种工作状态

状态 链路活动 推荐参数
待机(充电盒外) 偶尔有通知 Interval=500ms-1s, Attempt=8, Timeout=0
A2DP 播放 持续媒体流 Interval=40-60ms, Attempt=16, Timeout=16
HFP 通话 SCO/eSCO 占用 不能用 Sniff(见下文)
来电响铃 突发信令 用 Subrating,base=40ms, max=200ms

6.2 为什么 HFP 通话不能用 Sniff

HFP 通话走 SCO 或 eSCO 链路,SCO/eSCO 是同步链路,时隙是预留的。ACL 链路在通话期间仍然存在,但实际数据量很少(只有 HFP 控制信令)。

理论上 ACL 可以进 Sniff,但实际实现中:

  • SCO/eSCO 已经占用了大量时隙,剩余的 ACL 时隙很少
  • Sniff 协商的 Interval 可能和 SCO 时隙冲突
  • 大多数芯片厂商在 SCO 激活时不允许 ACL 进 Sniff

所以 TWS 通话期间,ACL 保持 Active,靠 SCO 的预留时隙机制省电。通话结束后再切回 Sniff。

6.3 A2DP 播放时的 Sniff 参数

A2DP 播放时,媒体流通过 ACL 传输,Sniff 参数直接影响音频稳定性:

Interval 的选择

SBC 帧周期约 20ms(48kHz × 16 blocks)。一个 RTP 包通常含 2-6 个 SBC 帧,即每 40-120ms 发一个 RTP 包。Interval 应该小于 RTP 包间隔,否则可能错过传输窗口。

推荐 Interval = 40-60ms,覆盖 1-2 个 RTP 包周期。

Attempt 的选择

Attempt 要足够收完一个 RTP 包。RTP 包 + L2CAP 头约 700-1000 字节,EDR 2Mbps 下传输约 4ms。考虑重传,Attempt 设 16 时隙(10ms)比较稳妥。

Timeout 的选择

A2DP 是连续流,一个 RTP 包后面可能紧跟着另一个。Timeout 设 16 时隙(10ms),让从设备在收完一个包后多醒一会儿,收下一个。

复制代码
A2DP Sniff 参数推荐:
Interval = 40ms  (64 × 0.625ms)
Attempt  = 10ms  (16 × 0.625ms)
Timeout  = 10ms  (16 × 0.625ms)

时间线:
│←── Interval 40ms ──→│←── Interval 40ms ──→│
│←Attempt+Timeout=20ms→│
▏醒▕────收RTP────────────▏醒▕────收RTP────────────
   ↑ 连续收完媒体包后休眠

6.4 待机时的 Sniff 参数

待机时没有持续数据,只需要偶尔收控制信令(如 AVRCP 按键):

复制代码
待机 Sniff 参数推荐:
Interval = 800ms (1280 × 0.625ms)
Attempt  = 2.5ms (4 × 0.625ms)
Timeout  = 0     (不额外等待)

时间线:
│←────────── Interval 800ms ──────────→│
│←2.5ms→│
▏醒▕─────────────睡─────────────────────
   ↑ 确认无数据,立即休眠

待机时 Interval 可以拉得很大,但要考虑两个约束:

  1. 链路监督超时(Supervision Timeout):Interval 必须 < Supervision Timeout,否则链路会被判超时断开。通常 Supervision Timeout 设 5-10s,Interval 最大不超过它的一半。
  2. 应用响应延迟:如果用户按按键,响应延迟 ≈ Interval。Interval=800ms 意味着按键响应最多延迟 800ms------这对 AVRCP 不可接受。

解决响应延迟的方法:用 Sniff Subrating。基础 Interval=40ms(低延迟响应),Max_Latency=800ms(待机时拉长)。有按键时立刻回到 40ms,无数据时逐步拉到 800ms。

6.5 主从切换时的 Sniff 处理

TWS 耳机主从切换时,Sniff 参数需要重新协商:

  1. 主从切换前,先 Exit Sniff 回到 Active
  2. 执行角色切换(LMP_switch_req)
  3. 切换完成后,新主设备重新发起 Sniff 协商

如果直接在 Sniff 状态下做角色切换,可能因为 Sniff 窗口不对齐导致切换失败。稳妥的做法是先 Exit Sniff。


7. 抓包分析 Sniff

7.1 HCI 层抓包

用 btmon 或 hcidump 抓 HCI 层,能看到 Host 和 Controller 的交互:

bash 复制代码
# btmon 抓包
sudo btmon -w sniff.pcap &

# 触发 Sniff 进入/退出
# ...

# 停止
sudo pkill btmon

Wireshark 过滤 Sniff 相关事件:

复制代码
# Sniff 模式切换事件
bt HCI_EVT_Mode_Change

# Sniff 命令
btcmd.opcode == 0x0803   # HCI_Sniff_Mode
btcmd.opcode == 0x0804   # HCI_Exit_Sniff_Mode
btcmd.opcode == 0x0C11   # HCI_Sniff_Subrating

HCI 层抓包看到的是"Host 下命令 → Controller 确认 → Mode_Change 事件",看不到具体的 LMP 交互。

7.2 空口抓包看 LMP

要看 Sniff 的 LMP 交互,需要空口抓包仪(Ellisys / Frontline)。关键 LMP PDU:

Opcode 命令 抓包看什么
0x08 LMP_sniff 参数协商:D_Sniff, T_Sniff, D_Sniff_Attempt, T_Sniff_Attempt
0x09 LMP_unsniff 退出 Sniff
0x54 LMP_sniff_subrating 子速率协商
0x00 LMP_accepted 接受 Sniff 请求

空口抓包还能看到 Sniff 模式下的实际时隙占用------主设备只在 Sniff 窗口发数据,其余时隙静默。这是验证参数是否生效的最直接方法。

7.3 验证 Sniff 是否生效

抓包看三个信号:

  1. HCI_Mode_Change 事件:Mode 字段从 0x00(Active)变成 0x02(Sniff),说明进入 Sniff 成功
  2. ACL 数据包的时间分布:Sniff 模式下,ACL 包应该集中在 Sniff 窗口附近,非窗口期应该没有包
  3. 电流测试:用功耗分析仪测电流,Sniff 模式下应该看到周期性的电流脉冲(Attempt 窗口),其余时间电流极低

8. 常见问题排查

8.1 进入 Sniff 失败

现象 :发 HCI_Sniff_Mode 后收到 HCI_Command_Status (Status=ERROR) 或长时间无 Mode_Change 事件。

排查

  • 检查参数合法性:Interval ≥ 0x0004,Attempt ≥ 0x0001
  • 检查连接状态:链路必须是 Active 才能进 Sniff
  • 检查是否有 SCO 激活:SCO 激活时通常不允许进 Sniff
  • 检查对端是否支持 Sniff:查 LMP features(Page 0 bit 5)

8.2 Sniff 模式下音频卡顿

现象:A2DP 播放时进入 Sniff 后,音频周期性卡顿。

原因:Interval 太大或 Attempt 太小,导致 RTP 包传输不及时。

调参方向

  • 缩小 Interval(如从 100ms 降到 40ms)
  • 加大 Attempt(如从 8 时隙加到 16 时隙)
  • 加大 Timeout(如从 0 加到 16 时隙)

8.3 Sniff 模式下链路断开

现象:Sniff 模式下链路突然断开,日志显示 Supervision Timeout。

原因:Sniff Interval > Supervision Timeout,或者信号质量差导致 Sniff 窗口里收不到数据,累计超时。

调参方向

  • 检查 Interval 是否 < Supervision Timeout / 2
  • 弱信号环境下加大 Attempt,增加接收成功率
  • 适当缩短 Interval,增加链路存活机会

8.4 待机功耗没降

现象:进了 Sniff,但整机功耗没明显下降。

排查

  • 确认 Sniff 真的生效(看 Mode_Change 事件)
  • 测控制器电流:如果控制器电流没降,可能是 RF 没真正休眠(检查硬件设计)
  • 检查其他耗电大户:传感器、LED、充电电路等可能抵消 Sniff 的省电效果
  • Interval 是否足够大:Interval=100ms 省电效果不明显,至少 500ms 以上

8.5 Sniff Subrating 不生效

现象:发了 HCI_Sniff_Subrating,但有效 Interval 没变化。

排查

  • 确认双方都支持 Sniff Subrating(LMP features Page 0 bit 15)
  • 检查 Max_Latency 是否 > 当前 Interval(否则没有拉长空间)
  • 检查链路状态:必须在 Sniff 模式下才能用 Subrating
  • 查控制器实现:某些低端芯片的 Subrating 实现有 bug,需要厂商确认

相关推荐
星核0penstarry3 小时前
从 Dialog-RSN-1 看语音 Agent 走向:企业如何评估音频原生模型与 API 服务
人工智能·音视频·音频·api
明如正午4 小时前
【python】Python + OpenCV 实现视频关键帧提取与智能去重
python·opencv·音视频
呆呆敲代码的小Y5 小时前
5 分钟上手 OpenMontage:把 AI 编程助手变成视频工作室
人工智能·aigc·音视频·ai视频生成·claude code·openmontage
奈斯先生Vector5 小时前
大模型 Agentic Workflow 架构解构:异构 API 调度与 Token 路由的多模态系统设计
开发语言·前端·架构·prompt·aigc·音视频
海带紫菜菠萝汤5 小时前
FFmpeg.wasm 实践:在浏览器中运行 FFmpeg 的能力边界与性能瓶颈
前端·javascript·ffmpeg·音视频·wasm
甜甜小酒窝5 小时前
通过Canvas在网页中将后端发来的一帧帧图片渲染成“视频”的实现过程
音视频
DogDaoDao6 小时前
深度学习增强的视频压缩框架:Neutron Star 如何超越 VVC
人工智能·深度学习·音视频·视频编解码·h266·vvc·视频压缩
byte轻骑兵6 小时前
蓝牙PBP公共广播协议:解锁LE Audio的公共音频新生态
音视频·蓝牙·蓝牙耳机·le audio·低功耗蓝牙音频
AI创界者6 小时前
【8G显存福音】最新LTX-2.3-22B-DISTILLED-1.1-VBVR 整合包文生视频、图生视频,支持首尾帧/单图无限时长,50系显卡全适配!
人工智能·音视频