主控睡下了,电源路径却未必断开
L02量到的是整板和外设的总和;L03让主控深睡,仍不能说明屏幕稳压器、上拉、传感器和电容没有耗电。本篇把问题缩到一个能复验的动作:先给SHT40上电读一次并关闭,再给OLED上电显示同一份值3秒,最后两路都请求关闭、主控请求深睡60秒。屏幕不是持续显示的看板,3秒后熄灭是明确功能取舍。
完成物是独立原生IDF工程、两路电源/两组信号接线、软件关断轨迹、空电压与电量表,以及常供电对照方法。没有实板时能检查代码与构建,不能把REQUEST OFF打印当测到0V,也不能先写一个节能百分比。
- 做到这里,你应该看到
- 你能沿着VIN、VOUT、信号与地分别追踪两组外设,而非只找供电正线。
开关型号、QOD和默认态分别确认
本例指定两块TI TPS22919EVM/PSIL050,器件TPS22919DCKR;3.3V在其1.6–5.5V输入范围内,ON的高低阈值按SLVSEN5B电气表核。最大1.5A是开关额定,不能据此要求DevKit的3V3输出1.5A;本例负载与浪涌仍须检查供电压降。TPS22919未提供本例可依赖的反向电流阻断保障,不能把“有负载开关”写成杜绝任何反供。
两块JP1跳帽均移除,用TP2接受GPIO控制;每块另接100kΩ ON到GND,保证ROM/复位/深睡前后的默认关断路径。控制高时该下拉约消耗33µA,这是3.3V/100k的计算,须算进总能量。内部Smart Pull Down不替代外部默认态核验。只装JP2使QOD直连VOUT,JP3/JP4留空;内部24Ω是典型值,放电时间还取决于负载电容,100ms等待不是测量结果。
SLVUBH0中JP3段落的300Ω与图/BOM的2kΩ不一致,本例不使用JP3,也不靠该数值外推下降时间。不要另从USB/STEMMA给VOUT一侧供电,否则既可能绕过开关,又可能经过器件内部路径返回VIN。
参考:[1] TI TPS22919 SLVSEN5B:ON/QOD/电气表与功能[2] TPS22919EVM SLVUBH0:PSIL050图1及§4.1端子/跳帽
让每组上拉跟着自己的电源走
SHT40的VIN接EVM S的VOUT,GPIO8/9分别接SDA/SCL;OLED的VIN接EVM O的VOUT,GPIO17/18分别接SDA/SCL。两组分别用I2C0/I2C1,绝不相连。所有GND保持连通;SHT的3V输出、OLED的RST及其它SPI焊盘留空。PID326限定2020后自动复位的128x64默认I2C/0x3D修订,旧模块需要重新核对复位路线。
排针必须已可靠焊接。模块已有上拉随其VIN及板载电源关断,不再加到常供3V3的上拉;检查任何电平转换板、调试器、其它I2C器件和接线仪表是否另有供电路径。仅把主控GPIO设输入还不够:内部上拉仍可能把关断外设拉高,程序明确禁止输入输出驱动并设GPIO_FLOATING关闭内部上下拉。浮空只描述驱动配置,并不承诺信号电压一定为0。
| 信号 | SHT40域 | OLED域 |
|---|---|---|
| 3V3/GND | EVM S J1 VIN/GND | EVM O J1 VIN/GND |
| 控制 | GPIO5→TP2,另100k到GND | GPIO6→TP2,另100k到GND |
| 下游电源 | EVM S J2→PID4885 VIN/GND | EVM O J2→PID326 VIN/GND |
| I2C | GPIO8 SDA / 9 SCL,0x44 | GPIO17 SDA / 18 SCL,0x3D |
参考:[2] TPS22919EVM SLVUBH0:PSIL050图1及§4.1端子/跳帽[6] PID326修订与I2C接线[7] DevKitC-1 v1.1引脚表
先完成事务,再删除句柄,最后拉低ON
源码用同一个PowerSequence策略组织正常与失败路径:SHT上电后等20ms,创建总线/设备,FD转换等10ms再收6字节验双CRC;不复用关电前的I2C句柄。20ms考虑裸芯片达到最低电压后最大1ms启动及板级开关/稳压余量,实际VOUT上升仍要测。
所有事务同步完成后先移设备,再删总线;随后禁止GPIO输入输出并关闭内部上下拉,才把EN5拉低、等100ms。这里故意不只调用gpio_reset_pin:固定IDF实现会为有效输出脚打开上拉。然后EN6高、等100ms、重建I2C1,重新送完整SSD1306命令与1024字节帧。OLED行顶整数单位0.1°C,底整数单位0.1%RH,一条/两条左标记区分;例如231表示23.1°C,是取整显示。
3秒观察窗后先发AE和8D10关显示/电荷泵,再同样移设备、删总线、释放17/18、关EN6。任何启动/CRC/清理错误都请求相应关断并停留FAULT awake;它不是有效省电周期。主机8条策略路径能证明调用顺序,不证明GPIO、电源或画面。
参考:[3] IDF5.5.5 GPIO:DISABLE/FLOATING实现[4] IDF5.5.5 I2C:设备和总线生命周期[5] SHT4x固定手册:启动、FD转换与CRC
编译与首次操作先把外设电源断开
下载是完整CMake/main/头文件/版本锁/许可,固定IDF5.5.5,S3 DIO80MHz 8MB、PSRAM关闭、桥接UART115200。激活这个版本,在解压目录执行idf.py set-target esp32s3、idf.py build,再用自己的PORT运行idf.py -p PORT flash monitor。上传时先断开两EVM VIN;已确认新程序后整机断电,再按表恢复接线。
首次不接负载先测两EVM的TP2/TP5/TP6;核默认OFF、控制变化与QOD下降,再分别接SHT、OLED。预期日志是请求上电→INIT→SAMPLE valid→HIZ/OFF→OLED传输→HIZ/OFF→SLEEP,实际图像要肉眼核对。深睡计时请求60秒,完整周期还含ROM、启动、显示和串口清空,不称精确每分钟。
- 做到这里,你应该看到
- 软件请求轨迹、TP6实际电压和肉眼画面三类证据各自有记录;没有其中一项就留待测。
原样主程序与同源顺序策略
下面文件与下载逐字一致。PowerSequence负责控制顺序,Hardware负责实际GPIO/I2C及清理。ShtSample.h、完整构建配置与主机fixture在下载目录,温湿度转换和CRC沿用L03固定实现。原生SSD1306缓冲仅1024字节,不需要图形库或PSRAM。
// ---- main/PowerSequence.h ----
// SPDX-License-Identifier: MIT
#pragma once
// This exact policy is used by the firmware and host fixture. IO cleanup must
// release bus/device and float both signal pads before requesting rail OFF.
template<class IO> bool runPowerCycle(IO& io){
bool ok=io.startSensor();if(ok)ok=io.readSensor();
bool closed=io.stopSensor();ok=ok&&closed;
if(!ok)return false;
ok=io.startDisplay();if(ok)ok=io.drawDisplay();
if(ok)io.viewWindow();
bool slept=io.displayOff();bool released=io.stopDisplay();
return ok&&slept&&released;
}
// ---- main/main.cpp ----
// SPDX-License-Identifier: MIT
#include <cstdio>
#include <cstring>
#include <initializer_list>
#include <inttypes.h>
#include "esp_sleep.h"
#include "esp_timer.h"
#include "driver/gpio.h"
#include "driver/i2c_master.h"
#include "driver/uart.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "ShtSample.h"
#include "PowerSequence.h"
struct Channel {gpio_num_t en,sda,scl;int port;uint8_t address;int waitMs;i2c_master_bus_handle_t bus=nullptr;i2c_master_dev_handle_t dev=nullptr;};
static void waitMs(int ms){vTaskDelay(pdMS_TO_TICKS(ms));}
static void floatPad(gpio_num_t pin){ESP_ERROR_CHECK(gpio_set_direction(pin,GPIO_MODE_DISABLE));ESP_ERROR_CHECK(gpio_set_pull_mode(pin,GPIO_FLOATING));}
static void offPads(Channel& c){floatPad(c.sda);floatPad(c.scl);}
static bool start(Channel& c){
ESP_ERROR_CHECK(gpio_set_level(c.en,1));printf("REQUEST ON EN%d; wait_ms=%d (measure TP6; no rail feedback)\n",int(c.en),c.waitMs);waitMs(c.waitMs);
i2c_master_bus_config_t b{};b.i2c_port=c.port;b.sda_io_num=c.sda;b.scl_io_num=c.scl;b.clk_source=I2C_CLK_SRC_DEFAULT;b.glitch_ignore_cnt=7;b.flags.enable_internal_pullup=false;
esp_err_t e=i2c_new_master_bus(&b,&c.bus);i2c_device_config_t d{};d.device_address=c.address;d.dev_addr_length=I2C_ADDR_BIT_LEN_7;d.scl_speed_hz=100000;
if(e==ESP_OK){e=i2c_master_bus_add_device(c.bus,&d,&c.dev);}
printf("INIT EN%d api=%s\n",int(c.en),esp_err_to_name(e));return e==ESP_OK;
}
static bool stop(Channel& c){
bool ok=true;if(c.dev){auto e=i2c_master_bus_rm_device(c.dev);ok=e==ESP_OK;if(ok)c.dev=nullptr;}
if(c.bus){auto e=i2c_del_master_bus(c.bus);ok=(e==ESP_OK)&&ok;if(e==ESP_OK)c.bus=nullptr;}
// All requests above are synchronous, no outstanding transactions/callbacks.
// Even driver cleanup failure must not leave pads driving a powered-off module.
offPads(c);ESP_ERROR_CHECK(gpio_set_level(c.en,0));printf("REQUEST HIZ SDA%d/SCL%d -> OFF EN%d; discharge_wait_ms=100\n",int(c.sda),int(c.scl),int(c.en));waitMs(100);return ok;
}
static esp_err_t tx(Channel& c,const uint8_t* p,size_t n){return c.dev?i2c_master_transmit(c.dev,p,n,200):ESP_ERR_INVALID_STATE;}
// Native SSD1306, 128x64, I2C=0x3D, horizontal addressing, 1024-byte framebuffer.
static uint8_t frame[1024];
static void pixel(int x,int y){if(x>=0&&x<128&&y>=0&&y<64)frame[x+(y/8)*128]|=uint8_t(1u<<(y&7));}
static void number(int value,int x,int y){
static const uint8_t digits[10][5]={{0x3e,0x51,0x49,0x45,0x3e},{0,0x42,0x7f,0x40,0},{0x42,0x61,0x51,0x49,0x46},{0x21,0x41,0x45,0x4b,0x31},{0x18,0x14,0x12,0x7f,0x10},{0x27,0x45,0x45,0x45,0x39},{0x3c,0x4a,0x49,0x49,0x30},{1,0x71,9,5,3},{0x36,0x49,0x49,0x49,0x36},{6,0x49,0x49,0x29,0x1e}};
char text[16];snprintf(text,sizeof text,"%d",value);for(const char* p=text;*p;++p,x+=12)for(int col=0;col<5;col++)for(int row=0;row<7;row++)if((*p=='-'?row==3:digits[*p-'0'][col]&(1u<<row)))for(int dx=0;dx<2;dx++)for(int dy=0;dy<2;dy++)pixel(x+col*2+dx,y+row*2+dy);
}
struct Hardware {
Channel sensor{GPIO_NUM_5,GPIO_NUM_8,GPIO_NUM_9,0,0x44,20},display{GPIO_NUM_6,GPIO_NUM_17,GPIO_NUM_18,1,0x3d,100};ShtSample sample{};
bool startSensor(){return start(sensor);}bool stopSensor(){return stop(sensor);}bool startDisplay(){return start(display);}bool stopDisplay(){return stop(display);}
bool readSensor(){uint8_t cmd=0xfd,b[6];esp_err_t e=tx(sensor,&cmd,1);if(e==ESP_OK){waitMs(10);e=i2c_master_receive(sensor.dev,b,6,200);}if(e==ESP_OK)sample=decodeSht(b);printf("SAMPLE api=%s valid=%u t_mC=%d rh_mPct=%d\n",esp_err_to_name(e),sample.valid,sample.t_mC,sample.rh_mPct);return e==ESP_OK&&sample.valid;}
bool drawDisplay(){
const uint8_t init[]={0,0xae,0xd5,0x80,0xa8,63,0xd3,0,0x40,0x8d,0x14,0x20,0,0xa1,0xc8,0xda,0x12,0x81,0x7f,0xd9,0xf1,0xdb,0x20,0xa4,0xa6,0x2e,0x21,0,127,0x22,0,7};
esp_err_t e=tx(display,init,sizeof init);memset(frame,0,sizeof frame);number(sample.t_mC/100,12,8);number(sample.rh_mPct/100,12,36);
// Top integer is T in 0.1 C, bottom is RH in 0.1%; row markers distinguish them.
for(int x=0;x<5;x++){pixel(x,8);pixel(x,36);pixel(x,40);}
for(int p=0;p<8&&e==ESP_OK;p++){uint8_t b[129];b[0]=0x40;memcpy(b+1,frame+128*p,128);e=tx(display,b,sizeof b);}if(e==ESP_OK){const uint8_t on[]={0,0xaf};e=tx(display,on,sizeof on);}printf("OLED transfer=%s; visual result pending\n",esp_err_to_name(e));return e==ESP_OK;
}
void viewWindow(){waitMs(3000);}bool displayOff(){if(!display.dev)return false;const uint8_t off[]={0,0xae,0x8d,0x10};return tx(display,off,sizeof off)==ESP_OK;}
};
extern "C" void app_main(){
int64_t began=esp_timer_get_time();Hardware io;
// 100k external EN-to-GND per EVM is required during ROM/boot/deep sleep.
for(auto pin:{GPIO_NUM_5,GPIO_NUM_6}){ESP_ERROR_CHECK(gpio_set_level(pin,0));ESP_ERROR_CHECK(gpio_set_direction(pin,GPIO_MODE_OUTPUT));ESP_ERROR_CHECK(gpio_set_pull_mode(pin,GPIO_FLOATING));}
offPads(io.sensor);offPads(io.display);waitMs(2000);bool ok=runPowerCycle(io);
if(!ok){puts("FAULT: rails requested OFF; stay awake, verify VOUT/signals and reset after repair");return;}
ESP_ERROR_CHECK(esp_sleep_enable_timer_wakeup(60000000ULL));printf("SLEEP active_app_us=%" PRId64 " request_us=60000000; current/rail voltage unmeasured\n",esp_timer_get_time()-began);fflush(stdout);ESP_ERROR_CHECK(uart_wait_tx_idle_polling(UART_NUM_0));esp_deep_sleep_start();
}
测残压与整周期电量,再决定是否值得关电
示波器记录ON→VOUT有效电压、VOUT下降、SDA/SCL与各自VOUT关系,写入observations.csv。20/100ms等待都需用真实模块验证;若关断后残压不降,先查常供上拉、第二供电、电平转换器和探头影响。SHT至少要实际穿过掉电条件才能保证下一次为冷启动,不能只看到ON低。不要故意把GPIO驱动高来演示反供。
常供电对照须整机断电,先拆掉GPIO5/6到TP2的线,再把每块JP1接ON到VIN(图中1–2)。不能同时保留GPIO线和高跳帽。用相同固件、同3秒画面和60秒睡眠请求;常供电组仍发软关屏命令,比较的是软休眠留电与实际切电。
按L02整板边界:PPK2 Source Meter5.0V供DevKit5V/GND,DUT总电流上限600mA,DUT两USB和其它供电旁路断开;仪表自己的USB仍接电脑。至少各30个有效完成周期,从同一边界积分到最后一个完整睡眠结束,包含浪涌、初始化、控制电阻、失败重试和睡眠。导出后显式映射时间/电流单位,用下载的normalize_csv.py和integrate.py计算Q与E;缺口上限按实际采样设定,不能跨丢失数据积分。两组结果都还空着,因此本篇没有本站电量收益数字。
参考:[1] TI TPS22919 SLVSEN5B:ON/QOD/电气表与功能[2] TPS22919EVM SLVUBH0:PSIL050图1及§4.1端子/跳帽
失败先查供电域,修好后再重新开始一组
INIT NACK先看TP6是否有效、地址/模块修订是否匹配,再看总线分组;CRC失败先看电源上升与等待、SDA/SCL完整性,不填假温湿度。OLED transfer=OK但空屏时查0x3D、自动RST修订、完整重初始化和实际VCC,而不是把API成功当图像证明。FAULT后两路请求OFF、主控故意不睡,修复后RESET重开记录。
若无法下载,先拔外设VIN,按住BOOT再按RESET进ROM恢复;不关欠压检测,不先擦全部Flash。保存原日志与接线配置,坏周期不混进节能平均。下一篇L06固定外设供电,比较每次联网与攒批上传,把不同机制的收益分别测。
资料来源与版本说明
逐项核固定源码、原厂电路与实际构建;电压、能量、无线、ADC和电池结果仍待物理实验。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。