低功耗与电池 / 可复现实验与完整工程发布 内容更新

ESP32 要一直在线也能省电吗?试试自动 Light-sleep

保持同一UDP命令和实体按钮任务,对照只切换自动Light-sleep的两个固件,分别记录能量、主机RTT与掉线。

读完能做什么
保持同一UDP命令和实体按钮任务,对照只切换自动Light-sleep的两个固件,分别记录能量、主机RTT与掉线。
开始之前
完成所列前置,能激活固定IDF、断电核对板图、保存原始日志,并按仪器手册接线。
所需器材
官方ESP32-S3-DevKitC-1 v1.1 N8R8;QIO80/OPI PSRAM80。、Kingbright WP7113ID红LED、330Ω(或前篇1kΩ,两组一致)、10kΩ、常开按钮。、固定AP、受信任LAN内Python主机,PPK2 Source Meter5.0V/600mA及DMM。
版本组合
ESP-IDF:5.5.5 / b774170ff46c393eeb5e495ea37936038d3f4f4f工具链:GCC14.2.0 / esp-14.2.0_20260121;Python3.12.13两组差异:CMake L09_AUTOSLEEP=0/1;PM_ENABLE/TICKLESS/PM_PROFILING=y,HZ100,idle3tick主机:Python3.12.13 stdlib UDP / monotonic RTT
适用范围

IDF5.5.5 / S3 v1.1 N8R8。两个固件都DFS160/40、WIFI_PS_MIN_MODEM;只改自动Light-sleep请求。GPIO5在队列等待后轮询(空队列最多等100ms)而非GPIO唤醒。无实际AP关联、休眠占比、能量或GPIO实测。

这篇验证到了哪一步

资料核对已通过
docs/esp32-evidence/low-power-field.md#l09验证日期:
构建 / 配置已通过
docs/esp32-evidence/low-power-field-builds.json对象:实际固定目标构建Exit0;输入、生成配置、最终bin/ELF/map/CPP及原日志SHA可复查版本:ESP-IDF5.5.5验证日期:
仿真已通过
docs/esp32-evidence/low-power-field-hosts.json对象:同源逻辑与实际本机服务检查;不模拟射频、电气、电流或电池验证日期:
本站实板待验证
没有实板/仪器/无线/ADC/电池或七天运行,实际观察表仅表头。

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

01 / 可复现实验与完整工程

这次设备必须保持能接命令

L08可以工作后断开无线深睡;本篇任务是LAN中随时可查询/控制一盏灯,同时保留实体按钮。比较同板、同AP、同请求频率下,允许自动Light-sleep是否降低整板能量,以及主机往返与超时付出什么代价。没有手动esp_light_sleep_start,也没有“睡之前先断Wi-Fi”的替代路线。

两个构建都启用DFS160/40MHz及WIFI_PS_MIN_MODEM,基线仅light_sleep_enable=false,优化为true。因此基线不是“所有省电全关、CPU永远满速”。getter只验证配置已被接受,实际驱动锁、任务就绪、定时器可能使自动休眠很少甚至没有发生。

参考:[1] IDF5.5.5电源管理:锁、DFS、Tickless条件[2] IDF5.5.5 PM结构与getter[3] IDF5.5.5 Wi-Fi低功耗:MIN_MODEM/DTIM与自动睡眠[4] IDF5.5.5 Wi-Fi接口/监听间隔说明

02 / 可复现实验与完整工程

100ms轮询是这个服务自己的成本

CONFIG_PM_ENABLE与CONFIG_FREERTOS_USE_TICKLESS_IDLE均为y,FreeRTOS100Hz、idle阈值3tick;esp_pm_configure失败即报错,不静默退化。owner在空队列时最多阻塞100ms后轮询输入,来包可提前返回,UDP任务阻塞socket,心跳每60s,离线重连每10s,所有这些成本都留在工作负载中。

GPIO5稳定30ms才确认状态,但空队列时最多等100ms才观测一次,所以不是“30ms一定响应”。本例没有GPIO唤醒源,短脉冲可能完全漏掉;功能操作按住/松开各≥300ms,真正按钮延迟另测。MIN_MODEM按AP DTIM醒来;sta.listen_interval=3不替代DTIM。来包会提前结束队列等待,忙碌流量可减少空闲,100ms不是固定睡眠长度。AP beacon/DTIM、信道、固件、RSSI、客户端隔离和测试时流量都要记录。

