每一个把 ESP32-C3 接到电池上的人,都会撞上同一堵墙。数据手册里的睡眠电流是微安 级,可设备周末就挂了。原因几乎从来不在芯片本身,而在射频。一旦 Wi-Fi 打开, ESP32 电源管理就不再是关于 CPU 的故事,而是关于一个数字的故事:那颗 2.4GHz 调制解调器每秒实际上醒着多少毫秒。在一颗正在发射的射频旁边,核心频率、外设,甚至 一颗状态指示灯,都是可以忽略的零头。
这些是我在主要靠电池供电的 C3 设备上写下的笔记。先把一个诚实的限制说在前面:具体 的毫安、微安数字取决于你的板子、固件和接入点,所以我只会谈相对量级,以及更有用的 东西——如何测出你自己的数字。能迁移的是各个模式和取舍;数字不能。本文默认你已经 完成了ESP32-C3 入门里的 ESP-IDF 环境搭建。
为什么 Wi-Fi 射频压倒其他一切
一颗处于最深睡眠的 C3 耗电极少,普通万用表读出来只当是噪声。可它一旦完成关联、 开始监听,平均电流就跳升几个数量级——这就是一颗纽扣电池能撑几个月、还是只能撑 一两天的差别。
跳升有两个推力。其一,接收不是免费的——射频要为它承诺观察的每个 beacon 和 DTIM 窗口唤醒接收机。其二,短暂地发射远比接收贵:一次全功率的短突发,能在那几毫秒里 拉出空闲调制解调器平均值的几十倍。所以可用的杠杆永远就那么几根——少发射、少接收、 中间睡得更深。那种”只把连接保活”的固件,其实是在默默选择最贵的默认值。
ESP32 的电源模式,以及每种模式对连通性的代价
Active,不省电。 CPU 全速运行,Wi-Fi 接收机基本一直开着。延迟是你所能得到的最好 水平——接入点想什么时候找你都行——而电池为此买单。市电供电没问题,电池供电就是错的。
Modem sleep(Wi-Fi 默认的省电模式)。 CPU 继续运行,但驱动会和接入点协商,在 beacon 之间把射频睡下去。连接保持存活:你的 IP、DHCP 租约、TCP 状态都还在,接入点 会替你缓存下行帧,直到你下一个监听窗口。这是居中的一块,也是大多数常驻节点应该 首先测量的模式。
Light sleep。 CPU 在两次唤醒之间停机,而 RAM 和 Wi-Fi 连接状态得以保留。它比单靠
modem sleep 更省,但唤醒必须足够频繁才能接住下一个 beacon,任何时序敏感的任务都要
小心。ESP-IDF 通过 esp_pm 加 FreeRTOS 的无滴答空闲(tickless idle)把这套接起来,
由调度器替你协调。
深度睡眠。 除 RTC 内存外全部掉电。电流降到微安量级,代价是你丢掉所有状态: 连接、IP、RAM。下次醒来时从零启动并重连——这正是低平均电流要交的税,而值不值得交, 就是设计的核心问题。
Modem sleep:留在网络上
对大多数常驻节点来说,modem sleep 是最划算的一档。你仍是一等的 Wi-Fi 客户端—— 接入点还认识你,还会缓存你的包,还会转交入站 TCP——但你的接收机大部分时间是关的。 驱动直接暴露了这个开关:
#include "esp_wifi.h"
/* 很多应用的默认值,但显式写出来,让这个选择可见。 */
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MIN_MODEM));
/* 更激进:在监听窗口之间睡得更久。
* 更省电,但延迟更差。 */
// ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MAX_MODEM));
WIFI_PS_MIN_MODEM 每个 beacon 都醒,用更少的省电换更低的延迟。WIFI_PS_MAX_MODEM
监听得更少、省得更多,代价是响应性变差。两者都不会断开连接——这正是 modem sleep 的
全部意义,也是它胜过深度睡眠的地方,只要设备必须接收未经请求的流量:一条命令主题、
一个门铃、一台家庭自动化控制器。如果你的设备的工作就是能被找到,那你要的是
modem sleep,不是深度睡眠。
DTIM、监听间隔,以及延迟与续航的取舍
接入点是这场协商的另一半。beacon 以固定间隔发出(常见在 100ms 左右),而每隔一段 就有一个 beacon 是 DTIM——Delivery Traffic Indication Message,此时接入点会 冲刷缓存的广播和组播帧,并告诉睡眠中的客户端”现在醒,有你的流量”。客户端的 **监听间隔(listen interval)**则表示它在两次监听之间可以跳过多少个 beacon。
算术很简单:监听间隔越长、跳过的 DTIM 周期越多,射频就能关得越久、平均电流越低—— 但延迟越高,因为下行流量要在接入点的缓存里等到你下一个窗口。典型家用接入点的 beacon/DTIM 周期是几十到两百毫秒量级;接入点那一侧不由你控制,你能选的只是观察得 多勤快。客户端这一侧可以通过 station 配置微调:
#include "esp_wifi.h"
wifi_config_t cfg = { 0 };
cfg.sta.listen_interval = 3; /* 最多跳过这么多个 beacon */
cfg.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
/* ... 设置 ssid/password,然后: */
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &cfg));
把这个旋钮拧大,正是”低功耗 Wi-Fi”的取舍:每多跳过一个 beacon,就多省一点电, 也多给最坏情况下的下行延迟加上几毫秒。往外上报的传感器几乎不在乎延迟——尽管激进。 交互式节点则应该先量一量用户真正能察觉的延迟,再去为省电调参。
ESP32 深度睡眠与重连的代价
深度睡眠是唯一能触及微安底线的模式,而它每次唤醒都要收你一次完整的启动加重连。 这笔开销不是一个数字,而是好几个:射频唤醒与校准、扫描、关联、四次握手,然后是 DHCP(或用静态地址跳过它)和 DNS。按墙上时钟算,这往往是几秒钟,而其中大部分时间 射频都在耗电。如果你每十分钟醒一次,重连的能量就能吞掉整个预算——所以正确的问题 不是”我能睡多深?“,而是”我能多不常醒?”。
#include "esp_sleep.h"
#include "esp_log.h"
RTC_DATA_ATTR static int s_boot_count; /* 能挺过深度睡眠 */
void app_main(void)
{
esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
ESP_LOGI("power", "wake %d, cause %d", ++s_boot_count, (int)cause);
/* ... 连接、读传感器、上报、断开 ... */
esp_sleep_enable_timer_wakeup(10ULL * 60 * 1000000); /* 10 分钟 */
esp_deep_sleep_start(); /* 不会返回 */
}
RTC_DATA_ATTR 是唯一能幸存的 RAM;其余一切都要在唤醒后重建。而且上面这段示意是
刻意粗糙的:真机上你会加退避、缓存 BSSID 和信道来缩短扫描,并用静态 IP 跳过 DHCP。
诚实的工程时间就花在这里。
测量真实电流(一种低成本的剖析方法)
不测量就无法优化,而测量比听起来容易。最便宜且诚实的方法,是用一个 USB 功率计, 或者在供电线上串一个分流电阻配万用表;它看不到微秒级的发射尖峰,但能看到平均值, 而续航恰恰取决于平均值。要更精确,就用一台专门的电流剖析工具(Nordic PPK2、 INA219/INA260 模块、Joulescope),它能给你真实波形,让你看见发射突发和重连风暴。
无论用什么仪器,方法都一样:把同一份固件在你关心的每种配置下——active、
WIFI_PS_MIN_MODEM、WIFI_PS_MAX_MODEM、深度睡眠占空比——各测几分钟,比较
平均值。这就是我拒绝编造数字的原因:板子的 LDO 静态电流、一颗没省掉的 LED、
一个从不休眠的 USB-UART 桥、或者一根悬空的 strapping 引脚,都可能盖过芯片自身的
睡眠电流。你的测量才是真相;数据手册上的表格只是起点。
低功耗 Wi-Fi 的设计指引
几乎每个电池供电的 ESP32-C3 项目都落在这三种形态之一,选对形态比任何一个寄存器 微调都重要。
周期上报的传感器(定时上报,从不接收)。 用深度睡眠配定时唤醒。连接、上报、 断开、睡下。要激进:睡眠间隔拉长,用静态 IP。平均电流主要由重连能量主导,所以 尽量缩短唤醒时间、拉长周期。
常驻监听节点(必须接收未经请求的流量)。 用 modem sleep,不是深度睡眠。你 需要连接持续存在,所以赢面在于更长的监听间隔和更少的唤醒——但你永远到不了深度 睡眠的电流,因为射频必须不断回连。这是控制器以及任何带命令通道的设备该用的模式。
市电供电。 别优化了。跑 active 模式、关掉省电,拿最低延迟。在这里加省电一无所获, 只损失响应性。唯一值得做的是别浪费电:关掉不用的外设和 LED。
在这三种之上还有一层:在 ESP-IDF 里,动态调频和自动 light sleep 是通过 esp_pm
一次性配置的——它独立于 Wi-Fi 省电设置,并与 FreeRTOS 的无滴答空闲协同:
#include "esp_pm.h"
#include "esp_log.h"
void configure_dynamic_power(void)
{
esp_pm_config_t pm = {
.max_freq_mhz = 160, /* 只在有活干时把 CPU 拉满 */
.min_freq_mhz = 10, /* 空闲时把时钟降下来 */
.light_sleep_enable = true,
};
ESP_ERROR_CHECK(esp_pm_configure(&pm));
}
这需要在 menuconfig 里打开电源管理加无滴答空闲;具体菜单路径在版本之间会挪动,
所以在 Component config 下面去找,别去背它。esp_pm 和 esp_wifi_set_ps() 是
搭档而非二选一——前者管 CPU 和 light sleep,后者管射频。
我会先做什么
先测默认值:把你现在这份常连接固件烧进去,盯住平均电流——那个基线通常比你想的更糟。 然后判断自己属于三种形态中的哪一种,只套用它该用的模式,再测一次。对任何必须保持 可达的设备,modem sleep 配更长的监听间隔是最便宜的大胜;深度睡眠最深,但前提是设备 在两次上报之间真的不需要在场。在设备上省电只算设计的一半——正如我的 ESP32-C3 EasyTier 节点所示,服务器必须能容忍 一个会消失又回来的客户端。本文周边的 ESP-IDF 代码都在 入门指南里。