经常有朋友问我:经典蓝牙耳机、鼠标、键盘这些设备,为什么在不传输数据时还能做到那么省电?答案其实就藏在经典蓝牙协议里那三个看起来有点年代感的名字里——Sniff、Hold、Park。别看这三个模式名字老,却一直活到今天所有BR/EDR设备的低功耗设计里。这篇文章就把它们的原理、参数、差异和实际项目中的坑一次讲透,适合做嵌入式蓝牙开发、硬件功耗优化,或者单纯想搞懂蓝牙为什么省电的朋友参考。
先说结论:Sniff、Hold、Park都是针对ACL链路的低功耗机制,它们的目标不是让设备断开连接,而是让设备在“保持连接”的前提下,尽量减少射频收发机的工作时间。理解了这个前提,后面所有细节都能串起来。
1. 为什么经典蓝牙要给ACL链路做省电设计
经典蓝牙在连接状态下,主设备和从设备之间是一条ACL链路,这条链路靠跳频和时隙来维持同步。如果你没做过底层开发,可以把ACL链路想象成两个人在一个固定的频道上不断隔空喊话:主设备隔一会儿就问一次“在吗”,从设备必须回应“在”。这套机制保证了数据随时能传,但也意味着从设备的射频接收机不能彻底关掉,它得一直竖着耳朵听。
问题就在这里。射频收发机一旦处于接收或发射状态,电流轻轻松松到十几甚至几十毫安。如果设备只是偶尔传一次数据,绝大部分时间都花在“等待被呼叫”上,那功耗基本都浪费在干等上了。我见过不少项目,功能做出来了,结果实测待机电流特别难看,一查全是经典蓝牙ACL链路在活跃状态下裸奔。
1.1 省电到底省在哪里
把这块电省下来,本质就是一句话:减少射频模块处于“唤醒监听”状态的时间。一个蓝牙设备正常情况下会经历两个状态:唤醒态和睡眠态。唤醒态里射频接收机在工作,可能是接收、发射,也可能只是在盲等;睡眠态里射频接收机关闭或进入极低功耗模式,只保留必要的外围时钟。
省电设计的目标,就是让设备尽量多待在睡眠态,同时保证主设备随时能找到它。听起来很简单,但蓝牙的跳频机制和时隙结构决定了两台设备必须约定好“下次什么时候再对表”,否则一觉睡过头,整个连接就断了。Sniff、Hold、Park三种模式,本质上就是三套不同的“对表协议”,它们在唤醒频率、恢复速度和参数灵活度上做了不同的取舍。
1.2 状态切换和使用前提
这三个模式不是设备想进就进的。它们要满足几个前提条件:首先设备必须在经典蓝牙BR/EDR模式下,并且已经建立起ACL连接;其次,模式切换由链路管理器(LMP)负责协商,主机控制器接口(HCI)向下发命令,最后由主从双方共同确认。
如果链路上还挂着SCO或eSCO语音链路,情况会复杂很多。因为SCO链路有固定的语音时隙,设备不能随便睡着。比如蓝牙通话耳机通常不会把整条链路放到Hold或Park模式里,顶多对ACL部分做一些低功耗处理。这是我之前做车载蓝牙项目踩过的坑:只考虑了ACL数据,结果通话链路一建立,整个省电策略全被语音时隙打断了。
2. 三种模式逐层拆解:Sniff、Hold、Park
这一部分我尽量把机制讲透。先记住一个基本模型:主设备通过轮询来维持连接,从设备在规定时间点醒来监听。三种模式的区别,就在于“监听的时间点”和“监听的时间长度”不一样。
2.1 Sniff模式:最实用、最灵活的降占空比方案
Sniff模式是我在实际项目里用得最多的一种,也是三种模式里技术含量最高、参数最多的一个。
它的核心思路是:把从设备的监听行为从“每个时隙都听”改成“每隔一段时间集中听一下”。这个“间隔”就是Sniff Interval,主从设备按这个间隔约定好时间点,从设备只在Sniff窗口附近醒来。窗口内双方正常收发数据,窗口一过,从设备立刻进入睡眠。
Sniff模式最关键的参数有四个:
- Sniff Interval:两个监听窗口之间隔多长时间,单位是1.25ms时隙。典型值可以从几十毫秒到一秒以上,视业务而定。
- Sniff Offset:从进入模式的时刻到第一个监听窗口的起始时间,用来对齐master和slave的时钟。
- Sniff Attempt:每次监听窗口内,从设备尝试监听的连续时隙数。
- Sniff Timeout:在监听窗口内如果一直没收到主设备的任何轮询包,从设备最多等多久就自动回去睡觉。
我用一个实际例子来说明功耗是怎么算的。假设一个蓝牙鼠标,业务特性是每40ms上报一次数据,那么可以把Sniff Interval设成40ms,Sniff窗口设成5ms。那这个设备平均每个周期里只有5ms在监听,睡眠35ms。如果唤醒态电流是15mA,睡眠态电流是0.5mA,那么平均电流就是:
15mA × 5/40 + 0.5mA × 35/40 = 1.875mA + 0.4375mA ≈ 2.3mA
对比活跃状态下常年15mA的水平,这个功耗一下子降了一个数量级。这就是为什么很多无线鼠标、键盘能做到“一节电池用半年”的原因。
Sniff模式的另一个好处是恢复速度比较快。因为从设备还保留着AM_ADDR,连接参数和跳频同步信息都没丢,只要到点醒来,立刻就能收发数据,不需要额外的重新同步过程。
2.2 Hold模式:简单粗暴的暂停
Hold模式是三兄弟里最简单的那个,也很容易理解:主设备或者从设备发起一个Hold请求,两边商量好一个Hold Timeout,在Timeout这段时间里,ACL链路上的数据全部暂停,从设备直接进入睡眠,不用监听任何东西。
Hold模式下设备省电效果很好,因为它是完全不听的。但也正因为完全不听,主设备在这段时间内发什么它都收不到,所以两个限制马上出现了:第一,业务数据必须能容忍中段空白,不能有实时性要求;第二,Hold的时长必须准确,到点了双方要能重新恢复联系。
Hold模式在实际项目里的定位比较尴尬。你说它省电吧,确实省;但Sniff模式下把监听时间压得很短之后,功耗已经很低了,Hold那点优势并不明显。你说它简单吧,它又要专门安排一段“断流时间”,业务逻辑反而要额外处理超时。
我用Hold模式做得比较多的场景是这种:某个外设需要周期性地和主机同步一次大数据,比如同步通讯录或者批量传文件。同步过程结束之后,未来几十秒都没有任何交互逻辑,那么直接进Hold模式睡一大觉,比在Sniff模式下每隔几十毫秒醒一次更划算。
2.3 Park模式:暂时冻结连接但保留身份
Park模式和前两个模式有个本质区别:Sniff和Hold模式下,从设备依然占着它的AM_ADDR,也就是说它仍然是“活动成员”;Park模式下,从设备会把AM_ADDR让出来,暂时变成一个“休眠成员”。
讲到Park模式,必须先讲清楚蓝牙的地址机制。一个主设备下面可能挂了好几个从设备,每个活动的从设备都分配一个3位的AM_ADDR。这个地址空间很小,最多也就支持7个活跃从设备。如果某些从设备数据极低频但又不能断线,一直占着AM_ADDR就很浪费。
Park模式的玩法是:把暂时不活跃的从设备停放起来,释放AM_ADDR给新的设备用。被停放的设备分到两个新地址,一个是PM_ADDR,用于主设备单独唤醒它时寻址;另一个是AR_ADDR,用于被停放设备主动发起唤醒请求。有一个细节值得注意:多个被停放设备可以共享同一个AR_ADDR。也就是说,它们主动请求唤醒的入口是同一扇门,但每次只能有一个从设备在访问窗口里发请求,否则就冲突了。
Park模式下,被停放的从设备并不会彻底失联。主设备会周期性地发送信标信号,信标里包含了信标间隔、访问窗口等同步信息。被停放的设备只需要按信标周期醒来,对一次时隙同步,然后继续睡。
Park模式能支持大量的从机挂在同一个主设备下面而不占用活动连接资源,这在某些多设备组网的场景里很有价值。但它的问题也同样明显:唤醒过程太长了。因为被停放的设备要先等下一个信标周期,再在访问窗口里用AR_ADDR发出请求,然后等主设备用PM_ADDR回复并重新分配AM_ADDR,整个流程下来几百毫秒甚至一两秒都可能。对延迟敏感的业务来说,这种唤醒速度根本没法用。
2.4 除了ACL,还有SCO/eSCO那些事
做经典蓝牙低功耗设计时,最容易被新手忽略的就是SCO和eSCO链路对省电模式的限制。
SCO链路是同步面向连接的语音链路,它占用固定时隙传输语音数据包。如果设备正在通电话或者用HFP协议,这时候ACL链路想进Hold、Park模式基本免谈,因为语音时隙是硬实时需求,不能因为要省电就跳过一个周期。
实际项目里,耳机、音箱这类音频设备会采用折中方案:SCO/eSCO继续维持,ACL部分用Sniff模式做一些低频数据通道的降功耗处理,比如完成A2DP的控制指令、AVRCP的按键响应。当音频流停止时,再把整条链路切到更激进的低功耗模式。设计这类策略时,务必要把“语音激活”和“语音空闲”当成两个完全不同的功耗状态来测试。
3. 三种模式横向对比与选型思路
为了让你快速做判断,我把常用维度的对比整理成一张表:
| 对比维度 | Sniff模式 | Hold模式 | Park模式 |
|---|---|---|---|
| 是否保留AM_ADDR | 保留 | 保留 | 释放 |
| 监听方式 | 按间隔监听窗口 | 不监听 | 按信标周期醒来 |
| 恢复活跃状态速度 | 最快,毫秒级 | 看Hold时长 | 较慢,可达百毫秒到秒级 |
| 参数复杂度 | 高,四五个参数 | 低,主要是一个超时时间 | 中高,信标和访问窗口 |
| 适合的数据类型 | 周期上报数据 | 可容忍静默期的批量业务 | 极低频心跳,多从机设备 |
| 多设备容量影响 | 不释放地址 | 不释放地址 | 释放地址,可挂更多 |
从当前市场看,Sniff模式毫无疑问使用最广。原因很简单:它把“省电”和“响应速度”平衡得最好。无线键盘、鼠标、遥控器、游戏手柄、运动手环,凡是走经典蓝牙做周期性数据上报的,基本上都是Sniff模式的忠实用户。
Hold模式适合那种“明确知道接下来一段时间完全没数据”的场景。它的特点是干净利落,但不够灵活。如果你拿不准下一包数据什么时候来,用Hold反而尴尬,因为设备可能在睡眠中被业务唤醒,整体功耗和延迟表现都不如Sniff。
Park模式在经典蓝牙里属于“听起来很美,用起来麻烦”的类型。它能支持大量从机,但它用唤醒延迟换取连接容量,只有在你真的需要让一个主设备挂很多个从设备、而且这些从设备的唤醒频率极低时才值得用。
选型时还有个隐藏维度:协议栈和芯片的支持情况。很多芯片厂商对Park模式的支持很保守,甚至直接不支持。我建议你先翻datasheet或者找FAE确认好,不然等画完板子、写完驱动才发现芯片不支持,会很被动。
4. 主机侧配置与参数配置实操
理论讲完了,下面进入实操环节。这些模式最终都是通过HCI命令和底层LMP协商完成的,但在主机侧,我们看到的通常是HCI命令和协议栈API。
4.1 HCI命令调用路径
在经典蓝牙的HCI层,对应这几个模式的命令主要有:
- HCI_Sniff_Mode:核心参数是Connection_Handle、Sniff_Max_Interval和Sniff_Min_Interval、Sniff_Attempt、Sniff_Timeout。
- HCI_Exit_Sniff_Mode:让从设备从Sniff模式回到Active模式。
- HCI_Hold_Mode:核心参数是Connection_Handle、Hold_Mode_Max_Interval、Hold_Mode_Min_Interval。
- HCI_Park_Mode:核心参数是Connection_Handle、Beacon_Max_Interval、Beacon_Min_Interval。
- HCI_Exit_Park_Mode:把停放设备重新激活。
注意这里有个有意思的设计:所有间隔参数都为“最小”和“最大”两个值。这不是重复,而是给链路层协商留了余地。举个例子,你设置Sniff_Min_Interval是30ms,Sniff_Max_Interval是50ms,那么LMP协商时双方可以在30到50ms之间取一个两边都舒服的数值,这样协议栈和芯片实现各有一些弹性空间。
时间单位的换算也很容易出错。这些间隔参数的单位是蓝牙时隙,一个slot是0.625ms,而Sniff Interval的参数值在常见芯片实现里通常以1.25ms为单位计,也就是2个slot。HCI命令的参数长度是2字节,最大值0xFFFF,所以理论上最多可以到81918.75ms,但实际控制器一般不会让你设那么大。拿到一个蓝牙芯片时,第一件事就是查厂商对每个参数的取值范围,别直接拿规范理论值去填。
4.2 协议栈适配要点
在Linux环境里,BlueZ是目前最主流的协议栈。内核的蓝牙子系统会管理HCI层,用户态通过bluetoothd和各类工具交互。不过,经典的Sniff、Hold、Park设置更多发生在HCI命令层,而不是常见的DBus API层。日常做应用开发的程序员很少直接碰这几个命令,它们多数由协议栈内部根据连接状态自动发起。
安卓平台的情况类似,BTA层已经封装好了不少链路策略。比如HID设备连接时,协议栈会按配置决定是否进入Sniff模式、间隔设多大。如果你在安卓源码里搜sniff相关的配置项,能看到不少默认参数是跟着profile走的。问题在于,不同手机厂商对底层控制器参数的调整程度不一样,所以同样一个蓝牙键盘,连着iPhone和连着某款安卓机的待机功耗可能差出一截,这锅很多时候不在键盘硬件,而在协议栈的策略配置上。
嵌入式项目里自由度就高很多。我常接触的经典蓝牙主控方案,比如TI的CC2564系列、高通或CSR的经典蓝牙芯片,都提供HCI命令的直接通路。你可以自己写一个简单的HCI命令发送函数,把HCI_Sniff_Mode的命令参数组装好,通过UART或USB发送给控制器。这段时间做功耗调试,几乎每一步都得手动调参数,靠协议栈的默认配置往往拿不到最优结果。
4.3 一个实际参数调试过程
拿我之前做过的一个经典蓝牙遥控器项目举例。设备是一块纽扣电池供电,业务是用户偶尔按键,平时大部分时间静默。最初协议栈默认配置下,设备每分钟被主机轮询很多次,实测整机待机电流在6mA左右,这个数据对产品来说完全不合格。
我的调试步骤是这样的:先用电流探头抓一段完整的电流波形,确认设备的唤醒间隔和唤醒窗口。然后通过HCI命令把Sniff Interval从默认值逐步拉大到160ms,同时在每次调参后跑一轮按键唤醒测试,测从按下按键到主机收到数据的总延迟。最后把Sniff Interval定在80ms,Sniff窗口尽量压缩,整机平均电流降到了1.5mA,按键延迟从原来的几十毫秒增加到200多毫秒,但用户体验还能接受。
这个过程的经验是:参数不能一次拉太狠。Sniff Interval一次翻好几倍,很可能导致连接不稳定或者延迟超标,你得在功耗和体验之间来回调几轮,找到一个甜点值。
5. 常见问题与排查技巧实录
最后这部分,我把这些年做经典蓝牙低功耗调试时遇到的典型问题整理成一个速查表,每个问题都对应了具体的排查思路。
5.1 电池寿命不如预期,先抓电流波形
不要凭感觉判断省电策略有没有生效,最直接的手段就是测量电流波形。方法也很简单:在电池供电回路里串一个低阻值的采样电阻,比如10mΩ,用示波器或者高精度电流探头观察电流变化。
波形一看就能看出很多东西:Sniff模式下会有规律的小凸起,每个凸起就是一次监听窗口;Hold模式是一段平滑低电流之后突然进入活跃;Park模式则能明显看到周期性的信标唤醒。如果电流波形里出现不规律的高频凸起,那多半是扫描、广播或者其他外设干扰,这时低功耗模式再优秀也没用。
5.2 唤醒延迟导致业务超时
经典蓝牙低功耗设计里,最容易被吐槽的就是唤醒延迟。现象是设备平时好好的,但一段时间不操作之后,第一次按键明显“迟钝”。
排查这个问题的思路是分清楚延迟出在哪一层。如果是Sniff模式的监听窗口没有对齐,可以通过调整Sniff Offset和Sniff Attempt来解决;如果是芯片从睡眠模式恢复需要时间,那就要查芯片底层的唤醒时间和主时钟启动时间;如果是主设备和从设备的时钟漂移太大导致监听窗口错过,就要适当增大Sniff窗口的时间。
很多MCU在进入低功耗模式后会关闭外部晶振,蓝牙控制器和MCU之间通过一个唤醒引脚下发中断。这个引脚的响应速度和固件里中断处理函数的执行时间,都可能成为延迟的一部分。实测时,我把整个链路的每一段延迟都单独测了一遍,才发现MCU固件里那个无关紧要的ADC采集函数占了将近60ms的执行时间,优化之后立竿见影。
5.3 Park模式连接掉线与芯片差异
Park模式有个经典问题:被停放设备醒来后,发现主设备已经把它“忘记”了,连接直接断开。这个问题的根源往往是主设备和从设备对信标周期的理解不一致,或者有一方的时钟漂移过大,导致被停放设备错过了访问窗口。
遇到这种掉线问题,我一般先做两个检查:第一,确认主设备有没有定期发信标,信标周期和访问窗口设置得靠不靠谱;第二,从设备在Park模式下有没有真正按照信标周期醒来,如果固件里有一个定时器优先级设置不当,很容易导致醒来时机不对。
如果芯片对Park模式支持不完善,我的建议是干脆不用它。工业级方案里稳定优先,让设备保留AM_ADDR,用比较长的Sniff间隔代替Park,连接可靠性会高很多,代价只是多占一点地址资源。反正经典蓝牙一个主设备下面挂7个活跃从机,大部分场景也够用了。
5.4 排查速查表
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 平均功耗偏高 | 未进Sniff/Hold/Park,或间隔太小 | 抓电流波形,确认模式切换状态 |
| 按键响应突然变慢 | Sniff间隔过大或唤醒链路阻塞 | 逐段测延迟,调整监听窗口和芯片唤醒逻辑 |
| 低功耗模式下偶尔掉线 | 时钟漂移、监听窗口太短 | 增大Sniff窗口、检查主从晶振精度 |
| Park模式无法唤醒 | 信标参数不匹配、厂商不支持 | 换用Sniff长间隔方案 |
| 音频通话时功耗异常 | SCO/eSCO链路限制了ACL省电 | 单独测通话态,配合协议栈做策略区分 |
再补一个容易被忽略的点:寄存器配置和固件版本。有些芯片的省电模式参数藏在厂商专用HCI扩展命令里,比如Sniff Subrating相关命令,默认可能没开启。如果你用的芯片支持Sniff Subrating,强烈建议打开,它可以在Sniff模式基础上进一步优化监听窗口的退出时机,功耗还能再降一截。
最后分享一个我做项目时的心得:经典蓝牙的省电设计,本质上就是一个“监听间隔经济学”问题。间隔越大,功耗越低,但唤醒越慢;间隔越小,响应越快,但功耗越高。没有什么参数是永恒的正确答案,只有适不适合你的业务场景。做功耗优化的朋友,建议把电流波形测量设备常备在工位上,每一次调参都对着波形说话,而不是对着规格书空想。这样踩过几次坑之后,你自然就能在功耗和体验之间找到那个舒服的平衡点了。