低功耗与电池 / 低功耗实验方法发布 内容更新

明明 Deep-sleep 却耗电很高?先量整块开发板

锁定5V整板边界、仪器量程与USB旁路,给出板/传感器/屏幕三组固件、空记录和可拒绝缺样的电量积分工具;无本站电流结果。

读完能做什么
锁定5V整板边界、仪器量程与USB旁路,给出板/传感器/屏幕三组固件、空记录和可拒绝缺样的电量积分工具;无本站电流结果。
开始之前
已完成所列前置,能按具体板级引脚断电接线、构建原生IDF工程并保存串口日志。
所需器材
官方ESP32-S3-DevKitC-1 v1.1 / ESP32-S3-WROOM-1-N8R8,8MB QIO Flash / 8MB OPI PSRAM;UART0桥接115200(GPIO43/44)。、Nordic PPK2,Source Meter 5.0V、DUT≤600mA;高阻电压表,必要时满足带宽的示波器、可选PID4885 SHT40-AD1B 0x44、PID326 2020-08后SSD1306 128×64 I²C 0x3D,可靠焊接排针、短线≤20cm,无电池/其他电源;Python3标准库积分工具
版本组合
参考板:官方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;无外部组件依赖测流仪器范围:Nordic PPK2;Source Meter最大600mA / 0.8–5.0V,本文5.0V;手册修订历史2025-05,读取2026-10-18模块:PID4885 SHT40-AD1B / PID326 SSD1306 128×64,2020后自动复位I²C默认修订
适用范围

官方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#l02版本: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对象:10组合成积分边界及实际Python CLI;不是DUT电流数据验证日期:
本站实板待验证
没有实板、PPK2、电流/连续唤醒/门磁/无线实测;实际观察CSV保持仅表头。

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

01 / 低功耗实验方法

先确认你量的是芯片、模组还是整板

Deep-sleep API只控制芯片相关域;5V排针上的电流还经过LDO,板上常亮灯、USB桥接相关电路、Flash/PSRAM模组和外设支路也在测量范围。拿裸芯片典型值要求整块DevKit一样,是边界不一致。

本篇不拆板、不短接LDO、不靠切铜获得一个好看的最低值。完成物是可复现的5V整板实验、三组完整固件和空表;“很高”的判断必须来自你自己的仪器原始记录,本站没有读数。

参考:[1] DevKitC-1 v1.1供电互斥与脚位[2] DevKit v1.1原理图20221130:LDO/LED/桥接/模组[7] IDF5.5.5 S3 Sleep Modes

02 / 低功耗实验方法

PPK2的供电模式不能直接套1A测量范围

固定主路线是PPK2 Source Meter,设置5.0V。Nordic最大DUT表分别写Source Meter600mA、Ampere Meter连续1A;后者需要不同外部供电路线,本文不把二者混用。无无线也要先看启动峰/削顶/掉压,超出600mA则停止并换适配仪器,不能继续据削顶曲线声称峰值。

资料给出自动五量程与100kS/s,但采样速度、分辨率、零偏与准确度不同。记录仪器序列号/固件/Power Profiler实际应用版本及采样率,本站没有接仪器或安装应用,不预填版本。负担电压和线阻会改变DUT的供电点,必须同时核板端5V/3V3。

若另用万用表串联,断电后从有保险高电流量程开始;µA档可能导致压降/复位,电流挡不可并到电源。动态压降和更短尖峰需要合适示波器/探头另测,普通慢速电压读数不能排除所有瞬态。

参考:[3] PPK2 Source Meter接线[4] PPK2最大DUT电流:Source600mA/Ampere1A[5] PPK2供电/400mA以上额外USB供电[6] PPK2量程与分辨率

03 / 低功耗实验方法

把“烧录”和“测流”做成两套互斥条件

功能日志阶段只用开发板USB-to-UART;PPK2输出和其他外部电源全断。测流阶段开发板两USB全拔,且无供电调试器/串口、3V3、电池或独立屏幕电源。PPK2自身USB接电脑用于仪器供电/通信,这条线不是DUT旁路。

  1. USB单电源烧录所选变体,先确认wake/外围正常,记源SHA与配置;退出monitor。
  2. 断掉全部DUT电源;关闭PPK2输出时VOUT→J1-21/5V,GND→J1-22,VIN留空。
  3. 应用选择Source Meter5.0V,复核极性和空闲电压后开输出;实际可能>400mA时给PPK2 POWER ONLY额外≥1A USB供电,DUT仍不得越600mA。
  4. 从上电捕获,再至少保存3完整启动/活动/30s睡眠周期;另截稳定睡眠区,确认无过载/RESET/削顶。
  5. 需要补日志或换模块时先关输出断电,再回USB单电源;不边测边插USB。
