Skip to content

Fix dynamic battery min max#6

Merged
dacarson merged 10 commits intomainfrom
Fix-dynamic-battery-min-max
Mar 6, 2026
Merged

Fix dynamic battery min max#6
dacarson merged 10 commits intomainfrom
Fix-dynamic-battery-min-max

Conversation

@dacarson
Copy link
Copy Markdown
Owner

@dacarson dacarson commented Mar 6, 2026

Battery Full and Empty where not functioning as expected. Fix this along with other issues discovered while testing Full and Empty values and conditions.

dacarson added 10 commits March 5, 2026 13:05
auto_power_on not persisting across unexpected power loss:
- Add persist_bypass flag in ProcessI2CPendingWrite; REG_AUTO_POWER_ON
  writes now use RequestFlashSave(bypass=1) for immediate flash commit,
  bypassing the rate-limit window.

Empty voltage corrupted to near-full after I2C failure:
- Add EMPTY_LEARN_MIN_MEANINGFUL_DISCHARGE_MV (300 mV) guard in
  EmptyLearn_CommitTrackedMin; rejects commits where the tracked minimum
  is within 300 mV of full voltage (genuine discharge required).

Empty learning hardening:
- Add 3 s load-off debounce (EMPTY_LEARN_LOAD_OFF_HOLD_TICKS) to avoid
  single-sample current glitches triggering early commit.
- Add 30 s brownout debounce (EMPTY_LEARN_BROWNOUT_HOLD_TICKS) to
  distinguish I2C polling gaps from genuine brownouts; cancelled if
  validity restores before hold expires.
- Exclude FORCED_OFF_WINDOW from empty tracking (calibration VBAT dip).
- Add reset_min parameter to EmptyLearn_CommitTrackedMin: load-off and
  protection paths reset the tracker; brownout path preserves it.

Full learning detection refactor:
- Add independent taper path for battery_full (OR semantics with plateau
  path); accepts bidirectional current (+/-I_TAPER_MA).
- Add PLATEAU_MEAN_MIN_MV (4150 mV) gate to prevent mid-voltage plateau
  misclassification.
- Remove PLATEAU_MIN_CHANGE_MV threshold; EMA update always applies.
- Remove PLATEAU_POWER_STABLE_SEC guard and plateau_last_power_state.
- Fix FORCED_OFF_WINDOW handling: charger_connected uses != ABSENT so
  plateau session survives calibration windows; session reset only on
  ABSENT -> non-ABSENT transition.

