Skip to content

Add ToughC5 charge current control - #335

Closed
TinyuZhao wants to merge 39 commits into
m5stack:masterfrom
TinyuZhao:develop
Closed

Add ToughC5 charge current control#335
TinyuZhao wants to merge 39 commits into
m5stack:masterfrom
TinyuZhao:develop

Conversation

@TinyuZhao

Copy link
Copy Markdown
Member
  • PM1: expose PWR_SRC as a bitmap
  • Tab5: fix the battery current sign
  • Add PWM control to M5PM1_Class
  • Configure the ToughC5 buzzer line at startup
  • Replace the IO expander pull enable API with explicit modes
  • Give PM1 PWM duty APIs explicit units
  • Give M5IOE1 PWM duty APIs explicit units
  • Report M5IOE1 PWM frequency write results
  • Enable PM1 external power output(MBUS) for CoreMatrix.
  • Harden PWM initialization failure handling
  • Flush the final partial speaker buffer
  • Round the flushed sample count up to even
  • Use m5gfx::delay for internal hardware waits
  • Return the write status from the IO expander virtuals
  • Validate PI4IOE5V6408 pins and state the acknowledged-write contract
  • Fix PM1 button and wake IRQ handling and correct the power API docs
  • Reset the double-click latch on every reported click
  • Probe the address the board detection is looking for
  • Take the internal bus first, and ask the sensor that is always there
  • Confirm the bus before driving it, and keep the probe slot apart
  • Require the M5GFX that has the software I2C the probe needs
  • Free a data line that is still held before giving up on the bus
  • Wait for the devices to power up once, not once per address
  • Park the lines low before the pins become outputs
  • Add ESP32C5 RTC IRQ clear and unsupported-sleep log warnings
  • Add ToughC5 charge current control

ainyan03 and others added 30 commits August 17, 2026 04:02
The PM1 datasheet defines PWR_SRC as independent 5VIN, 5VINOUT, and battery-valid bits that may be set simultaneously. Return that bitmap directly instead of imposing an undocumented primary-source priority, and update the PaperMono charging check to test the external-power bits.
getBatteryCurrent() documents "+ = charge / - = discharge", but on the
Tab5 the INA226 shunt is wired so that charging reads negative. Verified
on real hardware: with a discharged pack under USB power (voltage rising,
charge status asserted) the API returned about -650 mA. Invert the sign
on the Tab5 path to match the documented convention.
The PM1 exposes two PWM channels, on GPIO3 and GPIO4. Follow the naming and
the units of the standalone M5PM1 driver so that code can move between the
two without surprises: setPwmDuty takes percent, setPwmDuty12bit takes the
raw 12-bit value, and the two boolean arguments are ordered polarity then
enable. The return value is bool for the I2C result, matching the rest of
this class.

Both channels share a single frequency register, so changing it affects a
channel that is already running as well.
The ToughC5 buzzer is driven by PM1 PWM channel 1, so GPIO4 has to be set up
for that function before an application can drive it at all. The PM1 keeps
running across ESP resets and retains its PWM state, so the channel is also
put off at boot, the same way the vibration motor is on the StopWatch.

GPIO4 is normalized in an order that does not drive the buzzer on the way: the
output latch is cleared before the pin becomes an output, and the PWM function
is selected last. Selecting that function is also what would make a retained
duty audible again, so it is only done once the channel is confirmed off. The
write is retried a few times, and if it still cannot be confirmed the pin is
left as a plain output driving low, which is silent whatever the retained PWM
state is. Failing closed loses the buzzer until the next boot, which is the
better direction for a step whose purpose is to guarantee silence.

Stopping the channel on the way into sleep is deliberately left to the
application. The PM1 stays powered while the ESP sleeps, so the application
may intend the PWM output to remain active; whether to stop it is application
policy rather than board initialization.
Add PM1 PWM control and use it for the ToughC5 buzzer
The pull resistor state was split across two virtuals: setPullMode picked
up or down, and enablePull turned pulling on or off. What that pair means
differs per expander. On the M5IOE1 the two share one register pair, so
setPullMode alone already established the state and enablePull(pin, true)
only set the pull-up bit without clearing the pull-down one, which could
leave both enabled at once. On the PI4IOE5V6408 the registers are separate
and the pair genuinely has to be used together, yet nothing in this
repository ever enabled it: the pull-ups in use rely on the reset default
of the enable register.

