ESP32拉低电源保持脚后为何仍不关机
按下关机键,程序已经把电源保持脚拉低,屏幕却还亮着。拔掉 USB,它又真的关机了。
这个现象不一定是 GPIO 失灵。自保持电源往往有不止一个“开门人”。主控撤回自己的使能,USB 或按键仍可能把稳压器的 EN 托在高电平。更容易被忽略的后半程是,程序以为下一条指令不会再执行,结果供电没断,业务循环继续跑,甚至又把保持脚拉高。
这次把电路的使能路径和软件的退出路径放在一起核对,并实现一个有明确终态的控制器。它解决的是关机流程的确定性,不把“写了低电平”等同于“整机已经没电”。

图1 根据实际连接绘制的路径示意,只表达控制关系;导通压降和逻辑门限仍以器件、电路为准。
EN 并不只听主控一个人的
先看电源页。U1 使用 SPX3819M5-L-3-3/TR,3 脚是 EN,5 脚输出 3.3V。沿着 EN 向外追,会遇到三条输入路径。

图2 实际 EDA 原图的两块局部纵向排版,保留位号与阻值;分隔处不是实际连线。D2、D3、D4 的阴极汇到同一个 EN 节点。
POWER_IO 经 D2 维持供电;VCC_KEY 经 R2(2kΩ)和 D4 让按键参与使能;USB 的 5V 经 R7(10kΩ)、R8(20kΩ)分压,再经 D3 参与使能。三条路径共同决定 EN,POWER_IO 只是其中一路。
当 USB 路径把 EN 维持在高电位,而 POWER_IO 被拉低,D2 会隔离这一路。主控不能隔着一个反向偏置的二极管,把另一条路径提供的高电平硬拽下来。按键仍按着时,D4 路径也可能产生相同现象。
忽略负载时,分压点是 5×20/(10+20)≈3.33V。但这个数字不是 EN 的实测值。分压网络的等效电阻约 6.67kΩ,二极管压降、EN 输入电流以及 KEY_USB 分支负载都会影响实际电压。SPX3819 数据手册给出的 EN 高电平最低值是 2.0V,低电平最高值是 0.4V;判断“高”必须留出裕量,不能只写一个理想分压结果。
还有一处值得克制。手册的引脚说明明确写着 EN 为低或悬空时关闭,因此不能仅因图里没有外置 EN 下拉,就断言电路有缺陷。盲目补一个下拉,还会改变 USB 分压支路的负载。
先把 GPIO 和焊盘对上
MCU 页使用 ESP32-S3。KEY_POWER 接 GPIO1,KEY_USB 接 GPIO2,POWER_IO 接 GPIO45。这里的 GPIO45 是信号编号,不是芯片封装第 45 脚;在该符号中它对应 U5 的 51 脚。

图3 MCU 页左、右引脚分别裁取后纵向排版,便于手机阅读;GPIO1、GPIO2 和 GPIO45 与网络名一一对应。
同一工程的 PCB 网络面板进一步给出了对应关系:D2、D3、D4 的 1 焊盘和 U1 的 3 焊盘都属于 $3N1359;D2 的 2 焊盘与 U5 的 51 焊盘属于 POWER_IO。先验证这些连接,再讨论软件,能避免拿着一个版本的图去解释另一个版本的板。

图4 对应工程的 PCB 顶面实取图,用于确认板型、位号和器件分布;细小网络以图2及编辑器焊盘查询为准。

