一按就醒之后,为什么不能立刻再睡?
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
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-5 | 10k±1%到3V3;常开按钮到GND | 松开HIGH / 按下LOW |
| GPIO6 J1-6 | 10k±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常开干簧开关:磁铁靠近闭合
按钮按下、磁铁靠近与开门分成三种配置
S3 EXT1同一配置的多个脚必须同一触发电平,不能随意OR“LOW按钮+HIGH开门”。旧ESP32 ALL_LOW规则也不能套成S3 ANY_LOW;本例用enable_ext1_wakeup_io追加前先disable_ext1(0)清旧mask。
PID375常开是指无磁铁时触点开路;安装门关时磁铁靠近,则LOW代表关门。距13/15mm的产品描述不当本站安装保证,先通断检查自己的方向/距离;只用短线干触点,不接建筑安防长线电源。
| 变体 | 选择/条件 | 可以再次睡眠的状态 |
|---|---|---|
| ext0-button | GPIO5 LOW | 按钮松开HIGH |
| ext1-two-low | GPIO5或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常开干簧开关:磁铁靠近闭合
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路径
完整事件记录器与同源释放逻辑
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路径
按模式编译,不靠改一行掩码猜语义
固定官方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,没接门磁也能先跑按钮。
- 全部断电接好上拉与干触点,通断检查按钮两组及门磁靠近/移开。
- 单USB-to-UART刷所选变体,记录源SHA/配置/实际串口;不并接外部电源。
- 先按下节各自的非触发状态开机,查看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排针/占用脚
保持、释放、重上电分别验收
以下都是待执行实物步骤。不能用host输出PASS补掉硬件缺项;每模式原始日志与observations.csv独立记录,测流按L02互斥供电。
- 模式0:按钮松开开机→ARM;睡后按一次→EXT0。持续按住30s,应只有此一次外部唤醒且停WAIT_RELEASE;松开100ms后再次ARM,再按触发下一次。
- 模式0再做上电按住、RESET按住:应NEW+等待,不应产生持续睡醒循环;松开后再测。
- 模式1:磁铁移开且按钮松开后ARM,分别按钮按下/磁铁靠近触发,记录EXT1_mask。两者同触发只记一次启动及mask,保持任一路LOW不能再ARM。
- 模式2:磁铁靠近闭门LOW后ARM,移开开门HIGH触发GPIO6;保持开门30s等待,闭门后再ARM。此模式GPIO5动作不能成为唤醒源。
- 各模式做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常开干簧开关:磁铁靠近闭合
同源边界输入能证明什么?
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
按日志层级排查,保留仍未实测的边界
连续启动先看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路径
资料来源与版本说明
已核固定版本API与模块/仪器手册;实际构建和主机逻辑单独列证据,硬件仍待执行。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。