Battery Management

In applications that depend primarily on battery power, monitoring of battery voltage, knowing when it might be necessary to replace or recharge a battery, has been essential. To this end, following the lead from the work of Wijnand Nijs, I included circuitry to measure battery voltage in all of my base PCB designs.

More recently, however, all of the Heltec boards that I have been using include, as part of their power management circuitry, the ability to measure battery voltage. Nonetheless, the forums seem to receive regular queries from people struggling to work out the exact software configuration required to take advantage of this feature, so it seemed worthwhile to run through the details of the requirements for the different configurations that I have worked with.

Power Board Circuitry

As noted, from the outset, I included circuitry to measure battery voltage in all of my base PCB designs. That circuitry, which is common to all of the PCBs described on this site that include the capability, is illustrated below.

PCB-based Battery Voltage Measurement Circuitry

There are two essential features of this circuit that play into the supporting software design. First, the circuit is only active when the enable line (EN) is brought HIGH, so there is no drain on the battery when not in use. The second is that the ADC is connected through a voltage divider and this must be taken into account when calculating the measured voltage.

MOSFET Switch

When compared to the Heltec circuit configurations discussed below, the use of a dual MOSFET configuration in this circuit might be considered unnecessary. The original goal here, however, was for the circuit to be normally OFF, and to be switched on by application of a positive logic signal from the host MCU. Assuming that we are using enhancement-mode devices, the primary switch must then be an N-channel MOSFET, which is in the OFF state when no voltage is applied to its gate.

Switching an N-channel MOSFET, however, requires the gate voltage, the MCU logic level (3.3V), to be higher than the dain voltage. Our battery voltage (3.7V nominal), however, will normally be higher than the MCU logical level, so we cannot use the N-channel device directly to switch on the battery-powered voltage divider we are using to take our measurement. Instead, we use the N-channel device to drive a second device, a P-channel device, which requires a voltage that is lower, rather than higher than that of our battery to switch on the voltage divider circuit. The voltage divider is then configured on the drain side of the P-channel device, together with a noise dampening capacitor.

Our circuit thus includes the two MOSFETs, an N-channel device, Q1, configured with a pull-down resistor (R2) and a P-channel device, Q2, with a pull-up resistor (R3), so that both are in a normally open (OFF) state. The source of Q1 is connected to ground while its drain is connected to the gate of Q2 so that, when the Q1 is ON, the Q2 gate is pulled to ground, switching Q2 ON and connecting the battery to the voltage divider that is used to measure the applied voltage.

The practical reality has been that the single P-channel MOSFET configuration used by Heltec, and described below, has been entirely adequate. The big difference between the dual MOSFET configuration, described above, and the Heltec configuration is that the former was intended to minimise battery drain in applications that were solely battery-powered. As it turns out, in our applications, where we have augmented battery power with a solar recharging system, any battery drain due to leakage across an 'active' voltage divider has been easily offset by the recharging provided by the solar panel.

A detailed discussion on the use of MOSFETs in load switch applications can be found in onsemi™ Application Note AND9093-D.

Voltage Divider

Given that our prototyping work involves several different MCUs, it is also important to recognise the specific characteristics of the ADCs employed by the individual MCUs.

Analog-to-Digital Converter (ADC) Characteristics
ProcessorResolutionVmax
ATmega328P (Pro Mini 3.3V)103.3V
ESP8266101.0V
NodeMCU (ESP8266)103.3V
ESP32123.3V
ASR6501/ASR6502 (CubeCell [Plus])122.4V

The need for a voltage divider in the measurement circuit is evident in the fact that the voltage of a fully charged Li-Ion battery is around 4.2V, while the maximum input for all of the above ADCs is less than this. The above circuit was originally designed to support a 3.3V Arduino Pro mini and thus incorporated a 27kΩ/100kΩ voltage divider. This scales the input voltage by a factor of 0.7874, bringing a voltage of 4.2V back to 3.3V.

A factor to bear in mind when configuring a voltage divider is that the ADCs included in some processor modules, the ESP32 in particular, are notoriously non-linear, especially towards the high end of their range. Accordingly, in such cases, it's not a bad idea to use a voltage divider that brings the maximum voltage back to well within the range of the target ADC.

To this end, in more recent configurations, I have used a 33kΩ/100kΩ or even 47kΩ/100kΩ configuration to avoid using the high end of the ADC range, particularly when used in conjunction with an ESP32 processor.

