Skip to main content

Optimizing IoT Battery Life: ESP32 Deep Sleep with RTC Memory Data Preservation

 In the constraint-heavy world of low-power IoT, the radio is your enemy. An ESP32 attempting to maintain a Wi-Fi connection consumes between 160mA and 260mA. In contrast, Deep Sleep mode draws approximately 10µA to 150µA (depending on the board design).

The mathematical reality is harsh: if you transmit data every time you sample a sensor (e.g., every minute), a 2500mAh battery might last a few days. If you batch data and transmit once an hour, that same battery can last months.

However, Deep Sleep introduces a significant architectural hurdle: Volatility. When the ESP32 enters Deep Sleep, the main CPU, wireless peripherals, and standard SRAM (volatile memory) are powered down. When the device wakes up, it does not resume execution where it left off; it reboots. It runs the bootloader and setup() from scratch. Consequently, your sensor reading variables, counters, and flags are wiped.

Writing to NVS (Non-Volatile Storage) or EEPROM every minute is not a viable solution due to flash wear leveling; you will burn out the flash cells within a year at high write frequencies.

The solution lies in the ESP32's RTC Slow Memory.

The Architecture of Sleep

To understand why data persists in RTC memory but not SRAM, we must look at the ESP32's power domains.

  1. PD_CPU (CPU Power Domain): Contains the Xtensa Lx6 cores and standard internal SRAM. During Deep Sleep, this domain is completely powered off.
  2. PD_RTC (RTC Power Domain): Contains the ULP (Ultra Low Power) coprocessor, the RTC controller, and—crucially—8KB of RTC Slow Memory.

During Deep Sleep, the voltage regulator keeps the PD_RTC domain alive. Any data stored in the memory addresses mapped to RTC Slow Memory remains intact as long as the battery is connected, even while the rest of the chip is effectively dead.

The challenge is instructing the linker to place specific variables into this protected memory segment rather than the standard heap or stack.

The Fix: Using RTC_DATA_ATTR

We will implement a robust data batching system. The device will wake up every 10 seconds, take a reading, store it in RTC memory, and go back to sleep. Once the buffer is full (every 6th reading), it will simulate a Wi-Fi connection to transmit the batch and reset the counter.

Implementation Details

This solution uses the RTC_DATA_ATTR macro provided by the Espressif toolchain to map variables to the RTC memory segment.

#include <Arduino.h>
#include <esp_sleep.h>

// CONFIGURATION
#define uS_TO_S_FACTOR 1000000ULL  // Conversion factor for micro seconds to seconds
#define TIME_TO_SLEEP  10          // Time ESP32 will go to sleep (in seconds)
#define BATCH_SIZE     6           // Send data after this many readings (e.g., 6 * 10s = 60s)

// DATA STRUCTURES

// Define a struct to hold our sensor data.
// We keep this simple, but in production, align your structs to 4 bytes
// to prevent padding issues if sharing with the ULP.
struct SensorReading {
    uint64_t timestamp;
    float temperature;
    float humidity;
};

// PERSISTENT MEMORY ALLOCATION
// The RTC_DATA_ATTR macro forces the linker to place these variables
// in the RTC Slow Memory (8KB max).
RTC_DATA_ATTR int bootCount = 0;
RTC_DATA_ATTR int readingIndex = 0;
RTC_DATA_ATTR SensorReading readingBuffer[BATCH_SIZE];

// MOCK SENSOR FUNCTION
// In a real app, this would read from DHT22, BME280, etc.
void captureSensorData(int index) {
    // Simulating sensor drift
    float simulatedTemp = 20.0 + (float)random(0, 50) / 10.0;
    float simulatedHum = 50.0 + (float)random(0, 20) / 10.0;

    readingBuffer[index].timestamp = millis(); // Relative time since boot (accumulation)
    readingBuffer[index].temperature = simulatedTemp;
    readingBuffer[index].humidity = simulatedHum;
    
    Serial.printf("Sample [%d/%d] stored. Temp: %.2f C\n", 
                  index + 1, BATCH_SIZE, simulatedTemp);
}

