Skip to content

Power: add getChargeState() with per-charger capabilities, make isCharging() a bool - #353

Open
ainyan03 wants to merge 1 commit into
m5stack:developfrom
ainyan03:charge_state_api
Open

Power: add getChargeState() with per-charger capabilities, make isCharging() a bool#353
ainyan03 wants to merge 1 commit into
m5stack:developfrom
ainyan03:charge_state_api

Conversation

@ainyan03

@ainyan03 ainyan03 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Summary

isCharging() returned is_charging_t, which folded "no information" and "not charging" into one value, and several boards reported states their charger cannot actually distinguish. On IP5306 boards it read the charge-enable bit and answered is_discharging while a full battery sat on USB power (#349). The setBatteryCharge / setChargeCurrent / setChargeVoltage family returned void, so a caller could not tell a rejected or clamped request from an applied one.

This PR adds a charge state API that only reports what each charger can actually observe, and makes the control functions report what they applied.

New API

API Meaning
charge_state_t getChargeState() charging / not_charging / full / disabled / discharging / idle (positive) or not_initialized / io_error / undetermined / unsupported (non-positive). Only charging means the battery is being charged.
bool getChargeStateCaps(charge_state_set_t*) / bool canReport(charge_state_t) The set of states this board can report. Lets an application decide whether e.g. full can ever be observed.
charge_states_known / charge_states_any_charging / charge_states_any_not_charging Predefined sets. isCharging() is charge_states_any_charging.contains(getChargeState()).
battery_presence_t getBatteryPresence() present / absent on boards whose charger reports it (AXP2101) or where it is estimated (ToughC5, CoreMatrix); unsupported elsewhere.
uint8_t getChargeControlCaps() Which of charge enable / current / voltage this board can set.
bool setBatteryCharge(bool) / bool setChargeCurrent(mA, uint16_t* applied = nullptr) / bool setChargeVoltage(mV, uint16_t* applied = nullptr) Return whether the request was applied; applied_* receives the step actually selected.

Step selection: the largest step not above the request; a request below the lowest step is clamped up to it; 0 is not a step.

What each charger reports

Charger / board Reported states Source
AXP192 (Core2, Tough, Station, StickC, StickC Plus) charging, not_charging, disabled REG00 bit2, REG33 bit7. No full: REG01 bit6 means "not charging or done"
AXP2101 (CoreS3, CoreS3 SE, StackChan, Core2 v1.1) charging, not_charging, full, disabled, discharging, idle + presence REG01 charger state machine and current direction, REG18 bit1, REG00 bit3
IP5306 (Core BASIC, Fire, GO, ...) charging, not_charging, full, disabled REG_READ1 bit3 (full), REG_READ0 bit3 (effective enable), SYS_CTL0 bit4
AW32001 (Nesso N1) charging, not_charging, full, disabled charge status register
M5PM1 boards (StampS3Bat, PaperS3, PaperColor, ...) charging, not_charging CHG_STAT input
ToughC5, CoreMatrix charging, not_charging + presence CHG_STAT gated by a battery-presence estimate (the charger retries into an empty connector and toggles CHG_STAT)
Tab5 / Tab5X charging, discharging, idle INA226 battery current (IOE1 G6 is the IP2326 stage flag and reads high both while charging and while disabled)

Every state procedure uses status-returning register reads; an I2C failure is io_error, never a positive state. Charger identity is settled once (AXP192 / AXP2101 probes retry for a positive ID, and a later Power.begin() keeps it). Before M5.begin() completes, getChargeState() is not_initialized.

Breaking changes

  • isCharging() returns bool (true only while charging). Code that compared against is_charging_t::charge_unknown should use getChargeState() / canReport().
  • setBatteryCharge / setChargeCurrent / setChargeVoltage return bool instead of void.
  • Tab5 / Tab5X: setChargeCurrent(0) applies 500 mA (the lowest step) instead of stopping the charge. Use setBatteryCharge(false) to stop. setChargeCurrent(0) now logs a warning once that points to setBatteryCharge(false).
  • StampS3Bat: begin() releases CHG_PROG (PM1 G3), so the charge current after begin() is 200 mA; setChargeCurrent(650) drives it low for 650 mA. Previously the pin was left at the PM1 default.
  • IP5306: setChargeVoltage selects and reports the effective step including the boost offset (4228 / 4314 / 4364 / 4414 mV).

Verification

Checked on hardware:

Board Charger Checked
Core BASIC IP5306 full on USB now reports full (#349), disabled, charging, voltage / current steps
CoreS3 (DIN BASE) AXP2101 charging / full / disabled / idle / discharging, presence with and without battery, clamps, not_initialized before begin()
Core2 v1.1 (2 units) AXP2101 via fallback identity settles as AXP2101, without battery: absent / idle / disabled; with battery: charging / disabled / re-enable, clamps
Tough AXP192 not_charging / charging / disabled / re-enable, current 300 -> 280 / 50 -> 100, voltage 4200 / 3000 -> 4100
StampS3Bat M5PM1 charging / not_charging, CHG_PROG 650 / 200 mA, with and without battery
ToughC5 M5PM1 + CHG_STAT presence gate with and without battery, disabled, 830 / 180 mA
Tab5 INA226 idle without battery, charging (CC 495 mA) / idle on disable / discharging on USB removal, setChargeCurrent 0 -> 500, 700 -> 500, 1500 -> 1000

Builds: ESP32 / S3 / C3 / C5 / C6 / P4 (Arduino) and native (PC build with the caps assertion), no new warnings.

Closes #349

…rging() a bool

isCharging() returned is_charging_t, which folded "no information" and
"not charging" into one value and let boards report states their charger
cannot actually distinguish. On IP5306 boards it read the charge enable bit
and answered "discharging" while a full battery sat on USB power (m5stack#349).
The setBatteryCharge / setChargeCurrent / setChargeVoltage family returned
void, so callers could not tell a rejected or clamped request from an
applied one.

New API
- charge_state_t: not_initialized / io_error / undetermined / unsupported
  (non-positive), charging / not_charging / full / disabled / discharging /
  idle (positive). Only `charging` means the battery is being charged.
- charge_state_set_t with charge_states_known / any_charging /
  any_not_charging, getChargeStateCaps(), canReport(state): the set of
  states a board can report, from one table keyed by charger and board.
- getChargeState(): not_initialized until M5.begin() completes, unsupported
  when the board can report nothing, io_error on a failed read, otherwise
  the per-charger procedure below.
- getBatteryPresence() / battery_presence_t for boards whose charger reports
  battery presence (AXP2101) or where it is estimated (ToughC5, CoreMatrix).
- getChargeControlCaps(): which of enable / current / voltage a board can set.

Per charger
- AXP192: REG00 bit2 -> charging, REG33 bit7 clear -> disabled, else
  not_charging. No `full` (REG01 bit6 does not report completion).
- AXP2101: REG01 charger state machine (tri/pre/CC/CV -> charging, done ->
  full), REG18 bit1 -> disabled, REG00 bit3 -> battery presence, battery
  current direction -> discharging / idle.
- IP5306: SYS_CTL0 bit4 clear -> disabled, REG_READ0 bit3 (effective
  enable) clear -> not_charging, REG_READ1 bit3 -> full, else charging. A
  board without I2C access to the IP5306 reports io_error.
- AW32001: charge status register -> charging / full / disabled / not_charging.
- M5PM1 boards (StampS3Bat, ToughC5, CoreMatrix, PaperS3, ...): CHG_STAT
  through the PM1 / IOE1 inputs; ToughC5 and CoreMatrix gate CHG_STAT with
  a battery presence estimate so a charger retrying into an empty connector
  is not reported as charging. Inside that estimate a failed read, a
  disabled charger or a setBatteryCharge() call is a gap: the evaluation
  ends with the cached verdict and all transient evidence (the CHG_STAT low
  streak, the VBAT sample baseline and counters) is dropped, so nothing
  observed across the gap can complete a streak or a sample count that
  began before it.
- Tab5 / Tab5X: INA226 battery current -> charging / discharging / idle
  (IOE1 G6 is the IP2326 BAT_STAT stage flag and cannot distinguish charging
  from disabled).
- PowerHub, PaperMono, PaperColor, CoreP4X: as their hardware allows.

Control functions
- setBatteryCharge / setChargeCurrent / setChargeVoltage return bool and
  take an optional `applied_*` output. A request selects the largest step
  not above the request; requests below the lowest step are clamped up to
  it; 0 is not a step. `applied_*` is untouched on failure.
- Tab5 / Tab5X: setChargeCurrent selects {500, 1000} mA; 0 no longer means
  "stop charging" (use setBatteryCharge(false)).
- StampS3Bat: CHG_PROG (PM1 G3) is driven low for 650 mA or released for
  200 mA in one transaction that also sets the pin mux; every releasing
  step is attempted even after an earlier failure and the pin only becomes
  an output when all of them succeeded. begin() releases it so the default
  is 200 mA.
- IP5306: voltage steps are reported as the effective values including the
  boost offset (4228 / 4314 / 4364 / 4414 mV).

Robustness
- Status-returning register reads in every state procedure; an I2C failure
  is io_error, never a positive state.
- Charger identity is settled once: on the boards that carry either an
  AXP192 or an AXP2101 the probes retry up to three times for a positive
  chip ID, and once a probe has answered a later Power_Class::begin() keeps
  that identity. If every probe failed the board default (AXP192) stays
  provisional: the capability set is the default, but getChargeState() /
  getBatteryPresence() report io_error and the charge setters return false
  until a later begin() gets a positive ID, so the charge state API does
  not interpret, and the setters do not write, a chip that was never
  identified with the wrong register map. (begin() itself still applies
  the default's register setup, as before.) Charge state before M5.begin()
  completes is not_initialized.
- M5PM1: getPowerSource(pwr_src_t*), checked getVbatNodePowered, and
  read-modify-write helpers that report I2C failures. INA226:
  readShuntCurrent(float*) fails unless the chip identified itself in begin().

Breaking changes
- isCharging() returns bool (true only while charging). Code that compared
  against is_charging_t::charge_unknown must use getChargeState().
- setBatteryCharge / setChargeCurrent / setChargeVoltage return bool.
- Tab5 / Tab5X: setChargeCurrent(0) applies 500 mA instead of stopping.
  setChargeCurrent(0) logs a warning once, pointing to setBatteryCharge(false).
- StampS3Bat: charge current after begin() is 200 mA (previously left as
  the PM1 default).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant