ESP32拉低电源保持脚后为何仍不关机

2026-10-284
AI 快速预览详细 收起
本文探讨了ESP32在关机过程中遇到的问题,即按下关机键后,程序已经把电源保持脚拉低,但设备仍未关机。文章详细解析了电路使能路径和软件退出路径,并提供了一个有明确终态的控制器解决方案。通过实现这个控制器,可以确保关机流程的确定性。文章还提供了完整的代码示例和测试验证方法。

按下关机键,程序已经把电源保持脚拉低,屏幕却还亮着。拔掉 USB,它又真的关机了。

这个现象不一定是 GPIO 失灵。自保持电源往往有不止一个“开门人”。主控撤回自己的使能,USB 或按键仍可能把稳压器的 EN 托在高电平。更容易被忽略的后半程是,程序以为下一条指令不会再执行,结果供电没断,业务循环继续跑,甚至又把保持脚拉高。

这次把电路的使能路径和软件的退出路径放在一起核对,并实现一个有明确终态的控制器。它解决的是关机流程的确定性,不把“写了低电平”等同于“整机已经没电”。

三路使能关系示意

图1 根据实际连接绘制的路径示意,只表达控制关系;导通压降和逻辑门限仍以器件、电路为准。

EN 并不只听主控一个人的

先看电源页。U1 使用 SPX3819M5-L-3-3/TR,3 脚是 EN,5 脚输出 3.3V。沿着 EN 向外追,会遇到三条输入路径。

实际EDA电源使能局部

图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 脚。

MCU电源控制引脚

图3 MCU 页左、右引脚分别裁取后纵向排版,便于手机阅读;GPIO1、GPIO2 和 GPIO45 与网络名一一对应。

同一工程的 PCB 网络面板进一步给出了对应关系:D2、D3、D4 的 1 焊盘和 U1 的 3 焊盘都属于 $3N1359;D2 的 2 焊盘与 U5 的 51 焊盘属于 POWER_IO。先验证这些连接,再讨论软件,能避免拿着一个版本的图去解释另一个版本的板。

PCB顶面整体

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

PCB底面整体

图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来自实际主机测试,不表示电路改版或实测结果。

技术交流,欢迎关注微信公众号:美男子玩编程。

创作许可协议

本项目采用 CC BY(署名) 进行许可。

评论(0)
- 没有更多了 -