When the above circuit is used in conjunction with an ESP12-E/F/S (ESP8266) processor, an additional voltage divider (220kΩ/100kΩ) must be configured, or the above circuit modified accordingly, to bring the maximum input voltage back below 1.0V. A 220kΩ/100kΩ voltage divider is configured on the NodeMCU [ESP8266] board, so nothing extra is required in this case.

While the CubeCell ASR650x ADCs are limited to 2.4V, and would also require a modified voltage divider if using the above circuitry, these MCUs have their own, built-in battery voltage measurement capability (see below) and thus have no need of the above circuit elements at all.

The factor appropriate for the voltage divider configuration in use, whatever it may be, must then be allowed for when using our measurements to calculate the battery voltage.

Example Code

The following is a typical code segment used to measure battery voltage using the circuit described above on the base power PCBs described elsewhere on this site.

Battery Voltage Measurement
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#define batteryMonitor36
#define wakeControl13

uint16_t readBatteryVoltage() {
// ADC resolution
const int resolution = 12;
const int adcMax = pow(2,resolution) - 1;
const float adcMaxVoltage = 3.3;
// On-board voltage divider
const int R1 = 27;
const int R2 = 100;
// Calibration measurements
const float measuredVoltage = 4.2;
const float reportedVoltage = 4.14;
// Measured value multiplication factor
const float factor = (adcMaxVoltage / adcMax) * ((R1 + R2)/(float)R2) * (measuredVoltage / reportedVoltage);

float averageReading;
uint16_t analogValue = 0;
uint16_t voltage = 0;

digitalWrite(wakeControl,HIGH);
delay(500); // Wait a moment for things to settle down

// The ESP32 doesn't have a particularly good ADC, so we calculate an average

int averageCount = 10;
for (int i = 0; i < averageCount; i++) {
analogValue += analogRead(batteryMonitor);
delay(10);
}
averageReading = analogValue / (float)averageCount;

digitalWrite(wakeControl,LOW);
voltage = (unit16_t)(averageReading * factor * 1000); // milliVolts
return voltage;
}

The MOSFET switch is turned on by raising the signal on the pin assigned to wakeControl. The actual reading of an ADC is a simple matter of performing an analogRead() on the relevant processor pin, in this case the pin assigned to batteryMonitor. More complicated is the derivation of the calibration mathematics.

There are basically three calibration points: the resolution of the ADC, the configuration of any voltage divider, and basic variation between processor modules.

The ADC resolution paramaters are derived from the relevant processor datasheet (see above).

The resistor values for an appropriate voltage divider must be calculated noting the scaling required to be provided by the voltage divider. In the formula provided in the above code example, R2 is the resister that is connected to ground, the resister across which our configuration takes its measurement. To use only the lower part of the range of an ADC, in the case of an ESP32 processor for example, one would simply modify the configuration of resistors in the voltage divider (decrease the relative value of R2), so that the maximum voltage actually measured is well below the 3.3V maximum that can be recorded.

The manual calibration parameters are set after the first code execution, with both measuredVoltage and reportedVoltage initially set to the battery voltage as measured with a multimeter. The voltage that is reported by this initial sketch run is then used to set the value of reportedVoltage, and the code recompiled for subsequent execution.

Processor Module Power Management Circuitry

Heltec Dev-Boards, with the exception of the first WiFI LoRa 32, as part of their power management circuitry include the ability to measure battery voltage (the battery must be connected via the on-board JST 1.25 battery socket to use this feature). Unfortunately that's about where the commonality ends. Almost every Heltec board has a different configuration, although there is some convergence with current board revisions. Different boards employ different GPIOs for control and reading, and different resistor values in their respective voltage divider configurations. Care must therefore be taken when porting software from one processor to the other, even when they appear to only be different revisions of the same board.

The Heltec Hardware Update Logs do provide some useful information in this regard and, with any luck, the following will help clarify some of the individual board requirements.

Much of the above discussion also applies in the present case. Even though the relevant circuitry varies slightly, the basic principles are the same—an ADC is used to make a measurement across a voltage divider, and that measurement is adjusted using an appropriate calibration factor to yield a final voltage measurement.

Heltec WiFi LoRa 32 (V1)

The original Heltec WiFi LoRa 32 development board, while it did include an on-board Li-Po battery charging module, did not include any circuitry to support monitoring of the battery voltage.

