Heltec Wifi LoRa V2 battery management

I think I just answered this with some DMM (Fluke 289) measurements on my board; for these measurements, I had USB connected together with LiPo (charging), where my Vbat was 4.15V.

  1. When GPIO21 was HIGH 3.3V, GPIO13 was 0V, GPIO37 was 3.58V, Vext was 2.92V.
  2. When GPIO21 was LOW 0V, GPIO13 was 0V, GPIO37 was 1.28V, Vext was 3.3V.

Ignoring the negligible Rds of the AO3401, with the 320K/100K voltage divider, for case (2) I expected approximately 3.2*1.28=4.096V, which was pretty close to my Vbat of 4.15V.

Also, G6EJD has shared a simple ESP32 voltage reading function on github which applies a polynomial correction to the ADC read. Here’s some example output from my test sketch (when my DMM was measuring Vbat of 4.16V:

-> analogRead(37): 1424
-> ReadVoltage(37): 1.30
-> Battery voltage: 4.17

Hello,

I’ve been developing sketches for my Heltec Lora V2 and working to try to read the battery voltage. I’ve read a lot here that indicates that the internal divider provided (activated by GPIO21 = LOW) tied to originally GPIO13/ADC2_4 and has moved to GPIO37/ADC1_1. I don’t seem to be getting any good values out of mine over about 1.2v (before x3.2 divider). That is on a 6dB gain. I think that means I need to move to a higher gain, even with the divider, but the range for 6dB atten setting says that should go from 150mV-1750mV… I really think I need to know how to determine WHICH version of V2 I have (old GPIO13 vs. new 37)? GPIO13 seems to be at or near 100mV for both states of GPIO21. But I really am struggling with why I can’t get readings that never seem to differ from 1.21v (even with a very low lipo around 3.2v or up to 4.2v).

atten

BTW, I think I have found the V2.1 documentation and revision history, however, they both show V2 on board, so no way that I can see to determine difference, other than compare voltage on GPIO37 vs 13, I think.
https://heltec-automation-docs.readthedocs.io/en/latest/esp32/wifi_lora_32/hardware_update_log.html#v2-1

Somehow, I seem to have things working with the 6dB atten now and I’m using the EFuse compensated internal calibration readings as well for ADC1. I am not certain what was being done incorrectly before (possibly something I had in the ADC setup was setting me at 0dB atten through the various sketches I was trying to copy ADC readings from). I’ll try to clean up what I have working to just demo what I’ve got working and upload sketch.

Also, the V2.1 pinout document is wrong:

It shows labeled 3v3 and GND in upper left (3rd/4th pin from top left), where those are BOTH actually clearly labeled Vext on the board silkscreen on back and I’ve confirmed are = 3.3v when GPIO21 low (see the image below for the silkscreen label that shows it also Vext and Vext):

Here is a sample sketch that will setup the ADC reads (with per board EFuse calibration conversion/compensation), ensures some settle time for GPIO pins to ADC, adds in a sample / averaging buffer, has low voltage cutoff to deep sleep, uses light sleep if on battery mode at some threshold, and works with both V2.0 and V2.1 boards.

Note: I’m not sure why I can’t set GPIO21 LOW in setup and never digital write it low again, but have to continually assert it low in the ADC reading routines (this must have been my original issue with floating values and why I couldn’t get things functioning well…once I put the GPIO21 drive low in the loop at each ADC scan, it works). Something in my routines is turning off GPIO21 or something in the OLED drivers is…

Originally, I tried to set this up to just pulse the GPIO21 low and connect the ADC read during the scan and then turn GPIO21 back off, but it appears the OLED display is now powered off of Vext since V2.0 and flipping GPIO21 high disconnects Vext from 3.3v now. The OLED seems to still work, just lower brightness, which I can only assume is because the display is powering from forward biasing input clamps from I2C SCL/SDA when writing…anyway…pretty obvious from the V2.0 schematic, you shouldn’t raise GPIO21 high if you want to use OLED…

vext

// Heltec WiFi LoRa V2 battery read example 
// by Jeff McClain  jeff@themcclains.net
//
#include <Arduino.h>
#include <esp_adc_cal.h>
#include <driver/adc.h>
#include "heltec.h"

#define MAXBATT                 4200    // The default Lipo is 4200mv when the battery is fully charged.
#define LIGHT_SLEEP_VOLTAGE     3750    // Point where start light sleep
#define MINBATT                 3200    // The default Lipo is 3200mv when the battery is empty...this WILL be low on the 3.3v rail specs!!!

#define VOLTAGE_DIVIDER         3.20    // Lora has 220k/100k voltage divider so need to reverse that reduction via (220k+100k)/100k on vbat GPIO37 or ADC1_1 (early revs were GPIO13 or ADC2_4 but do NOT use with WiFi.begin())
#define DEFAULT_VREF            1100    // Default VREF use if no e-fuse calibration
#define VBATT_SAMPLE            500     // Battery sample rate in ms
#define VBATT_SMOOTH            50      // Number of averages in sample
#define ADC_READ_STABILIZE      5       // in ms (delay from GPIO control and ADC connections times)
#define LO_BATT_SLEEP_TIME      10*60*1000*1000     // How long when low batt to stay in sleep (us)
#define HELTEC_V2_1             1       // Set this to switch between GPIO13(V2.0) and GPIO37(V2.1) for VBatt ADC.
#define VBATT_GPIO              21      // Heltec GPIO to toggle VBatt read connection ... WARNING!!! This also connects VEXT to VCC=3.3v so be careful what is on header.  Also, take care NOT to have ADC read connection in OPEN DRAIN when GPIO goes HIGH
#define __DEBUG                 0       // DEBUG Serial output

uint16_t Sample();
void drawBattery(uint16_t, bool = false);

esp_adc_cal_characteristics_t *adc_chars;



void setup() {
  while (! Serial);
  delay(20);

  // Characterize ADC at particular atten
  #if (defined(HELTEC_V2_1))
  adc_chars = (esp_adc_cal_characteristics_t*)calloc(1, sizeof(esp_adc_cal_characteristics_t));
  esp_adc_cal_value_t val_type = esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_6, ADC_WIDTH_BIT_12, DEFAULT_VREF, adc_chars);
  adc1_config_width(ADC_WIDTH_BIT_12);
  adc1_config_channel_atten(ADC1_CHANNEL_1,ADC_ATTEN_DB_6);
  #else
  // Use this for older V2.0 with VBatt reading wired to GPIO13
  adc_chars = (esp_adc_cal_characteristics_t*)calloc(1, sizeof(esp_adc_cal_characteristics_t));
  esp_adc_cal_value_t val_type = esp_adc_cal_characterize(ADC_UNIT_2, ADC_ATTEN_DB_6, ADC_WIDTH_BIT_12, DEFAULT_VREF, adc_chars);
  adc2_config_channel_atten(ADC2_CHANNEL_4,ADC_ATTEN_DB_6);
  #endif

  #if defined(__DEBUG) && __DEBUG > 0
  Serial.printf("ADC Calibration: ");
  if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) {
      Serial.printf("eFuse Vref\n");
  } else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP) {
      Serial.printf("Two Point\n");
  } else {
      Serial.printf("Default[%dmV]\n",DEFAULT_VREF);
  }
  #else
  if (val_type);    // Suppress warning
  #endif

  Heltec.begin(true /*DisplayEnable Enable*/, false /*LoRa Disable*/, true /*Serial Enable*/);
  Heltec.display->flipScreenVertically();
  Heltec.display->setFont(ArialMT_Plain_10);
  Heltec.display->clear();

  #if defined(__DEBUG) && __DEBUG >= 1
  Serial.printf("ADC Calibration: ");
  if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) {
      Serial.printf("eFuse Vref\n");
  } else if (val_type == ESP_ADC_CAL_VAL_EFUSE_TP) {
      Serial.printf("Two Point\n");
  } else {
      Serial.printf("Default[%dmV]\n",DEFAULT_VREF);
  }
  #else
  if (val_type);    // Suppress warning
  #endif

  // Prime the Sample register
  for (uint8_t i = 0;i < VBATT_SMOOTH;i++) {
    Sample();
  }

  pinMode(VBATT_GPIO,OUTPUT);
  digitalWrite(VBATT_GPIO, LOW);              // ESP32 Lora v2.1 reads on GPIO37 when GPIO21 is low
  delay(ADC_READ_STABILIZE);                  // let GPIO stabilize

}


