ESP32 room thermometer: with 18650 battery level indicator

It seemed like a good idea at the time. The TTGO ESP32 microcontroller board that I’m using for a room thermometer project comes with a battery connect and charging circuitry for a Li-Ion cell. It would be so much easier to deploy the thermometer if I could run it off a battery.

And it works. Kinda.

There are issues, though. One of them is that I had no idea when the battery was getting low. Nor how long it would last. [Spoiler alert: not long enough.]

I needed a battery level indicator. And that turned out to be not that difficult.

Wiring the battery

To read the battery voltage you could just feed the positive of the battery into one of the analogue-to-digital converter (ADC) pins on the board – maybe with a resistor in line to  limit current. But don’t do that!

The 18650 Li-Ion cell I’m using has a nominal voltage of 3.7V and a maximum, when fully charged, of 4.2V. Both are above the 3.3V limit for the GPIO pins on the ESP32 board.

This is where a voltage divider comes in. The concept is simple enough: you connect the positive and negative terminals of the battery together via two resistors in series. In between the resistors, you tap the circuit with another wire that goes to the ADC pin of the board. With both resistors having the same value, each will drop the voltage by the same amount – ie, at the mid-way point between them, the voltage will be half the total potential of the battery – in our case, a maximum of 2.1V and a nominal voltage of 1.85V. We measure this via an analogue (ADC) pin and do some maths to determine the actual voltage.

It seems, from what Googling I’ve done, that some ESP32 boards come with voltage dividers on board, which are linked to one of the ADC pins. I’m fairly confident that’s not the case with the boards I’m using.

The two resistors soldered to the battery holder.

This method is not going to provide great accuracy. And the reading won’t be linear. But I don’t want complete accuracy – I’m not even going to treat the result of the measurement as a quantity in Volts. I just want a rough indication of battery percentage.

Wired up

I soldered a 100KΩ resistor to each of the battery holder’s terminals. Then I covered them in heatshrink before twisting the free ends together, along with a wire to run to the ESP32.

After I took the photos above, I covered the solder joint with Kapton tape, then hot-glued the resistors to the side of the battery holder. BTW, I did all this, with no battery in the holder.

According to Ohm’s Law, with the cell at its full 4.2V charge, the resistors should pull just 0.021mA, so they’re not going to be a serious energy drain.

Cracking the code

Next step, the code for the battery level reading. I’ll just be showing some code snippets here. The full code (note: still a work in progress) is on GitHub.

First we’ll define a few things:

#define VBAT_PIN 35
#define BATTV_MAX    4.1     // maximum voltage of battery
#define BATTV_MIN    3.2     // what we regard as an empty battery
#define BATTV_LOW    3.4     // voltage considered to be low battery

You can see that I’m using pin 35 to read the battery level. In the setup() function, we need to set this as an input.


Elsewhere in the code I’ve created a global variable battv. This is a float. In the main loop, we could just read this pin:

battv = (float)analogRead(VBAT_PIN);

That would give a number between 0 (no Volts) and 4095 (maximum voltage). But that’s not helpful. For one thing, our ‘maximum voltage’ here is 3.3V, because that’s the maximum for the ADC. But, because of our voltage divider, the most we’ll ever see is 2.1V. We need to calibrate the scale.

First, let’s divide the reading we’re getting from the ADC by 4095 to get a reading in what we could call ADC ‘units’.

battv = (float)analogRead(VBAT_PIN) / 4095;

This tells us how many 4095ths of 3.3V we’re seeing on the incoming voltage. We simply multiply by 3.3 to give us a voltage reading.

battv = ((float)analogRead(VBAT_PIN) / 4095) * 3.3;

And that’s fine, except that it tells us the voltage of the point in the circuit we’re monitoring – ie, the mid-point of the voltage divider. That’s half the actually battery voltage, so to get the latter we need to multiply by 2.

battv = ((float)analogRead(VBAT_PIN) / 4095) * 3.3 * 2;

In theory, this gives us a reading, in Volts, for the battery. In practice, it’s a little off. I added a fudge factor to bring the reading shown by this calculation to somewhere close to the reading I could see on my multimeter.