Heltec WiFi LoRa 32 (V2)

The first iteration of Heltec battery voltage monitoring circuitry appeared in the second revision of their WiFi LoRa 32 Dev-Board. The relevant details from the WiFi LoRa 32 (V2) schematic are illustrated below.

Heltec WiFi LoRa 32 V2 ADC Control Circuitry

This configuration used a 220kΩ/100kΩ configuration in its voltage divider, a scaling factor of ~0.3125, bringing the voltage of a fully charged Li-Ion battery (4.2V) back to ~1.3V for measurement, well within the lower half of the ADC's range. I had always assumed that the reason for this was to manage the notoriously poor linearity at the higher end of the range of the ESP32 ADC, this reduction in range being offset somewhat by the ESP32 ADC's 12 bit resolution. See, however, the further discussion below on this subject.

I have never possessed a V2 board, so I am unable to offer any sample code that has actually run on one of these boards.

Heltec WiFi LoRa 32 (V2.1)

Apparently, there were problems with the V2 board and Heltec quietly released a revised V2.1 board. Unfortunately, the V2.1 board used different, internally configured GPIOs for some functions, limiting portability of software between board revisions. To make matters worse, the V2.1 boards were still labelled V2, with only subtle layout revisions distinguishing the two different boards.

Nonetheless, the relevant details from the WiFi LoRa 32 (V2.1) schematic are illustrated below. The circuitry is similar to the V2 configuration, but note the use of GPIO37 rather than GPIO13 for the ADC used for battery voltage measurement.

Heltec WiFi LoRa 32 V2.1 ADC Control Circuitry

As with the V2 board, the use of GPIO21 to activate the MOSFET switch (it simultaneously activated a similar MOSFET switch to turn on Vext) caused conflict with its normal use as the I2C SDA GPIO—SDA had to be redefined when using Vbat (or Vext).

Note also the indecision in relation to the location of the voltage divider resistors and load in the MOSFET switch configuration. It is generally recommended to configure the load, the ADC and associated voltage divider, on the drain of the MOSFET, as in the configuration finally adopted on the V3 board (see below).

Example Sketch

The principle is the same, and the code here is therefore very similar to that described above. We turn on a MOSFET switch, take a reading on an ADC, then convert this reading to a voltage measurement.

WiFi LoRa 32 V2.1 Battery Voltage Measurement
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <Arduino.h>
#include <Wire.h>// I2C bus
#include <SSD1306.h>// OLED display

#define Vbat_Read37
#define ADC_Ctrl21

SSD1306 myDisplay(0x3c, SDA_OLED, SCL_OLED);

void setup() {
Serial.begin(115200);
delay(500);
Serial.println();
Serial.print("[setup] Battery Voltage Read");
Serial.println();
Serial.println("[setup] Commence Set-up...");

pinMode(RST_OLED,OUTPUT);// GPIO16
digitalWrite(RST_OLED,LOW);// Set GPIO16 LOW to reset OLED
delay(50);
digitalWrite(RST_OLED,HIGH);

myDisplay.init();
myDisplay.clear();
myDisplay.setFont(ArialMT_Plain_16);
myDisplay.setTextAlignment(TEXT_ALIGN_LEFT);

pinMode(ADC_Ctrl,OUTPUT);
pinMode(VBAT_Read,INPUT);
adcAttachPin(VBAT_Read);
analogReadResolution(12);
readBatteryVoltage();

Serial.println("[setup] Set-up Complete");
Serial.println();
}

void loop() {
}

uint16_t readBatteryVoltage() {
// ADC resolution
const int resolution = 12;
const int adcMax = pow(2,resolution) - 1;
const float adcMaxVoltage = 3.3;
// On-board voltage divider
const int R1 = 220;
const int R2 = 100;
// Calibration measurements
const float measuredVoltage = 4.2;
const float reportedVoltage = 3.82;
// Calibration factor
const float factor = (adcMaxVoltage / adcMax) * ((R1 + R2)/(float)R2) * (measuredVoltage / reportedVoltage);
digitalWrite(ADC_Ctrl,LOW);
delay(100);
int analogValue = analogRead(VBAT_Read);
digitalWrite(ADC_Ctrl,HIGH);

float floatVoltage = factor * analogValue;
uint16_t voltage = (int)(floatVoltage * 1000.0);

Serial.print("[readBatteryVoltage] ADC : ");
Serial.println(analogValue);
Serial.print("[readBatteryVoltage] Float : ");
Serial.println(floatVoltage,3);
Serial.print("[readBatteryVoltage] milliVolts : ");
Serial.println(voltage);

myDisplay.clear();
myDisplay.drawString(0,5,"Battery Level");
myDisplay.display();
myDisplay.drawString(0,25, String(floatVoltage) + " Volts");
myDisplay.display();

return voltage;
}

