diff --git a/README.md b/README.md index 78b3f08a..164e8ded 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ Battery states will overwrite LED connection states. All LED states can be manua | 🟢 Green - Pulsing | Trickle charge; final voltage (constant voltage) reached. Can be disabled via config | | 🟩 Green - Solid | Fully charged | -*If your OpenEarable goes into deep discharge (solid red) after pre-charge (red pulse), you can unplug the OpenEarable and plug it in again. This should recover the device. +Solid red indicates a charging fault or inhibit. Firmware attempts one safety-timer recovery per USB connection; repeated faults require disconnecting USB before retrying. ### Discharging States diff --git a/src/Battery/BQ25120a.cpp b/src/Battery/BQ25120a.cpp index 71da49d9..cc9c441c 100644 --- a/src/Battery/BQ25120a.cpp +++ b/src/Battery/BQ25120a.cpp @@ -2,9 +2,30 @@ #include "openearable_common.h" +#include #include LOG_MODULE_REGISTER(bq25120a, LOG_LEVEL_DBG); +namespace { +K_MUTEX_DEFINE(charger_mutex); + +// CD changes and register read/modify/write must not interleave. Zephyr mutexes +// are recursive so setup can call the same checked accessors as normal service. +class ChargerLock { +public: + ChargerLock() { k_mutex_lock(&charger_mutex, K_FOREVER); } + ~ChargerLock() { k_mutex_unlock(&charger_mutex); } +}; + +// Setup clears stale CE/HZ bits; normal current changes preserve them. +uint8_t encode_charge_current(float mA) { + if (mA >= 40) { + return (static_cast((mA - 40) / 10 + EPS) << 2) | BIT(7); + } + return static_cast(MIN(mA, 35.0f) - 5 + EPS) << 2; +} +} + BQ25120a battery_controller(&I2C1); BQ25120a::BQ25120a(TWIM * i2c) : _i2c(i2c) { //, load_switch(LoadSwitch(GPIO_DT_SPEC_GET(DT_NODELABEL(bq25120a), lsctrl_gpios))) { @@ -14,7 +35,8 @@ BQ25120a::BQ25120a(TWIM * i2c) : _i2c(i2c) { //, load_switch(LoadSwitch(GPIO_DT_ int BQ25120a::begin() { int ret; - ret = device_is_ready(pg_pin.port); //bool + ret = device_is_ready(pg_pin.port) && device_is_ready(int_pin.port) && + device_is_ready(cd_pin.port) && device_is_ready(_i2c->master); //bool if (!ret) { LOG_ERR("BQ25120a pins not ready.\n"); return -1; @@ -32,7 +54,11 @@ int BQ25120a::begin() { return ret; } - ret = gpio_pin_configure_dt(&cd_pin, GPIO_OUTPUT_INACTIVE); + // USB must not charge until the gauge and charger settings are checked. + charge_disabled = true; + configured = false; + ret = gpio_pin_configure_dt(&cd_pin, + power_connected() ? GPIO_OUTPUT_ACTIVE : GPIO_OUTPUT_INACTIVE); if (ret != 0) { LOG_ERR("Failed to set GPOUT as input.\n"); return ret; @@ -59,12 +85,18 @@ int BQ25120a::begin() { } int BQ25120a::reset() { - uint8_t val = 0x80; - writeReg(registers::ILIM_UVLO, &val, sizeof(val)); - - k_usleep(1000); //TODO: check value - - return 0; + // Register reset loses the safety profile; keep charging inhibited. + ChargerLock lock; + configured = false; + int ret = disable_charge(); + if (ret) return ret; + ret = exit_high_impedance(); + if (ret) return ret; + const uint8_t value = BIT(7); + bool ok = writeReg(registers::ILIM_UVLO, &value, sizeof(value)); + k_usleep(BQ25120a_HIGH_Z_TIMEOUT_US); + int restore = enter_high_impedance(); + return ok ? restore : -EIO; } int BQ25120a::set_wakeup_int() { @@ -93,83 +125,103 @@ int BQ25120a::set_wakeup_int() { bool BQ25120a::readReg(uint8_t reg, uint8_t * buffer, uint16_t len) { int ret; - uint64_t now = micros(); - int delay = MIN(BQ25120a_I2C_TIMEOUT_US - (int)(now - last_i2c), BQ25120a_I2C_TIMEOUT_US); - int delay_hz = MIN(BQ25120a_HIGH_Z_TIMEOUT_US - (int)(now - last_high_z), BQ25120a_HIGH_Z_TIMEOUT_US); - - delay = MAX(delay, delay_hz); - - if (delay > 0) k_usleep(delay); - + // Hold CD and the quiet interval stable throughout the transfer. + ChargerLock lock; _i2c->aquire(); + wait_for_i2c(); ret = i2c_burst_read(_i2c->master, address, reg, buffer, len); if (ret) LOG_WRN("I2C read failed: %d\n", ret); - _i2c->release(); - last_i2c = micros(); + _i2c->release(); return ret == 0; } -void BQ25120a::writeReg(uint8_t reg, uint8_t *buffer, uint16_t len) { +bool BQ25120a::writeReg(uint8_t reg, const uint8_t *buffer, uint16_t len) { int ret; - uint64_t now = micros(); - int delay = MIN(BQ25120a_I2C_TIMEOUT_US - (int)(now - last_i2c), BQ25120a_I2C_TIMEOUT_US); - int delay_hz = MIN(BQ25120a_HIGH_Z_TIMEOUT_US - (int)(now - last_high_z), BQ25120a_HIGH_Z_TIMEOUT_US); - - delay = MAX(delay, delay_hz); - - if (delay > 0) k_usleep(delay); //TODO: assert no message ? - + // Serialize the quiet period with the transfer; a failed write is not setup. + ChargerLock lock; _i2c->aquire(); + wait_for_i2c(); ret = i2c_burst_write(_i2c->master, address, reg, buffer, len); if (ret) LOG_WRN("I2C write failed: %d", ret); + last_i2c = micros(); _i2c->release(); - last_i2c = micros(); + return ret == 0; } -void BQ25120a::setup(const battery_settings &_battery_settings) { - ilim_uvlo params; - params.lim_mA = _battery_settings.i_max; - params.uvlo_v = _battery_settings.u_vlo; - - exit_high_impedance(); - - //reset(); - setup_ts_control(); - write_battery_voltage_control(_battery_settings.u_term); - write_charging_control(_battery_settings.i_charge); - write_termination_control(_battery_settings.i_term); - write_LDO_voltage_control(3.3); - write_uvlo_ilim(params); +int BQ25120a::setup(const battery_settings &settings) { + ChargerLock lock; + configured = false; + int ret = disable_charge(); + if (ret) return ret; + ret = exit_high_impedance(); + if (ret) return ret; + + // Do not let invalid settings wrap into a higher charge current or voltage. + if (!isfinite(settings.i_charge) || settings.i_charge < 5 || settings.i_charge > 300 || + !isfinite(settings.i_term) || settings.i_term < 0.5f || settings.i_term > 37 || + !isfinite(settings.i_max) || settings.i_max < 50 || settings.i_max > 400 || + !isfinite(settings.u_term) || settings.u_term < 3.6f || settings.u_term > 4.65f || + !isfinite(settings.u_vlo) || settings.u_vlo < 2.2f || settings.u_vlo > 3.0f) { + enter_high_impedance(); + return -EINVAL; + } - enter_high_impedance(); + ilim_uvlo params; + params.lim_mA = settings.i_max; + params.uvlo_v = settings.u_vlo; + // USB can arrive between the ship-mode PG check and the command write. + // Clear any pending ship request before later USB removal can act on it. + const uint8_t normal_mode = 0; + ret = writeReg(registers::CTRL, &normal_mode, 1) ? 0 : -EIO; + if (ret >= 0) ret = setup_ts_control(); + if (ret >= 0) ret = write_battery_voltage_control(settings.u_term); + if (ret >= 0) ret = write_termination_control(settings.i_term); + if (ret >= 0) ret = write_uvlo_ilim(params); + // Keep the three-hour safety timer and enable its documented 2x slowdown + // during input/system-load/temperature current limiting (SLUSD08A 9.3.15). + if (ret >= 0) ret = write_verified(registers::TIMERS, 0x4A, 0xFE); + // Explicitly clear stale CE/HZ bits. Rewriting HZ=1 before clearing it could + // disable I2C and prevent the recovery write on battery power. + if (ret >= 0) ret = write_verified(registers::CHARGE_CTRL, + encode_charge_current(settings.i_charge)); + if (ret >= 0) ret = write_LDO_voltage_control(3.3f); + + int restore = enter_high_impedance(); + if (ret < 0) return ret; + if (restore) return restore; + configured = true; + return 0; } uint8_t BQ25120a::read_charging_state() { uint8_t status = 0; bool ret = readReg(registers::CTRL, (uint8_t *) &status, sizeof(status)); - return status; + // Failed reads must report a fault, never a healthy charger. + return ret ? status : 0xC0; } uint8_t BQ25120a::read_fault() { uint8_t status = 0; bool ret = readReg(registers::FAULT, (uint8_t *) &status, sizeof(status)); - return status; + // Failed reads must report a fault, never a healthy charger. + return ret ? status : 0xF0; } uint8_t BQ25120a::read_ts_fault() { uint8_t status = 0; bool ret = readReg(registers::TS_FAULT, (uint8_t *) &status, sizeof(status)); - return status; + // Failed reads must report a fault, never a healthy charger. + return ret ? status : 0xA0; } chrg_state BQ25120a::read_charging_control() { @@ -177,6 +229,8 @@ chrg_state BQ25120a::read_charging_control() { bool ret = readReg(registers::CHARGE_CTRL, (uint8_t *) &status, sizeof(status)); chrg_state chrg; + // Preserve disabled defaults when the register cannot be read. + if (!ret) return chrg; chrg.enabled = !(status & 0x2); chrg.high_impedance = status & 0x1; @@ -187,9 +241,9 @@ chrg_state BQ25120a::read_charging_control() { float mAh = (status & 0x7F) >> 2; if (status & (1 << 7)) { - mAh = 40 + mAh * 10; + mAh = MIN(40 + mAh * 10, 300); } else { - mAh += 5; + mAh = MIN(mAh + 5, 35); } chrg.mAh = mAh; @@ -197,11 +251,14 @@ chrg_state BQ25120a::read_charging_control() { return chrg; } - -uint8_t BQ25120a::write_charging_control(float mA) { +int BQ25120a::write_charging_control(float mA) { + // Reject invalid currents and failed reads before changing charge control. + ChargerLock lock; + if (!isfinite(mA) || mA < 5 || mA > 300) return -EINVAL; uint8_t status = 0; bool ret = readReg(registers::CHARGE_CTRL, &status, sizeof(status)); + if (!ret) return -EIO; status &= 0x3; if (mA >= 40) { @@ -210,53 +267,55 @@ uint8_t BQ25120a::write_charging_control(float mA) { status |= 1 << 7; } else { if (mA > 35) mA = 35; - status |= (((uint16_t)(mA - 5)) & 0x1F) << 2; + status |= (((uint16_t)(mA - 5 + EPS)) & 0x1F) << 2; } - writeReg(registers::CHARGE_CTRL, &status, sizeof(status)); - - return status; + return write_verified(registers::CHARGE_CTRL, status); } - -uint8_t BQ25120a::write_LS_control(bool enable) { +int BQ25120a::write_LS_control(bool enable) { + // Boot needs confirmed rail disable before changing its voltage. + ChargerLock lock; uint8_t status = 0; - readReg(registers::LS_LDO_CTRL, &status, sizeof(status)); + if (!readReg(registers::LS_LDO_CTRL, &status, sizeof(status))) return -EIO; uint8_t ls_bit = enable ? 1 : 0; status &= ~(1 << 7); status |= ls_bit << 7; - writeReg(registers::LS_LDO_CTRL, &status, sizeof(status)); - - return status; + return write_verified(registers::LS_LDO_CTRL, status, 0xFD); } -uint8_t BQ25120a::write_LDO_voltage_control(float volt) { +int BQ25120a::write_LDO_voltage_control(float volt) { + ChargerLock lock; uint8_t status = 0; if (volt > 10) volt /= 1000; - volt = CLAMP(volt, 0.8f, 3.3f); + // Invalid voltages must not silently become pass-through mode. + if (!isfinite(volt) || volt < 0.8f || volt > 3.3f) return -EINVAL; - readReg(registers::LS_LDO_CTRL, &status, sizeof(status)); + if (!readReg(registers::LS_LDO_CTRL, &status, sizeof(status))) return -EIO; + uint8_t previous = status; - //status |= (((uint16_t)((volt - 0.8) * 10)) & 0x1F) << 2; - status &= 1 << 7; + status &= 0x81; // Preserve enable and MR reset behavior. status |= ((uint8_t)((volt - 0.8f) * 10 + EPS)) << 2; - //status |= 1 << 7; - - writeReg(registers::LS_LDO_CTRL, &status, sizeof(status)); - - return status; + if ((previous & 0x7C) == (status & 0x7C)) return status; + // The IC ignores voltage changes while enabled. Report failure instead + // of interrupting a live rail; setup_boot disables both enable sources. + if (previous & (1 << 7)) return -EBUSY; + return write_verified(registers::LS_LDO_CTRL, status, 0xFD); } float BQ25120a::read_ldo_voltage() { uint8_t status = 0; bool ret = readReg(registers::LS_LDO_CTRL, (uint8_t *) &status, sizeof(status)); + // Pass-through is not a regulated voltage; failed reads are unknown. + if (!ret || ((status >> 2) & 0x1F) > 25) return NAN; + float voltage = 0.8f + ((status >> 2 & 0x1F)) * 0.1f; return voltage; @@ -266,24 +325,25 @@ float BQ25120a::read_battery_voltage_control() { uint8_t status = 0; bool ret = readReg(registers::BAT_VOL_CTRL, (uint8_t *) &status, sizeof(status)); + // A failed read is not a valid charge-voltage setting. + if (!ret) return NAN; + float voltage = 3.6f + (status >> 1) * 0.01f; - return voltage; + return MIN(voltage, 4.65f); } - -uint8_t BQ25120a::write_battery_voltage_control(float volt) { +int BQ25120a::write_battery_voltage_control(float volt) { uint8_t status = 0; if (volt > 10) volt /= 1000; - volt = CLAMP(volt, 3.6f, 4.65f); + // Refuse invalid settings rather than silently raising the cell limit. + if (!isfinite(volt) || volt < 3.6f || volt > 4.65f) return -EINVAL; status |= (((uint16_t)((volt - 3.6f) * 100 + EPS)) & 0x7F) << 1; - writeReg(registers::BAT_VOL_CTRL, &status, sizeof(status)); - - return status; + return write_verified(registers::BAT_VOL_CTRL, status); } chrg_state BQ25120a::read_termination_control() { @@ -292,7 +352,8 @@ chrg_state BQ25120a::read_termination_control() { struct chrg_state chrg; - // if (!ret) printk("failed to read\n"); + // Preserve disabled defaults on a failed read. + if (!ret) return chrg; chrg.enabled = status & 0x2; //chrg.high_impedance = status & 0x1; @@ -313,7 +374,9 @@ chrg_state BQ25120a::read_termination_control() { return chrg; } -uint8_t BQ25120a::write_termination_control(float mA, bool enable_termination) { +int BQ25120a::write_termination_control(float mA, bool enable_termination) { + // Underflow would encode an unsafe precharge/termination current. + if (!isfinite(mA) || mA < 0.5f || mA > 37) return -EINVAL; uint8_t status = 0; //bool ret = readReg(registers::TERM_CTRL, &status, sizeof(status)); @@ -321,20 +384,18 @@ uint8_t BQ25120a::write_termination_control(float mA, bool enable_termination) { if (mA >= 6) { if (mA > 37) mA = 37; - status |= (((uint16_t)(mA - 6)) & 0x1F) << 2; + status |= (((uint16_t)(mA - 6 + EPS)) & 0x1F) << 2; status |= 1 << 7; } else { if (mA > 5) mA = 5; - status |= (((uint16_t)(2 * (mA - 0.5))) & 0x1F) << 2; + status |= (((uint16_t)(2 * (mA - 0.5) + EPS)) & 0x1F) << 2; } if (enable_termination) { status |= 0x2; // enable termination } - writeReg(registers::TERM_CTRL, &status, sizeof(status)); - - return status; + return write_verified(registers::TERM_CTRL, status); } ilim_uvlo BQ25120a::read_uvlo_ilim() { @@ -343,7 +404,8 @@ ilim_uvlo BQ25120a::read_uvlo_ilim() { bool ret = readReg(registers::ILIM_UVLO, (uint8_t *) &status, sizeof(status)); - // if (!ret) printk("failed to read\n"); + // Do not report a successful cutoff setting after an I2C error. + if (!ret) return param; param.uvlo_v = CLAMP(3.0f- 0.2f * ((status & 0x7) - 2), 2.2, 3.0); param.lim_mA = 50.f + 50.f * ((status >> 3) & 0x7); @@ -351,66 +413,87 @@ ilim_uvlo BQ25120a::read_uvlo_ilim() { return param; } -uint8_t BQ25120a::write_uvlo_ilim(ilim_uvlo param) { - float mA = CLAMP(param.lim_mA, 50, 400); - float v = CLAMP(param.uvlo_v, 2.2, 3.0); +int BQ25120a::write_uvlo_ilim(ilim_uvlo param) { + // Reject invalid cutoff/current limits instead of wrapping their codes. + if (!isfinite(param.lim_mA) || param.lim_mA < 50 || param.lim_mA > 400 || + !isfinite(param.uvlo_v) || param.uvlo_v < 2.2f || param.uvlo_v > 3.0f) return -EINVAL; + float mA = param.lim_mA; + float v = param.uvlo_v; uint8_t status = 0; status |= ((uint16_t)(mA / 50 - 1) & 0x7) << 3; - status |= ((uint16_t)((3.0 - v) * 5 + 2) & 0x7); + status |= ((uint16_t)((3.0f - v) * 5 + EPS + 2) & 0x7); - writeReg(registers::ILIM_UVLO, &status, sizeof(status)); - - return status; + return write_verified(registers::ILIM_UVLO, status, 0x3F); } -void BQ25120a::setup_ts_control() { +int BQ25120a::setup_ts_control() { uint8_t ts_fault = 0; - writeReg(registers::TS_FAULT, (uint8_t *) &ts_fault, sizeof(ts_fault)); + // v2.7 has a fixed divider, not an NTC: verify TS stays disabled. + // EN_INT=0 also keeps the fault/button interrupt pulsed, not held low. + return write_verified(registers::TS_FAULT, ts_fault, 0x8F); } void BQ25120a::disable_ts() { - uint8_t ts_fault = read_ts_fault(); + // Do not write back a fabricated status after an I2C failure. + ChargerLock lock; + uint8_t ts_fault; + if (!readReg(registers::TS_FAULT, &ts_fault, sizeof(ts_fault))) return; ts_fault &= ~(1 << 7); - writeReg(registers::TS_FAULT, (uint8_t *) &ts_fault, sizeof(ts_fault)); + write_verified(registers::TS_FAULT, ts_fault, 0x8F); } bool BQ25120a::power_connected() { int pg = gpio_pin_get_dt(&pg_pin); - return pg; + return pg > 0; // GPIO errors must not be interpreted as USB power. } -void BQ25120a::enter_high_impedance() { - if (!power_connected()) gpio_pin_set_dt(&cd_pin, 0); +int BQ25120a::enter_high_impedance() { + ChargerLock lock; + // Always restore CD even if USB arrived while an I2C access had it high. + // Preserve an intentional charge inhibit whenever USB is present. + return set_cd(power_connected() && charge_disabled); } -void BQ25120a::exit_high_impedance() { - if (!power_connected()) { - gpio_pin_set_dt(&cd_pin, 1); - last_high_z = micros(); - } +int BQ25120a::exit_high_impedance() { + ChargerLock lock; + return set_cd(!power_connected() || charge_disabled); } -void BQ25120a::disable_charge() { - if (power_connected()) gpio_pin_set_dt(&cd_pin, 1); +int BQ25120a::disable_charge() { + ChargerLock lock; + charge_disabled = true; + return enter_high_impedance(); } -void BQ25120a::enable_charge() { - if (power_connected()) gpio_pin_set_dt(&cd_pin, 0); +int BQ25120a::enable_charge() { + ChargerLock lock; + if (!configured) return -EACCES; + int ret = set_cd(false); + if (ret) { + // A failed GPIO write must not authorize a later status read to drop + // CD. The manager still considers charging inhibited after this error, + // and battery conditions may become unsafe before the next poll. + charge_disabled = true; + (void)enter_high_impedance(); + return ret; + } + charge_disabled = false; + return 0; } - button_state BQ25120a::read_button_state() { struct button_state btn; uint8_t status = 0; bool ret = readReg(registers::BTN_CTRL, (uint8_t *) &status, sizeof(status)); - // if (!ret) printk("failed to read\n"); + // An unreadable register must not synthesize a button press. + if (!ret) return btn; btn.wake_1 = status & 0x2; btn.wake_2 = status & 0x1; @@ -426,4 +509,166 @@ int BQ25120a::set_power_connect_callback(gpio_callback_handler_t handler) { int BQ25120a::set_int_callback(gpio_callback_handler_t handler) { gpio_init_callback(&int_cb_data, handler, int_cb_data.pin_mask | BIT(int_pin.pin)); return gpio_add_callback(int_pin.port, &int_cb_data); -} \ No newline at end of file +} + +void BQ25120a::wait_for_i2c() { + const uint64_t now = micros(); + // Compare unsigned elapsed times before narrowing. Casting a long idle time + // to int could wrap and skip the mandatory inter-transaction quiet period. + const uint64_t bus_elapsed = now - last_i2c; + const uint64_t wake_elapsed = now - last_high_z; + uint32_t delay = bus_elapsed < BQ25120a_I2C_TIMEOUT_US ? + BQ25120a_I2C_TIMEOUT_US - bus_elapsed : 0; + if (wake_elapsed < BQ25120a_HIGH_Z_TIMEOUT_US) { + delay = MAX(delay, BQ25120a_HIGH_Z_TIMEOUT_US - wake_elapsed); + } + if (delay) k_usleep(delay); +} + +int BQ25120a::write_verified(uint8_t reg, uint8_t value, uint8_t mask) { + // An acknowledged write can still be ignored by the IC (notably LS/LDO). + ChargerLock lock; + uint8_t actual; + if (!writeReg(reg, &value, 1) || !readReg(reg, &actual, 1)) return -EIO; + if ((actual & mask) != (value & mask)) { + LOG_ERR("Charger register 0x%02x expected 0x%02x, read 0x%02x (mask 0x%02x)", + reg, value, actual, mask); + return -EIO; + } + return value; +} + +int BQ25120a::setup_boot(const battery_settings &settings) { + ChargerLock lock; + configured = false; + int ret = disable_charge(); + if (ret) return ret; + if (!device_is_ready(lsctrl_pin.port)) return -ENODEV; + // The PMIC ignores LDO voltage writes while either enable source is high. + // A CPU reset does not reset its registers, and the bootloader may have + // raised LSCTRL. Establish both off states before programming 3.3 V. + ret = gpio_pin_configure_dt(&lsctrl_pin, GPIO_OUTPUT_INACTIVE); + if (ret) return ret; + k_usleep(BQ25120a_HIGH_Z_TIMEOUT_US); + ret = exit_high_impedance(); + if (ret) return ret; + ret = write_LS_control(false); + int restore = enter_high_impedance(); + if (ret < 0) return ret; + if (restore) return restore; + return setup(settings); +} + +int BQ25120a::recover_charging(const battery_settings &settings) { + ChargerLock lock; + if (!power_connected()) return -ENODEV; + // A register reset does NOT clear TIMER. The documented recovery is a CD + // pulse; the power manager must bound attempts and check physical faults. + int ret = disable_charge(); + if (ret) return ret; + k_usleep(BQ25120a_HIGH_Z_TIMEOUT_US); + ret = setup(settings); + if (ret) return ret; + if (!power_connected()) return -ENODEV; + ret = set_cd(false); + if (ret) return ret; + k_usleep(BQ25120a_HIGH_Z_TIMEOUT_US); + uint8_t status; + bool ok = readReg(registers::CTRL, &status, 1); + int restore = disable_charge(); + if (!ok || (status & BIT(3))) return -EIO; + return restore; +} + +int BQ25120a::enter_ship_mode() { + ChargerLock lock; + if (power_connected()) return -EBUSY; + int ret = set_cd(true); + if (ret) return ret; + const uint8_t command = BIT(5); + if (!writeReg(registers::CTRL, &command, 1)) { + enter_high_impedance(); + return -EIO; + } + // Leave CD high. MR release completes ship entry after tQUIET; the host + // supply is then removed. Hi-Z would leave PMID/SYS and their loads powered. + k_usleep(BQ25120a_HIGH_Z_TIMEOUT_US); + return 0; +} + +bool BQ25120a::read_status(uint8_t &control, uint8_t &fault, uint8_t &ts_fault) { + // Publish only a complete read, and preserve intentional CD inhibition. + ChargerLock lock; + uint8_t values[3]; + if (exit_high_impedance()) return false; + bool ok = readReg(registers::CTRL, &values[0], 1) && + readReg(registers::FAULT, &values[1], 1) && + readReg(registers::TS_FAULT, &values[2], 1); + if (enter_high_impedance()) { + configured = false; + disable_charge(); + ok = false; + } + if (!ok) return false; + control = values[0]; + fault = values[1]; + ts_fault = values[2]; + return true; +} + +bool BQ25120a::configuration_valid(const battery_settings &settings, float current) { + // The PMIC watchdog can reset these registers while the CPU keeps running. + ChargerLock lock; + if (!configured) return false; + if (!isfinite(current) || current < 5 || current > 300) { + configured = false; + disable_charge(); + return false; + } + uint8_t term = settings.i_term >= 6 ? + ((uint8_t)(settings.i_term - 6 + EPS) << 2) | 0x80 : + (uint8_t)(2 * (MIN(settings.i_term, 5.0f) - 0.5f) + EPS) << 2; + term |= 0x02; + uint8_t voltage = (uint8_t)((settings.u_term - 3.6f) * 100 + EPS) << 1; + uint8_t limits = ((uint8_t)(settings.i_max / 50 - 1) << 3) | + ((uint8_t)((3.0f - settings.u_vlo) * 5 + EPS) + 2); + const struct { + uint8_t reg; + uint8_t value; + uint8_t mask; + } expected[] = { + {registers::TS_FAULT, 0, 0x8F}, + {registers::CHARGE_CTRL, encode_charge_current(current), 0xFF}, + {registers::TERM_CTRL, term, 0xFF}, + {registers::BAT_VOL_CTRL, voltage, 0xFF}, + {registers::LS_LDO_CTRL, 0x64, 0x7C}, + {registers::ILIM_UVLO, limits, 0x3F}, + {registers::TIMERS, 0x4A, 0xFE}, + }; + bool ok = exit_high_impedance() == 0; + for (const auto &item : expected) { + uint8_t actual; + if (!ok || !readReg(item.reg, &actual, 1) || + (actual & item.mask) != (item.value & item.mask)) { + ok = false; + break; + } + } + if (!ok) { + configured = false; + disable_charge(); + } + if (enter_high_impedance()) { + configured = false; + disable_charge(); + ok = false; + } + return ok; +} + +int BQ25120a::set_cd(bool high) { + // I2C needs 1 ms after CD wakes the battery-only device. + int ret = gpio_pin_set_dt(&cd_pin, high); + if (!ret && high) last_high_z = micros(); + return ret; +} diff --git a/src/Battery/BQ25120a.h b/src/Battery/BQ25120a.h index 38eb75f3..b15fb6de 100644 --- a/src/Battery/BQ25120a.h +++ b/src/Battery/BQ25120a.h @@ -42,7 +42,8 @@ class BQ25120a { BAT_VOL_CTRL = 0x05, LS_LDO_CTRL = 0x07, BTN_CTRL = 0x08, - ILIM_UVLO = 0x09 + ILIM_UVLO = 0x09, + TIMERS = 0x0B }; BQ25120a(TWIM * i2c); @@ -52,31 +53,42 @@ class BQ25120a { int reset(); - void setup_ts_control(); - - void setup(const battery_settings &_battery_settings); + int setup_ts_control(); + + // Configuration is verified with charging inhibited. The power manager must + // approve battery conditions and call enable_charge() afterwards. + int setup(const battery_settings &_battery_settings); + // Boot only: holds the peripheral 3.3 V rail off while repairing retained + // LS/LDO state. Call before starting any consumers of that rail. + int setup_boot(const battery_settings &_battery_settings); + int recover_charging(const battery_settings &_battery_settings); + int enter_ship_mode(); + bool read_status(uint8_t &control, uint8_t &fault, uint8_t &ts_fault); + // Detect watchdog/default-register drift without resetting the live rails. + // A mismatch or failed read inhibits charging until setup succeeds again. + bool configuration_valid(const battery_settings &settings, float current); bool power_connected(); - void enter_high_impedance(); - void exit_high_impedance(); - void disable_charge(); - void enable_charge(); + int enter_high_impedance(); + int exit_high_impedance(); + int disable_charge(); + int enable_charge(); uint8_t read_charging_state(); uint8_t read_fault(); uint8_t read_ts_fault(); chrg_state read_charging_control(); - uint8_t write_charging_control(float mA); + int write_charging_control(float mA); float read_battery_voltage_control(); - uint8_t write_battery_voltage_control(float volt); + int write_battery_voltage_control(float volt); struct chrg_state read_termination_control(); - uint8_t write_termination_control(float mA, bool enable_termination = true); + int write_termination_control(float mA, bool enable_termination = true); ilim_uvlo read_uvlo_ilim(); - uint8_t write_uvlo_ilim(ilim_uvlo param); + int write_uvlo_ilim(ilim_uvlo param); void disable_ts(); - uint8_t write_LDO_voltage_control(float volt); + int write_LDO_voltage_control(float volt); float read_ldo_voltage(); - uint8_t write_LS_control(bool enable); + int write_LS_control(bool enable); button_state read_button_state(); @@ -84,12 +96,17 @@ class BQ25120a { int set_int_callback(gpio_callback_handler_t handler); private: bool readReg(uint8_t reg, uint8_t * buffer, uint16_t len); - void writeReg(uint8_t reg, uint8_t * buffer, uint16_t len); + bool writeReg(uint8_t reg, const uint8_t * buffer, uint16_t len); + int write_verified(uint8_t reg, uint8_t value, uint8_t mask = 0xFF); + int set_cd(bool high); + void wait_for_i2c(); const int address = DT_REG_ADDR(DT_NODELABEL(bq25120a)); - uint64_t last_i2c; - uint64_t last_high_z; + uint64_t last_i2c = 0; + uint64_t last_high_z = 0; + bool charge_disabled = true; + bool configured = false; TWIM *_i2c; @@ -99,6 +116,8 @@ class BQ25120a { const struct gpio_dt_spec pg_pin = GPIO_DT_SPEC_GET(DT_NODELABEL(bq25120a), pg_gpios); const struct gpio_dt_spec cd_pin = GPIO_DT_SPEC_GET(DT_NODELABEL(bq25120a), cd_gpios); const struct gpio_dt_spec int_pin = GPIO_DT_SPEC_GET(DT_NODELABEL(bq25120a), int_gpios); + const struct gpio_dt_spec lsctrl_pin = + GPIO_DT_SPEC_GET(DT_CHILD(DT_NODELABEL(bq25120a), load_switch), enable_gpios); /*const struct gpio_dt_spec pg_pin = GPIO_DT_SPEC_GET_OR(DT_NODELABEL(bq25120a), pg_gpios, {0}); const struct gpio_dt_spec cd_pin = GPIO_DT_SPEC_GET_OR(DT_NODELABEL(bq25120a), cd_gpios, {0}); @@ -107,4 +126,4 @@ class BQ25120a { extern BQ25120a battery_controller; -#endif \ No newline at end of file +#endif diff --git a/src/Battery/BQ27220.cpp b/src/Battery/BQ27220.cpp index 715f22c1..8bd7d47e 100644 --- a/src/Battery/BQ27220.cpp +++ b/src/Battery/BQ27220.cpp @@ -1,10 +1,45 @@ #include "BQ27220.h" #include "openearable_common.h" +#include #include LOG_MODULE_REGISTER(bq27220, LOG_LEVEL_DBG); +namespace { + +// The gauge shares its RAM command window across transfers; lock whole operations. +K_MUTEX_DEFINE(gauge_mutex); + +class GaugeLock { +public: + GaugeLock() { k_mutex_lock(&gauge_mutex, K_FOREVER); } + ~GaugeLock() { k_mutex_unlock(&gauge_mutex); } +}; + +// Share the flag mapping with the checked safety snapshot. +bat_status decode_battery_status(uint16_t flags) { + bat_status status; + status.DSG = flags & (1 << 0); + status.SYSDWN = flags & (1 << 1); + status.TDA = flags & (1 << 2); + status.BATTPRES = flags & (1 << 3); + status.AUTH_GD = flags & (1 << 4); + status.OCVGD = flags & (1 << 5); + status.TCA = flags & (1 << 6); + status.CHGINH = flags & (1 << 8); + status.FC = flags & (1 << 9); + status.OTD = flags & (1 << 10); + status.OTC = flags & (1 << 11); + status.SLEEP = flags & (1 << 12); + status.OCVFAIL = flags & (1 << 13); + status.OCVCOMP = flags & (1 << 14); + status.FD = flags & (1 << 15); + return status; +} + +} + BQ27220 fuel_gauge(&I2C1); BQ27220::BQ27220(TWIM * i2c) : _i2c(i2c) { @@ -57,67 +92,81 @@ int BQ27220::set_wakeup_int() { return 0; } -bool BQ27220::readReg(uint8_t reg, uint8_t * buffer, uint16_t len) { - int ret; - uint64_t now = micros(); - int delay = MIN(BQ27220_I2C_TIMEOUT_US - (int)(now - last_i2c), BQ27220_I2C_TIMEOUT_US); - - if (delay > 0) k_usleep(delay); +int BQ27220::disable_wakeup_int() { + // Battery warnings must not repeatedly wake an intentionally off device. + return gpio_pin_interrupt_configure_dt(&gpout_pin, GPIO_INT_DISABLE); +} +bool BQ27220::readReg(uint8_t reg, uint8_t * buffer, uint16_t len) { + // Hold the locks through the quiet interval so another caller cannot shorten it. + GaugeLock lock; _i2c->aquire(); + int ret; + uint64_t elapsed = micros() - last_i2c; + if (elapsed < BQ27220_I2C_TIMEOUT_US) k_usleep(BQ27220_I2C_TIMEOUT_US - elapsed); ret = i2c_burst_read(_i2c->master, address, reg, buffer, len); if (ret) LOG_WRN("I2C read failed: %d\n", ret); - _i2c->release(); - last_i2c = micros(); + _i2c->release(); return (ret == 0); } -void BQ27220::writeReg(uint8_t reg, uint8_t *buffer, uint16_t len) { - int ret; - uint64_t now = micros(); - int delay = MIN(BQ27220_I2C_TIMEOUT_US - (int)(now - last_i2c), BQ27220_I2C_TIMEOUT_US); - - if (delay > 0) k_usleep(delay); - +bool BQ27220::writeReg(uint8_t reg, uint8_t *buffer, uint16_t len) { + // Writes need the same transaction lock and quiet interval as reads. + GaugeLock lock; _i2c->aquire(); + int ret; + uint64_t elapsed = micros() - last_i2c; + if (elapsed < BQ27220_I2C_TIMEOUT_US) k_usleep(BQ27220_I2C_TIMEOUT_US - elapsed); ret = i2c_burst_write(_i2c->master, address, reg, buffer, len); if (ret) LOG_WRN("I2C write failed: %d", ret); - _i2c->release(); - last_i2c = micros(); + _i2c->release(); + return ret == 0; } bat_status BQ27220::battery_status() { - bat_status status; uint16_t val = 0; - bool ret = readReg(registers::FLAGS, (uint8_t *) &val, sizeof(val)); - - status.DSG = val & 0x1; - status.SYSDWN = val & (1 << 1); - status.TDA = val & (1 << 2); - status.BATTPRES = val & (1 << 3); - status.AUTH_GD = val & (1 << 4); - status.OCVGD = val & (1 << 5); - status.TCA = val & (1 << 6); - status.CHGINH = val & (1 << 8); - status.FC = val & (1 << 9); - status.OTD = val & (1 << 10); - status.OTC = val & (1 << 11); - status.SLEEP = val & (1 << 12); - status.OCVFAIL = val & (1 << 13); - status.OCVCOMP = val & (1 << 14); - status.FD = val & (1 << 15); + readReg(registers::FLAGS, (uint8_t *) &val, sizeof(val)); + return decode_battery_status(val); +} + +bool BQ27220::read_safety_status(bat_status &status, float &voltage, float &temperature) { + GaugeLock lock; + uint8_t operation[2]; + uint8_t flags[2]; + uint8_t millivolts[2]; + uint8_t decikelvin[2]; + + // A failed configuration exit can leave measurements frozen. Successful + // transport alone does not make those values suitable for charging. + if (!readReg(registers::OP_STAT, operation, sizeof(operation)) || + ((operation[0] | (uint16_t(operation[1]) << 8)) & (1 << 10))) { + return false; + } - return status; + // Never turn a failed read into a plausible value (notably 0 V or clear + // alarm flags). Commit all outputs only after every read succeeds. + if (!readReg(registers::FLAGS, flags, sizeof(flags)) || + !readReg(registers::VOLT, millivolts, sizeof(millivolts)) || + !readReg(registers::TEMP, decikelvin, sizeof(decikelvin))) { + return false; + } + + status = decode_battery_status(flags[0] | (uint16_t(flags[1]) << 8)); + voltage = (millivolts[0] | (uint16_t(millivolts[1]) << 8)) / 1000.0f; + temperature = (decikelvin[0] | (uint16_t(decikelvin[1]) << 8)) / 10.0f - 273.15f; + return true; } gauge_status BQ27220::gauging_state() { + // Keep command selection and its response together, including during setup. + GaugeLock lock; gauge_status status; uint16_t state; @@ -240,19 +289,21 @@ op_state BQ27220::operation_state() { return state; } -void BQ27220::write_command(BQ27220::commands cmd) { - writeReg(registers::CTRL, (uint8_t *) &cmd, sizeof(cmd)); +bool BQ27220::write_command(BQ27220::commands cmd) { + // Propagate command failures so setup cannot report a partial profile as ready. + return writeReg(registers::CTRL, (uint8_t *) &cmd, sizeof(cmd)); } -void BQ27220::write_command(uint16_t cmd) { - writeReg(registers::CTRL, (uint8_t *) &cmd, sizeof(cmd)); +bool BQ27220::write_command(uint16_t cmd) { + return writeReg(registers::CTRL, (uint8_t *) &cmd, sizeof(cmd)); } -void BQ27220::full_access() { - write_command(0xffff); +bool BQ27220::full_access() { + if (!write_command(0xffff)) return false; k_msleep(100); - write_command(0xffff); + if (!write_command(0xffff)) return false; k_msleep(100); + return true; } /*void BQ27220::sleep_mode() { @@ -263,66 +314,60 @@ void BQ27220::active_mode() { }*/ -void BQ27220::enter_config_update() { - //write_command(0x14); //0x13); - op_state state = operation_state(); - if (state.CFG_UPDATE) { - LOG_WRN("Already in CONFIG UPDATE MODE."); - return; - } - write_command(CONFIG_UPDATE_ENTER); +int BQ27220::enter_config_update() { + // A missing gauge or rejected command must not trap boot before charging starts. + GaugeLock lock; + uint16_t status = 0; + if (!readReg(registers::OP_STAT, (uint8_t *)&status, sizeof(status))) return -EIO; + if (status & (1 << 10)) return 0; + if (!write_command(CONFIG_UPDATE_ENTER)) return -EIO; k_msleep(1100); - do { - state = operation_state(); + for (int attempt = 0; attempt < 30; ++attempt) { + if (!readReg(registers::OP_STAT, (uint8_t *)&status, sizeof(status))) return -EIO; + if (status & (1 << 10)) return 0; k_msleep(100); - } - while (!state.CFG_UPDATE); - LOG_INF("CONFIG UPDATE MODE entered."); - - //if (state.CFG_UPDATE) printk("CONFIG UPDATE MODE entered.\n"); - //else printk("Failed to enter CONFIG UPDATE MODE.\n"); + } + LOG_ERR("Timed out entering fuel gauge configuration"); + return -ETIMEDOUT; } -void BQ27220::exit_config_update(bool init) { - op_state state = operation_state(); - if (!state.CFG_UPDATE) { - LOG_WRN("Device is not in CONFIG UPDATE MODE."); - return; - } - if (init) write_command(CONFIG_UPDATE_EXIT); - else write_command(CONFIG_UPDATE_EXIT_NO_INIT); +int BQ27220::exit_config_update(bool init) { + // Bound exit too: measurements stay frozen while configuration is active. + GaugeLock lock; + uint16_t status = 0; + if (!readReg(registers::OP_STAT, (uint8_t *)&status, sizeof(status))) return -EIO; + if (!(status & (1 << 10))) return 0; + if (!write_command(init ? CONFIG_UPDATE_EXIT : CONFIG_UPDATE_EXIT_NO_INIT)) return -EIO; k_msleep(1100); - do { - state = operation_state(); + for (int attempt = 0; attempt < 30; ++attempt) { + if (!readReg(registers::OP_STAT, (uint8_t *)&status, sizeof(status))) return -EIO; + if (!(status & (1 << 10))) return 0; k_msleep(100); - } while (state.CFG_UPDATE); - LOG_INF("CONFIG UPDATE MODE exited."); - //if (!state.CFG_UPDATE) printk("CONFIG UPDATE MODE exited.\n"); - //else printk("Failed to exit CONFIG UPDATE MODE.\n"); + } + LOG_ERR("Timed out exiting fuel gauge configuration"); + return -ETIMEDOUT; } -void BQ27220::read_RAM(uint16_t ram_address, uint8_t * data, int len) { - bool ret; - - writeReg(0x3E, (uint8_t *) &ram_address, sizeof(ram_address)); +bool BQ27220::read_RAM(uint16_t ram_address, uint8_t * data, int len) { + // A failed address selection must not be mistaken for a successful readback. + if (!writeReg(0x3E, (uint8_t *) &ram_address, sizeof(ram_address))) return false; k_usleep(BQ27220_RAM_TIMEOUT_US); - ret = readReg(0x40, data, len); + return readReg(0x40, data, len); } int BQ27220::write_RAM(uint16_t ram_address, uint8_t * data, int len, bool check) { + // Stop on a failed transfer before using stale checksum data or committing it. uint8_t check_sum=0; uint8_t data_len=0; uint8_t buf[len]; - bool ret; - - writeReg(0x3E, (uint8_t *) &ram_address, sizeof(ram_address)); + if (!writeReg(0x3E, (uint8_t *) &ram_address, sizeof(ram_address))) return -EIO; k_usleep(BQ27220_RAM_TIMEOUT_US); - ret = readReg(0x61, (uint8_t *) &data_len, sizeof(data_len)); - ret = readReg(0x40, buf, len); - ret = readReg(0x60, (uint8_t *) &check_sum, sizeof(check_sum)); + if (!readReg(0x61, (uint8_t *) &data_len, sizeof(data_len)) || + !readReg(0x40, buf, len) || + !readReg(0x60, (uint8_t *) &check_sum, sizeof(check_sum))) return -EIO; uint8_t my_check = (uint8_t)0xFF-check_sum; // - data[0] - data[1]; @@ -333,21 +378,26 @@ int BQ27220::write_RAM(uint16_t ram_address, uint8_t * data, int len, bool check my_check = (uint8_t)0xFF - my_check; - writeReg(0x40, (uint8_t *) data, len); - writeReg(0x60, (uint8_t *) &my_check, sizeof(my_check)); - writeReg(0x61, (uint8_t *) &data_len, sizeof(data_len)); + if (!writeReg(0x40, (uint8_t *) data, len) || + !writeReg(0x60, (uint8_t *) &my_check, sizeof(my_check)) || + !writeReg(0x61, (uint8_t *) &data_len, sizeof(data_len))) return -EIO; k_usleep(BQ27220_RAM_TIMEOUT_US); if (check) { - uint8_t * read_buff = (uint8_t *) k_malloc(data_len); + // Compare only the caller's data, not the gauge's larger RAM block. + uint8_t * read_buff = (uint8_t *) k_malloc(len); + if (!read_buff) return -ENOMEM; - read_RAM(ram_address, read_buff, data_len); + if (!read_RAM(ram_address, read_buff, len)) { + k_free(read_buff); + return -EIO; + } - for (int i = 0; i < data_len; i++) { + for (int i = 0; i < len; i++) { if (read_buff[i] != data[i]) { k_free(read_buff); - return -1; + return -EIO; } } @@ -366,19 +416,23 @@ int BQ27220::write_RAM(uint16_t ram_address, uint16_t val, bool check) { return write_RAM(ram_address, data, sizeof(val), check); } -void BQ27220::setup(const battery_settings &_battery_settings, bool init) { +int BQ27220::setup(const battery_settings &_battery_settings, bool init) { + // Keep polling and BLE reads out of the shared configuration window. + GaugeLock lock; int ret; // unseal - write_command(0x0414); + if (!write_command(0x0414)) return -EIO; k_msleep(100); - write_command(0x3672); + if (!write_command(0x3672)) return -EIO; k_msleep(100); // full access - full_access(); + if (!full_access()) return -EIO; - enter_config_update(); + ret = enter_config_update(); + if (ret) return ret; + // Preserve the first write failure, then still exit configuration and seal. //k_usleep(1000); //hibernate off (not supported by fuel gauge) @@ -386,28 +440,28 @@ void BQ27220::setup(const battery_settings &_battery_settings, bool init) { // design and full charge capacity ret = write_RAM(0x929F, _battery_settings.capacity); - ret = write_RAM(0x929D, _battery_settings.capacity); //130 + if (!ret) ret = write_RAM(0x929D, _battery_settings.capacity); //130 // near full - ret = write_RAM(0x926B, 5); + if (!ret) ret = write_RAM(0x926B, 5); - ret = write_RAM(0x91F5, _battery_settings.temp_min * 10); - ret = write_RAM(0x91F7, _battery_settings.temp_max * 10); + if (!ret) ret = write_RAM(0x91F5, _battery_settings.temp_min * 10); + if (!ret) ret = write_RAM(0x91F7, _battery_settings.temp_max * 10); // charge current - ret = write_RAM(0x91FB, _battery_settings.i_charge); + if (!ret) ret = write_RAM(0x91FB, _battery_settings.i_charge); // charge voltage - ret = write_RAM(0x91FD, _battery_settings.u_term * 1000); + if (!ret) ret = write_RAM(0x91FD, _battery_settings.u_term * 1000); // taper current - ret = write_RAM(0x9201, _battery_settings.i_term); + if (!ret) ret = write_RAM(0x9201, _battery_settings.i_term); // experimental: min taper capacity - ret = write_RAM(0x9203, 4); // standard: 25 + if (!ret) ret = write_RAM(0x9203, 4); // standard: 25 // deadband uint8_t val = 1; - ret = write_RAM(0x91DE, &val, sizeof(uint8_t)); + if (!ret) ret = write_RAM(0x91DE, &val, sizeof(uint8_t)); // deadband CC (verursacht Probleme, rm zählt zu schnell?) /*val = 5; @@ -415,14 +469,14 @@ void BQ27220::setup(const battery_settings &_battery_settings, bool init) { */ // sleep current - ret = write_RAM(0x9217, 1); + if (!ret) ret = write_RAM(0x9217, 1); // dischage current trd - ret = write_RAM(0x9228, 2); + if (!ret) ret = write_RAM(0x9228, 2); // charge current trd - ret = write_RAM(0x922A, 2); + if (!ret) ret = write_RAM(0x922A, 2); // quit current - ret = write_RAM(0x922C, 1); + if (!ret) ret = write_RAM(0x922C, 1); //dod 0%: 4287 //dod 10%: 4125 @@ -444,29 +498,29 @@ void BQ27220::setup(const battery_settings &_battery_settings, bool init) { //dod: 103.25%: 3089 // sysDown set Voltage - ret = write_RAM(0x9240, _battery_settings.u_vlo * 1000 + CONFIG_BATTERY_SYSDOWN_SET_OFFSET); + if (!ret) ret = write_RAM(0x9240, _battery_settings.u_vlo * 1000 + CONFIG_BATTERY_SYSDOWN_SET_OFFSET); // sysDown clear Voltage - ret = write_RAM(0x9243, _battery_settings.u_vlo * 1000 + CONFIG_BATTERY_SYSDOWN_SET_OFFSET + CONFIG_BATTERY_SYSDOWN_HYSTERESIS); + if (!ret) ret = write_RAM(0x9243, _battery_settings.u_vlo * 1000 + CONFIG_BATTERY_SYSDOWN_SET_OFFSET + CONFIG_BATTERY_SYSDOWN_HYSTERESIS); // FD set - ret = write_RAM(0x9282, _battery_settings.u_vlo * 1000 + CONFIG_BATTERY_FD_SET_OFFSET); + if (!ret) ret = write_RAM(0x9282, _battery_settings.u_vlo * 1000 + CONFIG_BATTERY_FD_SET_OFFSET); // FD clear - ret = write_RAM(0x9284, _battery_settings.u_vlo * 1000 + CONFIG_BATTERY_FD_SET_OFFSET + CONFIG_BATTERY_FD_HYSTERESIS); + if (!ret) ret = write_RAM(0x9284, _battery_settings.u_vlo * 1000 + CONFIG_BATTERY_FD_SET_OFFSET + CONFIG_BATTERY_FD_HYSTERESIS); // FC Voltage - ret = write_RAM(0x9288, _battery_settings.u_term * 1000 - CONFIG_BATTERY_FC_VOLTAGE_OFFSET); + if (!ret) ret = write_RAM(0x9288, _battery_settings.u_term * 1000 - CONFIG_BATTERY_FC_VOLTAGE_OFFSET); // Electonic Load in 3µA steps - ret = write_RAM(0x9269, 6); // 18 µA + if (!ret) ret = write_RAM(0x9269, 6); // 18 µA // EMF //write_RAM(0x92A7, 36001); //C0 - ret = write_RAM(0x92A9, 480); //bat1:250 + if (!ret) ret = write_RAM(0x92A9, 480); //bat1:250 //R0 - ret = write_RAM(0x92AB, 19941); //bat1: 19941 //22542 //new bat: 17340 + if (!ret) ret = write_RAM(0x92AB, 19941); //bat1: 19941 //22542 //new bat: 17340 //R1 //write_RAM(0x92AF, 3160); @@ -478,24 +532,27 @@ void BQ27220::setup(const battery_settings &_battery_settings, bool init) { // do not use, only on CT makes sense: // SOC Flag, enable FC voltage detection uint8_t flags_b = 0x8C; - ret = write_RAM(0x9281, &flags_b, sizeof(flags_b)); + if (!ret) ret = write_RAM(0x9281, &flags_b, sizeof(flags_b)); // Overload current - ret = write_RAM(0x9264, _battery_settings.i_max); + if (!ret) ret = write_RAM(0x9264, _battery_settings.i_max); // CEDV Smoothing Config uint8_t cedv_conf = 0x0D; //Default: 0x08, Enable SMEXT, SMEN 0x0D - ret = write_RAM(0x9271, &cedv_conf, sizeof(cedv_conf)); + if (!ret) ret = write_RAM(0x9271, &cedv_conf, sizeof(cedv_conf)); - ret = write_RAM(0x9272, 3700); + if (!ret) ret = write_RAM(0x9272, 3700); - exit_config_update(init); + int exit_result = exit_config_update(init); // put fuel gauge to sealed state - write_command(SEAL); + bool sealed = write_command(SEAL); + if (ret) return ret; + if (exit_result) return exit_result; + return sealed ? 0 : -EIO; } int BQ27220::set_int_callback(gpio_callback_handler_t handler) { gpio_init_callback(&int_cb_data, handler, int_cb_data.pin_mask | BIT(gpout_pin.pin)); return gpio_add_callback(gpout_pin.port, &int_cb_data); -} \ No newline at end of file +} diff --git a/src/Battery/BQ27220.h b/src/Battery/BQ27220.h index 2b97c6a2..8003405b 100644 --- a/src/Battery/BQ27220.h +++ b/src/Battery/BQ27220.h @@ -118,6 +118,11 @@ class BQ27220 { int begin(); bat_status battery_status(); + /** Read the inputs used for battery safety decisions. + * Returns false on an I2C failure or while configuration mode is active; + * outputs are unchanged on failure. + */ + bool read_safety_status(bat_status &status, float &voltage, float &temperature); float temperature(); float voltage(); float capacity(); @@ -134,14 +139,16 @@ class BQ27220 { float standby_current(); op_state operation_state(); gauge_status gauging_state(); - void write_command(commands command); - void enter_config_update(); - void exit_config_update(bool init = true); + // Report configuration failures so charging stays inhibited until setup succeeds. + bool write_command(commands command); + int enter_config_update(); + int exit_config_update(bool init = true); - void full_access(); - void setup(const battery_settings &_battery_settings, bool init = true); + bool full_access(); + int setup(const battery_settings &_battery_settings, bool init = true); int set_wakeup_int(); + int disable_wakeup_int(); //void sleep_mode(); //void active_mode(); @@ -149,13 +156,13 @@ class BQ27220 { int set_int_callback(gpio_callback_handler_t handler); private: bool readReg(uint8_t reg, uint8_t * buffer, uint16_t len); - void writeReg(uint8_t reg, uint8_t * buffer, uint16_t len); + bool writeReg(uint8_t reg, uint8_t * buffer, uint16_t len); - void read_RAM(uint16_t ram_address, uint8_t * data, int len); + bool read_RAM(uint16_t ram_address, uint8_t * data, int len); int write_RAM(uint16_t ram_address, uint8_t * data, int len, bool check = false); int write_RAM(uint16_t ram_address, uint16_t val, bool check = false); - void write_command(uint16_t cmd); + bool write_command(uint16_t cmd); int address = DT_REG_ADDR(DT_NODELABEL(bq27220)); @@ -173,4 +180,4 @@ extern BQ27220 fuel_gauge; //extern BQ27220 battery_gauge; -#endif \ No newline at end of file +#endif diff --git a/src/Battery/BootState.h b/src/Battery/BootState.h index 1ba11992..a436bfc2 100644 --- a/src/Battery/BootState.h +++ b/src/Battery/BootState.h @@ -3,6 +3,33 @@ #include "openearable_common.h" +/* GPREGRET[1] belongs to battery management; GPREGRET[0] remains for DFU. + * Consume only the charge-only request at boot. Safety bits survive resets + * until USB absence is observed, so reboot cannot renew the charger timer. + */ +#define OE_BOOT_TAG 0xC8 +#define OE_BOOT_TAG_MASK 0xF8 +#define OE_BOOT_FLAG_CHARGE_ONLY 0x01 +#define OE_BOOT_FLAG_TIMER_USED 0x02 +#define OE_BOOT_FLAG_CHARGER_FAULT 0x04 +#define OE_BOOT_SAFETY_FLAGS (OE_BOOT_FLAG_TIMER_USED | OE_BOOT_FLAG_CHARGER_FAULT) +#define OE_BOOT_CHARGE_ONLY (OE_BOOT_TAG | OE_BOOT_FLAG_CHARGE_ONLY) + +static inline uint8_t oe_boot_flags(uint32_t retained) +{ + return (retained & OE_BOOT_TAG_MASK) == OE_BOOT_TAG ? retained & 0x07 : 0; +} + +static inline uint8_t oe_boot_encode(uint8_t flags) +{ + return (flags & 0x07) ? OE_BOOT_TAG | (flags & 0x07) : 0; +} + +static inline uint8_t oe_boot_request_charge_only(uint32_t retained) +{ + return oe_boot_encode(oe_boot_flags(retained) | OE_BOOT_FLAG_CHARGE_ONLY); +} + #ifdef __cplusplus extern "C" { #endif @@ -13,4 +40,4 @@ extern struct boot_state oe_boot_state; } #endif -#endif \ No newline at end of file +#endif diff --git a/src/Battery/Kconfig b/src/Battery/Kconfig index b0939acd..f6877f56 100644 --- a/src/Battery/Kconfig +++ b/src/Battery/Kconfig @@ -83,12 +83,16 @@ config BATTERY_CHARGE_CONTROLLER_INTERVAL_SECONDS config BATTERY_CHARGE_CONTROLLER_NORMAL_INTERVAL_SECONDS int "Normal Charge Controller Check Interval (seconds)" + # Keep polling comfortably within the charger's 50-second watchdog period. + range 1 20 default 10 help Time interval between charge controller status checks in normal operation config BATTERY_CHARGE_CONTROLLER_FAST_INTERVAL_SECONDS int "Fast Charge Controller Check Interval (seconds)" + # Fault recovery must keep servicing the charger watchdog too. + range 1 20 default 1 help Time interval between charge controller status checks when in FAULT or POWER_CONNECTED state diff --git a/src/Battery/PowerManager.cpp b/src/Battery/PowerManager.cpp index 628739da..c4567a1c 100644 --- a/src/Battery/PowerManager.cpp +++ b/src/Battery/PowerManager.cpp @@ -19,6 +19,11 @@ #endif #include +// Keep the network core off during charge-only recovery and final shutdown. +#include +#include +// Stop audio before removing its power supply. +#include "../audio/audio_datapath.h" #include "../drivers/LED_Controller/KTD2026.h" #include "../drivers/ADAU1860.h" @@ -35,240 +40,80 @@ #include LOG_MODULE_REGISTER(power_manager, LOG_LEVEL_DBG); -//K_TIMER_DEFINE(PowerManager::charge_timer, PowerManager::charge_timer_handler, NULL); - +// Charger servicing must not wait behind Bluetooth, storage, or sensor work. +K_THREAD_STACK_DEFINE(battery_work_stack, 4096); +static struct k_work_q battery_work_queue; K_WORK_DELAYABLE_DEFINE(PowerManager::charge_ctrl_delayable, PowerManager::charge_ctrl_work_handler); - K_WORK_DELAYABLE_DEFINE(PowerManager::power_down_work, PowerManager::power_down_work_handler); - -//K_WORK_DEFINE(PowerManager::power_down_work, PowerManager::power_down_work_handler); -//K_WORK_DEFINE(PowerManager::charge_ctrl_work, PowerManager::charge_ctrl_work_handler); K_WORK_DEFINE(PowerManager::fuel_gauge_work, PowerManager::fuel_gauge_work_handler); K_WORK_DEFINE(PowerManager::battery_controller_work, PowerManager::battery_controller_work_handler); ZBUS_CHAN_DEFINE(battery_chan, struct battery_data, NULL, NULL, ZBUS_OBSERVERS_EMPTY, ZBUS_MSG_INIT(0)); -static struct battery_data msg; - -//LoadSwitch PowerManager::v1_8_switch(GPIO_DT_SPEC_GET(DT_NODELABEL(load_switch), gpios)); - -void PowerManager::fuel_gauge_callback(const struct device *dev, struct gpio_callback *cb, uint32_t pins) { - LOG_DBG("Fuel Gauge GPOUT Interrupt"); - k_work_submit(&fuel_gauge_work); -} - -void PowerManager::battery_controller_callback(const struct device *dev, struct gpio_callback *cb, uint32_t pins) { - k_work_submit(&battery_controller_work); +void PowerManager::fuel_gauge_callback(const struct device *, struct gpio_callback *, uint32_t) { + if (!power_manager.stopping) { + k_work_submit_to_queue(&battery_work_queue, &fuel_gauge_work); + } } -void PowerManager::power_good_callback(const struct device *dev, struct gpio_callback *cb, uint32_t pins) { - bool power_good = battery_controller.power_connected(); - - k_work_submit(&fuel_gauge_work); - - if (power_good) { - power_manager.last_charging_state = 0; - k_work_schedule(&charge_ctrl_delayable, K_NO_WAIT); - } else { - k_work_cancel_delayable(&charge_ctrl_delayable); - if (!power_manager.power_on) k_work_reschedule(&power_manager.power_down_work, K_NO_WAIT); +void PowerManager::battery_controller_callback(const struct device *, struct gpio_callback *, uint32_t) { + if (!power_manager.stopping) { + k_work_submit_to_queue(&battery_work_queue, &battery_controller_work); } } -void PowerManager::power_down_work_handler(struct k_work * work) { - power_manager.power_down(); +void PowerManager::power_good_callback(const struct device *, struct gpio_callback *, uint32_t) { + if (!power_manager.stopping) { + // Both edges matter: unplugging must keep low-battery monitoring alive. + k_work_reschedule_for_queue(&battery_work_queue, &charge_ctrl_delayable, K_NO_WAIT); + } } -void PowerManager::charge_ctrl_work_handler(struct k_work * work) { - power_manager.charge_task(); - // Schedule next execution - k_work_schedule(&charge_ctrl_delayable, power_manager.chrg_interval); +void PowerManager::power_down_work_handler(struct k_work *) { + power_manager.finish_power_down(); } -void PowerManager::battery_controller_work_handler(struct k_work * work) { - button_state state; - - //uint8_t val = gpio_pin_get_dt(&power_manager.error_led); - //gpio_pin_set_dt(&power_manager.error_led, 1 - val); - - battery_controller.exit_high_impedance(); - state = battery_controller.read_button_state(); - battery_controller.enter_high_impedance(); - - if (state.wake_2) { - power_manager.power_on = !power_manager.power_on; - //LOG_INF("Power on: %i", power_manager.power_on); - - if (!power_manager.power_on) power_manager.power_down(); +void PowerManager::charge_ctrl_work_handler(struct k_work *) { + if (power_manager.stopping) return; + power_manager.charge_task(); + if (!power_manager.stopping) { + k_work_schedule_for_queue(&battery_work_queue, &charge_ctrl_delayable, + power_manager.chrg_interval); } - } -void PowerManager::fuel_gauge_work_handler(struct k_work * work) { - int ret; - battery_level_status status; - - msg.battery_level = fuel_gauge.state_of_charge(); - - bat_status bat = fuel_gauge.battery_status(); - - power_manager.get_battery_status(status); - - // full discharge - //if (bat.FD) k_work_reschedule(&power_manager.power_down_work, K_NO_WAIT); - if (power_manager.power_on && bat.SYSDWN) { - LOG_WRN("Battery reached system down voltage."); - k_work_reschedule(&power_manager.power_down_work, K_NO_WAIT); - } - - if (bat.CHGINH) { - power_manager.charging_disabled = true; - battery_controller.disable_charge(); - } else if (power_manager.charging_disabled) { - battery_controller.enable_charge(); - } - - float current; - float target_current; - float voltage; - +void PowerManager::battery_controller_work_handler(struct k_work *) { + if (power_manager.stopping) return; battery_controller.exit_high_impedance(); - - uint16_t charging_state = battery_controller.read_charging_state() >> 6; - gauge_status gs; - - switch (charging_state) { - case 0: - LOG_INF("charging state: discharge"); - msg.charging_state = DISCHARGING; - - gs = fuel_gauge.gauging_state(); - - if (gs.edv2) { - #ifdef CONFIG_BATTERY_ENABLE_LOW_STATE - msg.charging_state = BATTERY_LOW; - #endif - } - if (gs.edv1) { - msg.charging_state = BATTERY_CRITICAL; - } - break; - case 1: - LOG_INF("charging state: charging"); - - if (bat.SYSDWN) { - msg.charging_state = PRECHARGING; - break; - } - - current = fuel_gauge.current(); - target_current = fuel_gauge.charge_current(); - voltage = fuel_gauge.voltage(); - - msg.charging_state = POWER_CONNECTED; - - LOG_DBG("Voltage: %.3f V", voltage); - LOG_DBG("Charging current: %.3f mA", current); - LOG_DBG("Target current: %.3f mA", target_current); - LOG_DBG("State of charge: %.3f %%", fuel_gauge.state_of_charge()); - - // check if target current is met (if not tapering) - if (current > 0.8 * target_current - 2 * power_manager._battery_settings.i_term) { - msg.charging_state = CHARGING; - } - else if (voltage > power_manager._battery_settings.u_term - 0.02) { - #ifdef CONFIG_BATTERY_ENABLE_TRICKLE_CHARGE - msg.charging_state = TRICKLE_CHARGING; - #else - msg.charging_state = CHARGING; - #endif - } - - break; - case 2: - LOG_INF("charging state: done"); - msg.charging_state = FULLY_CHARGED; - break; - case 3: - LOG_WRN("charging state: fault"); - msg.charging_state = FAULT; - - uint8_t fault = battery_controller.read_fault(); - // Battery fuel gauge status - bat_status status = fuel_gauge.battery_status(); - voltage = fuel_gauge.voltage(); - current = fuel_gauge.current(); - - // cleared after read - if (fault & (1 << 4)) { - LOG_WRN("Input over voltage."); - } - - // as long as fault exists - if (fault & (1 << 5)) { - bool power_connected = battery_controller.power_connected(); - if (power_connected && current > 0.5 * power_manager._battery_settings.i_term) { - msg.charging_state = PRECHARGING; - } - LOG_WRN("Battery under voltage: %.3f V", voltage); - } - - // cleared after read - if (fault & (1 << 6)) { - LOG_WRN("Input under voltage"); - } - - // as long as fault exists - if (fault & (1 << 7)) { - LOG_WRN("Battery over voltage"); - } - - uint8_t ts_fault = battery_controller.read_ts_fault(); - - if ((ts_fault >> 5) & 0x7) { - LOG_WRN("TS_ENABLED: %i, TS FAULT: %i", ts_fault >> 7, (ts_fault >> 5) & 0x3); - battery_controller.setup(power_manager._battery_settings); - } - - LOG_DBG("------------------ Battery Info ------------------"); - LOG_DBG("Battery Status:"); - LOG_DBG(" Present: %d, Full Charge: %d, Full Discharge: %d", - status.BATTPRES, status.FC, status.FD); - - // Basic measurements - LOG_DBG("Basic Measurements:"); - LOG_DBG(" Voltage: %.3f V", voltage); - LOG_DBG(" Current: %.3f mA", current); - break; - } - + button_state state = battery_controller.read_button_state(); battery_controller.enter_high_impedance(); - - power_manager.last_charging_msg_state = msg.charging_state; - - // Adjust interval based on state - if (msg.charging_state == FAULT || msg.charging_state == POWER_CONNECTED) { - power_manager.chrg_interval = K_SECONDS(CONFIG_BATTERY_CHARGE_CONTROLLER_FAST_INTERVAL_SECONDS); + if (!state.wake_2) return; + if (power_manager.power_on) { + power_manager.power_down(); + } else if (power_manager.charger_configured && power_manager.check_battery()) { + power_manager.power_on = true; } else { - power_manager.chrg_interval = K_SECONDS(CONFIG_BATTERY_CHARGE_CONTROLLER_NORMAL_INTERVAL_SECONDS); + LOG_WRN("Ignoring start request until battery has recovered"); } +} - //ret = k_msgq_put(&battery_queue, &msg, K_NO_WAIT); - ret = zbus_chan_pub(&battery_chan, &msg, K_FOREVER); - if (ret) { - LOG_WRN("power manager msg queue full"); - } +void PowerManager::fuel_gauge_work_handler(struct k_work *) { + if (!power_manager.stopping) power_manager.charge_task(); } int PowerManager::begin() { - earable_state oe_state; + // An uninitialized LED mode could select the default black custom color. + earable_state oe_state = {}; oe_state.charging_state = DISCHARGING; oe_state.pairing_state = PAIRED; - battery_controller.begin(); - fuel_gauge.begin(); + k_work_queue_start(&battery_work_queue, battery_work_stack, + K_THREAD_STACK_SIZEOF(battery_work_stack), 5, NULL); + k_thread_name_set(&battery_work_queue.thread, "battery"); + int controller_ret = battery_controller.begin(); + int gauge_ret = fuel_gauge.begin(); earable_btn.begin(); battery_controller.exit_high_impedance(); @@ -277,6 +122,20 @@ int PowerManager::begin() { button_state btn = battery_controller.read_button_state(); + uint8_t retained = oe_boot_flags(NRF_POWER->GPREGRET[1]); + bool remain_off = retained & OE_BOOT_FLAG_CHARGE_ONLY; + // Keep the USB-session safety state even when boot consumes the off request. + NRF_POWER->GPREGRET[1] = oe_boot_encode(retained & OE_BOOT_SAFETY_FLAGS); + timer_recovery_used = retained & OE_BOOT_FLAG_TIMER_USED; + charger_fault_latched = retained & OE_BOOT_FLAG_CHARGER_FAULT; + if (!battery_controller.power_connected()) { + set_charger_session(false, false); + } else if (timer_recovery_used) { + // Boot's CD/configuration sequence resets the hardware timer, so its + // remaining recovery time or an unobserved second expiry is lost. + // Never grant another charging cycle after a used retry and a reset. + set_charger_session(true, true); + } power_on = btn.wake_2; // get reset reason @@ -287,7 +146,7 @@ int PowerManager::begin() { if (reset_reas & RESET_RESETREAS_RESETPIN_Msk) { oe_boot_state.timer_reset = bat_state & (1 << 4); - power_on |= oe_boot_state.timer_reset; + power_on = power_on || oe_boot_state.timer_reset; } /*if (reset_reas & RESET_RESETREAS_DOG1_Msk) { @@ -296,44 +155,56 @@ int PowerManager::begin() { if (reset_reas & RESET_RESETREAS_SREQ_Msk) { LOG_INF("Rebooting ..."); - power_on = true; + power_on = !remain_off; } + // A retained button/reset indication must not undo an explicit off request. + if (remain_off) power_on = false; /*if (reset_reas & RESET_RESETREAS_LOCKUP_Msk) { printk("Reset durch CPU Lockup\n"); }*/ - battery_controller.setup(_battery_settings); - battery_controller.set_int_callback(battery_controller_callback); - - // check setup - op_state state = fuel_gauge.operation_state(); - if (state.SEC != BQ27220::SEALED) { - //battery_controller.setup(); - fuel_gauge.setup(_battery_settings); + charger_configured = controller_ret == 0 && + battery_controller.setup_boot(_battery_settings) == 0; + if (controller_ret || gauge_ret || !charger_configured) { + LOG_ERR("Battery hardware initialization failed"); + power_on = false; } - //k_timer_init(&charge_timer, charge_timer_handler, NULL); - - bool battery_condition = check_battery(); + bool battery_condition = charger_configured && gauge_ret == 0 && check_battery(); + + // Gauge programming is never a prerequisite for depleted-cell recovery. + // Do it only on a healthy boot before any battery work can run. FCC is a + // learned value; use design capacity to identify an unconfigured profile. + if (battery_condition && power_on) { + op_state state = fuel_gauge.operation_state(); + if (state.SEC != BQ27220::SEALED || + fabsf(fuel_gauge.design_cap() - _battery_settings.capacity) > 0.5f || + IS_ENABLED(CONFIG_SETUP_FUEL_GAUGE)) { + if (fuel_gauge.setup(_battery_settings) != 0) battery_condition = false; + } + } if (!battery_condition) LOG_WRN("Battery check failed."); // check charging state bool charging = battery_controller.power_connected(); + // Setup above already covered this USB state. Only later edges should + // invalidate it; publishing callbacks must not race a redundant setup. + usb_connected = charging; if (!battery_condition) { power_on = false; // LOG_ERR("Bad battery condition."); if (!charging){ //TODO: Flash red LED once - return power_down(false); + power_down(false); + k_sleep(K_FOREVER); } } if (charging) { - power_manager.last_charging_state = 0; - + int ret = pm_device_runtime_enable(ls_1_8); if (ret != 0) { LOG_WRN("Error setting up load switch 1.8V."); @@ -349,9 +220,19 @@ int PowerManager::begin() { oe_state.charging_state = POWER_CONNECTED; - state_indicator.init(oe_state); + // A reset PMIC defaults to LDO pass-through. Do not energize the LED + // rail until its 3.3 V configuration has been verified. + if (charger_configured) { + state_indicator.init(oe_state); + indicator_ready = true; + } - k_work_schedule(&charge_ctrl_delayable, K_NO_WAIT); + // Keep the network core off during battery recovery/charge-only boot. + nrf_reset_network_force_off(NRF_RESET, true); + battery_controller.set_int_callback(battery_controller_callback); + battery_controller.set_power_connect_callback(power_good_callback); + fuel_gauge.set_int_callback(fuel_gauge_callback); + k_work_schedule_for_queue(&battery_work_queue, &charge_ctrl_delayable, K_NO_WAIT); while(!power_on && battery_controller.power_connected()) { //__WFE(); @@ -361,14 +242,20 @@ int PowerManager::begin() { oe_state.charging_state = DISCHARGING; } - if (!power_on) return power_down(); + if (!power_on || !check_battery() || !charger_configured || stopping) { + power_down(); + k_sleep(K_FOREVER); + } + nrf_reset_network_force_off(NRF_RESET, false); //TODO: check power on condition // either not charging and edv1 or charging and edv0 and temperature - battery_controller.set_power_connect_callback(power_good_callback); - fuel_gauge.set_int_callback(fuel_gauge_callback); - //battery_controller.set_int_callback(battery_controller_callback); + if (!charging) { + battery_controller.set_power_connect_callback(power_good_callback); + fuel_gauge.set_int_callback(fuel_gauge_callback); + battery_controller.set_int_callback(battery_controller_callback); + } //float voltage = battery_controller.read_ldo_voltage(); //if (voltage != 3.3) battery_controller.write_LDO_voltage_control(3.3); @@ -402,13 +289,6 @@ int PowerManager::begin() { //return ret; } - // check if fuel gauge has wrong value - float capacity = fuel_gauge.capacity(); - if (abs(capacity - _battery_settings.capacity) > 1e-4) { - fuel_gauge.setup(_battery_settings); - set_error_led(); - } - #ifdef CONFIG_BOOTLOADER_MCUBOOT bool img_confirmed = boot_is_img_confirmed(); @@ -420,13 +300,11 @@ int PowerManager::begin() { sys_reboot(SYS_REBOOT_COLD); } LOG_INF("Image confirmed"); - #ifdef CONFIG_SETUP_FUEL_GAUGE - fuel_gauge.setup(_battery_settings); - #endif } #endif state_indicator.init(oe_state); + indicator_ready = true; uint32_t device_id[2]; @@ -436,6 +314,9 @@ int PowerManager::begin() { oe_boot_state.device_id = (((uint64_t) device_id[1]) << 32) | device_id[0]; + // Poll on battery as well as USB: gauge IRQs are an optimization, not + // the only protection against discharging below the shutdown threshold. + k_work_schedule_for_queue(&battery_work_queue, &charge_ctrl_delayable, K_NO_WAIT); return 0; } @@ -444,40 +325,15 @@ void PowerManager::set_error_led(int val) { } bool PowerManager::check_battery() { - bool charging = battery_controller.power_connected(); - - if (charging) { - float voltage = fuel_gauge.voltage(); - - if (voltage < _battery_settings.u_charge_prevent) { - battery_controller.disable_charge(); - return false; - } - - float temp = fuel_gauge.temperature(); - - if (temp < _battery_settings.temp_min || temp > _battery_settings.temp_max) { - // set params - battery_controller.disable_charge(); - return false; - } else if (temp < _battery_settings.temp_fast_min || temp > _battery_settings.temp_fast_max) { - // set params - battery_controller.write_charging_control(_battery_settings.i_charge / 2); - battery_controller.enable_charge(); - } else { - // normal params - battery_controller.write_charging_control(_battery_settings.i_charge); - battery_controller.enable_charge(); - } - } - - bat_status bs = fuel_gauge.battery_status(); - if (bs.SYSDWN) return false; - - //gauge_status gs = fuel_gauge.gauging_state(); - //if (gs.edv1) return false; // critical battery state - - return true; + // Require checked measurements and hysteresis before starting application loads. + bat_status status = {}; + float voltage, temperature; + if (!fuel_gauge.read_safety_status(status, voltage, temperature)) return false; + const float start_voltage = _battery_settings.u_vlo + + (CONFIG_BATTERY_SYSDOWN_SET_OFFSET + CONFIG_BATTERY_SYSDOWN_HYSTERESIS) / 1000.0f; + return std::isfinite(voltage) && std::isfinite(temperature) && !status.SYSDWN && + voltage >= start_voltage && voltage <= _battery_settings.u_term + 0.1f && + temperature >= _battery_settings.temp_min && temperature <= _battery_settings.temp_max; } void PowerManager::get_battery_status(battery_level_status &status) { @@ -564,139 +420,237 @@ void PowerManager::reboot() { } int PowerManager::power_down(bool fault) { - int ret; - - // disconnect devices - uint8_t data = BT_HCI_ERR_REMOTE_USER_TERM_CONN; - bt_conn_foreach(BT_CONN_TYPE_ALL, bt_disconnect_handler, &data); - - ret = bt_le_adv_stop(); - - // power disonnected - // prepare interrupts + // Serialize all shutdown paths with the charger worker. Never return to + // normal operation after partially shutting down the peripherals. + if (stopping.exchange(true)) return 0; + power_on = false; + shutdown_fault = fault; + k_work_cancel_delayable(&charge_ctrl_delayable); + k_work_reschedule_for_queue(&battery_work_queue, &power_down_work, K_NO_WAIT); + return 0; +} - led_controller.begin(); - led_controller.power_off(); +void PowerManager::set_charger_session(bool recovery_used, bool fault_latched) { + // Preserve the retry budget across both normal and fatal reboots. + unsigned int key = irq_lock(); + timer_recovery_used = recovery_used; + charger_fault_latched = fault_latched; + uint8_t flags = oe_boot_flags(NRF_POWER->GPREGRET[1]) & OE_BOOT_FLAG_CHARGE_ONLY; + if (recovery_used) flags |= OE_BOOT_FLAG_TIMER_USED; + if (fault_latched) flags |= OE_BOOT_FLAG_CHARGER_FAULT; + NRF_POWER->GPREGRET[1] = oe_boot_encode(flags); + irq_unlock(key); +} +void PowerManager::finish_power_down() { + uint8_t reason = BT_HCI_ERR_REMOTE_USER_TERM_CONN; + bt_conn_foreach(BT_CONN_TYPE_ALL, bt_disconnect_handler, &reason); + (void)bt_le_adv_stop(); stop_sensor_manager(); + (void)audio_datapath_stop(); + (void)bt_mgmt_stop_watchdog(); + dac.end(); + if (indicator_ready) led_controller.power_off(); + gpio_pin_set_dt(&error_led, 0); - bool charging = battery_controller.power_connected(); - - if (!charging) { - ret = battery_controller.set_wakeup_int(); - if (ret != 0) return ret; - - ret = fuel_gauge.set_wakeup_int(); - if (ret != 0) return ret; - - // check battery good - //if (!fault) ret = power_switch.set_wakeup_int(); - //if (ret != 0) return ret; - - battery_controller.enter_high_impedance(); - } - - //TODO: prevent crashing with bt_disable (does not wake up) - /*ret = bt_disable(); + // A battery alert is not a request to turn an intentionally off device on. + (void)fuel_gauge.disable_wakeup_int(); + LOG_INF("Power off%s", shutdown_fault ? " due to fault" : ""); - if (ret != 0) { - NVIC_SystemReset(); + if (battery_controller.power_connected()) { + NRF_POWER->GPREGRET[1] = oe_boot_request_charge_only(NRF_POWER->GPREGRET[1]); sys_reboot(SYS_REBOOT_COLD); - }*/ - - if (fault) { - LOG_WRN("Power off due to fault"); - } else { - LOG_INF("Power off"); + CODE_UNREACHABLE; } - LOG_PANIC(); - - ret = bt_mgmt_stop_watchdog(); - //ERR_CHK(ret); - - dac.end(); - // TODO: check states of load switch (should already be suspended - // if all devieses have been terminated correctly) + set_charger_session(false, false); // USB absence ends the retained session. + (void)pm_device_action_run(ls_sd, PM_DEVICE_ACTION_SUSPEND); + (void)pm_device_action_run(ls_3_3, PM_DEVICE_ACTION_SUSPEND); + (void)pm_device_action_run(ls_1_8, PM_DEVICE_ACTION_SUSPEND); - // turn off error led - gpio_pin_set_dt(&error_led, 0); - - if (charging) { - //NVIC_SystemReset(); + // Ship mode removes SYS/PMID loads rather than just sleeping the CPU. + // MR must be released for the PMIC to complete this transition. + int ret = battery_controller.enter_ship_mode(); + if (ret == -EBUSY || battery_controller.power_connected()) { + NRF_POWER->GPREGRET[1] = oe_boot_request_charge_only(NRF_POWER->GPREGRET[1]); sys_reboot(SYS_REBOOT_COLD); - return 0; + CODE_UNREACHABLE; } - - ret = pm_device_action_run(ls_sd, PM_DEVICE_ACTION_SUSPEND); - ret = pm_device_action_run(ls_3_3, PM_DEVICE_ACTION_SUSPEND); - ret = pm_device_action_run(ls_1_8, PM_DEVICE_ACTION_SUSPEND); - ret = pm_device_action_run(cons, PM_DEVICE_ACTION_SUSPEND); - - /*const struct device *const i2c = DEVICE_DT_GET(DT_NODELABEL(i2c1)); - ret = pm_device_action_run(i2c, PM_DEVICE_ACTION_SUSPEND); - ERR_CHK(ret);*/ - - /*const struct device *const watch_dog = DEVICE_DT_GET(DT_CHOSEN(zephyr_bt_hci_rpmsg_ipc)); - ret = pm_device_action_run(watch_dog, PM_DEVICE_ACTION_SUSPEND); - ERR_CHK(ret);*/ - - /*const struct device *const watch_dog = DEVICE_DT_GET(DT_ALIAS(watchdog0)); - ret = pm_device_action_run(watch_dog, PM_DEVICE_ACTION_SUSPEND); - ERR_CHK(ret);*/ - + if (ret) { + LOG_ERR("Ship mode failed (%d); using SYSTEMOFF", ret); + battery_controller.enter_high_impedance(); + } + (void)pm_device_action_run(cons, PM_DEVICE_ACTION_SUSPEND); + nrf_reset_network_force_off(NRF_RESET, true); + // Level wake can already be asserted. Arm it only after blocking cleanup, + // with CPU interrupts masked, then enter SYSTEMOFF without another wait. + (void)irq_lock(); + (void)battery_controller.set_wakeup_int(); sys_poweroff(); - - // safety if poweroff failed - k_msleep(1000); - - //NVIC_SystemReset(); - sys_reboot(SYS_REBOOT_COLD); + CODE_UNREACHABLE; } - void PowerManager::charge_task() { - uint16_t charging_state = battery_controller.read_charging_state() >> 6; + if (stopping) return; + // A real USB disconnect ends the retry budget; polling continues on battery. + bool usb = battery_controller.power_connected(); + if (usb != usb_connected) { + usb_connected = usb; + charger_configured = false; + if (!usb) set_charger_session(false, false); + requested_current = 0; + } + if (!usb && !power_on) { + power_down(); + return; + } + + bat_status battery = {}; + float voltage = 0, temperature = 0; + bool valid = fuel_gauge.read_safety_status(battery, voltage, temperature); + // Charge-only recovery may run below the application's restart voltage. + const float shutdown_voltage = _battery_settings.u_vlo + CONFIG_BATTERY_SYSDOWN_SET_OFFSET / 1000.0f; + const float start_voltage = shutdown_voltage + CONFIG_BATTERY_SYSDOWN_HYSTERESIS / 1000.0f; + bool safe = valid && std::isfinite(voltage) && std::isfinite(temperature) && + !battery.CHGINH && !battery.OTC && + voltage >= _battery_settings.u_charge_prevent && voltage <= _battery_settings.u_term + 0.1f && + temperature >= _battery_settings.temp_min && temperature <= _battery_settings.temp_max; + // Stop application loads even on USB if the input cannot sustain them; + // charge-only recovery is allowed to stay on below this voltage. + if ((!usb || power_on) && (!valid || battery.SYSDWN || voltage <= shutdown_voltage)) { + LOG_WRN("Battery shutdown: valid=%d voltage=%.3f", valid, voltage); + if (usb) battery_controller.disable_charge(); + power_down(true); + return; + } - //LOG_INF("Charger Watchdog ..................."); - - if (last_charging_state == 0) { - LOG_INF("Setting up charge controller ........"); - battery_controller.setup(_battery_settings); - battery_controller.enable_charge(); - } - - //if (last_charging_state != charging_state || ) { - k_work_submit(&fuel_gauge_work); - //state_inidicator.set_state() - /*switch (charging_state) { - case 0: - LOG_INF("charging state: ready"); - break; - case 1: - LOG_INF("charging state: charging"); - break; - case 2: - LOG_INF("charging state: done"); - break; - case 3: - LOG_WRN("charging state: fault"); - - //battery_controller.setup(_battery_settings); - - break; - }*/ - //} - - last_charging_state = charging_state; + if (usb && !charger_configured && !charger_fault_latched) { + charger_configured = (indicator_ready ? battery_controller.setup(_battery_settings) + : battery_controller.setup_boot(_battery_settings)) == 0; + requested_current = 0; + if (!charger_configured && indicator_ready) { + // USB edges invalidate our configuration cache. If a PMIC reset + // also changed the LDO, setup cannot repair it with the live rail + // enabled. Retry through boot, where both enable sources are off. + // Persistent bus failures then stay in the boot retry path. + LOG_ERR("Runtime charger setup failed; restarting into charge-only mode"); + power_down(true); + return; + } + } + if (usb && charger_configured && !indicator_ready) { + // A failed boot setup can recover later; initialize the indicator only then. + earable_state state = {}; + state.pairing_state = PAIRED; + state.charging_state = POWER_CONNECTED; + state_indicator.init(state); + indicator_ready = true; + } + uint8_t ctrl = 0, fault = 0, ts = 0; + bool read_ok = battery_controller.read_status(ctrl, fault, ts); + if (usb && charger_configured && read_ok && !charger_fault_latched && + !battery_controller.configuration_valid(_battery_settings, + requested_current > 0 ? requested_current : _battery_settings.i_charge)) { + // A PMIC watchdog reset also restores the LDO to pass-through. Its + // voltage cannot be changed while LSCTRL is high. Inhibit charging and + // restart in charge-only mode so all rails are off before reconfiguration. + LOG_ERR("Charger configuration lost; restarting into charge-only mode"); + charger_configured = false; + power_down(true); + return; + } + // VIN_OV, VIN_UV, BAT_OCP and hot/cold are electrical/thermal faults. + // BAT_UVLO and warm/cool derating alone are expected during recovery. + bool blocked = charger_fault_latched || !read_ok || (fault & 0xD0) || + ((ts & BIT(7)) && ((ts >> 5) & 3) == 1); + if (usb && read_ok && (ctrl & BIT(3))) { + if (safe && !blocked && !timer_recovery_used) { + set_charger_session(true, false); + LOG_WRN("Recovering charger safety timer once for this USB session"); + charger_configured = battery_controller.recover_charging(_battery_settings) == 0; + if (!charger_configured) set_charger_session(true, true); + requested_current = 0; + read_ok = battery_controller.read_status(ctrl, fault, ts); + blocked = charger_fault_latched || !read_ok || (fault & 0xD0) || + ((ts & BIT(7)) && ((ts >> 5) & 3) == 1); + } else { + // CD inhibition itself clears TIMER: remember exhaustion in software + // so a later poll cannot silently restart the same stalled battery. + set_charger_session(timer_recovery_used, true); + } + if (ctrl & BIT(3)) set_charger_session(timer_recovery_used, true); + blocked |= charger_fault_latched; + } + // Keep charge permission in one place; status reads must not undo an inhibit. + bool allow = usb && safe && charger_configured && !blocked; + if (allow) { + float current = (temperature < _battery_settings.temp_fast_min || + temperature > _battery_settings.temp_fast_max) + ? _battery_settings.i_charge / 2 : _battery_settings.i_charge; + if (requested_current != current) { + if (battery_controller.write_charging_control(current) < 0) { + allow = false; + } else { + requested_current = current; + } + } + } + // The driver preserves inhibition across status reads; do not cache it twice. + if (allow) allow = battery_controller.enable_charge() == 0; + if (!allow) battery_controller.disable_charge(); + + // Report the actual charging permission, including recovery and latched faults. + struct battery_data message = {}; + message.battery_level = valid ? fuel_gauge.state_of_charge() : 0; + if (!usb) { + message.charging_state = DISCHARGING; +#ifdef CONFIG_BATTERY_ENABLE_LOW_STATE + // Preserve the optional early warning while voltage enforces shutdown. + if (fuel_gauge.gauging_state().edv2) message.charging_state = BATTERY_LOW; +#endif + if (voltage < start_voltage) { + message.charging_state = BATTERY_CRITICAL; + } + } else if (!allow) { + message.charging_state = FAULT; + } else if (battery.SYSDWN || voltage < start_voltage) { + message.charging_state = PRECHARGING; + } else if ((ctrl >> 6) == 2) { + message.charging_state = FULLY_CHARGED; + } else if ((ctrl >> 6) == 1) { + float current = fuel_gauge.current(); + message.charging_state = current > 0.8f * requested_current - 2 * _battery_settings.i_term + ? CHARGING : POWER_CONNECTED; + // Preserve the existing taper indication when trickle LEDs are disabled. + if (message.charging_state == POWER_CONNECTED && voltage > _battery_settings.u_term - 0.02f) { +#ifdef CONFIG_BATTERY_ENABLE_TRICKLE_CHARGE + message.charging_state = TRICKLE_CHARGING; +#else + message.charging_state = CHARGING; +#endif + } + } else { + message.charging_state = (ctrl >> 6) == 3 ? FAULT : POWER_CONNECTED; + } + chrg_interval = K_SECONDS(message.charging_state == FAULT || message.charging_state == POWER_CONNECTED ? + CONFIG_BATTERY_CHARGE_CONTROLLER_FAST_INTERVAL_SECONDS : + CONFIG_BATTERY_CHARGE_CONTROLLER_NORMAL_INTERVAL_SECONDS); + // Bound channel-lock/subscriber waits; synchronous listeners must also + // keep their I2C transactions bounded by the bus driver's transfer timeout. + int ret = zbus_chan_pub(&battery_chan, &message, K_MSEC(100)); + if (ret) LOG_WRN("Battery state publication failed: %d", ret); } int cmd_setup_fuel_gauge(const struct shell *shell, size_t argc, const char **argv) { ARG_UNUSED(argc); ARG_UNUSED(argv); - fuel_gauge.setup(power_manager._battery_settings); - + // Leave a failed configuration visible instead of rebooting as if it succeeded. + int ret = fuel_gauge.setup(power_manager._battery_settings); + if (ret) { + shell_error(shell, "Fuel gauge setup failed: %d", ret); + return ret; + } power_manager.reboot(); return 0; @@ -758,4 +712,4 @@ SHELL_STATIC_SUBCMD_SET_CREATE(battery_cmd, SHELL_CMD_REGISTER(battery, &battery_cmd, "Power Manager Commands", NULL); -PowerManager power_manager; \ No newline at end of file +PowerManager power_manager; diff --git a/src/Battery/PowerManager.h b/src/Battery/PowerManager.h index e010e5f6..21bd3d67 100644 --- a/src/Battery/PowerManager.h +++ b/src/Battery/PowerManager.h @@ -2,6 +2,8 @@ #define _POWER_MANAGER_H #include +// Boot/button handling and the battery worker share these state flags. +#include #include "BQ27220.h" #include "BQ25120a.h" @@ -11,17 +13,14 @@ #include "openearable_common.h" #include "BootState.h" -#define DEBOUNCE_POWER_MS K_MSEC(1000) - class PowerManager { public: int begin(); + /** Queue an irreversible shutdown; USB reboots into charge-only mode. + * Returns 0 after accepting the request, including repeated requests. + */ int power_down(bool fault = false); - //bool check_boot_condition(); - - //static LoadSwitch v1_8_switch; - void reboot(); void get_battery_status(battery_level_status &status); @@ -32,23 +31,27 @@ class PowerManager { static k_work_delayable power_down_work; private: - bool power_on = false; - bool charging_disabled = false; - uint16_t last_charging_state = 0; - - enum charging_state last_charging_msg_state = DISCHARGING; + // Serialize shutdown with battery servicing and retain a bounded timer retry. + std::atomic power_on{false}; + std::atomic stopping{false}; + bool shutdown_fault = false; + bool usb_connected = false; + std::atomic charger_configured{false}; + std::atomic indicator_ready{false}; + bool timer_recovery_used = false; + bool charger_fault_latched = false; + float requested_current = 0; + void finish_power_down(); + void set_charger_session(bool recovery_used, bool fault_latched); void charge_task(); - void power_connected(); - bool check_battery(); k_timeout_t chrg_interval = K_SECONDS(CONFIG_BATTERY_CHARGE_CONTROLLER_NORMAL_INTERVAL_SECONDS); static k_work_delayable charge_ctrl_delayable; - //static k_work power_down_work; static k_work fuel_gauge_work; static k_work battery_controller_work; @@ -61,11 +64,12 @@ class PowerManager { static void fuel_gauge_callback(const struct device *dev, struct gpio_callback *cb, uint32_t pins); static void battery_controller_callback(const struct device *dev, struct gpio_callback *cb, uint32_t pins); + // Match the CP1454 A4X charge limits and cut system loads off above deep discharge. const battery_settings _battery_settings = { - 3.7, 4.3, 3.0, 2.5, // Spannungen - 10, 110, 200, // Ströme - 108, // Kapazität - 0, 15, 45, 50 // Temperaturen + 3.7, 4.3, 3.0, 2.5, // Nominal, regulation, UVLO, charge-prevent voltage (V) + 10, 100, 200, // Shared precharge/termination, fast charge, input (mA) + 108, // Design capacity (mAh), VARTA CP1454 A4X + 0, 15, 45, 45 // Min, fast min/max, max charge temperature (C) }; const struct gpio_dt_spec error_led = GPIO_DT_SPEC_GET(DT_NODELABEL(led_error), gpios); @@ -75,4 +79,4 @@ class PowerManager { extern PowerManager power_manager; -#endif \ No newline at end of file +#endif diff --git a/src/SD_Card/SDLogger/SDLogger.cpp b/src/SD_Card/SDLogger/SDLogger.cpp index 335d4b1b..12bf836f 100644 --- a/src/SD_Card/SDLogger/SDLogger.cpp +++ b/src/SD_Card/SDLogger/SDLogger.cpp @@ -474,10 +474,16 @@ int SDLogger::flush() { written = sd_card->write((char*)data, &req, false); k_mutex_unlock(&file_mutex); - if (written < 0) { + if (written <= 0) { + // FatFs reports a full volume as a successful zero-byte write. + // Retrying without consuming data would keep shutdown here forever. + k_mutex_lock(&ring_mutex, K_FOREVER); + ring_buf_get_finish(&ring_buffer, 0); + k_mutex_unlock(&ring_mutex); + int error = written < 0 ? written : -ENOSPC; state_indicator.set_sd_state(SD_FAULT); - LOG_ERR("Failed to flush SD buffer: %d", written); - break; + LOG_ERR("Failed to flush SD buffer: %d", error); + return error; } k_mutex_lock(&ring_mutex, K_FOREVER); @@ -510,12 +516,13 @@ int SDLogger::end() { atomic_set(&g_stop_writing, 1); k_poll_signal_raise(&logger_sig, 0); - ret = flush(); - if (ret < 0) { + int flush_result = flush(); + if (flush_result < 0) { LOG_ERR("Failed to flush file buffer."); - return ret; } + // Close even when the card is full or flushing failed. Shutdown must be + // able to continue, and already-written file metadata still needs syncing. LOG_INF("Close File ...."); LOG_DBG("Max buffer fill: %d bytes", count_max_buffer_fill); @@ -534,7 +541,7 @@ int SDLogger::end() { atomic_clear(&g_stop_writing); atomic_clear(&g_sd_removed); - return 0; + return flush_result < 0 ? flush_result : 0; } bool SDLogger::is_active() { diff --git a/src/utils/error_handler.c b/src/utils/error_handler.c index 7e5ae97e..33135915 100644 --- a/src/utils/error_handler.c +++ b/src/utils/error_handler.c @@ -9,6 +9,9 @@ #include #include #include +/* Request charge-only recovery without I2C from the fatal handler. */ +#include +#include "../Battery/BootState.h" /* Print everything from the error handler */ #include @@ -23,6 +26,15 @@ static const struct gpio_dt_spec center_led_b = GPIO_DT_SPEC_GET(DT_NODELABEL(rg void error_handler(unsigned int reason, const struct arch_esf *esf) { +#if CONFIG_BOARD_OPENEARABLE_V2_NRF5340_CPUAPP + /* A wearable must not drain its cell in the debug-build panic loop. + * Re-enter only the battery/charging path; do not restart the failing app. + * This path may run with interrupts locked, so no I2C or blocking cleanup. + */ + NRF_POWER->GPREGRET[1] = oe_boot_request_charge_only(NRF_POWER->GPREGRET[1]); + sys_reboot(SYS_REBOOT_COLD); + CODE_UNREACHABLE; +#endif #if (CONFIG_DEBUG) LOG_ERR("Caught system error -- reason %d. Entering infinite loop", reason); LOG_PANIC(); diff --git a/tools/battery/README.md b/tools/battery/README.md index 72425314..97f4808a 100644 --- a/tools/battery/README.md +++ b/tools/battery/README.md @@ -119,16 +119,12 @@ sequence is retried once. If a fault returns until `--max-fault-resets` is reached, recovery stops with an error instead of silently continuing with charging stopped. -Restoring precharge and battery UVLO is important for deeply discharged cells. -The BQ25120A reset defaults use 2 mA precharge and a 3.0 V battery UVLO threshold -with hysteresis. Recovery instead applies the verified OpenEarable settings of -10 mA precharge and 2.6 V UVLO, avoiding a prolonged low-current plateau. - -`BAT_UVLO`, VINDPM, and the cool/warm temperature derating states do not trigger -repeated resets; recovery monitors them while voltage progresses. Missing input -power, input overvoltage, battery overcurrent, and a hot/cold temperature -suspension stop recovery immediately because software cannot safely clear the -underlying electrical condition. +Recovery uses 100 mA fast charge, 10 mA precharge/termination, a 200 mA input +limit and 3.0 V battery UVLO. It requires readable safety flags and a battery +temperature of 0–45 °C. The v2.7 TS input is a fixed divider, not a thermistor; +recovery disables it after checking gauge temperature and electrical faults. +Missing input power, input overvoltage, battery overcurrent or unsafe gauge +readings stop recovery. Battery UVLO and current limiting do not trigger resets. ## Useful Options diff --git a/tools/battery/battery_debug.py b/tools/battery/battery_debug.py index bc03ae7f..606cb822 100755 --- a/tools/battery/battery_debug.py +++ b/tools/battery/battery_debug.py @@ -28,10 +28,16 @@ APP_CORE_DEVICE = "NRF5340_XXAA_APP" DEFAULT_SPEED_KHZ = 1000 -RECOVERY_CHARGE_CURRENT_MA = 110 +# Match the VARTA cell limits and firmware cutoff during manual recovery. +RECOVERY_CHARGE_CURRENT_MA = 100 RECOVERY_PRETERM_CURRENT_MA = 10.0 RECOVERY_INPUT_LIMIT_MA = 200 -RECOVERY_UVLO_MV = 2500 +RECOVERY_UVLO_MV = 3000 +RECOVERY_MIN_TEMPERATURE_C = 0.0 +RECOVERY_MAX_TEMPERATURE_C = 45.0 +# v2.7 has a fixed 57.7% VIN divider rather than a battery thermistor. +# It falls inside TSOFF's 55%-60% tolerance band: use checked gauge temperature. +RECOVERY_TS_CONFIG = 0x00 TWIM1 = 0x50009000 GPIO0 = 0x50842500 @@ -244,7 +250,8 @@ def fault_reasons(self) -> list[str]: reasons.append("BAT_UVLO") if self.battery_overcurrent: reasons.append("BAT_OCP") - if self.ts_fault_code: + # Disabled TS status bits are not temperature faults on v2.7. + if self.ts_enabled and self.ts_fault_code: reasons.append(self.ts_state) if self.vindpm_active and self.charging_state_code == 3: reasons.append("VINDPM") @@ -259,7 +266,7 @@ def blocking_fault_reasons(self) -> list[str]: reasons.append("VIN_OV") if self.battery_overcurrent: reasons.append("BAT_OCP") - if self.ts_fault_code == 1: + if self.ts_enabled and self.ts_fault_code == 1: reasons.append(self.ts_state) return reasons @@ -289,6 +296,8 @@ def ts_fault_code(self) -> int: @property def ts_state(self) -> str: + if not self.ts_enabled: + return "TS_disabled" return { 0: "TS_normal", 1: "TS_hot_or_cold", @@ -655,6 +664,7 @@ def read_charger(self) -> ChargerStatus: def configure_charger(self) -> None: self.set_cd(0) writes = [ + (0x02, RECOVERY_TS_CONFIG, "configure v2.7 fixed TS divider"), ( 0x03, encode_charge_current(RECOVERY_CHARGE_CURRENT_MA), @@ -716,9 +726,10 @@ def encode_termination_current(ma: float) -> int: def encode_ilim_uvlo(input_limit_ma: int, uvlo_mv: int) -> int: ilim = max(50, min(400, int(input_limit_ma))) - uvlo = max(2200, min(3000, int(uvlo_mv))) / 1000.0 - ilim_code = int(round(ilim / 50 - 1)) & 0x07 - uvlo_code = int(round((3.0 - uvlo) * 5 + 2)) & 0x07 + uvlo = max(2200, min(3000, int(uvlo_mv))) + ilim_code = ilim // 50 - 1 + # Round the protection threshold upward, never below the requested value. + uvlo_code = (3000 - uvlo) // 200 + 2 return (ilim_code << 3) | uvlo_code @@ -850,17 +861,38 @@ def open_link(args: argparse.Namespace) -> JLinkBatteryInterface: ) -def charger_blocking_reasons(charger: ChargerStatus) -> list[str]: +def charger_blocking_reasons( + charger: ChargerStatus, *, configuring_fixed_divider: bool = False +) -> list[str]: reasons = [] if not charger.pg_present: reasons.append("input_power_missing") - reasons.extend(charger.blocking_fault_reasons) + reasons.extend( + reason for reason in charger.blocking_fault_reasons + if not (configuring_fixed_divider and reason == "TS_hot_or_cold") + ) + return reasons + + +def fuel_recovery_blocking_reasons(fuel: FuelGaugeStatus) -> list[str]: + # The fixed TS divider cannot protect the cell; require valid gauge data. + reasons = [] + if fuel.temperature_c is None or not math.isfinite(fuel.temperature_c): + reasons.append("battery_temperature_unavailable") + elif not RECOVERY_MIN_TEMPERATURE_C <= fuel.temperature_c <= RECOVERY_MAX_TEMPERATURE_C: + reasons.append("battery_temperature_outside_0_to_45_C") + if fuel.flags is None: + reasons.append("battery_safety_flags_unavailable") + elif fuel.flags & ((1 << 8) | (1 << 11)): + reasons.append("battery_charge_inhibited") return reasons def charger_recovery_reason(charger: ChargerStatus, reset_on_fault: bool) -> str | None: if charger.timer_fault: return "safety timer fault" + if charger.ts_enabled: + return "v2.7 fixed TS divider needs host configuration" if not reset_on_fault: return None if charger.cd_stat or charger.cd_raw: @@ -875,7 +907,7 @@ def charger_recovery_reason(charger: ChargerStatus, reset_on_fault: bool) -> str charger.bat_uvlo or charger.vin_undervoltage or charger.vindpm_active - or charger.ts_fault_code in (2, 3) + or (charger.ts_enabled and charger.ts_fault_code in (2, 3)) ) if ( charger.charging_state_code == 3 @@ -894,6 +926,12 @@ def reset_and_configure_charger(link: JLinkBatteryInterface) -> ChargerStatus: link.configure_charger() time.sleep(0.050) charger = link.read_charger() + if (charger.ts_fault & 0x8F) != RECOVERY_TS_CONFIG: + raise BatteryDebugError( + "charger TS configuration did not stick: " + f"read 0x{charger.ts_fault:02x}, expected " + f"0x{RECOVERY_TS_CONFIG:02x} (mask 0x8f)" + ) if charger_blocking_reasons(charger): return charger if charger.timer_fault: @@ -973,8 +1011,14 @@ def cmd_recover(args: argparse.Namespace) -> int: charger = link.read_charger() print("initial:", format_status(fuel, charger)) - blockers = charger_blocking_reasons(charger) + # This board's fixed divider is not temperature data. Correct its TS + # setting only after independent gauge safety and electrical checks. + blockers = charger_blocking_reasons( + charger, configuring_fixed_divider=True + ) + fuel_recovery_blocking_reasons(fuel) if blockers: + if charger.pg_present: + link.set_cd(1) print( "Recovery cannot continue while blocking fault(s) are active: " f"{'+'.join(blockers)}.", @@ -1003,8 +1047,10 @@ def cmd_recover(args: argparse.Namespace) -> int: print(f"resetting/configuring charger ({reason})") charger = reset_and_configure_charger(link) print("after reset:", format_status(fuel, charger)) - blockers = charger_blocking_reasons(charger) + blockers = charger_blocking_reasons(charger) + fuel_recovery_blocking_reasons(fuel) if blockers: + if charger.pg_present: + link.set_cd(1) print( "Recovery stopped because blocking fault(s) remained after " f"reset: {'+'.join(blockers)}.", @@ -1035,8 +1081,10 @@ def cmd_recover(args: argparse.Namespace) -> int: charger = link.read_charger() print(time.strftime("%H:%M:%S"), format_status(fuel, charger), flush=True) - blockers = charger_blocking_reasons(charger) + blockers = charger_blocking_reasons(charger) + fuel_recovery_blocking_reasons(fuel) if blockers: + if charger.pg_present: + link.set_cd(1) print( "Recovery stopped because blocking fault(s) became active: " f"{'+'.join(blockers)}.", @@ -1088,8 +1136,10 @@ def cmd_recover(args: argparse.Namespace) -> int: ) charger = reset_and_configure_charger(link) print("after reset:", format_status(fuel, charger)) - blockers = charger_blocking_reasons(charger) + blockers = charger_blocking_reasons(charger) + fuel_recovery_blocking_reasons(fuel) if blockers: + if charger.pg_present: + link.set_cd(1) print( "Recovery stopped because blocking fault(s) remained " f"after reset: {'+'.join(blockers)}.", diff --git a/tools/battery/test_battery_debug.py b/tools/battery/test_battery_debug.py index 974bb647..dafe9452 100644 --- a/tools/battery/test_battery_debug.py +++ b/tools/battery/test_battery_debug.py @@ -27,10 +27,11 @@ def charger_status( *, ctrl: int = 0x41, fault: int = 0x00, - ts_fault: int = 0x88, - charge_ctrl: int = 0x9C, + # Match the v2.7 recovery profile: fixed TS divider off, 100 mA, 3.0 V UVLO. + ts_fault: int = 0x00, + charge_ctrl: int = 0x98, preterm_ctrl: int = 0x92, - ilim_uvlo: int = 0x1C, + ilim_uvlo: int = 0x1A, pg_present: bool = True, cd_raw: int = 0, ) -> battery.ChargerStatus: @@ -58,6 +59,21 @@ def fuel_status(voltage_mv: int) -> battery.FuelGaugeStatus: class ChargerStatusTests(unittest.TestCase): + # Recovery must preserve the cell limits and reject unavailable safety data. + def test_recovery_limits(self) -> None: + self.assertEqual(battery.RECOVERY_CHARGE_CURRENT_MA, 100) + self.assertEqual(battery.encode_ilim_uvlo(200, battery.RECOVERY_UVLO_MV), 0x1A) + self.assertEqual(charger_status(ts_fault=0x20).blocking_fault_reasons, []) + for temperature in [-1, 0, 45, 46, None, float("nan")]: + fuel = fuel_status(2564) + fuel.temperature_c = temperature + self.assertEqual(bool(battery.fuel_recovery_blocking_reasons(fuel)), + temperature not in (0, 45)) + for flags in [None, 1 << 8, 1 << 11]: + fuel = fuel_status(2564) + fuel.flags = flags + self.assertTrue(battery.fuel_recovery_blocking_reasons(fuel)) + def test_reset_and_timer_bits_are_distinct(self) -> None: charging = charger_status(ctrl=0x51) timer_fault = charger_status(ctrl=0xD9) @@ -187,6 +203,7 @@ def test_recovery_configuration_sets_current_precharge_and_uvlo(self) -> None: self.assertEqual( interface.bq25120a_write_u8.call_args_list, [ + mock.call(0x02, battery.RECOVERY_TS_CONFIG), mock.call( 0x03, battery.encode_charge_current(