void loop() {
  Heltec.display->clear();
  uint16_t voltage = Sample();
  drawBattery(voltage, voltage < LIGHT_SLEEP_VOLTAGE);
  Heltec.display->display();

  if (voltage < MINBATT) {                  // Low Voltage cut off shut down to protect battery as long as possible
    Heltec.display->setColor(WHITE);
    Heltec.display->setFont(ArialMT_Plain_10);
    Heltec.display->setTextAlignment(TEXT_ALIGN_CENTER);
    Heltec.display->drawString(64,24,"Shutdown!!");
    Heltec.display->display();
    delay(2000);
    #if defined(__DEBUG) && __DEBUG > 0
    Serial.printf(" !! Shutting down...low battery volotage: %dmV.\n",voltage);
    delay(10);
    #endif
    esp_sleep_enable_timer_wakeup(LO_BATT_SLEEP_TIME);
    esp_deep_sleep_start();
  } else if (voltage < LIGHT_SLEEP_VOLTAGE) {     // Use light sleep once on battery
    uint64_t s = VBATT_SAMPLE;
    #if defined(__DEBUG) && __DEBUG > 0
    Serial.printf(" - Light Sleep (%dms)...battery volotage: %dmV.\n",(int)s,voltage);
    delay(20);
    #endif
    esp_sleep_enable_timer_wakeup(s*1000);     // Light Sleep does not flush buffer
    esp_light_sleep_start();
  }
  delay(ADC_READ_STABILIZE);
}