Heltec WiFi LoRa 32 (V3)

The V3 board was a major upgrade of the original WiFi LoRa 32 configuration. It uses the ESP32-S3FN8 (rather than ESP32-D0WDQ6) processor and a newer LoRa chip, the SX1262. Relevant details from the WiFi LoRa 32 (V3) schematic are illustrated below.

Heltec WiFi LoRa 32 V3 ADC Control Circuitry

The AO7801 is a dual P-channel MOSFET. While it may not be entirely obvious to the untrained eye, the two channels, S1/G1/D1 and S2/G2/D2, are entirely independent and, in the above case, there is no implicit coupling of the Vext and Vbat functions.

With regard to the operation of the ADC, in the idle state, the gate voltage (G1) is pulled HIGH through the 10kΩ pull-up resistor (R8) to VDD. In this state, the drain circuit (D1) to the ADC is open. When the gate voltage, ADC_Ctrl, is taken LOW, the circuit between Vbat (S1) and the ADC (D1) is closed, and the battery voltage can be read.

Note the values of the resistors in the voltage divider (R14/R17)—390kΩ/100kΩ, a scaling factor of ~0.2041, which brings an input voltage of 4.2V (the maximum we would expect to see from a Li-Ion battery) back to less than 1V, when the ADC is capable of taking an input of up to 3.3V. I found this a little puzzling until it was brought to my attention that I had not considered the impact of attenuation on ADC readings, the full details of which are provided in the Espressif ESP32 API Reference.

I had assumed that this was to accommodate the notoriously non-linear behaviour of the ESP32 ADC and keep the ADC reading well within a linear part of its range. This may still be the case, but the [more or less] full resolution of the ADC is indeed available if the attenuation is set to 0dB, rather than 11dB, the default setting in the present case.

For reference, the following are the details of the relevant functions from the esp32‑hal‑adc.h library file:

typedef enum {
ADC_0db,
ADC_2_5db,
ADC_6db,
ADC_11db,
ADC_ATTENDB_MAX
} adc_attenuation_t;
.
.
.
/*
* Set the attenuation for all channels
* Default is 11db
* */
void analogSetAttenuation(adc_attenuation_t attenuation);

/*
* Set the attenuation for particular pin
* Default is 11db
* */
void analogSetPinAttenuation(uint8_t pin, adc_attenuation_t attenuation);

It's not clear why Heltec chose to use the higher attenuation and associated reduced resolution as default settings, but a lower attenuation, and wider range of the ESP32's 12-bit resolution, are available using the above functions, if required.

Example Sketch

The battery voltage read code for the WiFi LoRa 32 V3 board is basically the same as that for the V2.1 board, the only changes being the Vbat_Read and ADC_Ctrl pin numbers and the value of voltage divider resistor variable R1.

WiFi LoRa 32 V3 Battery Voltage Measurement
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#include <Arduino.h>
#include <Wire.h>// I2C bus (only relevant for WiFi LoRa 32)
#include <SSD1306.h>// OLED display (only relevant for WiFi LoRa 32)

#define VBAT_Read 1
#define ADC_Ctrl37

SSD1306 myDisplay(0x3c, SDA_OLED, SCL_OLED);

void setup() {
Serial.begin(115200);
delay(500);
Serial.println();
Serial.print("[setup] Battery Voltage Read");
Serial.println();
Serial.println("[setup] Commence Set-up...");

// The Wireless Stick Lite has no display, so the OLED stuff is irrelevant in that case

pinMode(RST_OLED,OUTPUT);// GPIO16
digitalWrite(RST_OLED,LOW);// Set GPIO16 LOW to reset OLED
delay(50);
digitalWrite(RST_OLED,HIGH);

myDisplay.init();
myDisplay.clear();
myDisplay.setFont(ArialMT_Plain_16);
myDisplay.setTextAlignment(TEXT_ALIGN_LEFT);

pinMode(ADC_Ctrl,OUTPUT);
pinMode(VBAT_Read,INPUT);
adcAttachPin(VBAT_Read);
analogReadResolution(12);
readBatteryVoltage();

Serial.println("[setup] Set-up Complete");
Serial.println();
}

