← 返回教程库

低功耗与深度睡眠:电池供电必修

最后更新 2026-06-22
L3 · 联网与 IoT ⏱ 约 22 分钟 🟡 涉接线/强电
你将学到
  • 说清 ESP32-S3 的几个功耗档位,知道正常工作、WiFi 发射、深度睡眠各自吃多少电流
  • 能用 esp_sleep_enable_timer_wakeup / esp_sleep_enable_ext1_wakeup 写出定时唤醒和引脚唤醒两种深睡代码
  • 用 RTC_DATA_ATTR 把跨睡眠的变量保住,用 esp_sleep_get_wakeup_cause() 判断这次是上电还是睡醒
  • 会用平均电流估算电池续航,知道哪些坑会让续航打骨折

你做了个电池供电的温湿度计:一节 18650 锂电、一颗 ESP32-S3、一个 DHT22,每隔几分钟读一次温湿度发到服务器。插着 USB 测的时候一切正常。拔了 USB 换电池,第二天早上一看——没电了。

不是你电路接错,是 ESP32-S3 默认就一直全速跑、WiFi 一直开着。这种状态下它平均要吃几十毫安,一节 2500mAh 的电池撑一天算它客气。要让这种设备跑几个月,核心只有一招:让它绝大部分时间睡着,只在需要干活的那几秒醒过来。这一节就把这招用 ESP-IDF 讲透。

开始前你应该已经会让 ESP32-S3 连 WiFi 上报数据(见 /guide/l3-wifi/),也知道锂电池供电的基本安全(见 /principle/lithium-safety/)。本篇的核心 API——esp_deep_sleep_start()esp_sleep_enable_*——本就是 ESP-IDF 原生的,Arduino 也是直接调它们;真正要换的是外壳:把 setup/loop 换成 app_mainSerial 换成 ESP_LOGI。这点我说在前头,免得你以为有什么魔法。


先认清功耗档位:你的电流花在哪

谈省电之前,得先有数量级的概念。ESP32-S3 不是"开"和"关"两档,而是好几个档位,电流差了上万倍:

状态 典型电流 在干什么
正常工作(CPU 全速,WiFi 关) 约 30~50mA 跑你的代码、读传感器
正常工作 + WiFi 已连接空闲 约 80~120mA WiFi 维持连接
WiFi 发射峰值 瞬时 240~500mA 正在发数据包
Modem-sleep 约 20~30mA CPU 跑,射频按需关
Light-sleep 约 0.8mA CPU 暂停,RAM 保留,可快速恢复
Deep-sleep 约 7~10μA 几乎全关,只留 RTC

注意单位:正常工作是毫安(mA),深度睡眠是微安(μA),差了一万倍。深睡 10μA 是什么概念?一节 2500mAh 的电池,光按这个电流算能放上百年(实际会被电池自放电和稳压器静态电流拖死,后面讲)。

所以省电的本质不是"把工作电流降一点",而是尽量缩短工作时间,把剩下时间全扔进深度睡眠。这是电池设备的第一性原理。


三种睡眠模式,该用哪个

ESP-IDF 给你三档睡眠,给你我的取舍建议:

  • Modem-sleep:只关射频、CPU 照跑。适合"必须秒级响应但又想省点"的设备,省得有限,电池设备基本看不上。
  • Light-sleep(esp_light_sleep_start()):CPU 暂停、外设和 RAM 都保留,来个中断就毫秒级恢复,接着上次的位置继续跑。适合"要保持响应、又能频繁打盹"的场景,比如一个要随时接按键的遥控器。
  • Deep-sleep(esp_deep_sleep_start()):除了 RTC 控制器和一小块 RTC 内存,几乎全断电。功耗压到约 10μA,代价是醒来等于重启——RAM 清空,从 app_main 从头跑,不会接着上次的位置继续。