做到这里,你应该看到
测点上的每条供电路径都画得出;若存在旁路,本次记录不能作为整板基线。

参考:[1] DevKitC-1 v1.1供电互斥与脚位[3] PPK2 Source Meter接线[4] PPK2最大DUT电流:Source600mA/Ampere1A[5] PPK2供电/400mA以上额外USB供电

04 / 低功耗实验方法

三组固件把外围状态说成可检查条件

A断开全部模块,HX_LOAD=0;B加PID4885且测一次SHT40;C再加指定PID326,初始化、写固定棋盘、亮1秒,随后AE关显示+8D10关电荷泵再深睡。熄屏不是电源门控,不能把仍接VIN的模块算成已断电。

每组保留相同3秒活动平台和1秒等待,传感器/图案传输额外时间实际计入,不承诺总周期完全相同。初始化或关闭失败会留醒诊断,此时不产生有效睡眠基线。ACK/传输成功也不证明光学显示,图案须目视记录。

连接PID4885PID326(2020后I²C默认版)
J1-1 3V3 / J1-22 GNDVIN / GNDVIN / GND
GPIO8 J1-12 / GPIO9 J1-15SDA / SCL,0x44Data / Clk,0x3D
留空3Vo输出RST(自动复位);不改SPI跳线
总线条件短线100kHz,模块已有10k上拉两模块上拉并联;不再加未知电阻
代码 / 配置
idf.py -B build-board -D SDKCONFIG=sdkconfig.board -D 'SDKCONFIG_DEFAULTS=sdkconfig.defaults;config/board.defaults' -D IDF_TARGET=esp32s3 build
idf.py -B build-sensor -D SDKCONFIG=sdkconfig.sensor -D 'SDKCONFIG_DEFAULTS=sdkconfig.defaults;config/sensor.defaults' -D IDF_TARGET=esp32s3 build
idf.py -B build-oled -D SDKCONFIG=sdkconfig.oled -D 'SDKCONFIG_DEFAULTS=sdkconfig.defaults;config/oled.defaults' -D IDF_TARGET=esp32s3 build

参考:[8] SHT4x v6.4:FD/10ms/CRC/公式[9] PID4885模块脚位/上拉[10] PID326修订/供电/地址[11] SSD1306 Rev1.1及附加charge pump应用说明[12] 固定Adafruit SSD1306 2.5.17初始化作为交叉核对(非工程依赖)

05 / 低功耗实验方法

完整测量固件与原生I²C辅助文件

不用Arduino显示/传感器库。原生事务的每步错误会被检查;SHT40每轮独立双CRC,不用上次读数补缺失。SSD1306为128×64 RAM共1024B,本例用129B数据包重复写八页棋盘,不分配通用绘图帧缓冲。

每次睡眠请求30秒;active_app_us从app_main进入起算,到日志前结束,不含ROM/bootloader或后续串口排空。因此周期耗电不能简单用这个软件时间代替仪器波形。

VCOMH取原厂Rev1.1命令表明确的DB 20复位级别;固定Arduino参考中的DB 40保留值未沿用。末附charge pump AppNote Rev0.4另核8D14/8D10,旧构建保留历史,当前三个固件均已用DB20重编。

