fix(Power): survive a BQ27220 fuel gauge that fails to init (#11401)

* 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.
This commit is contained in:
Thomas Göttgens
2026-08-11 07:00:01 +00:00
committed by GitHub
parent b8dee13d0b
commit bcf486fa8b
2 files changed
+76 -22

No files matched your search

+74 -22
View File
@@ -1117,6 +1117,7 @@ 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
@@ -1733,13 +1734,32 @@ bool Power::cw2015Init()
#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:
/**
@@ -1786,24 +1806,46 @@ class LipoCharger : public HasBatteryLevel
return false;
}
}
if (bq == nullptr) {
bq = new BQ27220;
bq->setDefaultCapacity(BQ27220_DESIGN_CAPACITY);
gaugeRunOnce();
// Ready on the charger alone, so Power stays enabled and can retry the gauge later.
return true;
}
bool result = bq->init();
if (result) {
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 true;
} else {
LOG_WARN("BQ27220 init failed");
delete bq;
bq = nullptr;
return false;
}
/// 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;
}
return false;
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);
}
/**
@@ -1819,7 +1861,7 @@ class LipoCharger : public HasBatteryLevel
/**
* The raw voltage of the battery in millivolts, or NAN if unknown
*/
virtual uint16_t getBattVoltage() override { return bq->getVoltage(); }
virtual uint16_t getBattVoltage() override { return bq ? bq->getVoltage() : PPM->getBattVoltage(); }
/**
* return true if there is a battery installed in this unit
@@ -1837,11 +1879,13 @@ class LipoCharger : public HasBatteryLevel
virtual bool isCharging() override
{
bool isCharging = PPM->isCharging();
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());
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;
@@ -1863,6 +1907,12 @@ bool Power::lipoChargerInit()
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
@@ -1871,6 +1921,8 @@ bool Power::lipoChargerInit()
{
return false;
}
void Power::lipoChargerRetry() {}
#endif
#ifdef HELTEC_MESH_SOLAR
+2
View File
@@ -121,6 +121,8 @@ class Power : public concurrency::OSThread
bool max17048Init();
/// Setup a Lipo charger
bool lipoChargerInit();
/// Retry a fuel gauge that did not come up during setup
void lipoChargerRetry();
/// Setup a meshSolar battery sensor
bool meshSolarInit();
/// Setup a serial battery sensor