void loop() {
}

uint16_t readBatteryVoltage() {
// ADC resolution
const int resolution = 12;
const int adcMax = pow(2,resolution) - 1;
const float adcMaxVoltage = 3.3;
// On-board voltage divider
const int R1 = 390;
const int R2 = 100;
// Calibration measurements
const float measuredVoltage = 4.2;
const float reportedVoltage = 4.095;
// Calibration factor
const float factor = (adcMaxVoltage / adcMax) * ((R1 + R2)/(float)R2) * (measuredVoltage / reportedVoltage);
digitalWrite(ADC_Ctrl,LOW);
delay(100);
int analogValue = analogRead(VBAT_Read);
digitalWrite(ADC_Ctrl,HIGH);

float floatVoltage = factor * analogValue;
uint16_t voltage = (int)(floatVoltage * 1000.0);

Serial.print("[readBatteryVoltage] ADC : ");
Serial.println(analogValue);
Serial.print("[readBatteryVoltage] Float : ");
Serial.println(floatVoltage,3);
Serial.print("[readBatteryVoltage] milliVolts : ");
Serial.println(voltage);

myDisplay.clear();
myDisplay.drawString(0,5,"Battery Level");
myDisplay.display();
myDisplay.drawString(0,25, String(floatVoltage) + " Volts");
myDisplay.display();

// The following code can be included to demonstrate the effect of
// changing the ADC attenuation.

digitalWrite(ADC_Ctrl,LOW);
delay(100);
analogSetAttenuation(ADC_0db);
analogValue = analogRead(VBAT_Read);
int analogMilliValue = analogReadMilliVolts(VBAT_Read);
Serial.println();
Serial.println("[readBatteryVoltage] Attenuation Test...");
Serial.println();
Serial.println("[readBatteryVoltage] Attenuation : ADC_0db");
Serial.print("[readBatteryVoltage] ADC : ");
Serial.println(analogValue);
Serial.print("[readBatteryVoltage] Raw MilliVolts : ");
Serial.println(analogMilliValue);

delay(100);
analogSetAttenuation(ADC_2_5db);
analogValue = analogRead(VBAT_Read);
analogMilliValue = analogReadMilliVolts(VBAT_Read);
Serial.println();
Serial.println("[readBatteryVoltage] Attenuation : ADC_2_5db");
Serial.print("[readBatteryVoltage] ADC : ");
Serial.println(analogValue);
Serial.print("[readBatteryVoltage] Raw MilliVolts : ");
Serial.println(analogMilliValue);

delay(100);
analogSetAttenuation(ADC_6db);
analogValue = analogRead(VBAT_Read);
analogMilliValue = analogReadMilliVolts(VBAT_Read);
Serial.println();
Serial.println("[readBatteryVoltage] Attenuation : ADC_6db");
Serial.print("[readBatteryVoltage] ADC : ");
Serial.println(analogValue);
Serial.print("[readBatteryVoltage] Raw MilliVolts : ");
Serial.println(analogMilliValue);

delay(100);
analogSetAttenuation(ADC_11db);
analogValue = analogRead(VBAT_Read);
analogMilliValue = analogReadMilliVolts(VBAT_Read);
Serial.println();
Serial.println("[readBatteryVoltage] Attenuation : ADC_11db");
Serial.print("[readBatteryVoltage] ADC : ");
Serial.println(analogValue);
Serial.print("[readBatteryVoltage] Raw MilliVolts : ");
Serial.println(analogMilliValue);

return voltage;
}

One might imagine that, since Heltec provide a shrink-wrapped getBatteryVoltage() function for the CubeCell [ASR650x] platform (see below), and since there's very little difference with what's actually going on under the covers, they might one day offer the same function for the ESP32 platform. At the time of writing, however, we have to go with the above, roll-your-own solution.

Heltec WiFi LoRa 32 (V3.2)

