ESP32 低功耗模式下的 RTC 与 ULP 协处理器:实现异步 GPIO 唤醒且不丢中断

在嵌入式开发中,低功耗设计是物联网设备的核心挑战之一。ESP32 的深度睡眠(Deep Sleep)模式可将功耗降至微安级,但默认情况下,CPU 停止运行,外设大多关闭。然而,RTC 外设(如 RTC GPIO、ULP 协处理器)仍可工作,这为异步事件检测提供了可能。本文将聚焦于如何利用 ULP 协处理器实现 GPIO 唤醒,同时确保事件不丢失,适用于需要超低功耗且对实时性有要求的场景(如智能门锁、环境传感器)。

原理概述

ESP32 的电源域与低功耗模式

ESP32 在深度睡眠时,主系统(包括 CPU、Wi-Fi、蓝牙)断电,但 RTC 域(RTC 内存、RTC 外设、ULP 协处理器)保持供电。RTC 外设包括 RTC GPIO、RTC 定时器、ULP 协处理器等。ULP 协处理器是一个超低功耗的 RISC-V 处理器,可在主 CPU 休眠时执行简单程序,访问 RTC 外设和传感器,并在满足条件时唤醒主 CPU。

异步 GPIO 唤醒与中断丢失问题

传统 GPIO 唤醒通常使用 EXT0/EXT1 唤醒源,但它们在深度睡眠时仅支持边沿触发,且如果事件发生在唤醒过程中(如 CPU 正在启动),可能丢失。ULP 协处理器则能持续监控 GPIO 状态,通过轮询或边沿检测记录事件,并在唤醒后由主 CPU 读取,从而避免丢失。此外,ULP 可执行更复杂的逻辑(如去抖、多引脚组合),提升可靠性。

ULP 协处理器的工作流程

  1. 主 CPU 配置 ULP 程序并加载到 RTC 内存。
  2. 进入深度睡眠,ULP 开始运行。
  3. ULP 定期采样 GPIO,检测到预设条件(如电平变化)时,可设置唤醒标志并触发系统唤醒。
  4. 主 CPU 唤醒后,读取 ULP 共享内存中的事件数据,进行处理。

硬件与软件准备

  • 硬件:ESP32 开发板(如 ESP32-DevKitC),一个按键连接到 GPIO2(RTC GPIO 引脚),另一端接 GND。
  • 软件:ESP-IDF v4.4 或更高版本,或 Arduino-ESP32(但本文以 ESP-IDF 为例,因其对 ULP 支持更完善)。

配置步骤

1. 选择 RTC GPIO 引脚

ESP32 的某些 GPIO 具有 RTC 功能,可通过 RTC_GPIO_IS_VALID_GPIO 宏检查。例如,GPIO2 是有效的 RTC GPIO。在 ULP 程序中,我们使用 RTC_GPIO 编号(如 RTC_GPIO_NUM_2)。

2. 编写 ULP 程序

ULP 程序使用汇编语言,但 ESP-IDF 提供了 ulp_riscv 工具链(对于 ESP32-S2/S3)或 ulp 汇编器(对于 ESP32)。本文以经典 ESP32 为例,使用 ulp 汇编。ULP 程序需完成:

  • 初始化 GPIO 为输入模式(带上拉)。
  • 循环读取 GPIO 状态,检测下降沿(按键按下)。
  • 若检测到事件,设置一个标志变量(位于 RTC 内存),并触发唤醒。

3. 主程序配置

主程序负责:

  • 加载 ULP 程序并启动。
  • 配置深度睡眠唤醒源(仅 ULP)。
  • 进入深度睡眠。
  • 唤醒后读取 ULP 变量,处理事件。

代码示例

ULP 程序(汇编)

// ulp_gpio_wakeup.S
#include "soc/rtc_io_reg.h"
#include "soc/rtc_cntl_reg.h"
#include "soc/soc_ulp.h"

    .data
.global wakeup_flag
wakeup_flag: .long 0

    .text
.global entry
entry:
    // 设置 GPIO2 为输入,带上拉
    WRITE_RTC_REG(RTC_IO_TOUCH_PAD1_REG, RTC_IO_TOUCH_PAD1_RDE_S, 1, 1) // 使能上拉
    WRITE_RTC_REG(RTC_IO_TOUCH_PAD1_REG, RTC_IO_TOUCH_PAD1_RUE_S, 1, 0) // 禁用下拉
    WRITE_RTC_REG(RTC_IO_TOUCH_PAD1_REG, RTC_IO_TOUCH_PAD1_DIE_S, 1, 0) // 禁用数字输入?实际需配置为输入
    // 更简单:使用 RTC_GPIO 配置,但需参考具体寄存器
    // 这里简化,假设已配置好

