大飞的技术博客

·4 分钟·

ESP32-C3 怎么从深度睡眠醒来:GPIO0-5、定时器与一坑三折

PocketOracle 是电池供电的:绝大多数时间深度睡眠,按键(或亮屏时的摇晃)才点亮 墨水屏抽一签。「怎么醒」这件事在这个项目上走完了完整的弧线:先踩到 ESP32-C3 的硬件红线,再赶上唯一接进 RTC 域的中断脚恰好有硬件缺陷,固件用定时器轮询 硬扛了几个版本,最后靠硬件改版收尾。这篇文章把整条线捋一遍。

现在的状态机长这样:

深度睡眠 ──KEY1 按下 / 定时器──▶ 醒来,抽签上屏 ──3 秒无操作──▶ 回去睡

硬件红线:只有 GPIO0-5 能做唤醒源

ESP32-C3 深度睡眠的 GPIO 唤醒只认 GPIO0–5(RTC 域引脚),其他引脚睡死过去就再也 叫不醒了。这块板子把两条路都踩了一遍:

  • 旧版布线里,加速度计 INT1 在 GPIO19、一个按键在 GPIO20——运行时当 输入随便用,但不能做睡眠唤醒源;
  • 唯一接进 RTC 域的是加速度计 INT2(GPIO2),它被指定为唤醒引脚——偏偏还是 个坏的(下文);

唤醒 API 是电平触发,不是边沿:

esp_deep_sleep_enable_gpio_wakeup(1ULL << PIN_KEY1, ESP_GPIO_WAKEUP_GPIO_LOW);

设计教训最终由改版落实:新版 PCB 把主按键(SW1,固件里叫 KEY1)挪到了 GPIO2,按键唤醒从此有了合法身份;摇晃只在亮屏窗口内当「换签」手势, 不再承担唤醒。

醒来第一件事:谁叫醒的我

深度睡眠醒来等于一次重启:ROM → 二级 bootloader → app_main 从头跑,RAM 内容不保留。入口先用唤醒原因分诊:

bool first_boot = esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_UNDEFINED;
if (first_boot) {
    enter_deep_sleep(SLEEP_US_12H);   // 装电池后的第一次开机:布置唤醒源,立刻回去睡
    return;
}
// 走到这里:KEY1 唤醒或定时器到点 —— 量电量、抽签、上屏

冷启动分支很关键:设备装电池那一刻开始,固件的任务就是布置好唤醒源然后立刻 消失,绝不能「亮一下屏表示开机」——现场没有人在看。

现役方案:按键电平唤醒 + 长短两档定时器

进睡前布置唤醒源:KEY1 低电平唤醒 + 一个兜底定时器——哪怕按键彻底失效, 设备也会定期醒来刷一次存在感:

void enter_deep_sleep(uint64_t sleep_us)
{
    // hold 住 I2C 引脚,让 RTC 域的 SC7A20 在睡眠中不掉电
    if (sc7a20_init() == ESP_OK) {
        gpio_hold_en(PIN_I2C_SDA);
        gpio_hold_en(PIN_I2C_SCL);
        gpio_deep_sleep_hold_en();
    }
    esp_deep_sleep_enable_gpio_wakeup(1ULL << PIN_KEY1, ESP_GPIO_WAKEUP_GPIO_LOW);
    esp_sleep_enable_timer_wakeup(sleep_us);   // 正常 12h;低电量时由上层缩短为 1h
    esp_deep_sleep_start();
}

定时器间隔现在是电源策略的入参:电量正常睡 12 小时,低电量缩短到 1 小时 巡检(见电源篇)。

弯路:唯一的唤醒脚是坏的,轮询硬扛了几个版本

旧板上被寄予厚望的 INT2(GPIO2)是 stuck HIGH 的硬件缺陷——引脚被外部驱动 为高,而唤醒要等的恰恰是高电平,挂上唤醒源芯片瞬间就被自己叫醒。等改版 之前,固件切到 POLL_MODE 硬扛:

深度睡眠(3 秒定时器)
   │ 醒来
   ▼
看 INT1(GPIO19)是否 HIGH(摇晃锁存)、按键是否 LOW
   │ 都不是 → 立刻回去睡 3 秒,循环
   │ 有触发
   ▼
抽签上屏,3 秒窗口内可连着换签
  • 传感器侧照常攒事件:SC7A20 靠 I2C 引脚的 gpio_hold 在睡眠中保住供电 和配置,摇晃会把 INT1 锁存成 HIGH,等着主控来读;
  • 主控每 3 秒付一次「假醒」的成本:完整跑一遍 bootloader → app_main → 自检 → I2C 初始化 → 读几个电平 → 回睡,单次几百毫秒;
  • 代价是响应延迟:摇晃到亮屏平均晚 1.5 秒、最差 3 秒。对一个「摇签」 设备,这个延迟反而有点像抽签的仪式感。

轮询方案撑到了改版那天,随后和摇晃唤醒一起整体退役——新版板上 INT2 不再 接线,唤醒职责完全交给 GPIO2 上的按键。

唤醒后的世界是全新的

深度睡眠唤醒等于重启,三条直接影响写代码的方式:

  1. 所有外设重新初始化。SPI/I2C/GPIO 配置一个都省不掉;驱动里的 s_ready 这类「已初始化」标志是静态变量,重启后自动归零,恰好是对的。
  2. gpio_hold 穿越睡眠。唤醒后先 gpio_hold_dis 释放,再重新配置引脚, I2C 总线才能正常驱动——顺序反了就是「设备第二次交互就失灵」。
  3. 想跨睡眠记状态,只能走 RTC_DATA_ATTR 慢速内存或 NVS。这个项目的 签文每次随机,干脆不记,少一个状态同步问题。

小结

现在 enter_deep_sleep 缩到了十行:hold 住 I2C、挂按键唤醒、上定时器、睡。 回头看这一坑三折:GPIO0-5 的红线是布线时欠的账,轮询是给硬件缺陷打的临时 补丁,改版才是真正的还债。「硬件缺陷不可怕,可怕的是固件没有给硬件留退路」 ——退路的价值不是永远走下去,而是撑到硬件修好的那天。