ESP8266 – running on battery power

by Teodor Costăchioiu
57 views

This is an older post, originally published on my previous tinkering blog at electronza.com. While it dates back a few years, it remains just as relevant today.

At the time, I was working on an IoT node based on the ESP8266, designed to collect sensor data and upload it to cloud services.

One of the main challenges was running the node on battery power. It had to operate for extended periods with no human intervention. I had found a 9800 mAh LiPo battery that should have provided enough energy to keep it running for several months. Still, achieving that required minimizing the ESP8266’s power consumption and protecting the LiPo battery from over-discharge (typically below ~3 V).

On its own, this proved to be a challenging problem — one that deserved a dedicated blog post. So, this article is focused on two aspects: reducing the power consumption of ESP8266 modules and protecting LiPo batteries from over-discharging.


ESP8266 Low Power Modes

The first and simplest way to cut down a few milliamps was to remove the power LED.

Some ESP8266 boards had a trace that could be cut, such as the ESP8266 Thing Dev I used in the project. On other boards, the LED had to be physically removed – SMD hot tweezers were a must. Either way, this step reduced consumption by about 8–10 mA.

Next, I looked into the ESP8266 sleep modes. There were three sleep modes, summarized below:

Modem sleepLight sleepDeep sleep
WiFiOFFOFFOFF
System clockONOFFOFF
RTCONONON
CPUONPendingOFF
Substrate current15mA0.4mA~ 20µA
Avg. Current DTIM = 116.2mA1.8 mA
Avg. Current DTIM = 315.4 mA0.9 mA
Avg. Current DTIM = 1015.2 mA0.55 mA

* On some routers, you cannot change the DTIM value

ESP8266 Deep Sleep

The most interesting mode for a sensor node that periodically woke up and sent data was deep sleep.

In the Arduino IDE, the ESP8266 could be put into deep sleep using:

ESP.deepSleep(sleepTimeSeconds * 1000000);

As shown in the example below:

#include <ESP8266WiFi.h>

/* ESP8266 Deep-sleep mode */
void setup() {
  Serial.begin(115200);
  Serial.setTimeout(2000);

  while(!Serial) { }

  Serial.println("I'm awake.");
  Serial.println("Going into deep sleep for 20 seconds");

  ESP.deepSleep(20e6); // 20 seconds
}

void loop() {}

However, there was a catch: to wake up the ESP8266, the RST pin had to be connected to GPIO16 (WAKE).

  • On the ESP8266 Thing Dev, this was done by closing the SJ2 jumper
  • On other boards, a wire connection between RST and GPIO16 was required

Another aspect is that the ESP8266 will lose everything in its memory, and it will run the code just as it does when it’s powered on for the first time.

Of course, one can save some context variables in the EEPROM. But if a node wakes up every five minutes, this will result in having 288 writes to the EEPROM per day. The AT25SF041 used by ESP8266 Thing Dev is rated for 100,000 Program/Erase Cycles. That means that the EEPROM will wear out in less than one year.

ESP8266 – Turning Off the Modem

This is a neat trick I found in the Arduino examples, in the ESP8266 board version 2.5.0 (or higher). It allows the ESP8266 to start with the modem off, then start the modem at the desired moment in the code. When dealing with slow sensors, such as the DGS-H2S from SPEC Sensors, one can use this trick to keep the modem off while the sensor data is collected and turn it on just before uploading to online services.

#include <ESP8266WiFi.h>

#ifndef STASSID
#define STASSID "your-ssid"
#define STAPSK  "your-password"
#endif

void preinit() {
  ESP8266WiFiClass::preinitWiFiOff();
}

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);

  Serial.println("sleeping 5s");
  delay(5000);

  Serial.println("waking WiFi up, sleeping 5s");
  WiFi.forceSleepWake();

  delay(5000);
  Serial.println("connecting to AP " STASSID);

  WiFi.mode(WIFI_STA);
  WiFi.begin(STASSID, STAPSK);
}

void loop() {}

During testing, a simple ammeter showed that consumption stayed around ~20 mA with WiFi off and increased to ~75–80 mA when WiFi was active.

Protecting the battery

This is where things become interesting. By design, most ESP8266 boards do not allow direct measurement of the battery voltage (Vin) without external components. However, there was a workaround: one can detect a discharged battery indirectly, by measuring the input voltage of ESP8266 – that would be V3.3.

This was enabled by adding:

ADC_MODE(ADC_VCC);

and reading the value with:

uint32_t voltage = ESP.getVcc();

as shown in the example below:

// Load Wi-Fi library
#include <ESP8266WiFi.h>

// Replace with your network credentials
const char* ssid     = "myssid";
const char* password = "mypassword";

