Tuesday, October 6, 2020

Measuring NiMH vs Li-Ion voltage sag at small loads with ESP32

This article is a brief excerpt from my experience with using Nickel Metal Hydride (NiMH) and Lithium-ion (Li-ion) cells for powering home sensors running the ESP32 Wi-Fi microcontroller.


I currently deploy around a dozen home environmental sensors using the ESP32 Wi-Fi microcontroller. Each sensor is battery powered, either from multiple NiMH Eneloop cells in series or a single Li-ion 18650 cells boosted to 5 V. I use the HUZZAH32 ESP32 breakouts from Adafruit (link).

The board is rated for 3.5 V to 6 V input voltage and should consume less than 500 mA under load. In my early quick tests using a multimeter, I noted current draw of around 250 mA @ 4.2 V while running a simple test (connect to Wi-Fi, download a 10 KiB file).

Since the board is expected to work with a 3.6 V input and I've got way too many NiMH cells lying around doing nothing, I originally planned to power each sensor with three cells in series for a total of ~4.2 V when fully charged and ~3.7 V nominal. Based on well-documented Eneloop discharge characteristics (link) and my earlier current estimates, I expected the battery voltage to stay within acceptable range under load i.e. I assumed that voltage sag won't be a problem and that I will be able to utilize the majority of the battery's total capacity without running into low voltage issues.

The Issue

After running initial sensor tests on ESP32 hooked up to ~3.7 V NiHM input, the results were hit-and-miss. Some runs were okay, other times the device would fail to send readings and would get stuck in what looked like a "reboot loop". I saw similar results while using a cheap bench power supply set to 3.7 - 4.2 V. I determined that the sensors were perfectly stable when using using four NiMH cells instead of three (~5 V total), or with the power supply set to ~4.5 V or higher. 

I could run further tests with Li-ion 18650 cells that I expected to have a much flatter voltage profile  under load, but it so happened that a 4x AA cell holder was almost exactly the size (area) of a completed sensor board and would allow the whole device to have a more tolerable form factor than if I used a relatively long 18650 cell. 

My early hypothesis was that 3.7 V input is not quite enough because the on-board regulator drops ~0.3 V and brings voltage down to ~3.3 V at the board, which is the lower end of the acceptable range. At that point, even a small amount of sag could bring input voltage into brownout territory. However, the voltage sag hypothesis was not supported by other tests done at higher voltages i.e. with the board not becoming fully stable until ~4.5 V.

The simplest answer is often the right one, and further testing confirmed that the issue was indeed Ohm's fault i.e. due to wiring resistance effectively choking the circuit. At the time I was merely beginning to scratch the surface of homebrew electronic projects, and I did not fully appreciate just how bad breadboard wiring can be. As it turns out, it can be bad enough to affect even the simplest builds. If you're interested in learning more about the wiring experiments I ran back then, see this article:

Having found the issue, I did not pursue the voltage sag question further. In the end, drilling into voltage response of various cell types was a low priority given all the other work remaining in the sensor project.

Curiosity Strikes Back

Fast forward a few months, and voltage sag is on my mind again. Those loose ends, they truly are annoying. Just for kicks, let's test some cells and see if voltage response of Li-ion and NiMH cells is anything worth worrying about under small loads.

ESP32 power draw

My initial optimistic assumptions were as follows:
  1. The board only draws ~250 - 500 mA. If more, then not a lot more.
  2. At a few hundred milliamps, a good quality NiMH cell or any healthy Li-ion cell shouldn't exhibit an amount of voltage sag worth blogging about.

How much power does the ESP32 board draw? Again, we're talking about a fairly basic breakout board with not much on it except the ESP32 itself and a voltage regulator, no USB-Serial converter, no LiPo charger:

Earlier on, I used a multimeter for all current draw measurements. This is good for measuring relatively stable currents, but would not catch short, intermittent spikes that could still brown out the circuit if they caused a serious voltage sag. This time around, I used an oscilloscope to take a high-resolution current measurement. I used a test sketch with three simple steps: connect to WiFi, make a HTTP connection to a LAN server, download a 10 KiB file.

The sketch code:

#include <WiFi.h>
#include <WiFiClientSecure.h>

