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

ESP32 定时唤醒:采一次温湿度就回去睡觉

用SHT40单次采样建立可校验RTC周期号,分清定时深睡与普通重启;给出30次连续唤醒记录与自动检查方法,实板实验待执行。

读完能做什么
用SHT40单次采样建立可校验RTC周期号,分清定时深睡与普通重启;给出30次连续唤醒记录与自动检查方法,实板实验待执行。
开始之前
已完成所列前置,能按具体板级引脚断电接线、构建原生IDF工程并保存串口日志。
所需器材
官方ESP32-S3-DevKitC-1 v1.1 / ESP32-S3-WROOM-1-N8R8,8MB QIO Flash / 8MB OPI PSRAM;UART0桥接115200(GPIO43/44)。、Adafruit PID4885 SHT40-AD1B / 0x44,可靠焊接排针;短线100kHz、可选SDA信号SPST开关;测流按L02独立5V边界,不并接USB
版本组合
参考板:官方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;无外部组件依赖传感器:PID4885 SHT40-AD1B 0x44;SHT4x datasheet v6.4(2023-11)
适用范围

官方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#l03版本:ESP-IDF v5.5.5验证日期:
构建 / 配置已通过
docs/esp32-evidence/low-power-builds.json对象:ESP32-S3 N8R8;1实际配置变体Exit0,最终源/日志/固件SHA已逐项核对版本:ESP-IDF v5.5.5 / GCC14.2.0验证日期:
仿真已通过
docs/esp32-evidence/low-power-host-checks.json对象:10组同源CRC/RTC、11组合成记录与实际空表/负序列拒绝;不是30次实板唤醒验证日期:
本站实板待验证
没有实板、PPK2、电流/连续唤醒/门磁/无线实测;实际观察CSV保持仅表头。

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

01 / 睡眠节点实战

成功和失败,都给这一轮一个结论

从S01持续串口温湿度计改成周期节点时,不能在采样while后只插入sleep就算完成。Deep-sleep每次重新启动,普通I²C对象和局部变量都要重建;失败也必须有退出策略。

本例只做一轮FD高精度采样,不开Wi-Fi、不上传、不驱动屏幕;本轮双CRC通过才打印数值,否则输出NA,随后同样安排60秒睡眠。没有本站实板;连续30次唤醒、活动/睡眠电流均给可执行方法。

参考:[1] IDF5.5.5 S3 Sleep Modes/定时与RTC内存[4] SHT4x v6.4 §4:测量/转换/CRC/时间

02 / 睡眠节点实战

指定模块的VIN与I²C,仍沿S01

官方ESP32-S3-DevKitC-1 v1.1 / ESP32-S3-WROOM-1-N8R8,8MB QIO Flash / 8MB OPI PSRAM;UART0桥接115200(GPIO43/44)。使用PID4885 SHT40-AD1B,默认0x44;可靠焊接排针、3.3V逻辑、短线≤20cm,模块自带10k上拉,不再添加未知并联电阻。3Vo是输出,不能当另一供电输入。

功能测试USB-to-UART单电源;测流必须切换为L02的断两USB/5V单点供电。传感器在睡眠中仍接3.3V,本例不是外设电源门控。

DevKit v1.1PID4885说明
J1-1 3V3VIN同3.3V逻辑供电
J1-22 GNDGND公共地
J1-12 GPIO8SDA100kHz/0x44
J1-15 GPIO9SCL短线
不连接3Vo输出留空

参考:[5] PID4885模块脚位[6] IDF5.5.5原生I²C主机[7] DevKitC-1 v1.1引脚

03 / 睡眠节点实战

不能用一个RTC计数掩盖普通重启

继续会话的条件同时包含reset=DEEPSLEEP、wake=TIMER与RTC记录magic/version/校验/计数范围。RESET、软件重启、掉电、结构损坏或uint32计数饱和都从NEW/cycle1开始;不能仅因RTC某个数字非0就算上次成功。

