【LMP】规范精讲[9]: 省电的阶梯——hold 打盹、sniff 值班与 park 的谢幕

发布时间:2026/10/29 2:11:17
【LMP】规范精讲[9]: 省电的阶梯——hold 打盹、sniff 值班与 park 的谢幕

上一篇讲角色切换时说过,重连那场戏里链路一建立就加密、一有空档就切换。这次把镜头对准空闲时段。连接保持期间,链路管理器有一套完整的功耗管理剧本:忙时全员值守,闲时分档打盹。先把抓包里的时间线摆出来——音频停止后约半分钟,外设主动发起进入嗅探模式,间隔 500ms,也就是每半秒才醒一次;三十秒后切歌需要重新配置流,外设自己叫醒自己退出嗅探;配置完还没开播,空档里又降频回去;直到播放开始前一刻,中心设备发起唤醒,退出嗅探后 4ms 就发出了 AVDTP Start。整条时间线进进出出四个来回,节奏完全跟着业务忙闲走。


目录

一、Hold:挂牌歇业的打盹

二、Sniff:定点值班的省电常态

2.1 进出协商与交接班困境

2.2 空口的三层解读

三、Sniff subrating:把班次再拉长

四、Park 的谢幕

五、测验


省电这件事在规范里是分台阶的:active 全天值守,hold 按需打盹,sniff 定点值班,subrating 再把班次拉长。而第四个名字 park,已经被移出舞台。下面按这个阶梯一层层往下走。


一、Hold:挂牌歇业的打盹

Hold 是最粗暴的省电方式——双方约好一个时间段,这段时间内 ACL 链路上不再收发任何异步数据,就像小店挂出歇业告示,写明几点到几点不开门。规范规范给的定义很克制:

The ACL logical transport of a connection between two Bluetooth devices can be placed in Hold mode for a specified hold time.

关键词是 for a specified hold time。Hold 天然带着起止时间,到点自动恢复,不需要任何唤醒消息。基带侧的补充描述更具体:歇业期间外设暂时不收发 ACL 包,但同步链路(SCO/eSCO)的保留槽照常服务,LT_ADDR 也原样保留,定时器 holdTO 一到点,外设醒来重新同步信道,等中心设备调度。所以 Hold 更像歇业不歇业到根:预定的 VIP 服务(语音同步业务)照常,只是散客(ACL 数据)一律不接。释放出来的容量可以去扫描、寻呼、查询,或者照顾另一个微微网里的连接——这正是 Hold 的核心动机,它是给分散网和并发业务腾时隙的。

两个 PDU 支撑整个流程:LMP_HOLD 和 LMP_HOLD_REQ,内容都是 Hold_Time 加 Hold_Instant。前者用于强制执行,后者用于协商。instant 用中心设备时钟值表示,双方都能解读,门槛有两道:

The hold instant is chosen by the sender of the message and should be at least 6×Tpoll slots in the future. The hold instant shall be within 12 hours of the current clock value to avoid clock wrap.

6 倍轮询间隔是给消息传播和收尾留的时间;12 小时上限又是老朋友了——上一篇切换时刻的 12 小时约束同源,28 位蓝牙时钟约 46.6 小时绕一圈,instant 传递的是绝对时钟值,双方各自算差值,一旦跨越回绕点就会解读出天壤之别的结果,所以差值必须压在半圈以内,12 小时是留足余量的工程上限。

强制执行有前提。中心设备或外设都只能在此前有过一次被对方接受的 Hold 请求之后,才能直接用 LMP_HOLD 生效,而且强制执行的保持时长不得超过对方此前接受过的值。这个设计很讲道理:协商请求相当于签了一份授权书,授权书里约定过最长打盹时间,之后单方面行使权力不能超出授权范围。没有授权书就强制歇业,等于剥夺对方的话语权,规范不允许。

外设强制 Hold 的流程里有个细节值得注意:外设先发 LMP_HOLD,然后等中心设备也回一条 LMP_HOLD——两边都确认到点后才真正歇业。中心设备收到后会检查 instant,如果不足 6×Tpoll 未来,直接改到至少 6×Tpoll 再回。中心设备拥有改期的最终决定权,这和角色切换里外设只能建议、中心设备拍板是一致的权力结构。

