3、深度睡眠(Deep-sleep)实战:进入Deep-sleep的API调用,唤醒源配置(定时器、外部GPIO、触摸传感器),RTC内存保留策略。

好,咱们直接进入正题。深度睡眠,说白了就是让ESP32进入一种「假死」状态——CPU停了,大部分外设也关了,只有RTC模块还在工作。这时候电流能降到个位数微安级别。嗯,这才是低功耗设计的核心战场。

我个人习惯把Deep-sleep比作「冬眠」。熊冬眠的时候心跳降到每分钟几次,ESP32 Deep-sleep的时候电流也就几微安。但冬眠的熊随时能被叫醒,对吧?ESP32也一样,得给它配好「闹钟」和「门铃」。

3.1 进入Deep-sleep的API调用

进入深度睡眠其实就一个函数的事。但别小看它,调用之前你得想清楚:醒来之后程序从哪开始跑?

#include <esp_sleep.h>

void setup() {
    Serial.begin(115200);
    delay(1000);
    
    Serial.println("准备进入深度睡眠...");
    
    // 设置唤醒源(这里先配一个定时器,10秒后唤醒)
    esp_sleep_enable_timer_wakeup(10 * 1000000); // 单位是微秒
    
    // 真正进入睡眠
    esp_deep_sleep_start();
}

void loop() {
    // 永远不会执行到这里
}

注意了,esp_deep_sleep_start() 调用之后,程序就停了。醒来时,ESP32会从头执行 setup(),就像按了复位键一样。我在项目中遇到过有人把初始化代码写在 loop() 里,结果深度睡眠醒来后直接跑飞——嗯,这是个经典坑。

⚠️ 重要提醒: 进入Deep-sleep前,记得把不需要的外设关掉。比如Wi-Fi、蓝牙、ADC等。虽然深度睡眠会自动关大部分外设,但有些RTC外设(比如RTC GPIO)还开着,会额外耗电。

3.2 唤醒源配置

ESP32支持三种主要的唤醒源:定时器、外部GPIO、触摸传感器。你想想看,这正好对应了三种典型场景:定时上报、按键唤醒、触摸交互。

3.2.1 定时器唤醒

这个最简单,也最常用。比如你的传感器每隔一小时上报一次数据,用定时器唤醒就对了。

// 设置定时器唤醒,单位微秒
// 1小时 = 3600秒 = 3,600,000,000 微秒
esp_sleep_enable_timer_wakeup(3600ULL * 1000000);

这里有个细节:参数是 uint64_t 类型,记得加 ULL 后缀,否则数值溢出你会怀疑人生。我曾经在这个坑里爬了半小时——定时器设了1小时,结果5分钟就醒了,查了半天才发现是类型转换的问题。

3.2.2 外部GPIO唤醒

外部唤醒分两种:电平唤醒和边沿唤醒。我个人更常用边沿唤醒,因为电平唤醒容易受干扰。

// 配置GPIO0为下降沿唤醒
esp_sleep_enable_ext0_wakeup(GPIO_NUM_0, 0); // 第二个参数:0=低电平/下降沿,1=高电平/上升沿

// 或者用ext1,支持多个GPIO
const uint64_t gpio_pin_mask = (1ULL << GPIO_NUM_0) | (1ULL << GPIO_NUM_2);
esp_sleep_enable_ext1_wakeup(gpio_pin_mask, ESP_EXT1_WAKEUP_ANY_HIGH);
💡 实战技巧: 用ext1唤醒时,可以配置成「任意一个引脚变化就唤醒」。我做过一个门磁传感器项目,用两个GPIO分别检测开门和关门,醒来后读一下引脚状态就知道是开门还是关门了。省电又高效。

为什么会有两种?ext0 只能配一个引脚,但响应快;ext1 可以配多个引脚,但醒来后需要软件判断是哪个引脚触发的。看场景选吧。

3.2.3 触摸传感器唤醒

这个功能挺有意思。ESP32有10个触摸传感器引脚,可以在深度睡眠下检测触摸。我做过一个智能灯控面板,手指一碰就亮,用的就是这个功能。

// 配置触摸唤醒,阈值越低越灵敏
esp_sleep_enable_touchpad_wakeup();

// 在setup中需要先初始化触摸
touchAttachInterrupt(T0, NULL, 40); // T0引脚,阈值40