ADC_MODE(ADC_VCC);
uint32_t Batt;

// Set web server port number to 80
WiFiServer server(80);

// Variable to store the HTTP request
String header;

void setup() {
  Serial.begin(9600);
  while(!Serial);

  // Connect to Wi-Fi network with SSID and password
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  // Print local IP address and start web server
  Serial.println("");
  Serial.println("WiFi connected.");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  server.begin();
}

void loop(){
  WiFiClient client = server.available();   // Listen for incoming clients

  if (client) {
    Serial.println("new client");
    // an http request ends with a blank line
    boolean currentLineIsBlank = true;
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        Serial.write(c);
        // if you've gotten to the end of the line (received a newline
        // character) and the line is blank, the http request has ended,
        // so you can send a reply
        if (c == '\n' && currentLineIsBlank) {
          // send a standard http response header
          client.println("HTTP/1.1 200 OK");
          client.println("Content-Type: text/html");
          client.println("Connection: close");  // the connection will be closed after completion of the response
          client.println("Refresh: 5");  // refresh the page automatically every 5 sec
          client.println();
          client.println("<!DOCTYPE HTML>");
          client.println("<html>");
          
          // output the battery value
          Batt = ESP.getVcc();
          client.println("Battery voltage is: ");  // refresh the page automatically every 5 sec
          client.println(Batt);
          
          client.println("</html>");
          break;
        }
        if (c == '\n') {
          // you're starting a new line
          currentLineIsBlank = true;
        } else if (c != '\r') {
          // you've gotten a character on the current line
          currentLineIsBlank = false;
        }
      }
    }
    // give the web browser time to receive the data
    delay(1);
    // close the connection:
    client.stop();
    Serial.println("client disconnected");
  }

}

the ESP8266 is configured to read the V3.3 voltage by placing ADC_MODE(ADC_VCC); on top of the sketch. Then, the battery voltage in mV is read as in uint32_t getVcc = ESP.getVcc();

In my experiments, I found the following relationship among Vin, V3.3, and the voltage measured by the ESP.getVcc(); This applies to the ESP8266 Thing Dev, which uses AP2112K-3.3V LDO regulator. Some other boards might behave differently, so you will have to redo my experiment.

ESP8266 battery test
VinV3.3Vmeasured/1000
53.29043.475
4.93.29053.475
4.83.29053.475
4.73.29043.474
4.63.29053.475
4.53.29043.474
4.43.29053.475
4.33.29053.475
4.23.29063.475
4.13.29053.475
43.293.474
3.93.28993.474
3.83.28993.474
3.73.28983.474
3.63.28973.473
3.53.28973.473
3.43.28953.472
3.33.25753.44
3.23.1533.331
3.13.05553.226
32.9473.106
2.92.7752.936
2.82.72.87

In my experiments (performed on the ESP8266 Thing Dev, which used an AP2112K-3.3V LDO regulator), I observed the following:

  • There was an offset of about 0.18 V between the measured value and the actual V3.3
  • The voltage remained stable while Vin was above 3.3 V (regulated region)
  • Once Vin dropped below 3.3 V, the regulator output began to drop

Most importantly:

  • When the battery voltage reached ~3.0 V (a safe discharge limit for LiPo batteries), the value returned by ESP.getVcc() was about 3100 mV

This provided a practical threshold for detecting low-battery conditions.

// Low voltage detection
// Note that we read Vin, and not the battery voltage,
// as the battery voltage is not accessible to be measured
// ESP8266 thing uses AP2112K-3.3V
// https://www.diodes.com/assets/Datasheets/AP2112.pdf
// Low DropoutVoltage (3.3V): 250mV (Typ.) @IOUT=600mA+

#include <ESP8266WiFi.h>

ADC_MODE(ADC_VCC);
int Batt;

// do other things here

void setup() {

  // ********************************************************************************
  // Check battery status
  // ******************************************************************************** 
  // Reads Vin (not battery voltage!!!)
  // But Vin = battery voltage if battery_voltage < 3.3V
  Batt = ESP.getVcc();
  // If the battery is discharged don't go any further!!!
  if(Batt < 3100){
     // Deep sleep for as long as you can
     ESP.deepSleep(ESP.deepSleepMax());
  }
  // your code goes here
}

void loop() {
  // your code goes here
}

In this approach:

  • The voltage was checked immediately after boot
  • If the value was below the threshold, the ESP8266 entered deep sleep for the maximum possible duration
  • It periodically woke up, checked again, and returned to sleep if needed

It is worth noting that other components (such as sensors) still consumed power, but the overall discharge rate was significantly reduced.

Leave a Comment