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() 里,结果深度睡眠醒来后直接跑飞——嗯,这是个经典坑。
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);
为什么会有两种?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内存里。醒来后值还在。我做过一个电池供电的温湿度记录仪,用这个特性存了采样次数和累计运行时间,方便后期分析电池寿命。
除了变量,你还可以存结构体:
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更实际。
嗯,这一章就到这。下一章咱们聊聊「外设功耗管理」——怎么在保持功能的同时,把每个外设的功耗压到最低。