这是 Light-sleep 和 Deep-sleep 最大的行为差异:浅睡是"暂停/继续",深睡是"关机/重新开机"。电池供电的"间歇上报"类设备(温湿度计、土壤传感器、水表),九成场景用 Deep-sleep。下面重点讲它。


第一步:先把定时唤醒这段跑通

最常见的需求:每隔一段时间醒一次,测个数发出去,再睡下。因为深睡醒来从 app_main 重跑,所以整个工作逻辑都写在 app_main 里,跑完就睡,没有 while(1)——这跟点灯那种常驻循环的程序完全不同,是深睡设备特有的"做完就睡"结构。

把下面这段放进工程的 main/main.c,idf.py set-target esp32s3 后直接 idf.py build flash monitor:

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_sleep.h"
#include "esp_log.h"

#define uS_PER_SECOND  1000000ULL    // 微秒到秒的换算,ULL 防溢出
#define SLEEP_MINUTES  10            // 每 10 分钟醒一次

static const char *TAG = "deepsleep";

// RTC_DATA_ATTR:变量存进 RTC 慢速内存,深睡期间持续供电,醒来不丢
RTC_DATA_ATTR static int boot_count = 0;

void app_main(void) {
    boot_count++;
    ESP_LOGI(TAG, "第 %d 次醒来", boot_count);

    // 判断这次是被谁叫醒的(首次上电 / 定时醒)
    esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
    if (cause == ESP_SLEEP_WAKEUP_TIMER) {
        ESP_LOGI(TAG, "是定时器把我叫醒的");
    } else {
        ESP_LOGI(TAG, "首次上电或复位");   // ESP_SLEEP_WAKEUP_UNDEFINED
    }

    // —— 这里是你每次醒来要干的活 ——
    // 1. 读传感器(尽量快,工作越快越省电)
    //    float temp = read_temperature();   // 你自己的读取函数
    ESP_LOGI(TAG, "读传感器、连 WiFi 上报...(略,见 l3-wifi 那套骨架)");
    vTaskDelay(pdMS_TO_TICKS(200));          // 占位:模拟干活耗时

    // 2. 配定时唤醒:参数单位是微秒
    esp_sleep_enable_timer_wakeup(SLEEP_MINUTES * 60 * uS_PER_SECOND);

    // 3. 进深睡。这行之后的代码永远不会执行——醒来是从 app_main 顶部重跑
    ESP_LOGI(TAG, "睡 %d 分钟,晚安", SLEEP_MINUTES);
    esp_deep_sleep_start();
}

注意 uS_PER_SECOND 后缀是 ULL(无符号长长整型)。定时唤醒的参数单位是微秒,10 分钟 = 600 秒 = 6 亿微秒,普通 int 装不下会溢出——这是新手最常踩的坑,Arduino 时代是这坑,ESP-IDF 也是这坑,API 没变。

还要注意:深睡设备的 app_main 不放 while(1)。普通常驻程序最后是个死循环不让 app_main 返回;深睡程序则相反,做完事直接 esp_deep_sleep_start(),这个函数不返回(它把芯片关了),所以你不用担心 app_main 跑到结尾——它根本到不了结尾。

你应该看到什么

idf.py monitor 里你应该看到这样的循环节奏:

I (312) deepsleep: 第 1 次醒来
I (315) deepsleep: 首次上电或复位
I (318) deepsleep: 读传感器、连 WiFi 上报...
I (520) deepsleep: 睡 10 分钟,晚安
(串口安静 10 分钟,期间只有进入深睡前系统打的几行底层日志)
ets ... rst:0x5 (DEEPSLEEP_RESET)      ← 这行是醒来时 bootloader 打的
I (312) deepsleep: 第 2 次醒来
I (315) deepsleep: 是定时器把我叫醒的
...