// Poll the proper ADC for VBatt on Heltec Lora 32 with GPIO21 toggled
uint16_t ReadVBatt() {
  uint16_t reading = 666;

  digitalWrite(VBATT_GPIO, LOW);              // ESP32 Lora v2.1 reads on GPIO37 when GPIO21 is low
  delay(ADC_READ_STABILIZE);                  // let GPIO stabilize
  #if (defined(HELTEC_V2_1))
  pinMode(ADC1_CHANNEL_1, OPEN_DRAIN);        // ADC GPIO37
  reading = adc1_get_raw(ADC1_CHANNEL_1);
  pinMode(ADC1_CHANNEL_1, INPUT);             // Disconnect ADC before GPIO goes back high so we protect ADC from direct connect to VBATT (i.e. no divider)
  #else
  pinMode(ADC2_CHANNEL_4, OPEN_DRAIN);        // ADC GPIO13
  adc2_get_raw(ADC2_CHANNEL_4,ADC_WIDTH_BIT_12,&reading);
  pinMode(ADC2_CHANNEL_4, INPUT);             // Disconnect ADC before GPIO goes back high so we protect ADC from direct connect to VBATT (i.e. no divider
  #endif

  uint16_t voltage = esp_adc_cal_raw_to_voltage(reading, adc_chars);  
  voltage*=VOLTAGE_DIVIDER;

  return voltage;
}

