低功耗与电池 / 睡眠节点实战发布 内容更新

按钮或门磁唤醒 ESP32:为什么睡下去又立刻醒?

用S3 RTC GPIO5/6建立EXT0与EXT1三种明确事件配置,等待非触发电平稳定后再武装;持续按下、开门和重上电各有可执行验收。

读完能做什么
用S3 RTC GPIO5/6建立EXT0与EXT1三种明确事件配置,等待非触发电平稳定后再武装;持续按下、开门和重上电各有可执行验收。
开始之前
已完成所列前置,能按具体板级引脚断电接线、构建原生IDF工程并保存串口日志。
所需器材
官方ESP32-S3-DevKitC-1 v1.1 / ESP32-S3-WROOM-1-N8R8,8MB QIO Flash / 8MB OPI PSRAM;UART0桥接115200(GPIO43/44)。、K1常开瞬时按钮,断电通断确认触点;两只10kΩ±1%上拉到3V3、Adafruit PID375两线常开干簧门磁(2021-06后端线修订),只接干触点;短线非安防长线
版本组合
参考板:官方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;无外部组件依赖外部输入:GPIO5 J1-5 / GPIO6 J1-6,S3 RTC IO;10kΩ±1%至3V3;PID375无磁铁开路
适用范围

官方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#l04版本:ESP-IDF v5.5.5验证日期:
构建 / 配置已通过
docs/esp32-evidence/low-power-builds.json对象:ESP32-S3 N8R8;3实际配置变体Exit0,最终源/日志/固件SHA已逐项核对版本:ESP-IDF v5.5.5 / GCC14.2.0验证日期:
仿真已通过
docs/esp32-evidence/low-power-host-checks.json对象:9组同源电平/释放/会话;没有RTC/门磁/深睡仿真验证日期:
本站实板待验证
没有实板、PPK2、电流/连续唤醒/门磁/无线实测;实际观察CSV保持仅表头。

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

01 / 睡眠节点实战

一按就醒之后,为什么不能立刻再睡?

EXT0/EXT1检查的是所选脚的电平条件。按钮按住LOW还没松开时再要求LOW唤醒,条件仍成立;把代码反复塞进Deep-sleep可能连续醒来,反而耗电。

本篇先定义事件再定义再武装。持续触发时明确保持醒着等待释放,不冒称已经深睡;只有全部选中脚非触发电平连续100ms后才允许入睡。无本站实板,没有“已经无循环”的观察结论。

参考:[1] IDF5.5.5 S3 Sleep Modes:EXT0/EXT1/hold[2] 固定esp_sleep.h:S3范围/ANY_LOW/追加mask/try rejection

02 / 睡眠节点实战

S3支持的RTC脚,不等于任何板脚都能用

IDF5.5.5列S3 EXT0/EXT1为RTC GPIO0–21。本例选GPIO5 J1-5与GPIO6 J1-6,避开BOOT0、原生USB19/20、N8R8内部35–37、RGB38;不能把普通数字GPIO中断可用当作Deep-sleep可用。

EXT0用一个RTC IO,依赖RTC外设域;EXT1用RTC控制器可在该外设域断电时工作。外接上拉固定电平,本文不拿内部拉电阻是否保持来赌接线,也不声称两种模式功耗相同。

输入接线静态含义
GPIO5 J1-510k±1%到3V3;常开按钮到GND松开HIGH / 按下LOW
GPIO6 J1-610k±1%到3V3;PID375两线触点到GND无磁铁HIGH / 磁铁靠近LOW
3V3 J1-1 / GND J1-22外部上拉/公共地不输入5V或12V
按钮四脚断电通断确认两组不能选内部已经相通的一对当开关

参考:[1] IDF5.5.5 S3 Sleep Modes:EXT0/EXT1/hold[2] 固定esp_sleep.h:S3范围/ANY_LOW/追加mask/try rejection[5] S3 RTC GPIO映射[6] DevKitC-1 v1.1排针/占用脚[7] PID375常开干簧开关:磁铁靠近闭合

03 / 睡眠节点实战

按钮按下、磁铁靠近与开门分成三种配置

S3 EXT1同一配置的多个脚必须同一触发电平,不能随意OR“LOW按钮+HIGH开门”。旧ESP32 ALL_LOW规则也不能套成S3 ANY_LOW;本例用enable_ext1_wakeup_io追加前先disable_ext1(0)清旧mask。

PID375常开是指无磁铁时触点开路;安装门关时磁铁靠近,则LOW代表关门。距13/15mm的产品描述不当本站安装保证,先通断检查自己的方向/距离;只用短线干触点,不接建筑安防长线电源。