两个关键信号:一是"晚安"之后串口彻底安静——芯片没在跑任何代码,这正是深睡的标志;二是醒来时 bootloader 会打一行 rst:0x5 (DEEPSLEEP_RESET),这是"从深睡复位"的铁证,和上电复位(POWERON_RESET)区分得清清楚楚。如果"晚安"之后串口立刻又冒出"第 N 次醒来",说明它根本没睡进去或立刻被唤醒了,去查唤醒源设置。

有条件的话串个电流表(万用表选 mA/μA 档,串进电源回路)。工作那几秒你会看到几十毫安、连 WiFi 时跳到上百毫安甚至峰值更高;"晚安"之后,读数会骤降到几十微安。这个肉眼可见的暴跌,就是省电真正生效的证据。注意:多数开发板上有 USB 转串口芯片、电源 LED、稳压器,它们自己就吃几毫安,会盖住 10μA 的真实功耗——要测准得用裸模组或砍掉这些外围(见后面的坑)。


把这段讲透:三个 ESP-IDF 概念

RTC_DATA_ATTR:醒来后怎么记住上次的事

代码里那个 RTC_DATA_ATTR static int boot_count 是关键。深睡醒来 RAM 全清空,普通全局变量每次都回到初始值。但带 RTC_DATA_ATTR 修饰的变量,编译器会把它放进 RTC 慢速内存(RTC slow memory)——这块内存在深睡期间持续供电,所以醒来后值还在。

ESP32-S3 的 RTC 慢速内存有约 8KB,够存计数器、累计值、上次状态这类小数据。比如累计上报次数、滑动平均、"上次是不是连上了 WiFi"的标志,都该放这里:

RTC_DATA_ATTR static int   boot_count = 0;
RTC_DATA_ATTR static float last_temp = 0;
RTC_DATA_ATTR static bool  wifi_failed_last = false;

别往里塞大数组或字符串缓冲——RTC 内存小,只放真正需要跨睡眠保留的轻量状态。要存几 KB 以上、或者要掉电也保住的数据,该用 NVS(flash 上的键值存储,见 l3-wifi 里讲的那块),而不是 RTC 内存。

esp_sleep_get_wakeup_cause():这次是上电还是睡醒

因为深睡醒来 = 从 app_main 重跑,你常常需要在开头判断:"这是第一次上电,还是从深睡里醒来的?是定时醒的还是被引脚叫醒的?"这就靠 esp_sleep_get_wakeup_cause(),它返回一个枚举:

  • ESP_SLEEP_WAKEUP_UNDEFINED:不是从深睡醒的(首次上电、按复位键、看门狗复位等)。
  • ESP_SLEEP_WAKEUP_TIMER:定时器到点叫醒的。
  • ESP_SLEEP_WAKEUP_EXT0 / ESP_SLEEP_WAKEUP_EXT1:引脚电平叫醒的(下一节讲)。
  • ESP_SLEEP_WAKEUP_GPIO / ESP_SLEEP_WAKEUP_TOUCHPAD:GPIO/触摸唤醒。

有了它,你可以"首次上电做一次完整初始化(比如配网、对时),之后每次定时醒只干轻活"——省下重复初始化的时间和电。

esp_deep_sleep_start():之后不返回

esp_deep_sleep_start() 调用后不会返回:它把 CPU、RAM、大部分外设断电,芯片进入深睡。醒来时不是接着这行往下走,而是整颗芯片复位、从 app_main 顶部重新开始。所以这行后面写任何代码都是死代码,别在它后面放清理逻辑——清理要放在它前面。


第二步:引脚唤醒——按钮或 PIR 触发醒来

除了定时,还能让某个引脚的电平变化把 ESP32-S3 叫醒。这适合事件驱动的设备——门磁、按钮、PIR 人体感应,平时睡死,有人动了才醒。

这里有个 ESP32-S3 和老 ESP32 的重要差异,必须讲清楚:老 ESP32 常用的 esp_sleep_enable_ext0_wakeup()(单引脚 ext0)在 ESP32-S3 上不支持——S3 没有 ext0,只有 ext1(可多引脚)。所以在 S3 上做引脚唤醒,用 esp_sleep_enable_ext1_wakeup():

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_sleep.h"
#include "esp_log.h"

