Add ToughC5 charge current control - #335
Closed
TinyuZhao wants to merge 39 commits into
Closed
Conversation
TinyuZhao
commented
Aug 20, 2026
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
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.
PM1: expose PWR_SRC as a bitmap
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.
Tab5: fix the battery current sign
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).
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
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.
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)
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.