diff --git a/src/M5Unified.hpp b/src/M5Unified.hpp index a24c9d10..231e1ddb 100644 --- a/src/M5Unified.hpp +++ b/src/M5Unified.hpp @@ -654,6 +654,10 @@ namespace m5 { Display.setBrightness(brightness); } + /// The charge state API reports not_initialized until here: the PMIC + /// identity is settled in Power.begin(), but the contract is tied to the + /// completion of M5.begin() itself. + Power._initialized = true; } void setTouchButtonHeightByRatio(uint8_t ratio); diff --git a/src/utility/Power_Class.cpp b/src/utility/Power_Class.cpp index 20aa923f..95360b75 100644 --- a/src/utility/Power_Class.cpp +++ b/src/utility/Power_Class.cpp @@ -5,6 +5,10 @@ #include "Power_Class.hpp" #include "M5IOE1_Class.hpp" +#if defined (M5UNIFIED_PC_BUILD) || defined (M5UNIFIED_CHECK_CHARGE_STATE_CAPS) +#include +#endif + #if !defined (M5UNIFIED_PC_BUILD) #include @@ -82,9 +86,9 @@ namespace m5 ioe1.digitalWrite(M5IOE1_Class::gpio11, false); } - static void set_papermono_ip2315_enabled(bool enable) + static bool set_papermono_ip2315_enabled(bool enable) { - M5.getIOExpander(0).digitalWrite(M5IOE1_Class::gpio11, enable); + return M5.getIOExpander(0).digitalWrite(M5IOE1_Class::gpio11, enable); } static bool wait_papermono_ip2315_ready(void) @@ -110,6 +114,17 @@ namespace m5 bool Power_Class::begin(void) { + /// On the boards that carry either an AXP192 or an AXP2101 the identity + /// is settled by a positive chip ID from the probe, and a later call + /// (M5.Power.begin() is public) then re-applies the register setup but + /// keeps the identity, so the capability set never changes afterwards. + /// If every probe failed, the board default (AXP192) is used but is not + /// treated as settled: a later begin() probes again instead of freezing + /// an identity that was never confirmed. Boards whose PMIC follows from + /// the board id alone do not go through the probe. + const bool identity_settled = _identity_settled; + const pmic_t settled_pmic = _pmic; + (void)identity_settled; (void)settled_pmic; // unused on chips without the AXP192/AXP2101 probe _pmic = pmic_t::pmic_unknown; #if !defined (M5UNIFIED_PC_BUILD) @@ -457,14 +472,27 @@ namespace m5 case board_t::board_M5StampS3Bat: _pmic = pmic_t::pmic_m5pm1; + /// G3 = CHG_PROG of the charger: driven low = 650mA, left floating = 200mA + /// (official documentation). Only those two levels are defined, and the + /// PM1 keeps its state across an ESP reset, so the pin is released to an + /// input first (before anything else can expose a held high latch), + /// then the latch is normalized low while it is still an input. + /// The pull is cleared before the pin becomes an input (a held pull-up + /// would otherwise show on the pin). Every step is attempted even when + /// an earlier one failed: each of them only moves the pin towards a + /// defined state (no pull, input, low latch, push-pull, GPIO mux), so + /// a transient write failure must not leave a held high latch driven + /// just because the release before it did not go through. + (void)M5pm1.setGPIOPull(M5PM1_Class::gpio3, M5PM1_Class::pull_none); + (void)M5pm1.setGPIOMode(M5PM1_Class::gpio3, M5PM1_Class::input); + (void)M5pm1.setGPIOOutput(M5PM1_Class::gpio3, false); + (void)M5pm1.setGPIODrive(M5PM1_Class::gpio3, M5PM1_Class::push_pull); + (void)M5pm1.setGPIOFunction(M5PM1_Class::gpio3, M5PM1_Class::gpio); M5pm1.setGPIOFunction(M5PM1_Class::gpio1, M5PM1_Class::gpio); M5pm1.setGPIOFunction(M5PM1_Class::gpio2, M5PM1_Class::gpio); - M5pm1.setGPIOFunction(M5PM1_Class::gpio3, M5PM1_Class::gpio); M5pm1.setGPIOMode(M5PM1_Class::gpio1, M5PM1_Class::output); M5pm1.setGPIOMode(M5PM1_Class::gpio2, M5PM1_Class::input); - M5pm1.setGPIOMode(M5PM1_Class::gpio3, M5PM1_Class::output); M5pm1.setGPIODrive(M5PM1_Class::gpio1, M5PM1_Class::push_pull); - M5pm1.setGPIODrive(M5PM1_Class::gpio3, M5PM1_Class::push_pull); break; case board_t::board_M5PaperS3: @@ -728,12 +756,28 @@ namespace m5 break; } - if (_pmic == Power_Class::pmic_t::pmic_axp192) { - if (!Axp192.begin()) { - if (Axp2101.begin()) { - _pmic = Power_Class::pmic_t::pmic_axp2101; - } + if (identity_settled) + { + _pmic = settled_pmic; + } + else if (_pmic == Power_Class::pmic_t::pmic_axp192) + { /// Both probes read the same ID register (0x03 = AXP192, 0x4A = AXP2101), + /// so a positive answer from either one settles the identity. A single + /// transient NACK must not leave the default in place while the other + /// chip already identified itself, so the pair is retried a few times + /// until one of them answers. + for (int retry = 0; retry < 3; ++retry) + { + if (Axp192.begin()) { _identity_settled = true; break; } + if (Axp2101.begin()) { _pmic = Power_Class::pmic_t::pmic_axp2101; _identity_settled = true; break; } + m5gfx::delay(1); } + /// Without a positive ID the board default stays provisional: the + /// capability set is published as the default, but the charge state + /// API reports io_error and the charge setters refuse, so a chip that + /// was never identified is not read or written with the wrong + /// register map. A later begin() probes again. + _identity_unconfirmed = !_identity_settled; } if (_pmic == Power_Class::pmic_t::pmic_axp192) @@ -870,6 +914,10 @@ namespace m5 #endif #endif + /// The PMIC identity is settled here (on the AXP192 / AXP2101 boards + /// only once a probe has answered; see the top of this function). + /// _initialized is raised by M5Unified::begin() once the whole + /// initialization has completed, not here. return (_pmic != pmic_t::pmic_unknown); } @@ -2183,12 +2231,15 @@ namespace m5 /// A batteryless charger can hold CHG_STAT low against a collapsed node, /// so a low CHG_STAT alone does not prove charge current (ToughC5 only; /// the CoreMatrix retry blips are filtered by the 100ms streak below). - bool Power_Class::_vbatNodeDown(void) + bool Power_Class::_vbatNodeDown(bool* io_ok) { #if defined (CONFIG_IDF_TARGET_ESP32C5) bool powered; - return M5pm1.getVbatNodePowered(&powered) && !powered; + bool ok = M5pm1.getVbatNodePowered(&powered); + if (io_ok) { *io_ok = ok; } + return ok && !powered; #else + if (io_ok) { *io_ok = true; } return false; #endif } @@ -2207,36 +2258,59 @@ namespace m5 /// Until the first verdict, -1 (unknown) is reported and the battery APIs /// pass that on instead of guessing. (On the CoreMatrix a detach while /// charging can go unnoticed until charging is disabled.) - std::int8_t Power_Class::_batteryPresent(void) + /// Every piece of presence evidence (the CHG_STAT low streak, the VBAT + /// sample baseline and its stable / unstable / low counters) only means + /// something as an unbroken sequence of successful, charger-enabled + /// observations. A failed read, a disabled charger, or a charge enable + /// switch is a gap: the evidence is dropped so nothing observed on the far + /// side of the gap can complete a streak or a sample count that began + /// before it. The verdict itself is kept. + void Power_Class::_bp_dropEvidence(void) { + _bp_chg_low_ms = 0; + _bp_last_ms = 0; + _bp_stable = 0; + _bp_unstable = 0; + _bp_low = 0; + } + + std::int8_t Power_Class::_batteryPresent(bool* io_ok) + { + /// io_ok reports whether every read of this evaluation succeeded. The + /// verdict itself is cached across failed reads (a transient NACK must not + /// flip the presence), so a caller that has to distinguish "read failed" + /// from "last known verdict" looks at io_ok, not at the return value. + /// A failed read ends the evaluation with the cached verdict and drops + /// the transient evidence (see _bp_dropEvidence). + if (io_ok) { *io_ok = true; } bool chg_enabled = true; - if (M5pm1.getBatteryCharge(&chg_enabled) && !chg_enabled) - { /// with the charger idle there is no float voltage: a collapsed node + if (!M5pm1.getBatteryCharge(&chg_enabled)) { if (io_ok) { *io_ok = false; } _bp_dropEvidence(); return _batt_present; } + if (!chg_enabled) + { /// a disabled charger breaks the continuity of the enabled-path + /// evidence, and nothing observed here is kept as a baseline for it: + /// the first sample after re-enabling starts the sampling afresh. The + /// settle rule below has its own timer (_chg_off_ms). + _bp_dropEvidence(); + /// with the charger idle there is no float voltage: a collapsed node /// proves "no battery" at once. A high reading is trusted as "present" /// only once the node has settled after charging stopped (the initial - /// _chg_off_ms = 0 gives a boot the same settle window); until then it - /// only seeds the sampling. + /// _chg_off_ms = 0 gives a boot the same settle window). std::uint16_t mv = 0; - if (M5pm1.getBatteryVoltage(&mv)) - { - if (mv <= 2600) { _batt_present = 0; } - else if ((m5gfx::millis() - _chg_off_ms) > 1500) { _batt_present = 1; } - else if (_bp_last_ms == 0) - { /// not settled yet: only seed the first sampling baseline. - auto t = m5gfx::millis(); - _bp_last_ms = t ? t : 1; - _bp_last_mv = mv; - } - } + if (!M5pm1.getBatteryVoltage(&mv)) { if (io_ok) { *io_ok = false; } } + else if (mv <= 2600) { _batt_present = 0; } + else if ((m5gfx::millis() - _chg_off_ms) > 1500) { _batt_present = 1; } return _batt_present; } std::uint32_t now = m5gfx::millis(); bool chg_stat; - if (_readChargeStat(&chg_stat)) + if (!_readChargeStat(&chg_stat)) { if (io_ok) { *io_ok = false; } _bp_dropEvidence(); return _batt_present; } { - if (!chg_stat && !_vbatNodeDown()) + bool node_ok; + bool node_down = _vbatNodeDown(&node_ok); + if (!node_ok) { if (io_ok) { *io_ok = false; } _bp_dropEvidence(); return _batt_present; } + if (!chg_stat && !node_down) { /// low = charging into a live node; require a >=100ms streak so a /// batteryless retry blip cannot pass as real charge current. if (_bp_chg_low_ms == 0) { _bp_chg_low_ms = now ? now : 1; } @@ -2253,7 +2327,7 @@ namespace m5 } std::uint16_t mv = 0; - if (!M5pm1.getBatteryVoltage(&mv)) { return _batt_present; } + if (!M5pm1.getBatteryVoltage(&mv)) { if (io_ok) { *io_ok = false; } _bp_dropEvidence(); return _batt_present; } if (mv > 4450) { /// only the batteryless sawtooth peaks above any real battery @@ -2299,6 +2373,24 @@ namespace m5 } return _batt_present; } + + /// ToughC5 / CoreMatrix: CHG_STAT behind the battery presence gate. + /// With no battery the charger retries periodically and CHG_STAT blips low, + /// so the presence has to be decided before the line is believed. + charge_state_t Power_Class::_chargeStateFromChgStat(void) + { + /// Procedure order: the presence gate first (its own reads decide io_error + /// and undetermined, and "no battery" settles not_charging without the + /// line), then CHG_STAT only when a battery is there. + bool io_ok; + std::int8_t present = _batteryPresent(&io_ok); + if (!io_ok) { return charge_state_t::io_error; } + if (present < 0) { return charge_state_t::undetermined; } + if (present == 0) { return charge_state_t::not_charging; } + bool level; + if (!_readChargeStat(&level)) { return charge_state_t::io_error; } + return level ? charge_state_t::not_charging : charge_state_t::charging; + } #endif int16_t Power_Class::getBatteryVoltage(void) @@ -2472,67 +2564,71 @@ namespace m5 #endif } - void Power_Class::setBatteryCharge(bool enable) + bool Power_Class::setBatteryCharge(bool enable) { + (void)enable; // some chip builds have no control path at all + if (_identity_unconfirmed) { return false; } // see begin(): the PMIC never answered its ID probe switch (_pmic) { #if defined (CONFIG_IDF_TARGET_ESP32C3) #elif defined (CONFIG_IDF_TARGET_ESP32C6) case pmic_t::pmic_aw32001: - Aw32001.setBatteryCharge(enable); - return; + return Aw32001.setBatteryCharge(enable); #elif defined (CONFIG_IDF_TARGET_ESP32C61) case pmic_t::pmic_m5pm1: - /// the presence check must not read VBAT before the node collapses + /// the presence check must not read VBAT before the node collapses, + /// and a charge enable switch is a gap in its evidence. if (!enable) { _chg_off_ms = m5gfx::millis(); } - M5pm1.setBatteryCharge(enable); - return; + _bp_dropEvidence(); + return M5pm1.setBatteryCharge(enable); #elif defined (CONFIG_IDF_TARGET_ESP32P4) case pmic_t::pmic_m5pm1: - M5pm1.setBatteryCharge(enable); - return; + return M5pm1.setBatteryCharge(enable); #else #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) case pmic_t::pmic_ip5306: - Ip5306.setBatteryCharge(enable); - return; + return Ip5306.setBatteryCharge(enable); case pmic_t::pmic_axp192: - Axp192.setBatteryCharge(enable); - return; + return Axp192.setBatteryCharge(enable); #endif case pmic_t::pmic_axp2101: - Axp2101.setBatteryCharge(enable); - break; + return Axp2101.setBatteryCharge(enable); #if defined (CONFIG_IDF_TARGET_ESP32S3) || defined (CONFIG_IDF_TARGET_ESP32C5) case pmic_t::pmic_m5pm1: { #if defined (CONFIG_IDF_TARGET_ESP32S3) // M5PaperColor does not support charge control + // (the PM1 CHG_EN_PP pin is not wired to the charger) if (M5.getBoard() == board_t::board_M5PaperColor) { - return; + return false; } // M5PaperMono: charging is controlled by the IP2316 charger, not PM1. if (M5.getBoard() == board_t::board_M5PaperMono) { - set_papermono_ip2315_enabled(true); - if (wait_papermono_ip2315_ready()) { - if (enable) { M5.In_I2C.bitOn (ip2315_i2c_addr, 0x01, 1 << 0, i2c_freq); } - else { M5.In_I2C.bitOff(ip2315_i2c_addr, 0x01, 1 << 0, i2c_freq); } + /// every write of the sequence counts: a gate left in the wrong + /// state after a failed close is not a success. + bool res = set_papermono_ip2315_enabled(true); + if (res && wait_papermono_ip2315_ready()) { + res = enable ? M5.In_I2C.bitOn (ip2315_i2c_addr, 0x01, 1 << 0, i2c_freq) + : M5.In_I2C.bitOff(ip2315_i2c_addr, 0x01, 1 << 0, i2c_freq); + } else { + res = false; } - set_papermono_ip2315_enabled(false); - return; + res = set_papermono_ip2315_enabled(false) && res; + return res; } #endif #if defined (CONFIG_IDF_TARGET_ESP32C5) - /// the presence check must not read VBAT before the node collapses + /// the presence check must not read VBAT before the node collapses, + /// and a charge enable switch is a gap in its evidence. if (!enable) { _chg_off_ms = m5gfx::millis(); } + _bp_dropEvidence(); #endif - M5pm1.setBatteryCharge(enable); + return M5pm1.setBatteryCharge(enable); } - return; #endif #endif @@ -2542,78 +2638,118 @@ namespace m5 #if defined (CONFIG_IDF_TARGET_ESP32P4) case board_t::board_M5Tab5: case board_t::board_M5Tab5X: - M5.getIOExpander(1).digitalWrite(7, enable); - break; + /// CHG_EN (IOE1 G7) is owned by this function alone; setChargeCurrent + /// only selects the QC step. + return M5.getIOExpander(1).digitalWrite(7, enable); #endif #if defined (CONFIG_IDF_TARGET_ESP32S3) case board_t::board_M5PowerHub: - M5.In_I2C.writeRegister8(powerhub_i2c_addr, 0x06, enable, i2c_freq); - break; + return M5.In_I2C.writeRegister8(powerhub_i2c_addr, 0x06, enable, i2c_freq); #endif default: - return; + break; } - return; + break; } + return false; } - void Power_Class::setChargeCurrent(std::uint16_t max_mA) - { + bool Power_Class::setChargeCurrent(std::uint16_t max_mA, std::uint16_t* applied_mA) + { (void)max_mA; (void)applied_mA; // some chip builds have no control path at all + if (_identity_unconfirmed) { return false; } // see begin(): the PMIC never answered its ID probe + if (max_mA == 0) + { /// 0 is not a step (where a current path exists it selects the lowest + /// one). Warn once: code written for the old Tab5 / Tab5X behaviour + /// used 0 to stop charging. The flag only limits the log output. + static bool warned = false; + if (!warned) + { + warned = true; + M5_LOGW("setChargeCurrent(0): 0 is not a charge current step. Use setBatteryCharge(false) to stop charging."); + } + } + /// The step contract: the highest step not above max_mA, clamped up to the + /// lowest step when the request is under all of them. 0 is not a step + /// (setBatteryCharge(false) stops charging), and applied_mA is only written + /// when the write actually went through. switch (_pmic) { #if defined (CONFIG_IDF_TARGET_ESP32C3) #elif defined (CONFIG_IDF_TARGET_ESP32C6) case pmic_t::pmic_aw32001: - Aw32001.setChargeCurrent(max_mA); - return; + return Aw32001.setChargeCurrent(max_mA, applied_mA); #elif defined (CONFIG_IDF_TARGET_ESP32C61) case pmic_t::pmic_m5pm1: if (M5.getBoard() == board_t::board_M5CoreMatrix) - { + { /// IOE1 G3 selects between two steps: driven low = 650mA, released to + /// an input = 180mA. auto& ioe1 = M5.getIOExpander(0); - if (max_mA >= 650) + const bool select_650mA = (max_mA >= 650); + bool res = ioe1.setPullMode(M5IOE1_Class::gpio3, IOExpander_Base::pull_none); + if (select_650mA) { - ioe1.setPullMode(M5IOE1_Class::gpio3, IOExpander_Base::pull_none); - ioe1.digitalWrite(M5IOE1_Class::gpio3, false); - ioe1.setHighImpedance(M5IOE1_Class::gpio3, false); - ioe1.setDirection(M5IOE1_Class::gpio3, true); + res = ioe1.digitalWrite(M5IOE1_Class::gpio3, false) && res; + res = ioe1.setHighImpedance(M5IOE1_Class::gpio3, false) && res; + res = ioe1.setDirection(M5IOE1_Class::gpio3, true) && res; } else { - ioe1.setPullMode(M5IOE1_Class::gpio3, IOExpander_Base::pull_none); - ioe1.setDirection(M5IOE1_Class::gpio3, false); + res = ioe1.setDirection(M5IOE1_Class::gpio3, false) && res; } + if (!res) { return false; } + if (applied_mA) { *applied_mA = select_650mA ? 650 : 180; } + return true; } - return; + break; #elif defined (CONFIG_IDF_TARGET_ESP32P4) case pmic_t::pmic_m5pm1: + /// CoreP4X has no charge current control path. (void)max_mA; - return; + break; #else #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) case pmic_t::pmic_ip5306: - Ip5306.setChargeCurrent(max_mA); - return; + return Ip5306.setChargeCurrent(max_mA, applied_mA); case pmic_t::pmic_axp192: - Axp192.setChargeCurrent(max_mA); - return; + return Axp192.setChargeCurrent(max_mA, applied_mA); #endif case pmic_t::pmic_axp2101: - Axp2101.setChargeCurrent(max_mA); - break; + return Axp2101.setChargeCurrent(max_mA, applied_mA); #if defined (CONFIG_IDF_TARGET_ESP32S3) case pmic_t::pmic_m5pm1: - if (M5.getBoard() == board_t::board_M5StampS3Bat) { - if (max_mA >= 650) - M5pm1.setGPIOOutput(M5PM1_Class::gpio3, false); - else - M5pm1.setGPIOOutput(M5PM1_Class::gpio3, true); + if (M5.getBoard() == board_t::board_M5StampS3Bat) + { /// PM1 G3 = CHG_PROG: driven low selects 650mA, left floating (input, + /// no pull) selects 200mA. The pin is never driven high. + const bool select_650mA = (max_mA >= 650); + /// The whole pin setup is part of the transaction, including the mux + /// (mode and latch only take effect while the pin is muxed to GPIO). + /// The pin is released to an input with no pull first, the low latch + /// and driver type are set, the mux is switched while the pin is + /// still an input, and only then (650mA) does it become an output. + /// Every releasing step is attempted even after an earlier failure + /// (each one only moves the pin towards a defined state, so a held + /// high latch is not left driven by a failed release), while the + /// output switch runs only when every step before it went through. + /// applied_mA is only reported when the whole transaction succeeded. + bool res = true; + res = M5pm1.setGPIOPull(M5PM1_Class::gpio3, M5PM1_Class::pull_none) && res; + res = M5pm1.setGPIOMode(M5PM1_Class::gpio3, M5PM1_Class::input) && res; + res = M5pm1.setGPIOOutput(M5PM1_Class::gpio3, false) && res; + res = M5pm1.setGPIODrive(M5PM1_Class::gpio3, M5PM1_Class::push_pull) && res; + res = M5pm1.setGPIOFunction(M5PM1_Class::gpio3, M5PM1_Class::gpio) && res; + if (select_650mA) + { + res = res && M5pm1.setGPIOMode(M5PM1_Class::gpio3, M5PM1_Class::output); } + if (!res) { return false; } + if (applied_mA) { *applied_mA = select_650mA ? 650 : 200; } + return true; + } break; #elif defined (CONFIG_IDF_TARGET_ESP32C5) case pmic_t::pmic_m5pm1: @@ -2624,55 +2760,73 @@ namespace m5 // selection of the opposite current during the mode transition. auto& ioe1 = M5.getIOExpander(0); const bool select_180mA = max_mA < 830; - ioe1.setPullMode(M5IOE1_Class::gpio1, IOExpander_Base::pull_none); - ioe1.digitalWrite(M5IOE1_Class::gpio1, select_180mA); - ioe1.setHighImpedance(M5IOE1_Class::gpio1, false); - ioe1.setDirection(M5IOE1_Class::gpio1, true); + bool res = ioe1.setPullMode(M5IOE1_Class::gpio1, IOExpander_Base::pull_none); + res = ioe1.digitalWrite(M5IOE1_Class::gpio1, select_180mA) && res; + res = ioe1.setHighImpedance(M5IOE1_Class::gpio1, false) && res; + res = ioe1.setDirection(M5IOE1_Class::gpio1, true) && res; + if (!res) { return false; } + if (applied_mA) { *applied_mA = select_180mA ? 180 : 830; } + return true; } - return; + break; #endif #endif default: -#if defined (CONFIG_IDF_TARGET_ESP32P4) - switch (M5.getBoard()) { - case board_t::board_M5Tab5: - case board_t::board_M5Tab5X: { - switch (max_mA) { - case 0: - // charge disable - M5.getIOExpander(1).digitalWrite(7, false); // CHG_EN = HIGH - // qc disable - M5.getIOExpander(1).digitalWrite(5, true); // CHG_EN = LOW - break; - - case 500: - // charge enable - M5.getIOExpander(1).digitalWrite(7, true); // CHG_EN = HIGH - // qc disable - M5.getIOExpander(1).digitalWrite(5, true); // CHG_EN = LOW - break; + break; + } - case 1000: - // charge enable - M5.getIOExpander(1).digitalWrite(7, true); // CHG_EN = HIGH - // qc enable - M5.getIOExpander(1).digitalWrite(5, false); // CHG_EN = LOW - break; + switch (M5.getBoard()) { +#if defined (CONFIG_IDF_TARGET_ESP32P4) + case board_t::board_M5Tab5: + case board_t::board_M5Tab5X: + { /// Two steps, selected by QC (IOE1 G5, active low): 500mA / 1000mA. + /// CHG_EN (G7) is not touched here - it belongs to setBatteryCharge(). + const bool select_1000mA = (max_mA >= 1000); + if (!M5.getIOExpander(1).digitalWrite(5, !select_1000mA)) { return false; } + if (applied_mA) { *applied_mA = select_1000mA ? 1000 : 500; } + return true; + } +#endif + default: + break; + } + return false; + } - default: - break; - } - } - break; + bool Power_Class::_readBatteryCurrent(std::int32_t* mA) + { /// The boards whose charge state is decided by the battery current read it + /// through here, so that a failed read stays distinguishable from 0mA. + /// The public getBatteryCurrent() keeps folding a failure into 0. + if (mA == nullptr) { return false; } + switch (M5.getBoard()) + { +#if defined (CONFIG_IDF_TARGET_ESP32P4) + case board_t::board_M5Tab5: + case board_t::board_M5Tab5X: + { // The shunt is wired so that charge current reads negative; invert to + // match the documented convention (+ = charge / - = discharge). + float ampere; + if (!Ina226.readShuntCurrent(&ere)) { return false; } + *mA = (std::int32_t)(-1000.0f * ampere); + return true; + } +#endif - default: - return; +#if defined (CONFIG_IDF_TARGET_ESP32S3) + case board_t::board_M5PowerHub: + { + uint8_t buf[2]; + if (!M5.In_I2C.readRegister(powerhub_i2c_addr, 0x32, buf, sizeof(buf), i2c_freq)) { return false; } + *mA = -(std::int16_t)((buf[1] << 8) | buf[0]); + return true; } #endif - return; + default: + break; } + return false; } int32_t Power_Class::getBatteryCurrent(void) @@ -2715,17 +2869,17 @@ namespace m5 #if defined (CONFIG_IDF_TARGET_ESP32P4) case board_t::board_M5Tab5: case board_t::board_M5Tab5X: - // The shunt is wired so that charge current reads negative; invert to - // match the documented convention (+ = charge / - = discharge). - return -1000.0f * Ina226.getShuntCurrent(); #endif - #if defined (CONFIG_IDF_TARGET_ESP32S3) case board_t::board_M5PowerHub: - uint8_t buf[2]; - if(M5.In_I2C.readRegister(powerhub_i2c_addr, 0x32, buf, sizeof(buf), i2c_freq)) - return -(int16_t)((buf[1] << 8) | buf[0]); - return 0; +#endif +#if defined (CONFIG_IDF_TARGET_ESP32P4) || defined (CONFIG_IDF_TARGET_ESP32S3) + { /// a failed read is reported as 0mA here; getChargeState() uses the + /// checked path instead. + std::int32_t mA = 0; + _readBatteryCurrent(&mA); + return mA; + } #endif default: return 0; @@ -2733,188 +2887,520 @@ namespace m5 } } - void Power_Class::setChargeVoltage(std::uint16_t max_mV) + bool Power_Class::setChargeVoltage(std::uint16_t max_mV, std::uint16_t* applied_mV) { + (void)max_mV; (void)applied_mV; // some chip builds have no control path at all + if (_identity_unconfirmed) { return false; } // see begin(): the PMIC never answered its ID probe switch (_pmic) { #if defined (CONFIG_IDF_TARGET_ESP32C3) #elif defined (CONFIG_IDF_TARGET_ESP32C6) + /// AW32001_Class::setChargeVoltage exists but is deliberately not wired up: + /// its step selection is defective below the lowest step and corrupts the + /// neighbouring fields. It is fixed separately, and until then the voltage + /// capability bit stays clear. #elif defined (CONFIG_IDF_TARGET_ESP32C61) #elif defined (CONFIG_IDF_TARGET_ESP32P4) #else #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) case pmic_t::pmic_ip5306: - Ip5306.setChargeVoltage(max_mV); - return; + return Ip5306.setChargeVoltage(max_mV, applied_mV); case pmic_t::pmic_axp192: - Axp192.setChargeVoltage(max_mV); - return; + return Axp192.setChargeVoltage(max_mV, applied_mV); #endif case pmic_t::pmic_axp2101: - Axp2101.setChargeVoltage(max_mV); + return Axp2101.setChargeVoltage(max_mV, applied_mV); + +#endif + + default: break; + } + return false; + } + + /// The one place where the reportable state set of each model lives. + /// getChargeState() and getChargeStateCaps() both read it, so the advertised + /// set and the answered value cannot drift apart. + /// The identity of a model is the pair (pmic, board): several board ids ship + /// with different PMICs, and the PMIC is only settled during begin(). + static charge_state_set_t _charge_state_caps(Power_Class::pmic_t pmic, board_t board) + { + switch (pmic) + { +#if defined (CONFIG_IDF_TARGET_ESP32C3) +#elif defined (CONFIG_IDF_TARGET_ESP32C6) + case Power_Class::pmic_t::pmic_aw32001: + return charge_state_t::charging | charge_state_t::not_charging + | charge_state_t::full | charge_state_t::disabled; +#elif defined (CONFIG_IDF_TARGET_ESP32C61) + case Power_Class::pmic_t::pmic_m5pm1: + if (board == board_t::board_M5CoreMatrix) + { + return charge_state_t::charging | charge_state_t::not_charging; + } + break; +#elif defined (CONFIG_IDF_TARGET_ESP32P4) + case Power_Class::pmic_t::pmic_m5pm1: + /// CoreP4X + return charge_state_t::charging | charge_state_t::not_charging; +#else +#if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) + case Power_Class::pmic_t::pmic_ip5306: + return charge_state_t::charging | charge_state_t::not_charging + | charge_state_t::full | charge_state_t::disabled; + + case Power_Class::pmic_t::pmic_axp192: + /// no full: REG01H bit6 means "not charging or charge complete", which + /// does not report completion on its own. + return charge_state_t::charging | charge_state_t::not_charging | charge_state_t::disabled; + +#endif + + case Power_Class::pmic_t::pmic_axp2101: + return charge_state_t::charging | charge_state_t::not_charging + | charge_state_t::full | charge_state_t::disabled + | charge_state_t::discharging | charge_state_t::idle; #endif default: - switch (M5.getBoard()) { + break; + } + + /// Models whose state is not decided by _pmic. (see getChargeState: a + /// pmic_m5pm1 case placed above these would make them unreachable without + /// any diagnostic) + switch (board) + { +#if defined (CONFIG_IDF_TARGET_ESP32S3) + case board_t::board_M5StickS3: // PM1 G0 + case board_t::board_M5PaperDIY: // PM1 G3 + case board_t::board_M5StopWatch: // PM1 G2 + case board_t::board_M5StampS3Bat: // PM1 G2 + case board_t::board_M5ChainCaptain: // IOE1 G3 + case board_t::board_M5PaperS3: // MCU GPIO + case board_t::board_M5PaperMono: // IP2315 0xC7 bit7 + case board_t::board_M5PowerHub: // battery current + /// no disabled: whether the CHG_EN these boards write is actually wired + /// to the charger is unconfirmed, and the PaperColor shows it can be + /// writable and read back while not being connected at all. + return charge_state_t::charging | charge_state_t::not_charging; + + case board_t::board_M5PaperColor: + /// only the negative side is answerable: the charger status reaches + /// neither the PM1 nor the MCU. + return charge_state_set_t() | charge_state_t::not_charging; +#endif + #if defined (CONFIG_IDF_TARGET_ESP32P4) - case board_t::board_M5Tab5: - case board_t::board_M5Tab5X: - // TODO:implement + case board_t::board_M5Tab5: + case board_t::board_M5Tab5X: + /// current based: "no charging" is observed as idle, never as not_charging. + return charge_state_t::charging | charge_state_t::discharging | charge_state_t::idle; #endif + +#if defined (CONFIG_IDF_TARGET_ESP32C5) + case board_t::board_M5ToughC5: + return charge_state_t::charging | charge_state_t::not_charging; +#endif + + default: + break; + } + return charge_state_set_t(); + } + + bool Power_Class::getChargeStateCaps(charge_state_set_t* caps) + { + if (!_initialized || caps == nullptr) { return false; } + *caps = _charge_state_caps(_pmic, M5.getBoard()); + return true; + } + + bool Power_Class::canReport(charge_state_t state) + { + charge_state_set_t caps; + return getChargeStateCaps(&caps) && caps.contains(state); + } + + std::uint8_t Power_Class::getChargeControlCaps(void) + { + if (!_initialized) { return 0; } + switch (_pmic) + { +#if defined (CONFIG_IDF_TARGET_ESP32C3) +#elif defined (CONFIG_IDF_TARGET_ESP32C6) + case pmic_t::pmic_aw32001: + /// no voltage: the driver has it, but it is not wired up. (setChargeVoltage) + return cap_set_charge_enable | cap_set_charge_current; +#elif defined (CONFIG_IDF_TARGET_ESP32C61) + case pmic_t::pmic_m5pm1: + if (M5.getBoard() == board_t::board_M5CoreMatrix) + { + return cap_set_charge_enable | cap_set_charge_current; + } + break; +#elif defined (CONFIG_IDF_TARGET_ESP32P4) + case pmic_t::pmic_m5pm1: + /// CoreP4X: PM1 CHG_EN only. + return cap_set_charge_enable; +#else +#if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) + case pmic_t::pmic_ip5306: + return cap_set_charge_enable | cap_set_charge_current | cap_set_charge_voltage; + + case pmic_t::pmic_axp192: + return cap_set_charge_enable | cap_set_charge_current | cap_set_charge_voltage; + +#endif + + case pmic_t::pmic_axp2101: + return cap_set_charge_enable | cap_set_charge_current | cap_set_charge_voltage; + +#if defined (CONFIG_IDF_TARGET_ESP32S3) + case pmic_t::pmic_m5pm1: + switch (M5.getBoard()) + { + case board_t::board_M5StampS3Bat: + return cap_set_charge_enable | cap_set_charge_current; + + case board_t::board_M5PaperColor: + /// CHG_EN_PP is not connected to the charger. + return 0; + + case board_t::board_M5StickS3: + case board_t::board_M5PaperDIY: + case board_t::board_M5StopWatch: + case board_t::board_M5ChainCaptain: + case board_t::board_M5PaperMono: + return cap_set_charge_enable; + default: - return; + break; } + break; +#elif defined (CONFIG_IDF_TARGET_ESP32C5) + case pmic_t::pmic_m5pm1: + if (M5.getBoard() == board_t::board_M5ToughC5) + { + return cap_set_charge_enable | cap_set_charge_current; + } + break; +#endif + +#endif + + default: + break; } + + switch (M5.getBoard()) + { +#if defined (CONFIG_IDF_TARGET_ESP32P4) + case board_t::board_M5Tab5: + case board_t::board_M5Tab5X: + return cap_set_charge_enable | cap_set_charge_current; +#endif +#if defined (CONFIG_IDF_TARGET_ESP32S3) + case board_t::board_M5PowerHub: + return cap_set_charge_enable; +#endif + default: + break; + } + return 0; } - Power_Class::is_charging_t Power_Class::isCharging(void) + charge_state_t Power_Class::_getChargeState(void) { + if (!_initialized) { return charge_state_t::not_initialized; } + if (_identity_unconfirmed) { return charge_state_t::io_error; } // see begin(): the PMIC never answered its ID probe + switch (_pmic) { #if defined (CONFIG_IDF_TARGET_ESP32C3) #elif defined (CONFIG_IDF_TARGET_ESP32C6) case pmic_t::pmic_aw32001: - return Aw32001.isCharging() ? is_charging_t::is_charging : is_charging_t::is_discharging; + { + auto status = Aw32001.getChargeStatus(); + if (status == AW32001_Class::CS_UNKNOWN) { return charge_state_t::io_error; } + if (status == AW32001_Class::CS_PRE_CHARGE + || status == AW32001_Class::CS_CHARGE) { return charge_state_t::charging; } + if (status == AW32001_Class::CS_CHARGE_DONE) { return charge_state_t::full; } + bool enabled; + if (!Aw32001.getBatteryCharge(&enabled)) { return charge_state_t::io_error; } + return enabled ? charge_state_t::not_charging : charge_state_t::disabled; + } #elif defined (CONFIG_IDF_TARGET_ESP32C61) case pmic_t::pmic_m5pm1: /// CoreMatrix: the AW32901 CHG_STAT is wired to IOE1 G8 (low = charging) if (M5.getBoard() == board_t::board_M5CoreMatrix) { - /// With no battery the charger retries periodically and CHG_STAT - /// blips low for a moment; report "not charging" instead. - { - std::int8_t present = _batteryPresent(); - if (present < 0) { return is_charging_t::charge_unknown; } - if (present == 0) { return is_charging_t::is_discharging; } - } - bool level; - if (!M5.getIOExpander(0).getInputLevel(M5IOE1_Class::gpio8, &level)) - { /// do not report an I2C failure as "charging" - return is_charging_t::charge_unknown; - } - return level ? is_charging_t::is_discharging : is_charging_t::is_charging; + return _chargeStateFromChgStat(); } - return is_charging_t::charge_unknown; + break; #elif defined (CONFIG_IDF_TARGET_ESP32P4) case pmic_t::pmic_m5pm1: - { + { /// CoreP4X: the charger status is wired to IOE1 G6 (low = charging). + /// There is no battery presence signal to gate it with. bool level; - if (!M5.getIOExpander(0).getInputLevel(M5IOE1_Class::gpio6, &level)) { - return is_charging_t::charge_unknown; + if (!M5.getIOExpander(0).getInputLevel(M5IOE1_Class::gpio6, &level)) + { /// do not report a failed read as "charging" + return charge_state_t::io_error; } - return level ? is_charging_t::is_discharging : is_charging_t::is_charging; + return level ? charge_state_t::not_charging : charge_state_t::charging; } #else #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) case pmic_t::pmic_ip5306: - return Ip5306.isCharging() ? is_charging_t::is_charging : is_charging_t::is_discharging; + { /// The enable setting (SYS_CTL0 bit4) and its effective value + /// (REG_READ0 bit3) are different registers: the setting survives an + /// unplugged supply, the effective one drops with it. Reading both is + /// what separates "the user disabled it" from "there is no supply". + /// Limit: the IP5306 has no battery presence signal at all, so a board + /// running with no cell installed still reports charging. + bool flag; + if (!Ip5306.getBatteryCharge(&flag)) { return charge_state_t::io_error; } + if (!flag) { return charge_state_t::disabled; } + if (!Ip5306.readChargeActive(&flag)) { return charge_state_t::io_error; } + if (!flag) { return charge_state_t::not_charging; } + if (!Ip5306.readChargeFull(&flag)) { return charge_state_t::io_error; } + return flag ? charge_state_t::full : charge_state_t::charging; + } case pmic_t::pmic_axp192: - return Axp192.isCharging() ? is_charging_t::is_charging : is_charging_t::is_discharging; + { /// REG00H bit2 is the battery current direction and REG33H bit7 the + /// charger enable. Completion has no bit of its own here, so full is + /// never reported. The enable register is only read once the first + /// step has not decided: a charge in progress does not depend on it. + bool charging, enabled; + if (!Axp192.readChargeActive(&charging)) { return charge_state_t::io_error; } + if (charging) { return charge_state_t::charging; } + if (!Axp192.getBatteryCharge(&enabled)) { return charge_state_t::io_error; } + return enabled ? charge_state_t::not_charging : charge_state_t::disabled; + } #endif case pmic_t::pmic_axp2101: - return Axp2101.isCharging() ? is_charging_t::is_charging : is_charging_t::is_discharging; - // return Axp2101.getChargeDirection() ? is_charging_t::is_charging : is_charging_t::is_discharging; + { /// One REG01H read carries both the charger state machine (bit[2:0]) + /// and the battery current direction (bit[6:5]). + /// Order matters: the charger is asked first, and only once it says it + /// is not charging does the current direction narrow the answer down. + std::uint8_t status; + if (!Axp2101.readPmuStatus2(&status)) { return charge_state_t::io_error; } + std::uint8_t machine = status & 0x07; + /// 0b000-0b011 = trickle / pre / constant current / constant voltage + if (machine <= 0x03) { return charge_state_t::charging; } + /// 0b100 = charge done. (disabling the charger clears it, so this does + /// not shadow the disabled branch below) + if (machine == 0x04) { return charge_state_t::full; } + bool enabled; + if (!Axp2101.getBatteryCharge(&enabled)) { return charge_state_t::io_error; } + if (!enabled) { return charge_state_t::disabled; } + switch (status & 0x60) + { + case 0x40: return charge_state_t::discharging; + case 0x00: return charge_state_t::idle; + default: break; + } + return charge_state_t::not_charging; + } #endif default: - switch (M5.getBoard()) { -#if defined (CONFIG_IDF_TARGET_ESP32S3) - case board_t::board_M5PaperMono: - { - // No external power -> not charging. PWR_SRC is a bitmap, and the battery bit may coexist with VIN. - auto sources = M5pm1.getPowerSource(); - if (!(sources & (M5PM1_Class::vin | M5PM1_Class::vinout))) { return is_charging_t::is_discharging; } - // External power present. The IP2316 charger reports - // its state in REG_CHG_STAT(0xC7): bit7 = charging in progress (measured: - // 0x82 charging / 0x45 charge-complete / 0x00 charge-disabled). - set_papermono_ip2315_enabled(true); - is_charging_t res = is_charging_t::is_discharging; - if (wait_papermono_ip2315_ready()) - { - uint8_t chg_stat = M5.In_I2C.readRegister8(ip2315_i2c_addr, 0xC7, i2c_freq); - res = (chg_stat & (1 << 7)) ? is_charging_t::is_charging : is_charging_t::is_discharging; - } - set_papermono_ip2315_enabled(false); - return res; - } - break; + break; + } - case board_t::board_M5StickS3: + switch (M5.getBoard()) { +#if defined (CONFIG_IDF_TARGET_ESP32S3) + case board_t::board_M5PaperMono: + { + // No external power -> not charging. PWR_SRC is a bitmap, and the battery bit may coexist with VIN. + M5PM1_Class::pwr_src_t sources; + if (!M5pm1.getPowerSource(&sources)) { return charge_state_t::io_error; } + if (!(sources & (M5PM1_Class::vin | M5PM1_Class::vinout))) { return charge_state_t::not_charging; } + // External power present. The IP2316 charger reports + // its state in REG_CHG_STAT(0xC7): bit7 = charging in progress (measured: + // 0x82 charging / 0x45 charge-complete / 0x00 charge-disabled). + // The other bits carry meaning too, but their definition is unconfirmed, + // so only bit7 is used and full / disabled are not reported. + /// a charger that does not answer through the gate is an unfinished + /// procedure, not missing evidence: io_error, not undetermined. The + /// gate writes themselves are part of the procedure, so a failed open + /// or close is io_error as well. + charge_state_t res = charge_state_t::io_error; + uint8_t chg_stat; + if (set_papermono_ip2315_enabled(true) + && wait_papermono_ip2315_ready() + && M5.In_I2C.readRegister(ip2315_i2c_addr, 0xC7, &chg_stat, 1, i2c_freq)) { - // PM1_G0 is charging status input pin, low=charging / high=not charging - return M5pm1.getGPIOInput(M5PM1_Class::gpio0) ? is_charging_t::is_discharging : is_charging_t::is_charging; + res = (chg_stat & (1 << 7)) ? charge_state_t::charging : charge_state_t::not_charging; } - break; + if (!set_papermono_ip2315_enabled(false)) { res = charge_state_t::io_error; } + return res; + } - case board_t::board_M5PaperDIY: - { - // PM1_G3 is CHG_STAT, low=charging / high=not charging - return M5pm1.getGPIOInput(M5PM1_Class::gpio3) ? is_charging_t::is_discharging : is_charging_t::is_charging; - } - break; - - case board_t::board_M5StopWatch: // M5PM1_G2 - case board_t::board_M5StampS3Bat: // M5PM1_G2 + case board_t::board_M5PaperColor: + { /// The charger status is wired to neither the PM1 nor the MCU, so only + /// the negative side can be answered: with no external power there can + /// be no charging. Anything else stays undetermined. + M5PM1_Class::pwr_src_t sources; + if (!M5pm1.getPowerSource(&sources)) { return charge_state_t::io_error; } + if (!(sources & (M5PM1_Class::vin | M5PM1_Class::vinout))) { return charge_state_t::not_charging; } + return charge_state_t::undetermined; + } + + case board_t::board_M5StickS3: // PM1 G0 + case board_t::board_M5PaperDIY: // PM1 G3 + case board_t::board_M5StopWatch: // PM1 G2 + case board_t::board_M5StampS3Bat: // PM1 G2 + { /// CHG_STAT on a PM1 GPIO, low = charging. + auto pin = (M5.getBoard() == board_t::board_M5StickS3) ? M5PM1_Class::gpio0 + : (M5.getBoard() == board_t::board_M5PaperDIY) ? M5PM1_Class::gpio3 + : M5PM1_Class::gpio2; + std::uint8_t bits; + /// getGPIOInput() folds a failed read into "high"; read the whole + /// register instead so that a failure is not reported as not_charging. + if (!M5pm1.getGPIOInputBits(&bits)) { return charge_state_t::io_error; } + return (bits & (1 << (std::uint8_t)pin)) ? charge_state_t::not_charging : charge_state_t::charging; + } + + case board_t::board_M5ChainCaptain: + { /// CHG_STAT is on IOE1 G3, low = charging. + bool level; + if (!M5.getIOExpander(0).getInputLevel(M5IOE1_Class::gpio3, &level)) { - // PM1_G2 is charging status input pin, low=charging / high=not charging - return M5pm1.getGPIOInput(M5PM1_Class::gpio2) ? is_charging_t::is_discharging : is_charging_t::is_charging; + return charge_state_t::io_error; } - break; - - case board_t::board_M5ChainCaptain: - return M5.getIOExpander(0).digitalRead(M5IOE1_Class::gpio3) - ? is_charging_t::is_discharging - : is_charging_t::is_charging; - break; + return level ? charge_state_t::not_charging : charge_state_t::charging; + } case board_t::board_M5PaperS3: - return (m5gfx::gpio_in(M5PaperS3_CHG_STAT_PIN) == false) ? is_charging_t::is_charging : is_charging_t::is_discharging; + /// CHG_STAT is wired to an MCU pin, so there is no failing read here: + /// this is the only model that never answers io_error. + return (m5gfx::gpio_in(M5PaperS3_CHG_STAT_PIN) == false) + ? charge_state_t::charging : charge_state_t::not_charging; - case board_t::board_M5PowerHub: // 0x50 reg is not accurate - return (getBatteryCurrent() > 10) ? is_charging_t::is_charging : is_charging_t::is_discharging; + case board_t::board_M5PowerHub: + { /// battery current over a threshold. The accuracy of this reading is + /// not trusted, so the sign is not used to tell discharging from idle. + std::int32_t mA; + if (!_readBatteryCurrent(&mA)) { return charge_state_t::io_error; } + return (mA > 10) ? charge_state_t::charging : charge_state_t::not_charging; + } #endif #if defined (CONFIG_IDF_TARGET_ESP32P4) case board_t::board_M5Tab5: case board_t::board_M5Tab5X: - return M5.getIOExpander(1).digitalRead(6) // io1.gpio6 == CHG_STAT - ? is_charging_t::is_charging : is_charging_t::is_discharging; + { /// The INA226 battery current decides the state. + /// IOE1 G6 is deliberately not used: it is the IP2326 BAT_STAT, which + /// only marks the trickle / constant-current stage and reads high both + /// while charging and while charging is disabled. + /// not_charging is never reported here - a stopped charge is observed + /// as idle. + std::int32_t mA; + if (!_readBatteryCurrent(&mA)) { return charge_state_t::io_error; } + static constexpr std::int32_t threshold_mA = 10; + if (mA > threshold_mA) { return charge_state_t::charging; } + if (mA < -threshold_mA) { return charge_state_t::discharging; } + return charge_state_t::idle; + } #endif #if defined (CONFIG_IDF_TARGET_ESP32C5) case board_t::board_M5ToughC5: - { // The LGS4056 CHG_STAT is wired to IOE1 G3, low=charging / high=not charging. // Near full charge it alternates with the charger's top-off cycle (~10-20s). - { /// with no battery the charger can still assert CHG_STAT briefly; - /// report "not charging" instead. - std::int8_t present = _batteryPresent(); - if (present < 0) { return is_charging_t::charge_unknown; } - if (present == 0) { return is_charging_t::is_discharging; } - } - bool level; - if (!M5.getIOExpander(0).getInputLevel(M5IOE1_Class::gpio3, &level)) - { // do not report an I2C failure as "charging" - return is_charging_t::charge_unknown; - } - return level ? is_charging_t::is_discharging : is_charging_t::is_charging; - } + return _chargeStateFromChgStat(); #endif default: - return is_charging_t::charge_unknown; + break; + } + return charge_state_t::unsupported; + } + + charge_state_t Power_Class::getChargeState(void) + { + charge_state_t res = _getChargeState(); +#if defined (M5UNIFIED_PC_BUILD) || defined (M5UNIFIED_CHECK_CHARGE_STATE_CAPS) + /// The procedure and the capability table are written separately, so the + /// two are cross checked where a failing check can actually be seen: a + /// state that is not advertised would silently break every caller that + /// asked getChargeStateCaps() what to expect. + assert(!charge_states_known.contains(res) + || _charge_state_caps(_pmic, M5.getBoard()).contains(res)); +#endif + return res; + } + + bool Power_Class::isCharging(void) + { + return charge_states_any_charging.contains(getChargeState()); + } + + battery_presence_t Power_Class::getBatteryPresence(void) + { + if (!_initialized) { return battery_presence_t::not_initialized; } + if (_identity_unconfirmed) { return battery_presence_t::io_error; } // see begin(): the PMIC never answered its ID probe + + switch (_pmic) + { +#if defined (CONFIG_IDF_TARGET_ESP32C3) +#elif defined (CONFIG_IDF_TARGET_ESP32C6) +#elif defined (CONFIG_IDF_TARGET_ESP32C61) + case pmic_t::pmic_m5pm1: + if (M5.getBoard() == board_t::board_M5CoreMatrix) + { /// there is no presence signal: it is inferred, and stays undetermined + /// until the first verdict. (see _batteryPresent) The verdict is + /// cached across failed reads, so the bus is probed first: a dead bus + /// is io_error, not the last verdict. + bool io_ok; + std::int8_t bp = _batteryPresent(&io_ok); + if (!io_ok) { return battery_presence_t::io_error; } + return (bp < 0) ? battery_presence_t::undetermined + : (bp == 0) ? battery_presence_t::absent + : battery_presence_t::present; } + break; +#elif defined (CONFIG_IDF_TARGET_ESP32P4) +#else + + case pmic_t::pmic_axp2101: + { /// REG00H bit3 follows an attach / detach. + std::uint8_t status; + if (!Axp2101.readPmuStatus1(&status)) { return battery_presence_t::io_error; } + return (status & 0x08) ? battery_presence_t::present : battery_presence_t::absent; + } + +#if defined (CONFIG_IDF_TARGET_ESP32C5) + case pmic_t::pmic_m5pm1: + if (M5.getBoard() == board_t::board_M5ToughC5) + { /// see the CoreMatrix note above. + bool io_ok; + std::int8_t bp = _batteryPresent(&io_ok); + if (!io_ok) { return battery_presence_t::io_error; } + return (bp < 0) ? battery_presence_t::undetermined + : (bp == 0) ? battery_presence_t::absent + : battery_presence_t::present; + } + break; +#endif + +#endif + + default: + break; } + return battery_presence_t::unsupported; } float Power_Class::_readExtValue(ext_port_mask_t port_mask, bool is_voltage) diff --git a/src/utility/Power_Class.hpp b/src/utility/Power_Class.hpp index 4c697250..952b859d 100644 --- a/src/utility/Power_Class.hpp +++ b/src/utility/Power_Class.hpp @@ -65,6 +65,114 @@ namespace m5 bool direction = 0; }; + /// Battery charge state. + /// The sign carries the meaning: negative = the state could not be obtained, + /// 0 = this model can never obtain it, positive = a state was obtained. + /// @note The numeric values are fixed. They are burned into the caller's + /// translation unit as immediates, so new values may only be appended and a + /// removed value stays retired forever. + enum class charge_state_t : std::int8_t + { not_initialized = -3 ///< M5.begin() has not completed yet. + , io_error = -2 ///< the read procedure failed. (I2C NACK, charger gate timeout, ...) + , undetermined = -1 ///< the read succeeded, but the available signals do not decide a single state. + , unsupported = 0 ///< this model can never assert any positive state. + , charging = 1 ///< charging. + , not_charging = 2 ///< not charging. (nothing more can be said) + , full = 3 ///< the charger reports the charge as complete. + , disabled = 4 ///< charging is disabled. + , discharging = 5 ///< not charging, and the battery is being drained. + , idle = 6 ///< not charging, and nothing flows in or out of the battery. + }; + /// The highest defined state. Bump it when a state is appended, so that the + /// set type keeps rejecting values above it. + constexpr charge_state_t charge_state_max = charge_state_t::idle; + + class charge_state_set_t; + constexpr charge_state_set_t operator|(charge_state_t a, charge_state_t b); + constexpr charge_state_set_t operator|(charge_state_set_t s, charge_state_t v); + + /// A set of charge states. + /// @note Negative values and 0 are members of no set and match no set, on the + /// construction side as well as on the query side. Out of range values are + /// ignored the same way. Carrying that guarantee in the type is the reason + /// this class exists: written by hand as "(int)state & mask", io_error would + /// match almost every mask and a failed read would be reported as + /// "not charging". + class charge_state_set_t + { + public: + typedef std::uint32_t storage_type; + + constexpr charge_state_set_t(void) : _bits { 0 } {} + + /// @return true if the state is a member of this set. A non positive state is never a member. + constexpr bool contains(charge_state_t state) const { return (_bits & _bit_of(state)) != 0; } + + /// @return true if at least one state is a member of both sets. + constexpr bool intersects(charge_state_set_t other) const { return (_bits & other._bits) != 0; } + + /// @return true if this set holds no state at all. + constexpr bool empty(void) const { return _bits == 0; } + + private: + constexpr explicit charge_state_set_t(storage_type bits) : _bits { bits } {} + + /// Range checked bit position. Only the defined positive states map to a + /// bit: negative values, 0 and anything above charge_state_max map to no + /// bit at all, so they can neither be added to a set nor match one. The + /// check comes before the shift (a negative or too wide shift is + /// undefined behaviour). + static constexpr storage_type _bit_of(charge_state_t state) + { + return ((std::int8_t)state > 0 && (std::int8_t)state <= (std::int8_t)charge_state_max) + ? (storage_type)1 << (std::int8_t)state + : (storage_type)0; + } + + storage_type _bits; + + friend constexpr charge_state_set_t operator|(charge_state_t a, charge_state_t b); + friend constexpr charge_state_set_t operator|(charge_state_set_t s, charge_state_t v); + }; + + constexpr charge_state_set_t operator|(charge_state_t a, charge_state_t b) + { return charge_state_set_t(charge_state_set_t::_bit_of(a) | charge_state_set_t::_bit_of(b)); } + + constexpr charge_state_set_t operator|(charge_state_set_t s, charge_state_t v) + { return charge_state_set_t(s._bits | charge_state_set_t::_bit_of(v)); } + + /// The highest state must stay inside the storage of the set type. + static_assert((int)charge_state_max < (int)(sizeof(charge_state_set_t::storage_type) * 8) + , "charge_state_t no longer fits in charge_state_set_t::storage_type"); + + /// Every positive state. Use it to ask whether a state was obtained at all. + constexpr charge_state_set_t charge_states_known + = charge_state_t::charging | charge_state_t::not_charging + | charge_state_t::full | charge_state_t::disabled + | charge_state_t::discharging | charge_state_t::idle; + + /// The states that mean "charging". (a future trickle state would be added here) + constexpr charge_state_set_t charge_states_any_charging + = charge_state_set_t() | charge_state_t::charging; + + /// The states that mean "not charging". + /// full belongs here: it only claims that the charger reports completion, not + /// that nothing is being drawn from the battery. + constexpr charge_state_set_t charge_states_any_not_charging + = charge_state_t::not_charging | charge_state_t::full | charge_state_t::disabled + | charge_state_t::discharging | charge_state_t::idle; + + /// Whether a battery is attached. + /// The sign rule is the same as charge_state_t. + enum class battery_presence_t : std::int8_t + { not_initialized = -3 ///< M5.begin() has not completed yet. + , io_error = -2 ///< the read procedure failed. + , undetermined = -1 ///< the read succeeded, but the presence is not decided yet. + , unsupported = 0 ///< this model can never tell. + , absent = 1 ///< no battery is attached. + , present = 2 ///< a battery is attached. + }; + class Power_Class { friend M5Unified; @@ -80,12 +188,23 @@ namespace m5 , pmic_m5pm1 }; + /// @deprecated No function returns this any more; it will be removed in the + /// next release. Use charge_state_t. (no attribute is attached on purpose) enum is_charging_t { is_discharging = 0 , is_charging , charge_unknown }; + /// The charge control paths a model provides. (see getChargeControlCaps) + /// @note This mask is deliberately separate from the state set, so that a + /// state value never gets tied to a bit position. + enum charge_control_capability_t : std::uint8_t + { cap_set_charge_enable = 1u << 0 + , cap_set_charge_current = 1u << 1 + , cap_set_charge_voltage = 1u << 2 + }; + bool begin(void); /// Set power output of the external ports. @@ -163,23 +282,93 @@ namespace m5 /// set battery charge enable. /// @param enable true=enable / false=disable - void setBatteryCharge(bool enable); + /// @return true if the path exists and the write succeeded. + /// @note false only means that the requested setting did not take effect; + /// it says nothing about what the hardware currently holds. + bool setBatteryCharge(bool enable); /// set battery charge current /// @param max_mA milli ampere. + /// @param applied_mA optional. receives the step that was applied. + /// @return true if the path exists and the write succeeded. + /// @note The highest step not exceeding max_mA is selected; a request below + /// the lowest step is clamped up to it instead of being rejected. 0 is not a + /// step: use setBatteryCharge(false) to stop charging. + /// @note applied_mA is left untouched when false is returned. /// @note CoreMatrix selects 180 mA below 650 mA, otherwise 650 mA. /// @note ToughC5 selects 180 mA below 830 mA, otherwise 830 mA. - /// @attention Non-functioning models : CoreInk , M5Paper , M5Stack(with non I2C IP5306) - void setChargeCurrent(std::uint16_t max_mA); + /// @note 0 is not a step: where a current path exists it selects the + /// lowest one, and a warning is logged once. Use setBatteryCharge(false) + /// to stop charging. + /// @note Tab5 / Tab5X select 500 mA below 1000 mA, otherwise 1000 mA. + /// @note StampS3Bat selects 200 mA below 650 mA, otherwise 650 mA. + /// @attention Returns false on models without a current control path; see + /// getChargeControlCaps(). The M5Stack with a non-I2C IP5306 also returns false. + bool setChargeCurrent(std::uint16_t max_mA, std::uint16_t* applied_mA = nullptr); /// set battery charge voltage /// @param max_mV milli volt. - /// @attention Non-functioning models : CoreInk , M5Paper , M5Stack(with non I2C IP5306) - void setChargeVoltage(std::uint16_t max_mV); + /// @param applied_mV optional. receives the step that was applied. + /// @return true if the path exists and the write succeeded. + /// @note The step selection rule is the same as setChargeCurrent. + /// @note applied_mV is left untouched when false is returned. + /// @attention Returns false on models without a voltage control path; see + /// getChargeControlCaps(). The M5Stack with a non-I2C IP5306 also returns false. + bool setChargeVoltage(std::uint16_t max_mV, std::uint16_t* applied_mV = nullptr); + + /// Get which charge control paths this model provides. + /// @return bitmask of charge_control_capability_t. 0 = no control path, + /// which is also what is returned before M5.begin() has completed. + /// @note Only the set side is declared here; there is no readback getter + /// yet, so no readback capability is advertised. + std::uint8_t getChargeControlCaps(void); + + /// Get the battery charge state. + /// @return a single charge_state_t value. Never a set. + /// @note The idiom is a set test on both sides: + /// @code + /// auto s = M5.Power.getChargeState(); + /// if (charge_states_any_charging.contains(s)) { /* charging */ } + /// else if (charge_states_any_not_charging.contains(s)) { /* not charging (how detailed depends on the model) */ } + /// else { /* could not be obtained */ } + /// @endcode + /// Comparing against a single value ( s == charge_state_t::not_charging ) + /// is model dependent and breaks silently when a model learns to report a + /// more detailed state, so both branches use a set. + /// @attention The last branch merges four different reasons: + /// not_initialized (called too early) / io_error (not readable right now) / + /// undetermined (not enough evidence) / unsupported (model can never tell). + /// Look at the value itself to tell them apart. + charge_state_t getChargeState(void); + + /// Get the set of states this model can report. + /// @param caps output parameter, receives the set of reportable states. + /// @return false before M5.begin() has completed. caps is left untouched then. + /// @note An empty set after M5.begin() means getChargeState() always answers + /// unsupported. The set is decided once during M5.begin() and does not + /// change afterwards: a communication failure is reported as io_error and + /// never shrinks the set. The one exception is a board that carries either + /// an AXP192 or an AXP2101 whose chip-ID probes all failed during + /// M5.begin(): it runs with the board default (AXP192) until an explicit + /// M5.Power.begin() gets a positive ID, which can widen the set. + bool getChargeStateCaps(charge_state_set_t* caps); + + /// @return true if the caps could be obtained and they contain the state. + bool canReport(charge_state_t state); /// Get whether the battery is currently charging or not. - /// @attention Non-functioning models : CoreInk , M5Paper , M5Stack(with non I2C IP5306) - is_charging_t isCharging(void); + /// @return true only while charging. Everything else - full, disabled, + /// unknown, and a failed read - returns false. + /// @note This is charge_states_any_charging.contains(getChargeState()). + /// @attention Always false on models that cannot report charging; see + /// canReport(charge_state_t::charging). The M5Stack with a non-I2C IP5306 + /// also returns false (io_error). + bool isCharging(void); + + /// Get whether a battery is attached. + /// @note The existing sentinels of getBatteryVoltage() (0 / -1) are kept + /// unchanged for compatibility; this is the typed way to ask. + battery_presence_t getBatteryPresence(void); /// Get VBUS voltage /// @return VBUS voltage [mV] / -1=not supported model @@ -262,18 +451,30 @@ namespace m5 private: /// CoreS3 family: AW9523 のビット操作を setExtOutput / setUsbOutput と同じ排他区間で行う (スピーカー制御用) static void _core_s3_aw9523_bit(uint8_t reg, uint8_t mask, bool on); + /// Battery current with I2C error reporting, for the boards whose charge + /// state is decided by the current. @return false = not readable / no path. + bool _readBatteryCurrent(std::int32_t* mA); + /// The state procedure itself. getChargeState() wraps it with the check + /// that the answer is one of the advertised capabilities. + charge_state_t _getChargeState(void); std::int32_t _getBatteryAdcRaw(void); void _powerOff(bool withTimer); void _timerSleep(void); #if defined (CONFIG_IDF_TARGET_ESP32C5) || defined (CONFIG_IDF_TARGET_ESP32C61) /// Check whether a battery is actually attached (non-blocking). - /// @return 1=present / 0=absent / -1=not yet determined - std::int8_t _batteryPresent(void); + /// @param io_ok optional. receives false if any read of this evaluation failed. + /// @return 1=present / 0=absent / -1=not yet determined (cached across failed reads) + std::int8_t _batteryPresent(bool* io_ok = nullptr); + /// Drop the transient presence evidence (streak, sample baseline, counters); the verdict is kept. + void _bp_dropEvidence(void); /// Read the raw charger CHG_STAT line. @return false=not readable bool _readChargeStat(bool* level); /// Whether the VBAT node is confirmed collapsed (false when unreadable). - bool _vbatNodeDown(void); + /// @param io_ok optional. receives false when the node state could not be read. + bool _vbatNodeDown(bool* io_ok = nullptr); + /// CHG_STAT behind the battery presence gate. (ToughC5 / CoreMatrix) + charge_state_t _chargeStateFromChgStat(void); /// Battery presence. -1 = not yet determined. std::int8_t _batt_present = -1; /// Tick when charging last stopped (0 = at reset, which clears PWR_CFG). @@ -294,6 +495,12 @@ namespace m5 float _readExtValue(ext_port_mask_t port_mask, bool is_voltage); float _adc_ratio = 0; + /// true once M5Unified::begin() has completed (set there, not in + /// Power_Class::begin()). _pmic alone cannot tell a pmic_unknown model + /// from a not yet initialized one. + bool _initialized = false; + bool _identity_settled = false; ///< a probe answered with a positive chip ID (AXP192 / AXP2101 boards) + bool _identity_unconfirmed = false; ///< every chip-ID probe failed: the board default is provisional (see begin()) std::uint8_t _wakeupPin = 255; std::uint8_t _rtcIntPin = 255; pmic_t _pmic = pmic_t::pmic_unknown; diff --git a/src/utility/power/AW32001_Class.cpp b/src/utility/power/AW32001_Class.cpp index 7243aa70..f74a5439 100644 --- a/src/utility/power/AW32001_Class.cpp +++ b/src/utility/power/AW32001_Class.cpp @@ -51,14 +51,26 @@ namespace m5 } } - bool AW32001_Class::setChargeCurrent(std::uint16_t max_mA) + bool AW32001_Class::getBatteryCharge(bool* enabled) + { + uint8_t reg_value = 0; + if (!_init || enabled == nullptr) { return false; } + if (!readRegister(AW32001_REG_PWR_CFG, ®_value, 1)) { return false; } + // bit3 disables charging, so the enable state is its inverse. + *enabled = (reg_value & (1 << 3)) == 0; + return true; + } + + bool AW32001_Class::setChargeCurrent(std::uint16_t max_mA, std::uint16_t* applied_mA) { if (!_init) return false; int value = max_mA / 8; // Convert mA to register value (8mA per step) if (value > 0) { value -= 1; // 0 = 8mA, 63 = 512mA if (value >= 64) value = 63; // max value is 512mA (8 + 63*8) } - return writeRegister8(AW32001_REG_CHR_CUR, value); + if (!writeRegister8(AW32001_REG_CHR_CUR, value)) { return false; } + if (applied_mA) { *applied_mA = (std::uint16_t)((value + 1) * 8); } + return true; } bool AW32001_Class::setChargeVoltage(std::uint16_t max_mV) diff --git a/src/utility/power/AW32001_Class.hpp b/src/utility/power/AW32001_Class.hpp index 1862e5bc..7db104ba 100644 --- a/src/utility/power/AW32001_Class.hpp +++ b/src/utility/power/AW32001_Class.hpp @@ -32,9 +32,17 @@ namespace m5 /// @param enable true=enable / false=disable bool setBatteryCharge(bool enable); + /// get battery charge enable state with I2C error reporting. + /// (PWR_CFG bit3, inverted : the register bit disables charging) + /// @param enabled output parameter, receives the charge enable state. + /// @return false on I2C failure. + bool getBatteryCharge(bool* enabled); + /// set battery charge current /// @param max_mA milli ampere. (8 - 512). - bool setChargeCurrent(std::uint16_t max_mA); + /// @param applied_mA optional. receives the step that was applied. + /// @return false on I2C failure. applied_mA is left untouched then. + bool setChargeCurrent(std::uint16_t max_mA, std::uint16_t* applied_mA = nullptr); /// set battery charge voltage /// @param max_mV milli volt. (3600 - 4545). diff --git a/src/utility/power/AXP192_Class.cpp b/src/utility/power/AXP192_Class.cpp index 06b20806..85967051 100644 --- a/src/utility/power/AXP192_Class.cpp +++ b/src/utility/power/AXP192_Class.cpp @@ -143,17 +143,16 @@ namespace m5 writeRegister8(0x95, reg0x95 | (num ? 0x84 : 0x81)); // set GPIO mode } - void AXP192_Class::setBatteryCharge(bool enable) + bool AXP192_Class::setBatteryCharge(bool enable) { std::uint8_t val = 0; - if (readRegister(0x33, &val, 1)) - { - writeRegister8(0x33, (val & 0x7F) + (enable ? 0x80 : 0x00)); - } + if (!readRegister(0x33, &val, 1)) { return false; } + return writeRegister8(0x33, (val & 0x7F) + (enable ? 0x80 : 0x00)); } - void AXP192_Class::setChargeCurrent(std::uint16_t max_mA) - { + bool AXP192_Class::setChargeCurrent(std::uint16_t max_mA, std::uint16_t* applied_mA) + { /// reg 0x33 bit3:0 selects the step. table[] holds the steps above the + /// lowest one (100mA) in units of 10mA, so index i selects step i. max_mA /= 10; if (max_mA > 132) { max_mA = 132; } static constexpr std::uint8_t table[] = { 19, 28, 36, 45, 55, 63, 70, 78, 88, 96, 100, 108, 116, 124, 132, 255 }; @@ -162,13 +161,13 @@ namespace m5 while (table[i] <= max_mA) { ++i; } std::uint8_t val = 0; - if (readRegister(0x33, &val, 1)) - { - writeRegister8(0x33, (val & 0xF0) + i); - } + if (!readRegister(0x33, &val, 1)) { return false; } + if (!writeRegister8(0x33, (val & 0xF0) + i)) { return false; } + if (applied_mA) { *applied_mA = i ? (std::uint16_t)(table[i - 1] * 10) : 100; } + return true; } - void AXP192_Class::setChargeVoltage(std::uint16_t max_mV) + bool AXP192_Class::setChargeVoltage(std::uint16_t max_mV, std::uint16_t* applied_mV) { /// reg 0x33 bit6:5 selects the target voltage. Compare in millivolts: /// the earlier form subtracted a bias first, which underflowed the /// unsigned argument for anything below the lowest step and then clamped @@ -180,10 +179,26 @@ namespace m5 while (i && table[i] > max_mV) { --i; } std::uint8_t val = 0; - if (readRegister(0x33, &val, 1)) - { - writeRegister8(0x33, (val & 0x9F) + (i << 5)); - } + if (!readRegister(0x33, &val, 1)) { return false; } + if (!writeRegister8(0x33, (val & 0x9F) + (i << 5))) { return false; } + if (applied_mV) { *applied_mV = table[i]; } + return true; + } + + bool AXP192_Class::readChargeActive(bool* charging) + { + std::uint8_t val = 0; + if (charging == nullptr || !readRegister(0x00, &val, 1)) { return false; } + *charging = (val & 0x04) != 0; + return true; + } + + bool AXP192_Class::getBatteryCharge(bool* enabled) + { + std::uint8_t val = 0; + if (enabled == nullptr || !readRegister(0x33, &val, 1)) { return false; } + *enabled = (val & 0x80) != 0; + return true; } std::int8_t AXP192_Class::getBatteryLevel(void) diff --git a/src/utility/power/AXP192_Class.hpp b/src/utility/power/AXP192_Class.hpp index dd1691fa..988967d0 100644 --- a/src/utility/power/AXP192_Class.hpp +++ b/src/utility/power/AXP192_Class.hpp @@ -26,11 +26,14 @@ namespace m5 /// set battery charge enable. /// @param enable true=enable / false=disable - void setBatteryCharge(bool enable); + /// @return false on I2C failure. + bool setBatteryCharge(bool enable); /// set battery charge current /// @param max_mA milli ampere. (100 - 1320). - void setChargeCurrent(std::uint16_t max_mA); + /// @param applied_mA optional. receives the step that was applied. + /// @return false on I2C failure. applied_mA is left untouched then. + bool setChargeCurrent(std::uint16_t max_mA, std::uint16_t* applied_mA = nullptr); /// set battery charge voltage /// @param max_mV milli volt. (4100 - 4360). @@ -39,11 +42,23 @@ namespace m5 /// Supported steps are 4100 / 4150 / 4200 / 4360 mV; a request under the /// lowest step selects that step, and one above the highest selects the /// highest. - void setChargeVoltage(std::uint16_t max_mV); + /// @param applied_mV optional. receives the step that was applied. + /// @return false on I2C failure. applied_mV is left untouched then. + bool setChargeVoltage(std::uint16_t max_mV, std::uint16_t* applied_mV = nullptr); /// Get whether the battery is currently charging or not. bool isCharging(void); + /// read REG00H bit2 (battery current direction : 1 = into the battery) with I2C error reporting. + /// @return false on I2C failure. + /// @note isCharging() folds a failed read into false; use this where the + /// difference between "not charging" and "could not read" matters. + bool readChargeActive(bool* charging); + + /// read the charger enable (REG33H bit7) with I2C error reporting. + /// @return false on I2C failure. + bool getBatteryCharge(bool* enabled); + inline void setDCDC1(int voltage) { _set_DCDC(0, voltage); } inline void setDCDC2(int voltage) { _set_DCDC(1, voltage); } inline void setDCDC3(int voltage) { _set_DCDC(2, voltage); } diff --git a/src/utility/power/AXP2101_Class.cpp b/src/utility/power/AXP2101_Class.cpp index c46931b6..e99885b5 100644 --- a/src/utility/power/AXP2101_Class.cpp +++ b/src/utility/power/AXP2101_Class.cpp @@ -92,13 +92,29 @@ namespace m5 return res; } - void AXP2101_Class::setBatteryCharge(bool enable) + bool AXP2101_Class::setBatteryCharge(bool enable) { std::uint8_t val = 0; - if (readRegister(0x18, &val, 1)) - { - writeRegister8(0x18, (val & 0xFD) | (enable << 1)); - } + if (!readRegister(0x18, &val, 1)) { return false; } + return writeRegister8(0x18, (val & 0xFD) | (enable << 1)); + } + + bool AXP2101_Class::getBatteryCharge(bool* enabled) + { + std::uint8_t val = 0; + if (enabled == nullptr || !readRegister(0x18, &val, 1)) { return false; } + *enabled = (val & 0x02) != 0; + return true; + } + + bool AXP2101_Class::readPmuStatus1(std::uint8_t* value) + { + return value != nullptr && readRegister(0x00, value, 1); + } + + bool AXP2101_Class::readPmuStatus2(std::uint8_t* value) + { + return value != nullptr && readRegister(0x01, value, 1); } void AXP2101_Class::setPreChargeCurrent(std::uint16_t max_mA) @@ -112,19 +128,21 @@ namespace m5 writeRegister8(0x61, i); } - void AXP2101_Class::setChargeCurrent(std::uint16_t max_mA) - { + bool AXP2101_Class::setChargeCurrent(std::uint16_t max_mA, std::uint16_t* applied_mA) + { /// reg 0x62 counts in steps of 25mA up to 200mA and 100mA above it, so the + /// register value is the table index + 4 (code 4 = the lowest step, 100mA). max_mA /= 5; if (max_mA > 1000/5) { max_mA = 1000/5; } static constexpr std::uint8_t table[] = { 125 / 5, 150 / 5, 175 / 5, 200 / 5, 300 / 5, 400 / 5, 500 / 5, 600 / 5, 700 / 5, 800 / 5, 900 / 5, 1000 / 5, 255 }; size_t i = 0; while (table[i] <= max_mA) { ++i; } - i += 4; - writeRegister8(0x62, i); + if (!writeRegister8(0x62, i + 4)) { return false; } + if (applied_mA) { *applied_mA = i ? (std::uint16_t)(table[i - 1] * 5) : 100; } + return true; } - void AXP2101_Class::setChargeVoltage(std::uint16_t max_mV) + bool AXP2101_Class::setChargeVoltage(std::uint16_t max_mV, std::uint16_t* applied_mV) { /// reg 0x64 selects the constant-voltage target: 1 = 4.0V through 5 = 4.4V, /// with 0 reserved. An early revision of the datasheet also documented a /// 4.6V setting, which later revisions dropped; nothing this library runs @@ -143,7 +161,9 @@ namespace m5 /// step when the request is under all of them. while (i && table[i] > max_mV) { --i; } - writeRegister8(0x64, static_cast(i + 1)); + if (!writeRegister8(0x64, static_cast(i + 1))) { return false; } + if (applied_mV) { *applied_mV = table[i]; } + return true; } std::int8_t AXP2101_Class::getBatteryLevel(void) diff --git a/src/utility/power/AXP2101_Class.hpp b/src/utility/power/AXP2101_Class.hpp index e5aa66d0..1857136d 100644 --- a/src/utility/power/AXP2101_Class.hpp +++ b/src/utility/power/AXP2101_Class.hpp @@ -74,24 +74,34 @@ namespace m5 /// set battery charge enable. /// @param enable true=enable / false=disable - void setBatteryCharge(bool enable); + /// @return false on I2C failure. + bool setBatteryCharge(bool enable); + + /// get battery charge enable state with I2C error reporting. (REG18H bit1) + /// @param enabled output parameter, receives the charge enable state. + /// @return false on I2C failure. + bool getBatteryCharge(bool* enabled); /// set battery precharge current /// @param max_mA milli ampere. (0 - 200). void setPreChargeCurrent(std::uint16_t max_mA); /// set battery charge current - /// @param max_mA milli ampere. (100 - 1320). - void setChargeCurrent(std::uint16_t max_mA); + /// @param max_mA milli ampere. (100 - 1000). + /// @param applied_mA optional. receives the step that was applied. + /// @return false on I2C failure. applied_mA is left untouched then. + bool setChargeCurrent(std::uint16_t max_mA, std::uint16_t* applied_mA = nullptr); /// set battery charge voltage - /// @param max_mV milli volt. (4100 - 4360). + /// @param max_mV milli volt. (4000 - 4400). /// set the constant-voltage charge target. /// @param max_mV the highest step at or below this value is selected. /// Supported steps are 4000 / 4100 / 4200 / 4350 / 4400 mV; a request under /// the lowest step selects that step, and one above the highest selects /// the highest. - void setChargeVoltage(std::uint16_t max_mV); + /// @param applied_mV optional. receives the step that was applied. + /// @return false on I2C failure. applied_mV is left untouched then. + bool setChargeVoltage(std::uint16_t max_mV, std::uint16_t* applied_mV = nullptr); /// @return -1:discharge / 0:standby / 1:charge int getChargeStatus(void); @@ -99,6 +109,19 @@ namespace m5 /// Get whether the battery is currently charging or not. bool isCharging(void); + /// read REG00H (PMU status 1) with I2C error reporting. + /// bit3 = battery present, bit5 = VBUS present. + /// @return false on I2C failure. + bool readPmuStatus1(std::uint8_t* value); + + /// read REG01H (PMU status 2) with I2C error reporting. + /// bit[2:0] = charger state machine (0-3 charging / 4 charge done), + /// bit[6:5] = battery current direction (0b00 standby / 0b01 charge / 0b10 discharge). + /// @return false on I2C failure. + /// @note isCharging() folds a failed read into false; use this where the + /// difference between "not charging" and "could not read" matters. + bool readPmuStatus2(std::uint8_t* value); + inline void setALDO1(int voltage) { _set_LDO(0, voltage); } inline void setALDO2(int voltage) { _set_LDO(1, voltage); } diff --git a/src/utility/power/INA226_Class.cpp b/src/utility/power/INA226_Class.cpp index 87840975..20f488eb 100644 --- a/src/utility/power/INA226_Class.cpp +++ b/src/utility/power/INA226_Class.cpp @@ -56,6 +56,14 @@ namespace m5 return (raw * _cur_lsb); } + bool INA226_Class::readShuntCurrent(float* ampere) + { + std::uint8_t buf[2] = {0}; + if (!_init || ampere == nullptr || !readRegister(INA226_CURRENT, buf, 2)) { return false; } + *ampere = (float)(std::int16_t)(buf[0] << 8 | buf[1]) * _cur_lsb; + return true; + } + float INA226_Class::getPower(void) { auto raw = (int16_t)readRegister16(INA226_POWER); diff --git a/src/utility/power/INA226_Class.hpp b/src/utility/power/INA226_Class.hpp index 02941d72..f7dcf4d1 100644 --- a/src/utility/power/INA226_Class.hpp +++ b/src/utility/power/INA226_Class.hpp @@ -114,6 +114,13 @@ namespace m5 float getShuntCurrent(void); float getPower(void); + /// read the shunt current with I2C error reporting. + /// @param ampere output parameter, receives the current in ampere. + /// @return false on I2C failure. + /// @note getShuntCurrent() folds a failed read into 0A, which is a + /// plausible reading; use this where that difference matters. + bool readShuntCurrent(float* ampere); + private: std::size_t readRegister16(std::uint8_t addr); bool writeRegister16(std::uint8_t addr, std::uint16_t data); diff --git a/src/utility/power/IP5306_Class.cpp b/src/utility/power/IP5306_Class.cpp index 8a203792..0e6dfea3 100644 --- a/src/utility/power/IP5306_Class.cpp +++ b/src/utility/power/IP5306_Class.cpp @@ -57,50 +57,71 @@ namespace m5 return -1; } - void IP5306_Class::setBatteryCharge(bool enable) + static constexpr std::uint8_t CHARGE_EN_BIT = 0x10; + + bool IP5306_Class::setBatteryCharge(bool enable) { - static constexpr std::uint8_t CHARGE_OUT_BIT = 0x10; + std::uint8_t val = 0; + if (!readRegister(REG_SYS_CTL0, &val, 1)) { return false; } + return writeRegister8(REG_SYS_CTL0, enable ? (val | CHARGE_EN_BIT) : (val & (~CHARGE_EN_BIT))); + } + bool IP5306_Class::getBatteryCharge(bool* enabled) + { std::uint8_t val = 0; - if (readRegister(REG_SYS_CTL0, &val, 1)) - { - writeRegister8(REG_SYS_CTL0, enable ? (val | CHARGE_OUT_BIT) : (val & (~CHARGE_OUT_BIT))); - } + if (enabled == nullptr || !readRegister(REG_SYS_CTL0, &val, 1)) { return false; } + *enabled = (val & CHARGE_EN_BIT) != 0; + return true; } - void IP5306_Class::setChargeCurrent(std::uint16_t max_mA) + bool IP5306_Class::readChargeActive(bool* active) { - max_mA = (max_mA > 50) ? (max_mA - 50) / 100 : 0; - if (max_mA > 31) { max_mA = 31; } + std::uint8_t val = 0; + if (active == nullptr || !readRegister(REG_READ0, &val, 1)) { return false; } + *active = (val & 0x08) != 0; + return true; + } + bool IP5306_Class::readChargeFull(bool* full) + { /// REG_READ0 and REG_READ1 sit at adjacent addresses but are read + /// separately on purpose: the register document only ever shows a + /// single byte read and nowhere states that the address auto-increments. std::uint8_t val = 0; - if (readRegister(REG_CHG_DIG_CTL0, &val, 1)) - { - writeRegister8(REG_CHG_DIG_CTL0, (val & 0xE0) + max_mA); - } + if (full == nullptr || !readRegister(REG_READ1, &val, 1)) { return false; } + *full = (val & 0x08) != 0; + return true; } - void IP5306_Class::setChargeVoltage(std::uint16_t max_mV) - { - max_mV = (max_mV / 10); - max_mV = (max_mV > 410) ? max_mV - 410 : 0; - if (max_mV > 436 - 410) { max_mV = 436 - 410; } - static constexpr std::uint8_t table[] = - { 430 - 410 /// 4300mV - , 435 - 410 /// 4350mV - , 440 - 410 /// 4400mV - , 255 - }; - size_t i = 0; - while (table[i] <= max_mV) { ++i; } + bool IP5306_Class::setChargeCurrent(std::uint16_t max_mA, std::uint16_t* applied_mA) + { /// the register is a weighted sum with a 100mA step over a 50mA floor. + std::uint16_t steps = (max_mA > 50) ? (max_mA - 50) / 100 : 0; + if (steps > 31) { steps = 31; } + + std::uint8_t val = 0; + if (!readRegister(REG_CHG_DIG_CTL0, &val, 1)) { return false; } + if (!writeRegister8(REG_CHG_DIG_CTL0, (val & 0xE0) + steps)) { return false; } + if (applied_mA) { *applied_mA = (std::uint16_t)(50 + steps * 100); } + return true; + } + bool IP5306_Class::setChargeVoltage(std::uint16_t max_mV, std::uint16_t* applied_mV) + { /// reg CHG_CTL2 selects the constant-voltage target plus the boost the + /// datasheet recommends for each step (28mV at 4.2V, 14mV above). The + /// cell sees the sum, so the steps are compared and reported as the + /// effective values: the highest one not above the request, clamped up to + /// the lowest when the request is under all of them. + static constexpr std::uint16_t table[4] = { 4228, 4314, 4364, 4414 }; static constexpr std::uint8_t regdata[4] = { 0x02 // 4.2v + boost 28mV , 0x05 // 4.3v + boost 14mV , 0x09 // 4.35v + boost 14mV , 0x0D // 4.4v + boost 14mV }; - writeRegister8(REG_CHG_CTL2, regdata[i]); + size_t i = 3; + while (i && table[i] > max_mV) { --i; } + if (!writeRegister8(REG_CHG_CTL2, regdata[i])) { return false; } + if (applied_mV) { *applied_mV = table[i]; } + return true; } bool IP5306_Class::isCharging(void) diff --git a/src/utility/power/IP5306_Class.hpp b/src/utility/power/IP5306_Class.hpp index 87ece26d..5897a524 100644 --- a/src/utility/power/IP5306_Class.hpp +++ b/src/utility/power/IP5306_Class.hpp @@ -26,15 +26,44 @@ namespace m5 /// set battery charge enable. /// @param enable true=enable / false=disable - void setBatteryCharge(bool enable); + /// @return false on I2C failure. + bool setBatteryCharge(bool enable); + + /// get the charge enable setting. (SYS_CTL0 bit4) + /// @param enabled output parameter, receives the charge enable setting. + /// @return false on I2C failure. + /// @note This is the value that was written, which survives an unplugged + /// supply. readChargeActive() reports the effective one. + bool getBatteryCharge(bool* enabled); + + /// read the effective charge enable flag. (REG_READ0 bit3) + /// @param active output parameter. false when charging is disabled or no supply is present. + /// @return false on I2C failure. + /// @note This flag is "charging is switched on", not "the cell is filling": + /// it stays set after the charge completes. Pair it with readChargeFull(). + bool readChargeActive(bool* active); + + /// read the charge complete flag. (REG_READ1 bit3) + /// @param full output parameter, receives the charge complete flag. + /// @return false on I2C failure. + /// @note The flag has no defined reset value, so shortly after power up it + /// can read as full before the charger has settled. + bool readChargeFull(bool* full); /// set battery charge current - /// @param max_mA milli ampere. (150 - 3150). - void setChargeCurrent(std::uint16_t max_mA); + /// @param max_mA milli ampere. (50 - 3150, in steps of 100mA over a 50mA floor). + /// @param applied_mA optional. receives the step that was applied. + /// @return false on I2C failure. applied_mA is left untouched then. + bool setChargeCurrent(std::uint16_t max_mA, std::uint16_t* applied_mA = nullptr); /// set battery charge voltage - /// @param max_mV milli volt. (4200 - 4400). - void setChargeVoltage(std::uint16_t max_mV); + /// @param max_mV milli volt. The steps are the effective values including + /// the constant-voltage boost the datasheet recommends: 4228 / 4314 / + /// 4364 / 4414 mV. The highest step not above max_mV is selected, and a + /// request under the lowest step selects that step. + /// @param applied_mV optional. receives the effective step that was applied. + /// @return false on I2C failure. applied_mV is left untouched then. + bool setChargeVoltage(std::uint16_t max_mV, std::uint16_t* applied_mV = nullptr); /// Get whether the battery is currently charging or not. bool isCharging(void); diff --git a/src/utility/power/M5PM1_Class.cpp b/src/utility/power/M5PM1_Class.cpp index 76dc2118..56e25393 100644 --- a/src/utility/power/M5PM1_Class.cpp +++ b/src/utility/power/M5PM1_Class.cpp @@ -112,10 +112,21 @@ namespace m5 return static_cast(readRegister8(M5PM1_REG_PWR_SRC) & 0x07); } + bool M5PM1_Class::getPowerSource(pwr_src_t* source) + { + if (!_init || source == nullptr) { return false; } + std::uint8_t value; + if (!readRegister(M5PM1_REG_PWR_SRC, &value, 1)) { return false; } + *source = static_cast(value & 0x07); + return true; + } + bool M5PM1_Class::getVbatNodePowered(bool* powered) { if (!_init || powered == nullptr) { return false; } - *powered = readRegister8(M5PM1_REG_PWR_SRC) & 0x04; + std::uint8_t value; + if (!readRegister(M5PM1_REG_PWR_SRC, &value, 1)) { return false; } + *powered = (value & 0x04) != 0; return true; } @@ -127,7 +138,8 @@ namespace m5 auto reg = num < 4 ? M5PM1_REG_GPIO_FUNC0 : M5PM1_REG_GPIO_FUNC1; auto shift = static_cast((num < 4 ? num : num - 4) * 2); std::uint8_t mask = 0x03 << shift; - std::uint8_t reg_val = readRegister8(reg); + std::uint8_t reg_val = 0; + if (!readRegister(reg, ®_val, 1)) { return false; } // a folded read would rewrite the other pins of the register reg_val = (reg_val & ~mask) | (static_cast(function) << shift); return writeRegister8(reg, reg_val); } @@ -147,7 +159,8 @@ namespace m5 auto reg = num < 4 ? M5PM1_REG_GPIO_PUPD0 : M5PM1_REG_GPIO_PUPD1; auto shift = static_cast((num < 4 ? num : num - 4) * 2); std::uint8_t mask = 0x03 << shift; - std::uint8_t reg_val = readRegister8(reg); + std::uint8_t reg_val = 0; + if (!readRegister(reg, ®_val, 1)) { return false; } // a folded read would rewrite the other pins of the register reg_val = (reg_val & ~mask) | (static_cast(pull) << shift); return writeRegister8(reg, reg_val); } diff --git a/src/utility/power/M5PM1_Class.hpp b/src/utility/power/M5PM1_Class.hpp index 88138e4e..f5cc813a 100644 --- a/src/utility/power/M5PM1_Class.hpp +++ b/src/utility/power/M5PM1_Class.hpp @@ -94,6 +94,13 @@ namespace m5 /// bit2=VBAT node valid. Multiple sources may be present simultaneously. pwr_src_t getPowerSource(void); + /// get the PM1 PWR_SRC bitmap with I2C error reporting. + /// @param source output parameter, receives the PWR_SRC bitmap. + /// @return false on I2C failure. + /// @note the no-argument overload cannot tell a failed read from + /// "no source present"; use this one where that distinction matters. + bool getPowerSource(pwr_src_t* source); + /// get whether PWR_SRC reports the VBAT node rail as powered. /// note: this tracks the node voltage, not physical battery presence. bool getVbatNodePowered(bool* powered);