阈值怎么调?我一般先读一下环境值:

int touch_val = touchRead(T0);
Serial.printf("触摸值: %d\n", touch_val);

然后阈值设成比环境值低20-30左右。环境值会受温度、湿度影响,所以产品量产时最好留点余量。我记得有一次在南方客户现场,湿度大,触摸阈值直接漂了30%,还好当时留了余量,不然就翻车了。

3.3 RTC内存保留策略

这个知识点容易被忽略,但我觉得它才是深度睡眠的精髓。ESP32进入深度睡眠后,大部分RAM都掉电了,但RTC内存(RTC_SLOW_MEM和RTC_FAST_MEM)还活着。这意味着你可以在这里存数据,醒来后接着用。

说白了,就是给数据找个「避难所」。

// 声明一个RTC内存变量
RTC_DATA_ATTR int boot_count = 0;

void setup() {
    Serial.begin(115200);
    delay(1000);
    
    // 每次醒来,计数加1
    boot_count++;
    Serial.printf("这是第 %d 次唤醒\n", boot_count);
    
    // 设置定时器10秒后唤醒
    esp_sleep_enable_timer_wakeup(10 * 1000000);
    esp_deep_sleep_start();
}

你看,RTC_DATA_ATTR 这个宏,就把变量放到了RTC内存里。醒来后值还在。我做过一个电池供电的温湿度记录仪,用这个特性存了采样次数和累计运行时间,方便后期分析电池寿命。

📌 重点: RTC内存容量有限。RTC_SLOW_MEM只有8KB,RTC_FAST_MEM也只有8KB。别往里面塞大数组,否则编译都过不去。

除了变量,你还可以存结构体:

typedef struct {
    uint32_t sample_count;
    float    last_temperature;
    float    last_humidity;
    uint8_t  error_code;
} rtc_data_t;

RTC_DATA_ATTR rtc_data_t rtc_data = {0};

嗯,这里要注意:RTC内存里的变量不能有构造函数,也不能是C++对象。老老实实用C结构体或者基本类型就好。

3.4 综合实战:一个低功耗传感器节点

来,咱们把上面这些知识点串起来。假设你要做一个电池供电的温湿度传感器,每10分钟采集一次数据,同时支持按键唤醒(用于调试)。

#include <esp_sleep.h>
#include <driver/gpio.h>

RTC_DATA_ATTR uint32_t wake_count = 0;

void setup() {
    Serial.begin(115200);
    delay(100);
    
    // 唤醒计数
    wake_count++;
    
    // 判断唤醒源
    esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
    if (cause == ESP_SLEEP_WAKEUP_TIMER) {
        Serial.println("定时器唤醒:采集数据");
        // 这里采集温湿度,然后上报
    } else if (cause == ESP_SLEEP_WAKEUP_EXT0) {
        Serial.println("按键唤醒:进入调试模式");
        // 进入调试模式,不自动睡眠
        debug_mode();
        return;
    }
    
    Serial.printf("总唤醒次数: %u\n", wake_count);
    
    // 配置下次唤醒
    esp_sleep_enable_timer_wakeup(10 * 60 * 1000000); // 10分钟
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_0, 0);      // 按键唤醒
    
    Serial.println("进入深度睡眠...");
    esp_deep_sleep_start();
}

void loop() {}

void debug_mode() {
    // 调试模式:不睡眠,等待串口指令
    while (true) {
        if (Serial.available()) {
            char cmd = Serial.read();
            if (cmd == 's') {
                Serial.println("手动进入睡眠");
                esp_sleep_enable_timer_wakeup(10 * 60 * 1000000);
                esp_sleep_enable_ext0_wakeup(GPIO_NUM_0, 0);
                esp_deep_sleep_start();
            }
        }
        delay(100);
    }
}
🔧 我的经验: 调试阶段,我习惯用按键唤醒进入调试模式,这样不用每次改代码。量产时去掉调试模式,只保留定时器唤醒,功耗能降到最低。

最后说一句:深度睡眠不是万能的。如果你的设备需要频繁响应(比如每秒一次),那深度睡眠反而不合适,因为每次唤醒的启动时间(约30ms)会吃掉大量功耗。这时候考虑Modem-sleep或者Light-sleep更实际。

嗯,这一章就到这。下一章咱们聊聊「外设功耗管理」——怎么在保持功能的同时,把每个外设的功耗压到最低。