协商流程(LMP_HOLD_REQ)则是标准的三段式:请求、反请求、接受或拒绝。参数谈不拢就用错误码 0x20(Unsupported LMP Parameter Value)终结,谈得拢就 LMP_ACCEPTED 收尾。注意这里 instant 的门槛悄悄提高了:

The LM sending the LMP_HOLD_REQ PDU selects the hold instant, that shall be at least 9×Tpoll slots in the future.

强制是 6 倍,协商是 9 倍。多出来的 3 倍正好是一条请求加一条响应的往返余量——协商消息自己也要在路上跑,出发时刻必须给这一来一回留出时间,否则响应还没回来,歇业时刻就到了,时序就乱了。另外协商双方在到点前都保留了反悔窗口:定时器到期前收到 NOT_ACCEPTED 或新的 HOLD_REQ,都可以叫停这次打盹。

还有一个容易忽略的坑:中心设备发出 LMP_HOLD 后,如果迟迟等不到基带确认,它可以选择照样进入 Hold,但规范明确 it shall not use its low accuracy clock during the hold——不准切到低精度时钟。原因想想就通:外设可能压根没收到这条消息,它还在正常值守随时可能发数据上来,中心设备如果为了省电切到粗糙的时钟,醒来时的定时就不可靠了。牺牲一点省电收益,换取对不确定性的兜底。

二、Sniff:定点值班的省电常态

Hold 是歇业,sniff 是排班。进入嗅探后,外设不再每个轮询周期都竖着耳朵,而是只在固定的嗅探锚点醒来。锚点的节奏由两个参数决定:间隔 TSniff 和偏移 DSniff,DSniff 定位第一个锚点,之后每隔 TSniff 一个。中心设备则承诺只在嗅探槽里发起传输。空口抓包里那条请求的参数翻译过来就是:每 800 槽(正好 500ms)一个锚点,监听窗口 4 槽,超时延长 1 槽。

锚点定位的公式藏在基带规范里,也是 Timing_Control_Flags 存在的意义:

$$CLK27−1 mod Tsniff = Dsniff (initialization 1) CLK27,CLK26−1 mod Tsniff = Dsniff (initialization 2, this implies that Dsniff must be even)$$

两个初始化选项按发起方看到中心设备时钟最高位 CLK27 是 0 还是 1 来选择,接收方不看自己的时钟、严格按标志位执行。这又是一次防回绕的对齐操作:时钟 28 位循环计数,直接拿时钟值算第一个锚点,一旦算出的时刻落在过去,双方对该不该加一圈的理解就可能分叉。用 CLK27 把时钟切成上下两个半幅,各自套用对应公式,保证算出的锚点永远落在未来的半幅以内,歧义被结构性消除。这和角色切换里 slot offset 的两级对表、12 小时上限,是同一套时钟哲学在不同场景下的变奏。

监听行为由两个参数精细控制,规范规范值得逐字读:

The Sniff_Attempt parameter determines for how many slots the Peripheral shall listen ... beginning at the sniff slot, even if it does not receive a packet with its own LT_ADDR. The Sniff_Timeout parameter determines for how many additional slots the Peripheral shall listen ... if it continues to receive only packets with its own LT_ADDR.

Attempt 是保底值班时长:从锚点起无条件听满这么多槽,哪怕一个自己的包都没收到。Timeout 是加班规则:只要一直有发给自己的包,就再多听几槽。基带规范把加班条件列得更细——收到含 ACL 数据的匹配包要继续听,自己刚发过含 ACL 数据的包也要继续听(对方大概率要回确认),收到任何匹配 LT_ADDR 的包则可以继续听,另一个微微网有活动时可以提前离席。两个极端参数能勾勒出性格:attempt 等于 1 且 timeout 等于 0,就是只听锚点那一槽的铁公鸡模式;attempt 为 0 则被禁止,保底都不能没有。抓包里 att=4、timeout=1 属于典型均衡档:醒 4 槽保平安,有活多待 1 槽。

两条纪律要记住。其一,嗅探模式运行期间不能改参数,想换间隔必须先退出再重新协商。其二,嗅探只管异步逻辑传输,同步链路的保留槽和重传窗口照旧,语音业务不受这个省电机制影响——和 Hold 一样,省电永远不碰已承诺的同步业务。

