先写一条不能丢的响应要求
一只在线灯控与每小时报一次温度的节点,空闲时间可能相同,睡眠选择却不同。前者需要远程命令仍能到达,后者可能接受每次重新启动和联网。第三种门磁记录器又取决于事件能否保持电平,不能只根据“想更省电”选最深模式。
本篇产出一张约束表和一个完整IDF对照工程。没有电流或延迟测量,因此不列谁低多少毫安,也不把 enabled 日志当系统已经睡着。
参考:[1] IDF5.5.5 S3 Sleep Modes:连接、RAM、唤醒和UART[2] IDF5.5.5 Power Management:PM锁与自动Light-sleep
三种任务的连接与唤醒代价
手动调用Light-sleep和Deep-sleep时,S3无线外设掉电,连接不保留;有在线要求时应从Modem-sleep与自动Light-sleep的系统协作出发。自动模式让驱动在需要时唤醒,仍受AP与应用延迟约束。
能否丢普通RAM、是否允许启动开销、事件能否保持到芯片醒来,必须一并写入选择。小于唤醒/采样窗口的脉冲不能由本例电平方案保证捕获。
| 任务 | 入口选择 | 必须承担/验证的成本 |
|---|---|---|
| 空闲仍接远程灯控命令 | Modem-sleep + 自动Light-sleep | AP Beacon/DTIM、PM锁、命令往返、断线维护 |
| 每小时采集/上传一轮 | 定时Deep-sleep | 每轮启动/初始化/连接/发送/重试 + 睡眠整体电量 |
| 按钮/门磁持续电平事件 | RTC EXT0/EXT1 Deep-sleep | RTC脚、触发极性、释放再武装、短脉冲可能丢失 |
| 既不许丢RAM也不能漏事件 | 先保持唤醒,重新设计 | 不要直接把最深睡眠当唯一答案 |
参考:[1] IDF5.5.5 S3 Sleep Modes:连接、RAM、唤醒和UART[2] IDF5.5.5 Power Management:PM锁与自动Light-sleep
五秒之后,是返回原函数还是重启入口?
普通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
四份配置,避免旧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,不调用手动睡眠。
- 只接USB-to-UART单电源,不接外部5V/3V3。核对v1.1/N8R8和实际串口PORT。
- 选择一个变体,使用同一-B和SDKCONFIG执行flash monitor,保存源SHA/配置/完整日志。
- 离线三组逐一比较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计数不等于掉电持久化。
完整对照源码:策略与执行路径分开
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
自动模式要先证明连接与响应,再量电流
在自己的私有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
本机可检查路线,板子才能检查睡眠
下载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
保留能交接的选择理由
在observations.csv写实际任务的响应上限、允许丢失的状态、唤醒源和日志证据。此处没有预填本站数据。需要恢复刷机,单USB供电下BOOT+RESET进入下载,退出深睡后重建;不改eFuse或先擦Flash。 下载task-comparison.csv另有任务响应/连接/RAM/事件保持字段,留空供自己的任务填写。
- 做到这里,你应该看到
- 能用具体任务说明为什么需要/不需要保持连接,并承认尚未取得电流或延迟实测。
资料来源与版本说明
已核固定版本API与模块/仪器手册;实际构建和主机逻辑单独列证据,硬件仍待执行。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。