Saving power on an ESP8266 web server using delays

13.08.2022 17:19

ESP8266 core support on Arduino comes with a library that allows you to quickly setup and run a web server from your device. Commonly the code using the ESP8266WebServer library looks something like the following:

#include <ESP8266WebServer.h>

ESP8266WebServer server(80);

void setup(void)
{
	... various wi-fi and request handler setup calls here ...
}

void loop(void)
{
	server.handleClient();
}

For a more concrete example, take a look at the "Hello World" example that is included with the library.

In the following we'll focus on the loop() and ignore the rest. As the name implies, the Arduino framework calls the loop() function in an infinite loop. The handleClient() method checks if any client has connected over the Wi-Fi network and issued an HTTP request. If so, it handles the request and sends a response back to the client. If not, it exits immediately, doing nothing.

In other words, loop() implements a busy-wait for HTTP requests. Even when there is nothing to do, the CPU still runs the busy loop roughly 100000 times per second. In the common case where a page is only requested from the server every once in a while, the system will spend almost all of its time in this state.

Busy loops are common on microcontrollers. On an ATmega microcontroller, another popular target for Arduino code, this is hardly a problem. Unless you're running your hardware off a battery and really counting every microwatt, a busy CPU uses negligible extra power over a CPU that's sleeping. The ESP8266 is a bit more powerful than an 8-bit microcontroller though and correspondingly has a higher power consumption when not sleeping. Hence it makes sense to be a bit more sensible about when the CPU is running or not.

The best solution would be to not have a busy loop at all. Ideally we could use the underlying RTOS to only schedule a handleClient() task in response to network events. The RTOS is smart enough to put the CPU to sleep when no task needs to run. Unfortunately, the simple Arduino environment does not want us to mess with the RTOS. The ESP8266WebServer library certainly isn't written with such use in mind. This approach would require a lot of refactoring of ESP8266WebServer and the code that uses it.

A simpler way is to slow the busy loop down. There is no need to check for client connections hundreds of thousand times per second. A more modest rate would be good enough and the CPU can sleep in between. This would decrease the power consumption at the cost of the response time for HTTP requests. Inserting the Arduino-provided delay() function into loop() does exactly what we want:

void loop(void)
{
	server.handleClient();
	delay(...);
}

The question that remains is what is a good value to choose for delay in this case. What value gives the best trade off between decreased power consumption and increased response time?

My setup for measuring ESP8266 power consumption.

To check I've used this somewhat messy breadboard setup. I've measured the power consumption and response times when running a simple web server Arduino sketch, similar to the "Hello World" included with the ESP8266WebServer library, but with various delays inserted into the loop.

A multimeter with an averaging function measured the current on the 3.3 V supply line of an ESP-01 module. Averaging time was 30 s. I've measured the power consumption while the module was idle (not answering any HTTP requests)

The module was connected through an USB development board to a PC for easy programming from the Arduino IDE. I've used Arduino 2.8.19 and the ESP8266 core 2.4.2.

I've measured HTTP server response times over the Wi-Fi network using Apache Bench (ab -n10). As the representative metric I've chosen the median time from the Waiting row. This is the time it took ESP8266 to send the first byte of the response.

Supply current versus various delays in loop()

Response time versus various delays in loop()

Here are the results, comparing code with just a call to handleClient() to code that is also calling delay with various argument values. The argument to delay is wait time in milliseconds. I've also did a measurement with yield() instead of delay(), something that I've seen used in some examples.

Even a 1 millisecond delay, the minimum allowed by the function, decreases the idle power consumption by 60%. Not surprising if you consider that this small delay causes the CPU to be idle 99.999% of time. The added delay does also measurably increase the response time, as was expected, but the difference between 6 or 8 ms should be negligible for most practical use cases.

Further increasing the delay does not bring any benefits. It only increases the response time without measurably decreasing power consumption. I suspect the variations in measured current with higher delay values are likely due to uncontrolled traffic on the network waking up the ESP8266's radio more often in some test runs than in others.

Adding a yield() call to loop() does not show any benefits in this test. In fact, handleClient() itself calls yield() in some cases before returning. Similarly, adding delay(0) has no measurable effect.

In conclusion, I recommend using a loop() with 1 ms delay:

void loop(void)
{
	server.handleClient();
	delay(1);
}

The added delay decreases the idle power consumption at 3.3 V by about 60%, from around 230 mW to around 70 mW. This is significant enough that you can feel the board running cooler by touch. On a yearly basis it saves around 1.4 kWh per device that's powered on continuously.

For most simple Arduino sketches using ESP8266WebServer, like using the request handler to read a sensor or actuate a relay, this is just a simple one-line change, so I think the power saving is worth the effort. Of course, if you're doing something else in loop() in addition to just calling handleClient(), adding a delay might have other side effects. In such case the code running in loop() might need some adjusting to account for the delay.

Posted by Tomaž | Categories: Digital

Add a new comment


(No HTML tags allowed. Separate paragraphs with a blank line.)