低功耗与电池 / 模式选型与对照发布 内容更新

ESP32 省电模式怎么选?从一直在线到每小时上报一次

从远程响应、周期与事件保持三项约束选模式,用可复建工程比较任务等待、手动Light-sleep、Deep-sleep和自动管理的执行位置。

读完能做什么
从远程响应、周期与事件保持三项约束选模式,用可复建工程比较任务等待、手动Light-sleep、Deep-sleep和自动管理的执行位置。
开始之前
已完成所列前置,能按具体板级引脚断电接线、构建原生IDF工程并保存串口日志。
所需器材
官方ESP32-S3-DevKitC-1 v1.1 / ESP32-S3-WROOM-1-N8R8,8MB QIO Flash / 8MB OPI PSRAM;UART0桥接115200(GPIO43/44)。、USB-to-UART数据线;本篇不接外设或电池、自动联网扩展需要自己的2.4GHz AP,SSID/密码默认留空
版本组合
参考板:官方ESP32-S3-DevKitC-1 v1.1 / ESP32-S3-WROOM-1-N8R8,8MB QIO Flash / 8MB OPI PSRAM;UART0桥接115200(GPIO43/44)。框架:ESP-IDF v5.5.5;原生I²C / Sleep / PM;无外部组件依赖
适用范围

官方ESP32-S3-DevKitC-1 v1.1 / ESP32-S3-WROOM-1-N8R8,8MB QIO Flash / 8MB OPI PSRAM;UART0桥接115200(GPIO43/44)。仅此组合,未取得实板;不声称本站电流/续航/无线或唤醒结果。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/low-power.md#l01版本:ESP-IDF v5.5.5验证日期:
构建 / 配置已通过
docs/esp32-evidence/low-power-builds.json对象:ESP32-S3 N8R8;5实际配置变体Exit0,最终源/日志/固件SHA已逐项核对版本:ESP-IDF v5.5.5 / GCC14.2.0验证日期:
仿真已通过
docs/esp32-evidence/low-power-host-checks.json对象:同源策略5组;没有无线/PM/睡眠仿真验证日期:
本站实板待验证
没有实板、PPK2、电流/连续唤醒/门磁/无线实测;实际观察CSV保持仅表头。

构建通过只说明指定环境中的代码或配置通过检查;实物功能、功耗和稳定性以对应的实板记录为准。

01 / 模式选型与对照

先写一条不能丢的响应要求

一只在线灯控与每小时报一次温度的节点,空闲时间可能相同,睡眠选择却不同。前者需要远程命令仍能到达,后者可能接受每次重新启动和联网。第三种门磁记录器又取决于事件能否保持电平,不能只根据“想更省电”选最深模式。

本篇产出一张约束表和一个完整IDF对照工程。没有电流或延迟测量,因此不列谁低多少毫安,也不把 enabled 日志当系统已经睡着。

参考:[1] IDF5.5.5 S3 Sleep Modes:连接、RAM、唤醒和UART[2] IDF5.5.5 Power Management:PM锁与自动Light-sleep

02 / 模式选型与对照

三种任务的连接与唤醒代价

手动调用Light-sleep和Deep-sleep时,S3无线外设掉电,连接不保留;有在线要求时应从Modem-sleep与自动Light-sleep的系统协作出发。自动模式让驱动在需要时唤醒,仍受AP与应用延迟约束。

能否丢普通RAM、是否允许启动开销、事件能否保持到芯片醒来,必须一并写入选择。小于唤醒/采样窗口的脉冲不能由本例电平方案保证捕获。

任务入口选择必须承担/验证的成本
空闲仍接远程灯控命令Modem-sleep + 自动Light-sleepAP Beacon/DTIM、PM锁、命令往返、断线维护
每小时采集/上传一轮定时Deep-sleep每轮启动/初始化/连接/发送/重试 + 睡眠整体电量
按钮/门磁持续电平事件RTC EXT0/EXT1 Deep-sleepRTC脚、触发极性、释放再武装、短脉冲可能丢失
既不许丢RAM也不能漏事件先保持唤醒,重新设计不要直接把最深睡眠当唯一答案

参考:[1] IDF5.5.5 S3 Sleep Modes:连接、RAM、唤醒和UART[2] IDF5.5.5 Power Management:PM锁与自动Light-sleep

03 / 模式选型与对照

五秒之后,是返回原函数还是重启入口?