图5 独立翻板导出的底面视图,观察方向已翻转,不能直接与顶面按左右位置对照。两面图均保留实际走线,未绘制所谓“优化后布线”。
GPIO45 还是启动绑带引脚。不要为了“尽早保持供电”就直接加一个强上拉。上电采样、电源爬升、Flash 供电配置和板载外部网络需要一起核对。下面的控制器从系统已经正常启动、保持输出已经建立的阶段开始,不替代启动电路设计。
关机流程必须有一个能待住的地方
最危险的写法,是关机分支只执行一次拉低,然后直接返回普通业务循环。电池独供时,这个错误可能被真实掉电掩盖;接上 USB 后,它就显出来了。
把状态分成 RUN、STOPPING、PARKED 更清楚。RUN 收到关机请求后锁存请求,进入 STOPPING;业务先停妥,按键连续释放至少 50ms,再进入 PARKED 并撤回保持。50ms 是本例的软件策略,实际按键应结合波形与采样周期调整。
services_quiet 不是“已经等了一会儿”,而是相关业务的完成确认。比如停止新增 SD 写入、等当前事务完成、提交需要保存的配置、让音频与显示停止访问共享资源。具体先后次序由实际应用决定,不能拿一个固定延时代替成功状态。
进入 PARKED 后,控制器持续要求 POWER_IO 为低。USB 让芯片继续运行也没关系,业务保持停止。它不会把“仍能执行下一轮”误解为需要重新开机,也不要求用 KEY_USB 的数字电平去证明 USB 已经拔掉。
下面是完整关键控制代码,保存为 power_release.c。所有状态由同一个任务持有,now_ms 使用单调递增的 uint32_t 毫秒计数。
/* SPDX-License-Identifier: MIT
* Independent example, v1.0.0. C11, single owner, monotonic uint32_t ms.
* This controller withdraws one enable source; it cannot prove power is off.
*/
#include <stdbool.h>
#include <stdint.h>
typedef enum { RUN, STOPPING, PARKED } power_state;
typedef struct {
power_state state;
uint32_t released_since;
bool timing_release;
} power_ctrl;
static void power_init(power_ctrl *p)
{
*p = (power_ctrl){ .state = RUN };
}
static bool power_step(power_ctrl *p, uint32_t now_ms,
bool shutdown_request, bool button_down,
bool services_quiet)
{
if (p->state == RUN && shutdown_request)
p->state = STOPPING;
if (p->state == STOPPING) {
if (button_down) {
p->timing_release = false;
} else if (!p->timing_release) {
p->released_since = now_ms;
p->timing_release = true;
}
if (p->timing_release && services_quiet &&
(uint32_t)(now_ms - p->released_since) >= UINT32_C(50))
p->state = PARKED;
}
return p->state != PARKED; /* desired POWER_IO, not power-good */
}
返回值是期望的保持电平。上层每轮调用后都要检查实际 GPIO 写入的返回状态;进入 PARKED 只表示软件撤回了自身使能,不代表测得断电。STOPPING 和 PARKED 期间都不再派发普通业务,停机请求应由各模块幂等处理。
PARKED 也不是死循环或“直接关掉调度器”。仍供电时应让出 CPU,并按系统设计维持调度和看门狗契约。否则看门狗复位后重新执行初始化,依然可能把机器启动起来。复位后的启动策略、是否允许 USB 插入唤醒,要在产品层单独确定;本例不承诺跨复位保持关机状态。
把拔线时容易掩盖的问题先测出来
将下面的自测段接在同一文件后。断言在优化构建中也保持启用,否则测试可能看起来成功,实际没有执行条件检查。
#ifdef SELF_TEST
#ifdef NDEBUG
#undef NDEBUG /* Self-tests must execute even in optimized builds. */
#endif
#include <assert.h>
#include <stdio.h>
static void ready_and_button(void)
{
power_ctrl p;
power_init(&p);
assert(power_step(&p, 0, false, false, true));
assert(p.state == RUN);
assert(power_step(&p, 10, true, true, false));
assert(power_step(&p, 1000, false, true, true));
assert(p.state == STOPPING); /* pressed key still powers EN */
assert(power_step(&p, 1010, false, false, true));
assert(power_step(&p, 1059, false, false, true));
assert(!power_step(&p, 1060, false, false, true));
assert(p.state == PARKED);
puts("PASS held button and 49/50 ms boundary");
}
static void bounce_and_busy(void)
{
power_ctrl p;
power_init(&p);
assert(power_step(&p, 0, true, false, true));
assert(power_step(&p, 49, false, true, true));
assert(power_step(&p, 50, false, false, false));
assert(power_step(&p, 99, false, false, true));
assert(power_step(&p, 100, false, false, false));
assert(!power_step(&p, 101, false, false, true));
puts("PASS bounce restarts timer and busy vetoes release");
}
static void rollover(void)
{
power_ctrl p;
power_init(&p);
uint32_t start = UINT32_MAX - UINT32_C(20);
assert(power_step(&p, start, true, false, true));
assert(power_step(&p, start + UINT32_C(49), false, false, true));
assert(!power_step(&p, start + UINT32_C(50), false, false, true));
puts("PASS uint32 timer rollover");
}
static void parked_is_terminal(void)
{
power_ctrl p;
power_init(&p);
assert(power_step(&p, 0, true, false, true));
assert(!power_step(&p, 50, false, false, true));
for (uint32_t t = 51; t < 10051; ++t) {
assert(!power_step(&p, t, (t & 1) != 0,
(t & 2) != 0, (t & 4) != 0));
assert(p.state == PARKED);
}
puts("PASS 10000 parked steps never reassert hold");
}
static void enable_paths(void)
{
/* Boolean connectivity model only: adequate voltage assumed. */
for (unsigned h = 0; h < 2; ++h)
for (unsigned k = 0; k < 2; ++k)
for (unsigned u = 0; u < 2; ++u) {
bool en = h || k || u;
assert(en == ((h + k + u) != 0));
if (!h && (k || u)) assert(en);
}
puts("PASS 8 enable-path combinations (not analog simulation)");
}
int main(void)
{
ready_and_button();
bounce_and_busy();
rollover();
parked_is_terminal();
enable_paths();
puts("ALL PASS; no target build or hardware measurements");
return 0;
}
#endif
可用 C11 编译器在主机执行。例如使用 Zig 的 C 前端,Windows 下运行:
zig cc -std=c11 -O2 -Wall -Wextra -Werror -DSELF_TEST power_release.c -o power_release_test.exe
.\power_release_test.exe

