RK平台BLE广播扫描低功耗唤醒方案设计与实践
RK平台上的 BLE 低功耗唤醒,是这两年做电池类产品绕不开的一个方案点。我接触过的项目里,凡是带锂电池、要求待机几个月起步的设备,最后几乎都会把“谁能把主控叫醒”这个问题单独拆出来做设计。原因很直接:主控 SoC 跑起来的时候,系统待机电流动辄几十毫安,就算进了 suspend,配上 DDR 保持和外设漏电,整机也常在 5~20 mA 徘徊,一块 2000 mAh 的电池撑不过一周。但人体或遥控器真正需要设备响应的时间,一天加起来可能就几分钟。所以把“一直醒着监听”这件事交给一颗常驻待机的 BLE 协处理器,让它用几百微安的代价守住空中接口,一旦收到约定好的 BLE 广播包,就通过一根 GPIO 把主控拉起来——这套思路就是本文要拆的 BLE 广播扫描唤醒方案。
它解决的问题说白了就是待机功耗和响应实时性之间的矛盾。适合谁来参考?做智能门锁、遥控器、骑行码表、车载 T-Box 待机唤醒、便携医疗设备、工业手持终端的硬件工程师和嵌入式软件工程师,只要有“设备大部分时间沉睡、偶尔被一个近距离信号叫醒”的场景,这套东西都能直接搬。下文我会从选型、时序计算、广播包格式、协处理器固件、主机侧唤醒配置一直写到实测数据和踩坑记录,代码和参数都给到能照着改的程度,同时把每个决定背后的“为什么”讲清楚,避免你抄了个不合适的配置回去。
1. 先想清楚:BLE 唤醒到底解决的是什么问题
行业里常见的“低功耗唤醒”有几种做法,很多团队一开始就走了弯路,原因不是技术选错了,而是没先算清楚功耗账。我见过最典型的失误是拿主控自带的蓝牙模块做扫描唤醒:主控进了 suspend,蓝牙控制器确实还在工作,让系统不真正“睡死”,结果整机待机从 15 mA 只降到 12 mA,白忙一场。所以在动手前,必须先把可选路径摊开对比,把每条路的电流、延迟、成本、开发量摆到一张表上,再决定往哪走。
1.1 三条技术路线的取舍与代价
BLE 场景下叫醒一台设备,主流就三条路:连接式、广播扫描式、以及混合式。三者的差别不在“能不能实现”,而在谁在守夜、守夜的代价是多少。
连接式唤醒是让外设一直维持 BLE 连接,靠连接事件的周期通信保活。好处是链路可靠、有握手、能做双向确认;坏处是连接事件本身要求两端保持时序同步,每次连接事件都要开射频,连接间隔最小 7.5 ms,做到 1 s 已经算省电,但平均电流依然在几百微安到毫安级,而且断连重连的状态机很啰嗦。更麻烦的是连接状态对“一对多”不友好,一把锁配十个遥控器就得维护十条链路。
广播扫描式则是反过来的思路:唤醒源(手机、遥控器、手环、另一台设备)只管每隔几百毫秒发一个广播包,不建立连接、不握手、不管有没有人听。接收侧用一颗低功耗 BLE 芯片常驻被动扫描,收包后比对特征字段,命中就拉 GPIO。这条路把“守夜人”的角色彻底交给了射频前端,主控可以真正断电或进深度休眠。
第三条是混合式:平时广播扫描守着,命中后快速建一次连接把数据取回来,取完立刻断开。它把广播的低功耗和连接的可靠性结合起来了,代价是固件复杂度上一个台阶。我的经验是:只有“唤醒后要传数据”且“数据量超过一个广播包”时才值得上混合式,否则纯广播就够。
| 路线 | 待机平均电流(整机) | 典型唤醒延迟 | 一对多支持 | 开发量 | 适用场景 |
|---|---|---|---|---|---|
| 连接式保活 | 0.5~3 mA | 一个连接间隔内 | 差 | 中 | 需要双向实时通信 |
| 纯广播扫描 | 20~200 µA | 200 ms~1 s | 好 | 低 | 单向触发、遥控、门锁 |
| 广播触发+短连接 | 30~250 µA | 300 ms~1.5 s | 好 | 高 | 唤醒后需传配置或数据 |
选型时还有一个容易被忽略的维度:唤醒源是谁。如果唤醒源是手机,那必须考虑 iOS 和 Android 的后台广播限制——iOS 在后台的广播间隔会被系统拉长到接近 1 s 甚至更久,Android 各厂商的后台策略差异更大。这意味着你的扫描参数不能按“广播间隔 100 ms”来设计,否则真机上唤醒延迟会难看到无法交付。我一般按“广播间隔最坏 2 s”做兜底,宁可多扫一点。
1.2 广播扫描为什么天然比连接更省电
很多人对 BLE 省电的理解停留在“BLE 就是低功耗”,但真要抠到微安级,得理解它的射频占空比模型。广播和扫描都属于无状态、无同步的通信:发送方按自己的节奏发,接收方按自己的节奏开窗,两者不需要对齐时钟,也不需要维护链路层的状态机。
这带来的直接好处是:接收方可以自由决定“我什么时候开射频”。开窗 20 ms、间隔 200 ms,射频 90% 的时间是关着的,这就是省电的物理基础。而连接式通信里,两个设备必须严格对齐锚点,为了不错过连接事件,接收方要在预期时刻提前开窗(通常提前 1.25 ms 到 6 ms 不等),而且连接间隔一到就必须醒,没有“这次不听了”的选项。
还有一层是协议栈开销。广播扫描在链路层之上几乎不需要协议栈参与,收到包做个过滤就完事;连接式通信要维护 L2CAP、ATT、GATT 那一整套,RAM 和 CPU 的唤醒次数完全不是一个量级。我在实测中对比过同一颗 BLE 芯片:广播扫描配置成 10% 占空比,平均电流约 700 µA;换成 500 ms 连接间隔的保活,平均电流反而到了 900 µA 左右,而且还多了一个“连接断了要重连”的额外功耗峰值。
注意:广播扫描的低功耗优势建立在“不做主动扫描”上。主动扫描会额外发 SCAN_REQ 并等 SCAN_RSP,每次扫描窗口都被拉长,功耗至少涨 30%~50%。除非你确实需要读扫描响应里的数据,否则一律配被动扫描。
2. 广播与扫描的底层机制:把功耗算明白
参数不是拍脑袋定的。我见过不少项目直接把别人的配置抄过来,窗口 50 ms、间隔 100 ms,一算占空比 50%,待机电流直接飙到 3 mA,然后回头怀疑芯片有问题。所以在写代码之前,先把广播和扫描的时序模型在纸上推一遍,这一步省不掉。
2.1 广播信道与扫描窗口的时序关系
BLE 的广播走三个固定信道:信道 37 在 2402 MHz,信道 38 在 2426 MHz,信道 39 在 2480 MHz。这三个频点刻意避开了 Wi-Fi 最拥挤的频段中心,是当年标准制定时留下的抗干扰设计。一个广播事件里,发送方会在这三个信道上依次各发一包,包与包之间的间隔很小(通常几百微秒),整轮发完大概 1~2 ms。
扫描方这边有两个参数:扫描间隔(scan interval)和扫描窗口(scan window),单位都是 0.625 ms,且窗口必须小于等于间隔。扫描器在一个间隔周期内开窗听一段时间,窗口内它会在三个信道之间轮换驻留——具体轮换策略各家芯片实现不同,有的按固定顺序切换,有的是在一个窗口内平均分配时间。
这里就出现了第一个关键设计点:窗口必须足够长,让扫描器在三个信道上都有驻留机会。如果窗口只开 3 ms,扫描器可能只来得及在信道 37 上听一下,而广播方这一轮正好在信道 39 上发,那就完美错过。我一直用的经验值是:单次窗口不少于 15 ms,让每个信道至少能分到 5 ms 左右的驻留时间,这样一轮广播事件被捕获的概率才有保障。
第二个设计点是间隔与广播间隔的互质关系。假设扫描间隔固定 200 ms,广播间隔固定 100 ms,两者是整数倍关系,那么扫描器每次开窗的时刻相对于广播事件的相位是锁定的。如果这个相位刚好落在广播事件的空隙里,你就会遇到“设备在面前怎么都不唤醒,走远两步反而醒了”的诡异现象。解决办法很简单:给扫描间隔加 ±10% 的随机抖动,或者在固件里让间隔在 180~220 ms 之间轮换,把相位锁死这件事彻底打散。
2.2 唤醒成功率与延迟的估算方法
产品交付时老板一定会问“多久能唤醒”。这个数字不能靠感觉,我给一个我常用的估算模型,实测误差通常在 20% 以内。
先定义几个量:扫描窗口 W,扫描间隔 T_s,广播间隔 T_a。在一个扫描周期内,扫描器处于接收状态的时间占比是 W/T_s,这就是占空比 D。广播方每 T_a 发一轮,每轮包含三个信道的包,持续约 1.5 ms。
粗略估算:单个广播事件被捕获的概率约等于 D 乘以信道匹配修正系数。因为扫描窗口内扫描器会轮换三个信道,理论上一轮广播事件里的三个包都有机会被听到,所以修正系数接近 1。那么期望捕获时间约为 T_a / D。
举个例子:W = 20 ms,T_s = 200 ms,D = 10%,T_a = 100 ms。期望捕获时间 = 100 / 0.1 = 1000 ms。看起来有点长?但这是“期望值”,实际工程上我们会加一条规则:连续收到 2 次特征包才确认唤醒,用来防误触发。那么总延迟大约是 1 次捕获时间加上一个广播周期,即 1.1 s 左右。这个量级对于门锁、遥控这类场景完全够用。
如果你需要更快的唤醒,只有两条路:提高占空比(费电),或者缩短广播间隔(费发送方的电)。我一般建议先优化发送方:遥控器按一下才发广播,那发送方把广播间隔压到 30 ms、连发 10 轮,总共只发 300 ms,接收方依然可以保持 10% 的低占空比,唤醒延迟却能压到 300 ms 以内。这个技巧叫“突发式广播”,是我做遥控类产品时最常用的一招,把功耗压力从常年守夜的接收端,转移到偶尔工作的发送端,账算得过来。
2.3 电流预算表怎么搭才有说服力
功耗预算这张表,是判断方案能不能落地的唯一依据。我习惯按“分项累加 + 加权平均”的方式做,而不是只看芯片手册上的典型值。手册上的数字通常是某个特定条件下的最优值,跟你实际板子上的表现差一大截。
| 分项 | 典型值 | 说明 |
|---|---|---|
| BLE 芯片接收峰值电流 | 6~8 mA | 含射频和基带,不含 MCU 唤醒开销 |
| BLE 芯片深度休眠电流 | 1~3 µA | RTC 保持 + RAM 保持 |
| 协处理器 MCU 唤醒处理电流 | 1~2 mA(持续 < 2 ms) | 收包后的比对逻辑 |
| LDO 静态电流 | 1~5 µA | 选低静态电流型号,别用普通 1117 |
| 主控 suspend 电流 | 5~20 mA | 视 DDR 是否保持、外设是否断电 |
| 主控完全断电(若支持) | 0 | 需要硬件支持电源域切换 |
然后按占空比加权。以 W = 20 ms、T_s = 200 ms、D = 10%、接收峰值 7 mA 计算:
平均接收电流 = 7 mA × 0.10 = 0.70 mA 休眠与静态电流 = 3 µA + 3 µA = 6 µA 协处理器处理开销 ≈ 1.5 mA × 2 ms / 200 ms = 0.015 mA -------------------------------------------- 协处理器侧总计 ≈ 0.72 mA到这里很多人就停了,觉得 0.72 mA 挺好。但整机还要加上主控的 suspend 电流,这一项才是大头。假如主控 suspend 是 8 mA,那整机待机就是 8.72 mA,协处理器的省电努力只占了 8%。这就是为什么必须同步优化主控的休眠深度——该关的电源域关掉、该断的外设断掉、DDR 能自刷新就别保持、不用的 clock 全部 gate 掉。
反过来,如果主控能进到 200 µA 级的深度休眠,那整机就变成 0.92 mA,2000 mAh 电池理论待机 90 天,这才叫能打的产品。同样的传感器、同样的电池,两条路差了十倍,区别只在于有没有认真做电源域设计。
提示:做电池类产品时,把“整机待机电流”当作一个独立的验收指标写进测试计划,别只看协处理器那一颗芯片的电流。我踩过的坑是协处理器做到 700 µA 很漂亮,结果整机一测 15 mA,回头查了两周才发现是一颗电平转换芯片的使能脚悬空导致漏电。
3. 硬件与固件设计:协处理器常驻扫描的实现
确定了路线,接下来是具体怎么搭。这一部分我会按“硬件连接 → 广播包协议 → 固件关键代码”的顺序讲,因为协议设计会反过来约束硬件(比如你要不要留一根额外的状态线),顺序反了会返工。
3.1 硬件连接与电源域规划
最小系统需要这几根线:BLE 协处理器的 UART(TX/RX)接主控的调试串口,用于配置下发和状态查询;一根 GPIO 从协处理器指向主控,作为唤醒信号,这根线必须是主控支持的唤醒源引脚,通常要求落在特定的 wakeup GPIO bank 上,这一点一定要在原理图阶段就跟主控的引脚复用表核对;一根 GPIO 反向,从主控指向协处理器,作为“我已醒,请停止拉高”的握手信号,避免协处理器一直拉着导致主控反复触发中断。
电源域这块要单独说。理想结构是:BLE 协处理器挂在一个常开电源域(always-on domain),主控挂在可控电源域。主控断电时,协处理器靠电池直供或低静态 LDO 供电;主控上电后,协处理器通过 UART 接收配置。这里有个坑:如果协处理器和主控共用一颗 LDO,主控进深度休眠时把 LDO 关了,协处理器也一起断电,唤醒功能直接失效。所以要么分两颗 LDO,要么协处理器单独走一条常电路径。
另一个细节是电平匹配。主控 IO 常见是 3.3 V 或 1.8 V,BLE 芯片的 IO 可能是 1.8 V 或 3.0 V。别指望内部上拉能解决,跨压域一定要用正经的电平转换,而且选带输出使能脚(OE)的型号,主控休眠时把 OE 拉低,彻底切断反向漏电。我见过一块板子因为省了两毛钱的电平转换芯片,待机多出 400 µA,最后改板才解决。
天线部分给几个实操建议:如果协处理器和主控的 Wi-Fi 天线靠得近,务必拉开距离,至少 15 mm 以上,或者做定向隔离;PCB 天线下方禁止铺铜、禁止走线,净空区要留足;天线匹配网络留 π 型位置(两个电容一个电感),打样后实测回波损耗再定值,别照抄参考设计——板厚、介质、外壳材料一变,匹配就跑了。
3.2 广播包格式与匹配策略
广播包怎么设计,直接决定了误唤醒率和安全性。传统广播的 PDU 里,AdvA 占 6 字节,AdvData 最多 31 字节,这 31 字节必须精打细算。
我通常按这个结构分:
| 字段 | 长度 | 内容 |
|---|---|---|
| Flags | 3 字节 | 固定02 01 06,表示 LE General Discoverable、不支持 BR/EDR |
| Manufacturer Specific Data AD | 2 字节 | AD 类型0xFF+ 长度 |
| 厂商 ID(Company ID) | 2 字节 | 用一个内部约定的标识,别用别人的注册值 |
| Magic 字段 | 2 字节 | 固定魔数,第一道过滤,快速丢包 |
| 设备类型 / 动作码 | 1 字节 | 区分遥控、门锁、传感器上报 |
| 滚动码 | 4 字节 | 递增计数,防重放 |
| 校验 / 认证标签 | 4~8 字节 | HMAC 截断或简单 CRC,视安全等级 |
厂商 ID 加魔数这两层,是为了让协处理器在收包的第一时间就能用最廉价的比较逻辑丢掉 99% 的无关广播。这个优化很实在:如果每一包都要走完整解析,MCU 唤醒次数会翻好几倍,平均电流涨个几十微安很常见。我的做法是把魔数放在广播数据的固定偏移位置,固件里直接用硬件 pattern match(如果芯片支持)或者在中断回调里比较前 4 个字节,不匹配立即返回休眠,全程不超过 50 µs。
安全这块要提一句。纯广播唤醒是单向的、无握手的,任何一台能抓到包的设备都能原样重放,把你的门锁叫醒。所以滚动码几乎是必须的:发送端每次发一个递增的计数,接收端记录已见过的最大值,收到小于等于该值的一律丢弃。更严格一点,用双方预共享的密钥对“计数+设备ID”做 HMAC 截断,取前 4 字节放进广播包。代价是发送端要能做运算(手机 App 完全没问题),接收端要在唤醒后校验。
注意:如果使用扩展广播(Extended Advertising),载荷可以到 255 字节,塞完整签名都没问题,但代价是老设备兼容性和扫描端功耗上升。除非你的场景明确只用新芯片,否则我建议老老实实控制在 31 字节以内,用截断标签加滚动码的组合。
3.3 协处理器固件的关键逻辑
固件结构可以拆成四块:扫描参数初始化、广播包回调过滤、去抖与确认、唤醒脉冲输出。下面这段是核心逻辑的示意代码,具体 API 名字按各家 SDK 替换,但流程是通用的。
#define MAGIC_0 0xA5 #define MAGIC_1 0x5A #define WAKE_PULSE_MS 80 #define CONFIRM_CNT 2 static volatile uint32_t g_last_counter = 0; static volatile uint8_t g_hit_cnt = 0; /* 扫描参数:被动扫描,窗口20ms,间隔200ms,间隔带±10%抖动 */ static void scan_init(void) { ble_gap_scan_params_t p = { .passive = 1, /* 被动扫描,不发 SCAN_REQ */ .interval_ms = 200, .window_ms = 20, .filter_dup = 0, /* 关闭去重,否则重复包被协议栈吞掉 */ .jitter_pct = 10, }; ble_gap_scan_start(&p); } /* 广播回调:先做最廉价的魔数比对,再做完整校验 */ static void on_adv_report(const uint8_t *data, uint8_t len, int8_t rssi) { if (len < 12) return; /* 第 0~3 字节是 AD 结构与厂商 ID,直接跳过 */ if (data[4] != MAGIC_0 || data[5] != MAGIC_1) return; /* 信号太弱直接丢,防止远处干扰源反复触发 */ if (rssi < -85) return; uint8_t action = data[6]; uint32_t counter = (uint32_t)data[7] | ((uint32_t)data[8] << 8) | ((uint32_t)data[9] << 16) | ((uint32_t)data[10] << 24); /* 滚动码:必须严格大于历史值,防重放 */ if (counter <= g_last_counter) return; g_last_counter = counter; /* 连续命中确认,压制偶发误包 */ if (++g_hit_cnt < CONFIRM_CNT) return; g_hit_cnt = 0; wake_pulse_high(); /* 拉高唤醒脚 */ start_timer(WAKE_PULSE_MS, wake_pulse_low); }这段代码里有几个地方值得展开说。
filter_dup = 0这一行很关键。很多 BLE 协议栈默认开启重复包过滤,同一个 AdvA 加同样的数据在短时间内会被协议栈自己丢掉。但我们的场景里,发送方是连续发重复内容的(只是滚动码在变),如果协议栈把包吞了,回调根本进不来,表现就是“偶尔能唤醒、大部分时候叫不醒”。这个坑我在两个项目上各踩了一次,第二次才记住。
rssi < -85这个阈值也要按场景调。近距离遥控场景可以卡到 -75,能把误触发的概率压得很低;但如果你希望“人在客厅就能唤醒卧室的设备”,就得放宽到 -90。这里没有标准答案,建议做一张 RSSI 与唤醒距离的实测表,用数据定阈值,别凭感觉。
唤醒脉冲的宽度WAKE_PULSE_MS同样不能随便给。太短了主控的 GPIO 中断来不及响应,尤其是在主控刚上电、电源还没稳的阶段;太长了如果主控没醒成功,脉冲会一直拉着,某些主控的唤醒脚是电平触发的话会反复进中断。我的经验值是 80~150 ms,并且配上“主控醒来后拉高握手线,协处理器看到握手线就立刻释放”的双向机制,这条反馈线能省掉大量莫名其妙的重复唤醒。
4. 主机侧落地:从 GPIO 中断到系统真正跑起来
协处理器把引脚拉高了,事情只完成了一半。真正难的是主控这一侧——从 suspend 状态被 GPIO 拉起来,到业务逻辑真正跑起来,中间有一堆坑等着。
4.1 设备树与唤醒源配置
主控的唤醒脚必须在设备树里正确声明,否则内核根本不把这个 GPIO 当成唤醒源,只会当普通中断处理,系统永远不会退出 suspend。设备树里关键的是wakeup-source这个属性,以及把 GPIO 节点挂到正确的 keys 或中断控制器下面。
/ { ble_wakeup: ble-wakeup { compatible = "gpio-keys"; pinctrl-names = "default"; pinctrl-0 = <&ble_wake_pin>; wake-key { label = "ble-wakeup"; gpios = <&gpio1 12 GPIO_ACTIVE_HIGH>; linux,code = <KEY_WAKEUP>; gpio-key,wakeup; /* 老内核写法,新内核用 wakeup-source */ wakeup-source; debounce-interval = <50>; }; }; };debounce-interval建议设 20~50 ms。广播包是突发式的,如果不去抖,一个包可能触发好几次中断,虽然不影响功能,但会让唤醒计数统计变得很难看,排查问题时容易被误导。
配置完还要在用户态确认这个唤醒源被正确注册:
# 查看已注册的唤醒源 cat /sys/kernel/debug/wakeup_sources | grep -i ble # 查看某个 GPIO 是否可唤醒 cat /sys/class/gpio/gpio44/power/wakeup # 手动测试进入 suspend echo mem > /sys/power/state # 查看唤醒原因 cat /sys/power/wakeup_count dmesg | grep -i "wakeup\|resume"suspend的深度选项也要留意。echo mem进的是 suspend-to-RAM,主控大部分模块断电,DDR 自刷新,这个模式唤醒快、电流低,是首选。有些平台支持freeze(suspend-to-idle),电流降得不多但唤醒极快,适合对延迟敏感的场景。具体支持哪几种,看/sys/power/state输出的内容。
提示:第一次调唤醒,务必先做“不接协处理器、手动用镊子短接唤醒脚到地/高”的裸测。这一步能把“是硬件没接对”还是“是软件没配好”彻底分开。我见过太多人一上来就整机联调,结果在软件配置和硬件虚焊之间来回怀疑,浪费一周。
4.2 唤醒后的上下文恢复与业务处理
系统从 suspend 醒来,不等于业务已经就绪。这里有个时序问题:GPIO 中断触发 → 内核 resume 流程 → 驱动恢复 → 用户态进程被调度 → 业务逻辑读到协处理器通过 UART 发来的数据。这一串走完,快的几十毫秒,慢的能到 500 ms 以上,取决于你的系统里挂了多少驱动、有没有做驱动异步恢复。
我的一般做法是把“醒来后第一件事”做成一个极简的 systemd 服务或守护进程,优先级拉满,只负责读 UART、解析动作码、然后转交给真正干活的业务进程。这样即便业务进程启动慢,用户也能在 100 ms 内看到“设备有反应了”(比如指示灯亮起),体验上就不会觉得迟钝。
[Unit] Description=BLE wakeup handler After=multi-user.target Wants=multi-user.target [Service] Type=simple ExecStart=/usr/bin/ble_wake_handler Restart=always RestartSec=0.2 Nice=-10 IOSchedulingClass=realtime IOSchedulingPriority=0 [Install] WantedBy=multi-user.targetNice=-10和 realtime 调度是为了保证这个进程在系统刚醒来、各种服务抢 CPU 的时候能优先跑起来。这一条在低端芯片上效果特别明显,不设的话经常出现“明明 100 ms 就唤醒了,业务却要 1 秒才响应”。
还有一个反向问题:唤醒后什么时候再睡回去。如果业务处理完不主动回 suspend,设备就一直醒着,电池照样掉得快。我的做法是设定一个“唤醒保持窗口”,比如 30 s,窗口内如果有持续交互就续期,没有就自动echo mem > /sys/power/state。这个超时值要跟产品形态对齐:门锁可以 10 s,遥控类可以 5 s,需要传大数据的场景可以放宽到 60 s。
最后说一个很容易被忽略的点:唤醒计数与日志。在产品里留一个计数器,记录“收到多少广播包、命中多少次、成功唤醒多少次、主控响应了多少次”,这几个数字在排查问题时的价值极高。用户反馈“设备有时候不灵”,你让他看这几个数就能判断是射频没收到、还是匹配逻辑太严、还是主控醒了但业务挂了。没有这些数据,只能靠猜。
5. 实测与调优:数据说话
方案跑通只是及格,能不能交付看的是实测数据。这一节讲测试方法和几组我实际调出来的对比数据,供你参考坐标系。
5.1 功耗测试的正确姿势
测微安级电流,万用表是不行的,量程和采样率都不够。我的标准配置是一台源表(SMU)或者高精度电流分析仪,配合上位机做长时间记录。
测法上要分三段:
第一段测协处理器单独的电流。断开主控电源,只给 BLE 芯片和 MCU 供电,让扫描持续跑 10 分钟,看平均电流和峰值。这一段用来验证扫描参数和休眠策略是否正确,能不能做到理论值。如果实测比理论高 50% 以上,通常是这三个原因:LDO 静态电流太大、协议栈没真正进深度休眠、天线匹配不好导致重传(广播场景没有重传,但会有额外的唤醒次数)。
第二段测主控 suspend的电流。断开协处理器,主控进 suspend 后稳定下来测 5 分钟。这一段能暴露电源域设计问题,常见的是某个外设没关、DDR 没进自刷新、某些 clock 仍然在跑。
第三段才是整机待机。两段加起来,再对比实测,差值就是漏电流。我做过的一个项目,两段相加理论 0.9 mA,整机实测 1.6 mA,差了 700 µA,最后定位到是一颗 USB 开关芯片的使能脚浮空,跟着环境电平漂。这种问题只能靠分段测量才能揪出来。
# 用脚本记录长时间待机电流(假设设备已通过串口暴露采样接口) #!/bin/bash LOG=/var/log/power_$(date +%Y%m%d_%H%M).csv echo "timestamp,current_uA" > $LOG while true; do ts=$(date +%s) cur=$(cat /sys/class/power_supply/battery/current_now 2>/dev/null || echo -1) echo "$ts,$cur" >> $LOG sleep 5 done这个脚本只是样例,实际产品里更靠谱的做法是在硬件上串一颗采样电阻,配合数据记录仪做长时间采集,避免软件采样和实际电流之间有偏差。
5.2 唤醒率与误唤醒的统计方法
唤醒率这个指标,要拆成两层看:物理层收包率和应用层唤醒成功率。
物理层收包率是协处理器收到有效包的次数除以发送方发出的次数。测试方法很简单:发送端以固定间隔连续发 1000 包,接收端计数。好的设计在 3 米视距内应该做到 98% 以上。低于 90% 就要查天线、查干扰、查扫描窗口是否够长。
应用层唤醒成功率是主控真正醒来的次数除以协处理器拉高唤醒脚的次数。这个应该接近 100%,如果只有 80%,问题出在主机侧——量产上最常见的是 GPIO 唤醒配置在某些板子上因为 pinctrl 冲突失效,或者 suspend 没真正进去(设备其实一直醒着,当然一叫就醒,但这会把功耗指标掩盖掉,属于假象)。
误唤醒率同样重要。把设备放在一个没有目标发送源的环境里跑 24 小时,记下唤醒次数。理想值是 0。如果一天几十次,说明魔数太短或者 RSSI 阈值太宽;如果一天几百次,那可能是附近的 Wi-Fi 或其他 BLE 设备触发的,得回头检查匹配逻辑有没有真正生效。
| 测试项 | 测试条件 | 合格标准(我的经验值) |
|---|---|---|
| 收包率 | 3 m 视距,1000 包 | ≥ 98% |
| 唤醒成功率 | 协处理器拉高 500 次 | ≥ 99% |
| 唤醒延迟 | 10% 占空比,100 ms 广播 | ≤ 1.2 s(P95) |
| 误唤醒 | 无目标源环境 24 h | 0 次 |
| 整机待机 | 常温,电池供电 | 按产品定义,通常 ≤ 1 mA |
| 长期稳定性 | 连续 72 h 反复唤醒 | 无死机、无漏唤醒 |
这张表里最容易被砍掉的是“长期稳定性”。我的建议是无论如何都要做 72 小时循环测试,因为短期测试永远发现不了内存泄漏、计数器溢出、状态机卡死这类问题。我在一个项目上就是因为省了这步,量产前一周才发现协处理器固件里的一个计数器在 4 万次后回绕,导致滚动码校验失效,所有设备在连续使用 11 天后无法唤醒。
6. 踩坑记录与排查速查表
最后这部分是我认为最有价值的内容,因为书上都不会写。下面这些坑,基本都是花时间最多、看似最玄学、解决后又特别朴素的问题。
6.1 七个高频故障的定位思路
第一个坑:设备在面前叫不醒,走远了反而醒。前面提过,这是扫描间隔和广播间隔相位锁死导致的。解决方法是给扫描间隔加抖动,让两者形成拍频。还有一个变种情况是发送方用了固定的广播信道顺序,而接收方的信道轮换策略刚好错开,表现一样。我一般直接把扫描窗口拉到 30 ms 验证,如果好了,就是窗口太短的问题。
第二个坑:唤醒后系统卡死或重启。大概率是 resume 时序问题。某个驱动的 resume 回调里访问了还没恢复的外设,或者供电还没稳就初始化了。定位办法是在 resume 路径上加打点,看卡在哪一步。常见的补救措施是把可疑驱动改成异步恢复,或者给电源加一个 20 ms 的稳定延时。
第三个坑:待机电流比理论值高一个数量级。先排除测量误差(万用表测微安就是白测),然后分段测。九成的情况是某个外设的使能脚状态不确定,或者是调试串口的 TX 一直输出数据把某个模块唤醒了。把串口日志关掉再测一次,往往立竿见影。
第四个坑:唤醒功能在实验室好用,客户现场失灵。现场有大量 Wi-Fi 和蓝牙设备,2.4G 频段非常拥挤。这时候要检查的是抗干扰能力:RSSI 阈值是不是太宽(导致被干扰包触发后又被过滤逻辑丢弃,看起来像没收到)、天线有没有被金属外壳挡住、扫描窗口是否被干扰占满。解决方案是提高确认次数、收紧 RSSI、必要时在广播包里加更长的魔数。
第五个坑:唤醒偶尔成功偶尔失败,没有规律。这种最头疼。我遇到过的原因是协处理器的 RAM 保持区域在深度休眠时被写坏了,导致计数器变量异常,滚动码校验随机失败。解决方法是把关键状态放到备份寄存器或者带校验的存储区,每次唤醒先校验再使用。
第六个坑:换了批次的主控板,唤醒就不好使了。很可能是 GPIO 的默认上下拉配置不同,或者不同批次的 PCB 上唤醒脚走线长度变了引起干扰。这类问题只能靠出厂测试覆盖,把唤醒功能纳入每块板的产测项,别指望一致性。
第七个坑:低电量时唤醒失效。电池电压掉到 3.0 V 以下,LDO 输出可能不稳,或者 BLE 芯片的射频性能下降。要在低电量条件下专门测一遍接收灵敏度,必要时在固件里做电压监测,低电时降低扫描占空比保功能。
6.2 排查速查表
| 现象 | 优先怀疑 | 快速验证方法 | 处理方向 |
|---|---|---|---|
| 完全唤不醒 | 唤醒脚未配置为唤醒源 | cat /sys/kernel/debug/wakeup_sources | 补wakeup-source属性 |
| 偶尔唤不醒 | 扫描间隔与广播间隔锁相 | 改扫描间隔加抖动后复测 | 引入 ±10% 随机抖动 |
| 唤醒后立刻又睡 | 业务未续期保持窗口 | 观察唤醒计数与日志时间戳 | 调整保持窗口时长 |
| 误唤醒频繁 | 魔数过短或 RSSI 太宽 | 屏蔽目标源跑 24 h 计数 | 收紧匹配条件 |
| 待机电流偏高 | 外设未断电或电平漏电 | 分段测量定位 | 断电源域、加电平转换 |
| 唤醒延迟大 | 占空比太低或广播间隔太长 | 提高占空比对比延迟 | 改用突发式广播 |
| 唤醒后系统异常 | resume 时序或驱动冲突 | resume 路径打点 | 异步恢复或加延时 |
| 量产一致性差 | GPIO 配置或天线差异 | 产测全检唤醒功能 | 纳入产测项 |
写到这里,我个人在实操中体会最深的一点是:BLE 唤醒方案的成败,八成不在射频和协议,而在电源域和时序。射频部分只要天线不犯低级错误、参数按公式算,基本都能跑通;真正让人返工的是“休眠时某根线漏电”“resume 时某个驱动抢跑”“协处理器和主控的唤醒握手没设计”。所以我现在做这类项目,原理图评审阶段就会专门拉一页纸,把每一根跨电源域的线、每一个唤醒源、每一次状态切换都画出来逐个过,比后面调两周有用得多。
另外再分享一个小技巧:在协处理器固件里留一个“静默模式”,通过 UART 下发命令让它暂停唤醒输出、但仍然计数。产线测试或者固件升级时开这个模式,能避免升级过程中设备被不断唤醒导致升级失败,这个小开关在量产阶段帮我省了很多事。