Docs: update spec for new power-state transitions, button FSM, plateau/taper OR semantics, and
standalone INA219 note.
The reset cause capture implemented in Phase 3 always returned 0x00
because the STM32 bootloader writes RMVF=1 to RCC_CSR before jumping
to the application. Factory test selectors 0x08 (this boot's CSR) and
0x09 (last persisted cause) were non-functional from the start.

Removed:
- boot_reset_csr, last_persisted_reset_cause, last_persisted_reset_seq
  static variables and RCC_CSR_RESET_FLAGS_* defines from main.c
- Factory test cases 0x08/0x09 from StateToRegisterBuffer()
- last_reset_cause/last_reset_seq from Flash_FillRecordFromState() and
  Flash_Load(); replaced with reserved_padding2[4] in the flash struct
  to preserve layout compatibility with existing flash records

The RMVF clear at boot is retained for hygiene (prevents flag
accumulation across resets). Spec and debugging guide
updated to document that reset cause is not available and why.
After an MCU reset with the I2C peripheral already enabled, the ADDR flag can be set and SCL held low indefinitely if the NVIC interrupt has not yet been re-armed. The main loop had no mechanism to detect or recover from this without a full power cycle.

Added:
- I2C1_IsAddrFlagSet() in I2C_Slave.c/.h: reads the ADDR hardware flag
  directly (atomic register read, safe from main-loop context)
- I2C_STUCK_ADDR_RECOVERY_SEC (5) constant and i2c_stuck_addr_sec
  counter in main.c
- tick_1s watchdog block: increments counter while ADDR is set, resets
  on any clear second; on >= 5 consecutive seconds calls
  MX_I2C1_Slave_Init() to disable the peripheral (releasing SCL),
  clear all flags, and reinitialise the slave

MX_I2C1_Slave_Init() already performs LL_I2C_Disable → ClearAllFlags →
reconfigure → LL_I2C_Enable → NVIC_EnableIRQ, which is the correct
sequence to release a clock-stretched bus.
After an MCU crash or IWDG reset, battery_percent calculated as 0%
because last_true_vbat_mv was lost and UpdateBatteryPercentage() had
nothing to work with while the charger was influencing VBAT. This
prevented CheckPowerOnConditions() from ever transitioning to
LOAD_ON_DELAY in a crash-reset loop, leaving the RPi permanently
unpowered even with a charged battery and charger connected.

Persist last_true_vbat_mv and last_true_vbat_age_sec in the flash
record (version 2 → 3). On load, restore them into authoritative
state so that IsTrueVbatUsableForDecision() — which now replaces the
raw IsTrueVbatSampleFresh() call in CheckPowerOnConditions(),
UpdateBatteryPercentage(), and the button short-press handler — can
accept a persisted sample within TRUE_VBAT_MAX_AGE_SEC (10 min) as
valid. Minimum saved age is clamped to 1 s to distinguish a fresh
sample from the sentinel value 0 = "no data". Age is accumulated
correctly across multiple save/load cycles via
cumulative_runtime_sec_at_flash_load.
After I2C1_ExitMasterWindow() restores slave config (CR1 write sets
PE=1), the peripheral immediately begins address-matching. If the RPi
master begins a transaction before NVIC_EnableIRQ(), ADDR is latched
and SCL is held low by clock-stretching with no ISR to release it.

Fix: add I2C1_BusIdleBrief() which samples SCL (PA9) and SDA (PA10)
twice with a 1ms gap. If either line is low, I2C1_BusRecovery() is
called to release the bus before the NVIC is re-enabled. The 1ms
window also catches a mid-transaction ADDR latch that set PE=1
triggered: SCL will read low on the second sample, forcing recovery.

I2C1_BusRecovery() already calls LL_I2C_Disable() internally, so no
redundant pre-call is needed. LL_mDelay() is already in the binary
via I2C1_BusRecovery(), adding only a call site.
CheckPowerOnConditions() was only called from the main loop when
auto_power_on != 0. The PROTECTION_LATCHED exit logic lived inside
that function after the !auto_power_on early return, so it was
never reached when auto_power_on = 0 — the state machine stalled
permanently after a protection event.

Fix: move the PROTECTION_LATCHED block to the top of
CheckPowerOnConditions(), before the early return. Remove the
call-site guard so the function runs unconditionally each loop.
When latched and recovered, the device now always clears to RPI_OFF
(or LOAD_ON_DELAY if auto_power_on = 1). The !auto_power_on early
return continues to block normal auto power-on logic.
Extend the short-press outer guard to include POWER_STATE_PROTECTION_LATCHED
so a button press can initiate power-on when the battery has recovered above
the protection voltage.

Remove the dead inner PROTECTION_LATCHED check (unreachable behind the
RPI_OFF-only outer guard) and the redundant re-enable block. The shared
charger and battery-voltage checks produce the correct allow/deny result for
both states without state-specific logic, reducing flash consumption.
… threshold

When battery_ok fails during a LOAD_ON_DELAY countdown, the cancellation
now transitions to PROTECTION_LATCHED (instead of RPI_OFF) if the voltage
is also below the protection threshold. This prevents an immediate re-entry
into LOAD_ON_DELAY on the next CheckPowerOnConditions() call if the voltage
transiently reads above protection again.
Writing to REG_LOAD_ON_DELAY_L (0x2C–0x2D) only updated
load_on_delay_config_sec. If a LOAD_ON_DELAY countdown was already
in progress, the running countdown was unaffected and a register
readback would return the old remaining value, not the newly written
one.

Split the validation block so that the config update (with its
persist flag) is guarded on the value changing, while a separate
check applies the new value to load_on_delay_remaining_sec whenever
power_state == POWER_STATE_LOAD_ON_DELAY. A write now takes effect
on the active countdown immediately.
I2C1_GuardWindowReady() used a 16-bit 1 MHz timer to guard against
MCU master-window collisions with an active RPi. With no RPi present,
the timestamps never updated and the 16-bit counter wrapped every
~65 ms, making the comparisons unreliable and permanently blocking
INA219 reads. This caused output_current_valid and
battery_current_valid to age out, stalling taper detection, empty-
voltage learning, and battery-percent updates during standalone
(no-RPi) operation.

Fix: track last I2C master activity at tick resolution (1 s). If no
STOP or ADDR event has been observed since boot, or none for more than
5 s (I2C_MASTER_IDLE_ALLOW_READ_TICKS), the guard bypasses the 50 ms
µs check and allows INA219 reads after confirming the bus lines are
high and stable. The 50 ms µs guard is retained for the window when
a master has been recently active, where it remains effective.

The first_call tracking variable was removed: initialising
last_seen_{stop,addr}_us to 0 is sufficient — if the hardware
timestamps are also 0 at boot (no events yet), last_i2c_activity_tick
stays 0 and the idle path opens immediately; if they differ, a
conservative 5 s delay before idle reads is harmless given the INA
probe timer is gated ~2 minutes out.
@dacarson dacarson merged commit 57aa7d2 into main Mar 6, 2026
@dacarson dacarson deleted the Fix-dynamic-battery-min-max branch March 6, 2026 01:04
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