RTC_DATA_ATTR只服务同次供电内的深睡续接,保存cycle和累计success;不是NVS,不保证掉电保持。success并不证明连续成功,因为错误轮也会增加cycle。记录校验是完整性检查,不是安全认证。

首次NEW等待5秒方便维护;正常定时CONTINUED没有该等待。计时源esp_timer从启动重新开始,active_app_us只覆盖app_main内到END前,不能当含ROM/bootloader的完整活动时长。

本版本日志枚举:reset=8为DEEPSLEEP,wake=4为TIMER;其他数字按固定esp_system.h/esp_sleep.h解释,不把RESET按钮必然叫作EXT。VALID只指本轮I²C与CRC协议检查通过,不代表已校准或超出额定环境仍有精度保证。

参考:[1] IDF5.5.5 S3 Sleep Modes/定时与RTC内存[2] 固定sleep API与返回原因[3] 固定reset原因枚举[8] IDF固定RTC_DATA_ATTR定义

04 / 睡眠节点实战

完整固件:事务关闭后再入睡

原生I²C创建bus、添加0x44设备、发送FD、等待10ms、收6字节并检查两个CRC。API每步最多指定100ms超时,不以无限重试拖住活动;数值转换保留原始RH和0–100%裁剪RH,不能把裁剪叫校准。

正常与错误都更新RTC本轮记录,移除device/删除bus,再配置timer。外围总线失败并不被伪装成正常样本。此工程没有外部库、没有传感器加热器。

代码 / 配置
// ---- main/ShtSample.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <stdint.h>
struct ShtSample { bool valid=false;int t_mC=0;int rh_raw_mPct=0;int rh_mPct=0; };
inline uint8_t shtCrc(const uint8_t* b){uint8_t c=0xff;for(int k=0;k<2;++k){c^=b[k];for(int j=0;j<8;++j)c=(c&0x80)?uint8_t((c<<1)^0x31):uint8_t(c<<1);}return c;}
inline ShtSample decodeSht(const uint8_t b[6]){ShtSample s;if(shtCrc(b)!=b[2]||shtCrc(b+3)!=b[5])return s;uint16_t t=(uint16_t(b[0])<<8)|b[1],h=(uint16_t(b[3])<<8)|b[4];s.valid=true;s.t_mC=-45000+int(175000LL*t/65535);s.rh_raw_mPct=-6000+int(125000LL*h/65535);s.rh_mPct=s.rh_raw_mPct<0?0:(s.rh_raw_mPct>100000?100000:s.rh_raw_mPct);return s;}

// ---- main/SensorBus.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include "driver/i2c_master.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "ShtSample.h"
inline esp_err_t createBus(i2c_master_bus_handle_t* bus){i2c_master_bus_config_t c{};c.i2c_port=I2C_NUM_0;c.sda_io_num=GPIO_NUM_8;c.scl_io_num=GPIO_NUM_9;c.clk_source=I2C_CLK_SRC_DEFAULT;c.glitch_ignore_cnt=7;c.flags.enable_internal_pullup=false;return i2c_new_master_bus(&c,bus);}
inline esp_err_t addDevice(i2c_master_bus_handle_t bus,uint8_t addr,i2c_master_dev_handle_t* dev){i2c_device_config_t c{};c.dev_addr_length=I2C_ADDR_BIT_LEN_7;c.device_address=addr;c.scl_speed_hz=100000;return i2c_master_bus_add_device(bus,&c,dev);}
inline esp_err_t takeSample(i2c_master_dev_handle_t dev,ShtSample& sample){sample={};uint8_t cmd=0xfd,b[6];esp_err_t e=i2c_master_transmit(dev,&cmd,1,100);if(e!=ESP_OK)return e;vTaskDelay(pdMS_TO_TICKS(10));e=i2c_master_receive(dev,b,6,100);if(e==ESP_OK)sample=decodeSht(b);return e;}

