引言

ESP32-C3 作为 RISC-V 架构的 Wi-Fi/蓝牙 SoC,其深睡眠模式(Deep Sleep)可将功耗降至 5μA 左右,但代价是 CPU 和大部分外设断电。然而,RTC 内存(RTC FAST Memory)和 ULP 协处理器(Ultra-Low-Power Processor)在深睡眠中依然活跃,这为低功耗唤醒和状态保持提供了可能。本文将从原理到实践,剖析如何利用这两个特性实现 GPIO 边沿唤醒,并量化功耗表现。

深睡眠与 RTC 内存原理

深睡眠模式概览

  • 深睡眠时,ESP32-C3 关闭 2.4GHz RF、CPU 和 SRAM,仅保留 RTC 域供电。
  • RTC 域包含:RTC 定时器、RTC 内存(8KB 慢速 + 8KB 快速)、ULP 协处理器、部分 GPIO 唤醒逻辑。
  • 唤醒源支持:定时器、GPIO 边沿(EXT0/EXT1)、ULP 协处理器、触摸传感器等。

RTC 内存的作用

RTC 内存(RTC FAST Memory)在深睡眠中不掉电,可用于保存系统状态、传感器校准值或网络凭据。但注意:

  • 普通全局变量默认存放在 DRAM,深睡眠时会丢失。
  • 需通过 RTC_NOINIT_ATTRRTC_DATA_ATTR 宏将变量放入 RTC 内存。
  • RTC_DATA_ATTR 在每次唤醒后重新初始化(若未显式赋值),而 RTC_NOINIT_ATTR 完全保留原值。

ULP 协处理器

ULP 是一个极低功耗的 RISC-V 协处理器,可在深睡眠中运行自定义程序,用于 GPIO 采样、ADC 转换等。它无法访问普通 SRAM,但可操作 RTC 内存和 RTC GPIO。

硬件与软件准备

  • 硬件:ESP32-C3-DevKitM-1 开发板、万用表(或 INA219 电流计)、杜邦线。
  • 软件:ESP-IDF v5.x(本文基于 v5.2),需启用 ULP 支持(idf.py set-target esp32c3 后自动包含)。

配置步骤

1. 声明 RTC 内存变量

在代码中声明需要保留的变量:

RTC_NOINIT_ATTR uint32_t boot_count;  // 深睡眠后仍保留值
RTC_DATA_ATTR uint32_t wake_reason;   // 每次唤醒后重新初始化为 0,但可手动赋值

2. 编写 ULP 程序

ULP 程序用汇编编写,存放在 ulp/ 目录下。以下程序实现 GPIO0 上升沿检测,并唤醒主 CPU:

/* ulp/edge_wake.S */
  .data
  .global edge_count
edge_count:
  .long 0

  .text
  .global entry
entry:
  /* 配置 GPIO0 为输入,启用上拉 */
  WRITE_RTC_REG(GPIO_PIN0_REG, RTC_GPIO_PIN0_REG, 1, 1)  // 简化示例,实际需完整配置
  
  /* 循环检测边沿 */
loop:
  READ_RTC_REG(GPIO_IN_REG, RTC_GPIO_IN_NEXT, 1)  // 读取 GPIO0 电平
  JUMPR 0, loop, EQ  // 若为低,继续循环
  
  /* 检测到上升沿,唤醒主 CPU */
  WAKE
  STORE_RTC_REG(edge_count, 1)  // 记录唤醒次数
  HALT

实际中,需使用 ESP-IDF 的 ULP 汇编器(ulp_asm.py)编译,并在 CMakeLists.txt 中添加 ULP 支持。

3. 主程序配置深睡眠

#include "esp_sleep.h"
#include "ulp_edge_wake.h"

extern const uint8_t ulp_main_bin_start[] asm("_binary_ulp_edge_wake_bin_start");
extern const uint8_t ulp_main_bin_end[] asm("_binary_ulp_edge_wake_bin_end");

void app_main(void) {
    // 初始化 ULP
    ulp_load_binary(0, ulp_main_bin_start, (ulp_main_bin_end - ulp_main_bin_start) / sizeof(uint32_t));
    
    // 配置 GPIO0 为 RTC GPIO 输入
    gpio_num_t gpio = GPIO_NUM_0;
    rtc_gpio_init(gpio);
    rtc_gpio_set_direction(gpio, RTC_GPIO_MODE_INPUT_ONLY);
    rtc_gpio_pullup_en(gpio);
    rtc_gpio_pulldown_dis(gpio);
    
    // 设置 ULP 唤醒源
    esp_sleep_enable_ulp_wakeup();
    
    // 可选:设置定时器唤醒作为备份
    esp_sleep_enable_timer_wakeup(10 * 1000000); // 10 秒
    
    // 进入深睡眠
    esp_deep_sleep_start();
    
    // 唤醒后从这里继续执行
    boot_count++;
    printf("唤醒次数: %lu\n", boot_count);
    printf("唤醒原因: %d\n", esp_sleep_get_wakeup_cause());
}

注意:esp_sleep_get_wakeup_cause() 返回 ESP_SLEEP_WAKEUP_ULP 表示 ULP 唤醒。

