mirror of
https://github.com/meshtastic/firmware.git
synced 2026-09-21 05:45:27 -04:00
* fix(Power): survive a BQ27220 fuel gauge that fails to init Keep the BQ25896 as the battery source when only the gauge fails, so Power stays enabled instead of falling through to an ADC that these variants do not have. Null-guard the gauge in getBattVoltage() and isCharging(). Retry the gauge from the power thread (3 attempts, 60s apart, address probe first) since it is soldered on, and reset the I2C master after a failed init so the bus scan does not run against a stale transaction. Fixes #11372 * fix(Power): address review feedback on the BQ27220 retry Derive "no attempt yet" from gaugeAttemptsLeft instead of a millis() zero sentinel, drop the zero-padding on the logged I2C address, and condense the new comment blocks.
2130 lines
68 KiB
C++
2130 lines
68 KiB
C++
/**
|
||
* @file Power.cpp
|
||
* @brief This file contains the implementation of the Power class, which is
|
||
* responsible for managing power-related functionality of the device. It
|
||
* includes battery level sensing, power management unit (PMU) control, and
|
||
* power state machine management. The Power class is used by the main device
|
||
* class to manage power-related functionality.
|
||
*
|
||
* The file also includes implementations of various battery level sensors, such
|
||
* as the AnalogBatteryLevel class, which assumes the battery voltage is
|
||
* attached via a voltage-divider to an analog input.
|
||
*
|
||
* This file is part of the Meshtastic project.
|
||
* For more information, see: https://meshtastic.org/
|
||
*/
|
||
#include "Power.h"
|
||
#include "BluetoothCommon.h"
|
||
#include "MessageStore.h"
|
||
#include "NodeDB.h"
|
||
#include "PowerFSM.h"
|
||
#include "Throttle.h"
|
||
#include "buzz/buzz.h"
|
||
#include "configuration.h"
|
||
#include "main.h"
|
||
#include "meshUtils.h"
|
||
#include "power/PowerHAL.h"
|
||
#include "power/SGM41562.h"
|
||
#include "sleep.h"
|
||
#ifdef ARCH_ESP32
|
||
// #include <driver/adc.h>
|
||
#include <esp_adc/adc_cali.h>
|
||
#include <esp_adc/adc_cali_scheme.h>
|
||
#include <esp_adc/adc_oneshot.h>
|
||
#include <esp_err.h>
|
||
#endif
|
||
|
||
#if defined(ARCH_PORTDUINO)
|
||
#include "api/WiFiServerAPI.h"
|
||
#include "input/LinuxInputImpl.h"
|
||
#include "input/LinuxJoystick.h"
|
||
#endif
|
||
|
||
// Working USB detection for powered/charging states on the RAK platform
|
||
#ifdef NRF_APM
|
||
#include "nrfx_power.h"
|
||
#endif
|
||
|
||
#if defined(ARCH_NRF52)
|
||
#include "Nrf52SaadcLock.h"
|
||
#include "concurrency/LockGuard.h"
|
||
#endif
|
||
|
||
#if defined(ARCH_STM32) && defined(BATTERY_PIN)
|
||
#include "stm32yyxx_ll_adc.h"
|
||
|
||
/* Analog read resolution */
|
||
#if defined(LL_ADC_RESOLUTION_12B)
|
||
#define LL_ADC_RESOLUTION LL_ADC_RESOLUTION_12B
|
||
#define BATTERY_SENSE_RESOLUTION_BITS 12
|
||
#elif defined(LL_ADC_DS_DATA_WIDTH_12_BIT)
|
||
#define LL_ADC_RESOLUTION LL_ADC_DS_DATA_WIDTH_12_BIT
|
||
#define BATTERY_SENSE_RESOLUTION_BITS 12
|
||
#else
|
||
// The ST HAL headers that define these are outside cppcheck's include path (check_skip_packages), so static
|
||
// analysis always lands here even though real builds resolve one of the branches above.
|
||
// cppcheck-suppress preprocessorErrorDirective
|
||
#error "ADC resolution could not be defined!"
|
||
#endif
|
||
#define ADC_RANGE (1 << BATTERY_SENSE_RESOLUTION_BITS)
|
||
#endif
|
||
|
||
#if defined(DEBUG_HEAP_MQTT) && !MESHTASTIC_EXCLUDE_MQTT
|
||
#include "mqtt/MQTT.h"
|
||
#include "target_specific.h"
|
||
#if HAS_WIFI
|
||
#include <WiFi.h>
|
||
#endif
|
||
|
||
#if HAS_ETHERNET && defined(ARCH_ESP32)
|
||
#include <ETH.h>
|
||
#endif // HAS_ETHERNET
|
||
|
||
#endif
|
||
|
||
#ifndef DELAY_FOREVER
|
||
#define DELAY_FOREVER portMAX_DELAY
|
||
#endif
|
||
|
||
// How often the free-heap line is written to the debug log. The Power thread polls every
|
||
// 20s once it is initialized, so that is the effective granularity. Set to 0 to disable.
|
||
#ifndef HEAP_LOG_INTERVAL_MS
|
||
#define HEAP_LOG_INTERVAL_MS (5 * 60 * 1000)
|
||
#endif
|
||
|
||
#if defined(BATTERY_PIN) && defined(ARCH_ESP32)
|
||
|
||
#ifndef BAT_MEASURE_ADC_UNIT // ADC1 is default
|
||
static const adc_channel_t adc_channel = ADC_CHANNEL;
|
||
static const adc_unit_t unit = ADC_UNIT_1;
|
||
#else // ADC2
|
||
static const adc_channel_t adc_channel = ADC_CHANNEL;
|
||
static const adc_unit_t unit = ADC_UNIT_2;
|
||
#endif // BAT_MEASURE_ADC_UNIT
|
||
|
||
static adc_oneshot_unit_handle_t adc_handle = nullptr;
|
||
static adc_cali_handle_t adc_cali_handle = nullptr;
|
||
static bool adc_calibrated = false;
|
||
#ifndef ADC_ATTENUATION
|
||
static const adc_atten_t atten = ADC_ATTEN_DB_12;
|
||
#else
|
||
static const adc_atten_t atten = ADC_ATTENUATION;
|
||
#endif
|
||
#ifdef ADC_BITWIDTH
|
||
static const adc_bitwidth_t adc_width = ADC_BITWIDTH;
|
||
#else
|
||
static const adc_bitwidth_t adc_width = ADC_BITWIDTH_DEFAULT;
|
||
#endif
|
||
|
||
static int adcBitWidthToBits(adc_bitwidth_t width)
|
||
{
|
||
switch (width) {
|
||
case ADC_BITWIDTH_9:
|
||
return 9;
|
||
case ADC_BITWIDTH_10:
|
||
return 10;
|
||
case ADC_BITWIDTH_11:
|
||
return 11;
|
||
case ADC_BITWIDTH_12:
|
||
return 12;
|
||
#ifdef ADC_BITWIDTH_13
|
||
case ADC_BITWIDTH_13:
|
||
return 13;
|
||
#endif
|
||
default:
|
||
return 12;
|
||
}
|
||
}
|
||
|
||
static bool initAdcCalibration()
|
||
{
|
||
#if ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED
|
||
adc_cali_curve_fitting_config_t cali_config = {
|
||
.unit_id = unit,
|
||
.atten = atten,
|
||
.bitwidth = adc_width,
|
||
};
|
||
esp_err_t ret = adc_cali_create_scheme_curve_fitting(&cali_config, &adc_cali_handle);
|
||
if (ret == ESP_OK) {
|
||
LOG_INFO("ADC calibration: curve fitting enabled");
|
||
return true;
|
||
}
|
||
if (ret != ESP_ERR_NOT_SUPPORTED) {
|
||
LOG_WARN("ADC calibration: curve fitting failed: %s", esp_err_to_name(ret));
|
||
}
|
||
#endif
|
||
|
||
#if ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED
|
||
adc_cali_line_fitting_config_t cali_config = {
|
||
.unit_id = unit,
|
||
.atten = atten,
|
||
.bitwidth = adc_width,
|
||
.default_vref = DEFAULT_VREF,
|
||
};
|
||
esp_err_t ret = adc_cali_create_scheme_line_fitting(&cali_config, &adc_cali_handle);
|
||
if (ret == ESP_OK) {
|
||
LOG_INFO("ADC calibration: line fitting enabled");
|
||
return true;
|
||
}
|
||
if (ret != ESP_ERR_NOT_SUPPORTED) {
|
||
LOG_WARN("ADC calibration: line fitting failed: %s", esp_err_to_name(ret));
|
||
}
|
||
#endif
|
||
|
||
LOG_INFO("ADC calibration unsupported; use approx scaling");
|
||
return false;
|
||
}
|
||
|
||
#endif // BATTERY_PIN && ARCH_ESP32
|
||
|
||
#ifdef EXT_PWR_DETECT
|
||
#ifndef EXT_PWR_DETECT_MODE
|
||
#define EXT_PWR_DETECT_MODE INPUT
|
||
// If using internal pull resistors, we can infer EXT_PWR_DETECT_VALUE
|
||
#elif EXT_PWR_DETECT_MODE == INPUT_PULLUP
|
||
#define EXT_PWR_DETECT_VALUE LOW
|
||
#elif EXT_PWR_DETECT_MODE == INPUT_PULLDOWN
|
||
#define EXT_PWR_DETECT_VALUE HIGH
|
||
#endif
|
||
#ifndef EXT_PWR_DETECT_VALUE
|
||
#define EXT_PWR_DETECT_VALUE HIGH
|
||
#endif
|
||
#endif
|
||
|
||
#ifdef EXT_CHRG_DETECT
|
||
#ifndef EXT_CHRG_DETECT_MODE
|
||
#define EXT_CHRG_DETECT_MODE INPUT
|
||
// If using internal pull resistors, we can infer EXT_CHRG_DETECT_VALUE
|
||
#elif EXT_CHRG_DETECT_MODE == INPUT_PULLUP
|
||
#define EXT_CHRG_DETECT_VALUE LOW
|
||
#elif EXT_CHRG_DETECT_MODE == INPUT_PULLDOWN
|
||
#define EXT_CHRG_DETECT_VALUE HIGH
|
||
#endif
|
||
#ifndef EXT_CHRG_DETECT_VALUE
|
||
#define EXT_CHRG_DETECT_VALUE HIGH
|
||
#endif
|
||
#endif
|
||
|
||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||
#if __has_include(<Adafruit_INA219.h>)
|
||
INA219Sensor ina219Sensor;
|
||
#else
|
||
NullSensor ina219Sensor;
|
||
#endif
|
||
|
||
#if __has_include(<INA226.h>)
|
||
INA226Sensor ina226Sensor;
|
||
#else
|
||
NullSensor ina226Sensor;
|
||
#endif
|
||
|
||
#if __has_include(<Adafruit_INA260.h>)
|
||
INA260Sensor ina260Sensor;
|
||
#else
|
||
NullSensor ina260Sensor;
|
||
#endif
|
||
|
||
#if __has_include(<INA3221.h>)
|
||
INA3221Sensor ina3221Sensor;
|
||
#else
|
||
NullSensor ina3221Sensor;
|
||
#endif
|
||
|
||
#endif
|
||
|
||
#if !MESHTASTIC_EXCLUDE_I2C
|
||
#include "modules/Telemetry/Sensor/MAX17048Sensor.h"
|
||
#include <utility>
|
||
extern std::pair<uint8_t, TwoWire *> nodeTelemetrySensorsMap[_meshtastic_TelemetrySensorType_MAX + 1];
|
||
#if HAS_TELEMETRY && (!MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR || !MESHTASTIC_EXCLUDE_POWER_TELEMETRY)
|
||
#if __has_include(<Adafruit_MAX1704X.h>)
|
||
MAX17048Sensor max17048Sensor;
|
||
#else
|
||
NullSensor max17048Sensor;
|
||
#endif
|
||
#endif
|
||
#endif
|
||
|
||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && HAS_RAKPROT
|
||
RAK9154Sensor rak9154Sensor;
|
||
#endif
|
||
|
||
#ifdef HAS_PPM
|
||
// note: XPOWERS_CHIP_XXX must be defined in variant.h
|
||
#include <XPowersLib.h>
|
||
XPowersPPM *PPM = NULL;
|
||
#endif
|
||
|
||
#ifdef HAS_BQ27220
|
||
#include "bq27220.h"
|
||
#endif
|
||
|
||
#ifdef HAS_PMU
|
||
XPowersLibInterface *PMU = NULL;
|
||
#else
|
||
|
||
// Copy of the base class defined in axp20x.h.
|
||
// I'd rather not include axp20x.h as it brings Wire dependency.
|
||
class HasBatteryLevel
|
||
{
|
||
public:
|
||
/**
|
||
* Battery state of charge, from 0 to 100 or -1 for unknown
|
||
*/
|
||
virtual int getBatteryPercent() { return -1; }
|
||
|
||
/**
|
||
* The raw voltage of the battery or NAN if unknown
|
||
*/
|
||
virtual uint16_t getBattVoltage() { return 0; }
|
||
|
||
/**
|
||
* return true if there is a battery installed in this unit
|
||
*/
|
||
virtual bool isBatteryConnect() { return false; }
|
||
|
||
virtual bool isVbusIn() { return false; }
|
||
virtual bool isCharging() { return false; }
|
||
};
|
||
#endif
|
||
|
||
bool pmu_irq = false;
|
||
|
||
Power *power;
|
||
|
||
using namespace meshtastic;
|
||
|
||
// NRF52 has AREF_VOLTAGE defined in architecture.h but
|
||
// make sure it's included. If something is wrong with NRF52
|
||
// definition - compilation will fail on missing definition
|
||
#if !defined(AREF_VOLTAGE) && !defined(ARCH_NRF52)
|
||
#define AREF_VOLTAGE 3.3
|
||
#endif
|
||
|
||
/**
|
||
* If this board has a battery level sensor, set this to a valid implementation
|
||
*/
|
||
static HasBatteryLevel *batteryLevel; // Default to NULL for no battery level sensor
|
||
|
||
#ifdef BATTERY_PIN
|
||
|
||
void battery_adcEnable()
|
||
{
|
||
#ifdef ADC_CTRL // enable adc voltage divider when we need to read
|
||
#ifdef ADC_USE_PULLUP
|
||
pinMode(ADC_CTRL, INPUT_PULLUP);
|
||
#else
|
||
#ifdef HELTEC_V3
|
||
pinMode(ADC_CTRL, INPUT);
|
||
uint8_t adc_ctl_enable_value = !(digitalRead(ADC_CTRL));
|
||
pinMode(ADC_CTRL, OUTPUT);
|
||
digitalWrite(ADC_CTRL, adc_ctl_enable_value);
|
||
#else
|
||
pinMode(ADC_CTRL, OUTPUT);
|
||
digitalWrite(ADC_CTRL, ADC_CTRL_ENABLED);
|
||
#endif
|
||
#endif
|
||
delay(10);
|
||
#endif
|
||
}
|
||
|
||
static void battery_adcDisable()
|
||
{
|
||
#ifdef ADC_CTRL // disable adc voltage divider when we need to read
|
||
#ifdef ADC_USE_PULLUP
|
||
pinMode(ADC_CTRL, INPUT_PULLDOWN);
|
||
#else
|
||
#ifdef HELTEC_V3
|
||
pinMode(ADC_CTRL, ANALOG);
|
||
#else
|
||
digitalWrite(ADC_CTRL, !ADC_CTRL_ENABLED);
|
||
#endif
|
||
#endif
|
||
#endif
|
||
}
|
||
|
||
#endif
|
||
|
||
/**
|
||
* A simple battery level sensor that assumes the battery voltage is attached
|
||
* via a voltage-divider to an analog input
|
||
*/
|
||
class AnalogBatteryLevel : public HasBatteryLevel
|
||
{
|
||
public:
|
||
/**
|
||
* Battery state of charge, from 0 to 100 or -1 for unknown
|
||
*/
|
||
virtual int getBatteryPercent() override
|
||
{
|
||
#if defined(HAS_RAKPROT) && !defined(HAS_PMU)
|
||
if (hasRAK()) {
|
||
return rak9154Sensor.getBusBatteryPercent();
|
||
}
|
||
#endif
|
||
|
||
float v = getBattVoltage();
|
||
|
||
if (v < noBatVolt)
|
||
return -1; // If voltage is super low assume no battery installed
|
||
|
||
#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
|
||
/**
|
||
* @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;
|
||
}
|
||
}
|
||
#if defined(BATTERY_CHARGING_INV)
|
||
// bit of trickery to show 99% up until the charge finishes
|
||
if (!digitalRead(BATTERY_CHARGING_INV) && battery_SOC > 99)
|
||
battery_SOC = 99;
|
||
#endif
|
||
return clamp((int)(battery_SOC), 0, 100);
|
||
}
|
||
|
||
/**
|
||
* The raw voltage of the batteryin millivolts or NAN if unknown
|
||
*/
|
||
virtual uint16_t getBattVoltage() override
|
||
{
|
||
|
||
#if HAS_TELEMETRY && defined(HAS_RAKPROT) && !defined(HAS_PMU) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||
if (hasRAK()) {
|
||
return getRAKVoltage();
|
||
}
|
||
#endif
|
||
|
||
#if HAS_TELEMETRY && !defined(HAS_PMU) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||
if (hasINA()) {
|
||
return getINAVoltage();
|
||
}
|
||
#endif
|
||
|
||
#ifndef ADC_MULTIPLIER
|
||
#define ADC_MULTIPLIER 2.0
|
||
#endif
|
||
|
||
#ifndef BATTERY_SENSE_SAMPLES
|
||
#define BATTERY_SENSE_SAMPLES \
|
||
15 // Set the number of samples, it has an effect of increasing sensitivity in
|
||
// complex electromagnetic environment.
|
||
#endif
|
||
|
||
#ifdef BATTERY_PIN
|
||
// Override variant or default ADC_MULTIPLIER if we have the override pref
|
||
float operativeAdcMultiplier =
|
||
config.power.adc_multiplier_override > 0 ? config.power.adc_multiplier_override : ADC_MULTIPLIER;
|
||
// Do not call analogRead() often.
|
||
const uint32_t min_read_interval = 5000;
|
||
if (!initial_read_done || !Throttle::isWithinTimespanMs(last_read_time_ms, min_read_interval)) {
|
||
last_read_time_ms = millis();
|
||
|
||
uint32_t raw = 0;
|
||
float scaled = 0;
|
||
|
||
battery_adcEnable();
|
||
#ifdef ARCH_STM32
|
||
// STM32 ADC with VREFINT runtime calibration
|
||
Vref = __LL_ADC_CALC_VREFANALOG_VOLTAGE(analogRead(AVREF), LL_ADC_RESOLUTION);
|
||
raw = analogRead(BATTERY_PIN);
|
||
scaled = __LL_ADC_CALC_DATA_TO_VOLTAGE(Vref, raw, LL_ADC_RESOLUTION);
|
||
scaled *= operativeAdcMultiplier;
|
||
#elif defined(ARCH_ESP32) // ADC block for espressif platforms
|
||
raw = espAdcRead();
|
||
int voltage_mv = 0;
|
||
if (adc_calibrated && adc_cali_handle) {
|
||
if (adc_cali_raw_to_voltage(adc_cali_handle, raw, &voltage_mv) != ESP_OK) {
|
||
LOG_WARN("ADC calibration read failed; using raw value");
|
||
voltage_mv = 0;
|
||
}
|
||
}
|
||
if (voltage_mv == 0) {
|
||
// Fallback approximate conversion without calibration
|
||
const int bits = adcBitWidthToBits(adc_width);
|
||
const float max_code = powf(2.0f, bits) - 1.0f;
|
||
voltage_mv = (int)((raw / max_code) * DEFAULT_VREF);
|
||
}
|
||
scaled = voltage_mv * operativeAdcMultiplier;
|
||
#else // block for all other platforms
|
||
#ifdef ARCH_NRF52
|
||
concurrency::LockGuard saadcGuard(concurrency::nrf52SaadcLock);
|
||
#endif
|
||
for (uint32_t i = 0; i < BATTERY_SENSE_SAMPLES; i++) {
|
||
raw += analogRead(BATTERY_PIN);
|
||
}
|
||
raw = raw / BATTERY_SENSE_SAMPLES;
|
||
scaled = operativeAdcMultiplier * ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * raw;
|
||
#endif
|
||
battery_adcDisable();
|
||
|
||
if (!initial_read_done) {
|
||
// Flush the smoothing filter with an ADC reading, if the reading is
|
||
// plausibly correct
|
||
if (scaled > last_read_value)
|
||
last_read_value = scaled;
|
||
initial_read_done = true;
|
||
} else {
|
||
// Already initialized - filter this reading
|
||
last_read_value += (scaled - last_read_value) * 0.5; // Virtual LPF
|
||
}
|
||
|
||
// LOG_DEBUG("battery gpio %d raw val=%u scaled=%u filtered=%u",
|
||
// BATTERY_PIN, raw, (uint32_t)(scaled), (uint32_t) (last_read_value));
|
||
}
|
||
return last_read_value;
|
||
#endif // BATTERY_PIN
|
||
return 0;
|
||
}
|
||
|
||
#if defined(ARCH_ESP32) && !defined(HAS_PMU) && defined(BATTERY_PIN)
|
||
/**
|
||
* ESP32 specific function for getting calibrated ADC reads
|
||
*/
|
||
uint32_t espAdcRead()
|
||
{
|
||
|
||
uint32_t raw = 0;
|
||
uint8_t raw_c = 0; // raw reading counter
|
||
|
||
if (!adc_handle) {
|
||
LOG_ERROR("ADC oneshot handle not initialized");
|
||
return 0;
|
||
}
|
||
|
||
for (int i = 0; i < BATTERY_SENSE_SAMPLES; i++) {
|
||
int val = 0;
|
||
esp_err_t err = adc_oneshot_read(adc_handle, adc_channel, &val);
|
||
if (err == ESP_OK) {
|
||
raw += val;
|
||
raw_c++;
|
||
} else {
|
||
LOG_DEBUG("ADC read failed: %s", esp_err_to_name(err));
|
||
}
|
||
}
|
||
|
||
return (raw / (raw_c < 1 ? 1 : raw_c));
|
||
}
|
||
#endif
|
||
|
||
/**
|
||
* return true if there is a battery installed in this unit
|
||
*/
|
||
// if we have a integrated device with a battery, we can assume that the
|
||
// battery is always connected
|
||
#ifdef BATTERY_IMMUTABLE
|
||
virtual bool isBatteryConnect() override { return true; }
|
||
#elif defined(ADC_V)
|
||
virtual bool isBatteryConnect() override
|
||
{
|
||
int lastReading = digitalRead(ADC_V);
|
||
// 判断值是否变化
|
||
for (int i = 2; i < 500; i++) {
|
||
int reading = digitalRead(ADC_V);
|
||
if (reading != lastReading) {
|
||
return false; // 有变化,USB供电, 没接电池
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
#else
|
||
virtual bool isBatteryConnect() override { return getBatteryPercent() != -1; }
|
||
#endif
|
||
|
||
// Detect if an external power source is connected if we don’t have a PMIC;
|
||
// Firstly prefer EXT_PWR_DETECT GPIO if available,
|
||
// secondly try an nRF52-specific routine on some variants,
|
||
// lastly provide a fallback to indicate external power when fully charged.
|
||
virtual bool isVbusIn() override
|
||
{
|
||
#ifdef HAS_SGM41562
|
||
if (sgm41562 && sgm41562->refresh())
|
||
return sgm41562->isInputPowerGood();
|
||
#endif
|
||
#ifdef EXT_PWR_DETECT
|
||
if (digitalRead(EXT_PWR_DETECT) == EXT_PWR_DETECT_VALUE)
|
||
return true;
|
||
#ifdef EXT_CHRG_DETECT
|
||
// EXT_PWR_DETECT alone may not catch active charging (e.g. a charge-complete
|
||
// pin that only asserts once the battery is full) - CHRG being active implies
|
||
// power is present regardless.
|
||
return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE;
|
||
#else
|
||
return false;
|
||
#endif
|
||
|
||
// technically speaking this should work for all(?) NRF52 boards
|
||
// but needs testing across multiple devices. NRF52 USB would not even work if
|
||
// VBUS was not properly connected and detected by the CPU
|
||
#elif defined(MUZI_BASE) || defined(PROMICRO_DIY_TCXO) || defined(ELECROW_ThinkNode_M8)
|
||
return powerHAL_isVBUSConnected();
|
||
#endif
|
||
return getBattVoltage() > chargingVolt;
|
||
}
|
||
|
||
/// Assume charging if we have a battery and external power is connected.
|
||
/// we can't be smart enough to say 'full'?
|
||
virtual bool isCharging() override
|
||
{
|
||
#ifdef HAS_SGM41562
|
||
if (sgm41562 && sgm41562->refresh())
|
||
return sgm41562->isCharging();
|
||
#endif
|
||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) && !defined(HAS_PMU)
|
||
if (hasRAK()) {
|
||
return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse;
|
||
}
|
||
#endif
|
||
#if defined(ELECROW_ThinkNode_M6)
|
||
return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE || isVbusIn();
|
||
#elif defined(EXT_CHRG_DETECT)
|
||
return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE;
|
||
#elif defined(BATTERY_CHARGING_INV)
|
||
return !digitalRead(BATTERY_CHARGING_INV);
|
||
#else
|
||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(DISABLE_INA_CHARGING_DETECTION)
|
||
if (hasINA()) {
|
||
// get current flow from INA sensor - negative value means power flowing
|
||
// into the battery default assuming BATTERY+ <--> INA_VIN+ <--> SHUNT
|
||
// RESISTOR <--> INA_VIN- <--> LOAD
|
||
LOG_DEBUG("Using INA on I2C addr 0x%x for charging detection", config.power.device_battery_ina_address);
|
||
#if defined(INA_CHARGING_DETECTION_INVERT)
|
||
return getINACurrent() > 0;
|
||
#else
|
||
return getINACurrent() < 0;
|
||
#endif
|
||
}
|
||
return isBatteryConnect() && isVbusIn();
|
||
#endif
|
||
#endif
|
||
// by default, we check the battery voltage only
|
||
return isVbusIn();
|
||
}
|
||
|
||
private:
|
||
/// If we see a battery voltage higher than physics allows - assume charger is
|
||
/// pumping in power
|
||
|
||
/// 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.
|
||
// This value is over-written by the first ADC reading, it the voltage seems
|
||
// reasonable.
|
||
bool initial_read_done = false;
|
||
float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS);
|
||
uint32_t last_read_time_ms = 0;
|
||
#ifdef ARCH_STM32
|
||
// 3300mV placeholder for STM32 errata where VREFINT factory calibration may be missing
|
||
// (e.g. STM32U0, see DS14756 Rev 3 §2.4.1 "VREFINT offset")
|
||
uint32_t Vref = 3300;
|
||
#endif
|
||
|
||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT)
|
||
|
||
uint16_t getRAKVoltage() { return rak9154Sensor.getBusVoltageMv(); }
|
||
|
||
bool hasRAK()
|
||
{
|
||
if (!rak9154Sensor.isInitialized())
|
||
return rak9154Sensor.runOnce() > 0;
|
||
return rak9154Sensor.isRunning();
|
||
}
|
||
#endif
|
||
|
||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
||
uint16_t getINAVoltage()
|
||
{
|
||
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) {
|
||
return ina219Sensor.getBusVoltageMv();
|
||
} else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA226].first ==
|
||
config.power.device_battery_ina_address) {
|
||
return ina226Sensor.getBusVoltageMv();
|
||
} else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA260].first ==
|
||
config.power.device_battery_ina_address) {
|
||
return ina260Sensor.getBusVoltageMv();
|
||
} else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA3221].first ==
|
||
config.power.device_battery_ina_address) {
|
||
return ina3221Sensor.getBusVoltageMv();
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
int16_t getINACurrent()
|
||
{
|
||
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) {
|
||
return ina219Sensor.getCurrentMa();
|
||
} else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA226].first ==
|
||
config.power.device_battery_ina_address) {
|
||
return ina226Sensor.getCurrentMa();
|
||
} else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA3221].first ==
|
||
config.power.device_battery_ina_address) {
|
||
return ina3221Sensor.getCurrentMa();
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
bool hasINA()
|
||
{
|
||
if (!config.power.device_battery_ina_address) {
|
||
return false;
|
||
}
|
||
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA219].first == config.power.device_battery_ina_address) {
|
||
if (!ina219Sensor.isInitialized())
|
||
return ina219Sensor.runOnce() > 0;
|
||
return ina219Sensor.isRunning();
|
||
} else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA226].first ==
|
||
config.power.device_battery_ina_address) {
|
||
if (!ina226Sensor.isInitialized())
|
||
return ina226Sensor.runOnce() > 0;
|
||
return ina226Sensor.isRunning();
|
||
} else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA260].first ==
|
||
config.power.device_battery_ina_address) {
|
||
if (!ina260Sensor.isInitialized())
|
||
return ina260Sensor.runOnce() > 0;
|
||
return ina260Sensor.isRunning();
|
||
} else if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_INA3221].first ==
|
||
config.power.device_battery_ina_address) {
|
||
if (!ina3221Sensor.isInitialized())
|
||
return ina3221Sensor.runOnce() > 0;
|
||
return ina3221Sensor.isRunning();
|
||
}
|
||
return false;
|
||
}
|
||
#endif
|
||
};
|
||
|
||
static AnalogBatteryLevel analogLevel;
|
||
|
||
Power::Power() : OSThread("Power")
|
||
{
|
||
statusHandler = {};
|
||
low_voltage_counter = 0;
|
||
#ifdef DEBUG_HEAP
|
||
lastheap = memGet.getFreeHeap();
|
||
#endif
|
||
}
|
||
|
||
bool Power::analogInit()
|
||
{
|
||
#ifdef EXT_PWR_DETECT
|
||
pinMode(EXT_PWR_DETECT, EXT_PWR_DETECT_MODE);
|
||
#endif
|
||
#ifdef EXT_CHRG_DETECT
|
||
pinMode(EXT_CHRG_DETECT, EXT_CHRG_DETECT_MODE);
|
||
#endif
|
||
|
||
#ifdef BATTERY_PIN
|
||
LOG_DEBUG("Use analog input %d for battery level", BATTERY_PIN);
|
||
|
||
// disable any internal pullups
|
||
pinMode(BATTERY_PIN, INPUT);
|
||
|
||
#ifndef BATTERY_SENSE_RESOLUTION_BITS
|
||
#define BATTERY_SENSE_RESOLUTION_BITS 10
|
||
#endif
|
||
|
||
#ifdef ARCH_STM32
|
||
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
|
||
#elif defined(ARCH_ESP32) // ESP32 needs special analog stuff
|
||
adc_oneshot_unit_init_cfg_t init_config = {
|
||
.unit_id = unit,
|
||
};
|
||
|
||
if (!adc_handle) {
|
||
esp_err_t err = adc_oneshot_new_unit(&init_config, &adc_handle);
|
||
if (err != ESP_OK) {
|
||
LOG_ERROR("ADC oneshot init failed: %s", esp_err_to_name(err));
|
||
return false;
|
||
}
|
||
}
|
||
|
||
adc_oneshot_chan_cfg_t chan_cfg = {
|
||
.atten = atten,
|
||
.bitwidth = adc_width,
|
||
};
|
||
|
||
esp_err_t err = adc_oneshot_config_channel(adc_handle, adc_channel, &chan_cfg);
|
||
if (err != ESP_OK) {
|
||
LOG_ERROR("ADC channel config failed: %s", esp_err_to_name(err));
|
||
return false;
|
||
}
|
||
|
||
adc_calibrated = initAdcCalibration();
|
||
#endif // ARCH_ESP32
|
||
|
||
// NRF52 ADC init moved to powerHAL_init in nrf52 platform
|
||
|
||
#if !defined(ARCH_ESP32) && !defined(ARCH_STM32)
|
||
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
|
||
#endif
|
||
|
||
batteryLevel = &analogLevel;
|
||
return true;
|
||
#else
|
||
return false;
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* Initializes the Power class.
|
||
*
|
||
* @return true if the setup was successful, false otherwise.
|
||
*/
|
||
bool Power::setup()
|
||
{
|
||
#ifdef HAS_SGM41562
|
||
// Initialize the charger early so AnalogBatteryLevel can read charging
|
||
// state from it. The charger does not provide battery voltage / percent -
|
||
// those still come from the platform ADC via analogInit() below.
|
||
initSGM41562(SGM41562_WIRE);
|
||
#endif
|
||
bool found = false;
|
||
if (axpChipInit()) {
|
||
found = true;
|
||
} else if (cw2015Init()) {
|
||
found = true;
|
||
} else if (max17048Init()) {
|
||
found = true;
|
||
} else if (lipoChargerInit()) {
|
||
found = true;
|
||
} else if (serialBatteryInit()) {
|
||
found = true;
|
||
} else if (meshSolarInit()) {
|
||
found = true;
|
||
} else if (analogInit()) {
|
||
found = true;
|
||
} else {
|
||
#ifdef NRF_APM
|
||
found = true;
|
||
#endif
|
||
}
|
||
attachPowerInterrupts();
|
||
enabled = found;
|
||
low_voltage_counter = 0;
|
||
|
||
#ifdef ARCH_ESP32
|
||
// Register callbacks for before and after lightsleep
|
||
// Used to detach and reattach interrupts
|
||
lsObserver.observe(¬ifyLightSleep);
|
||
lsEndObserver.observe(¬ifyLightSleepEnd);
|
||
#endif
|
||
|
||
return found;
|
||
}
|
||
|
||
void Power::powerCommandsCheck()
|
||
{
|
||
if (rebootAtMsec && millis() > rebootAtMsec) {
|
||
LOG_INFO("Rebooting");
|
||
reboot();
|
||
}
|
||
|
||
if (shutdownAtMsec && millis() > shutdownAtMsec) {
|
||
shutdownAtMsec = 0;
|
||
shutdown();
|
||
}
|
||
}
|
||
|
||
void Power::reboot()
|
||
{
|
||
notifyReboot.notifyObservers(NULL);
|
||
#if defined(ARCH_ESP32)
|
||
ESP.restart();
|
||
#elif defined(ARCH_NRF52)
|
||
NVIC_SystemReset();
|
||
#elif defined(ARCH_RP2040)
|
||
rp2040.reboot();
|
||
#elif defined(ARCH_PORTDUINO_WASM)
|
||
// Browser/headless WASM node: no in-process restart. notifyReboot above
|
||
// already let modules persist; hand off to the host (reboot() ->
|
||
// location.reload() in a tab, or Module.onReboot() headless). Deliberately
|
||
// skip the ARCH_PORTDUINO SPI/Wire/Serial teardown below - it would kill the
|
||
// radio with no actual restart to follow, leaving a wedged node. Must come
|
||
// before the ARCH_PORTDUINO arm: the wasm build defines both macros.
|
||
::reboot();
|
||
#elif defined(ARCH_PORTDUINO)
|
||
deInitApiServer();
|
||
#ifdef __linux__
|
||
if (aLinuxInputImpl)
|
||
aLinuxInputImpl->deInit();
|
||
if (aLinuxJoystick)
|
||
aLinuxJoystick->deInit();
|
||
#endif
|
||
SPI.end();
|
||
Wire.end();
|
||
Serial1.end();
|
||
if (screen) {
|
||
screen = nullptr;
|
||
}
|
||
LOG_DEBUG("final reboot");
|
||
::reboot();
|
||
#elif defined(ARCH_STM32)
|
||
HAL_NVIC_SystemReset();
|
||
#else
|
||
rebootAtMsec = -1;
|
||
LOG_WARN("FIXME implement reboot for this platform; some settings "
|
||
"need restart to apply");
|
||
#endif
|
||
}
|
||
|
||
void Power::shutdown()
|
||
{
|
||
|
||
#if HAS_SCREEN
|
||
if (screen) {
|
||
#ifdef T_DECK_PRO
|
||
screen->showSimpleBanner("Device is powered off.\nConnect USB to start!",
|
||
0); // T-Deck Pro has no power button
|
||
#elif defined(USE_EINK)
|
||
screen->showSimpleBanner("Shutting Down...",
|
||
2250); // dismiss after 3 seconds to avoid the
|
||
// banner on the sleep screen
|
||
#else
|
||
screen->showSimpleBanner("Shutting Down...", 0); // stays on screen
|
||
#endif
|
||
}
|
||
#endif
|
||
#if !defined(ARCH_STM32WL)
|
||
playShutdownMelody();
|
||
#endif
|
||
nodeDB->saveToDisk();
|
||
#if HAS_SCREEN
|
||
messageStore.saveToFlash();
|
||
#endif
|
||
#if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)
|
||
#ifdef PIN_LED1
|
||
ledOff(PIN_LED1);
|
||
#endif
|
||
#ifdef PIN_LED2
|
||
ledOff(PIN_LED2);
|
||
#endif
|
||
#ifdef PIN_LED3
|
||
ledOff(PIN_LED3);
|
||
#endif
|
||
#ifdef LED_NOTIFICATION
|
||
ledOff(LED_NOTIFICATION);
|
||
#endif
|
||
doDeepSleep(DELAY_FOREVER, true, true);
|
||
#elif defined(ARCH_PORTDUINO)
|
||
exit(EXIT_SUCCESS);
|
||
#else
|
||
LOG_WARN("FIXME implement shutdown for this platform");
|
||
#endif
|
||
}
|
||
|
||
/// Reads power status to powerStatus singleton.
|
||
//
|
||
// TODO(girts): move this and other axp stuff to power.h/power.cpp.
|
||
void Power::readPowerStatus()
|
||
{
|
||
int32_t batteryVoltageMv = -1; // Assume unknown
|
||
int8_t batteryChargePercent = -1;
|
||
OptionalBool usbPowered = OptUnknown;
|
||
OptionalBool hasBattery = OptUnknown; // These must be static because NRF_APM
|
||
// code doesn't run every time
|
||
OptionalBool isChargingNow = OptUnknown;
|
||
|
||
if (batteryLevel) {
|
||
hasBattery = batteryLevel->isBatteryConnect() ? OptTrue : OptFalse;
|
||
#ifndef NRF_APM
|
||
usbPowered = batteryLevel->isVbusIn() ? OptTrue : OptFalse;
|
||
isChargingNow = batteryLevel->isCharging() ? OptTrue : OptFalse;
|
||
#endif
|
||
if (hasBattery) {
|
||
batteryVoltageMv = batteryLevel->getBattVoltage();
|
||
// If the AXP192 returns a valid battery percentage, use it
|
||
if (batteryLevel->getBatteryPercent() >= 0) {
|
||
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 open circuit 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);
|
||
}
|
||
}
|
||
}
|
||
|
||
// FIXME: IMO we shouldn't be littering our code with all these ifdefs. Way
|
||
// better instead to make a Nrf52IsUsbPowered subclass (which shares a
|
||
// superclass with the BatteryLevel stuff) that just provides a few methods. But
|
||
// in the interest of fixing this bug I'm going to follow current practice.
|
||
#ifdef NRF_APM // Section of code detects USB power on the RAK4631 and updates
|
||
// the power states. Takes 20 seconds or so to detect changes.
|
||
|
||
nrfx_power_usb_state_t nrf_usb_state = nrfx_power_usbstatus_get();
|
||
// LOG_DEBUG("NRF Power %d", nrf_usb_state);
|
||
|
||
// If changed to DISCONNECTED
|
||
if (nrf_usb_state == NRFX_POWER_USB_STATE_DISCONNECTED)
|
||
isChargingNow = usbPowered = OptFalse;
|
||
// If changed to CONNECTED / READY
|
||
else
|
||
isChargingNow = usbPowered = OptTrue;
|
||
|
||
#endif
|
||
|
||
// Notify any status instances that are observing us
|
||
const PowerStatus powerStatus2 = PowerStatus(hasBattery, usbPowered, isChargingNow, batteryVoltageMv, batteryChargePercent);
|
||
|
||
// Log battery-presence transitions once; skip OptUnknown so we don't lie before the first probe.
|
||
static OptionalBool prevHasBattery = OptUnknown;
|
||
if (hasBattery != OptUnknown && hasBattery != prevHasBattery) {
|
||
LOG_INFO("Power: battery hardware %s", hasBattery == OptTrue ? "detected" : "absent (USB-only)");
|
||
prevHasBattery = hasBattery;
|
||
}
|
||
|
||
// Periodic telemetry only emits when a battery is actually present (otherwise values are constant -1/0).
|
||
if (hasBattery == OptTrue && !Throttle::isWithinTimespanMs(lastLogTime, 50 * 1000)) {
|
||
LOG_DEBUG("Battery: usbPower=%d, isCharging=%d, batMv=%d, batPct=%d", powerStatus2.getHasUSB(),
|
||
powerStatus2.getIsCharging(), powerStatus2.getBatteryVoltageMv(), powerStatus2.getBatteryChargePercent());
|
||
lastLogTime = millis();
|
||
}
|
||
newStatus.notifyObservers(&powerStatus2);
|
||
|
||
// Mirror battery level to the BLE Battery Service (0x2A19); the platform layer clamps and dedupes.
|
||
if (hasBattery == OptTrue)
|
||
updateBatteryLevel(powerStatus2.getBatteryChargePercent());
|
||
#ifdef DEBUG_HEAP
|
||
if (lastheap != memGet.getFreeHeap()) {
|
||
// Use stack-allocated buffer to avoid heap allocations in monitoring code
|
||
char threadlist[256] = "Threads running:";
|
||
int threadlistLen = strlen(threadlist);
|
||
int running = 0;
|
||
for (int i = 0; i < MAX_THREADS; i++) {
|
||
auto thread = concurrency::mainController.get(i);
|
||
if ((thread != nullptr) && (thread->enabled)) {
|
||
// Use snprintf to safely append to stack buffer without heap allocation
|
||
int remaining = sizeof(threadlist) - threadlistLen - 1;
|
||
if (remaining > 0) {
|
||
int written = snprintf(threadlist + threadlistLen, remaining, " %s", thread->ThreadName.c_str());
|
||
if (written > 0 && written < remaining) {
|
||
threadlistLen += written;
|
||
}
|
||
}
|
||
running++;
|
||
}
|
||
}
|
||
LOG_HEAP(threadlist);
|
||
LOG_HEAP("Heap status: %d/%d bytes free (%d), running %d/%d threads", memGet.getFreeHeap(), memGet.getHeapSize(),
|
||
memGet.getFreeHeap() - lastheap, running, concurrency::mainController.size(false));
|
||
lastheap = memGet.getFreeHeap();
|
||
}
|
||
#ifdef DEBUG_HEAP_MQTT
|
||
if (mqtt) {
|
||
// send MQTT-Packet with Heap-Size
|
||
uint8_t dmac[6];
|
||
getMacAddr(dmac); // Get our hardware ID
|
||
char mac[18];
|
||
sprintf(mac, "!%02x%02x%02x%02x", dmac[2], dmac[3], dmac[4], dmac[5]);
|
||
|
||
auto newHeap = memGet.getFreeHeap();
|
||
// Use stack-allocated buffers to avoid heap allocations in monitoring code
|
||
char heapTopic[128];
|
||
snprintf(heapTopic, sizeof(heapTopic), "%s/2/heap/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac);
|
||
char heapString[16];
|
||
snprintf(heapString, sizeof(heapString), "%u", newHeap);
|
||
mqtt->pubSub.publish(heapTopic, heapString, false);
|
||
|
||
auto wifiRSSI = WiFi.RSSI();
|
||
char wifiTopic[128];
|
||
snprintf(wifiTopic, sizeof(wifiTopic), "%s/2/wifi/%s", (*moduleConfig.mqtt.root ? moduleConfig.mqtt.root : "msh"), mac);
|
||
char wifiString[16];
|
||
snprintf(wifiString, sizeof(wifiString), "%d", wifiRSSI);
|
||
mqtt->pubSub.publish(wifiTopic, wifiString, false);
|
||
}
|
||
#endif
|
||
|
||
#endif
|
||
|
||
// 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 (batteryLevel && powerStatus2.getHasBattery() && !powerStatus2.getHasUSB()) {
|
||
if (batteryLevel->getBattVoltage() < OCV[NUM_OCV_POINTS - 1]) {
|
||
low_voltage_counter++;
|
||
LOG_DEBUG("Low voltage counter: %d/10", low_voltage_counter);
|
||
if (low_voltage_counter > 10) {
|
||
LOG_INFO("Low voltage detected, trigger deep sleep");
|
||
powerFSM.trigger(EVENT_LOW_BATTERY);
|
||
}
|
||
} else {
|
||
low_voltage_counter = 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Emit a free-heap line to the debug log every HEAP_LOG_INTERVAL_MS, so a slow leak shows up
|
||
* as a trend in a field log instead of only as an out-of-memory reboot. Unlike the DEBUG_HEAP
|
||
* instrumentation above this is always on, and costs one line per interval.
|
||
*/
|
||
void Power::logHeapUsage()
|
||
{
|
||
#if HEAP_LOG_INTERVAL_MS > 0
|
||
if (Throttle::isWithinTimespanMs(lastHeapLogTime, HEAP_LOG_INTERVAL_MS))
|
||
return;
|
||
|
||
const uint32_t heapTotal = memGet.getHeapSize();
|
||
// Platforms without heap accounting report UINT32_MAX (or 0) - nothing worth logging
|
||
if (heapTotal == 0 || heapTotal == UINT32_MAX)
|
||
return;
|
||
|
||
const uint32_t heapFree = memGet.getFreeHeap();
|
||
// The first line has no earlier sample to difference against
|
||
const int32_t delta = lastHeapLogTime ? (int32_t)(heapFree - lastHeapLogFree) : 0;
|
||
|
||
const uint32_t psramTotal = memGet.getPsramSize();
|
||
if (psramTotal)
|
||
LOG_INFO("Heap: %u/%u bytes free (%d since last), PSRAM: %u/%u bytes free", heapFree, heapTotal, delta,
|
||
memGet.getFreePsram(), psramTotal);
|
||
else
|
||
LOG_INFO("Heap: %u/%u bytes free (%d since last)", heapFree, heapTotal, delta);
|
||
|
||
lastHeapLogFree = heapFree;
|
||
lastHeapLogTime = millis();
|
||
#endif
|
||
}
|
||
|
||
int32_t Power::runOnce()
|
||
{
|
||
readPowerStatus();
|
||
logHeapUsage();
|
||
lipoChargerRetry();
|
||
|
||
#ifdef HAS_PMU
|
||
// WE no longer use the IRQ line to wake the CPU (due to false wakes from
|
||
// sleep), but we do poll the IRQ status by reading the registers over I2C
|
||
if (PMU) {
|
||
|
||
PMU->getIrqStatus();
|
||
|
||
if (PMU->isVbusRemoveIrq()) {
|
||
LOG_INFO("USB unplugged");
|
||
powerFSM.trigger(EVENT_POWER_DISCONNECTED);
|
||
}
|
||
|
||
if (PMU->isVbusInsertIrq()) {
|
||
LOG_INFO("USB plugged In");
|
||
powerFSM.trigger(EVENT_POWER_CONNECTED);
|
||
}
|
||
|
||
#ifdef PMU_POWER_BUTTON_IS_CANCEL
|
||
// cancel action also turns the screen on and off.
|
||
if (PMU->isPekeyShortPressIrq()) {
|
||
LOG_INFO("Input: Corona Button Click");
|
||
InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_CANCEL, .kbchar = 0, .touchX = 0, .touchY = 0};
|
||
inputBroker->injectInputEvent(&event);
|
||
}
|
||
#endif
|
||
/*
|
||
Other things we could check if we cared...
|
||
|
||
if (PMU->isBatChagerStartIrq()) {
|
||
LOG_DEBUG("Battery start charging");
|
||
}
|
||
if (PMU->isBatChagerDoneIrq()) {
|
||
LOG_DEBUG("Battery fully charged");
|
||
}
|
||
if (PMU->isBatInsertIrq()) {
|
||
LOG_DEBUG("Battery inserted");
|
||
}
|
||
if (PMU->isBatRemoveIrq()) {
|
||
LOG_DEBUG("Battery removed");
|
||
}
|
||
*/
|
||
|
||
PMU->clearIrqStatus();
|
||
}
|
||
#endif
|
||
// Only read once every 20 seconds once the power status for the app has been
|
||
// initialized
|
||
return (statusHandler && statusHandler->isInitialized()) ? (1000 * 20) : RUN_SAME;
|
||
}
|
||
|
||
#ifdef ARCH_ESP32
|
||
|
||
// Detach our class' interrupts before lightsleep
|
||
// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press
|
||
int Power::beforeLightSleep(void *unused)
|
||
{
|
||
LOG_WARN("Detaching power interrupts for sleep");
|
||
detachPowerInterrupts();
|
||
return 0; // Indicates success
|
||
}
|
||
|
||
// Reconfigure our interrupts
|
||
// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep
|
||
int Power::afterLightSleep(esp_sleep_wakeup_cause_t cause)
|
||
{
|
||
attachPowerInterrupts();
|
||
return 0; // Indicates success
|
||
}
|
||
|
||
#endif
|
||
|
||
/*
|
||
* Attach (or re-attach) hardware interrupts for power management
|
||
* Public method. Used outside class when waking from MCU sleep
|
||
*/
|
||
void Power::attachPowerInterrupts()
|
||
{
|
||
#ifdef EXT_PWR_DETECT
|
||
attachInterrupt(
|
||
EXT_PWR_DETECT,
|
||
[]() {
|
||
power->setIntervalFromNow(0);
|
||
runASAP = true;
|
||
},
|
||
CHANGE);
|
||
#endif
|
||
#ifdef BATTERY_CHARGING_INV
|
||
attachInterrupt(
|
||
BATTERY_CHARGING_INV,
|
||
[]() {
|
||
power->setIntervalFromNow(0);
|
||
runASAP = true;
|
||
},
|
||
CHANGE);
|
||
#endif
|
||
#ifdef EXT_CHRG_DETECT
|
||
attachInterrupt(
|
||
EXT_CHRG_DETECT,
|
||
[]() {
|
||
power->setIntervalFromNow(0);
|
||
runASAP = true;
|
||
BaseType_t higherWake = 0;
|
||
},
|
||
CHANGE);
|
||
#endif
|
||
#ifdef PMU_IRQ
|
||
if (PMU) {
|
||
attachInterrupt(
|
||
PMU_IRQ,
|
||
[]() {
|
||
pmu_irq = true;
|
||
power->setIntervalFromNow(0);
|
||
runASAP = true;
|
||
},
|
||
FALLING);
|
||
}
|
||
#endif
|
||
}
|
||
|
||
/*
|
||
* Detach the "normal" button interrupts.
|
||
* Public method. Used before attaching a "wake-on-button" interrupt for MCU sleep
|
||
*/
|
||
void Power::detachPowerInterrupts()
|
||
{
|
||
#ifdef EXT_PWR_DETECT
|
||
detachInterrupt(EXT_PWR_DETECT);
|
||
#endif
|
||
#ifdef BATTERY_CHARGING_INV
|
||
detachInterrupt(BATTERY_CHARGING_INV);
|
||
#endif
|
||
#ifdef EXT_CHRG_DETECT
|
||
detachInterrupt(EXT_CHRG_DETECT);
|
||
#endif
|
||
#ifdef PMU_IRQ
|
||
if (PMU) {
|
||
detachInterrupt(PMU_IRQ);
|
||
}
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
* Init the power manager chip
|
||
*
|
||
* axp192 power
|
||
DCDC1 0.7-3.5V @ 1200mA max -> OLED // If you turn this off you'll lose
|
||
comms to the axp192 because the OLED and the axp192 share the same i2c bus,
|
||
instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max ->
|
||
ESP32 (keep this on!) LDO1 30mA -> charges GPS backup battery // charges the
|
||
tiny J13 battery by the GPS to power the GPS ram (for a couple of days), can
|
||
not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS
|
||
*
|
||
*/
|
||
bool Power::axpChipInit()
|
||
{
|
||
|
||
#ifdef HAS_PMU
|
||
|
||
TwoWire *w = NULL;
|
||
|
||
// Use macro to distinguish which wire is used by PMU
|
||
#ifdef PMU_USE_WIRE1
|
||
w = &Wire1;
|
||
#else
|
||
w = &Wire;
|
||
#endif
|
||
|
||
/**
|
||
* It is not necessary to specify the wire pin,
|
||
* just input the wire, because the wire has been initialized in main.cpp
|
||
*/
|
||
if (!PMU) {
|
||
PMU = new XPowersAXP2101(*w);
|
||
if (!PMU->init()) {
|
||
LOG_WARN("No AXP2101 power management");
|
||
delete PMU;
|
||
PMU = NULL;
|
||
} else {
|
||
LOG_INFO("AXP2101 PMU init succeeded");
|
||
}
|
||
}
|
||
|
||
if (!PMU) {
|
||
PMU = new XPowersAXP192(*w);
|
||
if (!PMU->init()) {
|
||
LOG_WARN("No AXP192 power management");
|
||
delete PMU;
|
||
PMU = NULL;
|
||
} else {
|
||
LOG_INFO("AXP192 PMU init succeeded");
|
||
}
|
||
}
|
||
|
||
if (!PMU) {
|
||
/*
|
||
* In XPowersLib, if the XPowersAXPxxx object is released, Wire.end() will
|
||
* be called at the same time. In order not to affect other devices, if the
|
||
* initialization of the PMU fails, Wire needs to be re-initialized once, if
|
||
* there are multiple devices sharing the bus.
|
||
* * */
|
||
#ifndef PMU_USE_WIRE1
|
||
w->begin(I2C_SDA, I2C_SCL);
|
||
#endif
|
||
return false;
|
||
}
|
||
|
||
batteryLevel = PMU;
|
||
|
||
if (PMU->getChipModel() == XPOWERS_AXP192) {
|
||
|
||
// lora radio power channel
|
||
PMU->setPowerChannelVoltage(XPOWERS_LDO2, 3300);
|
||
PMU->enablePowerOutput(XPOWERS_LDO2);
|
||
|
||
// oled module power channel,
|
||
// disable it will cause abnormal communication between boot and AXP power
|
||
// supply, do not turn it off
|
||
PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300);
|
||
// enable oled power
|
||
PMU->enablePowerOutput(XPOWERS_DCDC1);
|
||
|
||
// gnss module power channel - now turned on in setGpsPower
|
||
PMU->setPowerChannelVoltage(XPOWERS_LDO3, 3300);
|
||
// PMU->enablePowerOutput(XPOWERS_LDO3);
|
||
|
||
// protected oled power source
|
||
PMU->setProtectedChannel(XPOWERS_DCDC1);
|
||
// protected esp32 power source
|
||
PMU->setProtectedChannel(XPOWERS_DCDC3);
|
||
|
||
// disable not use channel
|
||
PMU->disablePowerOutput(XPOWERS_DCDC2);
|
||
|
||
// disable all axp chip interrupt
|
||
PMU->disableIRQ(XPOWERS_AXP192_ALL_IRQ);
|
||
|
||
// Set constant current charging current
|
||
PMU->setChargerConstantCurr(XPOWERS_AXP192_CHG_CUR_450MA);
|
||
|
||
// Set up the charging voltage
|
||
PMU->setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2);
|
||
} else if (PMU->getChipModel() == XPOWERS_AXP2101) {
|
||
|
||
/*The alternative version of T-Beam 1.1 differs from T-Beam V1.1 in that it
|
||
* uses an AXP2101 power chip*/
|
||
if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) {
|
||
// Unuse power channel
|
||
PMU->disablePowerOutput(XPOWERS_DCDC2);
|
||
PMU->disablePowerOutput(XPOWERS_DCDC3);
|
||
PMU->disablePowerOutput(XPOWERS_DCDC4);
|
||
PMU->disablePowerOutput(XPOWERS_DCDC5);
|
||
PMU->disablePowerOutput(XPOWERS_ALDO1);
|
||
PMU->disablePowerOutput(XPOWERS_ALDO4);
|
||
PMU->disablePowerOutput(XPOWERS_BLDO1);
|
||
PMU->disablePowerOutput(XPOWERS_BLDO2);
|
||
PMU->disablePowerOutput(XPOWERS_DLDO1);
|
||
PMU->disablePowerOutput(XPOWERS_DLDO2);
|
||
|
||
// GNSS RTC PowerVDD 3300mV
|
||
PMU->setPowerChannelVoltage(XPOWERS_VBACKUP, 3300);
|
||
PMU->enablePowerOutput(XPOWERS_VBACKUP);
|
||
|
||
// ESP32 VDD 3300mV
|
||
// ! No need to set, automatically open , Don't close it
|
||
// PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300);
|
||
// PMU->setProtectedChannel(XPOWERS_DCDC1);
|
||
|
||
// LoRa VDD 3300mV
|
||
PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300);
|
||
PMU->enablePowerOutput(XPOWERS_ALDO2);
|
||
|
||
// GNSS VDD 3300mV
|
||
PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300);
|
||
PMU->enablePowerOutput(XPOWERS_ALDO3);
|
||
} else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE ||
|
||
HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) {
|
||
// t-beam s3 core
|
||
/**
|
||
* gnss module power channel
|
||
* The default ALDO4 is off, you need to turn on the GNSS power first,
|
||
* otherwise it will be invalid during initialization
|
||
*/
|
||
PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300);
|
||
PMU->enablePowerOutput(XPOWERS_ALDO4);
|
||
|
||
// lora radio power channel
|
||
PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300);
|
||
PMU->enablePowerOutput(XPOWERS_ALDO3);
|
||
|
||
// m.2 interface
|
||
PMU->setPowerChannelVoltage(XPOWERS_DCDC3, 3300);
|
||
PMU->enablePowerOutput(XPOWERS_DCDC3);
|
||
|
||
/**
|
||
* ALDO2 cannot be turned off.
|
||
* It is a necessary condition for sensor communication.
|
||
* It must be turned on to properly access the sensor and screen
|
||
* It is also responsible for the power supply of PCF8563
|
||
*/
|
||
PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300);
|
||
PMU->enablePowerOutput(XPOWERS_ALDO2);
|
||
|
||
// 6-axis , magnetometer ,bme280 , oled screen power channel
|
||
PMU->setPowerChannelVoltage(XPOWERS_ALDO1, 3300);
|
||
PMU->enablePowerOutput(XPOWERS_ALDO1);
|
||
|
||
// sdcard (T-Beam S3) / gnns (T-Watch S3 Plus) power channel
|
||
PMU->setPowerChannelVoltage(XPOWERS_BLDO1, 3300);
|
||
#ifndef T_WATCH_S3
|
||
PMU->enablePowerOutput(XPOWERS_BLDO1);
|
||
#else
|
||
// DRV2605 power channel
|
||
PMU->setPowerChannelVoltage(XPOWERS_BLDO2, 3300);
|
||
PMU->enablePowerOutput(XPOWERS_BLDO2);
|
||
#endif
|
||
|
||
// PMU->setPowerChannelVoltage(XPOWERS_DCDC4, 3300);
|
||
// PMU->enablePowerOutput(XPOWERS_DCDC4);
|
||
|
||
// not use channel
|
||
PMU->disablePowerOutput(XPOWERS_DCDC2); // not elicited
|
||
PMU->disablePowerOutput(XPOWERS_DCDC5); // not elicited
|
||
PMU->disablePowerOutput(XPOWERS_DLDO1); // Invalid power channel, it does not exist
|
||
PMU->disablePowerOutput(XPOWERS_DLDO2); // Invalid power channel, it does not exist
|
||
PMU->disablePowerOutput(XPOWERS_VBACKUP);
|
||
} else if (HW_VENDOR == meshtastic_HardwareModel_TBEAM_BPF) {
|
||
// T-Beam BPF rail map (per schematic LilyGo_TBeam_BPF r2025-05-08):
|
||
// DCDC1 -> ESP32 + OLED 3V3 (always on, protected)
|
||
// ALDO2 -> MicroSD 3V3 (OFF at reset, must enable)
|
||
// ALDO4 -> L76K GNSS 3V3 (OFF at reset, must enable)
|
||
// ALDO1/3, BLDO1/2, DLDO1 -> user headers / unused at boot, leave at reset defaults.
|
||
// LoRa power is outside the PMU (external P-MOSFET switched by RF95_POWER_EN / IO16).
|
||
PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300);
|
||
PMU->enablePowerOutput(XPOWERS_ALDO4);
|
||
|
||
PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300);
|
||
PMU->enablePowerOutput(XPOWERS_ALDO2);
|
||
|
||
// Make sure nothing's driving into an unused rail
|
||
PMU->disablePowerOutput(XPOWERS_DCDC5);
|
||
PMU->disablePowerOutput(XPOWERS_DLDO1);
|
||
}
|
||
|
||
// disable all axp chip interrupt
|
||
PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ);
|
||
|
||
// Set the constant current charging current of AXP2101, temporarily use
|
||
// 500mA by default
|
||
PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA);
|
||
|
||
// Set up the charging voltage
|
||
PMU->setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2);
|
||
}
|
||
|
||
PMU->clearIrqStatus();
|
||
|
||
// TBeam1.1 /T-Beam S3-Core has no external TS detection,
|
||
// it needs to be disabled, otherwise it will cause abnormal charging
|
||
PMU->disableTSPinMeasure();
|
||
|
||
// PMU->enableSystemVoltageMeasure();
|
||
PMU->enableVbusVoltageMeasure();
|
||
PMU->enableBattVoltageMeasure();
|
||
|
||
if (PMU->isChannelAvailable(XPOWERS_DCDC1)) {
|
||
LOG_DEBUG("DC1 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC1) ? "+" : "-",
|
||
PMU->getPowerChannelVoltage(XPOWERS_DCDC1));
|
||
}
|
||
if (PMU->isChannelAvailable(XPOWERS_DCDC2)) {
|
||
LOG_DEBUG("DC2 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC2) ? "+" : "-",
|
||
PMU->getPowerChannelVoltage(XPOWERS_DCDC2));
|
||
}
|
||
if (PMU->isChannelAvailable(XPOWERS_DCDC3)) {
|
||
LOG_DEBUG("DC3 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC3) ? "+" : "-",
|
||
PMU->getPowerChannelVoltage(XPOWERS_DCDC3));
|
||
}
|
||
if (PMU->isChannelAvailable(XPOWERS_DCDC4)) {
|
||
LOG_DEBUG("DC4 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_DCDC4) ? "+" : "-",
|
||
PMU->getPowerChannelVoltage(XPOWERS_DCDC4));
|
||
}
|
||
if (PMU->isChannelAvailable(XPOWERS_LDO2)) {
|
||
LOG_DEBUG("LDO2 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_LDO2) ? "+" : "-",
|
||
PMU->getPowerChannelVoltage(XPOWERS_LDO2));
|
||
}
|
||
if (PMU->isChannelAvailable(XPOWERS_LDO3)) {
|
||
LOG_DEBUG("LDO3 : %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_LDO3) ? "+" : "-",
|
||
PMU->getPowerChannelVoltage(XPOWERS_LDO3));
|
||
}
|
||
if (PMU->isChannelAvailable(XPOWERS_ALDO1)) {
|
||
LOG_DEBUG("ALDO1: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO1) ? "+" : "-",
|
||
PMU->getPowerChannelVoltage(XPOWERS_ALDO1));
|
||
}
|
||
if (PMU->isChannelAvailable(XPOWERS_ALDO2)) {
|
||
LOG_DEBUG("ALDO2: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO2) ? "+" : "-",
|
||
PMU->getPowerChannelVoltage(XPOWERS_ALDO2));
|
||
}
|
||
if (PMU->isChannelAvailable(XPOWERS_ALDO3)) {
|
||
LOG_DEBUG("ALDO3: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO3) ? "+" : "-",
|
||
PMU->getPowerChannelVoltage(XPOWERS_ALDO3));
|
||
}
|
||
if (PMU->isChannelAvailable(XPOWERS_ALDO4)) {
|
||
LOG_DEBUG("ALDO4: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_ALDO4) ? "+" : "-",
|
||
PMU->getPowerChannelVoltage(XPOWERS_ALDO4));
|
||
}
|
||
if (PMU->isChannelAvailable(XPOWERS_BLDO1)) {
|
||
LOG_DEBUG("BLDO1: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_BLDO1) ? "+" : "-",
|
||
PMU->getPowerChannelVoltage(XPOWERS_BLDO1));
|
||
}
|
||
if (PMU->isChannelAvailable(XPOWERS_BLDO2)) {
|
||
LOG_DEBUG("BLDO2: %s Voltage:%u mV ", PMU->isPowerChannelEnable(XPOWERS_BLDO2) ? "+" : "-",
|
||
PMU->getPowerChannelVoltage(XPOWERS_BLDO2));
|
||
}
|
||
|
||
// We can safely ignore this approach for most (or all) boards because MCU
|
||
// turned off earlier than battery discharged to 2.6V.
|
||
//
|
||
// Unfortunately for now we can't use this killswitch for RAK4630-based boards
|
||
// because they have a bug with battery voltage measurement. Probably it
|
||
// sometimes drops to low values.
|
||
#ifndef RAK4630
|
||
// Set PMU shutdown voltage at 2.6V to maximize battery utilization
|
||
PMU->setSysPowerDownVoltage(2600);
|
||
#endif
|
||
|
||
#ifdef PMU_IRQ
|
||
uint64_t pmuIrqMask = 0;
|
||
|
||
if (PMU->getChipModel() == XPOWERS_AXP192) {
|
||
pmuIrqMask = XPOWERS_AXP192_VBUS_INSERT_IRQ | XPOWERS_AXP192_VBUS_REMOVE_IRQ | XPOWERS_AXP192_PKEY_SHORT_IRQ;
|
||
} else if (PMU->getChipModel() == XPOWERS_AXP2101) {
|
||
pmuIrqMask = XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_VBUS_REMOVE_IRQ | XPOWERS_AXP2101_PKEY_SHORT_IRQ;
|
||
}
|
||
|
||
pinMode(PMU_IRQ, INPUT);
|
||
|
||
// We wake on IRQ, so only enable the IRQs that we care about.
|
||
// we want USB plug and unplug to update the screen and LED status,
|
||
// and short press on the power button to trigger the "cancel" action in the UI (which also turns the screen on and off).
|
||
PMU->enableIRQ(pmuIrqMask);
|
||
|
||
PMU->clearIrqStatus();
|
||
#endif /*PMU_IRQ*/
|
||
|
||
readPowerStatus();
|
||
|
||
pmu_found = true;
|
||
|
||
return pmu_found;
|
||
|
||
#else
|
||
return false;
|
||
#endif
|
||
}
|
||
|
||
#if !MESHTASTIC_EXCLUDE_I2C && __has_include(<Adafruit_MAX1704X.h>)
|
||
|
||
/**
|
||
* Wrapper class for an I2C MAX17048 Lipo battery sensor.
|
||
*/
|
||
class MAX17048BatteryLevel : public HasBatteryLevel
|
||
{
|
||
private:
|
||
MAX17048Singleton *max17048 = nullptr;
|
||
|
||
public:
|
||
/**
|
||
* Init the I2C MAX17048 Lipo battery level sensor
|
||
*/
|
||
bool runOnce()
|
||
{
|
||
if (max17048 == nullptr) {
|
||
max17048 = MAX17048Singleton::GetInstance();
|
||
}
|
||
|
||
// try to start if the sensor has been detected
|
||
if (nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX17048].first != 0) {
|
||
return max17048->runOnce(nodeTelemetrySensorsMap[meshtastic_TelemetrySensorType_MAX17048].second);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Battery state of charge, from 0 to 100 or -1 for unknown
|
||
*/
|
||
virtual int getBatteryPercent() override { return max17048->getBusBatteryPercent(); }
|
||
|
||
/**
|
||
* The raw voltage of the battery in millivolts, or NAN if unknown
|
||
*/
|
||
virtual uint16_t getBattVoltage() override { return max17048->getBusVoltageMv(); }
|
||
|
||
/**
|
||
* return true if there is a battery installed in this unit
|
||
*/
|
||
virtual bool isBatteryConnect() override { return max17048->isBatteryConnected(); }
|
||
|
||
/**
|
||
* return true if there is an external power source detected
|
||
*/
|
||
virtual bool isVbusIn() override { return max17048->isExternallyPowered(); }
|
||
|
||
/**
|
||
* return true if the battery is currently charging
|
||
*/
|
||
virtual bool isCharging() override { return max17048->isBatteryCharging(); }
|
||
};
|
||
|
||
MAX17048BatteryLevel max17048Level;
|
||
|
||
/**
|
||
* Init the Lipo battery level sensor
|
||
*/
|
||
bool Power::max17048Init()
|
||
{
|
||
bool result = max17048Level.runOnce();
|
||
LOG_DEBUG("Power::max17048Init lipo sensor is %s", result ? "ready" : "not ready yet");
|
||
if (!result)
|
||
return false;
|
||
batteryLevel = &max17048Level;
|
||
return true;
|
||
}
|
||
|
||
#else
|
||
/**
|
||
* The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel
|
||
*/
|
||
bool Power::max17048Init()
|
||
{
|
||
return false;
|
||
}
|
||
#endif
|
||
|
||
#if !MESHTASTIC_EXCLUDE_I2C && HAS_CW2015
|
||
|
||
class CW2015BatteryLevel : public AnalogBatteryLevel
|
||
{
|
||
public:
|
||
/**
|
||
* Battery state of charge, from 0 to 100 or -1 for unknown
|
||
*/
|
||
virtual int getBatteryPercent() override
|
||
{
|
||
int data = -1;
|
||
Wire.beginTransmission(CW2015_ADDR);
|
||
Wire.write(0x04);
|
||
if (Wire.endTransmission() == 0) {
|
||
if (Wire.requestFrom(CW2015_ADDR, (uint8_t)1)) {
|
||
data = Wire.read();
|
||
}
|
||
}
|
||
return data;
|
||
}
|
||
|
||
/**
|
||
* The raw voltage of the battery in millivolts, or NAN if unknown
|
||
*/
|
||
virtual uint16_t getBattVoltage() override
|
||
{
|
||
uint16_t mv = 0;
|
||
Wire.beginTransmission(CW2015_ADDR);
|
||
Wire.write(0x02);
|
||
if (Wire.endTransmission() == 0) {
|
||
if (Wire.requestFrom(CW2015_ADDR, (uint8_t)2)) {
|
||
mv = Wire.read();
|
||
mv <<= 8;
|
||
mv |= Wire.read();
|
||
// Voltage is read in 305uV units, convert to mV
|
||
mv = mv * 305 / 1000;
|
||
}
|
||
}
|
||
return mv;
|
||
}
|
||
};
|
||
|
||
CW2015BatteryLevel cw2015Level;
|
||
|
||
/**
|
||
* Init the CW2015 battery level sensor
|
||
*/
|
||
bool Power::cw2015Init()
|
||
{
|
||
|
||
Wire.beginTransmission(CW2015_ADDR);
|
||
uint8_t getInfo[] = {0x0a, 0x00};
|
||
Wire.write(getInfo, 2);
|
||
Wire.endTransmission();
|
||
delay(10);
|
||
Wire.beginTransmission(CW2015_ADDR);
|
||
Wire.write(0x00);
|
||
bool result = false;
|
||
if (Wire.endTransmission() == 0) {
|
||
if (Wire.requestFrom(CW2015_ADDR, (uint8_t)1)) {
|
||
uint8_t data = Wire.read();
|
||
LOG_DEBUG("CW2015 init read data: 0x%x", data);
|
||
if (data == 0x73) {
|
||
result = true;
|
||
batteryLevel = &cw2015Level;
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
#else
|
||
/**
|
||
* The CW2015 battery level sensor is unavailable - default to AnalogBatteryLevel
|
||
*/
|
||
bool Power::cw2015Init()
|
||
{
|
||
return false;
|
||
}
|
||
#endif
|
||
|
||
#if defined(HAS_PPM) && HAS_PPM
|
||
|
||
// The gauge is soldered on, so a failed init means wedged rather than absent - retry from
|
||
// the power thread before writing it off.
|
||
#define BQ27220_INIT_ATTEMPTS 3
|
||
#define BQ27220_RETRY_INTERVAL_MS (60 * 1000)
|
||
|
||
/**
|
||
* Adapter class for BQ25896/BQ27220 Lipo battery charger.
|
||
*
|
||
* The gauge only adds time-to-full/empty, so its failure must not take the charger down.
|
||
*/
|
||
class LipoCharger : public HasBatteryLevel
|
||
{
|
||
private:
|
||
BQ27220 *bq = nullptr;
|
||
uint8_t gaugeAttemptsLeft = BQ27220_INIT_ATTEMPTS;
|
||
uint32_t lastGaugeAttemptMs = 0;
|
||
|
||
// An aborted transfer leaves the i2c_master driver holding a stale transaction, which
|
||
// the next transfer trips over. Deleting the bus frees it along with the interrupt.
|
||
void recoverI2CBus()
|
||
{
|
||
#ifdef ARCH_ESP32
|
||
Wire.end();
|
||
Wire.begin(I2C_SDA, I2C_SCL);
|
||
#endif
|
||
}
|
||
|
||
public:
|
||
/**
|
||
* Init the I2C BQ25896 Lipo battery charger
|
||
*/
|
||
bool runOnce()
|
||
{
|
||
if (PPM == nullptr) {
|
||
PPM = new XPowersPPM;
|
||
bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR);
|
||
if (result) {
|
||
LOG_INFO("PPM BQ25896 init succeeded");
|
||
// Set the minimum operating voltage. Below this voltage, the PPM will
|
||
// protect PPM->setSysPowerDownVoltage(3100);
|
||
|
||
// Set input current limit, default is 500mA
|
||
// PPM->setInputCurrentLimit(800);
|
||
|
||
// Disable current limit pin
|
||
// PPM->disableCurrentLimitPin();
|
||
|
||
// Set the charging target voltage, Range:3840 ~ 4608mV ,step:16 mV
|
||
PPM->setChargeTargetVoltage(4288);
|
||
|
||
// Set the precharge current , Range: 64mA ~ 1024mA ,step:64mA
|
||
// PPM->setPrechargeCurr(64);
|
||
|
||
// The premise is that limit pin is disabled, or it will
|
||
// only follow the maximum charging current set by limit pin.
|
||
// Set the charging current , Range:0~5056mA ,step:64mA
|
||
PPM->setChargerConstantCurr(1024);
|
||
|
||
// To obtain voltage data, the ADC must be enabled first
|
||
PPM->enableMeasure();
|
||
|
||
// Turn on charging function
|
||
// If there is no battery connected, do not turn on the charging
|
||
// function
|
||
PPM->enableCharge();
|
||
} else {
|
||
LOG_WARN("PPM BQ25896 init failed");
|
||
delete PPM;
|
||
PPM = nullptr;
|
||
return false;
|
||
}
|
||
}
|
||
gaugeRunOnce();
|
||
// Ready on the charger alone, so Power stays enabled and can retry the gauge later.
|
||
return true;
|
||
}
|
||
|
||
/// Bring up the BQ27220 fuel gauge, unless it is already up or out of attempts
|
||
void gaugeRunOnce()
|
||
{
|
||
if (bq != nullptr || gaugeAttemptsLeft == 0)
|
||
return;
|
||
if (gaugeAttemptsLeft < BQ27220_INIT_ATTEMPTS &&
|
||
Throttle::isWithinTimespanMs(lastGaugeAttemptMs, BQ27220_RETRY_INTERVAL_MS))
|
||
return;
|
||
|
||
lastGaugeAttemptMs = millis();
|
||
gaugeAttemptsLeft--;
|
||
|
||
// Cheap probe first: a silent gauge costs one transaction instead of the
|
||
// multi-second unseal/reset/provision sequence inside init().
|
||
Wire.beginTransmission(BQ27220_I2C_ADDRESS);
|
||
if (Wire.endTransmission() != 0) {
|
||
LOG_WARN("BQ27220 not responding at 0x%x", BQ27220_I2C_ADDRESS);
|
||
return;
|
||
}
|
||
|
||
bq = new BQ27220;
|
||
bq->setDefaultCapacity(BQ27220_DESIGN_CAPACITY);
|
||
|
||
if (bq->init()) {
|
||
LOG_DEBUG("BQ27220 design capacity: %d", bq->getDesignCapacity());
|
||
LOG_DEBUG("BQ27220 fullCharge capacity: %d", bq->getFullChargeCapacity());
|
||
LOG_DEBUG("BQ27220 remaining capacity: %d", bq->getRemainingCapacity());
|
||
return;
|
||
}
|
||
|
||
delete bq;
|
||
bq = nullptr;
|
||
// init() bails out mid-sequence, so hand the next bus user a sane driver state.
|
||
recoverI2CBus();
|
||
LOG_WARN("BQ27220 init failed (%d retries left), use BQ25896 for battery state", (int)gaugeAttemptsLeft);
|
||
}
|
||
|
||
/**
|
||
* Battery state of charge, from 0 to 100 or -1 for unknown
|
||
*/
|
||
virtual int getBatteryPercent() override
|
||
{
|
||
return -1;
|
||
// return bq->getChargePercent(); // don't use BQ27220 for battery percent,
|
||
// it is not calibrated
|
||
}
|
||
|
||
/**
|
||
* The raw voltage of the battery in millivolts, or NAN if unknown
|
||
*/
|
||
virtual uint16_t getBattVoltage() override { return bq ? bq->getVoltage() : PPM->getBattVoltage(); }
|
||
|
||
/**
|
||
* return true if there is a battery installed in this unit
|
||
*/
|
||
virtual bool isBatteryConnect() override { return PPM->getBattVoltage() > 0; }
|
||
|
||
/**
|
||
* return true if there is an external power source detected
|
||
*/
|
||
virtual bool isVbusIn() override { return PPM->isVbusIn(); }
|
||
|
||
/**
|
||
* return true if the battery is currently charging
|
||
*/
|
||
virtual bool isCharging() override
|
||
{
|
||
bool isCharging = PPM->isCharging();
|
||
if (bq) {
|
||
if (isCharging) {
|
||
LOG_DEBUG("BQ27220 time to full charge: %d min", bq->getTimeToFull());
|
||
} else {
|
||
if (!PPM->isVbusIn()) {
|
||
LOG_DEBUG("BQ27220 time to empty: %d min (%d mAh)", bq->getTimeToEmpty(), bq->getRemainingCapacity());
|
||
}
|
||
}
|
||
}
|
||
return isCharging;
|
||
}
|
||
};
|
||
|
||
LipoCharger lipoCharger;
|
||
|
||
/**
|
||
* Init the Lipo battery charger
|
||
*/
|
||
bool Power::lipoChargerInit()
|
||
{
|
||
bool result = lipoCharger.runOnce();
|
||
LOG_DEBUG("Power::lipoChargerInit lipo sensor is %s", result ? "ready" : "not ready yet");
|
||
if (!result)
|
||
return false;
|
||
batteryLevel = &lipoCharger;
|
||
return true;
|
||
}
|
||
|
||
/// Retry a fuel gauge that did not come up during setup
|
||
void Power::lipoChargerRetry()
|
||
{
|
||
lipoCharger.gaugeRunOnce();
|
||
}
|
||
|
||
#else
|
||
/**
|
||
* The Lipo battery level sensor is unavailable - default to AnalogBatteryLevel
|
||
*/
|
||
bool Power::lipoChargerInit()
|
||
{
|
||
return false;
|
||
}
|
||
|
||
void Power::lipoChargerRetry() {}
|
||
#endif
|
||
|
||
#ifdef HELTEC_MESH_SOLAR
|
||
#include "meshSolarApp.h"
|
||
|
||
/**
|
||
* meshSolar class for an SMBUS battery sensor.
|
||
*/
|
||
class meshSolarBatteryLevel : public HasBatteryLevel
|
||
{
|
||
|
||
public:
|
||
/**
|
||
* Init the I2C meshSolar battery level sensor
|
||
*/
|
||
bool runOnce()
|
||
{
|
||
meshSolarStart();
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Battery state of charge, from 0 to 100 or -1 for unknown
|
||
*/
|
||
virtual int getBatteryPercent() override { return meshSolarGetBatteryPercent(); }
|
||
|
||
/**
|
||
* The raw voltage of the battery in millivolts, or NAN if unknown
|
||
*/
|
||
virtual uint16_t getBattVoltage() override { return meshSolarGetBattVoltage(); }
|
||
|
||
/**
|
||
* return true if there is a battery installed in this unit
|
||
*/
|
||
virtual bool isBatteryConnect() override { return meshSolarIsBatteryConnect(); }
|
||
|
||
/**
|
||
* return true if there is an external power source detected
|
||
*/
|
||
virtual bool isVbusIn() override { return meshSolarIsVbusIn(); }
|
||
|
||
/**
|
||
* return true if the battery is currently charging
|
||
*/
|
||
virtual bool isCharging() override { return meshSolarIsCharging(); }
|
||
};
|
||
|
||
meshSolarBatteryLevel meshSolarLevel;
|
||
|
||
/**
|
||
* Init the meshSolar battery level sensor
|
||
*/
|
||
bool Power::meshSolarInit()
|
||
{
|
||
bool result = meshSolarLevel.runOnce();
|
||
LOG_DEBUG("Power::meshSolarInit sensor is %s", result ? "ready" : "not ready yet");
|
||
if (!result)
|
||
return false;
|
||
batteryLevel = &meshSolarLevel;
|
||
return true;
|
||
}
|
||
|
||
#else
|
||
/**
|
||
* The meshSolar battery level sensor is unavailable - default to
|
||
* AnalogBatteryLevel
|
||
*/
|
||
bool Power::meshSolarInit()
|
||
{
|
||
return false;
|
||
}
|
||
#endif
|
||
|
||
#ifdef HAS_SERIAL_BATTERY_LEVEL
|
||
#include <SoftwareSerial.h>
|
||
|
||
/**
|
||
* SerialBatteryLevel class for pulling battery information from a secondary MCU over serial.
|
||
*/
|
||
class SerialBatteryLevel : public HasBatteryLevel
|
||
{
|
||
|
||
public:
|
||
/**
|
||
* Init the I2C meshSolar battery level sensor
|
||
*/
|
||
bool runOnce()
|
||
{
|
||
BatterySerial.begin(4800);
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Battery state of charge, from 0 to 100 or -1 for unknown
|
||
*/
|
||
virtual int getBatteryPercent() override { return v_percent; }
|
||
|
||
/**
|
||
* The raw voltage of the battery in millivolts, or NAN if unknown
|
||
*/
|
||
virtual uint16_t getBattVoltage() override { return voltage * 1000; }
|
||
|
||
/**
|
||
* return true if there is a battery installed in this unit
|
||
*/
|
||
virtual bool isBatteryConnect() override
|
||
{
|
||
// definitely need to gobble up more bytes at once
|
||
if (BatterySerial.available() > 5) {
|
||
// LOG_WARN("SerialBatteryLevel: %u bytes available", BatterySerial.available());
|
||
while (BatterySerial.available() > 11) {
|
||
BatterySerial.read(); // flush old data
|
||
}
|
||
// LOG_WARN("SerialBatteryLevel: %u bytes now available", BatterySerial.available());
|
||
int tries = 0;
|
||
while (BatterySerial.read() != 0xFE) {
|
||
tries++; // wait for start byte
|
||
if (tries > 10) {
|
||
LOG_WARN("SerialBatteryLevel: no start byte found");
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
Data[1] = BatterySerial.read();
|
||
Data[2] = BatterySerial.read();
|
||
Data[3] = BatterySerial.read();
|
||
Data[4] = BatterySerial.read();
|
||
Data[5] = BatterySerial.read();
|
||
if (Data[5] != 0xFD) {
|
||
LOG_WARN("SerialBatteryLevel: invalid end byte %02x", Data[5]);
|
||
return true;
|
||
}
|
||
v_percent = Data[1];
|
||
voltage = Data[2] + (((float)Data[3]) / 100) + (((float)Data[4]) / 10000);
|
||
voltage *= 2;
|
||
// LOG_WARN("SerialBatteryLevel: received data %u, %f, %02x", v_percent, voltage, Data[5]);
|
||
return true;
|
||
}
|
||
// This function runs first, so use it to grab the latest data from the secondary MCU
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* return true if there is an external power source detected
|
||
*/
|
||
virtual bool isVbusIn() override
|
||
{
|
||
#if defined(EXT_CHRG_DETECT)
|
||
|
||
return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE;
|
||
|
||
#endif
|
||
return false;
|
||
}
|
||
|
||
virtual bool isCharging() override
|
||
{
|
||
#ifdef EXT_CHRG_DETECT
|
||
return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE;
|
||
|
||
#endif
|
||
// by default, we check the battery voltage only
|
||
return isVbusIn();
|
||
}
|
||
|
||
private:
|
||
SoftwareSerial BatterySerial = SoftwareSerial(SERIAL_BATTERY_RX, SERIAL_BATTERY_TX);
|
||
uint8_t Data[6] = {0};
|
||
int v_percent = 0;
|
||
float voltage = 0.0;
|
||
};
|
||
|
||
SerialBatteryLevel serialBatteryLevel;
|
||
|
||
/**
|
||
* Init the serial battery level sensor
|
||
*/
|
||
bool Power::serialBatteryInit()
|
||
{
|
||
#ifdef EXT_PWR_DETECT
|
||
pinMode(EXT_PWR_DETECT, EXT_PWR_DETECT_MODE);
|
||
#endif
|
||
#ifdef EXT_CHRG_DETECT
|
||
pinMode(EXT_CHRG_DETECT, EXT_CHRG_DETECT_MODE);
|
||
#endif
|
||
|
||
bool result = serialBatteryLevel.runOnce();
|
||
LOG_DEBUG("Power::serialBatteryInit sensor is %s", result ? "ready" : "not ready yet");
|
||
if (!result)
|
||
return false;
|
||
batteryLevel = &serialBatteryLevel;
|
||
return true;
|
||
}
|
||
|
||
#else
|
||
/**
|
||
* If this device has no serial battery level sensor, don't try to use it.
|
||
*/
|
||
bool Power::serialBatteryInit()
|
||
{
|
||
return false;
|
||
}
|
||
#endif
|