// ---- 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 "esp_sleep.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "driver/uart.h"
#include "SensorBus.h"
#include "CycleRecord.h"
RTC_DATA_ATTR static CycleRecord record{};
extern "C" void app_main(){
 int64_t start=esp_timer_get_time();auto reset=esp_reset_reason();auto wake=esp_sleep_get_wakeup_cause();
 bool continued=beginCycle(record,reset==ESP_RST_DEEPSLEEP&&wake==ESP_SLEEP_WAKEUP_TIMER);
 printf("L03 reset=%d wake=%d session=%s cycle=%" PRIu32 " prior_success=%" PRIu32 "\n",int(reset),int(wake),continued?"CONTINUED":"NEW",record.cycle,record.success);
 if(!continued)vTaskDelay(pdMS_TO_TICKS(5000)); // first boot/RESET recovery window, not on normal timer wakes
 i2c_master_bus_handle_t bus=nullptr;i2c_master_dev_handle_t dev=nullptr;ShtSample sample;
 esp_err_t e=createBus(&bus);if(e==ESP_OK)e=addDevice(bus,0x44,&dev);if(e==ESP_OK)e=takeSample(dev,sample);
 finishCycle(record,e==ESP_OK&&sample.valid);
 if(e==ESP_OK&&sample.valid)printf("VALID T_mC=%d RH_raw_mPct=%d RH_display_mPct=%d\n",sample.t_mC,sample.rh_raw_mPct,sample.rh_mPct);
 else printf("ERROR I2C=%s CRC_valid=%u T=NA RH=NA; sleep still scheduled\n",esp_err_to_name(e),sample.valid);
 if(dev)ESP_ERROR_CHECK(i2c_master_bus_rm_device(dev));
 if(bus)ESP_ERROR_CHECK(i2c_del_master_bus(bus));
 ESP_ERROR_CHECK(esp_sleep_enable_timer_wakeup(60000000ULL));
 printf("END cycle=%" PRIu32 " success=%" PRIu32 " active_app_us=%" PRId64 " sleep_request_us=60000000; period includes boot+active+sleep\n",record.cycle,record.success,esp_timer_get_time()-start);
 fflush(stdout);ESP_ERROR_CHECK(uart_wait_tx_idle_polling(UART_NUM_0));esp_deep_sleep_start();
}

参考:[2] 固定sleep API与返回原因[4] SHT4x v6.4 §4:测量/转换/CRC/时间[6] IDF5.5.5原生I²C主机[8] IDF固定RTC_DATA_ATTR定义

05 / 睡眠节点实战

60秒睡眠不等于每分钟固定时刻采样

esp_sleep_enable_timer_wakeup(60000000)定义睡眠计时请求,实际精度受RTC_SLOW_CLK影响。两轮之间还包含ROM/bootloader、I²C初始化/测量、串口排空与应用处理。若要整分钟对齐,应另设计可信绝对时间与补偿,不能用本例日志宣布已经实现。

活动时间日志不包含前段启动或END后的UART排空;用L02方法捕获全周期面积并记录板端电压。功能串口与独立供电测流是不同条件,不把一份CSV的电流塞进另一份USB条件。

参考:[1] IDF5.5.5 S3 Sleep Modes/定时与RTC内存[2] 固定sleep API与返回原因

06 / 睡眠节点实战

先完成一个真实周期,再累计30次

下载进入工程,以现有IDF v5.5.5构建并刷USB-to-UART端口。第一轮上电NEW不是一次成功timer wake;它只证明开始了一次会话。

  1. 保存首次NEW、cycle1、实际VALID或ERROR及END日志,不删启动信息。
  2. 保持电源,后续记录应是DEEPSLEEP/TIMER、CONTINUED且cycle相邻;本轮VALID才能记valid=1。
  3. 保存至少30条后续连续成功timer记录;转录cycles.csv时run_id一致,reset/wake按原日志解释填写DEEPSLEEP/TIMER。
  4. 运行verify_cycles.py;缺行、错误轮、普通重启必须使这一连续段不通过。修复后另开一段,不从累计success挑30条。
  5. RESET、完全断电后上电分别确认NEW;另按L02至少3完整测流周期,活动与电流两维记录清楚。
