ESP8266 deep-sleep repeatedly

How to deep-sleep for a long time and save power

So I have this beautiful houseplant which I'm planning on keeping alive. Having killed even a cactus of dehydration as a kid, I wanted to make sure this one won't suffer the same fate.

The setup and the problem

The hardware is a cheap D1 mini clone using the ESP8266 chip. I used a capacitive soil moisture meter so it doesn't rust as quickly as a resistive one would. The power source for now is a USB power bank which constantly gives power. I'm planning on changing it to batteries, but first I have to switch to a better voltage regulator for the D1 mini.

Everything went well until I realized the deep-sleep function of an ESP chip is very unreliable. Apparently, originally it supported only 32 bit integer values. Since the ESP.deepSleep function takes the time in microseconds, a quick calculation (4294967296*1e-6/60 = ~71.58) shows it can sleep a maximum of approximately 71 minutes at a time. With any larger value it never wakes up.

Also apparently, this was later changed so that the ESP can sleep for a few hours at a time, but it's not constant at all for how long - the length depends on the state of the RTC. One would put it to sleep like this: ESP.deepSleep(ESP.deepSleepMax()). In my testing I noticed it sometimes slept for 3 hours, sometimes for 6, and then sometimes it didn't wake up at all.

The solution

I decided to make the ESP8266 sleep one hour at a time. This approach uses the RTC memory to keep track on how many times it has awakened. To save battery in theory, I made it so that the first time it powers on (not waking from deep-sleep), it will calibrate the WiFi radio once. After that, radio will be disabled on wake up until it's time to actually do something, in which case the radio is enabled. According to power measurements, the ESP8266 consumes about 170 mA of current for a second when waking up. This also seems true whether calibrating the radio or not - in theory not calibrating should consume less energy. The modes and APIs can be found in the documentation.

The function I used to set the counter and deep-sleep is listed below.

// Deep-sleep for specified amount of hours, one hour at a time.
// If powered on (not a deep-sleep reset), nothing will happen.
// Call this twice: in the beginning of setup (end_of_setup == false)
// and at the end of setup (end_of_setup == true).
void deepSleepCycle(uint32_t hours, bool end_of_setup = false) {

    uint32_t reset_counter = 0;
    bool waking_from_sleep = ESP.getResetReason() == "Deep-Sleep Wake";

    if (!end_of_setup) {
        if (waking_from_sleep) {
            Serial.print("Waking up from deep-sleep via reset pin. Reset counter: ");
            ESP.rtcUserMemoryRead(0, &reset_counter, sizeof(reset_counter));
            reset_counter++;
            ESP.rtcUserMemoryWrite(0, &reset_counter, sizeof(reset_counter));
            Serial.println(String(reset_counter));
        } else {
            Serial.println("Zeroing reset counter.");
            ESP.rtcUserMemoryWrite(0, &reset_counter, sizeof(reset_counter));
            return;
        }
    }

    // With larger values, deep-sleep is unrealiable: it might never wake up and consume a lot of power.
    // Therefore sleep one hour at a time.
    // In reality, the ESP sleeps a bit less than the 60 minutes it is told to.
    if (reset_counter < hours) {
        // If this is the first time going to sleep, do the radio calibration once.
        // Otherwise, disable radio (WiFi).
        RFMode wake_mode = waking_from_sleep ? WAKE_RF_DISABLED : WAKE_RFCAL;
        if (reset_counter + 1 == hours) {
            // Wake up with radio on if the next power cycle finishes sleeping.
            wake_mode = WAKE_NO_RFCAL;
        }
        Serial.println("Going to deep-sleep for 1 hour.");
        // 1: WAKE_RFCAL
        // 2: WAKE_NO_RFCAL
        // 4: WAKE_RF_DISABLED
        Serial.println("Radio mode will be: " + String(wake_mode));
        ESP.deepSleep(3600*1e6, wake_mode);
    }
    reset_counter = 0;
    ESP.rtcUserMemoryWrite(0, &reset_counter, sizeof(reset_counter));

}

One would call the function like in the example below.

void setup() {

    Serial.begin(115200);

    deepSleepCycle(12);

    /*
    Setup the WiFi, measure values,
    call the cloud service, or do
    whatever you're trying to do.
    */

    deepSleepCycle(12, true);

}

The full code including the Azure functions is in my GitHub repository called analogging.

Update 2019-11-23

After finally ordering some battery holders I converted the device to run on 3xAA batteries. They provide 4.5V total, and the regulator my D1 mini has is the Torex XC6204 with 200mV voltage drop, so there's plenty of headroom for 3.3V operation.

To minimize battery usage I moved the capacitive soil measurement sensor from the 3.3V pin to a GPIO pin. Otherwise it would've been powered on all the time. The sensor uses only 5mA, which is well within the GPIO pin limit of 12mA. I measured the power consumption and recorded the video below. In it, I'm pressing the reset button twelve times to go one full cycle between reporting the measured values.

While everything is powered on (including the Wi-Fi), the board uses around 80mA of current. While in deep-sleep the current is 0.25mA. I did some theoretical calculations:

  • In a day, the value is reported two times. The power cycle then lasts around 5 seconds max, so the power consumption is: 2*5s*80mA/(3600s/h)/d
  • The one hour sleep cycle is done 22 times without reporting, and it takes about a second every time it's reset. Assuming the same current as when reporting, we get: 22*1s*80mA/(3600s/h)/d
  • The consumption in deep-sleep every day is approximately: 24h*0.25mA/d
  • Searching the Internet, Varta AA batteries are known to have a capacity between 2200mAh and 2700mAh. I'll assume the average, 2450mA.
  • Now, if we divide the capacity with the sum of all the power consumption numbers, we get the number of days the device should run in theory without changing batteries: 2450/((2*5*80/3600)+(22*1*80/3600)+(24*0.25)) = 365.066

So, with the three AA batteries my measurement device should go a full one year without needing to replace the batteries! Not bad at all. I actually ended up burying everything in the plant soil because of that. It's out of sight, out of mind, now.

Not so fun adventures in the Internet of Things

I had a really weird practical IoT problem when doing the switch to batteries. For some reason, my board stopped waking up from deep-sleep without manually pressing the reset button. I tried changing the output GPIO pin from D8 to D1, which is actually a recommended thing to do, as not all pins on the ESP8266 are equal and should be used. This didn't solve the problem, however.

After searching a bit, I found a thread and another in GitHub where people were having similar problems. I tried the provided C code to make the board sleep, but it still didn't wake up. I also noticed in my device monitor that my device was printing something when it was supposed to wake up, but it just wouldn't eventually.

So I ended up switching to another D1 mini, which solved the problem. Apparently at some point of me soldering the things in place the ESP8266 had broken down in a weird way so that everything worked except waking up from sleep via software. At least that's the only explanation I came up with.

With my second D1 mini I had another problem where it stopped working using the hardware reset button. Luckily I realized relatively quickly that the button was actually just broken off the PCB and resoldering it fixed the problem.

These experiences were a not so nice reminder about the annoying problems you might encounter while doing DIY IoT projects. Always be prepared that pretty much anything can happen and things don't always just work as they should.

Send me email or comment below:
(Please note: comments with direct links to commercial sites might not be published.)
Creative Commons License This article by Olli Helin is licensed under the Creative Commons Attribution 4.0 International License