The V3.2 board included a complete rework of the battery management system—the TP4054 battery charging IC has been replaced with an LGS4056HDA, there is a new regulator layout and, critically in the present context, a modified battery voltage measurement circuit layout. Unfortunately, all of these changes have been made without anything more than the release of a revised schematic diagram, even though the latter change requires inversion of the software logic used to measure battery voltage.

The schematic of the updated ADC control circuitry on the WiFi LoRa 32 (V3.2) module is illustrated below.

Heltec WiFi LoRa 32 V3.2 ADC Control Circuitry

Note that the inclusion of the transistor in the control circuit means that ADC_Ctrl now needs to be brought HIGH, to switch on the transistor and pull the MOSFET gate voltage LOW, to switch on the battery voltage measurement circuit. While perhaps more logical, the immediate result is that the earlier battery management software logic is incompatible with the V3.2 module. For pre-V3.2 modules, we set ADC_Ctrl LOW before reading the battery voltage, as per the code example above, but for the V3.2 module we must set ADC_Ctrl HIGH before reading the battery voltage.

Heltec Wireless Stick Lite

The relevant details from the Wireless Stick Lite schematic are illustrated below.

Heltec Wireless Stick Lite ADC Control Circuitry

Further details pending...

Heltec Wireless Stick Lite (V2/V2.1)

Until recently, I had lived under the impression that there had been no upgrades to the Wireless Stick Lite board configuration prior to the release of the new ESP32-S3-based (V3) boards. However, there does appear to have been at least one upgrade, although I can find no documentation relating to V2 boards or anything that distinguishes the original board and V2 or V2.1 boards. I have seen a photo of a V2.1 board, which is at least labelled as such—to date, I have not sighted a V2 board. In the absence of any information to the contrary, however, I can only conclude that the hardware configuration of the V2 and/or V2.1 boards is the same as the original board. If users of a V2 board encounter problems, it would be worth looking at the changes made between the various releases of the WiFi LoRa 32 board for potential causes.

Heltec Wireless Stick Lite (V3)

The relevant details from the Wireless Stick Lite (V3) schematic are illustrated below.

Heltec Wireless Stick Lite V3 ADC Control Circuitry

This is similar to the WiFi LoRa 32 circuitry, except that it is implemented with an AO3401 single P-channel MOSFET. The operation of the circuit, however, is identical to that using the AO7801 described above.

The code to measure battery voltage on the Wireless Stick Lite V3 is exactly the same as that used on the WiFi LoRa 32 V3 development board.

At some point, no doubt, there will be a V3.2 Wireless Stick Lite module that will incorporate the circuitry described above for the WiFi LoRa V3.2 module and that will also require inversion of the previously used software logic when reading the battery voltage.

Heltec CubeCell

The CubeCell platform was originally based on the ASR6501 processor. Unfortunately, there are discrepancies both between Heltec documents describing the CubeCell Dev-Board and also between the documentation and what is observed in practice. The schematic for both the original and the more recent V2 (ASR6502) boards includes an identical representation of the battery voltage measurement circuitry, but the pin labelling, at least, is incorrect. Further, the so-called User Manual for the original board notes that the voltage divider used in measuring the battery voltage comprises a 100kΩ/390kΩ pairing (p.7, footnote 3), in contrast to the 10kΩ/10kΩ pairing illustrated in the schematic diagram. Other discrepancies in this document, however, would suggest that this is just one of several errors arising from careless copying of material from an ESP32 dev-board specification.

Some of these problems appear to have been rectified in the CubeCell Dev-Board V2 documentation, but one had me quite confused for some time. I originally thought that the schematic (below) in relation to battery voltage measurement was incorrect. Test sketches suggested that VBAT_ADC_CTL resolved to 27, which I had always assumed would be GPIO27, not GPIO7 as illustrated. But it turns out that GPIO7, the label, also resolves to 27, the number, so I'm not really sure what Heltec is playing at here. In fact, printing out the numbers behind the labels GPIO0..GPIO7, for example, on the CubeCell Dev-Board yields: 2, 49, 50, 52, 7, 6, 26, 27. I haven't gone off and tested this any further, but it seems that if you want to address the pins labelled with a GPIO number on a Heltec CubeCell Dev-Board, you'd better use the specific "GPIOn" identifier, and not rely on this being the same as the number "n", as I had always assumed it would be.

The fact that the solder bridge indicated in the schematic is clearly visible on the original Dev-Board, but nowhere to be seen on the V2 board, would also seem to support a suggestion that this element of the schematic may have been simply copied over from the V1 schematic without due consideration.