图6 本次实际主机运行结果的整理图,覆盖按键长按、释放抖动、业务未就绪、计时回卷及撤回后的持续状态。它不是电路模拟波形。
回归里特意让 PARKED 后的输入继续变化 10000 次,确认保持输出不会复活。另一个 8 组合检查只验证三路“或”的连接模型,假定各有效输入已有足够电压;它不能证明真实板上二极管压降、USB 电压波动或 EN 裕量合格。
把代码接入设备前,再用示波器同时观察 POWER_IO、EN 和 3.3V,分别测试电池独供、USB 接入、关机时按住按键、关机后 USB 插拔。若 POWER_IO 已低而 EN 仍高,先定位是哪条支路维持;若 EN 已低而电源轨仍不降,再查外设反灌及储能释放。本文没有实物测量,不给出关断时间、待机电流或续航数字。
电路可以有多个供电理由,软件却需要明确知道自己已经退出业务。把“撤回保持”和“物理掉电”拆开,插着 USB 调试时,关机流程才不会变成一个靠断电遮住错误的巧合。
资料与验证范围
电路与 EDA 图来源于野生程序员的 gsm-fm桌面收音机工程,项目及 EDA 标注 GPL 3.0。图2~5由同一 EDA 工程重新导出后裁剪、等比缩放,保留对应许可与署名;板上丝印 GSM-FM-V5.0,电源页 V1.0 更新于 2023-02-20,MCU 页更新于 2022-12-19。工程入口提供对应可编辑源文件。
项目 https://oshwhub.com/yeshengchengxuyuan/gsm-fm-5-0
EDA https://pro.lceda.cn/editor#id=bef7b7e17d1949dcba9ab1b967054271
固件入口 https://gitee.com/gsm-fm/gsm-fm-software (本次未复制或分发其固件代码)
器件资料 https://www.maxlinear.com/spx3819 (SPX3819 Rev.2.0.5,EN 电气参数及引脚说明)
引脚资料 https://www.espressif.com/sites/default/files/documentation/esp32-s3_datasheet_en.pdf
许可 https://www.gnu.org/licenses/gpl-3.0.html
本文独立控制器 v1.0.0 采用 MIT 许可,验证环境为 Zig 0.13.0 的 C 前端与 Windows x86_64。已完成主机编译、逻辑回归和关键焊盘交叉核对;未完成 ESP32 目标固件集成、整机编译或硬件测量。图1为控制关系示意,图6来自实际主机测试,不表示电路改版或实测结果。
技术交流,欢迎关注微信公众号:美男子玩编程。