变体选择/条件可以再次睡眠的状态
ext0-buttonGPIO5 LOW按钮松开HIGH
ext1-two-lowGPIO5或GPIO6 ANY_LOW两路都HIGH;这是按钮/磁铁靠近,不是开门
ext1-door-high只GPIO6 ANY_HIGH磁铁靠近LOW;开门HIGH醒且维持等待

参考:[1] IDF5.5.5 S3 Sleep Modes:EXT0/EXT1/hold[2] 固定esp_sleep.h:S3范围/ANY_LOW/追加mask/try rejection[7] PID375常开干簧开关:磁铁靠近闭合

04 / 睡眠节点实战

100ms释放窗口后,还要应对入睡前的新触发

启动打印reset/wake/EXT1_mask,RTC会话按深睡来源+记录校验续接,RESET/掉电另开会话。深睡启动清hold,应用仍先hold_dis/deinit再init为RTC输入并关内部拉电阻。

先等2秒维护,再以10ms轮询观察全部选中脚稳定非触发100ms;任一路触发清计时。ARM日志排空UART后再读一次,随后try_to_start允许拒绝睡眠;返回时清再武装并至少等100ms,避免紧循环重试。

在最后检查之后才出现真实电平,芯片可能拒绝,也可能刚睡下就醒一次;这不等于程序保证零竞态。下一次启动继续等待释放,防止持有触点触发持续睡醒循环。短于采样/唤醒窗口的脉冲不保证捕获,100ms也不是硬件毛刺滤波器。

参考:[1] IDF5.5.5 S3 Sleep Modes:EXT0/EXT1/hold[3] 固定rtc_io.h:RTC复用和hold接口[4] 固定sleep_modes.c:唤醒准备与reject路径

05 / 睡眠节点实战

完整事件记录器与同源释放逻辑

CycleRecord保存当前会话启动轮数与外部唤醒数,先检查deep reset与当前模式来源,不能把普通开机算作EXT事件。EXT1返回GPIO位mask;它不是边沿顺序,也不能把一个mask里两位当作已捕获两次独立事件。

无无线/NVS/外部库;等待时CPU运行、每5秒诊断。触点闭合经10k上拉约3.3V/10k=0.33mA是电路计算,和CPU等待功耗一起属于实际待测条件,不是一个睡眠结果。

v5.5.5日志reset=8是DEEPSLEEP,wake=2是EXT0、3是EXT1;返回GPIO位mask中5/6脚为0x20/0x40。events.csv为空的专用事件记录,不预填任何按压或门磁结果。

代码 / 配置
// ---- main/ReleaseGuard.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <stdint.h>
struct ReleaseGuard {
 bool tracking=false;uint64_t since=0;
 void reset(){tracking=false;}
 bool update(bool inactive,uint64_t now){if(!inactive){tracking=false;return false;}if(!tracking){tracking=true;since=now;}return now>=since&&now-since>=100;}
};
inline bool allInactive(uint64_t highMask,uint64_t selected,bool activeHigh){return activeHigh?(highMask&selected)==0:(highMask&selected)==selected;}

// ---- main/CycleRecord.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <stdint.h>
struct CycleRecord { uint32_t magic,version,cycle,success,check; };
inline uint32_t recordCheck(const CycleRecord& r){uint32_t c=0x811c9dc5U;const uint32_t v[]={r.magic,r.version,r.cycle,r.success};for(auto w:v)for(int i=0;i<4;++i){c^=uint8_t(w>>(8*i));c*=16777619U;}return c;}
inline bool recordValid(const CycleRecord& r){return r.magic==0x4c503033U&&r.version==1&&r.success<=r.cycle&&r.check==recordCheck(r);}
inline bool beginCycle(CycleRecord& r,bool expectedDeepWake){bool continued=expectedDeepWake&&recordValid(r)&&r.cycle!=UINT32_MAX;if(!continued)r={0x4c503033U,1,0,0,0};++r.cycle;r.check=recordCheck(r);return continued;}
inline void finishCycle(CycleRecord& r,bool success){if(success&&r.success<r.cycle)++r.success;r.check=recordCheck(r);}

