
做ESP32电池供电的项目最难的地方往往不是把功能跑通而是让设备在电池上活过承诺的续航时间。我之前做过一个温湿度传感器节点功能很简单30秒上报一次数据结果用18650电池供电第一版实测不到7天就趴窝了——问题根本不是上报那几百毫秒而是Wi-Fi连接等待时的持续电流。后来把方案的功耗管理从Modem-sleep换成了Light-sleep同样一块电池直接跑了一个多月。这篇文章就来聊聊ESP32的Light-sleep低功耗模式到底怎么用尤其聚焦在“保持Wi-Fi连接的前提下如何省电”把原理、代码、实测数据和踩过的坑一次讲清楚。这个方案适合的场景很明确设备需要周期性上报数据、需要保持服务器长连接比如TCP、MQTT、但又不能忍受Deep-sleep醒来后重新连Wi-Fi那几秒的体验。如果你做的是智能插座、环境监测节点、资产追踪器这类设备Light-sleep基本是绕不开的必修课。1. 为什么选择Light-sleep低功耗方案的取舍逻辑1.1 从产品需求反推功耗方案很多人一说低功耗第一反应就是Deep-sleep。这没错Deep-sleep能把电流做到几十微安甚至更低在电池供电产品里确实香。但Deep-sleep有个硬伤Wi-Fi连接会断开醒来后要重新扫描、认证、关联顺利的话2-3秒信号差的时候10秒都正常。如果你的设备是每30秒就要上报一次数据睡眠30秒、重连5秒这比例谁都受不了不仅是体验差重连期间的功耗也高得吓人。Light-sleep的思路完全不同。它允许CPU暂停运行但RAM和部分外设继续供电最关键的是——Wi-Fi连接可以保持。也就是说设备看起来“睡着了”但网络连接没断醒来后直接发数据不用重新走一遍连接流程。这个特性让它成了中等频率上报场景下的最优解。按我的经验做方案选型时先别急着定技术路径把需求拆成三个问题上报频率是多少连接断开后重连的代价有多大服务器允不允许频繁掉线答案自然就出来了。上报间隔在秒级到分钟级、连接重连成本高、需要快速响应的优先考虑Light-sleep上报间隔超过几分钟、数据量小的Deep-sleep更合适。1.2 ESP32几种低功耗状态的对比ESP32的低功耗模式不是只有Light-sleep和Deep-sleep两个选项中间还有Modem-sleep很多人一直没搞清楚这三者的区别选型的时候就容易拍脑袋。Modem-sleep是Wi-Fi本身的一种省电状态核心思想是让Wi-Fi射频模块在DTIM周期内间歇性工作。比如路由器每100ms发一次beaconModem-sleep可以让ESP32只在beacon到达时醒来一小会儿其余时间射频关闭。CPU还在正常运行程序照常执行电流可以从峰值240mA降到20-40mA左右。Light-sleep在Modem-sleep的基础上更进一步CPU直接暂停大部分时钟关闭只保留RTC和少量外设。搭配Wi-Fi时连接由底层的Wi-Fi协处理器维护CPU不需要参与。实测下来保持Wi-Fi连接时Light-sleep的电流能压到1-3mA比Modem-sleep省了十倍以上。Deep-sleep就是我开头说的睡眠模式CPU、RAM全断电只剩下RTC和ULP协处理器存活。优点是把电流干到10uA以下代价是Wi-Fi连接彻底断开RAM中的数据全部丢失。三种模式各有各的位置。我的选择标准是需要保持长连接、上报间隙不超过一分钟的用Light-sleep业务间隔离、深度待机的用Deep-sleep处于收发状态的瞬态过程交给Modem-sleep就够了。1.3 什么时候不要用Light-sleepLight-sleep虽然好但不是万灵药。有一种情况我强烈不建议用设备上报间隔很长比如十分钟甚至半小时一次业务逻辑又简单这时候用Deep-sleep加定时唤醒功耗可以做到几十微安比Light-sleep再省一到两个数量级完全没必要为了保持连接牺牲这么多。还有一种情况也要注意你的路由器或者使用环境beacon非常密集、DTIM配置不合理有些路由器默认把DTIM设成1每个beacon都携带缓存指示Wi-Fi芯片需要频繁醒来接收beaconLight-sleep的效果会被打折扣。这种情况下要么改路由器配置要么接受现实用Deep-sleep方案。另外如果你的业务本身就需要CPU持续运行比如在做实时控制、音频处理、LED点阵刷新那Light-sleep完全帮不上忙这时候该考虑的是降低CPU频率、关闭不用的外设而不是睡不睡的问题。2. Light-sleep的工作原理与核心参数2.1 休眠时硬件在做什么理解Light-sleep省电的关键在于搞清楚一个事实CPU睡着了不等于整个芯片睡着了。打个比方Light-sleep就像晚上在家休息手机保持开机但人睡了——手机待机功耗很低但来了电话能响不会失联。ESP32在Light-sleep下CPU核心暂停执行指令系统时钟和大多数外设时钟被关闭但这几样东西还在RTC存储器和RTC外设继续供电部分GPIO可以触发唤醒事件Wi-Fi基带和射频部分可以通过Modem-sleep机制在需要时自动醒来处理beacon。所以Light-sleep能保持Wi-Fi连接靠的不是CPU而是Wi-Fi协处理器。ESP32的Wi-Fi模块本身有独立的状态机它可以自己处理beacon的收发、维护连接状态、回应连接保活包CPU完全不参与。只有当真的有数据需要处理时Wi-Fi模块才会唤醒CPU。这个机制在乐鑫的电源管理框架里叫PMPower Management底层通过FreeRTOS的Tickless Idle机制触发。这里有个很多新手忽略的点Light-sleep不是你想睡就立刻睡的它进入和退出的动作都有开销。进入Light-sleep需要把系统时钟切到RTC时钟源退出时要恢复主时钟这个过程本身耗时若干毫秒也有能量消耗。所以短时间频繁进出Light-sleep省下来的电还不如多花的电多这就是为什么我建议上报周期至少3秒以上再考虑Light-sleep。2.2 唤醒机制定时器与GPIOLight-sleep的唤醒方式和Deep-sleep基本一致最常用的是定时器唤醒和GPIO唤醒。定时器唤醒非常简单一个函数搞定。比如希望设备每30秒醒来一次上报数据// 设置定时器唤醒单位是微秒 esp_sleep_enable_timer_wakeup(30 * 1000000ULL); // 进入Light-sleep esp_light_sleep_start(); // 唤醒后从这行继续执行注意时长的类型是uint64_t单位是微秒如果要用秒做单位一定要先换算再传参。GPIO唤醒适合那些由外部事件触发的场景比如倾斜传感器检测到设备被移动、门磁检测到门开、按键被按下。ESP32的Light-sleep支持任意IO唤醒配置方式有两种// 方式一配置GPIO唤醒支持边沿或电平触发 gpio_wakeup_enable(GPIO_NUM_14, GPIO_INTR_LOW_LEVEL); esp_sleep_enable_gpio_wakeup();// 方式二使用外部唤醒引脚EXT0/EXT1Deep-sleep也能用 esp_sleep_enable_ext0_wakeup(GPIO_NUM_14, 0);GPIO唤醒有个容易踩的坑触发唤醒的那个GPIO在睡眠期间必须有确定的电平状态不能浮空。如果你用的是边沿触发睡眠前引脚需要被外部电路拉到一个确定的电平如果用低电平触发睡眠期间引脚必须保持高电平事件到来时拉低。如果引脚悬空轻则产生随机唤醒重则引脚内部上拉电阻持续耗电把刚才省下的几百微安又赔回去。2.3 影响功耗的三个隐藏因素很多人把esp_sleep_enable_timer_wakeup和esp_light_sleep_start写好测出来电流还是2mA多怎么都降不下去就开始怀疑代码有问题。实际上Light-sleep的最终功耗受三个隐藏因素影响代码只是其中之一。第一个因素是CPU最小频率。在启用电源管理框架的情况下esp_pm_configure里有个min_freq_mhz参数它决定了系统在轻负载时CPU最低能降到多少。如果你把这个值设成240MHz那Light-sleep期间虽然CPU休眠但每次因为Wi-Fi事件被唤醒处理的时候频率都是全速240MHz功耗自然降不下去。保守的做法是启用Wi-Fi时设为80MHz纯休眠不关心Wi-Fi时甚至可以设到40MHz甚至更低。第二个因素是DTIM间隔。DTIM是路由器控制无线终端睡眠的机制大概含义是“路由器每隔几个beacon数据帧广播一次缓存指示”。DTIM1表示每个beacon都携带缓存数据指示Wi-Fi模块需要频繁醒来DTIM3表示每3个beacon唤醒一次。这个参数完全由路由器决定ESP32只能被动适应。数据库路由器默认DTIM1实际使用中如果自己控制路由器建议把DTIM调到3功耗能明显下降。第三个因素是GPIO和外围电路的静态功耗。前面说的引脚浮空问题就是典型。还有板上稳压器、传感器、电平转换芯片的静态电流这些和芯片本身没关系但都在吃你的电池。我见过一个项目芯片Light-sleep已经做到1mA了结果一块开发板上AM1117稳压器自身就吃5mA前面省的全都白费了。后面我会专门说这个问题。3. 手把手配置Light-sleep并保持Wi-Fi连接3.1 基于ESP-IDF的完整代码基于ESP-IDF开发时配置Light-sleep保持Wi-Fi连接核心代码其实不多但有几个顺序和参数不能搞错。下面是一份可以直接用的最小示例#include string.h #include freertos/FreeRTOS.h #include freertos/task.h #include esp_log.h #include esp_pm.h #include esp_wifi.h #include esp_event.h #include esp_sleep.h #include nvs_flash.h static const char *TAG light_sleep_demo; // Wi-Fi初始化并连接 void wifi_init_sta(void) { wifi_init_config_t cfg WIFI_INIT_CONFIG_DEFAULT(); esp_wifi_init(cfg); esp_event_loop_create_default(); esp_wifi_set_storage(WIFI_STORAGE_RAM); wifi_config_t wifi_config { .sta { .ssid YOUR_SSID, .password YOUR_PASSWORD, .threshold.rssi -80, }, }; esp_wifi_set_mode(WIFI_MODE_STA); esp_wifi_set_config(WIFI_IF_STA, wifi_config); esp_wifi_start(); esp_wifi_connect(); // 关键Wi-Fi启用Modem-sleep省电策略 esp_wifi_set_ps(WIFI_PS_MIN_MODEM); } // 配置电源管理开启自动Light-sleep void pm_config_init(void) { esp_pm_config_esp32_t pm_config { .max_freq_mhz 160, .min_freq_mhz 80, // 启用Wi-Fi时建议不低于80MHz .light_sleep_enable true }; esp_pm_configure(pm_config); } void app_main(void) { esp_err_t ret nvs_flash_init(); if (ret ESP_ERR_NVS_NO_FREE_PAGES || ret ESP_ERR_NVS_NEW_VERSION_FOUND) { nvs_flash_erase(); nvs_flash_init(); } wifi_init_sta(); pm_config_init(); while (1) { // 业务逻辑30秒上报一次传感器数据 send_sensor_data(); // 你的HTTP/MQTT上报函数 vTaskDelay(pdMS_TO_TICKS(30000)); } }这段代码的精髓在于两个函数配合esp_wifi_set_ps(WIFI_PS_MIN_MODEM)让Wi-Fi工作在省电模式esp_pm_configure开启电源管理框架并允许Light-sleep。当系统检测到CPU空闲FreeRTOS进入IDLE任务、Wi-Fi没有数据要收发、定时器事件还没到达时就会自动让CPU进入Light-sleep。这里我特别说一下很多人疑惑为什么没有看到显式调用esp_light_sleep_start设备也能进Light-sleep因为这里是自动模式进入动作由电源管理框架触发不是业务代码控制。业务代码只管该睡睡该干干框架负责在合适的时候插一脚。3.2 Arduino环境下怎么配Arduino环境下大部分ESP32开发包的默认配置已经启用了电源管理你需要做的只是额外设置参数。#include WiFi.h #include esp_pm.h #include esp_wifi.h const char* ssid YOUR_SSID; const char* password YOUR_PASSWORD; unsigned long lastReport 0; void setup() { Serial.begin(115200); WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); while (WiFi.status() ! WL_CONNECTED) { delay(100); } Serial.println(WiFi connected); // 设置Wi-Fi省电模式 esp_wifi_set_ps(WIFI_PS_MIN_MODEM); // 配置电源管理启用Light-sleep esp_pm_config_esp32_t pm_config { .max_freq_mhz 160, .min_freq_mhz 80, .light_sleep_enable true }; esp_pm_configure(pm_config); Serial.println(Light-sleep enabled); } void loop() { if (millis() - lastReport 30000) { // 上报传感器数据 sendSensorData(); lastReport millis(); } delay(10); }注意一个细节Arduino环境里WiFi.begin()返回后并不代表连接一定成功我的代码里用的是阻塞等待实际工程建议加超时处理避免程序卡死在等待循环里。另外Arduino的delay()函数在电源管理框架下会自动产生空闲时间所以不需要改delay的逻辑它就能和Light-sleep共存。3.3 关键参数的设置依据参数设置的背后是硬件的物理限制搞清楚这些限制以后换芯片换场景也能举一反三。max_freq_mhz和min_freq_mhz是一对组合。ESP32最高主频是240MHz但跑Wi-Fi不是非得240MHz160MHz甚至80MHz都够用。max_freq设为160MHz的好处是减少全速运行时的瞬态电流性能影响微乎其微。min_freq设为80MHz的原因在于ESP32的Wi-Fi协议栈要求Wi-Fi活跃状态下CPU频率不能低于80MHz低于这个值会出现Wi-Fi断开甚至死机。light_sleep_enable这个是总开关只有在esp_pm_configure里设为true系统才允许IDLE任务在空闲时进入Light-sleep。如果你漏了这一项或者项目编译时没开CONFIG_PM_ENABLEesp_pm_configure会直接返回ESP_ERR_NOT_SUPPORTED后续任何低功耗都是空谈。还有一个不太起眼但很关键的设置Wi-Fi省电模式的三个值。ESP32的esp_wifi_set_ps支持三种WIFI_PS_NONE关闭省电Wi-Fi始终满功耗监听配合Light-sleep基本没有意义因为beacon导致频繁全速唤醒。WIFI_PS_MIN_MODEMModem-sleep按需启停射频是和Light-sleep搭配最常用的组合。WIFI_PS_MAX_MODEM最激进的Wi-Fi省电策略接收窗口更小但延迟也变大如果你对下行消息实时性要求低可以试试。我日常用MIN_MODEM它在功耗和实时性之间平衡得最好。MAX_MODEM在路由器兼容性上有时候会出幺蛾子表现为TCP偶尔超时丢包排查起来费劲。4. 实测功耗与续航计算4.1 测量工具与测量方法写低功耗代码不测电流等于没写。但测ESP32的电流有个特点动态范围太大峰值能到几百毫安休眠时一毫安不到普通万用表根本跟不上变化节奏。我的习惯是至少准备两种工具。第一种是万用表用来测静态场景的电流比如固定让设备进入Light-sleep、断开Wi-Fi这时候电流基本不变万用表的微安档位就能读得很准。第二种是电流波形记录仪我用的是Joulescope稍微便宜点可以看Nordic的Power Profiler Kit IIPPK2测动态电流波形。不是说非得买贵的但如果项目认真做功耗这类工具早晚要备。测电流前先把板上跳线改造成可串联测量的结构。大多数开发板USB供电线路和电池供电线路是连着的直接把表笔怼在USB口只能测到平均值。我的做法是把板子的VIN和3.3V之间断开外接一个跳线帽。测电流时把跳线帽去掉把万用表或电流探针串联进去。最后还有一点测量脚本要把设备跑起来让它工作在真实的周期上报模式。单测Light-sleep那几百毫秒的电流没有意义要看的是“正常业务状态下一个完整周期的平均电流”这才是电池续航的真相。4.2 不同模式下的实测数据下面这组数据是我在ESP32-WROOM-32模组上实测的外部供电3.3V模组上不带USB转串口芯片那种开发板。环境是一台普通家用路由器DTIM3Wi-Fi信号强度-50dBm左右。工作模式Wi-Fi状态实测平均电流说明Active持续收发已连接80-120mACPU全速运行TCP持续传数据Modem-sleep已连接18-25mACPU运行Wi-Fi按DTIM周期醒睡Light-sleep自动保持连接已连接1.2-2.5mACPU休眠Wi-Fi关联保持Light-sleep已断开0.4-0.8mACPU休眠Wi-Fi不工作Deep-sleep已断开约10uA仅RTC和ULP工作这组数据在不同模组之间会有差异但数量级是稳定的。最直观的结论是同样保持Wi-Fi连接从Modem-sleep换成Light-sleep电流降了一个数量级如果配合DTIM3效果更明显。和Deep-sleep比Light-sleep确实做不到微安级但它赢得了“连接不断”这个关键优势这是很多产品无法放弃的。这里要说明一点上面Light-sleep保持Wi-Fi连接的数据不是裸的Light-sleep电流而是包括了周期性的Wi-Fi beacon唤醒、TCP keepalive握手。如果完全空闲不保活电流还可以再低一些。4.3 续航估算公式与案例拿到平均电流以后续航就是个简单的除法。先给出公式续航时间小时 电池容量mAh / 平均电流mA但这个平均电流必须是把工作周期完整算进去的均值不是休眠电流也不是发送电流。我拿前文的温湿度传感器举例30秒一个周期发送数据耗时0.3秒发送期间平均电流150mA剩余29.7秒处于等待状态。分别用三种方案算Modem-sleep方案等待期间电流20mA平均电流 (150×0.3 20×29.7) / 30 21.3mA 续航 3000 / 21.3 ≈ 141小时 ≈ 5.9天Light-sleep保持连接方案等待期间电流2mA平均电流 (150×0.3 2×29.7) / 30 3.48mA 续航 3000 / 3.48 ≈ 862小时 ≈ 35.9天Deep-sleep方案等待期间电流0.01mA但每次要加5秒重连Wi-Fi时间重连期间平均80mA平均电流 (150×0.3 80×5 0.01×24.7) / 30 ≈ 14.8mA 续航 3000 / 14.8 ≈ 203小时 ≈ 8.4天这个对比很直观。同样的业务Light-sleep方案和Modem-sleep方案比续航翻了6倍甚至比Deep-sleep重连方案还省电因为Deep-sleep的重连开销在短周期上报场景下被放大了无数倍。这不是说Deep-sleep不好而是再次印证前面说的选型必须结合业务周期来算不能凭感觉。5. 常见问题与避坑指南5.1 明明配了Light-sleep功耗还是下不来这是我被问得最多的问题。代码一行没少esp_pm_configure也调了结果电流纹丝不动还是二十毫安以上。排查思路按优先级来。第一步确认CONFIG_PM_ENABLE有没有开。ESP-IDF里这个宏不是默认开启的需要确认项目配置文件里设了CONFIG_PM_ENABLEy或者在menuconfig里选中“Power Management”选项。Arduino环境一般已经开了但IDF经典工程容易漏。第二步看esp_wifi_set_ps的返回值。如果Wi-Fi还没初始化就调用或者参数非法函数会返回错误码代码里没检查就会当没发生过。我的建议是每个关键API都用ESP_ERROR_CHECK包一遍出问题第一时间暴雷而不是埋到后面变成玄学。第三步用日志确认有没有进入Light-sleep。把日志级别调到DEBUG观察串口输出正常会看到“light sleep enter”和“light sleep exit”之类的打印。如果压根没进入就回到前两步查原因。第四步查外围电路。这一条最容易漏也最致命。我遇到过排查到最后发现是板上一个LED指示灯全程亮着一颗LED大概2-3mA直接把结果带崩了。测试低功耗前把不相关的跳线、传感器、指示灯全部断开或者软件关闭。5.2 Wi-Fi连接在休眠后频繁断开如果你开启了Light-sleep保持连接但设备休眠一段时间后Wi-Fi就断了大概率是buck里调用了导致Wi-Fi模块无法正常收beacon的操作。最常见的原因是min_freq_mhz设得太低。Wi-Fi协议栈跑在CPU上如果休眠期间CPU频率降到了40MHz甚至更低某些时候beacon数据到达CPU处理不过来超过重试次数就会掉线。保守方案把min_freq_mhz设成80MHz一般就能解决。第二种原因是路由器开启了WMMWi-Fi多媒体节电里的省电模式或者路由器本身对空闲连接有超时回收策略。这种问题在公共Wi-Fi或企业AP环境尤其明显表现为“休眠前好好的睡完一觉醒来发现断线重连”。排查方法很简单把设备放到另一台路由器环境对比测试如果正常就是路由器策略问题。处理策略也分两种设备端尽量启用TCP keepalive心跳每隔几秒发个空数据包维持连接活跃业务端做断线检测Wi-Fi断开了就主动重连而不是干等。5.3 测开发板功耗和测模块功耗是两回事这是低功耗项目的经典大坑。很多初学者用ESP32 DevKit开发板做实验测出Light-sleep电流有5mA甚至更高就觉得Light-sleep没用怀疑人生。问题几乎都出在开发板而不是ESP32芯片上。常见的ESP32 DevKit板上有一颗USB转串口芯片CP2102或CH340USB口插着或者电源指示灯亮着芯片本身静态电流就有2-5mA板上还有稳压器LDO、LED、按键上拉电阻这些全都在持续耗电。芯片的Light-sleep功耗很优秀但开发板不等于芯片。做低功耗评估时如果你用的是开发板先查数据手册把板载耗电元件找出来或者干脆自己画一个最小系统板只保留ESP32模组、电源、必要的去耦电容。它在实际量产时才是和你电池寿命相关的本体。如果你只是在验证功能开发板也能用但一定要清楚测到的数字里有多少是板子贡献的别把这个数当芯片能力。5.4 别再忽略GPIO漏电低功耗项目的最后一道防线是GPIO状态。Light-sleep下GPIO保持休眠前的配置这个配置不正确就是活生生的漏电路径。最常见的场景是某个GPIO没有接外部上拉/下拉电阻但你在代码里也不管它结果休眠期间这个引脚悬空内部的上拉电阻加上外部电路的不确定状态产生漏电流。虽然单个引脚可能只有几十微安但架不住引脚多10个引脚的漏电就能吃掉所有优化成果。建议养成一个习惯进入休眠前把所有不用的GPIO统一配置为输入模式并且设置内部上拉或下拉确保电平确定。用到的GPIO逐个检查状态是否和外接电路匹配尤其是电平触发唤醒的引脚睡眠期间要处于非触发状态。还有一种情况是引脚接了传感器传感器本身待机电流不小。如果传感器支持关断用一颗MOS管或GPIO控制其电源休眠前直接断电醒来再上电。这个技巧在电池供电产品里基本是标配。6. 最后分享几个实战小技巧我在实际项目中吃过不少亏之后总结了几条Light-sleep相关的经验这里一次性分享出来。调试Light-sleep时先别急着上电池用稳压电源给设备供电把电流波形打出来先看清楚每个阶段的电流台阶。一个正常的30秒周期上报设备波形应该是发送时一个高台阶然后是几秒的中等电流平台等待TCP收到ACK接着跳到一个很低的台阶最后有一个下跳的瞬间进入Light-sleep。如果看不到低台阶说明Light-sleep没生效问题出在前面的配置。串口打印在低功耗调试里是双刃剑。调试时开着串口日志方便排查但量产时一定要关掉。UART模块本身不贵电但打印字符串会唤醒CPU、拉高总线电平频繁打印会让设备长期处于活跃状态。更隐蔽的是USB转串口芯片在连接状态下会试图和主机握手增加额外的电流消耗。我的做法是调试版本用宏控日志发布版本全部关闭串口。还有一个小细节RTC外设里的ADC、触摸传感器模块如果在Light-sleep期间没用记得禁用。有些ESP32型号触摸传感器默认开启睡眠期间会周期性采样又是几百微安级别的开销。在代码里显式调用touch_pad_deinit()或者对应的RTC外设停止函数把这个隐藏电流拔掉。如果项目允许更新硬件新设计可以考虑ESP32-C3或ESP32-S3它们的Light-sleep功耗比ESP32原版更低而且Wi-Fi连接稳定性更好。我从ESP32往C3迁移过一个产品保持Wi-Fi连接的Light-sleep电流能做到0.5-1mA级别续航又涨了一截。最后一点做低功耗不是一个功能点而是一种系统思维从芯片选型、电路设计、软件框架、业务周期设定到外围器件选择每一层都在影响最终续航。Light-sleep只是其中一环但它很值得投入时间去吃透——毕竟电池供电的产品续航就是用户最直接的体验。