setPullMode now takes gpio_pull_t and establishes the whole state in one
call, so neither the ordering nor a reset default matters. The values match
the standalone M5IOE1 driver constants. Writes are ordered so that two
pulls are never enabled at the same time, and the enable register is now
written explicitly on the PI4IOE5V6408 rather than assumed.

The setter returns whether the requested state was fully established, since
it takes more than one I2C write and a partial failure is what would leave
a pin in the state this change is meant to rule out. The other virtuals in
IOExpander_Base still return void.

BREAKING: enablePull is gone and setPullMode takes an enum instead of a
bool. Old calls do not compile, because neither bool nor an integer
converts to the enum implicitly.

| Old | New |
| --- | --- |
| enablePull(pin, false) | setPullMode(pin, pull_none) |
| setPullMode(pin, true), with pulling enabled | setPullMode(pin, pull_up) |
| setPullMode(pin, false), with pulling enabled | setPullMode(pin, pull_down) |
Separate percentage and raw 12-bit duty values in the method names, and use a
shared polarity type so boolean arguments cannot be transposed silently.

BREAKING: setPwmDuty is removed and polarity arguments now use pwm_polarity_t.

| Old | New |
| --- | --- |
| setPwmDuty(channel, percent, polarity, enable) | setPwmDutyPercent(channel, percent, polarity, enable) |
| setPwmDuty12bit(channel, raw, polarity, enable) | setPwmDuty12bit(channel, raw, polarity, enable) |

The duty parameters widen to uint32_t so that the documented range is what gets
checked, rather than whatever is left after the value has been narrowed at the
call boundary. Existing in-range calls are unaffected.

The unqualified setPwmDuty name is intentionally left vacant to eliminate silent
semantic changes. This change does not define or promise any future use for that
name.
Replace the IO expander pull enable API with explicit modes
Separate percentage and raw 12-bit duty values in the method names, reject
out-of-range inputs, and migrate existing raw-duty call sites without changing
their output behavior.

BREAKING: setPwmDuty is removed. Use setPwmDuty12bit for existing raw values or
setPwmDutyPercent for percentages; polarity now uses pwm_polarity_t and precedes
enable.

| Old | New |
| --- | --- |
| setPwmDuty(channel, raw, enable, polarity) | setPwmDuty12bit(channel, raw, polarity, enable) |
| No percentage API | setPwmDutyPercent(channel, percent, polarity, enable) |

The unqualified setPwmDuty name is intentionally left vacant to eliminate silent
semantic changes. This change does not define or promise any future use for that
name.
Give the PWM duty APIs explicit units and leave setPwmDuty vacant
Return the underlying I2C write status from setPwmFrequency and document that the configured frequency is shared by every PWM channel.

BREAKING: the setPwmFrequency return type changes from void to bool. Ordinary calls that discard the result remain source-compatible, but code that depends on the exact member-function type must update from void (M5IOE1_Class::*)(uint16_t) to bool (M5IOE1_Class::*)(uint16_t).
Enable PM1 external power output(MBUS) for CoreMatrix.
Harden PWM initialization failure handling
Flush the final partial speaker buffer
Use m5gfx::delay for internal hardware waits
Return the write status from the IO expander virtuals
Fix PM1 button and wake IRQ handling and correct the power API docs
The helper that decides which board this is did not speak I2C. It made no
start condition, clocked every address bit twice over twenty pulses, and
read the ninth bit a millisecond after the clock had gone back down, when
any device that acknowledged had long released the line. What came back
was the pull-up on the pins, not an answer from the address, and the
caller read that value as the device being present.

