Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Battery level with proportional filter and lookup table #3216

Merged
merged 12 commits into from
Feb 16, 2024
110 changes: 58 additions & 52 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,10 @@ 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,22 +224,24 @@ class AnalogBatteryLevel : public HasBatteryLevel
{

uint32_t raw = 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
int32_t adc_buf = 0;
Expand All @@ -235,10 +252,10 @@ class AnalogBatteryLevel : public HasBatteryLevel
SET_PERI_REG_MASK(SENS_SAR_READ_CTRL2_REG, SENS_SAR2_DATA_INV);
adc2_get_raw(adc_channel, ADC_WIDTH_BIT_12, &adc_buf);
raw += adc_buf;
raw_c ++;
}
#endif // BAT_MEASURE_ADC_UNIT
raw = raw / BATTERY_SENSE_SAMPLES;
return raw;
return (raw / (raw_c < 1 ? 1 : raw_c));
}
#endif

Expand Down Expand Up @@ -272,22 +289,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 @@ -377,10 +386,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 @@ -461,11 +466,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 @@ -530,10 +535,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
39 changes: 27 additions & 12 deletions src/power.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,31 @@
#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
#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
#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 +57,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 +68,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
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
Loading