做到这里,你应该看到
两个sdkconfig实际均核PM/Tickless条件,但这不是“已经联网并进入Light-sleep”的实测。

参考:[1] IDF5.5.5电源管理:锁、DFS、Tickless条件[3] IDF5.5.5 Wi-Fi低功耗:MIN_MODEM/DTIM与自动睡眠[4] IDF5.5.5 Wi-Fi接口/监听间隔说明[5] IDF5.5.5 FreeRTOS tickless条件

03 / 可复现实验与完整工程

先完成安全空配置和实体输入验收

GPIO4/J1-4→330Ω→WP7113ID阳极,阴极→J1-22 GND;沿用1kΩ也可但两组不改。GPIO5/J1-5经10kΩ到3V3/J1-1,常开按钮到GND,无内部上拉。LED的Vf1.9V是厂家10mA条件典型值,不是这块板当前实测。断电接线后用桥USB单供电下载,GPIO4不是板载RGB。

main/credentials.c默认空SSID/密码,打印CONFIG_DISABLED,按钮仍工作,没有联网成功。凭据函数独立C翻译单元,默认无LTO的真实固件仍编入网络路径;填写自有LAN凭据后重编,不把密码提交到日志或公开包。两个build目录及sdkconfig分开,避免旧缓存掩盖模式切换。

代码 / 配置
idf.py --version
idf.py -B build-auto -D IDF_TARGET=esp32s3 -D SDKCONFIG=sdkconfig.auto -D L09_AUTOSLEEP=1 build
idf.py -B build-auto -D SDKCONFIG=sdkconfig.auto -p YOUR_PORT flash monitor
idf.py -B build-baseline -D IDF_TARGET=esp32s3 -D SDKCONFIG=sdkconfig.baseline -D L09_AUTOSLEEP=0 build
idf.py -B build-baseline -D SDKCONFIG=sdkconfig.baseline -p YOUR_PORT flash monitor

参考:[6] DevKitC-1 v1.1排针[7] Kingbright WP7113ID原厂规格

04 / 可复现实验与完整工程

完整服务:状态只有一个拥有者

UDP任务只解析并入8格有界队列,owner按序修改GPIO锁存和去重模型;队列满会丢请求,客户端应记录超时。网络事件更新连接状态,独立任务退避重连,不在回调中阻塞等网。

每个boot随机64位标签只用于实验关联,不是认证或绝不碰撞承诺。命令含规范uint32序号;最近16条相同序号/操作重复返回原快照而不重做,即使其间实体按钮改变了当前灯。同序号不同操作、过旧序号和错boot拒绝;复位默认关灯、重置RAM历史。

