From 20c8af13927c9fc83c0c580e95c60814a8b4871e Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 17 Aug 2026 04:02:50 +0000 Subject: [PATCH 01/26] PM1: expose PWR_SRC as a bitmap 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. --- src/utility/Power_Class.cpp | 5 +++-- src/utility/power/M5PM1_Class.cpp | 5 ++--- src/utility/power/M5PM1_Class.hpp | 14 ++++++++------ 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/utility/Power_Class.cpp b/src/utility/Power_Class.cpp index 8531e11..7e61273 100644 --- a/src/utility/Power_Class.cpp +++ b/src/utility/Power_Class.cpp @@ -2531,8 +2531,9 @@ namespace m5 #if defined (CONFIG_IDF_TARGET_ESP32S3) case board_t::board_M5PaperMono: { - // Running from battery (no external power) -> not charging. - if (M5pm1.getPowerSource() == M5PM1_Class::battery) { return is_charging_t::is_discharging; } + // No external power -> not charging. PWR_SRC is a bitmap, and the battery bit may coexist with VIN. + auto sources = M5pm1.getPowerSource(); + if (!(sources & (M5PM1_Class::vin | M5PM1_Class::vinout))) { return is_charging_t::is_discharging; } // External power present. The IP2316 charger reports // its state in REG_CHG_STAT(0xC7): bit7 = charging in progress (measured: // 0x82 charging / 0x45 charge-complete / 0x00 charge-disabled). diff --git a/src/utility/power/M5PM1_Class.cpp b/src/utility/power/M5PM1_Class.cpp index bb95adc..a1a8e4e 100644 --- a/src/utility/power/M5PM1_Class.cpp +++ b/src/utility/power/M5PM1_Class.cpp @@ -99,9 +99,8 @@ namespace m5 M5PM1_Class::pwr_src_t M5PM1_Class::getPowerSource(void) { - if (!_init) { return unknown; } - auto src = readRegister8(M5PM1_REG_PWR_SRC) & 0x07; - return src <= static_cast(battery) ? static_cast(src) : unknown; + if (!_init) { return none; } + return static_cast(readRegister8(M5PM1_REG_PWR_SRC) & 0x07); } bool M5PM1_Class::getVbatNodePowered(bool* powered) diff --git a/src/utility/power/M5PM1_Class.hpp b/src/utility/power/M5PM1_Class.hpp index fd504e3..90e0c37 100644 --- a/src/utility/power/M5PM1_Class.hpp +++ b/src/utility/power/M5PM1_Class.hpp @@ -55,12 +55,12 @@ namespace m5 , open_drain = 1 }; - /// PM1 power source status. + /// PM1 power source bitmap. Multiple values may be combined. enum pwr_src_t : std::uint8_t - { vin = 0 - , vinout = 1 - , battery = 2 - , unknown = 3 + { none = 0 + , vin = 1 << 0 + , vinout = 1 << 1 + , battery = 1 << 2 }; /// set BOOST/Grove 5V output enable. @@ -82,7 +82,9 @@ namespace m5 /// @param level true=high / false=low bool setLedEnLevel(bool level); - /// get current PM1 power source. + /// get the PM1 PWR_SRC bitmap. + /// bit0=5VIN valid, bit1=5VINOUT valid (0 while the 5V boost is enabled), + /// bit2=VBAT node valid. Multiple sources may be present simultaneously. pwr_src_t getPowerSource(void); /// get whether PWR_SRC reports the VBAT node rail as powered. From 7103d650bb308516658471d86328bf86e835edcc Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 17 Aug 2026 04:49:06 +0000 Subject: [PATCH 02/26] Tab5: fix the battery current sign 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. --- src/utility/Power_Class.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utility/Power_Class.cpp b/src/utility/Power_Class.cpp index 7e61273..ca7b039 100644 --- a/src/utility/Power_Class.cpp +++ b/src/utility/Power_Class.cpp @@ -2423,7 +2423,9 @@ namespace m5 switch (M5.getBoard()) { #if defined (CONFIG_IDF_TARGET_ESP32P4) case board_t::board_M5Tab5: - return 1000.0f * Ina226.getShuntCurrent(); + // The shunt is wired so that charge current reads negative; invert to + // match the documented convention (+ = charge / - = discharge). + return -1000.0f * Ina226.getShuntCurrent(); #endif #if defined (CONFIG_IDF_TARGET_ESP32S3) From 56f6a927e946eb1c0d7ff83e7280792794694769 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 17 Aug 2026 08:47:27 +0000 Subject: [PATCH 03/26] Add PWM control to M5PM1_Class 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. --- src/utility/power/M5PM1_Class.cpp | 36 +++++++++++++++++++++++++++++++ src/utility/power/M5PM1_Class.hpp | 25 +++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/utility/power/M5PM1_Class.cpp b/src/utility/power/M5PM1_Class.cpp index a1a8e4e..130303b 100644 --- a/src/utility/power/M5PM1_Class.cpp +++ b/src/utility/power/M5PM1_Class.cpp @@ -30,6 +30,8 @@ namespace m5 static constexpr const uint8_t M5PM1_REG_VBAT_L = 0x22; static constexpr const uint8_t M5PM1_REG_VIN_L = 0x24; static constexpr const uint8_t M5PM1_REG_5VOUT_L = 0x26; + static constexpr const uint8_t M5PM1_REG_PWM0_L = 0x30; + static constexpr const uint8_t M5PM1_REG_PWM_FREQ_L = 0x34; static constexpr const uint8_t M5PM1_REG_IRQ_STATUS1 = 0x40; static constexpr const uint8_t M5PM1_REG_IRQ_STATUS2 = 0x41; static constexpr const uint8_t M5PM1_REG_IRQ_STATUS3 = 0x42; @@ -43,6 +45,8 @@ namespace m5 static constexpr const uint8_t M5PM1_PWR_CFG_BOOST_EN = 1 << 3; static constexpr const uint8_t M5PM1_PWR_CFG_LED_EN = 1 << 4; static constexpr const uint8_t M5PM1_SYS_CMD_SHUTDOWN = 0xA1; + static constexpr const uint8_t M5PM1_PWM_ENABLE = 1 << 4; + static constexpr const uint8_t M5PM1_PWM_POLARITY = 1 << 5; static constexpr bool is_valid_gpio(M5PM1_Class::gpio_t pin) { @@ -54,6 +58,11 @@ namespace m5 return static_cast(pin); } + static constexpr bool is_valid_pwm_channel(M5PM1_Class::pwm_channel_t channel) + { + return static_cast(channel) <= M5PM1_Class::pwm_ch1; + } + bool M5PM1_Class::begin(void) { if (!_init) { @@ -175,6 +184,33 @@ namespace m5 return readRegister8(M5PM1_REG_GPIO_OUT) & (1 << gpio_num(pin)); } + bool M5PM1_Class::setPwmFrequency(std::uint16_t frequency) + { + std::uint8_t data[2] = + { static_cast(frequency & 0xFF) + , static_cast(frequency >> 8) + }; + return writeRegister(M5PM1_REG_PWM_FREQ_L, data, sizeof(data)); + } + + bool M5PM1_Class::setPwmDuty(pwm_channel_t channel, std::uint8_t duty, bool polarity, bool enable) + { + if (duty > 100) { return false; } + auto duty12 = static_cast(static_cast(duty) * 0x0FFF / 100); + return setPwmDuty12bit(channel, duty12, polarity, enable); + } + + bool M5PM1_Class::setPwmDuty12bit(pwm_channel_t channel, std::uint16_t duty12, bool polarity, bool enable) + { + if (!is_valid_pwm_channel(channel) || duty12 > 0x0FFF) { return false; } + std::uint8_t high = static_cast(duty12 >> 8); + if (enable) { high |= M5PM1_PWM_ENABLE; } + if (polarity) { high |= M5PM1_PWM_POLARITY; } + std::uint8_t data[2] = { static_cast(duty12 & 0xFF), high }; + auto reg = static_cast(M5PM1_REG_PWM0_L + static_cast(channel) * 2); + return writeRegister(reg, data, sizeof(data)); + } + bool M5PM1_Class::clearWakeSource(std::uint8_t mask) { auto src = readRegister8(M5PM1_REG_WAKE_SRC); diff --git a/src/utility/power/M5PM1_Class.hpp b/src/utility/power/M5PM1_Class.hpp index 90e0c37..eefe7ab 100644 --- a/src/utility/power/M5PM1_Class.hpp +++ b/src/utility/power/M5PM1_Class.hpp @@ -55,6 +55,12 @@ namespace m5 , open_drain = 1 }; + /// PM1 PWM channel. Both channels share the same frequency setting. + enum pwm_channel_t : std::uint8_t + { pwm_ch0 = 0 // GPIO3; may be assigned to another function depending on the board. + , pwm_ch1 = 1 // GPIO4 + }; + /// PM1 power source bitmap. Multiple values may be combined. enum pwr_src_t : std::uint8_t { none = 0 @@ -115,6 +121,25 @@ namespace m5 /// get PM1 GPIO output latch level, not the physical input level. bool getGPIOOutputLatch(gpio_t pin); + /// set the PWM frequency in Hz. + /// @note The frequency is shared by both PWM channels, so changing it also + /// changes a channel that is already running. + bool setPwmFrequency(std::uint16_t frequency); + + /// set PWM duty in percent. + /// @param channel PWM channel (pwm_ch0 / pwm_ch1). + /// @param duty duty cycle in percent (0-100). + /// @param polarity false=normal / true=inverted. + /// @param enable true=enable / false=disable. + bool setPwmDuty(pwm_channel_t channel, std::uint8_t duty, bool polarity = false, bool enable = true); + + /// set PWM duty with 12-bit precision. + /// @param channel PWM channel (pwm_ch0 / pwm_ch1). + /// @param duty12 duty cycle (0-4095). + /// @param polarity false=normal / true=inverted. + /// @param enable true=enable / false=disable. + bool setPwmDuty12bit(pwm_channel_t channel, std::uint16_t duty12, bool polarity = false, bool enable = true); + /// clear PM1 wake source bits selected by mask. bool clearWakeSource(std::uint8_t mask = 0x7F); From 388d61ca5604c34165660da2d73e5d34bf0f3ce7 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 17 Aug 2026 08:47:38 +0000 Subject: [PATCH 04/26] Configure the ToughC5 buzzer line at startup 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. --- src/utility/Power_Class.cpp | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/utility/Power_Class.cpp b/src/utility/Power_Class.cpp index ca7b039..f1a1812 100644 --- a/src/utility/Power_Class.cpp +++ b/src/utility/Power_Class.cpp @@ -275,6 +275,35 @@ namespace m5 _wakeupPin = GPIO_NUM_4; /// bring up the PM1 early so its status registers are readable below. M5pm1.begin(); + /// GPIO4 drives the buzzer through PM1 PWM channel 1. The PM1 keeps + /// running across ESP resets and retains its PWM state, so put the + /// channel off at boot, then normalize the pin before selecting its PWM + /// function. Stopping the channel before sleep is left to the caller: + /// the PM1 stays powered while the ESP sleeps, so the application may + /// intend the PWM output to remain active, which makes it application + /// policy rather than board initialization. + /// Selecting the PWM function is what makes a retained duty audible + /// again, so it is only done once the channel is known to be off. If that + /// cannot be confirmed, the pin is left as a plain output driving low, + /// which is silent whatever the retained PWM state is. + bool pwm_off = false; + for (int retry = 3; !(pwm_off = M5pm1.setPwmDuty12bit(M5PM1_Class::pwm_ch1, 0, false, false)) && --retry; ) + { + m5gfx::delay(10); + } + M5pm1.setGPIODrive(M5PM1_Class::gpio4, M5PM1_Class::push_pull); + M5pm1.setGPIOPull(M5PM1_Class::gpio4, M5PM1_Class::pull_none); + M5pm1.setGPIOOutput(M5PM1_Class::gpio4, false); + M5pm1.setGPIOMode(M5PM1_Class::gpio4, M5PM1_Class::output); + if (pwm_off) + { + M5pm1.setGPIOFunction(M5PM1_Class::gpio4, M5PM1_Class::special); + } + else + { + M5_LOGE("PM1 PWM ch1 could not be turned off. Leaving GPIO4 as a low output."); + M5pm1.setGPIOFunction(M5PM1_Class::gpio4, M5PM1_Class::gpio); + } /// PM1 は常時給電で ESP のリセットを跨いで状態が残るため、直前に動いて /// いたファームの設定に依存しないよう IRQ 関連を初期化する M5pm1.clearWakeSource(); From b201d3c293cdb1694b7683da3f78cce28b8fe200 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 17 Aug 2026 10:48:50 +0000 Subject: [PATCH 05/26] Replace the IO expander pull enable API with explicit modes 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) | --- src/M5Unified.cpp | 4 ++-- src/utility/IOExpander_Base.hpp | 14 ++++++++---- src/utility/M5IOE1_Class.cpp | 34 ++++++++++++++---------------- src/utility/M5IOE1_Class.hpp | 4 +--- src/utility/PI4IOE5V6408_Class.cpp | 29 ++++++++++++------------- src/utility/PI4IOE5V6408_Class.hpp | 5 +---- src/utility/Power_Class.cpp | 12 +++++------ 7 files changed, 50 insertions(+), 52 deletions(-) diff --git a/src/M5Unified.cpp b/src/M5Unified.cpp index f1a80f8..9c6dce1 100644 --- a/src/M5Unified.cpp +++ b/src/M5Unified.cpp @@ -2170,13 +2170,13 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { auto& ioexp = getIOExpander(0); // lcd backlight ioexp.setDirection(7, true); - ioexp.setPullMode(7, false); + ioexp.setPullMode(7, IOExpander_Base::pull_down); ioexp.setHighImpedance(7, false); for (int i = 0; i < 3; ++i) { // button a~c ioexp.setDirection(i, false); - ioexp.setPullMode(i, true); + ioexp.setPullMode(i, IOExpander_Base::pull_up); ioexp.setHighImpedance(i, false); } delay(100); diff --git a/src/utility/IOExpander_Base.hpp b/src/utility/IOExpander_Base.hpp index 2580e4e..a382930 100644 --- a/src/utility/IOExpander_Base.hpp +++ b/src/utility/IOExpander_Base.hpp @@ -12,6 +12,12 @@ namespace m5 class IOExpander_Base : public I2C_Device { public: + enum gpio_pull_t : std::uint8_t + { pull_none = 0 + , pull_up = 1 + , pull_down = 2 + }; + IOExpander_Base(std::uint8_t i2c_addr, std::uint32_t freq = 400000, m5::I2C_Class* i2c = &m5::In_I2C) : I2C_Device(i2c_addr, freq, i2c) {} @@ -20,10 +26,10 @@ namespace m5 // false input, true output virtual void setDirection(uint8_t pin, bool direction) = 0; - virtual void enablePull(uint8_t pin, bool enablePull) = 0; - - // false down, true up - virtual void setPullMode(uint8_t pin, bool mode) = 0; + /// Set the GPIO pull resistor state. + /// @return true only when the requested state was completely established; + /// false for an invalid pin, mode, or I2C failure. + virtual bool setPullMode(uint8_t pin, gpio_pull_t mode) = 0; virtual void setHighImpedance(uint8_t pin, bool enable) = 0; diff --git a/src/utility/M5IOE1_Class.cpp b/src/utility/M5IOE1_Class.cpp index 7b0f08c..c8ad2f1 100644 --- a/src/utility/M5IOE1_Class.cpp +++ b/src/utility/M5IOE1_Class.cpp @@ -48,29 +48,27 @@ namespace m5 direction ? bitOn(reg, bit) : bitOff(reg, bit); } - void M5IOE1_Class::enablePull(uint8_t pin, bool enablePull) + bool M5IOE1_Class::setPullMode(uint8_t pin, gpio_pull_t mode) { - if (!_isValidPin(pin)) { return; } + if (!_isValidPin(pin)) { return false; } const auto pu_reg = _regForPin(M5IOE1_REG_GPIO_PU_L, pin); const auto pd_reg = _regForPin(M5IOE1_REG_GPIO_PD_L, pin); const auto bit = _bitForPin(pin); - if (enablePull) { - bitOn(pu_reg, bit); - } else { - bitOff(pu_reg, bit); - bitOff(pd_reg, bit); + switch (mode) { + case pull_none: { + const bool pu_ok = bitOff(pu_reg, bit); + const bool pd_ok = bitOff(pd_reg, bit); + return pu_ok && pd_ok; + } + case pull_up: + if (!bitOff(pd_reg, bit)) { return false; } + return bitOn(pu_reg, bit); + case pull_down: + if (!bitOff(pu_reg, bit)) { return false; } + return bitOn(pd_reg, bit); + default: + return false; } - } - - void M5IOE1_Class::setPullMode(uint8_t pin, bool mode) - { - if (!_isValidPin(pin)) { return; } - const auto pu_reg = _regForPin(M5IOE1_REG_GPIO_PU_L, pin); - const auto pd_reg = _regForPin(M5IOE1_REG_GPIO_PD_L, pin); - const auto bit = _bitForPin(pin); - // false=pull-down, true=pull-up. - mode ? bitOn(pu_reg, bit) : bitOff(pu_reg, bit); - mode ? bitOff(pd_reg, bit) : bitOn(pd_reg, bit); } void M5IOE1_Class::setHighImpedance(uint8_t pin, bool enable) diff --git a/src/utility/M5IOE1_Class.hpp b/src/utility/M5IOE1_Class.hpp index c6c7fc0..06dfd41 100644 --- a/src/utility/M5IOE1_Class.hpp +++ b/src/utility/M5IOE1_Class.hpp @@ -46,9 +46,7 @@ namespace m5 void setDirection(uint8_t pin, bool direction) override; - void enablePull(uint8_t pin, bool enablePull) override; - - void setPullMode(uint8_t pin, bool mode) override; + bool setPullMode(uint8_t pin, gpio_pull_t mode) override; void setHighImpedance(uint8_t pin, bool enable) override; diff --git a/src/utility/PI4IOE5V6408_Class.cpp b/src/utility/PI4IOE5V6408_Class.cpp index f841ed4..0f71450 100644 --- a/src/utility/PI4IOE5V6408_Class.cpp +++ b/src/utility/PI4IOE5V6408_Class.cpp @@ -29,22 +29,21 @@ void PI4IOE5V6408_Class::setDirection(uint8_t pin, bool direction) } } -void PI4IOE5V6408_Class::enablePull(uint8_t pin, bool enablePull) +bool PI4IOE5V6408_Class::setPullMode(uint8_t pin, gpio_pull_t mode) { - if (enablePull) { - bitOn(0x0B, 1 << pin); - } else { - bitOff(0x0B, 1 << pin); - } -} - -// false down, true up -void PI4IOE5V6408_Class::setPullMode(uint8_t pin, bool mode) -{ - if (mode) { - bitOn(0x0D, 1 << pin); - } else { - bitOff(0x0D, 1 << pin); + if (pin >= 8) return false; + const auto bit = 1 << pin; + switch (mode) { + case pull_none: + return bitOff(0x0B, bit); + case pull_up: + if (!bitOn(0x0D, bit)) { return false; } + return bitOn(0x0B, bit); + case pull_down: + if (!bitOff(0x0D, bit)) { return false; } + return bitOn(0x0B, bit); + default: + return false; } } diff --git a/src/utility/PI4IOE5V6408_Class.hpp b/src/utility/PI4IOE5V6408_Class.hpp index 97e1a95..b7cba8a 100644 --- a/src/utility/PI4IOE5V6408_Class.hpp +++ b/src/utility/PI4IOE5V6408_Class.hpp @@ -33,10 +33,7 @@ namespace m5 // false input, true output void setDirection(uint8_t pin, bool direction) override; - void enablePull(uint8_t pin, bool enablePull) override; - - // false down, true up - void setPullMode(uint8_t pin, bool mode) override; + bool setPullMode(uint8_t pin, gpio_pull_t mode) override; void setHighImpedance(uint8_t pin, bool enable) override; diff --git a/src/utility/Power_Class.cpp b/src/utility/Power_Class.cpp index f1a1812..43e376d 100644 --- a/src/utility/Power_Class.cpp +++ b/src/utility/Power_Class.cpp @@ -321,7 +321,7 @@ namespace m5 auto& ioe1 = M5.getIOExpander(0); ioe1.setDirection(M5IOE1_Class::gpio1, false); ioe1.setHighImpedance(M5IOE1_Class::gpio1, true); - ioe1.enablePull(M5IOE1_Class::gpio1, false); + ioe1.setPullMode(M5IOE1_Class::gpio1, IOExpander_Base::pull_none); ioe1.setDirection(M5IOE1_Class::gpio3, false); } M5pm1.setBatteryCharge(true); @@ -465,7 +465,7 @@ namespace m5 auto& ioe1 = M5.getIOExpander(0); // M5IOE1_G3 -- Charge Status ioe1.setDirection(M5IOE1_Class::gpio3, false); - ioe1.enablePull(M5IOE1_Class::gpio3, false); + ioe1.setPullMode(M5IOE1_Class::gpio3, IOExpander_Base::pull_none); // M5IOE1_G4 -- Boost Control ioe1.setHighImpedance(M5IOE1_Class::gpio4, false); ioe1.setDirection(M5IOE1_Class::gpio4, true); @@ -874,13 +874,13 @@ namespace m5 if (port_mask & ext_port_mask_t::ext_PA) { auto& ioe = M5.getIOExpander(0); - ioe.setPullMode(2, enable); + ioe.setPullMode(2, enable ? IOExpander_Base::pull_up : IOExpander_Base::pull_down); ioe.digitalWrite(2, enable); } if (port_mask & ext_port_mask_t::ext_USB) { auto& ioe = M5.getIOExpander(1); - ioe.setPullMode(3, enable); + ioe.setPullMode(3, enable ? IOExpander_Base::pull_up : IOExpander_Base::pull_down); ioe.digitalWrite(3, enable); } break; @@ -2331,14 +2331,14 @@ namespace m5 auto& ioe1 = M5.getIOExpander(0); if (max_mA >= 650) { - ioe1.enablePull(M5IOE1_Class::gpio3, false); + ioe1.setPullMode(M5IOE1_Class::gpio3, IOExpander_Base::pull_none); ioe1.digitalWrite(M5IOE1_Class::gpio3, false); ioe1.setHighImpedance(M5IOE1_Class::gpio3, false); ioe1.setDirection(M5IOE1_Class::gpio3, true); } else { - ioe1.enablePull(M5IOE1_Class::gpio3, false); + ioe1.setPullMode(M5IOE1_Class::gpio3, IOExpander_Base::pull_none); ioe1.setDirection(M5IOE1_Class::gpio3, false); } } From 3f665a2cb00700a55aca2346cd228126a65ac61f Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 17 Aug 2026 12:29:23 +0000 Subject: [PATCH 06/26] Give PM1 PWM duty APIs explicit units 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. --- src/utility/Power_Class.cpp | 2 +- src/utility/power/M5PM1_Class.cpp | 10 ++++++---- src/utility/power/M5PM1_Class.hpp | 11 +++++++---- src/utility/pwm_types.hpp | 21 +++++++++++++++++++++ 4 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 src/utility/pwm_types.hpp diff --git a/src/utility/Power_Class.cpp b/src/utility/Power_Class.cpp index f1a1812..8bb0aa6 100644 --- a/src/utility/Power_Class.cpp +++ b/src/utility/Power_Class.cpp @@ -287,7 +287,7 @@ namespace m5 /// cannot be confirmed, the pin is left as a plain output driving low, /// which is silent whatever the retained PWM state is. bool pwm_off = false; - for (int retry = 3; !(pwm_off = M5pm1.setPwmDuty12bit(M5PM1_Class::pwm_ch1, 0, false, false)) && --retry; ) + for (int retry = 3; !(pwm_off = M5pm1.setPwmDuty12bit(M5PM1_Class::pwm_ch1, 0, pwm_polarity_t::normal, false)) && --retry; ) { m5gfx::delay(10); } diff --git a/src/utility/power/M5PM1_Class.cpp b/src/utility/power/M5PM1_Class.cpp index 130303b..23c3bbd 100644 --- a/src/utility/power/M5PM1_Class.cpp +++ b/src/utility/power/M5PM1_Class.cpp @@ -193,19 +193,21 @@ namespace m5 return writeRegister(M5PM1_REG_PWM_FREQ_L, data, sizeof(data)); } - bool M5PM1_Class::setPwmDuty(pwm_channel_t channel, std::uint8_t duty, bool polarity, bool enable) + bool M5PM1_Class::setPwmDutyPercent(pwm_channel_t channel, std::uint32_t duty, + pwm_polarity_t polarity, bool enable) { if (duty > 100) { return false; } - auto duty12 = static_cast(static_cast(duty) * 0x0FFF / 100); + auto duty12 = duty * 0x0FFF / 100; return setPwmDuty12bit(channel, duty12, polarity, enable); } - bool M5PM1_Class::setPwmDuty12bit(pwm_channel_t channel, std::uint16_t duty12, bool polarity, bool enable) + bool M5PM1_Class::setPwmDuty12bit(pwm_channel_t channel, std::uint32_t duty12, + pwm_polarity_t polarity, bool enable) { if (!is_valid_pwm_channel(channel) || duty12 > 0x0FFF) { return false; } std::uint8_t high = static_cast(duty12 >> 8); if (enable) { high |= M5PM1_PWM_ENABLE; } - if (polarity) { high |= M5PM1_PWM_POLARITY; } + if (polarity == pwm_polarity_t::inverted) { high |= M5PM1_PWM_POLARITY; } std::uint8_t data[2] = { static_cast(duty12 & 0xFF), high }; auto reg = static_cast(M5PM1_REG_PWM0_L + static_cast(channel) * 2); return writeRegister(reg, data, sizeof(data)); diff --git a/src/utility/power/M5PM1_Class.hpp b/src/utility/power/M5PM1_Class.hpp index eefe7ab..a73f5c5 100644 --- a/src/utility/power/M5PM1_Class.hpp +++ b/src/utility/power/M5PM1_Class.hpp @@ -5,6 +5,7 @@ #define __M5_M5PM1_CLASS_H__ #include "../I2C_Class.hpp" +#include "../pwm_types.hpp" namespace m5 { @@ -129,16 +130,18 @@ namespace m5 /// set PWM duty in percent. /// @param channel PWM channel (pwm_ch0 / pwm_ch1). /// @param duty duty cycle in percent (0-100). - /// @param polarity false=normal / true=inverted. + /// @param polarity PWM output polarity. /// @param enable true=enable / false=disable. - bool setPwmDuty(pwm_channel_t channel, std::uint8_t duty, bool polarity = false, bool enable = true); + bool setPwmDutyPercent(pwm_channel_t channel, std::uint32_t duty, + pwm_polarity_t polarity = pwm_polarity_t::normal, bool enable = true); /// set PWM duty with 12-bit precision. /// @param channel PWM channel (pwm_ch0 / pwm_ch1). /// @param duty12 duty cycle (0-4095). - /// @param polarity false=normal / true=inverted. + /// @param polarity PWM output polarity. /// @param enable true=enable / false=disable. - bool setPwmDuty12bit(pwm_channel_t channel, std::uint16_t duty12, bool polarity = false, bool enable = true); + bool setPwmDuty12bit(pwm_channel_t channel, std::uint32_t duty12, + pwm_polarity_t polarity = pwm_polarity_t::normal, bool enable = true); /// clear PM1 wake source bits selected by mask. bool clearWakeSource(std::uint8_t mask = 0x7F); diff --git a/src/utility/pwm_types.hpp b/src/utility/pwm_types.hpp new file mode 100644 index 0000000..0dc6fe0 --- /dev/null +++ b/src/utility/pwm_types.hpp @@ -0,0 +1,21 @@ +// Copyright (c) M5Stack. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#ifndef __M5_PWM_TYPES_H__ +#define __M5_PWM_TYPES_H__ + +#include + +namespace m5 +{ + /// PWM output polarity. + /// normal : the duty is the high time. The output idles low (POL = 0). + /// inverted : the duty is the low time. The output idles high (POL = 1), + /// which the datasheet calls active low. + enum class pwm_polarity_t : std::uint8_t + { normal = 0 + , inverted = 1 + }; +} + +#endif From 5325b9ae6b4853f9e53e2eab9363a1eca332b6cb Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 17 Aug 2026 12:31:19 +0000 Subject: [PATCH 07/26] Give M5IOE1 PWM duty APIs explicit units 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. --- src/utility/M5IOE1_Class.cpp | 19 ++++++++++++++----- src/utility/M5IOE1_Class.hpp | 17 ++++++++++++++++- src/utility/Power_Class.cpp | 6 +++--- src/utility/led/LED_PaperMono_Class.cpp | 2 +- 4 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/utility/M5IOE1_Class.cpp b/src/utility/M5IOE1_Class.cpp index 7b0f08c..1d2515c 100644 --- a/src/utility/M5IOE1_Class.cpp +++ b/src/utility/M5IOE1_Class.cpp @@ -117,15 +117,24 @@ namespace m5 writeRegister(M5IOE1_REG_PWM_FREQ_L, data, sizeof(data)); } - void M5IOE1_Class::setPwmDuty(std::uint8_t channel, std::uint16_t duty12, bool enable, bool polarity) + bool M5IOE1_Class::setPwmDutyPercent(pwm_channel_t channel, std::uint32_t duty, + pwm_polarity_t polarity, bool enable) { - if (channel > pwm_ch4) { return; } - duty12 &= 0x0FFF; + if (duty > 100) { return false; } + auto duty12 = duty * 0x0FFF / 100; + return setPwmDuty12bit(channel, duty12, polarity, enable); + } + + bool M5IOE1_Class::setPwmDuty12bit(pwm_channel_t channel, std::uint32_t duty12, + pwm_polarity_t polarity, bool enable) + { + if (channel > pwm_ch4 || duty12 > 0x0FFF) { return false; } std::uint8_t high = static_cast(duty12 >> 8); if (enable) { high |= M5IOE1_PWM_ENABLE; } - if (polarity) { high |= M5IOE1_PWM_POLARITY; } + if (polarity == pwm_polarity_t::inverted) { high |= M5IOE1_PWM_POLARITY; } std::uint8_t data[2] = { static_cast(duty12 & 0xFF), high }; - writeRegister(static_cast(M5IOE1_REG_PWM1_DUTY_L + channel * 2), data, sizeof(data)); + auto reg = static_cast(M5IOE1_REG_PWM1_DUTY_L + static_cast(channel) * 2); + return writeRegister(reg, data, sizeof(data)); } void M5IOE1_Class::resetIrq() diff --git a/src/utility/M5IOE1_Class.hpp b/src/utility/M5IOE1_Class.hpp index c6c7fc0..0901e4e 100644 --- a/src/utility/M5IOE1_Class.hpp +++ b/src/utility/M5IOE1_Class.hpp @@ -5,6 +5,7 @@ #define __M5_M5IOE1_CLASS_H__ #include "IOExpander_Base.hpp" +#include "pwm_types.hpp" namespace m5 { @@ -61,7 +62,21 @@ namespace m5 void setPwmFrequency(std::uint16_t frequency); - void setPwmDuty(std::uint8_t channel, std::uint16_t duty12, bool enable = true, bool polarity = false); + /// set PWM duty in percent. + /// @param channel PWM channel (pwm_ch1 - pwm_ch4). + /// @param duty duty cycle in percent (0-100). + /// @param polarity PWM output polarity. + /// @param enable true=enable / false=disable. + bool setPwmDutyPercent(pwm_channel_t channel, std::uint32_t duty, + pwm_polarity_t polarity = pwm_polarity_t::normal, bool enable = true); + + /// set PWM duty with 12-bit precision. + /// @param channel PWM channel (pwm_ch1 - pwm_ch4). + /// @param duty12 duty cycle (0-4095). + /// @param polarity PWM output polarity. + /// @param enable true=enable / false=disable. + bool setPwmDuty12bit(pwm_channel_t channel, std::uint32_t duty12, + pwm_polarity_t polarity = pwm_polarity_t::normal, bool enable = true); void resetIrq() override; diff --git a/src/utility/Power_Class.cpp b/src/utility/Power_Class.cpp index 8bb0aa6..b1f6fe9 100644 --- a/src/utility/Power_Class.cpp +++ b/src/utility/Power_Class.cpp @@ -402,7 +402,7 @@ namespace m5 // IO9 (G9 motor / PWM1): push-pull output, duty off until setVibration ioe1.setHighImpedance(M5IOE1_Class::gpio9, false); ioe1.setDirection(M5IOE1_Class::gpio9, true); - ioe1.setPwmDuty(M5IOE1_Class::pwm_ch1, 0, false); // PWM off at boot + ioe1.setPwmDuty12bit(M5IOE1_Class::pwm_ch1, 0, pwm_polarity_t::normal, false); // PWM off at boot } break; @@ -2780,13 +2780,13 @@ namespace m5 // M5IOE1 PWM1 (0x1B/0x1C) -> pin IO9 / G9 motor; duty 12-bit in [11:0], EN=bit7 of high byte. auto& ioe1 = static_cast(M5.getIOExpander(0)); if (level == 0) { - ioe1.setPwmDuty(M5IOE1_Class::pwm_ch1, 0, false); + ioe1.setPwmDuty12bit(M5IOE1_Class::pwm_ch1, 0, pwm_polarity_t::normal, false); } else { // PWM needs IO9 in output mode (M5IOE1 pin index 8 -> GPIO_MODE_H bit0). ioe1.setHighImpedance(M5IOE1_Class::gpio9, false); ioe1.setDirection(M5IOE1_Class::gpio9, true); uint16_t duty12 = static_cast((static_cast(level) * 0x0FFFu) / 255u); - ioe1.setPwmDuty(M5IOE1_Class::pwm_ch1, duty12); + ioe1.setPwmDuty12bit(M5IOE1_Class::pwm_ch1, duty12); } return; } diff --git a/src/utility/led/LED_PaperMono_Class.cpp b/src/utility/led/LED_PaperMono_Class.cpp index 8650fdd..bb3ab26 100644 --- a/src/utility/led/LED_PaperMono_Class.cpp +++ b/src/utility/led/LED_PaperMono_Class.cpp @@ -76,7 +76,7 @@ namespace m5 ioe1.digitalWrite(ioe1_led_b_pin, b >= 2048); if (g > 4095) { g = 4095; } - ioe1.setPwmDuty(M5IOE1_Class::pwm_ch2, g, g > 0); + ioe1.setPwmDuty12bit(M5IOE1_Class::pwm_ch2, g, pwm_polarity_t::normal, g > 0); } } From 4817f6f1bad75afad0c47674109910fa6ea85555 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 17 Aug 2026 13:14:32 +0000 Subject: [PATCH 08/26] Report M5IOE1 PWM frequency write results 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). --- src/utility/M5IOE1_Class.cpp | 4 ++-- src/utility/M5IOE1_Class.hpp | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/utility/M5IOE1_Class.cpp b/src/utility/M5IOE1_Class.cpp index 591c261..5fb8d9c 100644 --- a/src/utility/M5IOE1_Class.cpp +++ b/src/utility/M5IOE1_Class.cpp @@ -109,10 +109,10 @@ namespace m5 return true; } - void M5IOE1_Class::setPwmFrequency(std::uint16_t frequency) + bool M5IOE1_Class::setPwmFrequency(std::uint16_t frequency) { std::uint8_t data[2] = { static_cast(frequency & 0xFF), static_cast(frequency >> 8) }; - writeRegister(M5IOE1_REG_PWM_FREQ_L, data, sizeof(data)); + return writeRegister(M5IOE1_REG_PWM_FREQ_L, data, sizeof(data)); } bool M5IOE1_Class::setPwmDutyPercent(pwm_channel_t channel, std::uint32_t duty, diff --git a/src/utility/M5IOE1_Class.hpp b/src/utility/M5IOE1_Class.hpp index d073aac..be1568b 100644 --- a/src/utility/M5IOE1_Class.hpp +++ b/src/utility/M5IOE1_Class.hpp @@ -58,7 +58,10 @@ namespace m5 bool digitalRead(uint8_t pin) override; bool getInputLevel(uint8_t pin, bool* level) override; - void setPwmFrequency(std::uint16_t frequency); + /// set the PWM frequency in Hz. + /// @note The frequency is shared by all PWM channels, so changing it also + /// changes a channel that is already running. + bool setPwmFrequency(std::uint16_t frequency); /// set PWM duty in percent. /// @param channel PWM channel (pwm_ch1 - pwm_ch4). From 871ed0199723f61fcd326ec4b9ba6844d2366846 Mon Sep 17 00:00:00 2001 From: Tinyu Date: Tue, 18 Aug 2026 11:51:38 +0800 Subject: [PATCH 09/26] Enable PM1 external power output(MBUS) for CoreMatrix. --- src/utility/Power_Class.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utility/Power_Class.cpp b/src/utility/Power_Class.cpp index aad2a6f..8c45bed 100644 --- a/src/utility/Power_Class.cpp +++ b/src/utility/Power_Class.cpp @@ -221,6 +221,8 @@ namespace m5 _wakeupPin = GPIO_NUM_2; /// bring up the PM1 early so its status registers are readable below. M5pm1.begin(); + // Enable the PM1 5V boost output (MBUS 5V and 3.3V) + M5pm1.setExtOutput(true); /// KEY1/2/3 are wired to PM1 GPIO0/1/2 (pressed = LOW) M5pm1.setGPIOFunction(M5PM1_Class::gpio0, M5PM1_Class::gpio); M5pm1.setGPIOFunction(M5PM1_Class::gpio1, M5PM1_Class::gpio); From 2800183e0dcd89ee91caedd1d5a7efe33c63bc71 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Tue, 18 Aug 2026 04:00:31 +0000 Subject: [PATCH 10/26] Harden PWM initialization failure handling --- src/utility/Power_Class.cpp | 36 ++++++++++++++++++++++++------- src/utility/power/M5PM1_Class.hpp | 6 ++++++ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/utility/Power_Class.cpp b/src/utility/Power_Class.cpp index aad2a6f..624b98b 100644 --- a/src/utility/Power_Class.cpp +++ b/src/utility/Power_Class.cpp @@ -301,8 +301,19 @@ namespace m5 } else { - M5_LOGE("PM1 PWM ch1 could not be turned off. Leaving GPIO4 as a low output."); - M5pm1.setGPIOFunction(M5PM1_Class::gpio4, M5PM1_Class::gpio); + bool gpio_fallback = false; + for (int retry = 3; !(gpio_fallback = M5pm1.setGPIOFunction(M5PM1_Class::gpio4, M5PM1_Class::gpio)) && --retry; ) + { + m5gfx::delay(10); + } + if (gpio_fallback) + { + M5_LOGE("PM1 PWM ch1 could not be turned off. GPIO4 was switched to GPIO mode."); + } + else + { + M5_LOGE("PM1 PWM ch1 could not be turned off, and GPIO4 could not be switched to GPIO mode; silence cannot be guaranteed."); + } } /// PM1 は常時給電で ESP のリセットを跨いで状態が残るため、直前に動いて /// いたファームの設定に依存しないよう IRQ 関連を初期化する @@ -321,7 +332,10 @@ namespace m5 auto& ioe1 = M5.getIOExpander(0); ioe1.setDirection(M5IOE1_Class::gpio1, false); ioe1.setHighImpedance(M5IOE1_Class::gpio1, true); - ioe1.setPullMode(M5IOE1_Class::gpio1, IOExpander_Base::pull_none); + if (!ioe1.setPullMode(M5IOE1_Class::gpio1, IOExpander_Base::pull_none)) + { + M5_LOGE("M5IOE1 CHG_PROG pull state could not be released."); + } ioe1.setDirection(M5IOE1_Class::gpio3, false); } M5pm1.setBatteryCharge(true); @@ -398,11 +412,17 @@ namespace m5 // M5IOE1: PWM1 drives IO9 (G9 motor). REG_PWM_FREQ 0x25/0x26 Hz LE; REG_PWM1_DUTY 0x1B/0x1C (bit7 EN). constexpr uint16_t motor_pwm_hz = 2000; auto& ioe1 = static_cast(M5.getIOExpander(0)); - ioe1.setPwmFrequency(motor_pwm_hz); - // IO9 (G9 motor / PWM1): push-pull output, duty off until setVibration - ioe1.setHighImpedance(M5IOE1_Class::gpio9, false); - ioe1.setDirection(M5IOE1_Class::gpio9, true); - ioe1.setPwmDuty12bit(M5IOE1_Class::pwm_ch1, 0, pwm_polarity_t::normal, false); // PWM off at boot + if (ioe1.setPwmDuty12bit(M5IOE1_Class::pwm_ch1, 0, pwm_polarity_t::normal, false)) + { + ioe1.setPwmFrequency(motor_pwm_hz); + // IO9 (G9 motor / PWM1): push-pull output, duty off until setVibration + ioe1.setHighImpedance(M5IOE1_Class::gpio9, false); + ioe1.setDirection(M5IOE1_Class::gpio9, true); + } + else + { + M5_LOGE("M5IOE1 PWM ch1 could not be turned off. Motor output was not enabled."); + } } break; diff --git a/src/utility/power/M5PM1_Class.hpp b/src/utility/power/M5PM1_Class.hpp index a73f5c5..f07796d 100644 --- a/src/utility/power/M5PM1_Class.hpp +++ b/src/utility/power/M5PM1_Class.hpp @@ -125,6 +125,8 @@ namespace m5 /// set the PWM frequency in Hz. /// @note The frequency is shared by both PWM channels, so changing it also /// changes a channel that is already running. + /// @note PWM channel 0 maps to GPIO3 and channel 1 maps to GPIO4. Call + /// setGPIOFunction() with special separately to route PWM to the pin. bool setPwmFrequency(std::uint16_t frequency); /// set PWM duty in percent. @@ -132,6 +134,8 @@ namespace m5 /// @param duty duty cycle in percent (0-100). /// @param polarity PWM output polarity. /// @param enable true=enable / false=disable. + /// @note PWM channel 0 maps to GPIO3 and channel 1 maps to GPIO4. Call + /// setGPIOFunction() with special separately to route PWM to the pin. bool setPwmDutyPercent(pwm_channel_t channel, std::uint32_t duty, pwm_polarity_t polarity = pwm_polarity_t::normal, bool enable = true); @@ -140,6 +144,8 @@ namespace m5 /// @param duty12 duty cycle (0-4095). /// @param polarity PWM output polarity. /// @param enable true=enable / false=disable. + /// @note PWM channel 0 maps to GPIO3 and channel 1 maps to GPIO4. Call + /// setGPIOFunction() with special separately to route PWM to the pin. bool setPwmDuty12bit(pwm_channel_t channel, std::uint32_t duty12, pwm_polarity_t polarity = pwm_polarity_t::normal, bool enable = true); From b477596c41952d4545bb9f6eeb6d44f4f44275bf Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Tue, 18 Aug 2026 04:00:31 +0000 Subject: [PATCH 11/26] Flush the final partial speaker buffer --- src/utility/Speaker_Class.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utility/Speaker_Class.cpp b/src/utility/Speaker_Class.cpp index 38d2fc9..061bc9c 100644 --- a/src/utility/Speaker_Class.cpp +++ b/src/utility/Speaker_Class.cpp @@ -682,6 +682,7 @@ namespace m5 uint8_t next_state = ch_info->wavinfo[flip].state.load(std::memory_order_acquire); size_t idx = 0; + bool flush_partial = false; if (current_wav->repeat == 0 || ((next_state & (wav_phase_mask | wav_state_stop_current)) == (wav_phase_published | wav_state_stop_current))) @@ -703,6 +704,7 @@ namespace m5 // further below, once flip has moved off of it - freeing it // here would let a writer claim it while flip still points at // it, and the later retirement would wipe that claim out. + flush_partial = false; current_wav->clear(); } // the finished (or cut) request goes back to the writers before @@ -737,6 +739,7 @@ namespace m5 { // nothing to do; a writer caught mid-publish raises the bit itself. ch_info->diff = 0; ch_info->index = 0; + if (flush_partial && data_length < idx) { data_length = idx; } continue; } self->_play_channel_bits.fetch_or(1 << ch); @@ -768,6 +771,7 @@ namespace m5 current_wav->repeat = --repeat; if (repeat == 0) { + flush_partial = true; goto label_next_wav; } } From 94dffd8fda11b864afeb8ba3c0e34522ab079ec1 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Tue, 18 Aug 2026 04:23:59 +0000 Subject: [PATCH 12/26] Round the flushed sample count up to even --- src/utility/Speaker_Class.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/utility/Speaker_Class.cpp b/src/utility/Speaker_Class.cpp index 061bc9c..a0df266 100644 --- a/src/utility/Speaker_Class.cpp +++ b/src/utility/Speaker_Class.cpp @@ -739,7 +739,11 @@ namespace m5 { // nothing to do; a writer caught mid-publish raises the bit itself. ch_info->diff = 0; ch_info->index = 0; - if (flush_partial && data_length < idx) { data_length = idx; } + if (flush_partial) + { // Keep I2S words aligned and avoid a trailing half-word on HW v1. + const size_t flush_length = (idx + 1) & ~size_t{1}; + if (data_length < flush_length) { data_length = flush_length; } + } continue; } self->_play_channel_bits.fetch_or(1 << ch); From e6bc8b573f60f20138620f6da8983c06a192c091 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Tue, 18 Aug 2026 04:26:23 +0000 Subject: [PATCH 13/26] Use m5gfx::delay for internal hardware waits --- src/M5Unified.cpp | 4 ++-- src/utility/power/BQ27220_Class.cpp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/M5Unified.cpp b/src/M5Unified.cpp index 9c6dce1..c447497 100644 --- a/src/M5Unified.cpp +++ b/src/M5Unified.cpp @@ -481,7 +481,7 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { while (*bulk_data) { uint8_t len = *bulk_data++; uint8_t r = retry + 1; - while (!M5.In_I2C.writeRegister(i2c_addr, bulk_data[0], &bulk_data[1], len - 1, i2c_freq) && --r) { M5.delay(1); } + while (!M5.In_I2C.writeRegister(i2c_addr, bulk_data[0], &bulk_data[1], len - 1, i2c_freq) && --r) { m5gfx::delay(1); } bulk_data += len; } } @@ -764,7 +764,7 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { // 状態へ落としてから終了することで、次回 enable を常に定義済み状態から始める。 M5.In_I2C.bitOff(pi4io1_i2c_addr, 0x05, 0b00000010, 400000); // AMP off (過渡音を出さない) M5.In_I2C.writeRegister8(es8388_i2c_addr, 25, 0x24, 400000); // DACCONTROL3: mute (SoftRamp 維持) - M5.delay(1); // soft-ramp 遷移待ち + m5gfx::delay(1); // soft-ramp 遷移待ち M5.In_I2C.writeRegister8(es8388_i2c_addr, 4, 0xC0, 400000); // DACPOWER: DAC L/R down + 全出力 off M5.In_I2C.writeRegister8(es8388_i2c_addr, 2, 0xFF, 400000); // CHIPPOWER: 全停止 (ADF deinit と同一の終端状態) } diff --git a/src/utility/power/BQ27220_Class.cpp b/src/utility/power/BQ27220_Class.cpp index 8786c1a..db2baf9 100644 --- a/src/utility/power/BQ27220_Class.cpp +++ b/src/utility/power/BQ27220_Class.cpp @@ -3,7 +3,7 @@ #include "BQ27220_Class.hpp" -#include +#include #include #include @@ -21,7 +21,7 @@ namespace m5 if (length == 0) { return true; } - M5.delay(10); + m5gfx::delay(10); if (_i2c->readRegister(_addr, 0x3E, regData, length, _freq)) { return true; } @@ -38,7 +38,7 @@ namespace m5 read_MuxAddrdata(0x00, 0x0001, read_data, 4); // printf("BQ27220 W:0x00->0x0001 R:0x%02X %02X %02X %02X\r\n", read_data[0], read_data[1], read_data[2], read_data[3]); - M5.delay(200); + m5gfx::delay(200); // exit_sealed read_MuxAddrdata(0x00, 0x8000); From 9336c5750dbab2e26b9328af18907b3c6a5067c8 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Tue, 18 Aug 2026 04:54:31 +0000 Subject: [PATCH 14/26] Return the write status from the IO expander virtuals --- src/utility/IOExpander_Base.hpp | 24 +++++++++++++++++------ src/utility/M5IOE1_Class.cpp | 30 ++++++++++++++--------------- src/utility/M5IOE1_Class.hpp | 12 ++++++------ src/utility/PI4IOE5V6408_Class.cpp | 31 +++++++++++++++--------------- src/utility/PI4IOE5V6408_Class.hpp | 12 ++++++------ 5 files changed, 61 insertions(+), 48 deletions(-) diff --git a/src/utility/IOExpander_Base.hpp b/src/utility/IOExpander_Base.hpp index a382930..f43eba1 100644 --- a/src/utility/IOExpander_Base.hpp +++ b/src/utility/IOExpander_Base.hpp @@ -24,18 +24,24 @@ namespace m5 IOExpander_Base(const IOExpander_Base&) = delete; // false input, true output - virtual void setDirection(uint8_t pin, bool direction) = 0; + /// @return true only when the requested state was completely established; + /// false for an invalid pin or I2C failure. + virtual bool setDirection(uint8_t pin, bool direction) = 0; /// Set the GPIO pull resistor state. /// @return true only when the requested state was completely established; /// false for an invalid pin, mode, or I2C failure. virtual bool setPullMode(uint8_t pin, gpio_pull_t mode) = 0; - virtual void setHighImpedance(uint8_t pin, bool enable) = 0; + /// @return true only when the requested state was completely established; + /// false for an invalid pin or I2C failure. + virtual bool setHighImpedance(uint8_t pin, bool enable) = 0; virtual bool getWriteValue(uint8_t pin) = 0; - virtual void digitalWrite(uint8_t pin, bool level) = 0; + /// @return true only when the requested state was completely established; + /// false for an invalid pin or I2C failure. + virtual bool digitalWrite(uint8_t pin, bool level) = 0; virtual bool digitalRead(uint8_t pin) = 0; @@ -48,11 +54,17 @@ namespace m5 return false; } - virtual void resetIrq() = 0; + /// @return true only when the requested state was completely established; + /// false on I2C failure. + virtual bool resetIrq() = 0; - virtual void disableIrq() = 0; + /// @return true only when the requested state was completely established; + /// false on I2C failure. + virtual bool disableIrq() = 0; - virtual void enableIrq() = 0; + /// @return true only when the requested state was completely established; + /// false on I2C failure. + virtual bool enableIrq() = 0; }; } diff --git a/src/utility/M5IOE1_Class.cpp b/src/utility/M5IOE1_Class.cpp index 5fb8d9c..bb55ba1 100644 --- a/src/utility/M5IOE1_Class.cpp +++ b/src/utility/M5IOE1_Class.cpp @@ -39,13 +39,13 @@ namespace m5 return _init; } - void M5IOE1_Class::setDirection(uint8_t pin, bool direction) + bool M5IOE1_Class::setDirection(uint8_t pin, bool direction) { - if (!_isValidPin(pin)) { return; } + if (!_isValidPin(pin)) { return false; } // false=input, true=output. MODE bit set means output. const auto reg = _regForPin(M5IOE1_REG_GPIO_MODE_L, pin); const auto bit = _bitForPin(pin); - direction ? bitOn(reg, bit) : bitOff(reg, bit); + return direction ? bitOn(reg, bit) : bitOff(reg, bit); } bool M5IOE1_Class::setPullMode(uint8_t pin, gpio_pull_t mode) @@ -71,13 +71,13 @@ namespace m5 } } - void M5IOE1_Class::setHighImpedance(uint8_t pin, bool enable) + bool M5IOE1_Class::setHighImpedance(uint8_t pin, bool enable) { - if (!_isValidPin(pin)) { return; } + if (!_isValidPin(pin)) { return false; } // M5IOE1 exposes drive mode here: 0=push-pull, 1=open-drain. const auto reg = _regForPin(M5IOE1_REG_GPIO_DRV_L, pin); const auto bit = _bitForPin(pin); - enable ? bitOn(reg, bit) : bitOff(reg, bit); + return enable ? bitOn(reg, bit) : bitOff(reg, bit); } bool M5IOE1_Class::getWriteValue(uint8_t pin) @@ -86,12 +86,12 @@ namespace m5 return (readRegister8(_regForPin(M5IOE1_REG_GPIO_OUT_L, pin)) & _bitForPin(pin)) != 0; } - void M5IOE1_Class::digitalWrite(uint8_t pin, bool level) + bool M5IOE1_Class::digitalWrite(uint8_t pin, bool level) { - if (!_isValidPin(pin)) { return; } + if (!_isValidPin(pin)) { return false; } const auto reg = _regForPin(M5IOE1_REG_GPIO_OUT_L, pin); const auto bit = _bitForPin(pin); - level ? bitOn(reg, bit) : bitOff(reg, bit); + return level ? bitOn(reg, bit) : bitOff(reg, bit); } bool M5IOE1_Class::digitalRead(uint8_t pin) @@ -135,21 +135,21 @@ namespace m5 return writeRegister(reg, data, sizeof(data)); } - void M5IOE1_Class::resetIrq() + bool M5IOE1_Class::resetIrq() { std::uint8_t data[2] = { 0x00, 0x00 }; - writeRegister(M5IOE1_REG_GPIO_IS_L, data, sizeof(data)); + return writeRegister(M5IOE1_REG_GPIO_IS_L, data, sizeof(data)); } - void M5IOE1_Class::disableIrq() + bool M5IOE1_Class::disableIrq() { std::uint8_t data[2] = { 0x00, 0x00 }; - writeRegister(M5IOE1_REG_GPIO_IE_L, data, sizeof(data)); + return writeRegister(M5IOE1_REG_GPIO_IE_L, data, sizeof(data)); } - void M5IOE1_Class::enableIrq() + bool M5IOE1_Class::enableIrq() { std::uint8_t data[2] = { 0xFF, 0x3F }; - writeRegister(M5IOE1_REG_GPIO_IE_L, data, sizeof(data)); + return writeRegister(M5IOE1_REG_GPIO_IE_L, data, sizeof(data)); } } diff --git a/src/utility/M5IOE1_Class.hpp b/src/utility/M5IOE1_Class.hpp index be1568b..0fd5e4a 100644 --- a/src/utility/M5IOE1_Class.hpp +++ b/src/utility/M5IOE1_Class.hpp @@ -45,15 +45,15 @@ namespace m5 bool begin(); - void setDirection(uint8_t pin, bool direction) override; + bool setDirection(uint8_t pin, bool direction) override; bool setPullMode(uint8_t pin, gpio_pull_t mode) override; - void setHighImpedance(uint8_t pin, bool enable) override; + bool setHighImpedance(uint8_t pin, bool enable) override; bool getWriteValue(uint8_t pin) override; - void digitalWrite(uint8_t pin, bool level) override; + bool digitalWrite(uint8_t pin, bool level) override; bool digitalRead(uint8_t pin) override; bool getInputLevel(uint8_t pin, bool* level) override; @@ -79,11 +79,11 @@ namespace m5 bool setPwmDuty12bit(pwm_channel_t channel, std::uint32_t duty12, pwm_polarity_t polarity = pwm_polarity_t::normal, bool enable = true); - void resetIrq() override; + bool resetIrq() override; - void disableIrq() override; + bool disableIrq() override; - void enableIrq() override; + bool enableIrq() override; private: static bool _isValidPin(uint8_t pin) { return pin < 14; } diff --git a/src/utility/PI4IOE5V6408_Class.cpp b/src/utility/PI4IOE5V6408_Class.cpp index 0f71450..a1be83f 100644 --- a/src/utility/PI4IOE5V6408_Class.cpp +++ b/src/utility/PI4IOE5V6408_Class.cpp @@ -20,12 +20,12 @@ bool PI4IOE5V6408_Class::begin() } // false input, true output -void PI4IOE5V6408_Class::setDirection(uint8_t pin, bool direction) +bool PI4IOE5V6408_Class::setDirection(uint8_t pin, bool direction) { if (direction) { - bitOn(0x03, 1 << pin); // Output, set 1 + return bitOn(0x03, 1 << pin); // Output, set 1 } else { - bitOff(0x03, 1 << pin); // Input, set 0 + return bitOff(0x03, 1 << pin); // Input, set 0 } } @@ -47,12 +47,12 @@ bool PI4IOE5V6408_Class::setPullMode(uint8_t pin, gpio_pull_t mode) } } -void PI4IOE5V6408_Class::setHighImpedance(uint8_t pin, bool enable) +bool PI4IOE5V6408_Class::setHighImpedance(uint8_t pin, bool enable) { if (enable) { - bitOn(0x07, 1 << pin); + return bitOn(0x07, 1 << pin); } else { - bitOff(0x07, 1 << pin); + return bitOff(0x07, 1 << pin); } } @@ -62,12 +62,12 @@ bool PI4IOE5V6408_Class::getWriteValue(uint8_t pin) return (data & (1 << pin)) != 0; } -void PI4IOE5V6408_Class::digitalWrite(uint8_t pin, bool level) +bool PI4IOE5V6408_Class::digitalWrite(uint8_t pin, bool level) { if (level) { - bitOn(0x05, 1 << pin); + return bitOn(0x05, 1 << pin); } else { - bitOff(0x05, 1 << pin); + return bitOff(0x05, 1 << pin); } } @@ -77,18 +77,19 @@ bool PI4IOE5V6408_Class::digitalRead(uint8_t pin) return (data & (1 << pin)) != 0; } -void PI4IOE5V6408_Class::resetIrq() +bool PI4IOE5V6408_Class::resetIrq() { - readRegister8(0x13); + uint8_t value; + return readRegister(0x13, &value, 1); } -void PI4IOE5V6408_Class::disableIrq() +bool PI4IOE5V6408_Class::disableIrq() { - writeRegister8(0x11, 0B11111111); + return writeRegister8(0x11, 0B11111111); } -void PI4IOE5V6408_Class::enableIrq() +bool PI4IOE5V6408_Class::enableIrq() { - writeRegister8(0x11, 0x0); + return writeRegister8(0x11, 0x0); } } diff --git a/src/utility/PI4IOE5V6408_Class.hpp b/src/utility/PI4IOE5V6408_Class.hpp index b7cba8a..6948022 100644 --- a/src/utility/PI4IOE5V6408_Class.hpp +++ b/src/utility/PI4IOE5V6408_Class.hpp @@ -31,23 +31,23 @@ namespace m5 bool begin(); // false input, true output - void setDirection(uint8_t pin, bool direction) override; + bool setDirection(uint8_t pin, bool direction) override; bool setPullMode(uint8_t pin, gpio_pull_t mode) override; - void setHighImpedance(uint8_t pin, bool enable) override; + bool setHighImpedance(uint8_t pin, bool enable) override; bool getWriteValue(uint8_t pin) override; - void digitalWrite(uint8_t pin, bool level) override; + bool digitalWrite(uint8_t pin, bool level) override; bool digitalRead(uint8_t pin) override; - void resetIrq() override; + bool resetIrq() override; - void disableIrq() override; + bool disableIrq() override; - void enableIrq() override; + bool enableIrq() override; }; } From f5afb020675921aff272e3fdfa1d435db770881a Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Tue, 18 Aug 2026 05:18:51 +0000 Subject: [PATCH 15/26] Validate PI4IOE5V6408 pins and state the acknowledged-write contract --- src/utility/IOExpander_Base.hpp | 18 +++++++++++------- src/utility/M5IOE1_Class.hpp | 2 ++ src/utility/PI4IOE5V6408_Class.cpp | 3 +++ 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/utility/IOExpander_Base.hpp b/src/utility/IOExpander_Base.hpp index f43eba1..32f901a 100644 --- a/src/utility/IOExpander_Base.hpp +++ b/src/utility/IOExpander_Base.hpp @@ -18,28 +18,32 @@ namespace m5 , pull_down = 2 }; + /// Mutating methods report their result: the written value is not read + /// back, and read-modify-write accesses are not atomic against + /// concurrent access to the same register. + IOExpander_Base(std::uint8_t i2c_addr, std::uint32_t freq = 400000, m5::I2C_Class* i2c = &m5::In_I2C) : I2C_Device(i2c_addr, freq, i2c) {} IOExpander_Base(const IOExpander_Base&) = delete; // false input, true output - /// @return true only when the requested state was completely established; + /// @return true when every required register access was acknowledged; /// false for an invalid pin or I2C failure. virtual bool setDirection(uint8_t pin, bool direction) = 0; /// Set the GPIO pull resistor state. - /// @return true only when the requested state was completely established; + /// @return true when every required register access was acknowledged; /// false for an invalid pin, mode, or I2C failure. virtual bool setPullMode(uint8_t pin, gpio_pull_t mode) = 0; - /// @return true only when the requested state was completely established; + /// @return true when every required register access was acknowledged; /// false for an invalid pin or I2C failure. virtual bool setHighImpedance(uint8_t pin, bool enable) = 0; virtual bool getWriteValue(uint8_t pin) = 0; - /// @return true only when the requested state was completely established; + /// @return true when every required register access was acknowledged; /// false for an invalid pin or I2C failure. virtual bool digitalWrite(uint8_t pin, bool level) = 0; @@ -54,15 +58,15 @@ namespace m5 return false; } - /// @return true only when the requested state was completely established; + /// @return true when every required register access was acknowledged; /// false on I2C failure. virtual bool resetIrq() = 0; - /// @return true only when the requested state was completely established; + /// @return true when every required register access was acknowledged; /// false on I2C failure. virtual bool disableIrq() = 0; - /// @return true only when the requested state was completely established; + /// @return true when every required register access was acknowledged; /// false on I2C failure. virtual bool enableIrq() = 0; }; diff --git a/src/utility/M5IOE1_Class.hpp b/src/utility/M5IOE1_Class.hpp index 0fd5e4a..f5ca244 100644 --- a/src/utility/M5IOE1_Class.hpp +++ b/src/utility/M5IOE1_Class.hpp @@ -49,6 +49,8 @@ namespace m5 bool setPullMode(uint8_t pin, gpio_pull_t mode) override; + /// On the M5IOE1 this selects the drive mode (open-drain), which keeps + /// sinking the pin while the output latch is low; it is not a disconnect. bool setHighImpedance(uint8_t pin, bool enable) override; bool getWriteValue(uint8_t pin) override; diff --git a/src/utility/PI4IOE5V6408_Class.cpp b/src/utility/PI4IOE5V6408_Class.cpp index a1be83f..44cd71b 100644 --- a/src/utility/PI4IOE5V6408_Class.cpp +++ b/src/utility/PI4IOE5V6408_Class.cpp @@ -22,6 +22,7 @@ bool PI4IOE5V6408_Class::begin() // false input, true output bool PI4IOE5V6408_Class::setDirection(uint8_t pin, bool direction) { + if (pin >= 8) { return false; } if (direction) { return bitOn(0x03, 1 << pin); // Output, set 1 } else { @@ -49,6 +50,7 @@ bool PI4IOE5V6408_Class::setPullMode(uint8_t pin, gpio_pull_t mode) bool PI4IOE5V6408_Class::setHighImpedance(uint8_t pin, bool enable) { + if (pin >= 8) { return false; } if (enable) { return bitOn(0x07, 1 << pin); } else { @@ -64,6 +66,7 @@ bool PI4IOE5V6408_Class::getWriteValue(uint8_t pin) bool PI4IOE5V6408_Class::digitalWrite(uint8_t pin, bool level) { + if (pin >= 8) { return false; } if (level) { return bitOn(0x05, 1 << pin); } else { From 0624b6fb2c32d4c02b723dd60c71aa1eb50ea76f Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Tue, 18 Aug 2026 05:53:43 +0000 Subject: [PATCH 16/26] Fix PM1 button and wake IRQ handling and correct the power API docs --- src/utility/Power_Class.hpp | 3 ++- src/utility/power/M5PM1_Class.cpp | 45 +++++++++++++------------------ src/utility/power/M5PM1_Class.hpp | 28 +++++++++++++++---- 3 files changed, 43 insertions(+), 33 deletions(-) diff --git a/src/utility/Power_Class.hpp b/src/utility/Power_Class.hpp index 5cde802..294294c 100644 --- a/src/utility/Power_Class.hpp +++ b/src/utility/Power_Class.hpp @@ -203,7 +203,8 @@ namespace m5 /// Get Power Key Press condition. /// @return 0=none / 1=long pressed / 2=short clicked / 3=both - /// @attention Only for models with AXP192 or AXP2101 + /// @attention Only for models with AXP192, AXP2101, or M5PM1. + /// @attention M5PM1 reports only 0 or 2. /// @attention Once this function is called, the value is reset to 0, and the next time it is pressed on, the value changes. uint8_t getKeyState(void); diff --git a/src/utility/power/M5PM1_Class.cpp b/src/utility/power/M5PM1_Class.cpp index 23c3bbd..529aa8b 100644 --- a/src/utility/power/M5PM1_Class.cpp +++ b/src/utility/power/M5PM1_Class.cpp @@ -121,7 +121,8 @@ namespace m5 bool M5PM1_Class::setGPIOFunction(gpio_t pin, gpio_function_t function) { - if (!is_valid_gpio(pin)) { return false; } + if (!is_valid_gpio(pin) + || (function != gpio && function != irq && function != special)) { return false; } auto num = gpio_num(pin); auto reg = num < 4 ? M5PM1_REG_GPIO_FUNC0 : M5PM1_REG_GPIO_FUNC1; auto shift = static_cast((num < 4 ? num : num - 4) * 2); @@ -215,8 +216,7 @@ namespace m5 bool M5PM1_Class::clearWakeSource(std::uint8_t mask) { - auto src = readRegister8(M5PM1_REG_WAKE_SRC); - return writeRegister8(M5PM1_REG_WAKE_SRC, src & ~mask); + return writeRegister8(M5PM1_REG_WAKE_SRC, static_cast(~mask & 0x7F)); } bool M5PM1_Class::clearGPIOIRQStatus(void) @@ -264,7 +264,7 @@ namespace m5 bool M5PM1_Class::getBatteryCharge(bool* enabled) { - if (!_init) { return false; } + if (!_init || enabled == nullptr) { return false; } std::uint8_t cfg = 0; if (!readRegister(M5PM1_REG_PWR_CFG, &cfg, 1)) { return false; } *enabled = cfg & M5PM1_PWR_CFG_CHG_EN; @@ -274,25 +274,11 @@ namespace m5 bool M5PM1_Class::setChargeCurrent(std::uint16_t max_mA) { return false; - // if (!_init) return false; - // int value = max_mA / 8; // Convert mA to register value (8mA per step) - // if (value > 0) { value -= 1; // 0 = 8mA, 63 = 512mA - // if (value >= 64) value = 63; // max value is 512mA (8 + 63*8) - // } - // return writeRegister8(M5PM1_REG_CHR_CUR, value); } bool M5PM1_Class::setChargeVoltage(std::uint16_t max_mV) { return false; - // if (!_init) return false; - // int value = (max_mV - 3600) / 15; // Convert mV to register value (15mV per step) - // if (value > 0) { value -= 1; // 0 = 3600mV, 63 = 4545mV - // if (value >= 64) value = 63; // max value is 4545mV (3600 + 63*15) - // } - // uint8_t reg_value = readRegister8(M5PM1_REG_CHR_VOL); - // reg_value &= 0xC0; - // return writeRegister8(M5PM1_REG_CHR_VOL, reg_value | value); } std::uint16_t M5PM1_Class::getChargeCurrent(void) @@ -314,14 +300,19 @@ namespace m5 { if (!_init) return 0; uint8_t irq3 = 0; - if (readRegister(M5PM1_REG_IRQ_STATUS3, &irq3, 1)) - { - if (irq3 & ((1 << 0) | (1 << 2))) { - writeRegister8(M5PM1_REG_IRQ_STATUS3, 0); - return 2; - } - } - return 0; + if (!readRegister(M5PM1_REG_IRQ_STATUS3, &irq3, 1)) return 0; + uint8_t pending = irq3 & ((1 << 0) | (1 << 2)); + if (!pending) return 0; + if (!writeRegister8(M5PM1_REG_IRQ_STATUS3, static_cast(~pending & 0x07))) return 0; + if (pending & (1 << 2)) { _pek_double_pending = true; } + return 2; + } + + bool M5PM1_Class::wasPekDoubleClicked(void) + { + bool result = _pek_double_pending; + _pek_double_pending = false; + return result; } std::uint16_t M5PM1_Class::getVBUSVoltage(void) @@ -339,7 +330,7 @@ namespace m5 bool M5PM1_Class::getBatteryVoltage(std::uint16_t* millivolt) { - if (!_init) { return false; } + if (!_init || millivolt == nullptr) { return false; } std::uint8_t buf[2] = {}; if (!readRegister(M5PM1_REG_VBAT_L, buf, sizeof(buf))) { return false; } *millivolt = (buf[1] << 8) | buf[0]; diff --git a/src/utility/power/M5PM1_Class.hpp b/src/utility/power/M5PM1_Class.hpp index f07796d..446f5c8 100644 --- a/src/utility/power/M5PM1_Class.hpp +++ b/src/utility/power/M5PM1_Class.hpp @@ -39,7 +39,7 @@ namespace m5 enum gpio_function_t : std::uint8_t { gpio = 0b00 , irq = 0b01 - , wake = 0b10 + // 0b10 is reserved in the datasheet. , special = 0b11 }; @@ -81,7 +81,7 @@ namespace m5 /// @param enable true=enable / false=disable bool setLDOOutput(bool enable); - /// set PM1 5V DCDC output enable. + /// set PM1 3.3V DCDC rail output enable (PWR_CFG bit1 = 3.3V_DCDC_EN). /// @param enable true=enable / false=disable bool setDCDCOutput(bool enable); @@ -150,6 +150,9 @@ namespace m5 pwm_polarity_t polarity = pwm_polarity_t::normal, bool enable = true); /// clear PM1 wake source bits selected by mask. + /// Single-write selective clear assuming the write-zero-to-clear behavior + /// adopted by the official driver; the datasheet does not specify the write polarity. + /// Bits outside [6:0] are written as zero, matching the full-clear precedent. bool clearWakeSource(std::uint8_t mask = 0x7F); /// clear all PM1 GPIO IRQ status bits. @@ -183,28 +186,40 @@ namespace m5 bool getBatteryCharge(bool* enabled); /// set battery charge current - /// @param max_mA milli ampere. (8 - 512). + /// @param max_mA ignored; the PM1 has no charge current register. + /// @note The PM1 register map exposes no charge current register; this is a permanent stub returning false. bool setChargeCurrent(std::uint16_t max_mA); /// set battery charge voltage - /// @param max_mV milli volt. (3600 - 4545). + /// @param max_mV ignored; the PM1 has no charge voltage register. + /// @note The PM1 register map exposes no charge voltage register; this is a permanent stub returning false. bool setChargeVoltage(std::uint16_t max_mV); /// Get whether the battery is currently charging or not. + /// @note The PM1 register map exposes no charging status register; this is a permanent stub returning false. bool isCharging(void); // get setting value of battery charge current /// @return milli ampere. (8 - 512). 0=unknown + /// @note The PM1 register map exposes no charge current register; this is a permanent stub returning 0. std::uint16_t getChargeCurrent(void); // get setting value of battery charge voltage /// @return milli volt. (3600 - 4545). 0=unknown + /// @note The PM1 register map exposes no charge voltage register; this is a permanent stub returning 0. std::uint16_t getChargeVoltage(void); /// Get power key press condition. - /// @return 0=none / 2=short clicked + /// @return 0=none / 2=short clicked. For AXP compatibility, a double click + /// also reports 2; use wasPekDoubleClicked() to distinguish it. + /// Only the consumed click flags are cleared; the WAKEUP flag is preserved. + /// Returns 0 and leaves the event pending if the clear write fails. uint8_t getPekPress(void); + /// Returns whether the most recently reported click (getPekPress() == 2) + /// was a double click, then clears the flag. + bool wasPekDoubleClicked(void); + /// get VIN voltage. /// @return milli volt. 0=read failed std::uint16_t getVBUSVoltage(void); @@ -224,6 +239,9 @@ namespace m5 /// power off PM1. bool powerOff(void); + + private: + bool _pek_double_pending = false; }; } From 01c07525899a8f7137e001755f3920689de60e28 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Tue, 18 Aug 2026 06:31:41 +0000 Subject: [PATCH 17/26] Reset the double-click latch on every reported click --- src/utility/power/M5PM1_Class.cpp | 2 +- src/utility/power/M5PM1_Class.hpp | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/utility/power/M5PM1_Class.cpp b/src/utility/power/M5PM1_Class.cpp index 529aa8b..76dc211 100644 --- a/src/utility/power/M5PM1_Class.cpp +++ b/src/utility/power/M5PM1_Class.cpp @@ -304,7 +304,7 @@ namespace m5 uint8_t pending = irq3 & ((1 << 0) | (1 << 2)); if (!pending) return 0; if (!writeRegister8(M5PM1_REG_IRQ_STATUS3, static_cast(~pending & 0x07))) return 0; - if (pending & (1 << 2)) { _pek_double_pending = true; } + _pek_double_pending = (pending & (1 << 2)) != 0; return 2; } diff --git a/src/utility/power/M5PM1_Class.hpp b/src/utility/power/M5PM1_Class.hpp index 446f5c8..88138e4 100644 --- a/src/utility/power/M5PM1_Class.hpp +++ b/src/utility/power/M5PM1_Class.hpp @@ -200,12 +200,12 @@ namespace m5 bool isCharging(void); // get setting value of battery charge current - /// @return milli ampere. (8 - 512). 0=unknown + /// @return always 0. /// @note The PM1 register map exposes no charge current register; this is a permanent stub returning 0. std::uint16_t getChargeCurrent(void); // get setting value of battery charge voltage - /// @return milli volt. (3600 - 4545). 0=unknown + /// @return always 0. /// @note The PM1 register map exposes no charge voltage register; this is a permanent stub returning 0. std::uint16_t getChargeVoltage(void); @@ -218,6 +218,8 @@ namespace m5 /// Returns whether the most recently reported click (getPekPress() == 2) /// was a double click, then clears the flag. + /// Call from the task that polls getPekPress() (typically right after + /// M5.update()); the flag is not synchronized across tasks. bool wasPekDoubleClicked(void); /// get VIN voltage. From 7349ed8c1dee403773ea4d8e792ebfafe4f437d1 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Wed, 19 Aug 2026 12:40:44 +0000 Subject: [PATCH 18/26] Probe the address the board detection is looking for 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. --- src/M5Unified.cpp | 141 +++++++++++++++++++++++++++++----------------- src/M5Unified.hpp | 8 ++- 2 files changed, 95 insertions(+), 54 deletions(-) diff --git a/src/M5Unified.cpp b/src/M5Unified.cpp index c447497..2e218bf 100644 --- a/src/M5Unified.cpp +++ b/src/M5Unified.cpp @@ -1368,65 +1368,100 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { static constexpr gpio_num_t CoreInk_BUTTON_PWR_PIN = GPIO_NUM_27; #endif - bool M5Unified::_detect_i2c_device(uint8_t sda, uint8_t scl, uint8_t addr, const uint8_t* cmd_list) + bool M5Unified::_probe_i2c_addr(uint8_t sda, uint8_t scl, uint8_t addr) { - uint32_t result = 0; #if defined(M5UNIFIED_PC_BUILD) - return result; + return false; #else - m5gfx::gpio::pin_backup_t pin_backup[] = {scl, sda}; + /// アドレスの存在確認はソフトウェア I2C ポートで行う (オープンドレイン駆動で + /// ACK 競合が起きず、ハードウェアのペリフェラルにも触れない)。 + /// ポートは M5GFX の autodetect probe と同じ -1 を使う。負ポートは 2 本しかなく、 + /// probe は使い終わりに release するので逐次実行なら共有できる。2 本目を + /// ライブラリ側で占有せず、利用者のソフト I2C バス用に空けておく。 + static constexpr int_fast16_t probe_i2c_port = -1; + + m5gfx::gpio::pin_backup_t pin_backup[] = { scl, sda }; + + // デバイスの電源投入直後の安定待ち。バスは解放した状態で待つ + // (SCL を Low に駆動したまま待つと、バスタイムアウトを持つデバイスに対して不正)。 + m5gfx::pinMode(scl, m5gfx::pin_mode_t::input_pullup); + m5gfx::pinMode(sda, m5gfx::pin_mode_t::input_pullup); + m5gfx::delay(50); + + // 前回の通信の途中で止まっているデバイスに STOP を見せて論理状態を戻す。 + // START だけで戻らないデバイスが実在する (同ファイルの StampS3/Capsule 判別に + // 「STOP を出さないと正しく動作しないデバイスがあった (UnitHEART MAX30100)」の記録がある)。 + // 線を Low へ駆動するか解放するかの 2 状態しか使わない (High を駆動しない) ので、 + // デバイスが線を握っていてもパッド同士の衝突にはならない。 { - if(cmd_list == nullptr) + auto line_lo = [](uint8_t pin) + { m5gfx::gpio_lo(pin); m5gfx::pinMode(pin, m5gfx::pin_mode_t::output); }; + auto line_hi = [](uint8_t pin) + { m5gfx::pinMode(pin, m5gfx::pin_mode_t::input_pullup); }; + for (int i = 0; i < 8; ++i) { - uint8_t cmd_low[] = { - m5gfx::gpio::command_write_low, scl, - m5gfx::gpio::command_mode_output, scl, // SCL - m5gfx::gpio::command_write_low, sda, - m5gfx::gpio::command_mode_output, sda, // SDA - m5gfx::gpio::command_end, - }; - m5gfx::gpio::command(cmd_low); + line_lo(scl); m5gfx::delayMicroseconds(5); + line_lo(sda); m5gfx::delayMicroseconds(5); + line_hi(scl); m5gfx::delayMicroseconds(5); + line_hi(sda); m5gfx::delayMicroseconds(5); // SCL High 中の SDA Low->High = STOP } - else m5gfx::gpio::command(cmd_list); - - delay(50); // 延时 50ms,保证设备上电稳定 - - for (uint8_t i2caddr : (const uint8_t[]){static_cast(addr << 1)}) { //detect address - delay(2); // 小延时 - bool nack = true; - // I2C START - m5gfx::gpio_lo(sda); // SDA LOW = START - for (int cycle = 0; cycle < 20; ++cycle) { - // SCL toggle - m5gfx::gpio_hi(scl); - delay(1); - m5gfx::gpio_lo(scl); - delay(1); - - if (cycle & 1) { - if (cycle == 17) { - nack = m5gfx::gpio_in(sda); // 读 ACK - } - } else { - if (i2caddr & 0x80) { - m5gfx::gpio_hi(sda); - } else { - m5gfx::gpio_lo(sda); - } - i2caddr <<= 1; - if (cycle >= 16) { - m5gfx::pinMode(sda, (cycle == 16) ? m5gfx::pin_mode_t::input : m5gfx::pin_mode_t::output); - } - } - } - m5gfx::gpio_hi(sda); // SDA HIGH = STOP - result = result << 1 | nack; + } + + // 指定ピンが「外部プルアップの載った I2C バス」かどうかを先に確かめる。 + // ここは I2C ピンとは限らない場所を駆動する機種判別なので、判定を外すと + // 機種の読みが変わる。判定方式は M5GFX の autodetect probe と同一のものを使う + // (実機での実績がある形から動かさない。変えるなら該当機種すべてで再検証が要る)。 + // + // 4 回の read のうち、後半 2 回は入力プルダウンにしても High になること + // (= 外部プルアップが内部プルダウンに勝つこと) を確認するもの。 + // 弱いプルアップ (内部プルダウンと同程度の抵抗値) では通らないが、 + // 「強いプルアップがある = I2C バスである」を要求するのがこの判定の趣旨。 + const uint8_t cmd_bus_check_list[] = { + m5gfx::gpio::command_write_low , scl, + m5gfx::gpio::command_read , scl, // low チェック + m5gfx::gpio::command_write_low , sda, + m5gfx::gpio::command_read , sda, // low チェック + m5gfx::gpio::command_mode_input_pulldown, scl, + m5gfx::gpio::command_delay_usec , 10, + m5gfx::gpio::command_read , scl, // 外部プルアップがあるなら High + m5gfx::gpio::command_mode_input_pullup , scl, + m5gfx::gpio::command_mode_input_pulldown, sda, + m5gfx::gpio::command_delay_usec , 10, + m5gfx::gpio::command_read , sda, // 外部プルアップがあるなら High + m5gfx::gpio::command_mode_input_pullup , sda, + m5gfx::gpio::command_end + }; + m5gfx::pinMode(scl, m5gfx::pin_mode_t::output); + m5gfx::pinMode(sda, m5gfx::pin_mode_t::output); + if (m5gfx::gpio::command(cmd_bus_check_list) != 0x03) + { + for (auto& backup : pin_backup) { backup.restore(); } + return false; + } + + bool hit = false; + if (m5gfx::i2c::init(probe_i2c_port, sda, scl).has_value()) + { + // クロックが上がらないバス、前の通信の途中でデバイスがデータ線を握っている + // バスは、いずれも beginTransaction 側が検出して復旧または中断する。 + // NACK の場合も beginTransaction 自体は成功を返し (内部で STOP を出して + // エラーをラッチする)、そのエラーは endTransaction が報告する。 + // したがって両方の成功をもって「ACK が返った」と判定する。 + // 旧実装はプルアップの有無しか見ておらず、デバイスが応答するかは確かめて + // いなかった。厳密に ACK を要求するようになったぶん、起動が遅い個体を + // 取りこぼす余地ができるので、間を空けて一度だけ試し直す。 + for (int attempt = 0; attempt < 2 && !hit; ++attempt) + { + if (attempt) { m5gfx::delay(20); } + hit = m5gfx::i2c::beginTransaction(probe_i2c_port, addr, 100000, false).has_value() + && m5gfx::i2c::endTransaction(probe_i2c_port).has_value(); } + m5gfx::i2c::release(probe_i2c_port); } for (auto& backup : pin_backup) { backup.restore(); } - return result; + return hit; #endif } @@ -1656,7 +1691,7 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { if (board == board_t::board_unknown) { /// PowerHub ? - if (_detect_i2c_device(45, 48, 0x50)) { + if (_probe_i2c_addr(45, 48, 0x50)) { board = board_t::board_M5PowerHub; } } @@ -1665,16 +1700,16 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { case 1: // EFUSE_PKG_VERSION_ESP32S3PICO: // LGA56 if (board == board_t::board_unknown) { /// AtomS3RExt / AtomS3RCam have a BMI270 on the internal I2C bus. - if (_detect_i2c_device(45, 0, 0x68) - || _detect_i2c_device(45, 0, 0x69)) { + if (_probe_i2c_addr(45, 0, 0x68) + || _probe_i2c_addr(45, 0, 0x69)) { board = board_t::board_M5AtomS3RExt; } /// Stamp-S3Bat ? - else if (_detect_i2c_device(48, 47, 0x6E)) { + else if (_probe_i2c_addr(48, 47, 0x6E)) { board = board_t::board_M5StampS3Bat; } /// AtomEchoS3R ? - else if(_detect_i2c_device(45, 0, 0x18)) { + else if(_probe_i2c_addr(45, 0, 0x18)) { board = board_t::board_M5AtomVoiceS3R; } /// StampS3Mini has no other onboard device that can identify it. diff --git a/src/M5Unified.hpp b/src/M5Unified.hpp index 7e94124..8327b91 100644 --- a/src/M5Unified.hpp +++ b/src/M5Unified.hpp @@ -666,7 +666,13 @@ namespace m5 board_t _check_boardtype(board_t); void _setup_i2c(board_t); void _setup_led(board_t); - bool _detect_i2c_device(uint8_t sda, uint8_t scl, uint8_t addr, const uint8_t* cmd_list=nullptr); + /// 指定ピンのバス上に、指定した 7bit アドレスのデバイスが居るかを調べる。 + /// (M5GFX にも同名だった _probe_i2c_addr があるが、あちらは複数アドレスを + /// ビット列で返す別物。取り違えを避けるため名前を分けている) + /// @return true = ACK が返った (デバイスが存在する)。 + /// false は「ACK を確認できなかった」であり、不在のほかバスが + /// 成立していない場合・probe 用ポートの初期化に失敗した場合を含む。 + bool _probe_i2c_addr(uint8_t sda, uint8_t scl, uint8_t addr); static void _setup_pinmap(board_t); static bool _speaker_enabled_cb_core2(void* args, bool enabled); From 9c97ef098d40617704bf0eed01e05c6fa12c0857 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Wed, 19 Aug 2026 21:28:53 +0000 Subject: [PATCH 19/26] Take the internal bus first, and ask the sensor that is always there 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. --- src/M5Unified.cpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/M5Unified.cpp b/src/M5Unified.cpp index 2e218bf..6e4b64e 100644 --- a/src/M5Unified.cpp +++ b/src/M5Unified.cpp @@ -1699,19 +1699,27 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { case 1: // EFUSE_PKG_VERSION_ESP32S3PICO: // LGA56 if (board == board_t::board_unknown) { + /// 内部 I2C バス (SDA45/SCL0) に載るデバイスで先に決める。他ピンの probe を + /// 間に挟まないので、ここで確定する機種は 48/47 に一切触れずに済む + /// (AtomVoiceS3R では GPIO48 が I2S の DOUT)。 + /// /// AtomS3RExt / AtomS3RCam have a BMI270 on the internal I2C bus. + /// この 2 機種は基板が共通で、カメラ部がユーザーの扱えるブレッドボードに + /// なっているため、内部バスにユーザーのデバイスが載りうる。オンボードで + /// 必ず存在する BMI270 を先に見て、後から載ったアドレスに identity を + /// 奪われないようにする。 if (_probe_i2c_addr(45, 0, 0x68) || _probe_i2c_addr(45, 0, 0x69)) { board = board_t::board_M5AtomS3RExt; } - /// Stamp-S3Bat ? + /// AtomVoiceS3R ? + else if (_probe_i2c_addr(45, 0, 0x18)) { + board = board_t::board_M5AtomVoiceS3R; + } + /// Stamp-S3Bat ? (内部バスに何も居なかったときだけ別のピンを触る) else if (_probe_i2c_addr(48, 47, 0x6E)) { board = board_t::board_M5StampS3Bat; } - /// AtomEchoS3R ? - else if(_probe_i2c_addr(45, 0, 0x18)) { - board = board_t::board_M5AtomVoiceS3R; - } /// StampS3Mini has no other onboard device that can identify it. else { board = board_t::board_M5StampS3Mini; From 9709e5f394c86c492ae80ebdbf375a45c4306fb0 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Wed, 19 Aug 2026 21:29:05 +0000 Subject: [PATCH 20/26] Confirm the bus before driving it, and keep the probe slot apart 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. --- src/M5Unified.cpp | 57 +++++++++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/src/M5Unified.cpp b/src/M5Unified.cpp index 6e4b64e..53bc11f 100644 --- a/src/M5Unified.cpp +++ b/src/M5Unified.cpp @@ -1375,10 +1375,10 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { #else /// アドレスの存在確認はソフトウェア I2C ポートで行う (オープンドレイン駆動で /// ACK 競合が起きず、ハードウェアのペリフェラルにも触れない)。 - /// ポートは M5GFX の autodetect probe と同じ -1 を使う。負ポートは 2 本しかなく、 - /// probe は使い終わりに release するので逐次実行なら共有できる。2 本目を - /// ライブラリ側で占有せず、利用者のソフト I2C バス用に空けておく。 - static constexpr int_fast16_t probe_i2c_port = -1; + /// ポートは M5GFX の autodetect probe (-1) と分ける。ソフト I2C のスロットは + /// 所有権を持たず、init が既存の設定を黙って奪う作りなので、ライブラリ同士で + /// 同じスロットを共有しない。 + static constexpr int_fast16_t probe_i2c_port = -2; m5gfx::gpio::pin_backup_t pin_backup[] = { scl, sda }; @@ -1388,25 +1388,6 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { m5gfx::pinMode(sda, m5gfx::pin_mode_t::input_pullup); m5gfx::delay(50); - // 前回の通信の途中で止まっているデバイスに STOP を見せて論理状態を戻す。 - // START だけで戻らないデバイスが実在する (同ファイルの StampS3/Capsule 判別に - // 「STOP を出さないと正しく動作しないデバイスがあった (UnitHEART MAX30100)」の記録がある)。 - // 線を Low へ駆動するか解放するかの 2 状態しか使わない (High を駆動しない) ので、 - // デバイスが線を握っていてもパッド同士の衝突にはならない。 - { - auto line_lo = [](uint8_t pin) - { m5gfx::gpio_lo(pin); m5gfx::pinMode(pin, m5gfx::pin_mode_t::output); }; - auto line_hi = [](uint8_t pin) - { m5gfx::pinMode(pin, m5gfx::pin_mode_t::input_pullup); }; - for (int i = 0; i < 8; ++i) - { - line_lo(scl); m5gfx::delayMicroseconds(5); - line_lo(sda); m5gfx::delayMicroseconds(5); - line_hi(scl); m5gfx::delayMicroseconds(5); - line_hi(sda); m5gfx::delayMicroseconds(5); // SCL High 中の SDA Low->High = STOP - } - } - // 指定ピンが「外部プルアップの載った I2C バス」かどうかを先に確かめる。 // ここは I2C ピンとは限らない場所を駆動する機種判別なので、判定を外すと // 機種の読みが変わる。判定方式は M5GFX の autodetect probe と同一のものを使う @@ -1439,6 +1420,25 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { return false; } + // 前回の通信の途中で止まっているデバイスに STOP を見せて論理状態を戻す。 + // START だけで戻らないデバイスが実在する (同ファイルの StampS3/Capsule 判別に + // 「STOP を出さないと正しく動作しないデバイスがあった (UnitHEART MAX30100)」の記録がある)。 + // 線を Low へ駆動するか解放するかの 2 状態しか使わない (High を駆動しない) ので、 + // デバイスが線を握っていてもパッド同士の衝突にはならない。 + { + auto line_lo = [](uint8_t pin) + { m5gfx::gpio_lo(pin); m5gfx::pinMode(pin, m5gfx::pin_mode_t::output); }; + auto line_hi = [](uint8_t pin) + { m5gfx::pinMode(pin, m5gfx::pin_mode_t::input_pullup); }; + for (int i = 0; i < 8; ++i) + { + line_lo(scl); m5gfx::delayMicroseconds(5); + line_lo(sda); m5gfx::delayMicroseconds(5); + line_hi(scl); m5gfx::delayMicroseconds(5); + line_hi(sda); m5gfx::delayMicroseconds(5); // SCL High 中の SDA Low->High = STOP + } + } + bool hit = false; if (m5gfx::i2c::init(probe_i2c_port, sda, scl).has_value()) { @@ -1447,15 +1447,8 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { // NACK の場合も beginTransaction 自体は成功を返し (内部で STOP を出して // エラーをラッチする)、そのエラーは endTransaction が報告する。 // したがって両方の成功をもって「ACK が返った」と判定する。 - // 旧実装はプルアップの有無しか見ておらず、デバイスが応答するかは確かめて - // いなかった。厳密に ACK を要求するようになったぶん、起動が遅い個体を - // 取りこぼす余地ができるので、間を空けて一度だけ試し直す。 - for (int attempt = 0; attempt < 2 && !hit; ++attempt) - { - if (attempt) { m5gfx::delay(20); } - hit = m5gfx::i2c::beginTransaction(probe_i2c_port, addr, 100000, false).has_value() - && m5gfx::i2c::endTransaction(probe_i2c_port).has_value(); - } + hit = m5gfx::i2c::beginTransaction(probe_i2c_port, addr, 100000, false).has_value() + && m5gfx::i2c::endTransaction(probe_i2c_port).has_value(); m5gfx::i2c::release(probe_i2c_port); } for (auto& backup : pin_backup) { From 87e604abcd23c80764c31c9f8071a40ab4a94af2 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Wed, 19 Aug 2026 21:29:15 +0000 Subject: [PATCH 21/26] Require the M5GFX that has the software I2C the probe needs 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. --- library.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.properties b/library.properties index bdc5630..ef6547e 100644 --- a/library.properties +++ b/library.properties @@ -8,4 +8,4 @@ category=Display url=https://github.com/m5stack/M5Unified.git architectures=esp32 includes=M5Unified.h -depends=M5GFX +depends=M5GFX (>=0.2.27) From 0ce7b470de91d44e8989b5688fc8d0c2e10d9b28 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Wed, 19 Aug 2026 22:08:05 +0000 Subject: [PATCH 22/26] Free a data line that is still held before giving up on the bus 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. --- src/M5Unified.cpp | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/M5Unified.cpp b/src/M5Unified.cpp index 53bc11f..6c67003 100644 --- a/src/M5Unified.cpp +++ b/src/M5Unified.cpp @@ -1412,9 +1412,20 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { m5gfx::gpio::command_mode_input_pullup , sda, m5gfx::gpio::command_end }; - m5gfx::pinMode(scl, m5gfx::pin_mode_t::output); - m5gfx::pinMode(sda, m5gfx::pin_mode_t::output); - if (m5gfx::gpio::command(cmd_bus_check_list) != 0x03) + auto bus_check = [&](void) -> uint32_t + { + m5gfx::pinMode(scl, m5gfx::pin_mode_t::output); + m5gfx::pinMode(sda, m5gfx::pin_mode_t::output); + return m5gfx::gpio::command(cmd_bus_check_list); + }; + + // 0x02 は「SCL は外部プルアップで戻るのに SDA だけ Low のまま」。前回の通信の + // 途中で止まったデバイスがデータ線を握っている典型で (ソフトリセットでは + // デバイスの電源が切れないため実際に起きる)、プルアップの無いただの Low ピンとは + // このシグネチャで区別できる。この場合だけは STOP を見せて握りを解かせ、 + // もう一度確かめる。それ以外の不一致は I2C バスではないとみなして即座に帰る。 + uint32_t check = bus_check(); + if (check != 0x03 && check != 0x02) { for (auto& backup : pin_backup) { backup.restore(); } return false; @@ -1439,6 +1450,12 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { } } + if (check != 0x03 && bus_check() != 0x03) + { // 握りが解けなかった。ここから先は probe しても意味がない。 + for (auto& backup : pin_backup) { backup.restore(); } + return false; + } + bool hit = false; if (m5gfx::i2c::init(probe_i2c_port, sda, scl).has_value()) { From 110fe5190ecbc91f831b9c05a586bea4ca5f1347 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Wed, 19 Aug 2026 22:08:17 +0000 Subject: [PATCH 23/26] Wait for the devices to power up once, not once per address 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. --- src/M5Unified.cpp | 23 ++++++++++++++++++++--- src/M5Unified.hpp | 3 +++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/M5Unified.cpp b/src/M5Unified.cpp index 6c67003..36e275d 100644 --- a/src/M5Unified.cpp +++ b/src/M5Unified.cpp @@ -1368,6 +1368,22 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { static constexpr gpio_num_t CoreInk_BUTTON_PWR_PIN = GPIO_NUM_27; #endif + /// probe を始める前に一度だけ、デバイスの電源が安定するのを待つ。 + /// 待ちが要るのは「電源投入から間もない」ことであってアドレスごとの事情ではないので、 + /// 判定の入口で一度払う。旧実装は最初の probe が常に真を返して連鎖が止まっていたため + /// 結果的に 1 回しか待っていなかった。それを意図として書き直したもの。 + void M5Unified::_wait_i2c_device_power(void) + { +#if !defined(M5UNIFIED_PC_BUILD) + static bool waited = false; + if (!waited) + { + waited = true; + m5gfx::delay(50); + } +#endif + } + bool M5Unified::_probe_i2c_addr(uint8_t sda, uint8_t scl, uint8_t addr) { #if defined(M5UNIFIED_PC_BUILD) @@ -1380,13 +1396,14 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { /// 同じスロットを共有しない。 static constexpr int_fast16_t probe_i2c_port = -2; + _wait_i2c_device_power(); + m5gfx::gpio::pin_backup_t pin_backup[] = { scl, sda }; - // デバイスの電源投入直後の安定待ち。バスは解放した状態で待つ - // (SCL を Low に駆動したまま待つと、バスタイムアウトを持つデバイスに対して不正)。 + // ここでは待たない。電源安定待ちは判定の入口で一度だけ行う (_wait_i2c_device_power)。 + // アドレスごとに待つと、判定が空振りするたびに数十 ms が起動時間へ積み上がる。 m5gfx::pinMode(scl, m5gfx::pin_mode_t::input_pullup); m5gfx::pinMode(sda, m5gfx::pin_mode_t::input_pullup); - m5gfx::delay(50); // 指定ピンが「外部プルアップの載った I2C バス」かどうかを先に確かめる。 // ここは I2C ピンとは限らない場所を駆動する機種判別なので、判定を外すと diff --git a/src/M5Unified.hpp b/src/M5Unified.hpp index 8327b91..53ebda9 100644 --- a/src/M5Unified.hpp +++ b/src/M5Unified.hpp @@ -666,6 +666,9 @@ namespace m5 board_t _check_boardtype(board_t); void _setup_i2c(board_t); void _setup_led(board_t); + /// probe を始める前に一度だけ、デバイスの電源が安定するのを待つ。 + static void _wait_i2c_device_power(void); + /// 指定ピンのバス上に、指定した 7bit アドレスのデバイスが居るかを調べる。 /// (M5GFX にも同名だった _probe_i2c_addr があるが、あちらは複数アドレスを /// ビット列で返す別物。取り違えを避けるため名前を分けている) From 3dd1440149672df4ed5aebb8577a03e1c29e7695 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Wed, 19 Aug 2026 22:35:18 +0000 Subject: [PATCH 24/26] Park the lines low before the pins become outputs 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. --- src/M5Unified.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/M5Unified.cpp b/src/M5Unified.cpp index 36e275d..eda7f43 100644 --- a/src/M5Unified.cpp +++ b/src/M5Unified.cpp @@ -1431,8 +1431,11 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { }; auto bus_check = [&](void) -> uint32_t { - m5gfx::pinMode(scl, m5gfx::pin_mode_t::output); - m5gfx::pinMode(sda, m5gfx::pin_mode_t::output); + // ラッチを Low にしてから出力へ切り替える。直前は input_pullup (ラッチ High) + // なので、先に出力にすると一瞬 push-pull で High を駆動してしまう。 + // ここは相手が何か分からないピンなので、High は一度も駆動しない。 + m5gfx::gpio_lo(scl); m5gfx::pinMode(scl, m5gfx::pin_mode_t::output); + m5gfx::gpio_lo(sda); m5gfx::pinMode(sda, m5gfx::pin_mode_t::output); return m5gfx::gpio::command(cmd_bus_check_list); }; From ba089f6b86ac2b9f887e6054c35c50ec32cbf282 Mon Sep 17 00:00:00 2001 From: Tinyu Date: Thu, 20 Aug 2026 11:28:31 +0800 Subject: [PATCH 25/26] Add ESP32C5 RTC IRQ clear and unsupported-sleep log warnings --- src/utility/Power_Class.cpp | 6 +++--- src/utility/RTC_Class.cpp | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/utility/Power_Class.cpp b/src/utility/Power_Class.cpp index 13b6f67..d6dbce8 100644 --- a/src/utility/Power_Class.cpp +++ b/src/utility/Power_Class.cpp @@ -1559,7 +1559,7 @@ namespace m5 #else ESP_LOGD("Power","deepSleep"); #if defined (CONFIG_IDF_TARGET_ESP32C3) || defined (CONFIG_IDF_TARGET_ESP32C6) // || defined (CONFIG_IDF_TARGET_ESP32P4) - + ESP_LOGW("Power","deepSleep: deep sleep is not supported on this target."); #else #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) @@ -1690,8 +1690,8 @@ namespace m5 (void)touch_wakeup; #else ESP_LOGD("Power","lightSleep"); -#if defined (CONFIG_IDF_TARGET_ESP32C3) || defined (CONFIG_IDF_TARGET_ESP32C6) || defined (CONFIG_IDF_TARGET_ESP32C5) || defined (CONFIG_IDF_TARGET_ESP32H2) || defined (CONFIG_IDF_TARGET_ESP32P4) - +#if defined (CONFIG_IDF_TARGET_ESP32C3) || defined (CONFIG_IDF_TARGET_ESP32C6) || defined (CONFIG_IDF_TARGET_ESP32H2) || defined (CONFIG_IDF_TARGET_ESP32P4) + ESP_LOGW("Power","lightSleep: light sleep is not supported on this target."); #else #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) diff --git a/src/utility/RTC_Class.cpp b/src/utility/RTC_Class.cpp index e52694c..2f227b4 100644 --- a/src/utility/RTC_Class.cpp +++ b/src/utility/RTC_Class.cpp @@ -29,6 +29,16 @@ namespace m5 M5.Power.M5pm1.clearIRQStatus(); } } +#elif defined (CONFIG_IDF_TARGET_ESP32C5) + static void clear_m5pm1_rtc_irq(void) + { + if (M5.getBoard() == board_t::board_M5ToughC5 + && M5.Power.getType() == Power_Class::pmic_t::pmic_m5pm1) + { + M5.Power.M5pm1.clearWakeSource(); + M5.Power.M5pm1.clearIRQStatus(); + } + } #else static void clear_m5pm1_rtc_irq(void) {} #endif From 8531fc9a554d43ffb737e918fade8ed8b27fad83 Mon Sep 17 00:00:00 2001 From: Tinyu Date: Thu, 20 Aug 2026 14:40:07 +0800 Subject: [PATCH 26/26] Add ToughC5 charge current control --- src/utility/Power_Class.cpp | 15 +++++++++++++++ src/utility/Power_Class.hpp | 3 ++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/utility/Power_Class.cpp b/src/utility/Power_Class.cpp index d6dbce8..f63a3b2 100644 --- a/src/utility/Power_Class.cpp +++ b/src/utility/Power_Class.cpp @@ -2391,6 +2391,21 @@ namespace m5 M5pm1.setGPIOOutput(M5PM1_Class::gpio3, true); } break; +#elif defined (CONFIG_IDF_TARGET_ESP32C5) + case pmic_t::pmic_m5pm1: + if (M5.getBoard() == board_t::board_M5ToughC5) + { + // ToughC5 CHG_PROG is IOE1 G1: low selects 830 mA, high selects 180 mA. + // Set the latch before enabling push-pull output to avoid a transient + // selection of the opposite current during the mode transition. + auto& ioe1 = M5.getIOExpander(0); + const bool select_180mA = max_mA < 830; + ioe1.setPullMode(M5IOE1_Class::gpio1, IOExpander_Base::pull_none); + ioe1.digitalWrite(M5IOE1_Class::gpio1, select_180mA); + ioe1.setHighImpedance(M5IOE1_Class::gpio1, false); + ioe1.setDirection(M5IOE1_Class::gpio1, true); + } + return; #endif #endif diff --git a/src/utility/Power_Class.hpp b/src/utility/Power_Class.hpp index 294294c..b02dfd9 100644 --- a/src/utility/Power_Class.hpp +++ b/src/utility/Power_Class.hpp @@ -161,7 +161,8 @@ namespace m5 /// set battery charge current /// @param max_mA milli ampere. - /// @note CoreMatrix selects the nearest supported maximum: 180 mA below 650 mA, otherwise 650 mA. + /// @note CoreMatrix selects 180 mA below 650 mA, otherwise 650 mA. + /// @note ToughC5 selects 180 mA below 830 mA, otherwise 830 mA. /// @attention Non-functioning models : CoreInk , M5Paper , M5Stack(with non I2C IP5306) void setChargeCurrent(std::uint16_t max_mA);