From 091780bb669fd06cb741472a6eed5366c6ba1337 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 10 Aug 2026 02:01:17 +0000 Subject: [PATCH 1/5] Add support for M5Stack CoreMatrix (ESP32-C61) - I2C pin table: the internal bus is SDA=G0 / SCL=G1. The Grove port shares the same bus through a level shifter, so Port A uses the same pins. - SD: SPI SCLK=G25 / MOSI=G27 / MISO=G26 / CS=G28. - M-Bus 30 pin table, taken from the V0.7 schematic and cross-checked against the factory firmware continuity test order. - BtnA/B/C: KEY1/2/3 are wired to PM1 GPIO0/1/2 (pressed = LOW), not to the ESP, so they are polled over I2C. The whole GPIO input register is read in one transaction (new M5PM1 getGPIOInputBits), and the button state is only updated when the read succeeds, so a bus error is not reported as a button press. BtnPWR maps to the PM1 power button. - PM1 GPIO3 is the IRQ output wired to ESP32 G2; configure it as a push-pull high output before switching it to the IRQ function, the same way as ToughC5. PM1 GPIO4 is the BMI270 INT1 input. - The M5IOE1 is exposed through getIOExpander(0). --- src/M5Unified.cpp | 53 ++++++++++++++++++++++++++++++- src/utility/Power_Class.cpp | 23 +++++++++++--- src/utility/power/M5PM1_Class.cpp | 6 ++++ src/utility/power/M5PM1_Class.hpp | 3 ++ 4 files changed, 80 insertions(+), 5 deletions(-) diff --git a/src/M5Unified.cpp b/src/M5Unified.cpp index f3b1308..ec861db 100644 --- a/src/M5Unified.cpp +++ b/src/M5Unified.cpp @@ -115,6 +115,8 @@ static constexpr const uint8_t _pin_table_i2c_ex_in[][5] = { { board_t::board_M5NanoC6 , 255 ,255 , GPIO_NUM_1 ,GPIO_NUM_2 }, { board_t::board_unknown , 255 ,255 , 255 ,255 }, #elif defined (CONFIG_IDF_TARGET_ESP32C61) +{ board_t::board_M5CoreMatrix , GPIO_NUM_1 ,GPIO_NUM_0 , GPIO_NUM_1 ,GPIO_NUM_0 }, // Grove shares the internal bus (level-shifted) +{ board_t::board_unknown , 255 ,255 , 255 ,255 }, #elif defined (CONFIG_IDF_TARGET_ESP32H2) { board_t::board_M5NanoH2 , 255 ,255 , GPIO_NUM_1 ,GPIO_NUM_2 }, { board_t::board_unknown , 255 ,255 , 255 ,255 }, @@ -204,6 +206,7 @@ static constexpr const uint8_t _pin_table_sd[][7] = { #elif defined (CONFIG_IDF_TARGET_ESP32C3) #elif defined (CONFIG_IDF_TARGET_ESP32C6) #elif defined (CONFIG_IDF_TARGET_ESP32C61) +{ board_t::board_M5CoreMatrix , GPIO_NUM_25, GPIO_NUM_27, GPIO_NUM_26, 255 , 255 , GPIO_NUM_28 }, #elif defined (CONFIG_IDF_TARGET_ESP32H2) #elif defined (CONFIG_IDF_TARGET_ESP32P4) { board_t::board_M5Tab5 , GPIO_NUM_43, GPIO_NUM_44, GPIO_NUM_39, GPIO_NUM_40, GPIO_NUM_41, GPIO_NUM_42 }, @@ -354,6 +357,23 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { #elif defined (CONFIG_IDF_TARGET_ESP32C3) #elif defined (CONFIG_IDF_TARGET_ESP32C6) #elif defined (CONFIG_IDF_TARGET_ESP32C61) +{ board_t::board_M5CoreMatrix, + 255 , GPIO_NUM_3 , + 255 , GPIO_NUM_4 , + 255 , 255 , + GPIO_NUM_27, GPIO_NUM_5 , + GPIO_NUM_26, GPIO_NUM_6 , + GPIO_NUM_25, 255 , + GPIO_NUM_10, GPIO_NUM_11, + GPIO_NUM_7 , GPIO_NUM_8 , + GPIO_NUM_0 , GPIO_NUM_1 , + GPIO_NUM_0 , GPIO_NUM_1 , + GPIO_NUM_23, GPIO_NUM_22, + GPIO_NUM_24, GPIO_NUM_9 , + 255 , GPIO_NUM_29, + 255 , 255 , + 255 , 255 , +}, #elif defined (CONFIG_IDF_TARGET_ESP32H2) #elif defined (CONFIG_IDF_TARGET_ESP32C5) #else @@ -1864,6 +1884,15 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { _io_expander[0].reset(ioexp); } break; +#elif defined (CONFIG_IDF_TARGET_ESP32C61) + case board_t::board_M5CoreMatrix: + { /// Controls the LED matrix / TF / Grove / buzzer power rails, + /// the charge current selector and the buzzer PWM. + auto ioexp = new M5IOE1_Class; + ioexp->begin(); + _io_expander[0].reset(ioexp); + } + break; #endif default: break; @@ -3273,6 +3302,28 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { break; } +#elif defined (CONFIG_IDF_TARGET_ESP32C61) + + switch (_board) + { + case board_t::board_M5CoreMatrix: + /// KEY1/2/3 are wired to PM1 GPIO0/1/2 (pressed = LOW), not to the ESP, + /// so they are read by I2C polling. Skip the update on an I2C failure so + /// a bus error is not reported as a button press. + { + uint8_t in; + if (Power.M5pm1.getGPIOInputBits(&in)) + { + use_rawstate_bits = 0b00111; + btn_rawstate_bits = (~in) & 0b00111; + } + } + break; + + default: + break; + } + #endif if (use_rawstate_bits) { @@ -3283,7 +3334,7 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { } } -#if defined (CONFIG_IDF_TARGET_ESP32) || defined (CONFIG_IDF_TARGET_ESP32S3) || defined (CONFIG_IDF_TARGET_ESP32C5) +#if defined (CONFIG_IDF_TARGET_ESP32) || defined (CONFIG_IDF_TARGET_ESP32S3) || defined (CONFIG_IDF_TARGET_ESP32C5) || defined (CONFIG_IDF_TARGET_ESP32C61) if (_use_pmic_button) { Button_Class::button_state_t state = Button_Class::button_state_t::state_nochange; diff --git a/src/utility/Power_Class.cpp b/src/utility/Power_Class.cpp index 98795a9..42be00a 100644 --- a/src/utility/Power_Class.cpp +++ b/src/utility/Power_Class.cpp @@ -206,14 +206,29 @@ namespace m5 case board_t::board_M5CoreMatrix: _pmic = pmic_t::pmic_m5pm1; + _wakeupPin = GPIO_NUM_2; + /// 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); M5pm1.setGPIOFunction(M5PM1_Class::gpio2, M5PM1_Class::gpio); - M5pm1.setGPIOFunction(M5PM1_Class::gpio3, M5PM1_Class::gpio); - M5pm1.setGPIOMode(M5PM1_Class::gpio1, M5PM1_Class::output); + M5pm1.setGPIOMode(M5PM1_Class::gpio0, M5PM1_Class::input); + M5pm1.setGPIOMode(M5PM1_Class::gpio1, M5PM1_Class::input); M5pm1.setGPIOMode(M5PM1_Class::gpio2, M5PM1_Class::input); + /// PM1 GPIO4 is the BMI270 INT1 input (motion wakeup) + M5pm1.setGPIOFunction(M5PM1_Class::gpio4, M5PM1_Class::gpio); + M5pm1.setGPIOMode(M5PM1_Class::gpio4, M5PM1_Class::input); + /// PM1 GPIO3 is the IRQ output wired to ESP32 G2. Without an IRQ pin + /// configured the PM1 auto-clears its IRQ status (0x40-0x42) and the + /// power button / wake events cannot be detected. + /// Configure it as a push-pull high output before switching to the IRQ + /// function, so the released line is actively driven high. M5pm1.setGPIOMode(M5PM1_Class::gpio3, M5PM1_Class::output); - M5pm1.setGPIODrive(M5PM1_Class::gpio1, M5PM1_Class::push_pull); M5pm1.setGPIODrive(M5PM1_Class::gpio3, M5PM1_Class::push_pull); + M5pm1.setGPIOPull(M5PM1_Class::gpio3, M5PM1_Class::pull_up); + M5pm1.setGPIOOutput(M5PM1_Class::gpio3, true); + M5pm1.setGPIOFunction(M5PM1_Class::gpio3, M5PM1_Class::irq); + /// make the PM1 IRQ output readable as the wakeup pin + m5gfx::pinMode(_wakeupPin, m5gfx::pin_mode_t::input_pullup); break; } @@ -247,7 +262,7 @@ namespace m5 /// RTC アラームが IRQ 出力 (= ESP32 G4 の Low) として伝わる。 M5pm1.setGPIOFunction(M5PM1_Class::gpio3, M5PM1_Class::gpio); M5pm1.setGPIOMode(M5PM1_Class::gpio3, M5PM1_Class::input); - /// PM1 の IRQ 出力を wakeup ピンとして読めるよう入力にしておく。 + /// make the PM1 IRQ output readable as the wakeup pin。 /// この線には外部プルアップが無く、IRQ 解放時に High へ戻す駆動も /// 期待できないため、内部プルアップを有効にする。 m5gfx::pinMode(_wakeupPin, m5gfx::pin_mode_t::input_pullup); diff --git a/src/utility/power/M5PM1_Class.cpp b/src/utility/power/M5PM1_Class.cpp index a6f643d..50e87d5 100644 --- a/src/utility/power/M5PM1_Class.cpp +++ b/src/utility/power/M5PM1_Class.cpp @@ -157,6 +157,12 @@ namespace m5 return readRegister8(M5PM1_REG_GPIO_IN) & (1 << gpio_num(pin)); } + bool M5PM1_Class::getGPIOInputBits(std::uint8_t* bits) + { + if (!_init || bits == nullptr) { return false; } + return readRegister(M5PM1_REG_GPIO_IN, bits, 1); + } + bool M5PM1_Class::getGPIOOutputLatch(gpio_t pin) { if (!_init || !is_valid_gpio(pin)) { return false; } diff --git a/src/utility/power/M5PM1_Class.hpp b/src/utility/power/M5PM1_Class.hpp index 64c1558..48e8cbd 100644 --- a/src/utility/power/M5PM1_Class.hpp +++ b/src/utility/power/M5PM1_Class.hpp @@ -103,6 +103,9 @@ namespace m5 /// get PM1 GPIO input level. bool getGPIOInput(gpio_t pin); + /// read all GPIO input levels at once. returns false on I2C failure. + bool getGPIOInputBits(std::uint8_t* bits); + /// get PM1 GPIO output latch level, not the physical input level. bool getGPIOOutputLatch(gpio_t pin); From 247c2728522458811bbefb10b884f3b0117791aa Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 10 Aug 2026 02:01:56 +0000 Subject: [PATCH 2/5] Add CoreMatrix power management - Battery voltage and level are read from the PM1. - Charge state: the AW32901 CHG_STAT output is wired to M5IOE1 G8 (low = charging). A new error-aware IOExpander_Base::getInputLevel (overridden by M5IOE1_Class with an I2C result) is used so a bus failure reports charge_unknown instead of "charging"; the base implementation returns false to mean "not supported". - The TF card power gate (M5IOE1 G1) is off at reset; enable it in Power.begin so the SD card is usable after M5.begin. - setExtOutput/getExtOutput drive the Grove port power gate (M5IOE1 G5, both the 3.3V rail and the 5V boost). Verified on hardware: battery readings, charge state, Grove power readback, and SD card mount after power-up. --- src/utility/IOExpander_Base.hpp | 9 +++++++ src/utility/M5IOE1_Class.cpp | 9 +++++++ src/utility/M5IOE1_Class.hpp | 1 + src/utility/Power_Class.cpp | 47 +++++++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+) diff --git a/src/utility/IOExpander_Base.hpp b/src/utility/IOExpander_Base.hpp index 93bade7..2580e4e 100644 --- a/src/utility/IOExpander_Base.hpp +++ b/src/utility/IOExpander_Base.hpp @@ -33,6 +33,15 @@ namespace m5 virtual bool digitalRead(uint8_t pin) = 0; + /// digitalRead with I2C error reporting. returns false on I2C failure + /// or when the expander driver does not implement error detection. + virtual bool getInputLevel(uint8_t pin, bool* level) + { + (void)pin; + (void)level; + return false; + } + virtual void resetIrq() = 0; virtual void disableIrq() = 0; diff --git a/src/utility/M5IOE1_Class.cpp b/src/utility/M5IOE1_Class.cpp index 4473ba5..7b0f08c 100644 --- a/src/utility/M5IOE1_Class.cpp +++ b/src/utility/M5IOE1_Class.cpp @@ -102,6 +102,15 @@ namespace m5 return (readRegister8(_regForPin(M5IOE1_REG_GPIO_IN_L, pin)) & _bitForPin(pin)) != 0; } + bool M5IOE1_Class::getInputLevel(uint8_t pin, bool* level) + { + if (!_isValidPin(pin) || level == nullptr) { return false; } + std::uint8_t v; + if (!readRegister(_regForPin(M5IOE1_REG_GPIO_IN_L, pin), &v, 1)) { return false; } + *level = (v & _bitForPin(pin)) != 0; + return true; + } + void M5IOE1_Class::setPwmFrequency(std::uint16_t frequency) { std::uint8_t data[2] = { static_cast(frequency & 0xFF), static_cast(frequency >> 8) }; diff --git a/src/utility/M5IOE1_Class.hpp b/src/utility/M5IOE1_Class.hpp index 3154fde..c6c7fc0 100644 --- a/src/utility/M5IOE1_Class.hpp +++ b/src/utility/M5IOE1_Class.hpp @@ -57,6 +57,7 @@ namespace m5 void digitalWrite(uint8_t pin, bool level) override; bool digitalRead(uint8_t pin) override; + bool getInputLevel(uint8_t pin, bool* level) override; void setPwmFrequency(std::uint16_t frequency); diff --git a/src/utility/Power_Class.cpp b/src/utility/Power_Class.cpp index 42be00a..233c033 100644 --- a/src/utility/Power_Class.cpp +++ b/src/utility/Power_Class.cpp @@ -229,6 +229,14 @@ namespace m5 M5pm1.setGPIOFunction(M5PM1_Class::gpio3, M5PM1_Class::irq); /// make the PM1 IRQ output readable as the wakeup pin m5gfx::pinMode(_wakeupPin, m5gfx::pin_mode_t::input_pullup); + /// charge detect input (IOE1 G8 = AW32901 CHG_STAT, low = charging) + M5.getIOExpander(0).setDirection(M5IOE1_Class::gpio8, false); + { /// TF card power (IOE1 G1) is off at reset; enable it so the SD card is usable + auto& ioe1 = M5.getIOExpander(0); + ioe1.setHighImpedance(M5IOE1_Class::gpio1, false); + ioe1.setDirection(M5IOE1_Class::gpio1, true); + ioe1.digitalWrite(M5IOE1_Class::gpio1, true); + } break; } @@ -809,6 +817,16 @@ namespace m5 } break; +#elif defined (CONFIG_IDF_TARGET_ESP32C61) + case board_t::board_M5CoreMatrix: + { /// IOE1 G5 gates the Grove port power (both the 3.3V rail and the 5V boost) + auto& ioe1 = M5.getIOExpander(0); + ioe1.setHighImpedance(M5IOE1_Class::gpio5, false); + ioe1.setDirection(M5IOE1_Class::gpio5, true); + ioe1.digitalWrite(M5IOE1_Class::gpio5, enable); + } + break; + #elif defined (CONFIG_IDF_TARGET_ESP32H2) #elif defined (CONFIG_IDF_TARGET_ESP32S3) @@ -987,6 +1005,11 @@ namespace m5 return M5pm1.getExtOutput(); break; +#elif defined (CONFIG_IDF_TARGET_ESP32C61) + case board_t::board_M5CoreMatrix: + return M5.getIOExpander(0).getWriteValue(M5IOE1_Class::gpio5); + break; + #elif !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) case board_t::board_M5Paper: return m5gfx::gpio_in(M5Paper_EXT5V_ENABLE_PIN); @@ -1758,6 +1781,8 @@ namespace m5 case pmic_t::pmic_aw32001: return Bq27220.getVoltage_mV(); #elif defined (CONFIG_IDF_TARGET_ESP32C61) + case pmic_t::pmic_m5pm1: + return M5pm1.getBatteryVoltage(); #elif defined (CONFIG_IDF_TARGET_ESP32P4) #else #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) @@ -1821,6 +1846,16 @@ namespace m5 } break; #elif defined (CONFIG_IDF_TARGET_ESP32C61) + case pmic_t::pmic_m5pm1: + { + // Get battery voltage in mV + int16_t bat_mv = getBatteryVoltage(); + if (bat_mv <= 0) { + return -1; // Error reading voltage + } + mv = bat_mv; + } + break; #elif defined (CONFIG_IDF_TARGET_ESP32P4) #else #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) @@ -2141,6 +2176,18 @@ namespace m5 return Aw32001.isCharging() ? is_charging_t::is_charging : is_charging_t::is_discharging; #elif defined (CONFIG_IDF_TARGET_ESP32C61) + case pmic_t::pmic_m5pm1: + /// CoreMatrix: the AW32901 CHG_STAT is wired to IOE1 G8 (low = charging) + if (M5.getBoard() == board_t::board_M5CoreMatrix) + { + bool level; + if (!M5.getIOExpander(0).getInputLevel(M5IOE1_Class::gpio8, &level)) + { /// do not report an I2C failure as "charging" + return is_charging_t::charge_unknown; + } + return level ? is_charging_t::is_discharging : is_charging_t::is_charging; + } + return is_charging_t::charge_unknown; #elif defined (CONFIG_IDF_TARGET_ESP32P4) #else #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) From fd925c47e925f028befa6ccbbfc1e5cd584f5106 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 10 Aug 2026 02:02:30 +0000 Subject: [PATCH 3/5] Align the CoreMatrix IMU axes with the device orientation The BMI270 is mounted differently on CoreMatrix; remap the accelerometer and gyro axes as X=-Y, Y=-X, Z=-Z (the same form of correction as ChainCaptain). Determined with an interactive tilt calibration on hardware. --- src/utility/IMU_Class.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/utility/IMU_Class.cpp b/src/utility/IMU_Class.cpp index 8ea9b75..4e402a4 100644 --- a/src/utility/IMU_Class.cpp +++ b/src/utility/IMU_Class.cpp @@ -108,6 +108,12 @@ namespace m5 _internal_axisorder_fixed[sensor_index_accel] = (internal_axisorder_t)(axis_order_yxz | axis_invert_x | axis_invert_y | axis_invert_z); _internal_axisorder_fixed[sensor_index_gyro ] = (internal_axisorder_t)(axis_order_yxz | axis_invert_x | axis_invert_y | axis_invert_z); } +#elif defined(CONFIG_IDF_TARGET_ESP32C61) + if (board == m5::board_t::board_M5CoreMatrix) + { // CoreMatrix BMI270 : X=+Y, Y=-X, Z=+Z (90-degree rotation about Z) + _internal_axisorder_fixed[sensor_index_accel] = (internal_axisorder_t)(axis_order_yxz | axis_invert_y); + _internal_axisorder_fixed[sensor_index_gyro ] = (internal_axisorder_t)(axis_order_yxz | axis_invert_y); + } #endif } From a2b4645e697b3b38cf20e867842884aa25611628 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 10 Aug 2026 02:02:30 +0000 Subject: [PATCH 4/5] Wake CoreMatrix from deep sleep through the PM1 IRQ output KEY presses and IMU wake events are funneled into the PM1, whose IRQ output (wired to ESP32 G2) is the only wakeup pin; arm it as an EXT1 ANY_LOW source. The IRQ line has no external pull-up, so keep the RTC-domain pull-up enabled while sleeping, the same way as ToughC5. On wakeup, clear WAKE_SRC before the IRQ status registers: while WAKE_SRC is set, the WAKEUP bit of IRQ status 3 keeps getting re-asserted and the IRQ output never releases. On the ESP32-C61, the wakeup pin is still owned by the RTC IO mux after an EXT1 wakeup and the digital GPIO input reads low forever, which made the release wait on the next sleep entry spin without ever sleeping. Call rtc_gpio_deinit in Power.begin to return the pad to the digital function. (Verified on a ToughC5 that the C5 does not exhibit this, so this is handled in the CoreMatrix path.) Verified on hardware: repeated timer wake and EXT1 wake cycles with re-entry into deep sleep. --- src/M5Unified.cpp | 17 +++++++++++++++++ src/utility/Power_Class.cpp | 11 +++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/M5Unified.cpp b/src/M5Unified.cpp index ec861db..cbc3d01 100644 --- a/src/M5Unified.cpp +++ b/src/M5Unified.cpp @@ -2943,6 +2943,23 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { } break; + default: + break; + } +#elif defined (CONFIG_IDF_TARGET_ESP32C61) + switch (getBoard()) + { + case board_t::board_M5CoreMatrix: + { // KEY and IMU wake events are funneled into the PM1, whose IRQ output + // (GPIO2) is the only wakeup pin. The IRQ output stays low until every + // IRQ status bit is cleared, so clear them here to release the pin. + // Clear WAKE_SRC first: while it is set, the WAKEUP bit of IRQ status 3 + // keeps getting re-asserted. + Power.M5pm1.clearWakeSource(); + Power.M5pm1.clearIRQStatus(); + } + break; + default: break; } diff --git a/src/utility/Power_Class.cpp b/src/utility/Power_Class.cpp index 233c033..3776706 100644 --- a/src/utility/Power_Class.cpp +++ b/src/utility/Power_Class.cpp @@ -227,6 +227,12 @@ namespace m5 M5pm1.setGPIOPull(M5PM1_Class::gpio3, M5PM1_Class::pull_up); M5pm1.setGPIOOutput(M5PM1_Class::gpio3, true); M5pm1.setGPIOFunction(M5PM1_Class::gpio3, M5PM1_Class::irq); +#if SOC_RTCIO_INPUT_OUTPUT_SUPPORTED + /// After an EXT1 wakeup the pin is still owned by the RTC IO mux and the + /// digital GPIO input reads low forever (the release wait on the next + /// sleep entry would never finish). Return it to the digital function. + rtc_gpio_deinit((gpio_num_t)_wakeupPin); +#endif /// make the PM1 IRQ output readable as the wakeup pin m5gfx::pinMode(_wakeupPin, m5gfx::pin_mode_t::input_pullup); /// charge detect input (IOE1 G8 = AW32901 CHG_STAT, low = charging) @@ -1460,8 +1466,9 @@ namespace m5 #if SOC_RTCIO_INPUT_OUTPUT_SUPPORTED if (pin_wakeup_enabled) { -#if defined (CONFIG_IDF_TARGET_ESP32C5) - if (M5.getBoard() == board_t::board_M5ToughC5) +#if defined (CONFIG_IDF_TARGET_ESP32C5) || defined (CONFIG_IDF_TARGET_ESP32C61) + if (M5.getBoard() == board_t::board_M5ToughC5 + || M5.getBoard() == board_t::board_M5CoreMatrix) { // PM1 の IRQ 出力線には外部プルアップが無く、プルダウンすると // Low に固定されて wakeup ピンが解放されなくなる。内部プルアップで // High を維持し、IRQ アサート (Low) だけを wakeup 条件にする。 From a3240f6d5d422dd0b8f1a866c09cfce411f1f9f7 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 10 Aug 2026 03:02:35 +0000 Subject: [PATCH 5/5] Detect battery presence on CoreMatrix With no battery attached, the PM1 VBAT ADC reads the AW32901 charger float voltage (about 4.2V), so getBatteryVoltage() reported a fully charged battery on battery-less units. Distinguish the two by briefly pausing the charger on the first battery API call: without a battery VBAT collapses well below 2V within one PM1 ADC update cycle (about 1 second), while a real battery holds its voltage. The result is cached, so only the first call blocks (~1.2s). With no battery attached: - getBatteryVoltage() returns 0 - getBatteryLevel() returns -1 - isCharging() returns is_discharging (without a battery the charger retries periodically and CHG_STAT blips low, which would otherwise be reported as charging) --- src/utility/Power_Class.cpp | 43 +++++++++++++++++++++++++++++++ src/utility/Power_Class.hpp | 8 ++++++ src/utility/power/M5PM1_Class.cpp | 21 +++++++++++++-- src/utility/power/M5PM1_Class.hpp | 10 +++++++ 4 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/utility/Power_Class.cpp b/src/utility/Power_Class.cpp index 3776706..39d4be6 100644 --- a/src/utility/Power_Class.cpp +++ b/src/utility/Power_Class.cpp @@ -1777,6 +1777,40 @@ namespace m5 return -1; } +#if defined (CONFIG_IDF_TARGET_ESP32C61) + /// CoreMatrix: with no battery attached, the PM1 VBAT ADC reads the + /// AW32901 charger float voltage (~4.2V), which is indistinguishable from + /// a fully charged battery. Distinguish them by briefly pausing the + /// charger: without a battery VBAT collapses well below 2V, while a real + /// battery holds its voltage. The PM1 refreshes the VBAT register on an + /// internal ~1 second ADC cycle, so the pause must cover one full cycle. + /// Only the first call blocks (~1.2s); the result is cached. + /// An I2C failure during the probe is not cached, so a later call retries + /// and the battery APIs can report the bus error instead of a wrong state. + std::int8_t Power_Class::_batteryPresent(void) + { + if (_batt_present < 0) + { + bool chg_enabled = true; + std::uint16_t pre_mv = 0, post_mv = 0; + bool ok = M5pm1.getBatteryCharge(&chg_enabled) + && M5pm1.getBatteryVoltage(&pre_mv) + && M5pm1.setBatteryCharge(false); + if (ok) + { /// wait only when the charger pause actually took effect + m5gfx::delay(1200); + ok = M5pm1.getBatteryVoltage(&post_mv); + } + M5pm1.setBatteryCharge(chg_enabled); + if (ok) + { + _batt_present = (post_mv > 2000) && ((std::int32_t)pre_mv - (std::int32_t)post_mv < 500); + } + } + return _batt_present; + } +#endif + int16_t Power_Class::getBatteryVoltage(void) { #if !defined (M5UNIFIED_PC_BUILD) @@ -1789,6 +1823,8 @@ namespace m5 return Bq27220.getVoltage_mV(); #elif defined (CONFIG_IDF_TARGET_ESP32C61) case pmic_t::pmic_m5pm1: + /// 0 = no battery attached or read failure (see _batteryPresent) + if (_batteryPresent() != 1) { return 0; } return M5pm1.getBatteryVoltage(); #elif defined (CONFIG_IDF_TARGET_ESP32P4) #else @@ -2187,6 +2223,13 @@ namespace m5 /// CoreMatrix: the AW32901 CHG_STAT is wired to IOE1 G8 (low = charging) if (M5.getBoard() == board_t::board_M5CoreMatrix) { + /// With no battery the charger retries periodically and CHG_STAT + /// blips low for a moment; report "not charging" instead. + { + std::int8_t present = _batteryPresent(); + if (present < 0) { return is_charging_t::charge_unknown; } + if (present == 0) { return is_charging_t::is_discharging; } + } bool level; if (!M5.getIOExpander(0).getInputLevel(M5IOE1_Class::gpio8, &level)) { /// do not report an I2C failure as "charging" diff --git a/src/utility/Power_Class.hpp b/src/utility/Power_Class.hpp index 8377e69..cff4fe1 100644 --- a/src/utility/Power_Class.hpp +++ b/src/utility/Power_Class.hpp @@ -251,6 +251,14 @@ namespace m5 void _powerOff(bool withTimer); void _timerSleep(void); +#if defined (CONFIG_IDF_TARGET_ESP32C61) + /// Check whether a battery is actually attached. + /// @return 1=present / 0=absent / -1=unknown (I2C failure, not cached) + std::int8_t _batteryPresent(void); + /// Cached result of the battery presence probe. -1 = not yet probed. + std::int8_t _batt_present = -1; +#endif + /// Release the wakeup pin so that it can be asserted again while sleeping. /// @return true if the pin is released ( high ). bool _releaseWakeupPin(std::uint_fast8_t wakeup_pin); diff --git a/src/utility/power/M5PM1_Class.cpp b/src/utility/power/M5PM1_Class.cpp index 50e87d5..c0dd328 100644 --- a/src/utility/power/M5PM1_Class.cpp +++ b/src/utility/power/M5PM1_Class.cpp @@ -218,6 +218,15 @@ namespace m5 : bitOff(M5PM1_REG_PWR_CFG, M5PM1_PWR_CFG_CHG_EN); } + bool M5PM1_Class::getBatteryCharge(bool* enabled) + { + if (!_init) { return false; } + std::uint8_t cfg = 0; + if (!readRegister(M5PM1_REG_PWR_CFG, &cfg, 1)) { return false; } + *enabled = cfg & M5PM1_PWR_CFG_CHG_EN; + return true; + } + bool M5PM1_Class::setChargeCurrent(std::uint16_t max_mA) { return false; @@ -280,9 +289,17 @@ namespace m5 std::uint16_t M5PM1_Class::getBatteryVoltage(void) { - if (!_init) { return 0; } + std::uint16_t mv = 0; + return getBatteryVoltage(&mv) ? mv : 0; + } + + bool M5PM1_Class::getBatteryVoltage(std::uint16_t* millivolt) + { + if (!_init) { return false; } std::uint8_t buf[2] = {}; - return readRegister(M5PM1_REG_VBAT_L, buf, sizeof(buf)) ? (buf[1] << 8) | buf[0] : 0; + if (!readRegister(M5PM1_REG_VBAT_L, buf, sizeof(buf))) { return false; } + *millivolt = (buf[1] << 8) | buf[0]; + return true; } std::uint16_t M5PM1_Class::get5VoutVoltage(void) diff --git a/src/utility/power/M5PM1_Class.hpp b/src/utility/power/M5PM1_Class.hpp index 48e8cbd..977e5ba 100644 --- a/src/utility/power/M5PM1_Class.hpp +++ b/src/utility/power/M5PM1_Class.hpp @@ -137,6 +137,11 @@ namespace m5 /// @param enable true=enable / false=disable bool setBatteryCharge(bool enable); + /// get battery charge enable state with I2C error reporting. + /// @param enabled output parameter, receives the charge enable state. + /// @return false on I2C failure. + bool getBatteryCharge(bool* enabled); + /// set battery charge current /// @param max_mA milli ampere. (8 - 512). bool setChargeCurrent(std::uint16_t max_mA); @@ -168,6 +173,11 @@ namespace m5 /// @return milli volt. 0=read failed std::uint16_t getBatteryVoltage(void); + /// get battery voltage with I2C error reporting. + /// @param millivolt output parameter, receives the battery voltage [mV]. + /// @return false on I2C failure. + bool getBatteryVoltage(std::uint16_t* millivolt); + /// get 5V output voltage. /// @return milli volt. 0=read failed std::uint16_t get5VoutVoltage(void);