#define WAKE_GPIO  GPIO_NUM_4                 // 必须是 RTC GPIO(S3:GPIO0~21)
#define WAKE_MASK  (1ULL << WAKE_GPIO)        // ext1 用位掩码指定引脚

static const char *TAG = "wake";

RTC_DATA_ATTR static int boot_count = 0;

void app_main(void) {
    boot_count++;

    // 判断这次是被谁叫醒的
    esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
    if (cause == ESP_SLEEP_WAKEUP_EXT1) {
        // ext1 还能告诉你具体是哪个引脚触发的(多引脚场景有用)
        uint64_t pins = esp_sleep_get_ext1_wakeup_status();
        ESP_LOGI(TAG, "引脚触发醒的!触发引脚掩码=0x%llx", (unsigned long long)pins);
        // 干你的活:开灯、拍照、报警...
    } else if (cause == ESP_SLEEP_WAKEUP_TIMER) {
        ESP_LOGI(TAG, "定时醒的");
    } else {
        ESP_LOGI(TAG, "首次上电(第 %d 次)", boot_count);
    }

    ESP_LOGI(TAG, "干完活,准备睡,等下次触发...");
    vTaskDelay(pdMS_TO_TICKS(200));

    // 配 ext1:WAKE_GPIO 变成高电平就唤醒
    // ESP_EXT1_WAKEUP_ANY_HIGH = 掩码里任一引脚拉高就醒;ANY_LOW = 任一拉低就醒
    esp_sleep_enable_ext1_wakeup(WAKE_MASK, ESP_EXT1_WAKEUP_ANY_HIGH);

    // 也可以同时配定时唤醒:做"有人触发立刻醒 + 没人触发每小时自检一次"的混合策略
    // esp_sleep_enable_timer_wakeup(60ULL * 60 * 1000000ULL);

    esp_deep_sleep_start();
}

注意三点:

一是唤醒引脚必须是 RTC GPIO。ESP32-S3 上 GPIO0~GPIO21 是 RTC GPIO,可以用来唤醒;GPIO22 以上不行。普通 GPIO 在深睡时整个 IO 子系统都断电了,叫不醒。具体哪些脚能用,以你手上开发板的 datasheet 为准——选脚前查一眼。

二是 ext1位掩码指定引脚(1ULL << gpio),触发模式选 ESP_EXT1_WAKEUP_ANY_HIGH(掩码里任一脚拉高就醒)或 ESP_EXT1_WAKEUP_ANY_LOW(任一拉低就醒),按你的传感器输出极性选。多个引脚可以"或"进同一个掩码,任意一个触发都醒,醒来用 esp_sleep_get_ext1_wakeup_status() 看是哪个。

三是按钮/传感器唤醒记得加硬件上下拉电阻。悬空的 RTC 引脚会被干扰乱触发,反复把芯片叫醒反而更费电。配 ANY_HIGH 就接下拉(平时拉低,触发时拉高),配 ANY_LOW 就接上拉。

📌 说明

如果你之前用过老 ESP32 的 esp_sleep_enable_ext0_wakeup(GPIO_NUM_33, 1) 写法,在 S3 上会编译不过或运行报错——这是芯片能力差异,不是你写错。S3 把单引脚 ext0 砍了,统一用更灵活的 ext1。另外 S3 还有 esp_deep_sleep_enable_gpio_wakeup() 这条路,行为类似,挑一种用即可。


续航估算:一个简单乘法

知道续航能不能撑几个月,靠一个核心公式——平均电流 = (工作电流 × 工作时长 + 睡眠电流 × 睡眠时长) ÷ 总时长

