一次“每次唤醒都要重连”的困境
某款电池供电的无线传感器,用 BLE 与网关通信,每 5 分钟唤醒一次,采集数据并上报,其余时间深度休眠。电池要求撑 1 年。
样机做出来,功耗比预期高 3 倍。用示波器抓电流波形,发现每次唤醒后,系统有大约2 秒的时间维持在高电流状态(约 8mA)。这 2 秒就是 BLE 建立连接的时间。
算一笔账:每次唤醒 2 秒 × 8mA = 16mAs,折算到 5 分钟周期(300 秒)的平均电流是 16/300 ≈ 53μA。加上休眠电流 2μA、采集电流 20μA,总平均电流约 75μA。电池 2000mAh,理论续航约 3 年。但实测平均电流 220μA,续航不到 1 年。
问题出在:每次唤醒都要重新建立 BLE 连接。BLE 的连接建立需要广播、扫描、连接、配对、服务发现等一系列流程,耗时 2 秒以上。如果每次上报都走完整流程,功耗必然高。
解决方案:用 BLE 的白名单 + 快速重连机制。设备记住网关的地址,唤醒后直接向网关发起连接(不需要广播和扫描),连接建立时间从 2 秒缩短到 200ms 以内。功耗降低 10 倍。
这个案例的核心问题是:低功耗场景下的通信,不能只考虑“数据传输”,还要考虑“连接建立”的开销。
这篇文章要讲的,就是低功耗通信的设计方法。核心是四个问题:通信状态怎么保持、协议怎么容忍时钟漂移、重传和超时怎么调、电源怎么在通信和休眠之间切换。
一、通信状态的保持策略
三种策略
低功耗设备的通信状态管理,有三种策略:
策略一:无状态(每次重连)。设备每次唤醒后,从零开始建立连接,传输数据,然后断开。优点是简单,不需要维护连接状态。缺点是每次都要走完整的连接建立流程,功耗高。
策略二:有状态(保持连接)。设备保持连接不断,即使休眠时也维持链路层连接。优点是唤醒后可以直接传输数据,延迟低。缺点是维持连接需要定期发送心跳包,休眠期间也有功耗。
策略三:半状态(快速重连)。设备在休眠前保存连接信息(对端地址、加密密钥、会话参数),唤醒后直接用保存的信息快速重连,跳过广播、扫描等耗时步骤。这是低功耗场景的推荐方案。
三种策略的对比
选择原则:
通信间隔 > 1 小时 → 无状态
通信间隔 < 10 秒 → 有状态
通信间隔 10 秒到 1 小时 → 半状态
半状态的实现要点
半状态的核心是保存连接上下文。以 BLE 为例,需要保存的信息包括:
对端设备地址(MAC)
连接参数(连接间隔、从机延迟、超时时间)
加密密钥(LTK、IRK)
服务发现结果(属性句柄)
这些信息在设备休眠前保存到非易失存储(Flash 或 EEPROM)。唤醒后读取,直接发起连接。
注意:保存连接上下文有安全风险。如果设备被物理攻击,密钥可能泄露。对于安全要求高的场景,密钥应该存在芯片的安全存储区(比如 STM32 的 OTP 区域或安全元件)。
二、协议设计:容忍时钟漂移
低功耗设备的时钟问题
低功耗设备在休眠期间,通常用 32.768kHz 晶振或内部 RC 维持 RTC。这两种时钟源的精度差异很大:
外部 32.768kHz 晶振:精度 ±20ppm,一天漂移约 1.7 秒
内部 RC:精度 ±5%,一天漂移约 72 分钟
时钟漂移对通信协议的影响:
如果设备每 1 小时唤醒一次,用内部 RC 作为时钟源,唤醒时间可能偏差 ±3 分钟。如果网关的接收窗口只有 10 秒,设备唤醒时网关已经关闭接收窗口,通信失败。
协议容忍时钟漂移的方法
方法一:扩大接收窗口。
网关的接收窗口要覆盖设备的时钟漂移范围。比如设备时钟精度 ±5%,唤醒周期 1 小时,漂移 ±3 分钟。网关的接收窗口至少要 6 分钟。
代价:网关的接收窗口越大,网关的平均功耗越高(如果网关也是电池供电)。而且窗口越大,多个设备同时上报时的冲突概率越高。
方法二:时间同步。
设备定期与网关同步时间,校准本地时钟。比如每 10 次唤醒,做一次时间同步。
同步流程:
设备唤醒,向网关发送时间同步请求
网关返回当前时间
设备计算时钟偏差,调整 RTC 的校准寄存器
设备继续休眠
代价:增加了一次通信开销。但相比扩大接收窗口,时间同步更高效。
方法三:事件驱动。
设备不按固定周期唤醒,而是等待事件触发。比如传感器检测到温度超过阈值,才唤醒上报。
优势:完全避免了时钟漂移问题。
代价:无法做周期性的数据上报。适用于事件驱动的场景。
一个实用的组合方案
对于周期上报 + 事件触发的场景,推荐组合方案:
周期上报:用外部 32.768kHz 晶振,精度 ±20ppm,漂移很小,网关接收窗口可以设得比较窄(比如 1 秒)
事件触发:立即唤醒上报,不需要等待周期
时间同步:每 100 次唤醒做一次时间同步,校准累积漂移
三、重传和超时策略
低功耗通信的重传代价
在低功耗场景下,重传的代价比有线通信高得多。一次重传意味着:
发射机再次上电(功耗)
等待对端响应(时间)
如果重传失败,可能进入更长的退避(更多功耗)
因此,低功耗通信的重传策略要更保守:
减少重传次数(比如最多 2 次,而不是 5 次)
增大重传间隔(比如 1 秒,而不是 100ms)
重传失败后进入“失败模式”(降低上报频率,或者等待下一次周期)
超时时间的设定
超时时间不能设得太短,否则会因为对端响应慢而误判失败。也不能设得太长,否则失败时浪费太多时间和功耗。
超时时间的经验公式:
超时时间 = 对端处理时间 + 传输时间 + 时钟漂移余量 + 安全余量
比如:
对端处理时间:50ms
传输时间:100ms(取决于数据量和速率)
时钟漂移余量:10ms
安全余量:50%
超时时间 = (50 + 100 + 10) × 1.5 ≈ 240ms
实际项目中,超时时间通常设为理论值的 2-3 倍。比如上面的例子,超时设为 500ms。
重传策略的代码示例
typedef struct { uint32_t timeout_ms; uint8_t max_retries; uint32_t retry_interval_ms; } CommConfig_t; static const CommConfig_t comm_config = { .timeout_ms = 500, .max_retries = 2, .retry_interval_ms = 1000, }; int Comm_SendWithRetry(const uint8_t *data, uint32_t len) { for (int retry = 0; retry <= comm_config.max_retries; retry++) { // 发送数据 int ret = Comm_Send(data, len); if (ret != 0) { continue; // 发送失败,重试 } // 等待响应 ret = Comm_WaitResponse(comm_config.timeout_ms); if (ret == 0) { return 0; // 成功 } // 重试前等待 if (retry < comm_config.max_retries) { vTaskDelay(pdMS_TO_TICKS(comm_config.retry_interval_ms)); } } return -1; // 所有重试都失败 }失败后的处理
重传全部失败后,不能无限重试。应该进入“失败模式”:
记录失败事件到日志
降低上报频率(比如从 5 分钟改为 30 分钟)
等待下一次周期唤醒时再试
如果连续多次失败,触发告警(比如点亮 LED 或发送特殊消息)
四、电源管理:通信和休眠的切换
通信期间的电源状态
通信期间,系统需要:
射频模块上电(BLE、NB-IoT、LoRa)
MCU 运行在较高频率(处理协议栈)
外设时钟使能(UART、SPI)
这些都会显著增加功耗。以 BLE 为例,发射电流约 8mA,接收电流约 6mA,MCU 运行电流约 3mA,合计约 17mA。
优化方向:
缩短通信时间。用快速重连、减少数据量、提高速率。
降低发射功率。如果通信距离近,可以降低发射功率。比如 BLE 的发射功率从 0dBm 降到 -20dBm,电流从 8mA 降到 4mA。
通信期间不用高频。如果协议栈允许,MCU 可以运行在较低频率(比如 16MHz 而不是 64MHz),降低功耗。
通信结束后的电源切换
通信结束后,要尽快进入低功耗模式。切换流程:
关闭射频模块
保存通信状态(连接上下文)
关闭不需要的外设时钟
配置唤醒源
进入低功耗模式
注意:关闭外设时钟的顺序很重要。如果先关闭了某个外设的时钟,但该外设还有未完成的操作(比如 DMA 传输),可能导致数据丢失或硬件错误。
void Comm_EnterLowPower(void) { // 1. 等待所有通信完成 while (Comm_IsBusy()) { vTaskDelay(pdMS_TO_TICKS(10)); } // 2. 关闭射频模块 Radio_Disable(); // 3. 保存连接上下文 Comm_SaveContext(); // 4. 关闭外设时钟 __HAL_RCC_USART1_CLK_DISABLE(); __HAL_RCC_SPI1_CLK_DISABLE(); // 5. 配置唤醒源 Wakeup_Configure(); // 6. 进入低功耗(由 Tickless 机制自动触发) }通信期间的看门狗策略
通信期间,如果看门狗超时,系统会复位。但通信可能正在进行中,复位会导致数据丢失。
解决方案:
在通信期间,临时延长看门狗超时时间
或者在通信的关键步骤之间喂狗
或者用独立的看门狗监控通信任务,通信任务卡死时触发任务级恢复而不是系统复位
五、一个完整的低功耗通信框架
把前面所有内容整合起来:
// low_power_comm.h #ifndef LOW_POWER_COMM_H #define LOW_POWER_COMM_H #include <stdint.h> #include <stdbool.h> typedef enum { COMM_STATE_IDLE, COMM_STATE_CONNECTING, COMM_STATE_CONNECTED, COMM_STATE_SENDING, COMM_STATE_WAITING_RESP, COMM_STATE_ERROR, } CommState_t; int Comm_Init(void); int Comm_Wakeup(void); int Comm_SendData(const uint8_t *data, uint32_t len); int Comm_EnterSleep(void); CommState_t Comm_GetState(void); #endif// low_power_comm.c #include "low_power_comm.h" #include "wakeup.h" #include "log.h" static CommState_t g_state = COMM_STATE_IDLE; static CommContext_t g_context; // 连接上下文 // 通信任务 void vCommTask(void *pvParameters) { for (;;) { // 等待通信请求 ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 1. 快速重连 g_state = COMM_STATE_CONNECTING; int ret = Comm_FastReconnect(&g_context); if (ret != 0) { Log_Event(LOG_EVT_COMM_CONNECT_FAIL, LOG_LEVEL_ERROR, 0, 0, 0); g_state = COMM_STATE_ERROR; continue; } g_state = COMM_STATE_CONNECTED; // 2. 发送数据(带重传) g_state = COMM_STATE_SENDING; ret = Comm_SendWithRetry(g_pending_data, g_pending_len); if (ret != 0) { Log_Event(LOG_EVT_COMM_SEND_FAIL, LOG_LEVEL_ERROR, 0, 0, 0); g_state = COMM_STATE_ERROR; continue; } // 3. 等待确认 g_state = COMM_STATE_WAITING_RESP; ret = Comm_WaitAck(500); if (ret != 0) { Log_Event(LOG_EVT_COMM_ACK_TIMEOUT, LOG_LEVEL_WARN, 0, 0, 0); } // 4. 进入低功耗 g_state = COMM_STATE_IDLE; Comm_EnterSleep(); } } // 唤醒后的通信流程 int Comm_Wakeup(void) { // 1. 恢复外设 UART_Init(); SPI_Init(); Radio_Init(); // 2. 加载连接上下文 Comm_LoadContext(&g_context); // 3. 通知通信任务 xTaskNotifyGive(xCommTaskHandle); return 0; }// 主任务:周期唤醒 void vMainTask(void *pvParameters) { // 初始化 Comm_Init(); Wakeup_Init(); for (;;) { // 等待唤醒事件 WakeupSource_t src = Wakeup_WaitEvent(); // 根据唤醒源处理 switch (src) { case WAKEUP_SRC_RTC_ALARM: // 周期上报 CollectAndSendData(); break; case WAKEUP_SRC_KEY: // 按键触发上报 CollectAndSendData(); break; default: break; } // 进入低功耗 Comm_EnterSleep(); } }六、常见误区与踩坑记录
误区一:每次唤醒都走完整连接流程
这是最常见的错误。BLE 的广播、扫描、连接、配对、服务发现,走完要 2 秒以上。如果每次上报都走一遍,功耗必然高。
正确做法:用白名单和快速重连,保存连接上下文,唤醒后直接连接。
误区二:超时时间设得太短
低功耗设备的通信环境往往比较恶劣(信号弱、干扰大),对端响应可能比较慢。超时时间设得太短,会导致频繁误判失败,触发不必要的重传。
正确做法:超时时间设为理论值的 2-3 倍。实测调整。
误区三:重传次数太多
重传次数太多,会导致失败时消耗大量功耗。在低功耗场景下,“快速失败,下次再试”比“反复重试”更省电。
正确做法:最多重传 2 次。失败后进入失败模式,降低上报频率。
误区四:通信期间不喂看门狗
通信期间如果看门狗超时,系统复位,正在传输的数据丢失。但如果通信期间频繁喂狗,又可能掩盖通信任务卡死的问题。
正确做法:用任务级看门狗监控通信任务。通信任务卡死时,触发任务级恢复(比如重启通信模块),而不是系统复位。
误区五:忽略时钟漂移
用内部 RC 作为 RTC 时钟源,漂移可能达到 ±5%。如果网关的接收窗口不够大,设备唤醒时网关已经关闭接收窗口,通信失败。
正确做法:时间敏感的场景用外部 32.768kHz 晶振。如果必须用内部 RC,要扩大网关的接收窗口,或者定期做时间同步。
七、本篇小结
低功耗通信的核心是在最短时间内完成通信,然后尽快回到休眠。
第一,通信状态管理用半状态策略。保存连接上下文,唤醒后快速重连,跳过广播、扫描等耗时步骤。通信频率很低时用无状态,很高时用有状态。
第二,协议要容忍时钟漂移。用外部 32.768kHz 晶振降低漂移,定期做时间同步校准累积误差,网关接收窗口要覆盖设备的时钟漂移范围。
第三,重传和超时要保守。超时时间设为理论值的 2-3 倍,重传次数最多 2 次,失败后进入失败模式而不是无限重试。
第四,电源切换要平滑。通信结束后,等待所有操作完成,关闭射频模块,保存连接上下文,关闭外设时钟,配置唤醒源,进入低功耗。
第五,看门狗策略要适配通信。通信期间用任务级看门狗,不要用系统复位来“解决”通信卡死。
模块二到此结束。我们讲了功耗预算、Tickless 模式、唤醒源管理、低功耗通信。这四个主题,构成了低功耗设计的完整链路。
下一篇开始模块三:Bootloader 与固件升级。第一篇讲最小可用 Bootloader 设计——跳转机制、中断向量表重映射、应用校验。这是量产设备的必修课,也是很多工程师的盲区。
思考题
你的低功耗设备,唤醒后建立通信连接需要多长时间?有没有优化空间?
如果你的设备用内部 RC 作为 RTC 时钟源,时钟漂移对通信有什么影响?
通信失败后,你的系统是怎么处理的?会不会无限重试?
欢迎在评论区留下你的答案。下一篇见。
💖 点赞 + 收藏 + 关注
如果这篇文章让你对低功耗通信有了系统的认识,点赞让更多同行看到,收藏方便随时查阅,关注不错过下一篇《最小可用 Bootloader 设计》。