Skip to content

Commit

Permalink
Battery level with proportional filter and lookup table (#3216)
Browse files Browse the repository at this point in the history
* Add battery level with lookup table

now uses a lookup table to better calculate battery level of different cells

* LifePo4 and PB battery table - added voltage filter

removed delay from adc reading, added a software filter to smooth out voltage readings. In those applications battery would last hours to days, no sudden change should be expected so a less frequent voltage reading or a more aggressive filtering could be done.
Note: to speed up convergence i initiliazied the last value to the minimum voltage, there are other and better ways to init the filter.

Added LiFePO4 and PB  open circuit volta battery tables,

* Fixed ADC_CTRL , Checks for valid ADC readings

line 230/386 For heltec v3 and heltec tracker a different approach was used with the ADC_CTRL pin, now is more uniform using the same code for the 3 boards.

line 236 Check if the raw reading we are getting is Valid or not, count only the valid readings. This could lead to a division by 0 (improbable) so that's why at line 258 there is a check for that.

* updated OCV values

updated value to not OCV but to very low current, almost the same anyway

* Added Alkaline/Nimh voltage curve

Added Alkaline/Nimh voltage curve for AA/AAA and similar cells

* updates variants for new capacity measurement

* trunk reformatting

* trunk fmt

* Add LTO chemistry

---------

Co-authored-by: Ben Meadors <[email protected]>
Co-authored-by: code8buster <[email protected]>
  • Loading branch information
3 people authored Feb 16, 2024
1 parent e3c4bc5 commit 7c9d1b0
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 78 deletions.
112 changes: 59 additions & 53 deletions src/Power.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,6 @@ class AnalogBatteryLevel : public HasBatteryLevel
{
/**
* Battery state of charge, from 0 to 100 or -1 for unknown
*
* FIXME - use a lipo lookup table, the current % full is super wrong
*/
virtual int getBatteryPercent() override
{
Expand All @@ -137,13 +135,32 @@ class AnalogBatteryLevel : public HasBatteryLevel
if (v < noBatVolt)
return -1; // If voltage is super low assume no battery installed

#ifdef ARCH_ESP32
#ifdef NO_BATTERY_LEVEL_ON_CHARGE
// This does not work on a RAK4631 with battery connected
if (v > chargingVolt)
return 0; // While charging we can't report % full on the battery
#endif

return clamp((int)(100 * (v - emptyVolt) / (fullVolt - emptyVolt)), 0, 100);
/**
* @brief Battery voltage lookup table interpolation to obtain a more
* precise percentage rather than the old proportional one.
* @author Gabriele Russo
* @date 06/02/2024
*/
float battery_SOC = 0.0;
uint16_t voltage = v / NUM_CELLS; // single cell voltage (average)
for (int i = 0; i < NUM_OCV_POINTS; i++) {
if (OCV[i] <= voltage) {
if (i == 0) {
battery_SOC = 100.0; // 100% full
} else {
// interpolate between OCV[i] and OCV[i-1]
battery_SOC = (float)100.0 / (NUM_OCV_POINTS - 1.0) *
(NUM_OCV_POINTS - 1.0 - i + ((float)voltage - OCV[i]) / (OCV[i - 1] - OCV[i]));
}
break;
}
}
return clamp((int)(battery_SOC), 0, 100);
}

/**
Expand All @@ -165,7 +182,7 @@ class AnalogBatteryLevel : public HasBatteryLevel

#ifndef BATTERY_SENSE_SAMPLES
#define BATTERY_SENSE_SAMPLES \
30 // Set the number of samples, it has an effect of increasing sensitivity in complex electromagnetic environment.
15 // Set the number of samples, it has an effect of increasing sensitivity in complex electromagnetic environment.
#endif

#ifdef BATTERY_PIN
Expand All @@ -191,12 +208,11 @@ class AnalogBatteryLevel : public HasBatteryLevel
raw = raw / BATTERY_SENSE_SAMPLES;
scaled = operativeAdcMultiplier * ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * raw;
#endif
// LOG_DEBUG("battery gpio %d raw val=%u scaled=%u\n", BATTERY_PIN, raw, (uint32_t)(scaled));
last_read_value = scaled;
return scaled;
} else {
return last_read_value;
last_read_value += (scaled - last_read_value) * 0.5; // Virtual LPF
// LOG_DEBUG("battery gpio %d raw val=%u scaled=%u filtered=%u\n", BATTERY_PIN, raw, (uint32_t)(scaled), (uint32_t)
// (last_read_value));
}
return last_read_value;
#endif // BATTERY_PIN
return 0;
}
Expand All @@ -209,23 +225,24 @@ class AnalogBatteryLevel : public HasBatteryLevel
{

uint32_t raw = 0;
uint8_t raw_c = 0;
uint8_t raw_c = 0; // raw reading counter

#ifndef BAT_MEASURE_ADC_UNIT // ADC1
#ifdef ADC_CTRL
if (heltec_version == 5) {
pinMode(ADC_CTRL, OUTPUT);
digitalWrite(ADC_CTRL, HIGH);
delay(10);
}
#ifdef ADC_CTRL // enable adc voltage divider when we need to read
pinMode(ADC_CTRL, OUTPUT);
digitalWrite(ADC_CTRL, ADC_CTRL_ENABLED);
delay(10);
#endif
for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) {
raw += adc1_get_raw(adc_channel);
}
#ifdef ADC_CTRL
if (heltec_version == 5) {
digitalWrite(ADC_CTRL, LOW);
int val_ = adc1_get_raw(adc_channel);
if (val_ >= 0) { // save only valid readings
raw += val_;
raw_c++;
}
// delayMicroseconds(100);
}
#ifdef ADC_CTRL // disable adc voltage divider when we need to read
digitalWrite(ADC_CTRL, !ADC_CTRL_ENABLED);
#endif
#else // ADC2
#ifdef ADC_CTRL
Expand Down Expand Up @@ -257,7 +274,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
}
}

#else // Other ESP32
#else // Other ESP32
int32_t adc_buf = 0;
for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) {
// ADC2 wifi bug workaround, see
Expand All @@ -268,7 +285,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
raw += adc_buf;
raw_c++;
}
#endif
#endif // BAT_MEASURE_ADC_UNIT

#ifdef ADC_CTRL
#if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0)
Expand Down Expand Up @@ -311,22 +328,14 @@ class AnalogBatteryLevel : public HasBatteryLevel
/// If we see a battery voltage higher than physics allows - assume charger is pumping
/// in power

#ifndef BAT_FULLVOLT
#define BAT_FULLVOLT 4200
#endif
#ifndef BAT_EMPTYVOLT
#define BAT_EMPTYVOLT 3270
#endif
#ifndef BAT_CHARGINGVOLT
#define BAT_CHARGINGVOLT 4210
#endif
#ifndef BAT_NOBATVOLT
#define BAT_NOBATVOLT 2230
#endif

/// For heltecs with no battery connected, the measured voltage is 2204, so raising to 2230 from 2100
const float fullVolt = BAT_FULLVOLT, emptyVolt = BAT_EMPTYVOLT, chargingVolt = BAT_CHARGINGVOLT, noBatVolt = BAT_NOBATVOLT;
float last_read_value = 0.0;
/// For heltecs with no battery connected, the measured voltage is 2204, so
// need to be higher than that, in this case is 2500mV (3000-500)
const uint16_t OCV[NUM_OCV_POINTS] = {OCV_ARRAY};
const float chargingVolt = (OCV[0] + 10) * NUM_CELLS;
const float noBatVolt = (OCV[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS;
// Start value from minimum voltage for the filter to not start from 0
// that could trigger some events.
float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS);
uint32_t last_read_time_ms = 0;

#if defined(HAS_TELEMETRY) && !defined(ARCH_PORTDUINO)
Expand Down Expand Up @@ -426,10 +435,6 @@ bool Power::analogInit()
else {
LOG_INFO("ADCmod: ADC characterization based on default reference voltage\n");
}
#if defined(HELTEC_V3) || defined(HELTEC_WSL_V3)
pinMode(37, OUTPUT); // needed for P channel mosfet to work
digitalWrite(37, LOW);
#endif
#endif // ARCH_ESP32

#ifdef ARCH_NRF52
Expand Down Expand Up @@ -510,11 +515,11 @@ void Power::readPowerStatus()
batteryChargePercent = batteryLevel->getBatteryPercent();
} else {
// If the AXP192 returns a percentage less than 0, the feature is either not supported or there is an error
// In that case, we compute an estimate of the charge percent based on maximum and minimum voltages defined in
// power.h
batteryChargePercent =
clamp((int)(((batteryVoltageMv - BAT_MILLIVOLTS_EMPTY) * 1e2) / (BAT_MILLIVOLTS_FULL - BAT_MILLIVOLTS_EMPTY)),
0, 100);
// In that case, we compute an estimate of the charge percent based on open circuite voltage table defined
// in power.h
batteryChargePercent = clamp((int)(((batteryVoltageMv - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS)) * 1e2) /
((OCV[0] * NUM_CELLS) - (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS))),
0, 100);
}
}

Expand Down Expand Up @@ -579,10 +584,11 @@ void Power::readPowerStatus()

#endif

// If we have a battery at all and it is less than 10% full, force deep sleep if we have more than 10 low readings in
// a row
// If we have a battery at all and it is less than 0%, force deep sleep if we have more than 10 low readings in
// a row. NOTE: min LiIon/LiPo voltage is 2.0 to 2.5V, current OCV min is set to 3100 that is large enough.
//
if (powerStatus2.getHasBattery() && !powerStatus2.getHasUSB()) {
if (batteryLevel->getBattVoltage() < MIN_BAT_MILLIVOLTS) {
if (batteryLevel->getBattVoltage() < OCV[NUM_OCV_POINTS - 1]) {
low_voltage_counter++;
LOG_DEBUG("Low voltage counter: %d/10\n", low_voltage_counter);
if (low_voltage_counter > 10) {
Expand Down
47 changes: 35 additions & 12 deletions src/power.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,39 @@
#include <esp_adc_cal.h>
#include <soc/adc_channel.h>
#endif
/**
* Per @spattinson
* MIN_BAT_MILLIVOLTS seems high. Typical 18650 are different chemistry to LiPo, even for LiPos that chart seems a bit off, other
* charts put 3690mV at about 30% for a lipo, for 18650 i think 10% remaining iis in the region of 3.2-3.3V. Reference 1st graph
* in [this test report](https://lygte-info.dk/review/batteries2012/Samsung%20INR18650-30Q%203000mAh%20%28Pink%29%20UK.html)
* looking at the red line - discharge at 0.2A - he gets a capacity of 2900mah, 90% of 2900 = 2610, that point in the graph looks
* to be a shade above 3.2V
*/
#define MIN_BAT_MILLIVOLTS 3250 // millivolts. 10% per https://blog.ampow.com/lipo-voltage-chart/

#define BAT_MILLIVOLTS_FULL 4100
#define BAT_MILLIVOLTS_EMPTY 3500

#ifndef NUM_OCV_POINTS
#define NUM_OCV_POINTS 11
#endif

// 3400,3350,3320,3290,3270,3260,3250,3230,3200,3120,3000 //3.4 to 3.0 LiFePO4
// 2120,2090,2070,2050,2030,2010,1990,1980,1970,1960,1950 //2.12 to 1.95 Lead Acid
// 4200,4050,3990,3890,3790,3700,3650,3550,3450,3300,3200 //4.2 to 3.2 LiIon/LiPo
// 4200,4050,3990,3890,3790,3700,3650,3550,3400,3300,3000 //4.2 to 3.0 LiIon/LiPo
// 4150,4050,3990,3890,3790,3690,3620,3520,3420,3300,3100 //4.15 to 3.1 LiIon/LiPo
// 2770,2650,2540,2420,2300,2180,2060,1940,1800,1680,1550 //2.8 to 1.5 Lithium Titanate

#ifndef OCV_ARRAY
#ifdef CELL_TYPE_LIFEPO4
#define OCV_ARRAY 3400, 3350, 3320, 3290, 3270, 3260, 3250, 3230, 3200, 3120, 3000
#elif defined(CELL_TYPE_LEADACID)
#define OCV_ARRAY 2120, 2090, 2070, 2050, 2030, 2010, 1990, 1980, 1970, 1960, 1950
#elif defined(CELL_TYPE_ALKALINE)
#define OCV_ARRAY 1580, 1400, 1350, 1300, 1280, 1250, 1230, 1190, 1150, 1100, 1000
#elif defined(CELL_TYPE_NIMH)
#define OCV_ARRAY 1400, 1300, 1280, 1270, 1260, 1250, 1240, 1230, 1210, 1150, 1000
#elif defined(CELL_TYPE_LTO)
#define OCV_ARRAY 2770, 2650, 2540, 2420, 2300, 2180, 2060, 1940, 1800, 1680, 1550
#else // LiIon
#define OCV_ARRAY 4190, 4050, 3990, 3890, 3800, 3720, 3630, 3530, 3420, 3300, 3100
#endif
#endif

/*Note: 12V lead acid is 6 cells, most board accept only 1 cell LiIon/LiPo*/
#ifndef NUM_CELLS
#define NUM_CELLS 1
#endif

#ifdef BAT_MEASURE_ADC_UNIT
extern RTC_NOINIT_ATTR uint64_t RTC_reg_b;
#include "soc/sens_reg.h" // needed for adc pin reset
Expand Down Expand Up @@ -44,6 +65,7 @@ class Power : private concurrency::OSThread
virtual bool setup();
virtual int32_t runOnce() override;
void setStatusHandler(meshtastic::PowerStatus *handler) { statusHandler = handler; }
const uint16_t OCV[11] = {OCV_ARRAY};

protected:
meshtastic::PowerStatus *statusHandler;
Expand All @@ -54,6 +76,7 @@ class Power : private concurrency::OSThread
bool analogInit();

private:
// open circuit voltage lookup table
uint8_t low_voltage_counter;
#ifdef DEBUG_HEAP
uint32_t lastheap;
Expand Down
15 changes: 7 additions & 8 deletions variants/chatter2/variant.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,13 @@
#define BATTERY_PIN 34 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage
#define ADC_CHANNEL ADC1_GPIO34_CHANNEL
#define ADC_ATTENUATION \
ADC_ATTEN_DB_2_5 // 2_5-> 100mv-1250mv, 11-> 150mv-3100mv for ESP32
// ESP32-S2/C3/S3 are different
// lower dB for lower voltage rnage
#define ADC_MULTIPLIER \
5.0 // VBATT---10k--pin34---2.5K---GND
// Chatter2 uses 3 AAA cells
#define BAT_FULLVOLT 4800 // with the 5.0 divider, input to BATTERY_PIN is 900mv
#define BAT_EMPTYVOLT 3300
ADC_ATTEN_DB_2_5 // 2_5-> 100mv-1250mv, 11-> 150mv-3100mv for ESP32
// ESP32-S2/C3/S3 are different
// lower dB for lower voltage rnage
#define ADC_MULTIPLIER 5.0 // VBATT---10k--pin34---2.5K---GND
// Chatter2 uses 3 AAA cells
#define CELL_TYPE_ALKALINE
#define NUM_CELLS 3
#undef EXT_PWR_DETECT

// GPS
Expand Down
3 changes: 2 additions & 1 deletion variants/heltec_v2.1/variant.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
#define LORA_DIO1 35 // https://www.thethingsnetwork.org/forum/t/big-esp32-sx127x-topic-part-3/18436
#define LORA_DIO2 34 // Not really used

#define ADC_MULTIPLIER 3.8
#define ADC_MULTIPLIER 3.2 // 220k + 100k (320k/100k=3.2)
// #define ADC_WIDTH ADC_WIDTH_BIT_10

#define BATTERY_PIN 37 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage
#define ADC_CHANNEL ADC1_GPIO37_CHANNEL
Expand Down
2 changes: 2 additions & 0 deletions variants/heltec_v3/variant.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
#define VEXT_ENABLE Vext // active low, powers the oled display and the lora antenna boost
#define BUTTON_PIN 0

#define ADC_CTRL 37
#define ADC_CTRL_ENABLED LOW
#define BATTERY_PIN 1 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage
#define ADC_CHANNEL ADC1_GPIO1_CHANNEL
#define ADC_ATTENUATION ADC_ATTEN_DB_2_5 // lower dB for high resistance voltage divider
Expand Down
1 change: 1 addition & 0 deletions variants/heltec_wireless_tracker/variant.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#define ADC_ATTENUATION ADC_ATTEN_DB_2_5 // lower dB for high resistance voltage divider
#define ADC_MULTIPLIER 4.9
#define ADC_CTRL 2 // active HIGH, powers the voltage divider. Only on 1.1
#define ADC_CTRL_ENABLED HIGH

#undef GPS_RX_PIN
#undef GPS_TX_PIN
Expand Down
2 changes: 2 additions & 0 deletions variants/heltec_wsl_v3/variant.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#define VEXT_ENABLE Vext // active low, powers the oled display and the lora antenna boost
#define BUTTON_PIN 0

#define ADC_CTRL 37
#define ADC_CTRL_ENABLED LOW
#define BATTERY_PIN 1 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage
#define ADC_CHANNEL ADC1_GPIO1_CHANNEL
#define ADC_ATTENUATION ADC_ATTEN_DB_2_5 // lower dB for high resistance voltage divider
Expand Down
6 changes: 2 additions & 4 deletions variants/station-g1/variant.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,8 @@
#define ADC_CHANNEL ADC1_GPIO35_CHANNEL
#define BATTERY_SENSE_SAMPLES 30 // Set the number of samples, It has an effect of increasing sensitivity.
#define ADC_MULTIPLIER 6.45
#define BAT_FULLVOLT 12600
#define BAT_EMPTYVOLT 8200
#define BAT_CHARGINGVOLT 12600
#define BAT_NOBATVOLT 6690
#define CELL_TYPE_LION // same curve for liion/lipo
#define NUM_CELLS 3

// different screen
#define USE_SH1106
Expand Down

0 comments on commit 7c9d1b0

Please sign in to comment.