普通vTaskDelay只让当前任务让出CPU。工程的awake模式显式关闭自动Light-sleep,使它成为“应用等待”的对照;这不意味着所有项目的delay都一定保持同一电流。

手动Light-sleep成功后从esp_light_sleep_start返回,局部localLoops继续;Deep-sleep成功进入后不返回,醒来经过boot再进入app_main,局部localLoops从0开始,RTC_deep_cycles单独保存。RTC数据不能代替掉电存储。

参考:[1] IDF5.5.5 S3 Sleep Modes:连接、RAM、唤醒和UART[3] 固定esp_sleep.h/返回与不返回API

04 / 模式选型与对照

四份配置,避免旧sdkconfig覆盖默认值

固定官方ESP32-S3-DevKitC-1 v1.1 / ESP32-S3-WROOM-1-N8R8,8MB QIO Flash / 8MB OPI PSRAM;UART0桥接115200(GPIO43/44)。工程无需新增库。默认是manual;下载后进入项目,用E01已建立的IDF v5.5.5环境复建。各变体独立SDKCONFIG与build,防止持久旧配置悄悄覆盖defaults。

前三种都不初始化Wi-Fi,先保留3秒维护窗口,再比较5秒等待。自动模式启用CONFIG_PM_ENABLE和FreeRTOS tickless idle,运行时设置240/80MHz与light_sleep_enable,不调用手动睡眠。

  1. 只接USB-to-UART单电源,不接外部5V/3V3。核对v1.1/N8R8和实际串口PORT。
  2. 选择一个变体,使用同一-B和SDKCONFIG执行flash monitor,保存源SHA/配置/完整日志。
  3. 离线三组逐一比较before/returned/app_main日志,保留真实错误码;不要把错误后task delay称为睡眠。
代码 / 配置
idf.py --version
idf.py -B build-manual -D SDKCONFIG=sdkconfig.manual -D 'SDKCONFIG_DEFAULTS=sdkconfig.defaults;config/manual.defaults' -D IDF_TARGET=esp32s3 build
idf.py -B build-awake -D SDKCONFIG=sdkconfig.awake -D 'SDKCONFIG_DEFAULTS=sdkconfig.defaults;config/awake.defaults' -D IDF_TARGET=esp32s3 build
idf.py -B build-deep -D SDKCONFIG=sdkconfig.deep -D 'SDKCONFIG_DEFAULTS=sdkconfig.defaults;config/deep.defaults' -D IDF_TARGET=esp32s3 build
idf.py -B build-automatic -D SDKCONFIG=sdkconfig.automatic -D 'SDKCONFIG_DEFAULTS=sdkconfig.defaults;config/automatic.defaults' -D IDF_TARGET=esp32s3 build
做到这里,你应该看到
能指明localLoops何时延续、何时重新初始化,并解释RTC计数不等于掉电持久化。

参考:[3] 固定esp_sleep.h/返回与不返回API[6] DevKitC-1 v1.1供电/引脚

05 / 模式选型与对照

完整对照源码:策略与执行路径分开

Policy.h在固件中打印三种任务的路线,也由主机测试原样包含。函数选择设计入口,没有功耗常数。空SSID让自动分支明确停在CONFIG,不补一个虚构IP或连接成功。

睡眠前fflush并轮询排空UART,沿用固定官方Light-sleep例程的接口;未安装驱动对象时不能直接套uart_wait_tx_done。自动运行的诊断日志本身会影响空闲机会,应记入测试条件。

代码 / 配置
// ---- main/Policy.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <stdint.h>
enum class Choice { ONLINE_AUTO, DEEP_TIMER, DEEP_LEVEL, KEEP_AWAKE };
inline Choice choose(bool remoteWhileIdle,bool periodic,bool levelCanWait,bool losesRamOK){
 if(remoteWhileIdle)return Choice::ONLINE_AUTO;
 if(!losesRamOK)return Choice::KEEP_AWAKE;
 if(periodic)return Choice::DEEP_TIMER;
 if(levelCanWait)return Choice::DEEP_LEVEL;
 return Choice::KEEP_AWAKE;
}
inline const char* choiceName(Choice c){switch(c){case Choice::ONLINE_AUTO:return "ONLINE_AUTO";case Choice::DEEP_TIMER:return "DEEP_TIMER";case Choice::DEEP_LEVEL:return "DEEP_LEVEL";default:return "KEEP_AWAKE";}}