代码 / 配置
// ---- main/main.cpp ----
// SPDX-License-Identifier: MIT
#include <cstdio>
#include <cstring>
#include <inttypes.h>
#include <atomic>
#include "sdkconfig.h"
#include "esp_pm.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "esp_timer.h"
#include "esp_random.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "lwip/sockets.h"
#include "credentials.h"
#include "CommandModel.h"
static_assert(CONFIG_PM_ENABLE&&CONFIG_FREERTOS_USE_TICKLESS_IDLE,"automatic sleep requires PM and tickless");
static const char* TAG="L09";static QueueHandle_t requests;static std::atomic<bool> online{false};static std::atomic<uint32_t> drops{0};
struct Event {bool edge;int socket;struct sockaddr_in peer;L09Request request;};
static void owner(void*){
 L09Model model{};model.session=(uint64_t(esp_random())<<32)|esp_random();if(!model.session)model.session=1;L09Button button{};int64_t heartbeat=esp_timer_get_time();
 for(;;){
  Event event{};bool got=xQueueReceive(requests,&event,pdMS_TO_TICKS(100))==pdTRUE;const uint64_t now=uint64_t(esp_timer_get_time());
  // 100ms polling leaves >=3 idle ticks at HZ100; both variants do identical work.
  // This is not GPIO light-sleep wake: a short pulse entirely between polls can be missed.
  if(button.tick(gpio_get_level(GPIO_NUM_5)==0,now)){model.button();gpio_set_level(GPIO_NUM_4,model.led);}
  if(got&&!event.edge){
   char reply[150];int n=0;
   if(event.request.session==0){n=snprintf(reply,sizeof reply,"HELLO %016" PRIx64 " mode=%d online=%u\n",model.session,L09_AUTOSLEEP,online.load());}
   else{L09Model::Reply snapshot{};auto result=model.apply(event.request,snapshot);if(result==L09Model::NEW||result==L09Model::DUPLICATE){gpio_set_level(GPIO_NUM_4,model.led);n=snprintf(reply,sizeof reply,"1 %016" PRIx64 " %" PRIu32 " %u %" PRIu32 " %u %" PRIu64 "\n",model.session,snapshot.seq,snapshot.led,snapshot.buttons,unsigned(result==L09Model::DUPLICATE),now);if(result==L09Model::NEW&&event.request.op==L09Request::LOCKS)ESP_ERROR_CHECK(esp_pm_dump_locks(stdout));}else n=snprintf(reply,sizeof reply,"REJECT %u\n",unsigned(result));}
   if(n>0&&n<int(sizeof reply)&&sendto(event.socket,reply,size_t(n),0,reinterpret_cast<sockaddr*>(&event.peer),sizeof event.peer)<0)ESP_LOGW(TAG,"reply failed errno=%d; host must not count ACK",errno);
  }
  if(esp_timer_get_time()-heartbeat>=60000000){heartbeat=esp_timer_get_time();ESP_LOGI(TAG,"ONLINE=%u mode=%d LED_latch=%u input_count=%" PRIu32 " queue_drop=%" PRIu32 "; no physical sleep/current proof",online.load(),L09_AUTOSLEEP,model.led,model.buttonCount,drops.load());}
 }
}
static void udp(void*){
 const int fd=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);if(fd<0){ESP_LOGE(TAG,"socket errno=%d",errno);vTaskDelete(nullptr);return;}
 timeval tv{1,0};setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof tv);tv={0,100000};setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof tv);
 sockaddr_in bindAddress{};bindAddress.sin_family=AF_INET;bindAddress.sin_port=htons(45679);bindAddress.sin_addr.s_addr=htonl(INADDR_ANY);
 if(bind(fd,reinterpret_cast<sockaddr*>(&bindAddress),sizeof bindAddress)<0){ESP_LOGE(TAG,"bind errno=%d",errno);close(fd);vTaskDelete(nullptr);return;}
 for(;;){char bytes[96];Event event{};event.socket=fd;socklen_t length=sizeof event.peer;int n=recvfrom(fd,bytes,sizeof bytes,0,reinterpret_cast<sockaddr*>(&event.peer),&length);if(n<0){if(errno!=EAGAIN&&errno!=EWOULDBLOCK)ESP_LOGW(TAG,"recv errno=%d",errno);continue;}if(n==6&&!memcmp(bytes,"HELLO\n",6)){event.request.session=0;}else if(!l09Parse(bytes,size_t(n),event.request))continue;if(xQueueSend(requests,&event,0)!=pdTRUE)drops.fetch_add(1);}
}
static void wifiEvent(void*,esp_event_base_t base,int32_t id,void* data){
 if(base==WIFI_EVENT&&id==WIFI_EVENT_STA_START){esp_err_t e=esp_wifi_connect();if(e!=ESP_OK)ESP_LOGW(TAG,"connect start %s",esp_err_to_name(e));}
 if(base==WIFI_EVENT&&id==WIFI_EVENT_STA_DISCONNECTED){online.store(false);auto* event=static_cast<wifi_event_sta_disconnected_t*>(data);ESP_LOGW(TAG,"disconnected reason=%u; reconnect worker",event->reason);}
 if(base==IP_EVENT&&id==IP_EVENT_STA_GOT_IP){online.store(true);auto* event=static_cast<ip_event_got_ip_t*>(data);ESP_LOGI(TAG,"IP=" IPSTR " PM requested, actual connection/sleep not inferred",IP2STR(&event->ip_info.ip));}
}
static void reconnect(void*){for(;;){vTaskDelay(pdMS_TO_TICKS(10000));if(!online.load()){esp_err_t e=esp_wifi_connect();if(e!=ESP_OK)ESP_LOGW(TAG,"retry %s",esp_err_to_name(e));}}}
extern "C" void app_main(){
 esp_pm_config_t pm{160,40,L09_AUTOSLEEP!=0};ESP_ERROR_CHECK(esp_pm_configure(&pm));esp_pm_config_t actual{};ESP_ERROR_CHECK(esp_pm_get_configuration(&actual));ESP_LOGI(TAG,"PM max=%d min=%d autoLS=%u tickless=%d idleTicks=%d; configuration is not measured sleep",actual.max_freq_mhz,actual.min_freq_mhz,actual.light_sleep_enable,CONFIG_FREERTOS_USE_TICKLESS_IDLE,CONFIG_FREERTOS_IDLE_TIME_BEFORE_SLEEP);
 requests=xQueueCreate(8,sizeof(Event));if(!requests){ESP_LOGE(TAG,"queue allocation failed");return;}
 gpio_config_t led{};led.pin_bit_mask=1ULL<<4;led.mode=GPIO_MODE_OUTPUT;ESP_ERROR_CHECK(gpio_config(&led));gpio_set_level(GPIO_NUM_4,0);
 gpio_config_t input{};input.pin_bit_mask=1ULL<<5;input.mode=GPIO_MODE_INPUT;input.intr_type=GPIO_INTR_DISABLE;ESP_ERROR_CHECK(gpio_config(&input));
 if(xTaskCreate(owner,"owner",4096,nullptr,5,nullptr)!=pdPASS){ESP_LOGE(TAG,"owner task allocation failed");return;}
 const char* ssid=l09_ssid();const char* password=l09_password();if(!ssid[0]){ESP_LOGW(TAG,"CONFIG_DISABLED: fill credentials.c locally; input still runs; no network result");return;}
 if(strlen(ssid)>31||strlen(password)>63){ESP_LOGE(TAG,"CONFIG_INVALID");return;}
 ESP_ERROR_CHECK(nvs_flash_init());ESP_ERROR_CHECK(esp_netif_init());ESP_ERROR_CHECK(esp_event_loop_create_default());if(!esp_netif_create_default_wifi_sta()){ESP_LOGE(TAG,"netif allocation failed");return;}
 ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT,ESP_EVENT_ANY_ID,wifiEvent,nullptr));ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT,IP_EVENT_STA_GOT_IP,wifiEvent,nullptr));
 wifi_init_config_t init=WIFI_INIT_CONFIG_DEFAULT();ESP_ERROR_CHECK(esp_wifi_init(&init));ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));wifi_config_t config{};strlcpy(reinterpret_cast<char*>(config.sta.ssid),ssid,sizeof config.sta.ssid);strlcpy(reinterpret_cast<char*>(config.sta.password),password,sizeof config.sta.password);config.sta.listen_interval=3;ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA,&config));ESP_ERROR_CHECK(esp_wifi_start());ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MIN_MODEM));
 if(xTaskCreate(udp,"udp",4096,nullptr,4,nullptr)!=pdPASS||xTaskCreate(reconnect,"reconnect",3072,nullptr,3,nullptr)!=pdPASS)ESP_LOGE(TAG,"network task allocation failed; do not count service ready");
 // No manual esp_light_sleep_start, no user PM lock held across idle, no SNTP/OTA.
}

