Battery: anchor SoC to AXP192 coulomb counter to stop the unplug-jump#10
Open
SnowWarri0r wants to merge 4 commits into
Open
Battery: anchor SoC to AXP192 coulomb counter to stop the unplug-jump#10SnowWarri0r wants to merge 4 commits into
SnowWarri0r wants to merge 4 commits into
Conversation
The reference firmware reported battery percentage as a naive
linear interpolation: pct = (vBat_mV - 3200) / 10. Two issues
combine to make this almost useless on a desk pet under typical
BLE + display load:
1. LiPo voltage drops 100-300 mV under load. The same actual
state-of-charge reads ~20% lower while busy than while idle,
and jumps that much when load disappears (e.g., USB unplug).
The current daemon log shows readings hopping between 31% and
51% from one minute to the next.
2. The real LiPo discharge curve is far from linear — it's
nearly flat across the middle 30-80% band and steep at the
extremes. The linear formula has roughly the right endpoints
but is wrong everywhere in between.
Replace with a small battery module that:
- Samples vBat at 1 Hz into a 30-slot ring buffer
- Returns the median voltage (rejects single-tick spikes from
the BLE radio TX bursts)
- Maps that smoothed voltage through a 12-point OCV-SOC table
sized for a typical 1S LiPo
Pure passive measurement — no coulomb counter, no NVS state, no
calibration cycles. ~600 bytes of flash, 128 bytes of RAM for the
ring buffer, ~5 µs per loop tick (1 Hz throttled).
xfer.h's status response now calls battery::percent() instead of
inlining the formula. Behaviour: SoC moves slowly and roughly
monotonically across genuine charge/discharge transitions; load
fluctuations no longer make the indicator jitter.
Watching the output for ~10 minutes after the initial commit, the 30-sample window still let through ±2% wiggles — physically real (charging current isn't perfectly constant; load varies over 1 minute windows), but visually noticeable on the hud's 8-segment progress bar where each segment is 12.5%, so a 2% wiggle near a segment boundary flips the bar one notch. 60 samples halves the rate at which a single new sample can drag the median across a percentile threshold. The OCV table's midpoint plateau (40–70%) is where the LiPo curve is flattest and our smoothing helps most.
…ary)
Voltage-only SoC has a fundamental limit on this hardware: charging
current inflates the measured voltage by 50–100 mV via charge
polarization. Pull USB and the same physical SoC reads ~10–15
percentage points lower. We confirmed this reproducibly: 64% while
charging dropped to 55% the instant USB came out, no real charge moved.
The AXP192 has a hardware coulomb counter (charge-mAh and
discharge-mAh accumulators clocked off the current-sense ADC). M5's
library exposes it as Axp.GetCoulombData() returning net mAh. That's
the right primary signal — it tracks actual charge in/out regardless
of polarization or load.
New algorithm:
1. setup(): EnableCoulombcounter().
2. Sample voltage at 1 Hz into a 30-slot ring (unchanged from prior
commit). This gives us a clean OCV reading at the moment the
buffer first fills (load is low at boot).
3. Anchor: at first full window, set SoC = OCV(median(voltage)) and
freeze the coulomb-counter value at that point.
4. From then on: SoC = anchor_pct + (coulomb_now - coulomb_anchor) /
120 mAh × 100. Bounded to 0–100.
5. Re-anchor to 100% any time vBat ≥ 4.10 and |I| < 30 mA — this
happens whenever the battery genuinely tops up, and corrects any
coulomb-counter drift accumulated over weeks.
6. Light 8-sample median on top of derived pct catches AXP register
glitches without flattening genuine charge/discharge motion.
The unplug step-down should now be invisible: coulomb_now barely
changes across an unplug event (no real mAh moved), so derived SoC
stays put even though voltage drops 50–100 mV.
The DEVICE info screen (info page 3) had an inline copy of the same naive (vBat-3200)/10 formula that xfer.h used for status acks. Switched xfer.h to battery::percent() in a previous commit but missed this one, so users watching the stick's own info page still saw the unplug jump. Now both surfaces (BLE status response + on-device info page) get the same coulomb-counter-anchored SoC.
a06e6ac to
2a60629
Compare
This was referenced Apr 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The reference firmware reports battery percentage from a single linear
formula on instantaneous voltage:
That formula has two well-known weaknesses on this hardware. Both produced
visible-and-confusing-to-the-user readings on my stick:
BLE/LCD load yanks the measured voltage 100–300 mV below the
open-circuit value. The same physical SoC reads ~10–20 percentage
points lower while busy than while idle, and the indicator wobbles in
sync with whatever the stick is doing.
Charging polarization inflates the measured voltage by 50–100 mV.
Pull USB and the reading drops 5–15 percentage points instantly,
despite no actual charge having moved. From
cc-buddy-bridge's daemonlog:
Both surfaces — the BLE status ack consumed by desktop UIs and the
on-device DEVICE info page — share the same formula and the same wobble.
Fix
Switch the primary SoC signal from voltage-direct to the AXP192's
hardware coulomb counter (already exposed by the M5StickC Plus library
as
M5.Axp.EnableCoulombcounter()/GetCoulombData()).New
batterynamespace insrc/battery.h:setup(): enable the coulomb counter.loop(): sample voltage at 1 Hz into a 60-slot ring buffer.low at boot, so this is the cleanest OCV reading available) and look
it up in a 12-point LiPo OCV-SOC table. Freeze that pct + the current
coulomb value as the anchor.
SoC = anchor_pct + (coulomb_now - coulomb_anchor) / 120 mAh × 100, clamped to 0–100. The coulomb counter is unaffectedby polarization or transient load — it counts actual charge in/out.
vBat ≥ 4.10 Vand|I| < 30 mA, the cellis genuinely topped up. Re-anchor to 100% to correct any drift the
coulomb counter accumulated over weeks of use.
AXP register glitches without flattening real charge motion.
xfer.h:statusand the on-device DEVICE info page (main.cpp:_drawDevicePage)both call
battery::percent()— same SoC value visible from bothsurfaces.
Cost
Testing
I flashed this to my own M5StickC Plus and watched the same daemon log
column over the same time window:
Scope
Only
battery.h(new),main.cpp(callbattery::begin()andbattery::poll(), swap inline pct calc forbattery::percent()), andxfer.h(same swap). No protocol changes — the wire format is unchanged,the
bat.pctfield just becomes meaningful.CONTRIBUTING.md fit
Per
CONTRIBUTING.md"What we will take": fixes for bugs that make thereference not work as a reference. A battery indicator that swings 9 %
on USB unplug isn't a working reference of how this should look.