Every board on the branch has pull-ups on its internal bus, so the first
condition matched on all of them. AtomS3RExt is checked first and Atom
VoiceS3R, which carries an ES8311 where the other carries a BMI270, was
reported as AtomS3RExt.

Send the address over the software I2C port instead, which drives the
lines open drain, keeps away from the peripheral, and reports the
acknowledge it actually receives. The return value now means what the
five call sites always read it as. A stop is offered first, because a
device left mid transfer needs one to let go and one board in this same
file already records a device that needed exactly that; it is made by
driving low and releasing, never by driving high, so a device holding a
line is not fought over. Pins that carry no pulled up bus are left alone,
the same test the display autodetection uses on pins that may not be I2C
at all. A single retry covers a device that is slower to answer than the
old test, which never waited for an answer in the first place.

The name says what it does now: the same name in M5GFX belongs to a
different function with a different contract.
ainyan03 and others added 9 commits August 19, 2026 21:28
The three boards that share the internal bus were separated by a probe on
another pair of pins in between: BMI270, then the Stamp-S3Bat address on
48 and 47, then the ES8311. Atom VoiceS3R drives its speaker out of
GPIO48, so a board fully identified by its own bus reached for the I2S
data line before it got there. Take the internal bus first and only step
off it when nothing there answered.

Within that bus the BMI270 is asked first. The AtomS3R-Ext and the
AtomS3RCam are the same board, and on the Ext the camera end is a small
breadboard the owner is free to wire up, so an address that is not the
BMI270 can appear on the internal bus of a board that is one. The sensor
that is always fitted decides, and something added later cannot take the
identity away.

Measured on hardware: Atom VoiceS3R answers at 0x18 alone, AtomS3RExt and
both AtomS3RCam variants at 0x68 alone, and StampS3Mini has a pull-up on
one line of the pair and nothing on the other.
The probe held the pins low nine times over before it had established that
they carry an I2C bus at all: the stop preamble ran first, and only then
came the test for a pull-up. These pins are how the board is identified,
so until that test passes they may be anything. Run the test first and
offer the stop only once the pins have answered for themselves.

Take the second software I2C slot rather than the one the display
autodetection uses. The slots carry no ownership - opening one takes over
whatever settings were there - so two libraries sharing a slot rests on
nothing but the order they happen to run in.

Drop the retry. Nothing between the two attempts changes the state of the
device being asked: no reset, no power, no clock. It doubled the time
spent on every address that is not answering, which the boards with
nothing on their internal bus pay three times over.
library.json and idf_component.yml already ask for it; the Arduino
manifest asked for M5GFX with no version at all. An older one has no
software I2C behind the negative port numbers, and the port number
reaches the hardware port table as an index instead.
Checking for the pull-up before offering a stop condition left the one bus
that most needs the stop unable to get it. A device interrupted mid read
holds the data line down, and a software reset does not take its power
away, so the line is still held when the board is identified again. The
check reads that as no bus and returns before the stop is ever made.

The signature is specific enough to act on: the clock comes back high
against the internal pull-down while the data line stays low. That is a
held bus, not a pin without a pull-up, where neither line comes back.
Offer the stop in that case and look again; every other answer returns
as it did.
Every probe waited 50ms before it touched the pins. The old probe always
answered on the first address, so that wait was paid once; now that an
address which is not there says so, it is paid again for every address
that misses - four times over on a board that answers nothing on its
internal bus.

The wait is about how long ago the board was powered, not about the
address being asked, so it belongs at the entrance to the board check.
Measured on an AtomS3RCam: one probe went from 49.8ms to 0.3ms, and the
50ms is now spent once for the whole identification.
The bus test set both pins to output while their latches were still high
from the pull-up mode before it, so each pin drove a push-pull high for
the moment before the test pulled it low. On pins that may not be an I2C
bus at all - which is the whole point of the test - that is a driven high
into whatever is on the other side.

Put the latch low first. The lines are still driven low by the test after
that, but a high is never driven at all.
Board detection: probe the address instead of the pull-up (m5stack#330)
@TinyuZhao TinyuZhao closed this Aug 20, 2026
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.

3 participants