2.1 进出协商与交接班困境

嗅探的进出协商和 Hold 的三段式几乎同构,任何一方都可以发起 LMP_SNIFF_REQ,对方可以拒绝、可以还价(回一条参数不同的 SNIFF_REQ)、也可以接受。但这里多出了一个 Hold 没有的角色:Sniff Transition mode。中心设备第一次发 SNIFF_REQ 之前必须先进这个过渡态,收到对方的 SNIFF_REQ 也要进,协商破裂(收发 NOT_ACCEPTED)就退出,收到 LMP_ACCEPTED 的基带确认才退出并正式进入嗅探。

为什么要这个过渡态?基带规范说得非常直白:

Sniff Transition mode is required because during this transition it is unclear which mode the Peripheral is in and it is necessary to ensure that the Peripheral is polled correctly regardless of which mode it is in.

消息在空口飞的时候,中心设备根本不知道对端这一刻已经切到嗅探了还是还在 active——确认也许还在重传路上。这个不确定性窗口里,稳妥的做法是两套值班表并行:中心设备维持 active 模式的轮询间隔(防对方还在 active),同时在每个嗅探锚点的监听窗口里至少发一次(防对方已经进嗅探),这次传输还兼着算 active 轮询的一次。代价是必须用高精度时钟,不能省。交接班期间宁可贵一点,也不能漏接。这和上一篇 hold 无确认也不准切低精度时钟是同一种保守主义。

退出用 LMP_UNSNIFF_REQ,被请求方必须回 ACCEPTED。中心设备发起时它先进过渡态,外设收到就退嗅探;外设发起时中心设备进过渡态。两边对过渡态的退出条件略有差异——中心设备都要等 ACCEPTED 的基带确认才收工,原因同样是确认可能还在重传,不能提前撤掉双保险。

2.2 空口的三层解读

回头看开头那张时间线图,有三层可以品。

第一层是发起方:两次进入嗅探都是外设发起的。外设最清楚自己的电量压力,主动降频合理,而且嗅探协商允许外设发起这件事本身就是规范给外设的话语权。

第二层是退出动机:第一次退出后 25ms 内紧跟着 AVDTP Set Configuration 和 Delay Report,外设为了传配置消息把自己叫醒;第二次退出是中心设备发起,4ms 后 AVDTP Start——播放是大数据量业务,500ms 一醒的节奏完全喂不饱,必须在开播前唤醒。

第三层是参数:间隔 500ms 意味着空口上每秒只有 2 个锚点,相比 active 状态毫秒级的轮询,射频开启时间的压缩是以数量级计的。

主机侧的对照图补上了另一半故事:进入嗅探是控制器协商完成后用 Mode Change 事件上报的,主机全程被动知情;而唤醒是主机主动下发 Exit Sniff Mode 命令触发的。更有意思的是唤醒 7ms 后,主机紧接着把链路策略设置改成了仅允许 Hold——把嗅探的授权直接收走了。这是主机的强势表态:接下来是播放业务,控制器不准再自作主张降频,要省电也只准用 Hold 这种到点自动醒的方式。链路策略位就是主机给控制器画的权力边界。

三、Sniff subrating:把班次再拉长

嗅探已经是每 500ms 醒一次,但要是几个小时没业务呢?subrating 的思路是在嗅探之上再叠一层更粗的班次:平时只在少数几个锚点醒来,一旦有数据往来,立刻恢复成全量锚点值守。

状态机只有两个状态和一个定时器:处于子速率状态时,Tsniff_mode_timeout 定时器一直倒数,期间任何 ACL-U、APB-U、ACL-C、APB-C 数据的收发都会重启定时器并保持全量嗅探;定时器耗尽都没有业务,才降级到稀疏锚点。有个容易漏的点:NULL 和 POLL 包不算数据、不重启定时器——空口的心跳包不构成业务繁忙的证据。另一个兜底是发包后的临时回岗:发完一个需要基带确认的包,要临时回到全量嗅探等确认到手,不能发完就睡。

