Heltec Board is deep discharging batteries?

Hi,

I’m quite new to meshtastic. I’ve got several Heltec boards to play around with. Friday I attached one board to a li-ion battery to do some experiments. Unfortunately I forgot to disconnect the battery in the evening.

Today the battery has a voltage around 1,3V. It seems as the board has deep discharged the battery.

I assumed that there is better battery management.

Is it a problem in my setup or is the battery management really so bad?

Regards,

regnerus

1 Like

The heltec boards have essentially no battery management (just a dumb charge controller). Alas, they don’t even have a way for us to read the battery voltage (which is the rule we use to enforce deep sleep when battery gets critically low on other boards)

1 Like

Oh, that is annoying.

Does one of the other supported boards have better battery management?

The TTGO TBeams and TTGO Lora 32 boards do provide a way to read the battery voltage.

1 Like

The TTGO uses a simple voltage divider. You could do the same but you would have to build the firmware yourself with some minor changes. Build instructions
image

3 Likes

Hi dafeman,

that yout for this workaround. That will help me very much.

Regards,

regnerus

2 Likes

So it looks like the Heltec LoRa 32 boards do have a voltage divider that is linked to the Vext control pin (this also turns the screen on). GPIO13 could be used to sense battery voltage.

5 Likes

Oh! I never found that schematic. Would you mind adding it to docs/hardware?

If someone wants to send in a PR to define pin_battery for heltec I bet everything would just work. Great find!

3 Likes

I went to upload it and found it sitting there already! https://github.com/meshtastic/Meshtastic-device/blob/master/docs/hardware/heltec-wifi-lora-32-v2-915.pdf

I’m not sure how it would work compared to other boards that report the battery state, but when the Heltec device sleeps it will disable Vext (turn off screen) so the battery voltage can’t be read.

It also would appear GPIO13 would see full battery voltage as the divider isn’t being pulled to ground…(doesn’t sound great, maybe the 220K resistor saves it as it is already what is happening). I think the input limit is VDD +10%. The Heltec uses a different divider ratio so I’m guessing something would have to change there too.

I have added it to the Issues in Github.

1 Like

Any progress on this? Is this something where I can add code via CLI and get the thing to show me voltage, or is it more in depth? I know, my ignorance is stunning. :slight_smile:

Thanks for the offer!

I think if you collect the output at three different configurations it would make it easy for me to add this for heltec.

If you install any fairly recent release occasionally you’ll see debug output on the serial port that looks like:

Battery: usbPower=1, isCharging=0, batMv=4159, batPct=100

If you could connect your heltec to a battery and see what these messages say when:

  • The battery is partially charged
  • The battery is fully charged
  • No battery is connected at all

I could reverse engineer what the right multipliers are for the heltec.

2 Likes

Trying to solve it out and make battery monitoring to work with heltec. According to this discussion Manufacturer changed reading voltage pin to 37. I was trying to compile heltec firmware with added battery_pin 37 but unfortunately after upload with platformIO i get this error Guru Meditation Error: Core 1 panic’ed (LoadProhibited) and device is keep restarting in loop. Here is also example of voltage calculating for heltec.
Anyway thanks in advance for any tips and thank you for your grate work!
Take care!

2 Likes

Ok, here what i found to summarize:

  • Manufacturer changed batt_pin to 37 so if you are using GPS you have to remember to change gps_pin config
  • The voltage divider used in heltec have different ratio: R1=100k R2=220k so ratio =3.2
  • Here is working code for reading battery voltage on heltec but unfortunately my programming knowledge is to little to incorporate it to main code of meshtastic. Hopefully its gonna help someone.
// 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
}

4 Likes

Hi @liniany that gets us most of the way there. Can you open a bug on GitHub and link to this thread. I can make the ratio change you recommend.

2 Likes

Done :slight_smile:

2 Likes

Hi! I see that team is pretty busy in making mqtt to work but any progress on power issues with heltec devices? Take care!

Looks like that’s still open. I cleaned up duplicates (there were three similar) and left the oldest one open.

2 Likes

Hello friends!
A deep analysis of most of the simple Meshtastic devices shows that there is NO power controller on these boards. There is only the simplest charger. Power management cannot be directly started by software, because there is no hardware charge/discharge control unit on the circuits. You can only control the voltage level, somehow signal it and, if possible, put the device to deep-sleep as much as possible. The hardware control unit is only available in smart T-beam boards.

It follows from this that simple Meshtastik modules cannot be directly powered with “clean” Li-Ion/Li-Po batteries. They will discharge to 0, which severely damages the battery, almost to death.
You should look for such Li-Ion/Li-Po batteries for purchase, which have a built-in deep discharge limitation circuit in their composition! This is a “battery with BMS”. In the worst case, this scheme will disconnect the battery from the device itself, which will save the chemical degradation of the battery.

If this is true, non-T-Beam boards shouldn’t be listed as Supported Hardware without specifying that protected cells are required. Having boards that allow for over-discharging creates explosion and overheating risk in lithium batteries, doesn’t it?