嵌入式驱动开发:量产级工程化实战(第9篇)——低功耗下的通信可靠性

一次"每次唤醒都要重连"的困境

某款电池供电的无线传感器,用 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 次唤醒,做一次时间同步。

同步流程:

  1. 设备唤醒,向网关发送时间同步请求

  2. 网关返回当前时间

  3. 设备计算时钟偏差,调整 RTC 的校准寄存器

  4. 设备继续休眠

代价: 增加了一次通信开销。但相比扩大接收窗口,时间同步更高效。

方法三:事件驱动。

设备不按固定周期唤醒,而是等待事件触发。比如传感器检测到温度超过阈值,才唤醒上报。

优势: 完全避免了时钟漂移问题。

代价: 无法做周期性的数据上报。适用于事件驱动的场景。

一个实用的组合方案

对于周期上报 + 事件触发的场景,推荐组合方案:

  • 周期上报: 用外部 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),降低功耗。

通信结束后的电源切换

通信结束后,要尽快进入低功耗模式。切换流程:

  1. 关闭射频模块

  2. 保存通信状态(连接上下文)

  3. 关闭不需要的外设时钟

  4. 配置唤醒源

  5. 进入低功耗模式

注意: 关闭外设时钟的顺序很重要。如果先关闭了某个外设的时钟,但该外设还有未完成的操作(比如 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 设计------跳转机制、中断向量表重映射、应用校验。这是量产设备的必修课,也是很多工程师的盲区。

思考题

  1. 你的低功耗设备,唤醒后建立通信连接需要多长时间?有没有优化空间?

  2. 如果你的设备用内部 RC 作为 RTC 时钟源,时钟漂移对通信有什么影响?

  3. 通信失败后,你的系统是怎么处理的?会不会无限重试?

欢迎在评论区留下你的答案。下一篇见。


💖 点赞 + 收藏 + 关注

如果这篇文章让你对低功耗通信有了系统的认识,点赞让更多同行看到,收藏方便随时查阅,关注不错过下一篇《最小可用 Bootloader 设计》。

相关推荐
创界工坊工作室2 小时前
24-基于单片机的仓鼠养殖环境监控系统设计
stm32·单片机·嵌入式硬件·proteus
学逆向的2 小时前
C 突破编译器限制
c语言·开发语言·网络安全
碧海银沙音频科技研究院2 小时前
基于炬芯ATS录音卡BLE控制协议
人工智能·嵌入式硬件·算法·语音识别
comyccccc3 小时前
PCB走线设计秘籍:从载流到3D预览全解析学习笔记
单片机·嵌入式硬件
俊基科技3 小时前
A-59 工业级 AI 语音处理模组:100 dB 消回音 + 神经网络降噪的工程实践
嵌入式硬件·神经网络·硬件开发·语音模块·回音消除·拾音降噪
endeavor123 小时前
can总线(五)
c语言·stm32·单片机·can
威嵌神州3 小时前
10.1 寸集成加密专网加固平板|外场加密通信硬件基线选型评估
人工智能·嵌入式硬件·平板
深圳市尚想信息技术有限公司4 小时前
TM52F1376海速芯HITENX Flash-8位MCU单片机,多通道ADC + 段码显示一体化主控芯片
单片机·嵌入式硬件·mcu·flask·芯片·控制器
威嵌神州4 小时前
国产化便携式加固终端选型|加固平板、手持终端产品谱系解析
嵌入式硬件·平板