协商用 LMP_SNIFF_SUBRATING_REQ 和 RES,参数三个:最大子速率、最小嗅探模式超时、生效时刻。生效时刻的约束相当具体:

The Sniff_Subrating_Instant value shall be a maximum of 2^16 time slots (40.9 seconds) from the Central's current clock and shall be a sniff anchor point.

一是不得超过 2 的 16 次方槽、即 40.9 秒,二是必须落在某个嗅探锚点上——子速率的稀疏锚点是从全量锚点里抽出来的,新参数的起点没有理由对不齐锚点网格。发起方要等时刻过了、且收到 RES 之后才切换新参数;非发起方还要再多等一步,RES 的基带确认到手才切换。稳字当头,每一方都要拿到对方明确在场的证据才动。

锚点怎么抽,由双方的 Max_Sniff_Subrate 决定,设中心设备的是 M、外设的是 N、生效时刻是 A:

两边相等就各自按 A + Tsniff × M × k 取锚点;不相等时,子速率大的一方至少每 j 个(大除以小)对方锚点要对齐一次。这套规则保证不管双方省电档位差多少倍,总存在双方同时醒着的公共锚点,链路不会退化成永远对不上表的悲剧。

和链路监督超时的关系是面试高频考点,规范规定得斩钉截铁:

The maximum clock interval between two sniff subrating anchor points shall be less than the link supervision timeout.

两个子速率锚点之间的最大间隔必须小于监督超时。道理很朴素:监督超时是链路的死亡判定线——超时没收到任何包就断链。如果最稀疏的两个锚点间隔比这条线还长,设备睡过头一觉醒来链路已经没了,省电省到掉线。如果确实需要把监督超时改得比锚点间隔还短,流程也规定死了:中心设备先禁用子速率,发 LMP_SUPERVISION_TIMEOUT 带上新值,收到基带确认后才开始用新超时,外设收到后同样先禁用子速率再用新值。顺序不能乱,先撤掉会踩线的省电机制,再收紧判定线。并且此后发起子速率时,最大子速率必须小于新的监督超时,把约束闭环。

用代码把这套校验写出来会更直观:

SLOT_US = 625 # 单槽 625 微秒 CLOCK_BITS = 28 # 蓝牙时钟 28 位 CLOCK_WRAP_SLOTS = 1 << CLOCK_BITS # 约 46.6 小时回绕一圈 def check_sniff_params(t_sniff, d_sniff, attempt, timeout): # 间隔:偶数槽,锚点才落在主->从的起始槽上 if t_sniff < 2 or t_sniff % 2: return False, "TSniff 必须为不小于 2 的偶数" if d_sniff % 2: # 初始化 2 要求 DSniff 为偶数 return False, "DSniff 必须为偶数" if attempt == 0: # 保底监听时长禁止为 0 return False, "Sniff_Attempt 禁止为 0" if timeout == 0 and attempt == 1: pass # 合法:只听锚点首槽的铁公鸡配置 return True, "ok" def first_anchor(clk, t_sniff, d_sniff, init_flag): # 防回绕的锚点定位:init 1 取低 27 位,init 2 取低 26 位 mask = (1 << 27) - 1 if init_flag == 1 else (1 << 26) - 1 return (clk & mask) - 1 + d_sniff % t_sniff if False else \ ((clk & mask) - 1) // t_sniff * t_sniff + d_sniff def check_subrating(t_sniff, max_subrate, supervision_slots): # 核心约束:最稀疏锚点间隔必须小于监督超时 gap = t_sniff * max_subrate if gap >= supervision_slots: return False, ("锚点间隔 %d 槽 >= 监督超时 %d 槽," "需先禁用子速率并重设监督超时" % (gap, supervision_slots)) if max_subrate >= supervision_slots: return False, "Max_Sniff_Subrate 必须小于监督超时" return True, "ok"

第一段校验嗅探参数的合法形状,第二段实现防回绕的锚点定位(真实芯片里这段逻辑跑在基带,链路管理器只负责把标志位带对),第三段把子速率与监督超时的死线关系变成可执行的断言。工程上这类约束最典型的翻车场景就是产品把嗅探间隔越调越大追求续航,某天叠加了子速率,最稀疏锚点间隔悄悄越过了监督超时,于是闲置一段时间后链路静默掉线,复现条件还极其刁钻。

