ESP32 低功耗实战:RTC 内存保持与 ULP 协处理器协同唤醒的深度配置

在物联网边缘节点、可穿戴设备等场景中,功耗直接决定产品续航。ESP32 的深度睡眠(Deep Sleep)模式能将功耗降至 ~10µA,但唤醒后重新初始化外设和变量会消耗时间和能量。幸运的是,ESP32 提供了 RTC 内存ULP 协处理器,两者结合可实现“数据保持 + 智能唤醒”的黄金组合。

一、核心原理:RTC 内存与 ULP 协处理器

1. RTC 内存(RTC Fast Memory)

  • 位于 RTC 域,深度睡眠期间由 RTC 电源供电,数据不会丢失。
  • 容量有限:ESP32 有 8KB 慢速 RTC 内存和 8KB 快速 RTC 内存,通常使用快速内存(地址 0x3FF80000 起)。
  • 通过 RTC_DATA_ATTR 属性将全局变量放入 RTC 内存,或使用 esp_sleep_get_wakeup_cause() 判断唤醒源。

2. ULP 协处理器(Ultra Low Power Processor)

  • 一个极低功耗的 RISC-V 核心,可在主 CPU 睡眠时独立运行。
  • 可访问 RTC 内存、GPIO、I2C 等外设,支持 ADC 采样、脉冲计数等。
  • 通过 ulp_set_wakeup_period() 设置周期唤醒,或通过 ulp_wakeup() 主动唤醒主 CPU。

3. 协同工作流程

  • 主 CPU 进入 Deep Sleep 前,将 ULP 程序加载到 RTC 内存,并配置唤醒源。
  • ULP 按周期运行,读取传感器数据,写入 RTC 内存中的变量。
  • 当 ULP 检测到特定条件(如阈值超限),调用 wake_up 指令唤醒主 CPU。
  • 主 CPU 从 RTC 内存读取累积数据,处理后再进入睡眠。

二、实战配置:温度监测 + 阈值唤醒

假设需求:使用内置温度传感器,每 10 秒采样一次,若温度超过 40°C 则唤醒主 CPU 并打印数据,否则继续睡眠。

1. 硬件准备

  • ESP32 DevKit 开发板(内置温度传感器,无需外接)。
  • 可选:外接 LED 指示唤醒状态。

2. 软件环境

  • ESP-IDF v4.4 或 v5.x(本文以 v5.1 为例)。
  • 启用 ULP 支持:在 menuconfig 中设置 Component config → ESP32-specific → Ultra Low Power (ULP) Coprocessor,选择 ULP RISC-V

3. 编写 ULP 程序(RISC-V 汇编)

ULP 程序需用汇编编写,但 ESP-IDF 提供了 ulp_riscv 宏和工具链。为简化,我们使用 ulp_riscv 的 C 语言支持(需在 CMakeLists.txt 中配置)。

ULP 程序文件 ulp_temperature.S(汇编)或 ulp_temperature.c(需额外配置)。这里采用官方推荐的汇编方式,但为可读性,使用 C 语言接口(ESP-IDF v5.1 支持)。

// ulp_temperature.c
#include "ulp_riscv.h"
#include "ulp_temperature.h"

volatile uint32_t temperature_raw; // 存储 ADC 原始值
volatile uint32_t wake_flag;       // 唤醒标志

int main(void) {
    // 初始化 ADC 通道(内部温度传感器对应 ADC1_CH0)
    ulp_riscv_adc_init();
    ulp_riscv_adc_enable(ADC1_CH0);
    
    // 采样并转换(简化:直接读取原始值)
    temperature_raw = ulp_riscv_adc_read(ADC1_CH0);
    
    // 简单阈值判断:假设原始值 > 3000 对应 40°C(需根据实际校准)
    if (temperature_raw > 3000) {
        wake_flag = 1;
        ulp_riscv_wakeup_main_processor();
    } else {
        wake_flag = 0;
    }
    
    return 0;
}

注意:ULP 程序中的变量需在头文件 ulp_temperature.h 中声明,并指定 RTC 内存地址。

// ulp_temperature.h
#pragma once
#include "ulp_riscv.h"

extern volatile uint32_t temperature_raw;
extern volatile uint32_t wake_flag;

4. 主程序配置