// ---- main/main.cpp ----
// SPDX-License-Identifier: MIT
#include <cstdio>
#include <cstring>
#include <inttypes.h>
#include <atomic>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_sleep.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "esp_pm.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "nvs_flash.h"
#include "driver/uart.h"
#include "Policy.h"
RTC_DATA_ATTR static uint32_t deepCycles=0;
static std::atomic<bool> online{false}, disconnected{false};
static void event(void*,esp_event_base_t base,int32_t id,void*){
 if(base==IP_EVENT&&id==IP_EVENT_STA_GOT_IP)online.store(true);
 if(base==WIFI_EVENT&&id==WIFI_EVENT_STA_DISCONNECTED){online.store(false);disconnected.store(true);}
}
static bool startWifi(){
 if(std::strlen(CONFIG_HX_SSID)==0){puts("CONFIG: empty SSID; no STA attempt, no online result");return false;}
 esp_err_t e=nvs_flash_init();if(e!=ESP_OK){printf("NVS error=%s; no erase\n",esp_err_to_name(e));return false;}
 ESP_ERROR_CHECK(esp_netif_init());ESP_ERROR_CHECK(esp_event_loop_create_default());
 if(!esp_netif_create_default_wifi_sta()){puts("STOP netif allocation");return false;}
 wifi_init_config_t init=WIFI_INIT_CONFIG_DEFAULT();ESP_ERROR_CHECK(esp_wifi_init(&init));
 ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT,ESP_EVENT_ANY_ID,event,nullptr));
 ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT,IP_EVENT_STA_GOT_IP,event,nullptr));
 wifi_config_t c{};std::snprintf((char*)c.sta.ssid,sizeof c.sta.ssid,"%s",CONFIG_HX_SSID);std::snprintf((char*)c.sta.password,sizeof c.sta.password,"%s",CONFIG_HX_PASSWORD);
 ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA,&c));
 ESP_ERROR_CHECK(esp_wifi_start());ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MIN_MODEM));return true;
}
extern "C" void app_main(){
 const int mode=CONFIG_HX_MODE;int localLoops=0;
 if(esp_reset_reason()!=ESP_RST_DEEPSLEEP)deepCycles=0;
 printf("L01 mode=%d reset=%d wake=%d RTC_deep_cycles=%" PRIu32 " localLoops=%d\n",mode,int(esp_reset_reason()),int(esp_sleep_get_wakeup_cause()),deepCycles,localLoops);
 printf("policy remote=%s hourly=%s contact=%s\n",choiceName(choose(true,false,false,true)),choiceName(choose(false,true,false,true)),choiceName(choose(false,false,true,true)));
 // No automatic sleep for the three offline comparisons, even though PM is compiled in.
 esp_pm_config_t pm{};pm.max_freq_mhz=240;pm.min_freq_mhz=80;pm.light_sleep_enable=mode==3;
 ESP_ERROR_CHECK(esp_pm_configure(&pm));
 if(mode==3){
  bool configured=startWifi(),attempting=false;uint64_t began=0,lastEnd=0,lastPrint=0;
  for(;;){
   uint64_t now=uint64_t(esp_timer_get_time()/1000);
   if(disconnected.exchange(false)){attempting=false;lastEnd=now;}
   if(configured&&!online.load()&&!attempting&&now-lastEnd>=5000){esp_err_t e=esp_wifi_connect();attempting=e==ESP_OK;began=now;lastEnd=now;printf("STA attempt api=%s\n",esp_err_to_name(e));}
   if(attempting&&now-began>=15000){esp_wifi_disconnect();attempting=false;lastEnd=now;puts("STA timeout; retry after >=5s");}
   if(online.load())attempting=false;
   if(now-lastPrint>=10000){lastPrint=now;printf("AUTO policy enabled, IP_event_online=%u; actual idle sleep/current/latency unmeasured\n",online.load());}
   vTaskDelay(pdMS_TO_TICKS(100));
  }
 }
 vTaskDelay(pdMS_TO_TICKS(3000)); // maintenance window before the offline comparison
 for(;;){
  ++localLoops;
  printf("before mode=%d localLoops=%d\n",mode,localLoops);fflush(stdout);
  if(mode==0){vTaskDelay(pdMS_TO_TICKS(5000));puts("awake: task delay returned; PM automatic sleep disabled");}
  else{
   ESP_ERROR_CHECK(esp_sleep_enable_timer_wakeup(5000000ULL));
   ESP_ERROR_CHECK(uart_wait_tx_idle_polling(UART_NUM_0));
   if(mode==1){esp_err_t e=esp_light_sleep_start();printf("manual returned=%s localLoops=%d wake=%d\n",esp_err_to_name(e),localLoops,int(esp_sleep_get_wakeup_cause()));if(e!=ESP_OK)vTaskDelay(pdMS_TO_TICKS(1000));}
   else{if(deepCycles!=UINT32_MAX)++deepCycles;puts("deep: next successful timer wake starts app_main again");fflush(stdout);esp_deep_sleep_start();}
  }
 }
}