四、Park 的谢幕

它曾经的定位是省电的终极形态:外设交出自己的 LT_ADDR,改用 PM_ADDR 和 AR_ADDR 两个身份,不再被轮询,只靠中心设备周期性广播的信标维持松散同步,几乎零功耗地挂在微微网里,需要时再被叫醒恢复活跃。听着很美好,但代价是把微微网的成员管理复杂度推到了极限——地址转换、信标调度、成员进出,每一项都是控制器实现和互操作测试的负担。而它省下的那点电,在低功耗蓝牙出现后忽然不香了:待机挂机这种场景,BLE 用一套完全不同的架构做得更省、更简单。于是较新版本的规范把它整体删除。

对工程师的实际影响是两件事:一是读老设备抓包或维护老协议栈时还会遇到 park 相关消息,得知道它从哪来;二是面试里被问到经典三模式(hold、sniff、park)时,能补一句 park 已在新规范中移除、以及为什么,立刻和背概念的人拉开差距。顺手澄清一个中文世界的常见误译:park 常被译成驻留模式或停等模式,都比不上理解它交出地址、只听广播的实质来得重要。

五、测验

问题一:LMP_HOLD 强制执行的 instant 门槛是至少 6×Tpoll,而 LMP_HOLD_REQ 协商的门槛是至少 9×Tpoll,为什么协商要多 3 倍?

参考答案:

强制执行的 LMP_HOLD 是单条消息,接收方收到后启动定时器即可,6 倍轮询间隔覆盖消息传播加收尾已经够用。而协商流程中 LMP_HOLD_REQ 发出后,对方还要回一条 LMP_ACCEPTED 或带新参数的反请求,这一来一回本身要消耗时间,出发时刻必须给请求与响应的完整往返留出余量,否则响应还在路上、歇业时刻已到,双方对是否进入 Hold 的判断就会分叉。多出的 3 倍正是给这一轮往返预留的时序空间。另外两道防线共同兜底:协商双方在定时器到期前收到 NOT_ACCEPTED 或新 HOLD_REQ 都可中止;中心设备未收到基带确认也可以进 Hold 但禁止使用低精度时钟,防止外设没收到消息时中心设备定时漂移。

问题二:嗅探协商为什么需要 Sniff Transition mode?中心设备在这个过渡态里具体要做什么?

参考答案:

协商消息在空口传输并等待基带确认期间,中心设备无法确定外设此刻处于 active 还是 sniff——ACCEPTED 可能还在重传。若中心设备按对方已进入嗅探处理,只发嗅探槽,而对方还在 active 全天值守,会触发监督超时;反之若按 active 处理持续轮询,对方已经睡了又会大量空发。因此规范引入过渡态:中心设备维持 active 模式的轮询间隔,同时保证在每个嗅探锚点的监听窗口内至少发起一次传输,且该传输计入 active 轮询,两套值班表并行直到拿到 ACCEPTED 的基带确认。过渡态期间还强制使用高精度时钟,牺牲功耗换确定性。工程实感:抓包里嗅探进出从不出错,靠的就是这段看似多余的双保险。

问题三:嗅探子速率与链路监督超时之间有什么强制约束?如果业务需要把监督超时改到比当前最稀疏锚点间隔更短,正确的操作顺序是什么?

参考答案:

约束是两个子速率锚点之间的最大时钟间隔必须小于链路监督超时,且 Max_Sniff_Subrate 参数本身也必须小于监督超时。

原因:监督超时是断链判定线,超过该时长未收到任何包即判定链路失效;若最稀疏锚点间隔大于等于该值,设备按子速率休眠一觉醒来链路已被判死,省电机制直接导致掉线。

正确顺序:中心设备先禁用子速率,发送 LMP_SUPERVISION_TIMEOUT 携带新超时值,收到该消息的基带确认后才开始使用新值;外设收到后同样先禁用子速率再启用新超时。此后若再启用子速率,最大子速率必须小于新监督超时。顺序不可颠倒:先撤掉可能踩线的省电机制,再收紧判定线,任何时刻都不允许出现锚点间隔大于等于监督超时的组合。