From 5eb09b27374f1044d1cc381f687fed391fecd5dc Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:30:00 +0000 Subject: [PATCH 1/8] AXP2101: set the TS pin to external input on CoreS3 and Core2 v1.1 so it does not block the charger TS has no battery thermistor on these boards; it senses the 5V bus (CoreS3) or reads about 2.9V (Core2 v1.1). REG 0x50 bit 4 selects whether TS qualifies charging, and its power-on default comes from the EFUSE and differs between chips: on units where it is 0, the charger treats the TS voltage as a cold battery and never charges (#387). Setting the bit is a no-op on chips whose default is already 1. --- src/utility/Power_Class.inl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/utility/Power_Class.inl b/src/utility/Power_Class.inl index f201de0..c693a83 100644 --- a/src/utility/Power_Class.inl +++ b/src/utility/Power_Class.inl @@ -425,6 +425,10 @@ namespace m5 , 0x30, 0x0F // ADC enabled (for voltage measurement) }; Axp2101.writeRegister8Array(reg_data_array, sizeof(reg_data_array)); + // TS senses the 5V bus (no battery thermistor). Its power-on default comes from the + // EFUSE and differs between chips; when it is left as the temperature input, the + // charger treats the bus voltage as a cold battery and never charges. + Axp2101.bitOn(0x50, 0x10); // TS pin: external input, does not affect the charger // The touch INT is routed to the ESP32 as TOUCH_INT -> AW9523 P1_2 -> AW9523 INTN // -> I2C_INT -> GPIO21. The power key is wired to the AXP2101 PWRON only, and the // AXP2101 IRQ pin is shared with the RTC INT, so neither reaches the ESP32. @@ -863,6 +867,8 @@ namespace m5 // setExtOutput) makes the AXP2101 power itself off, and it stays off until the power // key; let the ESP32 brown out and restart instead. Over-voltage (bit 5) stays enabled. Axp2101.bitOff(0x23, 0x1F); // DCDC1-5 UVP power-off disable + // No battery thermistor on TS (reads about 2.9V); see the CoreS3 setup. + Axp2101.bitOn(0x50, 0x10); // TS pin: external input, does not affect the charger // for Core2 v1.1 (AXP2101+INA3221) if (Ina3221[0].begin()) From c039679296c1389ec818bd6280d0c8971f3dd52a Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:30:00 +0000 Subject: [PATCH 2/8] Speaker: keep the DMA fed at the hold level while waiting for new data (#391) --- src/utility/Speaker_Class.inl | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/utility/Speaker_Class.inl b/src/utility/Speaker_Class.inl index cc8e0ef..05982e4 100644 --- a/src/utility/Speaker_Class.inl +++ b/src/utility/Speaker_Class.inl @@ -574,8 +574,22 @@ namespace m5 if (buf_cnt) { // (no data... wait for new data) --buf_cnt; - uint32_t wait_msec = 1 + (self->_cfg.dma_buf_len / (spk_sample_rate_x256 >> 17)); - flg_nodata = (0 == ulTaskNotifyTake( pdFALSE, wait_msec )); + if (self->_cfg.buzzer) + { + uint32_t wait_msec = 1 + (self->_cfg.dma_buf_len / (spk_sample_rate_x256 >> 17)); + flg_nodata = (0 == ulTaskNotifyTake( pdFALSE, wait_msec )); + } + else + { // Keep the DMA fed at the hold level instead of sleeping. A timed wait can let the + // DMA reach the auto-cleared buffers, which output 0 (on the DAC, a drop to the zero + // level and a jump back to the bias). The blocking write paces this loop. + uint32_t hold = self->_cfg.use_dac ? (uint32_t)(dac_offset | dac_offset << 16) : 0u; + size_t words = dma_buf_len >> 1; + for (size_t i = 0; i < words; ++i) { sound_buf32[i] = hold; } + size_t write_bytes; + _spk_i2s_write(i2s_port, sound_buf32, words * sizeof(int32_t), &write_bytes, portMAX_DELAY); + flg_nodata = (0 == ulTaskNotifyTake( pdFALSE, 0 )); + } } if (flg_nodata && 0 == buf_cnt) From 80c22f80524cf52e5c3897a9b46a2207462b157b Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:30:00 +0000 Subject: [PATCH 3/8] NanoC6 / NanoH2: switch the RGB LED power on when the LEDs are first used (#389) --- src/M5Unified.inl | 10 ++++++++++ src/utility/led/LED_Strip_Class.hpp | 3 +++ src/utility/led/LED_Strip_Class.inl | 12 ++++++++++++ 3 files changed, 25 insertions(+) diff --git a/src/M5Unified.inl b/src/M5Unified.inl index 491f0d9..21fea0c 100644 --- a/src/M5Unified.inl +++ b/src/M5Unified.inl @@ -2250,6 +2250,7 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { #if !defined (M5UNIFIED_PC_BUILD) int led_count = 1; int byte_per_led = 3; + int pin_power = -1; switch (board) { #if defined (CONFIG_IDF_TARGET_ESP32S3) @@ -2273,6 +2274,14 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { case board_t::board_M5PaperColor: led_count = 2; break; +#elif defined (CONFIG_IDF_TARGET_ESP32C6) + case board_t::board_M5NanoC6: + pin_power = GPIO_NUM_19; // RGB LED power switch + break; +#elif defined (CONFIG_IDF_TARGET_ESP32H2) + case board_t::board_M5NanoH2: + pin_power = GPIO_NUM_10; // RGB LED power switch + break; #else case board_t::board_M5AtomMatrix: led_count = 25; @@ -2288,6 +2297,7 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { auto busled = std::make_shared(); auto buscfg = busled->getConfig(); buscfg.pin_data = pin_rgb_led; + buscfg.pin_power = pin_power; busled->setConfig(buscfg); auto led_strip = std::make_shared(); auto ledcfg = led_strip->getConfig(); diff --git a/src/utility/led/LED_Strip_Class.hpp b/src/utility/led/LED_Strip_Class.hpp index c17161d..98a164b 100644 --- a/src/utility/led/LED_Strip_Class.hpp +++ b/src/utility/led/LED_Strip_Class.hpp @@ -102,6 +102,9 @@ namespace m5 uint16_t t1l_ns = 300; uint16_t reset_us = 280; int8_t pin_data = -1; + /// GPIO that switches the LED power supply on (active high); -1 if the LEDs are always powered. + /// It is driven at init(), i.e. when the LEDs are first used, so an unused LED stays unpowered. + int8_t pin_power = -1; }; const config_t& config(void) const { return _config; } const config_t& getConfig(void) const { return _config; } diff --git a/src/utility/led/LED_Strip_Class.inl b/src/utility/led/LED_Strip_Class.inl index 871ddea..aadb10d 100644 --- a/src/utility/led/LED_Strip_Class.inl +++ b/src/utility/led/LED_Strip_Class.inl @@ -141,6 +141,14 @@ namespace m5 // The RMT driver does not clear a stale open-drain pad flag. (ESP-IDF v6 removed io_od_mode) m5gfx::gpio_lo(_config.pin_data); m5gfx::pinMode(_config.pin_data, m5gfx::pin_mode_t::output); + if (_config.pin_power >= 0) + { + m5gfx::gpio_hi(_config.pin_power); + m5gfx::pinMode(_config.pin_power, m5gfx::pin_mode_t::output); + // A frame sent right after switching the supply on is lost (measured on NanoC6 / NanoH2: + // 0 ms fails, 1 ms works); give the LEDs a few ms to come up. + m5gfx::delay(5); + } rmt_tx_channel_config_t rmt_tx; memset(&rmt_tx, 0, sizeof(rmt_tx)); @@ -174,6 +182,10 @@ namespace m5 } void LedBus_RMT::release(void) { + if (_config.pin_power >= 0) + { + m5gfx::gpio_lo(_config.pin_power); + } #if M5UNIFIED_RMT_VERSION == 2 auto rmt_ch_handle = _rmt_ch_handle; auto led_encoder = _led_encoder; From 1dfa689247aa1fa4744dfa793b218c538c7df14b Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:30:00 +0000 Subject: [PATCH 4/8] Capsule: release GPIO13/15 after the I2C STOP issued at board detection (#393) --- src/M5Unified.inl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/M5Unified.inl b/src/M5Unified.inl index 21fea0c..81e8673 100644 --- a/src/M5Unified.inl +++ b/src/M5Unified.inl @@ -1902,6 +1902,9 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { board = board_t::board_M5Capsule; // 自動検出の際。PortAに余分な波形が出ているので、一度 I2C STOPコンディションを出しておく。 // ※ これをしないと正しく動作しないデバイスが存在した。UnitHEART MAX30100 + // Release the Grove pins afterwards: left as push-pull outputs they keep driving High, + // and drivers that only enable the input (e.g. RMT RX on ESP-IDF 5.4+) cannot see the line. + m5gfx::gpio::pin_backup_t grove_backup[] = { GPIO_NUM_15, GPIO_NUM_13 }; m5gfx::gpio::command( (const uint8_t[]) { m5gfx::gpio::command_mode_output, GPIO_NUM_15, @@ -1913,6 +1916,9 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { m5gfx::gpio::command_end } ); + for (auto &backup : grove_backup) { + backup.restore(); + } } } for (auto &backup : pin_backup) { From 58c94221dce49115424efc649382f5ab9a426c00 Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:30:00 +0000 Subject: [PATCH 5/8] ToughC5: select 650 mA, not 830 mA, as the high charge current step The charger's CHG_PROG pin (IOE1 G1) switches between 180 mA and 650 mA, the same two steps as CoreMatrix. The code assumed 830 mA for the high step, so setChargeCurrent() with a request between 650 and 829 mA picked 180 mA, and the applied current was reported as 830 mA. --- src/utility/Power_Class.hpp | 2 +- src/utility/Power_Class.inl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utility/Power_Class.hpp b/src/utility/Power_Class.hpp index eb66396..c84fb51 100644 --- a/src/utility/Power_Class.hpp +++ b/src/utility/Power_Class.hpp @@ -180,7 +180,7 @@ namespace m5 /// set battery charge current /// @param max_mA milli ampere. /// @note CoreMatrix selects 180 mA below 650 mA, otherwise 650 mA. - /// @note ToughC5 selects 180 mA below 830 mA, otherwise 830 mA. + /// @note ToughC5 selects 180 mA below 650 mA, otherwise 650 mA. /// @attention Non-functioning models : CoreInk , M5Paper , M5Stack(with non I2C IP5306) void setChargeCurrent(std::uint16_t max_mA); diff --git a/src/utility/Power_Class.inl b/src/utility/Power_Class.inl index c693a83..9769a0f 100644 --- a/src/utility/Power_Class.inl +++ b/src/utility/Power_Class.inl @@ -2652,11 +2652,11 @@ namespace m5 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. + // ToughC5 CHG_PROG is IOE1 G1: low selects 650 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; + const bool select_180mA = max_mA < 650; ioe1.setPullMode(M5IOE1_Class::gpio1, IOExpander_Base::pull_none); ioe1.digitalWrite(M5IOE1_Class::gpio1, select_180mA); ioe1.setHighImpedance(M5IOE1_Class::gpio1, false); From 705ef5944e3053252d92884b8fc541061579642f Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:30:00 +0000 Subject: [PATCH 6/8] Power: set the AXP2101 charge voltage to 4.2V at init (CoreS3 family, Core2 v1.1) The power-on value of REG 0x64 (constant-voltage target) is not always the datasheet default (011b = 4.2V): a CoreS3 SE was reported with 0x64 = 0x00, which is reserved in the current datasheet and was listed as 4.6V in an early revision. The cells on these boards are 4.2V types, so the target is now set explicitly at init, before the TS pin setting that lets the charger start. Chips that already read 0x03 are unaffected. A failed write is logged as a warning. A second Power.begin() call sets 4.2V again (when the write succeeds), replacing a lower target set with setChargeVoltage() before it. --- src/utility/Power_Class.inl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/utility/Power_Class.inl b/src/utility/Power_Class.inl index 9769a0f..53e1991 100644 --- a/src/utility/Power_Class.inl +++ b/src/utility/Power_Class.inl @@ -428,6 +428,10 @@ namespace m5 // TS senses the 5V bus (no battery thermistor). Its power-on default comes from the // EFUSE and differs between chips; when it is left as the temperature input, the // charger treats the bus voltage as a cold battery and never charges. + // The charge voltage (0x64) is not always at the datasheet default either: a unit read 0x00, + // which is reserved (4.6V in an early datasheet revision). The cell is a 4.2V type. + // Set it before the TS change below lets the charger start. + Axp2101.setChargeVoltage(4200); Axp2101.bitOn(0x50, 0x10); // TS pin: external input, does not affect the charger // The touch INT is routed to the ESP32 as TOUCH_INT -> AW9523 P1_2 -> AW9523 INTN // -> I2C_INT -> GPIO21. The power key is wired to the AXP2101 PWRON only, and the @@ -867,6 +871,8 @@ namespace m5 // setExtOutput) makes the AXP2101 power itself off, and it stays off until the power // key; let the ESP32 brown out and restart instead. Over-voltage (bit 5) stays enabled. Axp2101.bitOff(0x23, 0x1F); // DCDC1-5 UVP power-off disable + // Charge voltage: the power-on value may be reserved; see the CoreS3 setup. + Axp2101.setChargeVoltage(4200); // No battery thermistor on TS (reads about 2.9V); see the CoreS3 setup. Axp2101.bitOn(0x50, 0x10); // TS pin: external input, does not affect the charger From a98dc51d2071d0454f16f5304cb5ff5146d0370b Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:30:00 +0000 Subject: [PATCH 7/8] Power: precharge the CoreS3 BUS output before enabling it On a weak USB supply (several hub levels deep, no battery) the CoreS3 family powered itself off right after begin(): connecting the empty BUS_OUT capacitance through BUS_OUT_EN in setExtOutput(true) pulled the AXP2101 DCDC under its threshold and the PMIC shut down (REG21 = 0x20). Plugging USB in never booted the board; the power key did, because the charge left on BUS_OUT by the failed attempt remained. When BUS_OUT_EN goes from 0 to 1, pulse it first with a growing on-time: each pulse is one 100 kHz register write plus i * 16 us, 1 ms apart, 8 times (about 10 ms). BUS_OUT has a constant discharge path, so short fixed pulses do not accumulate; once it passes about 1.7 V it is pulled up from VBUS even with BUS_OUT_EN = 0 and the final enable is a small step. Measured on a CoreS3 SE, the point is crossed at the 3rd pulse. A write that fails during the ramp is followed by rewriting and reading back BUS_OUT_EN = 0 (up to three times): confirmed off returns false (after a failed on-pulse) or continues the ramp, still on goes to the final enable, unreadable returns false. The final enable is read back and retried, because a precharged BUS_OUT left with BUS_OUT_EN = 0 is pulled up from VBUS and then keeps later setExtOutput(true) calls cancelled by the TS check. The ramp runs only on the 0 -> 1 transition, inside the existing AW9523 lock. --- src/utility/Power_Class.hpp | 4 ++- src/utility/Power_Class.inl | 58 +++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/utility/Power_Class.hpp b/src/utility/Power_Class.hpp index c84fb51..68af41b 100644 --- a/src/utility/Power_Class.hpp +++ b/src/utility/Power_Class.hpp @@ -94,7 +94,9 @@ namespace m5 /// @note On the CoreS3 family (CoreS3 / CoreS3 SE / StackChan), disabling an enabled output blocks for /// about 200 ms (the boost converter is stopped first and the bus is left to discharge before the /// switch-over), and enabling without a battery may block for up to 1 s while the protection check - /// waits for the TS reading to settle. The switch-over is serialized with setUsbOutput and the + /// waits for the TS reading to settle. Enabling also precharges the bus with short pulses first + /// (about 10 ms), so that the empty bus does not pull the PMIC under its DCDC under-voltage + /// threshold on a weak USB supply. The switch-over is serialized with setUsbOutput and the /// internal speaker enable, so those may wait for it as well. /// @note Core2 v1.1 (AXP2101; a Tough with the AXP2101 takes the same path): disabling /// the output while powered from USB with no battery makes the ESP32 brownout and diff --git a/src/utility/Power_Class.inl b/src/utility/Power_Class.inl index 53e1991..cdec4ac 100644 --- a/src/utility/Power_Class.inl +++ b/src/utility/Power_Class.inl @@ -920,6 +920,50 @@ namespace m5 ~_core_s3_lock_t(void) { if (locked) { xSemaphoreGive(mutex); } } }; + // Before BUS_OUT_EN goes 0 -> 1, precharge BUS_OUT with short pulses of growing on-time. + // Connecting the empty BUS_OUT at once on a weak USB supply pulls the AXP2101 DCDC under + // its threshold through the inrush, and the PMIC powers the board off. + // BUS_OUT has a constant discharge path, so short fixed pulses do not accumulate. Once it + // passes about 1.7 V it is pulled up from VBUS even with BUS_OUT_EN = 0, and the final + // enable is a small step. Each on-time is one register write (about 0.3 ms at 100 kHz) + // plus i * 16 us; M5GFX's board detection runs the same sequence at 100 kHz. + // After a failed on- or off-pulse write, OFF is rewritten and read back up to three times: + // confirmed off aborts a failed on-pulse or continues the ramp after a failed off-pulse, + // still on goes to the final enable, and an unknown state returns false. + static bool _core_s3_bus_precharge(uint8_t port0_off) + { + static constexpr const uint8_t port0_reg = 0x02; + enum class off_state_t { unknown, off, on }; + auto confirm_off = [&]() { + uint8_t cur = 0; + bool read_ok = false; + for (int retry = 0; retry < 3; ++retry) + { + M5.In_I2C.writeRegister8(aw9523_i2c_addr, port0_reg, port0_off, i2c_freq); + read_ok = M5.In_I2C.readRegister(aw9523_i2c_addr, port0_reg, &cur, 1, i2c_freq); + if (read_ok && !(cur & _core_s3_bus_en)) { return off_state_t::off; } + } + return read_ok ? off_state_t::on : off_state_t::unknown; + }; + for (int i = 0; i < 8; ++i) + { + if (!M5.In_I2C.writeRegister8(aw9523_i2c_addr, port0_reg, port0_off | _core_s3_bus_en, i2c_freq)) + { + if (confirm_off() == off_state_t::on) { break; } + return false; + } + m5gfx::delayMicroseconds(i * 16); + if (!M5.In_I2C.writeRegister8(aw9523_i2c_addr, port0_reg, port0_off, i2c_freq)) + { + const auto state = confirm_off(); + if (state == off_state_t::unknown) { return false; } + if (state == off_state_t::on) { break; } + } + m5gfx::delayMicroseconds(1000); + } + return true; + } + static void _core_s3_output_locked(uint8_t mask, bool enable) { static constexpr const uint8_t port0_reg = 0x02; @@ -986,6 +1030,20 @@ namespace m5 if (enable) { if (!M5.In_I2C.writeRegister8(aw9523_i2c_addr, port1_reg, buf[1], i2c_freq)) { return; } + if ((mask & _core_s3_bus_en) && !(orig[0] & _core_s3_bus_en)) + { + if (!_core_s3_bus_precharge(orig[0])) { return; } + // A precharged BUS_OUT left with BUS_OUT_EN = 0 is pulled up from VBUS, and every later + // enable is then cancelled by the TS check; read the final write back, bounded retries. + for (int retry = 0; retry < 3; ++retry) + { + uint8_t cur; + if (M5.In_I2C.writeRegister8(aw9523_i2c_addr, port0_reg, buf[0], i2c_freq) + && M5.In_I2C.readRegister(aw9523_i2c_addr, port0_reg, &cur, 1, i2c_freq) + && cur == buf[0]) { return; } + } + return; + } M5.In_I2C.writeRegister8(aw9523_i2c_addr, port0_reg, buf[0], i2c_freq); } else From 914ac1d2c2aab2e687dcb1ee5f9390431b846e73 Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:30:00 +0000 Subject: [PATCH 8/8] Bump version to 0.2.26 --- idf_component.yml | 2 +- library.json | 2 +- library.properties | 2 +- src/gitTagVersion.h | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/idf_component.yml b/idf_component.yml index a05d126..b40dc9b 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -2,7 +2,7 @@ description: Unified library for M5Stack series issues: https://github.com/m5stack/M5Unified/issues repository: https://github.com/m5stack/M5Unified.git url: https://github.com/m5stack/M5Unified.git -version: 0.2.25 +version: 0.2.26 dependencies: m5stack/m5gfx: version: ">=0.2.31" diff --git a/library.json b/library.json index 1d5fba4..2a3199a 100644 --- a/library.json +++ b/library.json @@ -16,7 +16,7 @@ "version": ">=0.2.31" } ], - "version": "0.2.25", + "version": "0.2.26", "frameworks": ["arduino", "espidf", "*"], "platforms": ["espressif32", "native"], "headers": "M5Unified.h" diff --git a/library.properties b/library.properties index 59a8a63..d19bb00 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=M5Unified -version=0.2.25 +version=0.2.26 author=M5Stack maintainer=M5Stack sentence=Unified library for M5Stack series diff --git a/src/gitTagVersion.h b/src/gitTagVersion.h index 4a82349..043aae5 100644 --- a/src/gitTagVersion.h +++ b/src/gitTagVersion.h @@ -1,4 +1,4 @@ #define M5UNIFIED_VERSION_MAJOR 0 #define M5UNIFIED_VERSION_MINOR 2 -#define M5UNIFIED_VERSION_PATCH 25 +#define M5UNIFIED_VERSION_PATCH 26 #define M5UNIFIED_VERSION F( M5UNIFIED_VERSION_MAJOR "." M5UNIFIED_VERSION_MINOR "." M5UNIFIED_VERSION_PATCH )