代码 / 配置
idf.py --version
idf.py -B build -D IDF_TARGET=esp32s3 build
idf.py -B build -p PORT flash monitor
做到这里,你应该看到
30次是待执行硬件验收,不因为下面的30次合成循环而勾选。

参考:[1] IDF5.5.5 S3 Sleep Modes/定时与RTC内存[3] 固定reset原因枚举

07 / 睡眠节点实战

验收脚本只检查记录结构,不创造实测

脚本拒绝空表、混会话、不在1..UINT32_MAX内或非相邻cycle、非DEEPSLEEP/TIMER、valid非1和缺活动时间。它只能检验输入是否满足声明条件,不能证明串口记录真实、仪器已测或芯片确实睡过。

tests中的30条标SYNTHETIC,CRC/RTC同源测试也是纯内存输入;无I²C或RTC硬件仿真。实际cycles.csv和observations.csv保持空白。

代码 / 配置
# ---- tools/verify_cycles.py ----
# SPDX-License-Identifier: MIT
import argparse,csv

def verify(rows,required):
    if len(rows)<required:raise ValueError('not enough actual rows')
    run=rows[0]['run_id']
    if not run:raise ValueError('run_id required')
    previous=None
    for row in rows:
        if row['run_id']!=run:raise ValueError('mixed sessions')
        n=int(row['cycle'])
        if not 1<=n<=0xFFFFFFFF:raise ValueError('cycle must be firmware uint32 range 1..4294967295')
        if previous is not None and n!=previous+1:raise ValueError('nonconsecutive cycle')
        previous=n
        if row['reset']!='DEEPSLEEP' or row['wake']!='TIMER' or row['valid']!='1':raise ValueError('not a successful timer wake')
        if int(row['active_app_us'])<=0:raise ValueError('actual active time required')
    return len(rows)

def main():
    ap=argparse.ArgumentParser();ap.add_argument('csv');ap.add_argument('--required',type=int,default=30);a=ap.parse_args()
    if a.required<1:ap.error('required >=1')
    try:
        with open(a.csv,newline='') as f:n=verify(list(csv.DictReader(f)),a.required)
    except (KeyError,ValueError) as e:ap.error(str(e))
    print('PASS input records:',n,'consecutive valid TIMER/DEEPSLEEP; authenticity and current measurement require original logs')
if __name__=='__main__':main()
08 / 睡眠节点实战

让断线、松开和恢复也走完一个周期

断电预装SDA串联SPST;先读一轮有效,再只操作信号开关断开,下一轮应ERROR/NA但仍睡;闭合后续周期应重新有效,记录实际错误与恢复周期。不要在带电时挪VIN/GND或杜邦线。

每轮NEW先看reset/brownout,排供电/RESET线/RTC校验;每轮ERROR先回S01确认0x44/脚位/CRC,再读实际I²C错误,不靠保留上次值掩盖。没有串口时用供电波形与桥接供电条件区分,不能直接断言芯片没醒。

恢复重刷回单USB,BOOT+RESET进下载。不要改eFuse或关欠压检测,也不先擦Flash。若测得周期不稳定,报告实际范围和RTC时钟条件,不补一张理想值表。

参考:[1] IDF5.5.5 S3 Sleep Modes/定时与RTC内存[4] SHT4x v6.4 §4:测量/转换/CRC/时间[5] PID4885模块脚位

09 / 睡眠节点实战

从定时醒来,进入事件醒来

本篇完成的是有原因、有周期、有本轮有效性的节点。下一篇保留这种启动证据,但把RTC timer换成有释放协议的外部电平;测流记录仍沿L02边界。

BACK TO THE SOURCE

资料来源与版本说明

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

  1. IDF5.5.5 S3 Sleep Modes/定时与RTC内存
  2. 固定sleep API与返回原因
  3. 固定reset原因枚举
  4. SHT4x v6.4 §4:测量/转换/CRC/时间
  5. PID4885模块脚位
  6. IDF5.5.5原生I²C主机
  7. DevKitC-1 v1.1引脚
  8. IDF固定RTC_DATA_ATTR定义