Skip to content

CoreS3: fix external 5V output not turning off / re-enable being cancelled on USB-C without battery - #352

Merged
lovyan03 merged 1 commit into
m5stack:developfrom
ainyan03:cores3_extout_off_order
Sep 2, 2026
Merged

CoreS3: fix external 5V output not turning off / re-enable being cancelled on USB-C without battery#352
lovyan03 merged 1 commit into
m5stack:developfrom
ainyan03:cores3_extout_off_order

Conversation

@ainyan03

@ainyan03 ainyan03 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Problem

On CoreS3 / CoreS3 SE, setExtOutput(false) wrote BUS_OUT_EN (AW9523 P0_1) and BOOST_EN (P1_7) in a single two-byte transaction, so BUS_OUT_EN dropped before the boost converter stopped. BUS_OUT_EN=0 turns the BUS input-side switch (U19) on; when the bus is still at 5V at that moment, the switch never enters reverse-current blocking and USB VBUS keeps feeding BUS_OUT. The visible symptoms:

Fix

  • When BUS_OUT_EN goes 1→0: write BOOST_EN=0 first, wait 200 ms for the bus to discharge, re-read the expander and clear only the requested bit. If any I2C transfer fails, re-read the latch and recover from the actual state (restore BOOST_EN to its initial value while BUS_OUT_EN is still set, or leave BOOST_EN off once BUS_OUT_EN has dropped; if the latch cannot be read, write back the initial pair).
  • Other paths (enable, USB OTG) no longer use a single two-byte write that can be applied partially: bits are written one register at a time, BOOST_EN before the output enable when turning on and after it when turning off, stopping if the first write fails.
  • Serialize every AW9523 output-port writer inside M5Unified (setExtOutput, setUsbOutput, Power_Class::begin, the CoreS3 speaker amplifier enable) with one mutex, and run the no-battery protection check inside the same critical section so a concurrent disable cannot invalidate the check.
  • Protection check (no battery + VBUS + TS above 2.0 V): skip it when BUS_OUT_EN is already 1 (the 5 V on TS is our own output; this also removes the spurious warning on warm reset with output_power=true). It reads the raw registers and treats an I2C read failure or an invalid ADC value as unsafe (fail closed). When it reads unsafe it releases the lock, waits 20 ms and re-checks for up to 1 s, because the TS ADC lags the real voltage by several hundred ms and would otherwise cancel a re-enable issued right after a disable; external bus power keeps TS high, so the protection still cancels in that case. Requests carry a generation number so an enable that is still waiting yields to a later disable.

Measured discharge boundary on a CoreS3 (no battery, USB-C): 10 ms fails / 20 ms works with a Grove unit attached, 70 ms fails / 80 ms works with no load. 200 ms leaves margin.

Notes

  • On the CoreS3 family setExtOutput(false) now blocks for about 200 ms when the output was enabled, and setExtOutput(true) without a battery may block for up to 1 s while the protection check waits for TS to settle (both documented in the header).
  • The pre-existing fail-open window (external 5V applied to Port A and setExtOutput(true) called before the TS ADC has caught up) is unchanged by this PR.
  • The mutex covers M5Unified only. The display driver's panel reset also read-modify-writes the same expander port outside it; that race predates this PR and is unchanged.
  • Verified on CoreS3 (no battery, USB-C) with and without a Grove unit attached.

… output control

setExtOutput(false) wrote BUS_OUT_EN and BOOST_EN in one two-byte transaction, so
BUS_OUT_EN dropped while the bus was still at 5V. BUS_OUT_EN=0 turns the bus
input-side switch on; with the bus still at 5V that switch never enters reverse
blocking and USB VBUS keeps feeding BUS_OUT. The 5V stayed on after the disable,
and without a battery the TS reading (a divider of BUS_OUT) stayed above 2.0V so
every later setExtOutput(true) was cancelled until a power cycle.

- On BUS_OUT_EN 1->0: write BOOST_EN=0 first, wait 200 ms for the bus to
  discharge (measured boundary: 20 ms loaded, 80 ms unloaded), re-read the
  expander and clear only the requested bit. On an I2C failure re-read the latch
  and recover from the actual state, retrying with read-back up to 3 times.
- Other paths write one register at a time, BOOST_EN before the output enable
  when turning on and after it when turning off, and stop if the first write fails.
- Serialize all AW9523 output-port writers in M5Unified (setExtOutput,
  setUsbOutput, Power_Class::begin, the speaker amplifier enable) with a mutex
  and run the no-battery protection check inside the same critical section.
- Protection check: skip it when BUS_OUT_EN is already 1 (the 5V on TS is our own
  output), treat I2C failures and invalid ADC values as unsafe, and when it reads
  unsafe release the lock and re-check every 20 ms for up to 1 s so a re-enable
  right after a disable is not cancelled by the stale TS reading. External bus
  power keeps TS high, so it is still cancelled. Requests carry a generation
  number so an enable that is still waiting yields to a later disable.
@lovyan03
lovyan03 merged commit a977e40 into m5stack:develop Sep 2, 2026
27 checks passed
@ainyan03
ainyan03 deleted the cores3_extout_off_order branch September 2, 2026 10:07
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.

2 participants