From 7c9d1b0abf3163f9545acff20fc49c61ec017e82 Mon Sep 17 00:00:00 2001 From: Gabriele Russo Date: Fri, 16 Feb 2024 13:09:57 +0100 Subject: [PATCH] Battery level with proportional filter and lookup table (#3216) * 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 Co-authored-by: code8buster <20384924+code8buster@users.noreply.github.com> --- src/Power.cpp | 112 +++++++++++---------- src/power.h | 47 ++++++--- variants/chatter2/variant.h | 15 ++- variants/heltec_v2.1/variant.h | 3 +- variants/heltec_v3/variant.h | 2 + variants/heltec_wireless_tracker/variant.h | 1 + variants/heltec_wsl_v3/variant.h | 2 + variants/station-g1/variant.h | 6 +- 8 files changed, 110 insertions(+), 78 deletions(-) diff --git a/src/Power.cpp b/src/Power.cpp index 23b7900048..8e44ddb983 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -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 { @@ -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); } /** @@ -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 @@ -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; } @@ -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 @@ -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 @@ -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) @@ -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) @@ -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 @@ -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); } } @@ -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) { diff --git a/src/power.h b/src/power.h index 54d98e7150..4dd35e7103 100644 --- a/src/power.h +++ b/src/power.h @@ -5,18 +5,39 @@ #include #include #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 @@ -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; @@ -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; diff --git a/variants/chatter2/variant.h b/variants/chatter2/variant.h index b7ffcf7329..90faa1d7ba 100644 --- a/variants/chatter2/variant.h +++ b/variants/chatter2/variant.h @@ -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 diff --git a/variants/heltec_v2.1/variant.h b/variants/heltec_v2.1/variant.h index ed123efec8..8ebccc54fb 100644 --- a/variants/heltec_v2.1/variant.h +++ b/variants/heltec_v2.1/variant.h @@ -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 diff --git a/variants/heltec_v3/variant.h b/variants/heltec_v3/variant.h index 2532ea682a..70b122f584 100644 --- a/variants/heltec_v3/variant.h +++ b/variants/heltec_v3/variant.h @@ -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 diff --git a/variants/heltec_wireless_tracker/variant.h b/variants/heltec_wireless_tracker/variant.h index 88b4804a12..ba2a0676a4 100644 --- a/variants/heltec_wireless_tracker/variant.h +++ b/variants/heltec_wireless_tracker/variant.h @@ -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 diff --git a/variants/heltec_wsl_v3/variant.h b/variants/heltec_wsl_v3/variant.h index 0ad1b8487b..d3a009adeb 100644 --- a/variants/heltec_wsl_v3/variant.h +++ b/variants/heltec_wsl_v3/variant.h @@ -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 diff --git a/variants/station-g1/variant.h b/variants/station-g1/variant.h index e58853fb7d..9a3c37b735 100644 --- a/variants/station-g1/variant.h +++ b/variants/station-g1/variant.h @@ -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