代码 / 配置
// ---- 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/main.cpp ----
// SPDX-License-Identifier: MIT
#include <cstdio>
#include <inttypes.h>
#include "sdkconfig.h"
#include "esp_sleep.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "driver/uart.h"
#include "SensorBus.h"
static esp_err_t command(i2c_master_dev_handle_t dev,const uint8_t* c,size_t n){return i2c_master_transmit(dev,c,n,200);}
static esp_err_t oledPattern(i2c_master_dev_handle_t dev){
 // SSD1306 128x64, internal charge pump; 0x00 command stream, 0x40 data stream.
 const uint8_t init[]={0x00,0xae,0xd5,0x80,0xa8,0x3f,0xd3,0x00,0x40,0x8d,0x14,0x20,0x00,0xa1,0xc8,0xda,0x12,0x81,0x7f,0xd9,0xf1,0xdb,0x20,0xa4,0xa6,0x2e,0x21,0,127,0x22,0,7};
 esp_err_t e=command(dev,init,sizeof init);if(e!=ESP_OK)return e;
 uint8_t data[129];data[0]=0x40;for(int x=1;x<=128;++x)data[x]=(x&1)?0x55:0xaa;
 for(int page=0;page<8&&e==ESP_OK;++page)e=command(dev,data,sizeof data);
 if(e==ESP_OK){const uint8_t on[]={0x00,0xaf};e=command(dev,on,sizeof on);}return e;
}
extern "C" void app_main(){
 int64_t start=esp_timer_get_time();printf("L02 load=%d reset=%d wake=%d; no radio; board 5V boundary\n",CONFIG_HX_LOAD,int(esp_reset_reason()),int(esp_sleep_get_wakeup_cause()));
 vTaskDelay(pdMS_TO_TICKS(3000)); // same explicit active plateau for all three cases
 i2c_master_bus_handle_t bus=nullptr;i2c_master_dev_handle_t sensor=nullptr,oled=nullptr;bool ready=true;
 if(CONFIG_HX_LOAD>0){
  esp_err_t e=createBus(&bus);if(e==ESP_OK)e=addDevice(bus,0x44,&sensor);
  ShtSample sample;if(e==ESP_OK)e=takeSample(sensor,sample);
  ready=e==ESP_OK&&sample.valid;printf("SHT api=%s valid=%u; no stale value\n",esp_err_to_name(e),sample.valid);
  if(CONFIG_HX_LOAD==2){
   e=bus?addDevice(bus,0x3d,&oled):ESP_ERR_INVALID_STATE;
   if(e==ESP_OK)e=oledPattern(oled);
   ready=ready&&e==ESP_OK;printf("OLED pattern transfer=%s; visual result requires observation\n",esp_err_to_name(e));
   vTaskDelay(pdMS_TO_TICKS(1000));
   // Controlled sleep condition: display OFF and charge pump OFF, still powered.
   const uint8_t off[]={0x00,0xae,0x8d,0x10};esp_err_t offResult=oled?command(oled,off,sizeof off):ESP_ERR_INVALID_STATE;
   ready=ready&&offResult==ESP_OK;printf("OLED off/pump-off transfer=%s; not supply isolation\n",esp_err_to_name(offResult));
  }else vTaskDelay(pdMS_TO_TICKS(1000));
 }else vTaskDelay(pdMS_TO_TICKS(1000));
 if(oled)ESP_ERROR_CHECK(i2c_master_bus_rm_device(oled));
 if(sensor)ESP_ERROR_CHECK(i2c_master_bus_rm_device(sensor));
 if(bus)ESP_ERROR_CHECK(i2c_del_master_bus(bus));
 if(!ready){puts("INVALID comparison: peripheral failure; remain awake for diagnosis, no valid sleep-current baseline");return;}
 ESP_ERROR_CHECK(esp_sleep_enable_timer_wakeup(30000000ULL));
 printf("SLEEP_REQUEST active_app_us=%" PRId64 " timer_us=30000000; boot time excluded, current unmeasured\n",esp_timer_get_time()-start);fflush(stdout);ESP_ERROR_CHECK(uart_wait_tx_idle_polling(UART_NUM_0));esp_deep_sleep_start();
}

参考:[7] IDF5.5.5 S3 Sleep Modes[8] SHT4x v6.4:FD/10ms/CRC/公式[11] SSD1306 Rev1.1及附加charge pump应用说明

06 / 低功耗实验方法

最低平台、活动峰和周期面积分别留证据

对同板同仪器/电压/线材/环境各测A/B/C至少3周期。记录稳定睡眠区均值与离散程度、采样带宽内最大值、活动区与全周期电量;最开始上电与常规定时唤醒分开。C−B是该组合在该条件的增量,不是独立裸屏手册值。

measurement-runs.csv保存板修订、负载、仪器实际版本、供电测点和原始文件路径;samples.csv是规范化空表,均没有本站读数。PPK2应用原工程和原CSV一起保存,先检查实际列名/单位,别把我们的time_s列当作仪器原生格式。

参考:[3] PPK2 Source Meter接线[6] PPK2量程与分辨率

07 / 低功耗实验方法

用原始时间积分,拒绝跨越丢失的峰值

normalize_csv.py要求显式列名/时间单位/电流单位与自己测得的板端电压;不猜PPK2表头。voltage-v是恒压假设,电压变化较大时应提供逐样电压列,所得能量不得声称有未测电压波形精度。

积分以相邻样点梯形计算mC与mJ,分别除3600和3600000得到mAh/Wh。重复/倒序时间、NaN、负电流、非正电压、空表与过大的缺样都拒绝,不能放大max-gap来跨过没记录的启动峰。输出sampled_peak只表示采样带宽内的最大值。

代码 / 配置
# ---- tools/integrate.py ----
# SPDX-License-Identifier: MIT
"""Integrate normalized samples. Outputs are calculations, never fabricated measurements."""
import argparse,csv,json,math
from pathlib import Path