举个实在例子:每 10 分钟醒一次,醒来读数 + 连 WiFi 上报大约花 6 秒,然后睡 600 秒。

  • 工作那 6 秒:平均电流粗估 100mA(连 WiFi 比较费)
  • 睡的 600 秒:约 0.05mA(算上开发板外围漏电,比裸模组 10μA 高得多)

平均电流 ≈ (100mA × 6s + 0.05mA × 600s) ÷ 606s ≈ 1.04mA

一节 2500mAh 的电池:2500mAh ÷ 1.04mA ≈ 2400 小时 ≈ 100 天。从"撑一天"变成"撑三个月",全靠把那 600 秒压到 μA 级。

反过来看,如果你不睡、一直连着 WiFi 跑(平均 100mA),2500mAh ÷ 100mA = 25 小时——一天就没了,和开头那个翻车的温湿度计完全对上。

想算自己的设备:量出一次工作真正花多少秒、那几秒平均吃多少 mA(电流表看),代进公式即可。锂电池的容量、放电特性见 /principle/lithium-safety/,供电稳压器的静态电流也会偷电,见 /principle/power-regulator/


几个会让续航打骨折的坑

  • 深睡醒来程序从头跑:很多人下意识以为会接着上次的位置继续(那是 Light-sleep 的行为),结果状态全丢。记住:Deep-sleep 醒来 = 复位重启,逻辑写 app_main、跨睡眠的值放 RTC_DATA_ATTR,开头用 esp_sleep_get_wakeup_cause() 判断来路。
  • WiFi 每次重连又慢又耗电:从睡醒到 WiFi 连上常要 3~8 秒,这几秒是高电流大头。能少连就少连——比如攒够 6 次读数再一次性连一次上报,或改用更省的协议。连不上一定要设超时(见 l3-wifi 里那套带重连上限的事件骨架),别让它干等几十秒把电耗光。
  • 只有 RTC GPIO 能唤醒:用引脚唤醒时若选了非 RTC 引脚(S3 上 GPIO22 以上),深睡时那个 IO 已断电,叫不醒。先查 datasheet 确认你的唤醒脚在 RTC GPIO 范围内。
  • 外设漏电:DHT22、屏幕、SD 卡这些在深睡时如果还接着电,照样偷电。讲究的做法是用一个 GPIO 控制 MOSFET 给外设上电,睡前切断。
  • 开发板外围吃电:USB 转串口芯片、电源指示 LED、低效稳压器(AMS1117 静态电流就有几个 mA)能轻松吃掉几毫安,让你的 10μA 深睡毫无意义。量产省电设备得用裸 ESP32-S3 模组 + 高效低静态电流稳压器。
  • GPIO 悬空乱唤醒:用 ext1 唤醒时,唤醒脚没接上下拉电阻,会被干扰反复叫醒,反而更费电。

故障排查表

现象 可能原因 怎么查
"晚安"后立刻又醒 定时器参数溢出(没用 ULL)/ 唤醒脚被触发 检查微秒计算用了 ULL;唤醒脚加上下拉电阻
boot_count 永远是 1 变量没加 RTC_DATA_ATTR 把跨睡眠的变量加上该修饰符
esp_sleep_enable_ext0_wakeup 编译/运行报错 在 ESP32-S3 上用了 ext0(S3 不支持) 改用 esp_sleep_enable_ext1_wakeup + 位掩码
引脚唤醒不生效 用了非 RTC GPIO(S3 GPIO22 以上)/ 极性反了 换成 GPIO0~21;核对 ANY_HIGH/ANY_LOW 和上下拉
电流表测不到 μA 级骤降 开发板外围(USB 芯片/LED/稳压器)在吃电 换裸模组测,或砍掉外围;先确认是不是测错档位
续航远不如估算 WiFi 连接太频繁太久 / 外设没断电 量实际工作电流和秒数;睡前用 MOSFET 切外设
醒来后传感器读数是 0 或乱码 外设刚上电还没稳定 上电后 vTaskDelay 等传感器初始化完成再读