battv = ((float)analogRead(VBAT_PIN) / 4095) * 3.3 * 2 * 1.05;

That gives a good-enough voltage reading for the battery. But that’s not what I want. A simple percentage gauge would be more useful and more honest.

Well, fine. We just divide the voltage we’re reading by the maximum voltage of the battery (4.2V) to get the proportion of charge remaining and multiply by 100 to get the percentage, right?

Not really. That tells us what percentage of the maximum the battery is offering up, but not the percentage of usable power. Read on.

Not enough power

Once I started doing the calculations, I realised some of the limitations with this whole setup. The board requires a minimum of 3.3V to function. There’s possibly a little leeway there, but let’s say that the absolute minimum is 3.2V. The maximum voltage from the battery is 4.2V. This means that we only have one Volt to play with. Once the voltage has dropped just one Volt from the maximum, and half a Volt from the nominal voltage, it’s no longer viable. That’s not much.

To work out where we are in this range, we have to introduce the minimum voltage into the equation. Using the macros defined above, and a previously declared integer global called battpc, the calculation becomes:

battpc = (uint8_t)(((battv - BATTV_MIN) / (BATTV_MAX - BATTV_MIN)) * 100);

The constants in the #define statements may need tweaking. The battery level never gets to 100%, no doubt due to inaccuracies in how I’m reading the voltage. That’s why I’ve set the maximum (BATTV_MAX) to 4.1, and even that may still need adjustment. The same goes for the minimum (BATTV_MIN) and the level at which I decide to show ‘***LOW BATTERY+++’ on the display (BATTV_LOW).

Battery life

I haven’t done any full tests yet, but I don’t think the thermometer is going to run for long from a full charge. A couple of days, perhaps.

And that wouldn’t be so bad if it didn’t take so damn long to charge the battery via the onboard charging circuit. I’ve had the thermometer on charge for two days now and it still hasn’t cracked 87%.

The upshot of this is that powering this device from battery was an interesting experiment. And I’ve learned a lot that I’m going to apply to other microcontroller projects – ones not fitted with a power-crazed OLED display. My next step, for example, is to try a version of this project with an e-paper display.

In the meantime, I’ve also created a second version of the code that does away with all the battery stuff and assumes the thermometer will be powered from a 5V supply – eg, a USB power adapter.

I’m also exploring the use of an 18650 battery holder that has its own charging circuitry. The output from this holder is 5V – meaning that the holder has a boost converter. This is something the ESP32 board’s battery circuits lack. While boosting to 5V and then bucking this down to the board’s native 3.3V might be inefficient, it could still be an improvment if it allows greater exploitation of the battery’s capacity.

Stay tuned…

5 thoughts on “ESP32 room thermometer: with 18650 battery level indicator

  1. Michael Cook

    That’s awesome, I’ve tried using the so-called “DIY LM3914 Display Board” without much success. It fluctuates wildly while you’re actually using the battery.
    Looking forward to see your next developments in efficiency. Like you said, a Nokia style LCD or even e-paper are sooo much more efficient. And if it were an arduino nano rather than ESP, that deep sleeps.. it’ll last a lifetime! (well, except from the battery’s self-discharge)
    Good luck!

  2. Philippe Larroque

    Hello, good job and I thinked to the same solution for monitoring a 18650 battery for another project.

    Your solution is the simplest. But, I am worried by a deep discharge of the battery linked to the connection of the plus and the minus through two resistors, if we forget the assembly, I am almost sure that the battery will die because nothing will stop its voltage drop below the critical value of 3 volts.
    How to avoid this?


    1. SOS

      My knowledge is very limited as I am still on the beginner stages but I couldn’t stop thinking about your question.
      I am wondering if you could use a TP4056 Module for the battery and connect the +/- on TP4056 output as opposed to directly on the battery. Would the TP4056 cut power once it hits the low point considering it as current draw?

      1. Philippe

        Hello, yes it will be prevent deep decharge, but you still not have the information of the level of charge of the battery itself. We want to obtain this level for indicating on the LCD screen.



Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.