先确认你量的是芯片、模组还是整板
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
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量程与分辨率
把“烧录”和“测流”做成两套互斥条件
功能日志阶段只用开发板USB-to-UART;PPK2输出和其他外部电源全断。测流阶段开发板两USB全拔,且无供电调试器/串口、3V3、电池或独立屏幕电源。PPK2自身USB接电脑用于仪器供电/通信,这条线不是DUT旁路。
- USB单电源烧录所选变体,先确认wake/外围正常,记源SHA与配置;退出monitor。
- 断掉全部DUT电源;关闭PPK2输出时VOUT→J1-21/5V,GND→J1-22,VIN留空。
- 应用选择Source Meter5.0V,复核极性和空闲电压后开输出;实际可能>400mA时给PPK2 POWER ONLY额外≥1A USB供电,DUT仍不得越600mA。
- 从上电捕获,再至少保存3完整启动/活动/30s睡眠周期;另截稳定睡眠区,确认无过载/RESET/削顶。
- 需要补日志或换模块时先关输出断电,再回USB单电源;不边测边插USB。
- 做到这里,你应该看到
- 测点上的每条供电路径都画得出;若存在旁路,本次记录不能作为整板基线。
参考:[1] DevKitC-1 v1.1供电互斥与脚位[3] PPK2 Source Meter接线[4] PPK2最大DUT电流:Source600mA/Ampere1A[5] PPK2供电/400mA以上额外USB供电
三组固件把外围状态说成可检查条件
A断开全部模块,HX_LOAD=0;B加PID4885且测一次SHT40;C再加指定PID326,初始化、写固定棋盘、亮1秒,随后AE关显示+8D10关电荷泵再深睡。熄屏不是电源门控,不能把仍接VIN的模块算成已断电。
每组保留相同3秒活动平台和1秒等待,传感器/图案传输额外时间实际计入,不承诺总周期完全相同。初始化或关闭失败会留醒诊断,此时不产生有效睡眠基线。ACK/传输成功也不证明光学显示,图案须目视记录。
| 连接 | PID4885 | PID326(2020后I²C默认版) |
|---|---|---|
| J1-1 3V3 / J1-22 GND | VIN / GND | VIN / GND |
| GPIO8 J1-12 / GPIO9 J1-15 | SDA / SCL,0x44 | Data / 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初始化作为交叉核对(非工程依赖)
完整测量固件与原生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应用说明
最低平台、活动峰和周期面积分别留证据
对同板同仪器/电压/线材/环境各测A/B/C至少3周期。记录稳定睡眠区均值与离散程度、采样带宽内最大值、活动区与全周期电量;最开始上电与常规定时唤醒分开。C−B是该组合在该条件的增量,不是独立裸屏手册值。
measurement-runs.csv保存板修订、负载、仪器实际版本、供电测点和原始文件路径;samples.csv是规范化空表,均没有本站读数。PPK2应用原工程和原CSV一起保存,先检查实际列名/单位,别把我们的time_s列当作仪器原生格式。
用原始时间积分,拒绝跨越丢失的峰值
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()
合成数据检验单位,不填进实测栏
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- 做到这里,你应该看到
- 实测原表仍空;只有合成夹具能得到固定演算值,仪器记录另存且带条件。
没有低平台,先找证据,不先拆灯换电阻
先看外围失败是否停醒、是否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
资料来源与版本说明
已核固定版本API与模块/仪器手册;实际构建和主机逻辑单独列证据,硬件仍待执行。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。