参考:[3] 固定esp_sleep.h/返回与不返回API[4] 固定官方Wi-Fi power_save例程[5] 固定esp_pm_config_t与锁API

06 / 模式选型与对照

自动模式要先证明连接与响应,再量电流

在自己的私有menuconfig中填写AP:本例SSID最多31字节、密码最多63字节;开放网络密码留空。生成的sdkconfig不上传。默认空SSID不启动STA,PM策略可启用但没有“在线节点”证据。

程序以IP事件维护在线标志,15秒连接窗口、断线后至少5秒间隔;不打印密码,不自动擦NVS。IP_event_online为真也只能说明事件状态,不能代替手机/电脑到设备的端到端响应,更不能说明实际电流下降。

固定AP型号/频道/Beacon/DTIM,记录断网/重连和命令响应。每10秒状态输出、100ms任务等待、驱动锁都会改变可睡时间;看到AUTO enabled而没有下降时,先查忙任务/PM锁和AP条件,再下结论。

另有automatic-compile-only构建夹具,公开假SSID为HX_COMPILE_ONLY_AP、密码REPLACE_ME_ONLY,仅让真实Wi-Fi链接路径保留并核ELF符号;没有刷板、AP连接或响应结果,日常自动模式仍默认空SSID。

参考:[2] IDF5.5.5 Power Management:PM锁与自动Light-sleep[4] 固定官方Wi-Fi power_save例程[5] 固定esp_pm_config_t与锁API

07 / 模式选型与对照

本机可检查路线,板子才能检查睡眠

下载tests/policy_test.cpp使用同一Policy.h,覆盖在线优先、周期、事件、不能丢RAM和无触发策略的5组输入。这只是逻辑函数检查。

实板依次保留awake/manual/deep日志,再以相同AP/供电条件做自动模式;sleep API返回错误必须明确记录。比较功耗转L02,比较在线自动管理继续后续专题,不用本篇替代网络压力测试。

代码 / 配置
c++ -std=c++17 -Wall -Wextra -Werror tests/policy_test.cpp -o /tmp/l01-policy
/tmp/l01-policy
如果卡住了
Deep模式“没返回”是成功进入后的API语义;无后续启动则检查供电、timer与实际串口条件。反复NEW/复位先看reset原因,不关欠压检测。自动分支不联网先检查CONFIG与实际AP,再查窗口日志。

参考:[1] IDF5.5.5 S3 Sleep Modes:连接、RAM、唤醒和UART[2] IDF5.5.5 Power Management:PM锁与自动Light-sleep

08 / 模式选型与对照

保留能交接的选择理由

在observations.csv写实际任务的响应上限、允许丢失的状态、唤醒源和日志证据。此处没有预填本站数据。需要恢复刷机,单USB供电下BOOT+RESET进入下载,退出深睡后重建;不改eFuse或先擦Flash。 下载task-comparison.csv另有任务响应/连接/RAM/事件保持字段,留空供自己的任务填写。

做到这里,你应该看到
能用具体任务说明为什么需要/不需要保持连接,并承认尚未取得电流或延迟实测。
BACK TO THE SOURCE

资料来源与版本说明

已核固定版本API与模块/仪器手册;实际构建和主机逻辑单独列证据,硬件仍待执行。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. IDF5.5.5 S3 Sleep Modes:连接、RAM、唤醒和UART
  2. IDF5.5.5 Power Management:PM锁与自动Light-sleep
  3. 固定esp_sleep.h/返回与不返回API
  4. 固定官方Wi-Fi power_save例程
  5. 固定esp_pm_config_t与锁API
  6. DevKitC-1 v1.1供电/引脚