//  Use a buffer to average/sample ADC
uint16_t Sample() {
  static uint8_t i = 0;
  static uint16_t samp[VBATT_SMOOTH];
  static int32_t t = 0;
  static bool f = true;
  if(f){ for(uint8_t c=0;c<VBATT_SMOOTH;c++){ samp[c]=0; } f=false; }   // Initialize the sample array first time
  t -= samp[i];   // doing a rolling recording, so remove the old rolled around value out of total and get ready to put new one in.
  if (t<0) {t = 0;}

  // ADC read
  uint16_t voltage = ReadVBatt();

  samp[i]=voltage;
  #if defined(__DEBUG) && __DEBUG > 0
  Serial.printf("ADC Raw Reading[%d]: %d", i, voltage);
  #endif
  t += samp[i];

  if(++i >= VBATT_SMOOTH) {i=0;}
  uint16_t s = round(((float)t / (float)VBATT_SMOOTH));
  #if defined(__DEBUG) && __DEBUG > 0
  Serial.printf("   Smoothed of %d/%d = %d\n",t,VBATT_SMOOTH,s); 
  #endif

  return s;
}


void drawBattery(uint16_t voltage, bool sleep) {
  Heltec.display->setColor(BLACK);
  Heltec.display->fillRect(99,0,29,24);

  Heltec.display->setColor(WHITE);
  Heltec.display->drawRect(104,0,12,6);
  Heltec.display->fillRect(116,2,1,2);

  uint16_t v = voltage;
  if (v < MINBATT) {v = MINBATT;}
  if (v > MAXBATT) {v = MAXBATT;}
  double pct = map(v,MINBATT,MAXBATT,0,100);
  uint8_t bars = round(pct / 10.0);
  Heltec.display->fillRect(105,1,bars,4);
  Heltec.display->setFont(ArialMT_Plain_10);
  Heltec.display->setTextAlignment(TEXT_ALIGN_RIGHT);
  // Draw small "z" when using sleep
  if (sleep > 0) {
    Heltec.display->drawHorizontalLine(121,0,4);
    Heltec.display->drawHorizontalLine(121,5,4);
    Heltec.display->setPixel(124,1);
    Heltec.display->setPixel(123,2);
    Heltec.display->setPixel(122,3);
    Heltec.display->setPixel(121,4);
  }
  Heltec.display->drawString(127,5,String((int)round(pct))+"%");
  Heltec.display->drawString(127,14,String(round(voltage/10.0)/100.0)+"V");
  #if defined(__DEBUG) && __DEBUG > 0
  static uint8_t c = 0;
  if ((c++ % 10) == 0) {
    c = 1;
    Serial.printf("VBAT: %dmV [%4.1f%%] %d bars\n", voltage, pct, bars);
  }
  #endif
}
1 Like

Hi there.

I’m still having difficulties to read battery voltage. I’ve configured GPIOs and ADC attenuation like following:

#define GPIO_HABILITA_LEITURA_BATERIA 21
#define GPIO_ADC_BATERIA 13
#define BITS_RESOLUCAO_ADC 12 //max: 4095
#define MAX_VALOR_ADC_BAT 4095

void configura_leitura_tensao_bateria(void)
{
/* Configura GPIO do ADC inicialmente como open drain para evitar que
o habilitar da leitura de tensão de bateria envie diretamente 4.7V (max)
ao ADC */
pinMode(GPIO_ADC_BATERIA, OPEN_DRAIN);

/* Configura ADC */
analogSetPinAttenuation(GPIO_ADC_BATERIA, ADC_6db);
analogReadResolution(BITS_RESOLUCAO_ADC); 

/* Habilita leitura da tensão de bateria */    
pinMode(GPIO_HABILITA_LEITURA_BATERIA, OUTPUT);
digitalWrite(GPIO_HABILITA_LEITURA_BATERIA, LOW);

}

And I’m trying to read as follows:

bat_analog_read = analogRead(GPIO_ADC_BATERIA);
Serial.print("Analog read (battery): ");
Serial.println(bat_analog_read);

However, I’ve got very inconsistent readings:

ead (battery): 256
Analog read (battery): 502
Analog read (battery): 721
Analog read (battery): 448
Analog read (battery): 64
Analog read (battery): 6
Analog read (battery): 86
Analog read (battery): 256
Analog read (battery): 491
Analog read (battery): 709
Analog read (battery): 416
Analog read (battery): 49
Analog read (battery): 6

If I switch to GPIO 37 (as used in board V2.1), I only get 0 as readings.

Please, would you tell me what I’m doing wrong? I have no idea what’s wrong with my approach.

Hi, please refer to this two part about ADC voltage read:

Cool!!!

I will try you code tomorrow!

Hi Bro… You avatar blink blinded my eye :joy::joy::joy:

1 Like

When my son was born, a relative photoshopped my face onto his body and said “He looks just like you!” LoL.
avatar

I have some doubt about the discharge protection.
I have tested and the board did not cut the battery power when it reached 2.98V (measured with a multimeter). It is common practice to not allow a battery to go below 3.2 or 3V at worst to avoid damage. What is the voltage at which the power management chip cut the power?

Regarding the voltage measurement by the ESP32, I have found something that is working good for me, have a look here: Heltec battery power example not working

There is only charging protection on the development board, and no discharge protection. HelTec has a battery that matches the development board, which has discharge protection, and the discharge cut-off voltage is 2.75V. If you have not purchased a HelTec battery, you should check the discharge cut-off voltage of your battery.

Yes I also have a “1000 mAh” battery (more 650 in reality) that was sold with the board, not sure it was manufactured by Heltec as it does not have their brand on it but is seems it has cut out voltage at around 2.75 V which is a bit too low to be really safe for the battery but saying that the board has battery management is a bit an overstatement if it only has overcharge protection and not over discharge.

Hi…It uses a special power management chip that can enable/disable power for parts of the board. I am not aware of its lowest possible current during deep-sleep though. If you do not need the GPS then this board is relatively expensive.

pcb design for manufacturing checklist

Hi, I am using Heltec LoRa Stick and want to calculate battery percentage through inbuilt method i.e through pin 13. I have attached 4500mAh, 3.7V battery. I tried your battery code and I am getting " you can see OLED printed OLED initial done! " on serial monitor. Can kindly someone guide me where I am wrong?

Hi, I am using Heltec LoRa Stick and want to calculate battery percentage through inbuilt method i.e through pin 13. I have attached 4500mAh, 3.7V battery. I tried your battery code and I am getting " you can see OLED printed OLED initial done! " on serial monitor. Can kindly someone guide me where I am wrong?

Hi, I’m using Heltec LoRa Stick lite too (v 2.1). I’m not able to read the battery level or convert it into voltage. For example I tried with 3,6V and the analog read result on pin 37 in digit was 822. With the old version of the stick (pin 13) the analog read result (in digit) for the same voltage was 1964. (same sketch and pin 21 LOW). Thankyou

I’ve read the whole post and I’d like to know, finaly how can I read the battery voltage on my Helte Esp32 LoRa V2, trought the pin 37, and detect when 5V charging power supply pin is disconnected.

I’ve already tested the codes above, and the Jeff McClain code worked a little bit bad, and one another code I got from github “battery read” worked better but is an extensive code that I can’t include on my current sketch.

Does anyone know the easyest way to measure and detect whem USB or 5V power supply is disconnected?

Thanks to everyone.

true, i have the same issue… i’m still coming up with and idea how to deal with this.

