ESP32 低功耗模式下 RTC 内存保持与 WiFi 快速重连的冲突处理技巧

引言

在电池供电的 IoT 设备中,ESP32 的 deep sleep 模式是降低功耗的关键,但唤醒后 WiFi 重连的延迟往往成为痛点。同时,RTC 内存(RTC Fast Memory 和 RTC Slow Memory)在睡眠期间保持数据,但若与 WiFi 驱动冲突,可能导致数据损坏或连接失败。本文将从底层原理出发,剖析冲突根源,并给出实战代码。

1. 原理剖析:RTC 内存与 WiFi 的冲突根源

1.1 ESP32 的电源域与内存布局

ESP32 包含两个主要电源域:

  • VDD_SDIO:为 SDIO、WiFi、蓝牙等外设供电,在 deep sleep 时默认关闭。
  • RTC 域:始终供电,包含 RTC 定时器、ULP 协处理器以及 RTC 快速内存(8KB)和 RTC 慢速内存(8KB)。

RTC 内存用于在睡眠期间保存变量,但 WiFi 驱动在初始化时会占用部分 RTC 内存作为校准数据(如 RF 校准、MAC 地址等)。若用户数据与驱动数据重叠,唤醒后 WiFi 初始化可能覆盖用户数据,或导致驱动异常。

1.2 冲突的具体表现

  • 数据覆盖:用户存储在 RTC 内存的变量被 WiFi 驱动覆盖,导致数据丢失。
  • 重连延迟:唤醒后,WiFi 需要重新加载校准数据,若 RTC 内存被破坏,则需重新校准,增加 100-500ms 延迟。
  • 死机或重启:严重冲突可能导致系统崩溃。

2. 解决方案:分区规划与快速重连策略

2.1 RTC 内存分区规划

ESP-IDF 提供 RTC_NOINIT_ATTRRTC_DATA_ATTR 属性,分别用于不初始化和初始化变量。但更安全的方式是使用 esp_sleep_get_rtc_retain_mem()esp_sleep_set_rtc_retain_mem() 来管理保留内存区域,避免与驱动冲突。

推荐做法:

  • 将用户数据放在 RTC 慢速内存的高地址区域,通过 RTC_SLOW_MEM 宏手动偏移。
  • 使用 esp_partitionnvs_flash 存储关键数据,但 RTC 内存更适合快速访问。

2.2 WiFi 快速重连优化

  • 保存 WiFi 配置:在睡眠前,将 SSID、密码、信道等信息存入 RTC 内存,唤醒后直接使用,避免重新扫描。
  • 使用 esp_wifi_set_ps(WIFI_PS_MIN_MODEM):在唤醒后立即设置省电模式,减少功耗。
  • 禁用自动扫描:调用 esp_wifi_connect() 前,先设置 esp_wifi_set_config() 并指定信道。

3. 代码实现:完整示例

以下代码演示如何安全使用 RTC 内存并实现快速重连。

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_sleep.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "nvs_flash.h"

// 定义 RTC 内存结构体,注意对齐
RTC_DATA_ATTR struct {
    uint32_t magic;
    char ssid[32];
    char password[64];
    uint8_t channel;
    int32_t rssi_threshold;
} wifi_config_rtc;

#define MAGIC_NUMBER 0x5A5A5A5A

// 保存 WiFi 配置到 RTC 内存
void save_wifi_config(const char *ssid, const char *pass, uint8_t ch) {
    wifi_config_rtc.magic = MAGIC_NUMBER;
    strncpy(wifi_config_rtc.ssid, ssid, sizeof(wifi_config_rtc.ssid) - 1);
    strncpy(wifi_config_rtc.password, pass, sizeof(wifi_config_rtc.password) - 1);
    wifi_config_rtc.channel = ch;
}

// 从 RTC 内存读取配置,并检查有效性
bool load_wifi_config(char *ssid, char *pass, uint8_t *ch) {
    if (wifi_config_rtc.magic != MAGIC_NUMBER) {
        return false;
    }
    strncpy(ssid, wifi_config_rtc.ssid, 32);
    strncpy(pass, wifi_config_rtc.password, 64);
    *ch = wifi_config_rtc.channel;
    return true;
}

// WiFi 事件处理
static void wifi_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) {
    if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    } else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
        // 重连尝试
        esp_wifi_connect();
    }
}

// 初始化 WiFi 并快速连接
void wifi_init_fast() {
    nvs_flash_init();
    esp_netif_init();
    esp_event_loop_create_default();
    esp_netif_create_default_wifi_sta();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&cfg);
    esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL);

    char ssid[32], pass[64];
    uint8_t ch;
    if (load_wifi_config(ssid, pass, &ch)) {
        wifi_config_t wifi_config = {
            .sta = {
                .ssid = (uint8_t *)ssid,
                .password = (uint8_t *)pass,
                .channel = ch,
                .threshold.authmode = WIFI_AUTH_WPA2_PSK,
            },
        };
        esp_wifi_set_mode(WIFI_MODE_STA);
        esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
        esp_wifi_start();
        // 设置省电模式
        esp_wifi_set_ps(WIFI_PS_MIN_MODEM);
    } else {
        // 首次启动,需要完整扫描
        esp_wifi_set_mode(WIFI_MODE_STA);
        esp_wifi_start();
        // 假设后续通过配网获取配置
    }
}

// 进入深度睡眠
void enter_deep_sleep() {
    // 保存当前 WiFi 配置(假设已连接)
    wifi_config_t conf;
    esp_wifi_get_config(WIFI_IF_STA, &conf);
    save_wifi_config((char *)conf.sta.ssid, (char *)conf.sta.password, conf.sta.channel);

    // 设置唤醒源为定时器
    esp_sleep_enable_timer_wakeup(60 * 1000000); // 60秒
    esp_deep_sleep_start();
}

void app_main() {
    // 初始化 WiFi
    wifi_init_fast();

    // 模拟工作
    vTaskDelay(pdMS_TO_TICKS(5000));

    // 进入睡眠
    enter_deep_sleep();
}

4. 注意事项与调试技巧

  • RTC 内存大小限制:RTC 快速内存仅 8KB,慢速内存 8KB,且部分被系统占用,用户可用约 6KB。务必使用 RTC_DATA_ATTRRTC_NOINIT_ATTR,并避免大数组。
  • 对齐问题:结构体在 RTC 内存中可能未对齐,使用 __attribute__((aligned(4))) 确保安全。
  • WiFi 驱动占用:在调用 esp_wifi_init() 前,不要写入 RTC 内存,因为初始化会覆盖部分区域。建议在初始化后保存数据。
  • 快速重连的代价:指定信道可省去扫描,但若环境变化,可能导致连接失败。建议设置重试机制,如失败后回退到全信道扫描。
  • 功耗与延迟权衡:若对延迟要求极高,可使用 esp_wifi_set_ps(WIFI_PS_NONE),但功耗增加。根据应用场景调整。

5. 总结

通过合理规划 RTC 内存使用,并利用保存的 WiFi 配置进行快速重连,ESP32 可以在保持低功耗的同时,将唤醒后的连接时间从 1-2 秒降至 200ms 左右。关键在于理解 RTC 内存与 WiFi 驱动的交互,并采用防御性编程。希望本文的技巧能帮助你在实际项目中取得平衡。