// MOCK TRANSMISSION FUNCTION
void transmitBatch() {
    Serial.println("--- TRIGGERING UPLINK ---");
    Serial.println("Connecting to Wi-Fi...");
    // Simulate network delay
    delay(100); 
    
    Serial.printf("Transmitting %d packets:\n", BATCH_SIZE);
    for (int i = 0; i < BATCH_SIZE; i++) {
        Serial.printf("  -> Payload: { t: %llu, temp: %.2f, hum: %.2f }\n", 
            readingBuffer[i].timestamp, 
            readingBuffer[i].temperature, 
            readingBuffer[i].humidity);
    }
    
    Serial.println("Transmission Complete.");
    Serial.println("-------------------------");
}

void setup() {
    Serial.begin(115200);
    delay(1000); // Safety delay for serial monitor

    // 1. Identify Wakeup Reason
    // We need to know if this is a fresh boot (power on) or a wake from deep sleep.
    esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();

    if (wakeup_reason == ESP_SLEEP_WAKEUP_TIMER) {
        Serial.println("Wakeup caused by timer");
    } else {
        Serial.println("Fresh Boot (Cold Start)");
        // Initialize RTC variables on cold boot. 
        // If we don't do this, they contain random garbage from uninitialized memory.
        bootCount = 0;
        readingIndex = 0;
    }

    // 2. Increment Boot Counter (Proof of persistence)
    bootCount++;
    Serial.printf("Boot number: %d\n", bootCount);

    // 3. Application Logic
    captureSensorData(readingIndex);
    readingIndex++;

    // 4. Check if Batch is Full
    if (readingIndex >= BATCH_SIZE) {
        transmitBatch();
        // Reset index to overwrite buffer on next cycle
        readingIndex = 0; 
    } else {
        Serial.println("Batch not full. Saving to RTC and sleeping...");
    }

    // 5. Configure Deep Sleep
    // We turn off Wi-Fi/BT specifically before sleep if we used them, 
    // though Deep Sleep handles most power down sequences.
    esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);

    Serial.println("Entering Deep Sleep now...");
    Serial.flush(); 
    
    esp_deep_sleep_start();
    
    // Code here will NEVER be reached
}

void loop() {
    // This is not used in Deep Sleep architectures
}

Why This Works

1. The RTC_DATA_ATTR Macro

This is the linchpin of the solution. When you compile code for the ESP32, the linker script (.ld file) defines memory sections. Standard variables go into .dram0.data (RAM). RTC_DATA_ATTR is a definition usually found in esp_attr.h that resolves to: __attribute__((section(".rtc.data")))

This tells the compiler: "Do not put this in standard RAM. Put this in the memory block starting at address 0x50000000 (RTC Slow Memory)." Because this specific physical memory block is powered by the RTC regulator, data survives the sleep cycle.

2. Handling the "Cold Boot"

RTC memory is not non-volatile like Flash; it is battery-backed RAM. If power is completely cut (battery removed), the data is lost. Furthermore, on the very first power-up, that memory contains random noise.

This is why the esp_sleep_get_wakeup_cause() check is mandatory.

  • If ESP_SLEEP_WAKEUP_UNDEFINED (Cold Boot): We explicitly set readingIndex = 0. This initializes our buffer logic.
  • If ESP_SLEEP_WAKEUP_TIMER: We skip initialization, trusting the data currently residing in 0x50000000 is valid from the previous cycle.

3. Flash Endurance

If we wrote this data to Flash (NVS) every 10 seconds, and the Flash is rated for 100,000 write cycles: 100,000 cycles * 10 seconds = 1,000,000 seconds ≈ 11.5 days.

Your device would physically fail in two weeks. By using RTC RAM for buffering, we have infinite write endurance for the sampling phase, only engaging the radio and potentially Flash (if logging locally) once per hour.

Conclusion

Maximizing battery life in ESP32 applications requires treating the radio as a luxury resource. By leveraging RTC_DATA_ATTR and the RTC Slow Memory, you decouple your sampling rate from your transmission rate. You gain the ability to maintain complex state and historical data across sleep cycles without the latency of external storage or the lifespan penalties of Flash memory. This is the difference between an IoT device that lasts weeks and one that lasts years.