Hello, I recently had to work through the battery measurement issue myself and it seems that there is still considerable questions about how it works. Bits and pieces of useful data were available on the discussion but the same questions kept coming up on this group and other groups. I am new to Arduino and even the ESP32 board but I am not new to battery powered devices and believe my input can help resolve the issue because I noticed these key points were not always made together:

  1. Knowing which version of the board you are using is important because version 2.1 changed the pin connected to the battery (I/O Pin 37) to solve a problem it was causing with the WiFi.
    https://heltec-automation-docs.readthedocs.io/en/latest/esp32%2Barduino/wifi_kit_32/hardware_update_log.html

  2. The divider is needed to correctly read the battery voltage. The maximum battery voltage can be over 4 volts (typically 3.7 V when fully charged) and the ADC can only measure up to 3.3 V.

  3. Most LiIon batteries have built in protection to keep them from over-charging, over-discharghing, shorting out and overheating. The batteries will not self-destruct if they have been discharged, they simply disconnect themselves from the circuit and will recover after a bit of charging. They trickle charge until the internal voltage is high enough for them to safely recharge at the fast rate.

  4. One last point to make is that the P-Ch FET connected to the divider there will still be some leakage current through the ADC pin which has a protection diode connected to 3.6V. The current will be (VBATT - 3.6) / 220K or ~5 uA. The leakage current will drop once the battery voltage drops below 3.6 Volts.

After some research and experimentation I was able to get my WIFI Kit 32 to reliably measure the battery voltage by doing the following:

void Battery::measure(){
  float dividerRatio = 3.2; 	// Used measured values to determine the ratio
  Serial.println("Inside Battery::Measure");

// 3.3 Volts is the theoretical value. The actual value depends on the 
// board's voltage regulator.
  float FS = 3.3 / 4095;		// Calculate the bit weight from Full Scale 
  uint16_t MULT = 1000; 		// Convert to milliVolts
  delay(10);					// Allow things to stabilize

// Make a measurement without the divider.
  uint16_t c  =  analogRead(bat.measure_pin)*FS*MULT;
  Serial.println("Batt V (undivided): " + String( c )); 
  digitalWrite(bat.divider_on_pin, LOW);
  delay(10);

// Make a measurement with the divider.
  c  =  analogRead(bat.measure_pin)*XS*MUL;
  Serial.println("Batt V (divided): " + String( c )); 
  
// Multiply the measured voltage by the divider ratio
  Serial.println("Batt V (calibrated): " + String( c*divider )); 
  digitalWrite(bat.divider_on_pin, HIGH); // open divider
  bat.voltage = c*divider;

Without the battery plugged in I measured about 5.1 Volts which is the USB voltage. With a battery connected the voltage will be below 4.1 volts depending on the batteries charge level.

Here is the Serial ouptput:
Inside Battery::Measure
Batt V (undivided): 3300
Batt V (divided): 1691
Batt V (calibrated): 5411.20

1 Like

I loaded your code on a V2.1 board, and it’s hanging after it sets ADC1_CHANNEL_1 to open drain. Not sure why that would be. FWIW, USB is connected, but no battery. Here’s the output.

ADC Calibration: eFuse Vref
VBATT_GPIO LOW()
ADC1_CHANNEL_1 OPEN_DRAIN()
<nothing after this>

Modification to your code:

uint16_t ReadVBatt ()
{
  uint16_t reading = 666;

  Serial.printf ("VBATT_GPIO LOW()\n");
  delay (200);
  digitalWrite (VBATT_GPIO, LOW);

  delay (ADC_READ_STABILIZE);

  Serial.printf ("ADC1_CHANNEL_1 OPEN_DRAIN()\n");
  delay (200);
  pinMode (ADC1_CHANNEL_1, OPEN_DRAIN);

  Serial.printf ("adc1_get_raw()\n");
  delay (200);
  reading = adc1_get_raw (ADC1_CHANNEL_1);

  Serial.printf ("ADC1_CHANNEL_1 INPUT()\n");
  delay (200);
  pinMode (ADC1_CHANNEL_1, INPUT);

  Serial.printf ("esp_adc_cal_raw_to_voltage()\n");
  delay (200);
  uint16_t voltage = esp_adc_cal_raw_to_voltage (reading, adc_chars);
  voltage *= VOLTAGE_DIVIDER;

  Serial.printf ("Returning...\n");
  delay (200);

  return voltage;
}