变体:换个唤醒方式

PIR 低功耗人体感应:把 PIR 模块(见 /sensor/pir/)的输出接到 RTC GPIO 唤醒脚,用 ext1 + ESP_EXT1_WAKEUP_ANY_HIGH 高电平触发。平时 ESP32-S3 睡死、整机几十微安;有人经过 PIR 拉高电平,芯片秒醒,拍照或上报"有人来了",干完再睡。这是做电池供电安防/感应灯最省电的架构。

混合唤醒的手持设备:把按钮接 RTC 唤醒脚,同时配 esp_sleep_enable_timer_wakeup——做"按一下立刻醒 + 没人按也每小时自检一次"的混合策略。两个唤醒源可以同时设,醒来用 esp_sleep_get_wakeup_cause() 区分这次是按钮叫的还是定时叫的,分别处理。读完显示几秒、几秒无操作自动睡回去。

Light-sleep 保状态变体:如果你的设备需要"睡着也别丢内存状态、来个中断毫秒级接着跑"(比如低功耗遥控器),换 esp_light_sleep_start()——它醒来是从调用处往下继续,不是重启,RAM 全保留。代价是功耗比深睡高(约 0.8mA vs 10μA)。按"要不要保状态/响应速度"在浅睡和深睡之间选。


动手挑战

做一个每 15 分钟上报一次温湿度的电池设备,要求:

  1. 用 DHT22(见 /sensor/dht22/)读温湿度,逻辑全写在 app_main,做完就睡。
  2. esp_sleep_enable_timer_wakeup 设 15 分钟定时唤醒,工作完进 Deep-sleep。
  3. RTC_DATA_ATTR 记录累计上报次数,每次开头 ESP_LOGI 打印出来。
  4. 开头用 esp_sleep_get_wakeup_cause() 判断:首次上电时做一次完整初始化,定时醒时跳过初始化只读数上报。
  5. WiFi 连接复用 l3-wifi 那套带重连上限的事件骨架,连不上就跳过这次、照常睡下。
  6. 估算续航:量出一次工作实际花几秒、平均吃多少 mA,代进平均电流公式,算出一节 2000mAh 电池能撑多少天。
  7. 进阶:用一个 GPIO + MOSFET 在睡前切断 DHT22 供电,对比切与不切的平均电流差多少。

把你算出来的续航天数和电流表实测的深睡电流记下来——这两个数字就是你这台设备的"省电成绩单"。

本篇代码为参考实现,需结合你所用的最新 ESP-IDF 文档自校——尤其是 esp_sleep 的唤醒源 API、ESP32-S3 的 RTC GPIO 范围和 ext1 用法随版本/芯片型号可能微调,以官方 Sleep Modes 文档 为准。


小结与下一步

深度睡眠不是某个高级技巧,而是电池供电设备的默认架构:平时睡死(μA 级)、需要时秒醒干活(mA 级)、干完立刻睡回去。掌握了 esp_sleep_enable_timer_wakeup 定时唤醒 + esp_sleep_enable_ext1_wakeup 引脚唤醒 + RTC_DATA_ATTR 跨睡眠保值这三件套,再用 esp_sleep_get_wakeup_cause() 判断来路、用一个乘法估出续航,你就能把"撑一天"的玩具做成"撑几个月"的真设备。

要让多台这样的低功耗节点实时收发数据,下一步可以看长连接通信:/guide/l3-websocket/。如果你对前置的 WiFi 上报还不够熟,先回头巩固 /guide/l3-wifi/。想看整条学习路线,去 /roadmap/;更多实战项目在 /guide/

📄 来源 / 自校链接

本文为公开资料整理,非亲测。关键参数与代码请结合实物与下列官方来源验证。

内容有错、看不懂、或想看下一期?告诉我们 →

本文为公开资料的学习整理,非亲测。涉接线/花钱/合规的步骤请结合实物与官方最新资料验证,风险自负。见免责声明