def integrate(rows,max_gap):
    samples=[]
    for row in rows:
        t,i,v=(float(row[k]) for k in ('time_s','current_mA','voltage_V'))
        if not all(math.isfinite(x) for x in (t,i,v)) or t<0 or i<0 or v<=0:
            raise ValueError('finite t>=0, current>=0, voltage>0 required; inspect offset rather than silently clipping')
        if samples and t<=samples[-1][0]:raise ValueError('time must strictly increase')
        samples.append((t,i,v))
    if len(samples)<2:raise ValueError('at least two real samples required; empty template is not a result')
    charge=energy=0.0;gap_max=0.0
    for (t0,i0,v0),(t1,i1,v1) in zip(samples,samples[1:]):
        dt=t1-t0
        if dt>max_gap:raise ValueError('sample gap exceeds explicit --max-gap-s; do not bridge missing data')
        gap_max=max(gap_max,dt);charge+=(i0+i1)*0.5*dt;energy+=(i0*v0+i1*v1)*0.5*dt
    seconds=samples[-1][0]-samples[0][0]
    return dict(duration_s=seconds,charge_mC=charge,charge_mAh=charge/3600,energy_mJ=energy,energy_Wh=energy/3600000,average_mA=charge/seconds,sampled_peak_mA=max(s[1] for s in samples),max_gap_s=gap_max,samples=len(samples),method='trapezoids over actual timestamps; sampled peak is bandwidth-limited')

def main():
    ap=argparse.ArgumentParser();ap.add_argument('csv');ap.add_argument('--max-gap-s',type=float,required=True);a=ap.parse_args()
    if not math.isfinite(a.max_gap_s) or a.max_gap_s<=0:ap.error('positive finite max-gap-s required')
    try:
        with open(a.csv,newline='',encoding='utf-8-sig') as f:r=integrate(csv.DictReader(f),a.max_gap_s)
    except (ValueError,KeyError) as e:ap.error(str(e))
    r['input']=str(Path(a.csv));print(json.dumps(r,ensure_ascii=False,indent=2))
if __name__=='__main__':main()

参考:[6] PPK2量程与分辨率

08 / 低功耗实验方法

合成数据检验单位,不填进实测栏

tests/fixtures/SYNTHETIC-constant.csv的人为2mA、2s、5V仅用于检验4mC/20mJ,10组合成积分边界检查不是开发板电流。实测样本缺失时脚本拒绝出结果,不能拿合成文件给本板算续航。

必须先记录准确导出列名再运行normalizer;--max-gap-s按采样间隔和允许丢样设置。5V整板mAh不能直接除3.7V电池容量,转换效率和电池可用能量/温度/负载均需测量。

代码 / 配置
python3 tools/normalize_csv.py raw.csv samples-normalized.csv --time-column "实际时间列名" --time-unit us --current-column "实际电流列名" --current-unit uA --voltage-v 实测板端电压
python3 tools/integrate.py samples-normalized.csv --max-gap-s 0.001
python3 tests/integration_test.py
python3 tools/integrate.py tests/fixtures/SYNTHETIC-constant.csv --max-gap-s 1
做到这里,你应该看到
实测原表仍空;只有合成夹具能得到固定演算值,仪器记录另存且带条件。
09 / 低功耗实验方法

没有低平台,先找证据,不先拆灯换电阻

先看外围失败是否停醒、是否wrong变体、USB/调试器旁路、复位/欠压、屏是否关闭,再看量程压降和真正睡眠窗口。PPK2触顶、板端掉压或重复RESET标为无效,先恢复够用的供电;不关闭欠压检测来获得“稳定曲线”。

若日志与测流两种条件不同,报告中明确分行。保留整个原始波形,最低点不能代表平均电流,一张自动缩放截图也不能证明捕获了全部峰。记录完回单USB、BOOT+RESET可重刷,不操作eFuse。

参考:[1] DevKitC-1 v1.1供电互斥与脚位[2] DevKit v1.1原理图20221130:LDO/LED/桥接/模组[4] PPK2最大DUT电流:Source600mA/Ampere1A[7] IDF5.5.5 S3 Sleep Modes

BACK TO THE SOURCE

资料来源与版本说明

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

  1. DevKitC-1 v1.1供电互斥与脚位
  2. DevKit v1.1原理图20221130:LDO/LED/桥接/模组
  3. PPK2 Source Meter接线
  4. PPK2最大DUT电流:Source600mA/Ampere1A
  5. PPK2供电/400mA以上额外USB供电
  6. PPK2量程与分辨率
  7. IDF5.5.5 S3 Sleep Modes
  8. SHT4x v6.4:FD/10ms/CRC/公式
  9. PID4885模块脚位/上拉
  10. PID326修订/供电/地址
  11. SSD1306 Rev1.1及附加charge pump应用说明
  12. 固定Adafruit SSD1306 2.5.17初始化作为交叉核对(非工程依赖)