// ---- main/main.cpp ----
// SPDX-License-Identifier: MIT
#include <cstdio>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/rtc_io.h"
#include "driver/uart.h"
#include "esp_sleep.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "CycleRecord.h"
#include "ReleaseGuard.h"
RTC_DATA_ATTR static CycleRecord record{};
static uint64_t nowMs(){return uint64_t(esp_timer_get_time()/1000);}
static uint64_t readHigh(uint64_t selected){uint64_t high=0;for(int p=5;p<=6;++p)if((selected&(1ULL<<p))&&rtc_gpio_get_level(gpio_num_t(p)))high|=1ULL<<p;return high;}
extern "C" void app_main(){
 constexpr int mode=CONFIG_HX_WAKE_MODE;const bool high=mode==2;const uint64_t mask=mode==0?(1ULL<<5):(mode==1?((1ULL<<5)|(1ULL<<6)):(1ULL<<6));
 auto reset=esp_reset_reason();auto wake=esp_sleep_get_wakeup_cause();bool external=wake==(mode==0?ESP_SLEEP_WAKEUP_EXT0:ESP_SLEEP_WAKEUP_EXT1);
 bool continued=beginCycle(record,reset==ESP_RST_DEEPSLEEP&&external);finishCycle(record,external&&reset==ESP_RST_DEEPSLEEP);
 printf("L04 mode=%d reset=%d wake=%d session=%s boot_cycle=%" PRIu32 " external_wakes=%" PRIu32 " ext1_mask=0x%" PRIx64 "\n",mode,int(reset),int(wake),continued?"CONTINUED":"NEW",record.cycle,record.success,esp_sleep_get_ext1_wakeup_status());
 // Deep-sleep startup clears hold; release explicitly before reconfiguration anyway.
 for(int p=5;p<=6;++p)if(mask&(1ULL<<p)){
  gpio_num_t pin=gpio_num_t(p);if(!rtc_gpio_is_valid_gpio(pin)){puts("STOP non-RTC pin");return;}
  ESP_ERROR_CHECK(rtc_gpio_hold_dis(pin));ESP_ERROR_CHECK(rtc_gpio_deinit(pin));ESP_ERROR_CHECK(rtc_gpio_init(pin));ESP_ERROR_CHECK(rtc_gpio_set_direction(pin,RTC_GPIO_MODE_INPUT_ONLY));ESP_ERROR_CHECK(rtc_gpio_pullup_dis(pin));ESP_ERROR_CHECK(rtc_gpio_pulldown_dis(pin));
 }
 // External 10k to 3V3 defines both GPIO levels, even with RTC domain off.
 if(mode==0)ESP_ERROR_CHECK(esp_sleep_enable_ext0_wakeup(GPIO_NUM_5,0));
 else{ESP_ERROR_CHECK(esp_sleep_disable_ext1_wakeup_io(0));ESP_ERROR_CHECK(esp_sleep_enable_ext1_wakeup_io(mask,high?ESP_EXT1_WAKEUP_ANY_HIGH:ESP_EXT1_WAKEUP_ANY_LOW));}
 puts("WAIT_RELEASE: selected inputs must all remain inactive 100ms; held input keeps CPU awake");
 vTaskDelay(pdMS_TO_TICKS(2000));ReleaseGuard guard;uint64_t lastPrint=nowMs();
 for(;;){
  uint64_t now=nowMs(),levels=readHigh(mask);
  if(guard.update(allInactive(levels,mask,high),now)){
   printf("ARM mask=0x%" PRIx64 " active=%s; level wake, not edge capture\n",mask,high?"HIGH":"LOW");fflush(stdout);ESP_ERROR_CHECK(uart_wait_tx_idle_polling(UART_NUM_0));
   // Recheck after log/flush. try_to_start can reject a newly active source.
   if(!allInactive(readHigh(mask),mask,high)){guard.reset();continue;}
   esp_err_t e=esp_deep_sleep_try_to_start();printf("SLEEP_RETURN %s; disarm/release again, no immediate retry loop\n",esp_err_to_name(e));guard.reset();vTaskDelay(pdMS_TO_TICKS(100));
  }
  if(now-lastPrint>=5000){lastPrint=now;printf("WAIT_RELEASE high_mask=0x%" PRIx64 " selected=0x%" PRIx64 "; not a low-current state\n",levels,mask);}
  vTaskDelay(pdMS_TO_TICKS(10));
 }
}

参考:[2] 固定esp_sleep.h:S3范围/ANY_LOW/追加mask/try rejection[3] 固定rtc_io.h:RTC复用和hold接口[4] 固定sleep_modes.c:唤醒准备与reject路径

06 / 睡眠节点实战

按模式编译,不靠改一行掩码猜语义

固定官方ESP32-S3-DevKitC-1 v1.1 / ESP32-S3-WROOM-1-N8R8,8MB QIO Flash / 8MB OPI PSRAM;UART0桥接115200(GPIO43/44)。三个配置文件随完整工程交付;每次换模式使用独立SDKCONFIG和build,避免旧持久sdkconfig优先。默认ext0-button,没接门磁也能先跑按钮。

  1. 全部断电接好上拉与干触点,通断检查按钮两组及门磁靠近/移开。
  2. 单USB-to-UART刷所选变体,记录源SHA/配置/实际串口;不并接外部电源。
  3. 先按下节各自的非触发状态开机,查看NEW→WAIT_RELEASE→ARM;后续原始启动原因完整保留。