#define uS_TO_S_FACTOR 1000000  /* Conversion factor for micro seconds to seconds */

/* Wireless network creds (WPA2) */

const char* ssid     = "...";
const char* password = "...";

/* HTTP server and dummy file name */

const char* host = "";
String url   = "/dummy/10K.bin";

void setup()

    Serial.print("Connecting to ");

    /* Connect to wireless network */
    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED) {

    Serial.println("WiFi connected");

    /* Connect to server */

    WiFiClient client;
    const int httpPort = 80;
    if (!client.connect(host, httpPort)) {
        Serial.println("connection failed");

    /* Request the dummy file */

    Serial.print("Requesting URL: ");

    client.print(String("GET ") + url + " HTTP/1.1\r\n" +
                 "Host: " + host + "\r\n" +
                 "Connection: close\r\n\r\n");
    unsigned long timeout = millis();
    while (client.available() == 0) {
      if (millis() - timeout > 5000) {
        Serial.println(">>> Client Timeout !");        

     * Download the file contents.
     * Don't print to serial, just ignore.

    while(client.available()) {
        String line = client.readStringUntil('\r');

    /* Disconnect from wireless network */


    /* Sleep for 10 seconds before re-running test code */

    Serial.println("Sleeping for 10 seconds");
    esp_sleep_enable_timer_wakeup(10 * uS_TO_S_FACTOR);

void loop()

With the test code running, I used an oscilloscope to measure voltage drop across a low-value resistor in series with the load (board). The current draw can then be calculated using Ohm's law.

This is the voltage capture from the scope while executing a single iteration of the test code:

With a 0.11 Ohm series resistor and per the I=V/R formula, the peak voltage difference of ~50 mV translates to ~450 mA peak current. Not 250 mA after all, is it?

The graticule on the screenshot is somewhat dark, but the capture was done at 200 ms per division. With that, two things can immediately be observed:
  • Current spikes are sharp and last mere milliseconds. Not surprisingly, such spikes would be virtually invisible to a basic multimeter.
  • The most severe spikes happen near the beginning of the test when the board starts up and tries to connect to Wi-Fi. This might explain why a browned-out board exhibits behavior similar to a "reboot loop", since it's unable to go past the initial boot/connect stages.
The shape of the graph is not surprising after you see it, but this was the first time I properly measured a microcontroller's power draw, so it was still a bit of an eye candy.

The measured current of 450 mA would have been too much for the breadboard test rig I had used in the early experiments. Read this article for details:

Test Setup

Having determined the current draw, I built a simple test rig to measure voltage sag while running the test code. Technically I could've used a single test rig to measure both voltage and current using separate oscilloscope channels, but that's not what I did because... I lost track of the 2nd pair of scope probes until a week later.

The test rig looks like this:

Basic components:
  1. Power source. This is the single 18650 cell or a 3x AA NiMH battery under test.
  2. A power switch.
  3. Differential voltage probe connected to an oscilloscope.
  4. The ESP32 board with power leads connected to BAT and GND pins using red and black leads, respectively.

The following power sources will be tested:

From left to right:

    1. An unknown brand 18650 Li-ion cell pulled out of a cheap headlight. Rated for 1800 mAh, but actually delivering just under 1000 mAh.
    2. Samsung ICR18650-26J 5.2 A cell. Freshly purchased from a reputable source, 2600 mAh rated, 2600 mAh actual.
    3. Sanyo NCR18650GA 10 A cell. Freshly purchased from a reputable source, 3500 mAh rated, 3300 mAh actual.
    4. 3x Sanyo Eneloop AA BK-3MCCA. Not super fresh but under a dozen cycles, 1900 mAh rated, 1850 mAh actual.
    5. 3x Amazon Basics AA NiMH cells. Under half a dozen cycles, 2000 mAh rated, 1950 mAh actual.

All actual capacities were determined using an Opus BT-C3100 set to discharge mode at 200 mA. I tested two sets of cells for each configuration listed above to ensure the results results aren't skewed by a single bad cell.

Test procedure is as follows:
  1. Discharge 18650 cells and NiMH batteries down to just under 3.8 V. Voltage is measured without load, at room temperature, after allowing each cell to rest for at least an hour after discharge.
  2. Connect the cell/battery to the ESP32 test rig.
  3. Measure voltage sag while running test code.

Discharging the cells / batteries to <3.8 V brings them just over their nominal voltage level, which hopefully makes the tests a bit more accurate. Li-ion and NiMH cells spend the majority of their discharge cycle around the nominal voltage (3.6 - 3.7 V for Li-ion and 1.2 V for NiMH), so that's roughly the level I want to test them at.

For NiMH cells, I'm using a holder like this one:

Below are the readings from a multimeter after discharging each power source down to ~3.8 V. Measurements were taken in the order in which cells/batteries are listed above i.e. 1 through 3 are the Li-ion cells and 4, 5 are the NiMH batteries made of three AA cells each:

For the NiMH batteries, the individual cells were discharged together and measured within 1% of one another. Measurements 1 through 3 are for the Eneloops, and 4 through 6 are for the Amazon Basics:

With that, let's look at test results.

Testing, Testing

The following screenshots show the voltage recorded by the oscilloscope during a single run of the test code. Cursor Y1 aligns with the baseline voltage with the test board powered down, whereas Y2 aligns with the lowest point during the test i.e. the moment when the board drew the most power.

No-name Li-ion cell:

Samsung Li-ion cell:

Sanyo Li-ion cell:

Battery of three NiMH Eneloops:

Battery of three NiMH Amazon Basics:

Summary of the recorded voltage drops, approx. delta between baseline and lowest point:
  • No-name Li-ion cell: 74 mV
  • Samsung Li-ion cell: 58 mV
  • Sanyo Li-ion cell: 76 mV
  • Battery of Eneloops: 136 mV
  • Battery of Amazon Basics: 124 mV

The NiMH cells drop roughly twice as deep as the Li-ions, but it's still just around 3% voltage drop for the NiMH under this relatively light load. I wonder if it would different under sustained load, and whether it'd scale linearly.

More Testing

Let's put the cell under a sustained 500 mA and 1 A loads using a circuit with an adjustable load module:

I've calibrated the load against a known good power supply and disabled the on-board fan as I found it to introduce quite a bit of noise under oscilloscope measurement. We don't care about thermals for a short-lived low power test like this.

Results at 500 mA were almost identical to what we've seen with ESP32 at 450 mA, so I'll skip those. Results at 1 A sustained load are below.

No-name Li-ion cell:

Samsung Li-ion cell:

Sanyo Li-ion cell:

Battery of three NiMH Eneloops:

Battery of three NiMH Amazon Basics:

Summary of the recorded voltage drops, approx. delta between baseline and lowest point:

Cell / battery Voltage sag, ESP32 test @ 450 mA [mV] Voltage sag, synthetic test @ 1 A [mV]
No-name Li-ion 74 153
Samsung Li-ion 58 127
Sanyo Li-ion 76 100
3x Eneloop NiMH 136 292
3x Amazon Basics NiMH 124 271

I looks like the Sanyo cells show the best overall voltage response under load. This might be related to their higher overall current rating compared to Samsung, but this single test isn't enough to draw conclusions. In any case, all Li-ions tested here are way ahead of the NiMH batteries.

All tests up till this point were performed with cells discharged to around their nominal voltage. For the last round of tests, let's fully charge all cells and measure voltage drop under the same, 1 A sustained load.

When fully charged and rested for 1 hour, the voltage of our test subjects was as follows:

Cell / battery Voltage when fully charged [V]
No-name Li-ion 4.14
Samsung Li-ion 4.15
Sanyo Li-ion 4.13
3x Eneloop NiMH 4.27
3x Amazon Basics NiMH 4.25

Summary of results at 1 A sustained load is shown below. The numbers recorded during the previous test at ~nominal voltage are provided for comparison:

Cell / battery Voltage sag, 1 A load, nominal [mV] Voltage sag, 1 A load, fully charged [mV]
No-name Li-ion 153 141
Samsung Li-ion 127 122
Sanyo Li-ion 100 103
3x Eneloop NiMH 292 276
3x Amazon Basics NiMH 271 302

The NiMH numbers show some movement, but overall there isn't a large difference between fully charged and nominal states. 

That concludes the article, thank you for reading!

No comments:

Post a Comment