// ---- main/CommandModel.h ----
// SPDX-License-Identifier: MIT
#pragma once
#include <cstdint>
#include <cstddef>
#include <cstring>
#include <cstdio>
#include <limits>
struct L09Request {uint64_t session=0;uint32_t sequence=0;enum Op{GET,ON,OFF,LOCKS}op=GET;};
inline bool l09Decimal(const char* s,size_t n,uint32_t& value){if(!n||n>10||(n>1&&s[0]=='0'))return false;uint64_t v=0;for(size_t i=0;i<n;++i){if(s[i]<'0'||s[i]>'9')return false;v=v*10+s[i]-'0';if(v>UINT32_MAX)return false;}if(!v)return false;value=uint32_t(v);return true;}
inline bool l09Parse(const char* b,size_t n,L09Request& out){
 if(!b||n<24||n>48||b[n-1]!='\n'||std::memchr(b,0,n))return false;
 if(b[0]!='1'||b[1]!=' '||b[18]!=' ')return false;
 uint64_t session=0;for(size_t i=2;i<18;++i){unsigned v=b[i]>='0'&&b[i]<='9'?unsigned(b[i]-'0'):b[i]>='a'&&b[i]<='f'?unsigned(b[i]-'a'+10):16;if(v==16)return false;session=(session<<4)|v;}
 size_t sep=19;while(sep<n&&b[sep]!=' ')++sep;uint32_t seq=0;if(sep>=n||!l09Decimal(b+19,sep-19,seq))return false;
 const char* op=b+sep+1;size_t len=n-sep-2;L09Request::Op type;if(len==3&&!std::memcmp(op,"GET",3))type=L09Request::GET;else if(len==2&&!std::memcmp(op,"ON",2))type=L09Request::ON;else if(len==3&&!std::memcmp(op,"OFF",3))type=L09Request::OFF;else if(len==5&&!std::memcmp(op,"LOCKS",5))type=L09Request::LOCKS;else return false;
 if(!session){return false;}
 out={session,seq,type};return true;
}
struct L09Model {
 uint64_t session=0;uint32_t last=0,buttonCount=0,accepted=0;bool led=false;
 struct Reply {uint32_t seq;bool led;uint32_t buttons;L09Request::Op op;};Reply cache[16]{};unsigned cursor=0;
 enum Result{NEW,DUPLICATE,WRONG_BOOT,STALE,EXHAUSTED};
 Result apply(const L09Request& q,Reply& reply){
  if(q.session!=session)return WRONG_BOOT;
  for(const auto& old:cache)if(old.seq==q.sequence){if(old.op!=q.op)return STALE;reply=old;return DUPLICATE;}
  if(q.sequence<=last){return STALE;}
  if(last==UINT32_MAX){return EXHAUSTED;}
  if(q.op==L09Request::ON)led=true;else if(q.op==L09Request::OFF)led=false;
  last=q.sequence;++accepted;reply={q.sequence,led,buttonCount,q.op};cache[cursor++%16]=reply;return NEW;
 }
 void button(){led=!led;++buttonCount;}
};
struct L09Button {
 bool raw=false,stable=false,initialized=false;uint64_t changed=0;
 bool tick(bool pressed,uint64_t now){if(!initialized||pressed!=raw||now<changed){initialized=true;raw=pressed;changed=now;return false;}if(now-changed>=30000&&stable!=raw){stable=raw;return stable;}return false;}
};