代码 / 配置
idf.py -B build-ext0 -D SDKCONFIG=sdkconfig.ext0 -D 'SDKCONFIG_DEFAULTS=sdkconfig.defaults;config/ext0-button.defaults' -D IDF_TARGET=esp32s3 build
idf.py -B build-ext1 -D SDKCONFIG=sdkconfig.ext1 -D 'SDKCONFIG_DEFAULTS=sdkconfig.defaults;config/ext1-two-low.defaults' -D IDF_TARGET=esp32s3 build
idf.py -B build-door -D SDKCONFIG=sdkconfig.door -D 'SDKCONFIG_DEFAULTS=sdkconfig.defaults;config/ext1-door-high.defaults' -D IDF_TARGET=esp32s3 build

参考:[2] 固定esp_sleep.h:S3范围/ANY_LOW/追加mask/try rejection[6] DevKitC-1 v1.1排针/占用脚

07 / 睡眠节点实战

保持、释放、重上电分别验收

以下都是待执行实物步骤。不能用host输出PASS补掉硬件缺项;每模式原始日志与observations.csv独立记录,测流按L02互斥供电。

  1. 模式0:按钮松开开机→ARM;睡后按一次→EXT0。持续按住30s,应只有此一次外部唤醒且停WAIT_RELEASE;松开100ms后再次ARM,再按触发下一次。
  2. 模式0再做上电按住、RESET按住:应NEW+等待,不应产生持续睡醒循环;松开后再测。
  3. 模式1:磁铁移开且按钮松开后ARM,分别按钮按下/磁铁靠近触发,记录EXT1_mask。两者同触发只记一次启动及mask,保持任一路LOW不能再ARM。
  4. 模式2:磁铁靠近闭门LOW后ARM,移开开门HIGH触发GPIO6;保持开门30s等待,闭门后再ARM。此模式GPIO5动作不能成为唤醒源。
  5. 各模式做RESET/掉电与新会话、普通触发与深睡续接对照;保留实际电平、真实错误/拒绝、外部计数与供电条件。
做到这里,你应该看到
持续触发时“没再睡”是本方案保护行为;此时电流不能填为Deep-sleep。

参考:[1] IDF5.5.5 S3 Sleep Modes:EXT0/EXT1/hold[2] 固定esp_sleep.h:S3范围/ANY_LOW/追加mask/try rejection[7] PID375常开干簧开关:磁铁靠近闭合

08 / 睡眠节点实战

同源边界输入能证明什么?

ReleaseGuard.h由固件与原样host同时使用;9组检查覆盖上电保持、释放99/100ms边界、再触发重计时、跨32位时刻、ANY_LOW/HIGH和普通重启的会话清零。它不调用睡眠API,不模拟RTC/门磁/电气。

若产品要求触发保持很久仍低功耗,需另设计ULP/定时/状态变化捕获;本文不会动态翻转触发极性后声称已经获得严格边沿计数。

代码 / 配置
c++ -std=c++17 -Wall -Wextra -Werror tests/release_guard_test.cpp -o /tmp/l04-guard
/tmp/l04-guard

参考:[1] IDF5.5.5 S3 Sleep Modes:EXT0/EXT1/hold[2] 固定esp_sleep.h:S3范围/ANY_LOW/追加mask/try rejection

09 / 睡眠节点实战

按日志层级排查,保留仍未实测的边界

连续启动先看reset是否DEEPSLEEP,再查触点极性/10k/mask与guard是否被跳过。一直WAIT_RELEASE先对照模式表,不是先把防护删掉;ARM不醒则实测RTC脚电平与共地。SLEEP_RETURN保存SDK错误,不无限紧循环重试。

恢复回单USB,BOOT+RESET进入下载,不改eFuse、不关闭欠压检测、不以擦Flash作为第一步。本文没有电流、续航、无线与实际唤醒结果,空观察表留给自己的器件记录。

参考:[1] IDF5.5.5 S3 Sleep Modes:EXT0/EXT1/hold[3] 固定rtc_io.h:RTC复用和hold接口[4] 固定sleep_modes.c:唤醒准备与reject路径

BACK TO THE SOURCE

资料来源与版本说明

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

  1. IDF5.5.5 S3 Sleep Modes:EXT0/EXT1/hold
  2. 固定esp_sleep.h:S3范围/ANY_LOW/追加mask/try rejection
  3. 固定rtc_io.h:RTC复用和hold接口
  4. 固定sleep_modes.c:唤醒准备与reject路径
  5. S3 RTC GPIO映射
  6. DevKitC-1 v1.1排针/占用脚
  7. PID375常开干簧开关:磁铁靠近闭合