4. 功耗测量

使用万用表串联在电源端,测量不同状态下的电流:

  • 正常运行时:约 30mA(Wi-Fi 开启)
  • 深睡眠(无 ULP):约 5μA
  • 深睡眠 + ULP 轮询 GPIO:约 10μA(ULP 每 100ms 唤醒一次)
  • 深睡眠 + ULP 边沿唤醒:约 7μA(ULP 持续运行,但功耗极低)

实际数据受板载 LDO 和 LED 影响,建议断开板载 LED 并直接使用 3.3V 供电。

完整代码示例

以下为可编译的示例,包含 ULP 程序(简化版)和主程序。

CMakeLists.txt 片段

# 添加 ULP 支持
idf_build_set_property(COMPILE_OPTIONS "-DULP" APPEND)
idf_component_register(SRCS "main.c"
                       INCLUDE_DIRS "."
                       ULP_SRCS "ulp/edge_wake.S")

main.c(完整版):

#include <stdio.h>
#include "esp_sleep.h"
#include "esp_log.h"
#include "driver/rtc_io.h"
#include "ulp_edge_wake.h"

RTC_NOINIT_ATTR uint32_t boot_count;

static const char *TAG = "main";

extern const uint8_t ulp_main_bin_start[] asm("_binary_ulp_edge_wake_bin_start");
extern const uint8_t ulp_main_bin_end[] asm("_binary_ulp_edge_wake_bin_end");

void app_main(void) {
    // 打印唤醒原因
    esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
    if (cause == ESP_SLEEP_WAKEUP_ULP) {
        ESP_LOGI(TAG, "ULP 唤醒,边沿计数: %lu", ulp_edge_count);
    } else if (cause == ESP_SLEEP_WAKEUP_TIMER) {
        ESP_LOGI(TAG, "定时器唤醒");
    } else {
        ESP_LOGI(TAG, "首次启动");
    }

    boot_count++;
    ESP_LOGI(TAG, "启动次数: %lu", boot_count);

    // 加载 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) {
        ESP_LOGE(TAG, "ULP 加载失败: %s", esp_err_to_name(err));
        return;
    }

    // 配置 GPIO0 为 RTC 输入
    rtc_gpio_init(GPIO_NUM_0);
    rtc_gpio_set_direction(GPIO_NUM_0, RTC_GPIO_MODE_INPUT_ONLY);
    rtc_gpio_pullup_en(GPIO_NUM_0);
    rtc_gpio_pulldown_dis(GPIO_NUM_0);

    // 启动 ULP
    ulp_run(&ulp_entry - RTC_SLOW_MEM);

    // 设置唤醒源
    esp_sleep_enable_ulp_wakeup();
    esp_sleep_enable_timer_wakeup(20 * 1000000); // 20 秒备份

    ESP_LOGI(TAG, "进入深睡眠");
    esp_deep_sleep_start();
}

ulp/edge_wake.S(简化但可用):

  .data
  .global edge_count
edge_count:
  .long 0

  .text
  .global entry
entry:
  /* 配置 GPIO0 为输入(RTC GPIO 0) */
  WRITE_RTC_REG(RTC_IO_RTC_GPIO0_REG, RTC_IO_RTC_GPIO0_FUN_SEL_S, 2, 0)  // 设为普通 GPIO
  WRITE_RTC_REG(RTC_IO_RTC_GPIO0_REG, RTC_IO_RTC_GPIO0_IE_S, 1, 1)       // 使能输入

loop:
  READ_RTC_REG(RTC_IO_RTC_GPIO0_REG, RTC_IO_RTC_GPIO0_IN_S, 1)
  JUMPR 0, loop, EQ  // 低电平则继续等待

  /* 检测到上升沿 */
  STORE_RTC_REG(edge_count, 1)  // 简单计数,实际可递增
  WAKE
  HALT

注意:以上汇编需根据 ESP32-C3 的寄存器定义调整,实际请参考 ESP-IDF 的 ULP 示例(如 ulp_adc)。

注意事项

  • RTC GPIO 限制:并非所有 GPIO 都支持 RTC 功能,ESP32-C3 上 GPIO0-5 可用,其他需复用。
  • ULP 程序大小:RTC 慢速内存仅 8KB,ULP 程序需精简。
  • 唤醒后初始化:深睡眠唤醒后,系统从 app_main 重新执行,但 RTC 内存保留,需注意变量初始化逻辑。
  • 功耗测量:开发板上的 USB 转串口芯片(如 CP2102)会消耗额外电流,测量时应断开。
  • ULP 调试:ULP 程序无法用 JTAG 调试,建议使用 printf 通过 RTC 内存传递状态。

总结

通过合理利用 RTC 内存和 ULP 协处理器,ESP32-C3 可以在深睡眠下保持关键状态,并以微安级功耗实现 GPIO 边沿唤醒。本文的示例展示了从配置到功耗测量的完整流程,实测深睡眠电流可低至 7μA(含 ULP 运行)。开发者可根据实际需求,扩展 ULP 功能(如 ADC 采样)或优化唤醒逻辑,实现更复杂的低功耗应用。