// ---- main/credentials.h ----
// SPDX-License-Identifier: MIT
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
const char* l09_ssid(void);
const char* l09_password(void);
#ifdef __cplusplus
}
#endif

// ---- main/credentials.c ----
// SPDX-License-Identifier: MIT
// Trusted isolated LAN only. Fill locally, rebuild, do not publish credentials.
// Separate translation unit deliberately keeps the real network path compiled
// in the safe empty-config build (no link time optimization).
#include "credentials.h"
const char* l09_ssid(void){return "";}
const char* l09_password(void){return "";}
05 / 可复现实验与完整工程

主机RTT只能回答请求往返的问题

从UART IP行或实际AP客户端表取节点IP。HELLO后获得boot与mode,探测器发送规范GET,只有源IP/端口、boot、sequence都匹配才接受。迟到旧包/格式错包忽略到本请求2s单调钟截止,不把它误当当前响应。输出文件独占创建,超时RTT为空。

GET是GPIO输出锁存,无光学检测;ON/OFF和LOCKS格式见源码/README。UDP无认证仅用于受信任实验LAN。主机perf_counter_ns测出的RTT含OS、无线、AP和owner排队,不能改名成芯片唤醒时间。LOCKS打印可诊断驱动锁但会扰动测量,单列诊断窗口。

