These days I’m working on a somewhat bigger project, a sensor node with ESP8266 which has to collect some sensor data and upload the sensor values to cloud services. One of the challenges I’m facing is running the sensor node on battery power, and it should be able to run for extended periods with no human intervention. I have found a 9800mAh LiPo battery that should provide me enough juice to run for several months. Still, to achieve good results I have to reduce the power draw of the ESP8266 to the minimum possible, and I also have to provide a means to protect the LiPo battery when its voltage drops below-given value (typically 3V).
By itself, this proves to be a challenging thing – enough to become a separate blog post. So, this blog post is about implementing some strategies to reduce the power consumption of ESP8266 modules and of protecting LiPo batteries from over-discharging.
ESP8266 Low Power Modes
The first and the simplest way to cut down a few milliamps is to remove the power LED. Some ESP8266 boards have a trace that has to be cut, such as the ESP8266 Thing Dev I’m using in this tutorial. On other boards, one has to physically remove the LED – SMD hot tweezers are a must. Either way, you can shed 8-10 mA.
Then, we go deep into the ESP8266 sleep modes. There are three sleep modes, as detailed in the table below:
Modem sleep | Light sleep | Deep sleep | |
WiFi | OFF | OFF | OFF |
System clock | ON | OFF | OFF |
RTC | ON | ON | ON |
CPU | ON | Pending | OFF |
Substrate current | 15mA | 0.4mA | ~ 20µA |
Avg. Current DTIM = 1 | 16.2mA | 1.8 mA | – |
Avg. Current DTIM = 3 | 15.4 mA | 0.9 mA | – |
Avg. Current DTIM = 10 | 15.2 mA | 0.55 mA | – |
* On some routers you cannot change the DTIM value
ESP8266 deep sleep
The most interesting is the deep sleep mode for a sensor node that wakes up and sends data from time to time. On Arduino IDE, one can put the ESP8266 in deep sleep mode by using ESP.deepSleep(sleepTimeSeconds * 1000000);
#include <ESP8266WiFi.h> /* ESP8266 Deep-sleep mode*/ void setup() { Serial.begin(115200); Serial.setTimeout(2000); // Wait for serial to initialize. while(!Serial) { } Serial.println("I'm awake."); Serial.println("Going into deep sleep for 20 seconds"); ESP.deepSleep(20e6); // 20e6 is 20 microseconds } void loop() {}
However, there’s a catch: to wake up the ESP8266 one has to connect the RST pin to GPIO16 (WAKE). On the ESP8266 Thing Dev you do this by closing the SJ2 jumper. On other boards, one has to use a wire to connect the RST and GPIO16 pins.
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 in less than one year.
ESP8266 – turning off the modem
This is a neat trick I found in the Arduino examples, in ESP8266 board version 2.5.0 (or higher). It allows the ESP8266 to start with the modem off, then to start the modem at the desired moment in the code. When dealing with slow sensors such as the DGS-H2S sensor from SPEC sensors, one can use this trick to keep the modem off while the sensor data is gathered and to turn on the modem just before uploading to online services.
#include <ESP8266WiFi.h> #ifndef STASSID #define STASSID "your-ssid" #define STAPSK "your-password" #endif // preinit() is called before system startup // from nonos-sdk's user entry point user_init() void preinit() { // Global WiFi constructors are not called yet // (global class instances like WiFi, Serial... are not yet initialized).. // No global object methods or C++ exceptions can be called in here! //The below is a static class method, which is similar to a function, so it's ok. ESP8266WiFiClass::preinitWiFiOff(); } void setup() { Serial.begin(115200); Serial.setDebugOutput(true); Serial.println("sleeping 5s"); // during this period, a simple amp meter shows // an average of 20mA with a Wemos D1 mini delay(5000); Serial.println("waking WiFi up, sleeping 5s"); WiFi.forceSleepWake(); // amp meter raises to 75mA delay(5000); Serial.println("connecting to AP " STASSID); WiFi.mode(WIFI_STA); WiFi.begin(STASSID, STAPSK); // amp meter cycles within 75-80 mA } void loop() { }
Protecting the battery
This is where things become interesting. By the way most ESP8266 boards are designed, one cannot read the battery voltage Vin without external components. But there is a method to detect a discharged battery indirectly, by measuring the input voltage of ESP8266 – that would be V3.3.
// Load Wi-Fi library #include <ESP8266WiFi.h> // Replace with your network credentials const char* ssid = "myssid"; const char* password = "mypassword"; ADC_MODE(ADC_VCC); int 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"); } }
In the code example above, 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 relation between Vin, V3.3, and the Vmeasured by 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.
Relation between Vin, V3.3 and ESP.getVcc(); readout |
Vin | V3.3 | Vmeasured/1000 |
5 | 3.2904 | 3.475 |
4.9 | 3.2905 | 3.475 |
4.8 | 3.2905 | 3.475 |
4.7 | 3.2904 | 3.474 |
4.6 | 3.2905 | 3.475 |
4.5 | 3.2904 | 3.474 |
4.4 | 3.2905 | 3.475 |
4.3 | 3.2905 | 3.475 |
4.2 | 3.2906 | 3.475 |
4.1 | 3.2905 | 3.475 |
4 | 3.29 | 3.474 |
3.9 | 3.2899 | 3.474 |
3.8 | 3.2899 | 3.474 |
3.7 | 3.2898 | 3.474 |
3.6 | 3.2897 | 3.473 |
3.5 | 3.2897 | 3.473 |
3.4 | 3.2895 | 3.472 |
3.3 | 3.2575 | 3.44 |
3.2 | 3.153 | 3.331 |
3.1 | 3.0555 | 3.226 |
3 | 2.947 | 3.106 |
2.9 | 2.775 | 2.936 |
2.8 | 2.7 | 2.87 |
First of all, we see there’s an offset of about 0.18V between the Vmeasured and the actual V3.3. One can leave it like this or can compensate in the software.
Then we notice that, when the input voltage Vin = 3.3V, we have V3.3 = 3.2575 (it becomes to drop). The ESP8266 senses this small voltage drop, and it measures 3.44V.
Further, when the battery voltage drops to 3V (which is the safe margin to discharge LiPo batteries), the readout of the ESP.getVcc() is 3.106V. We can use this value to trigger a deep sleep to keep the battery from discharging, as in the code below:
// 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 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 }
What I did here is read the battery voltage once the ESP8266 module starts, and if the value returned by the ESP.getVcc() is below the safe threshold, the ESP8266 goes into a deep sleep for as long as it can. Then it will wake up and go back to deep sleep until the battery is recharged.
Please observe that there will be other parts (sensors, etc.) that will still drain power from the battery, but the overall discharge rate is considerably slowed down.
0 Comments