// main.c
#include <stdio.h>
#include "esp_sleep.h"
#include "ulp_riscv.h"
#include "ulp_temperature.h"

extern const uint8_t ulp_bin_start[] asm("_binary_ulp_temperature_bin_start");
extern const uint8_t ulp_bin_end[] asm("_binary_ulp_temperature_bin_end");

void app_main(void) {
    // 1. 加载 ULP 程序到 RTC 内存
    esp_err_t err = ulp_riscv_load_binary(ulp_bin_start, ulp_bin_end - ulp_bin_start);
    ESP_ERROR_CHECK(err);
    
    // 2. 设置 ULP 唤醒周期(10 秒)
    ulp_set_wakeup_period(0, 10 * 1000000); // 单位:微秒
    
    // 3. 配置唤醒源:ULP 唤醒
    ESP_ERROR_CHECK(esp_sleep_enable_ulp_wakeup());
    
    // 4. 进入深度睡眠
    printf("Entering deep sleep...\n");
    esp_deep_sleep_start();
}

// 唤醒后的回调(通过 RTC 内存变量判断)
void wakeup_handler(void) {
    // 读取 RTC 内存中的变量
    printf("Wake up! Temperature raw: %lu, flag: %lu\n", 
           (unsigned long)temperature_raw, (unsigned long)wake_flag);
    // 处理数据...
}

注意app_main 中调用 esp_deep_sleep_start() 后,代码不会继续执行。唤醒后,系统从 app_main 重新开始,因此需在开头判断唤醒原因。

5. 完整唤醒流程修正

void app_main(void) {
    // 获取唤醒原因
    esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
    
    if (cause == ESP_SLEEP_WAKEUP_ULP) {
        // ULP 唤醒:处理数据
        printf("ULP wakeup! Temperature raw: %lu\n", (unsigned long)temperature_raw);
        // 可在此处执行网络上传、日志等
    } else {
        // 首次启动或其它唤醒源
        printf("Normal boot or other wakeup\n");
        // 初始化 ULP 并进入睡眠
        load_and_start_ulp();
    }
    
    // 重新进入睡眠(或根据业务逻辑决定)
    esp_deep_sleep_start();
}

void load_and_start_ulp(void) {
    // 加载 ULP 程序
    esp_err_t err = ulp_riscv_load_binary(ulp_bin_start, ulp_bin_end - ulp_bin_start);
    ESP_ERROR_CHECK(err);
    
    // 设置周期
    ulp_set_wakeup_period(0, 10 * 1000000);
    
    // 使能 ULP 唤醒
    ESP_ERROR_CHECK(esp_sleep_enable_ulp_wakeup());
    
    // 启动 ULP
    ESP_ERROR_CHECK(ulp_riscv_run());
}

6. CMakeLists.txt 配置

在组件目录下,需添加 ULP 支持:

# CMakeLists.txt
idf_component_register(SRCS "main.c"
                       INCLUDE_DIRS ".")

# 添加 ULP 支持
set(ULP_SOURCES "ulp_temperature.c")
ulp_riscv_embed_binary(ulp_temperature)

三、关键注意事项

  • RTC 内存变量访问:在 ULP 程序和主程序间共享变量时,必须使用 volatile 且确保地址对齐。
  • ADC 校准:内置温度传感器的 ADC 值需查表或线性校准,示例中的阈值 3000 仅为演示。
  • ULP 程序大小:RTC 快速内存有限,ULP 程序需精简,避免大数组。
  • 唤醒周期精度ulp_set_wakeup_period 的周期基于 RTC 定时器,精度受晶振影响,通常 ±10%。
  • 调试技巧:在 ULP 程序中可设置 GPIO 翻转,用示波器观察唤醒周期。
  • 功耗优化:ULP 运行期间功耗约 20µA,加上 RTC 内存保持,整体可控制在 30µA 以下。

四、总结

通过 RTC 内存与 ULP 协处理器的协同,ESP32 实现了“低功耗待机 + 智能事件唤醒”的完美方案。ULP 负责持续监测,主 CPU 仅在必要时唤醒,极大延长了电池寿命。本文的实战配置可直接用于温度、湿度、运动传感器等场景,只需修改 ULP 程序中的采样逻辑和阈值判断。掌握这一技术,你的嵌入式产品将更具竞争力。