Heltec CubeCell ADC Control Circuitry
as illustrated in both the CubeCell Dev-Board 'original' and V2 Schematics

When it comes to the crunch, however, all this is a bit moot, as the Heltec getBatteryVoltage() function (see below) returns the battery voltage for any CubeCell Dev-Board, whatever the internal hardware configuration may be. In particular, the VBAT_ADC_CTL pin is set as required to make the reading, then reset on completion—there is no need for the user to worry about that aspect of reading the ADC.

Universal CubeCell getBatteryVoltage() Function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
uint16_t getBatteryVoltage(void) {
float temp = 0;
uint16_t volt;
uint8_t pin;
pin = ADC;

#if defined(CubeCell_Board_V2)
pin = ADC_VBAT;
#endif

#if defined(CubeCell_Board)||defined(CubeCell_Capsule)||defined(CubeCell_BoardPlus)||defined(CubeCell_BoardPRO)||defined(CubeCell_GPS)||defined(CubeCell_HalfAA)||defined(CubeCell_Board_V2)
/*
* have external 10K VDD pullup resistor
* connected to VBAT_ADC_CTL pin
*/

pinMode(VBAT_ADC_CTL, OUTPUT);
digitalWrite(VBAT_ADC_CTL, LOW);
#endif
for (int i = 0; i < 50; i++) //read 50 times and get average
temp += analogReadmV(pin);
volt = temp / 50;

#if defined(CubeCell_Board)||defined(CubeCell_Capsule)||defined(CubeCell_BoardPlus)||defined(CubeCell_BoardPRO)||defined(CubeCell_GPS)||defined(CubeCell_HalfAA)||defined(CubeCell_Board_V2)
pinMode(VBAT_ADC_CTL, INPUT);
#endif

volt = volt * 2;
return volt;
}

Heltec CubeCell Plus

Documentation for the CubeCell Dev-Board Plus claims that it uses a similar battery voltage measurement arrangement to that in the CubeCell Dev-Board, illustrated below, with the main differences being the values of the voltage divider resistors and the GPIO pins used for the control and measurement lines, although the latter are now only specified as 'labels', which are now the ones that have always been correct.

Heltec CubeCell Plus ADC Control Circuitry
as illustrated in the CubeCell Dev-Board Plus Schematic

Once again, the solder bridge illustrated in the schematic is not evident on the CubeCell Dev-Board Plus, although it would appear to be closed by default, as it is on the original CubeCell Dev-Board.

The processor on the CubeCell Dev-Board V2 was upgraded to the ASR6502, the same processor as is in the CubeCell Dev-Board Plus. The main difference with the ASR6502, in the present context, is that it includes three ADCs. Based on a bit of poking around in the supporting code, the processor section of the board schematic, and the results from the running of some test sketches, it would appear that the CubeCell Dev-Board V2 has dedicated the first of its ASR6502's ADCs, labelled ADC1 (schematic) or ADC_VBAT (pins_arduino.h), to reading the internal battery voltage with the second ADC, simply labelled ADC, available for other applications. The third ADC appears to be idle, not connected to anything and thus with no way of actually being used.

On the CubeCell Dev-Board Plus, all three ADCs are broken out. In this case, ADC1, also labelled ADC, is dedicated to the task of monitoring battery voltage, while ADC2 and ADC3 are available for general application use.

The circuitry itself is similar to the WiFi LoRa 32 circuitry described above, although, noting the voltage range supported by the ASR6502 ADCs, it incorporates a 100kΩ/100kΩ resistor configuration in its voltage divider, effectively halving the voltage of a fully charged Li-Ion battery (4.2V back to 2.1V) for measurement.

Example Sketch

Measuring the battery voltage on CubeCell Dev-Boards is a simple matter of including a call to the getBatteryVoltage() function.

CubeCell [Plus] Battery Voltage Measurement
1
2
3
4
5
6
7
8
9
10
11
12
13
void setup() {
Serial.begin(115200);
delay(200);
Serial.println();
Serial.println("[setup] CubeCell Battery Voltage Read");
uint16_t batteryVoltage = getBatteryVoltage();
Serial.print("[setup] Battery Voltage: ");
Serial.print( batteryVoltage );
Serial.println(" mV");
}

void loop() {
}
03-12-2024