loop:
    // 读取 GPIO2 状态(通过 RTC_GPIO_IN_REG)
    READ_RTC_REG(RTC_GPIO_IN_REG, RTC_GPIO_IN_NEXT_S, 1) // 读取位
    // 假设低电平表示按下,检测下降沿
    // 使用状态变量 prev_state
    // 此处省略详细边沿检测,仅演示轮询
    // 若检测到低电平,则设置标志并唤醒
    and r0, r0, 1
    jumpr set_flag, eq, 0  // 如果为0(低电平),跳转
    // 否则继续循环
    jmp loop

set_flag:
    // 设置 wakeup_flag 为 1
    move r0, 1
    st r0, wakeup_flag
    // 触发唤醒
    wake
    halt

注意:上述汇编为简化示例,实际需根据 ESP32 技术参考手册配置 RTC GPIO 寄存器。更推荐使用 ESP-IDF 的 ulp 工具链,并参考官方示例 ulp_adculp_gpio

主程序(C 语言)

#include <stdio.h>
#include "esp_sleep.h"
#include "ulp.h"
#include "soc/rtc_cntl_reg.h"
#include "soc/rtc_io_reg.h"
#include "soc/soc_ulp.h"

extern const uint8_t ulp_main_bin_start[] asm("_binary_ulp_main_bin_start");
extern const uint8_t ulp_main_bin_end[] asm("_binary_ulp_main_bin_end");

// ULP 变量地址
static uint32_t wakeup_flag_addr;

void app_main(void) {
    // 1. 初始化 ULP 程序
    esp_err_t err = ulp_load_binary(0, ulp_main_bin_start, (ulp_main_bin_end - ulp_main_bin_start) / sizeof(uint32_t));
    if (err != ESP_OK) {
        printf("ULP load failed\n");
        return;
    }

    // 2. 获取 wakeup_flag 的地址
    wakeup_flag_addr = ulp_process_macros_and_load(0, ulp_main_bin_start);
    // 实际上,需要先定义宏,这里简化
    // 正确做法:使用 ulp_handle_t 和 ulp_var_address()
    // 示例:
    // ulp_var_address(wakeup_flag);

    // 3. 配置 RTC GPIO 为输入(在 ULP 程序中已配置,但也可在此配置)
    // 例如:rtc_gpio_init(GPIO_NUM_2); rtc_gpio_set_direction(GPIO_NUM_2, RTC_GPIO_MODE_INPUT_ONLY);

    // 4. 启动 ULP
    ulp_run(0);

    // 5. 配置深度睡眠唤醒源为 ULP
    esp_sleep_enable_ulp_wakeup();

    // 6. 进入深度睡眠
    printf("Entering deep sleep\n");
    esp_deep_sleep_start();

    // 唤醒后继续执行
    printf("Woke up!\n");
    uint32_t flag = ulp_read(wakeup_flag_addr);
    if (flag) {
        printf("GPIO event detected\n");
        // 处理事件,然后清除标志
        ulp_write(wakeup_flag_addr, 0);
    }
}

注意事项

  • ULP 程序调试:ULP 程序无法直接打印,需通过共享内存变量与主 CPU 通信。建议使用 ulp_read/ulp_write 访问变量。
  • GPIO 配置:确保所选 GPIO 支持 RTC 功能,并在 ULP 程序中正确配置上拉/下拉。对于按键,建议使用上拉输入,检测低电平。
  • 去抖处理:机械按键存在抖动,ULP 程序中可加入延时或多次采样逻辑,避免误触发。
  • 唤醒延迟:ULP 唤醒主 CPU 需要一定时间(约 100μs),若对实时性要求极高,可考虑使用 RTC 定时器辅助。
  • 功耗优化:ULP 运行频率默认 8MHz,可降低至 2MHz 以节省功耗,但会降低采样速度。
  • 兼容性:ESP32-S2/S3 使用 ulp-riscv,程序结构不同,需参考对应文档。

总结

通过 ULP 协处理器,ESP32 在深度睡眠下仍能可靠监控 GPIO 事件,且不丢失中断。本文提供了原理和代码框架,实际项目需根据具体需求调整。掌握这一技术,能显著提升嵌入式设备的续航能力和响应可靠性。