代码 / 配置
python3 host/probe.py NODE_IP --mode auto --count 100 --period 1 --output auto-rtt.csv
python3 host/analyze.py auto-rtt.csv
python3 host/probe.py NODE_IP --mode baseline --count 100 --period 1 --output baseline-rtt.csv
python3 host/analyze.py baseline-rtt.csv
做到这里,你应该看到
这些命令是读者实板验收步骤;本次只对实际localhost同源UDP夹具执行过,不含无线或PM。
06 / 可复现实验与完整工程

同AP同任务,保存电流与命令两份原始证据

按L02先USB核功能与实际IP,退出monitor后完全断电换接PPK2 Source Meter5.0V到J1-21/GND到J1-22,拔两路USB和其他供电。确认峰值/量程600mA边界、压降和复位记录;超出仪器范围改仪器,不关闭brownout。桥/LED等板级消耗包含在5V整板边界内。

固定AP参数与距离,每组预热和测量窗口相同、GET发送节奏相同;先无实体操作量空闲在线,再用固定≥300ms操作序列量交互,别把两种窗口混均值。基线/auto交错重复,完整保存电流电压trace、探测CSV、AP关联记录、UART/IP/掉线/复位、固件SHA与观察表。

analyze拒混boot/模式、重复/越界序号、时间非递增、NaN和超时带伪造RTT;最近秩p95只对ACK有定义,无ACK则null,超时数量另外展示。仅凭偶尔GET能回复不能证明全窗口始终关联;电流下降也不能单凭配置名归因于Light-sleep。

参考:[1] IDF5.5.5电源管理:锁、DFS、Tickless条件[8] PPK2 Source Meter端子与边界[9] PPK2最大DUT电流

07 / 可复现实验与完整工程

在线却不省电,先找持续唤醒者

能回复但看不到节能,先核真实sdkconfig与PM getter,再检查频繁日志、LOCKS诊断、队列等待/轮询与任务就绪、AP DTIM和后台流量。不要通过删失败请求或裁掉重连区间得到漂亮均值。若按钮漏按,先复现采样间隔限制,调整需求/唤醒方案必须作为新固件、新比较边界。

无IP先查真实SSID/密码/AP隔离;默认CONFIG_DISABLED不算故障联网。相同seq改ON/OFF被拒是业务约束,不是省电延迟;新控制器从新boot/更高新序号开始独立窗口。掉线后记录重连耗时和失败,不承诺网络永不断。恢复时断电核线、重刷已知模式,保持NVS/凭据恢复步骤可追溯。

如果卡住了
不要调用手动sleep后把Wi-Fi断线说成自动Light-sleep的必然结果;这两个API任务边界不同。
08 / 可复现实验与完整工程

软件证据到这里为止

两个真实ESP-IDF构建Exit0并核生成配置;同源parser/model/debounce经ASan/UBSan,真实localhost UDP夹具跑5次GET及重复/冲突/错boot/前导零/NUL/超长边界,另15组合成分析器红门。localhost进程没有Wi-Fi、PM、GPIO或真实睡眠,所以不提供本站节能/无线RTT数字。

前篇L08比较周期性断线任务;下一篇L10把采样、电池电压、断网缓存和维护交接组成一周实验。若你的业务必须接收低延迟命令,应先把本页的服务响应需求测清,再决定是否能改成L10的长深睡节奏。

BACK TO THE SOURCE

资料来源与版本说明

固定源码逐字核对安装输入并真实构建;主机服务与合成数学夹具明确限于软件。没有本站物理量/无线/电池或一周实测。资料核对日期:。资料核对、程序构建与实板测试分别记录,不互相替代。

  1. IDF5.5.5电源管理:锁、DFS、Tickless条件
  2. IDF5.5.5 PM结构与getter
  3. IDF5.5.5 Wi-Fi低功耗:MIN_MODEM/DTIM与自动睡眠
  4. IDF5.5.5 Wi-Fi接口/监听间隔说明
  5. IDF5.5.5 FreeRTOS tickless条件
  6. DevKitC-1 v1.1排针
  7. Kingbright WP7113ID原厂规格
  8. PPK2 Source Meter端子与边界
  9. PPK2最大DUT电流