From 827a734415601ffb70ba570118d0bca053fbb9b9 Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Fri, 2 Oct 2026 00:23:15 +0000 Subject: [PATCH 1/6] Keep the display bus on GPIO46 when Display.init() ran before begin() begin() drove GPIO46 high as a power hold before the board was known. When the application had already called Display.init(), the StopWatch QSPI io2 on GPIO46 was overwritten and never re-routed, so every pixel had a fixed bit and black showed as a bright colour. Skip the early hold in that case and apply it after the pin map is known, only on boards whose power hold is GPIO46 (Dial, Capsule, AirQ, DinMeter). (cherry picked from commit 3d670223a32b1205ad3e7a1fbef59a2ef1353a34) --- src/M5Unified.hpp | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/M5Unified.hpp b/src/M5Unified.hpp index 60831cab..325aa699 100644 --- a/src/M5Unified.hpp +++ b/src/M5Unified.hpp @@ -356,9 +356,16 @@ namespace m5 // Boards where GPIO46 is exposed to the application (camera VSYNC on CoreS3, camera data on // AtomS3R Cam, header pin on StampS3 / AtomS3R Ext) get the pad restored once the board is // known, see below. + // Skipped when the application already ran Display.init(): GPIO46 may then carry the display + // bus (StopWatch QSPI io2), which this output would overwrite. The hold is applied below, + // once the pin map is known, only on boards that use GPIO46 as their power hold. + const bool gpio46_hold = (Display.getBoard() == m5gfx::board_t::board_unknown); m5gfx::gpio::pin_backup_t gpio46_backup(GPIO_NUM_46); - m5gfx::gpio_hi(GPIO_NUM_46); - m5gfx::pinMode(GPIO_NUM_46, m5gfx::pin_mode_t::output); + if (gpio46_hold) + { + m5gfx::gpio_hi(GPIO_NUM_46); + m5gfx::pinMode(GPIO_NUM_46, m5gfx::pin_mode_t::output); + } #endif auto brightness = Display.getBrightness(); @@ -376,10 +383,15 @@ namespace m5 _board = board; _setup_pinmap(board); #if defined ( CONFIG_IDF_TARGET_ESP32S3 ) + if (!gpio46_hold && getPin(pin_name_t::power_hold) == GPIO_NUM_46) + { + m5gfx::gpio_hi(GPIO_NUM_46); + m5gfx::pinMode(GPIO_NUM_46, m5gfx::pin_mode_t::output); + } // Restore only on boards positively identified as exposing GPIO46 to the application // (camera VSYNC / data, header pin); every other case (power hold, display bus pins // configured by Display.init(), a fallback board) keeps the previous behaviour. - switch (board_detected ? board : board_t::board_unknown) + switch (board_detected && gpio46_hold ? board : board_t::board_unknown) { case board_t::board_M5StackCoreS3: case board_t::board_M5StackCoreS3SE: From e017bd311174fc38e9467f864a72e83860ba218c Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Fri, 2 Oct 2026 00:23:15 +0000 Subject: [PATCH 2/6] Keep unknown chips off the ESP32 board tables and let the driver own the I2S clock where the raw divider is unverified A chip that none of the pin tables name fell through to the ESP32 rows, so a new target with enough GPIOs (ESP32-S31, H4) compiled and then probed the ESP32 I2C pins as board_unknown, while one with fewer (H21) failed to compile. The seven tables now name the ESP32 branch and end in the sentinel row for anything else. (The ESP32-C2, which has no board, moves from the ESP32 rows to the sentinel as well.) The I2S driver configuration asks for I2S_CLK_SRC_DEFAULT everywhere: on every chip whose raw divider path runs it is the PLL_160M source that path expects, so nothing changes on the existing targets, and a chip without it no longer needs naming. The raw clock divider that the speaker and mic tasks write afterwards depends on a per-chip source frequency (M5UNIFIED_I2S_PLL_D2_HZ) that cannot be derived from a capability macro, so it is defined only for the targets it has been verified on; any other chip takes the driver-managed path that the ESP32-P4 already uses (real rate passed at setup, no raw override, the over-sampled rate for PDM capture) instead of guessing 80 MHz. The unverified H4 entry is dropped from the table for the same reason. Together with the matching M5GFX change, ESP32-S31 and ESP32-H21 (ESP-IDF 6.1 preview) build; the existing targets keep their code paths. (cherry picked from commit 17c89a57f9506e9aff73eae23401e4a9de1eeba7) --- src/M5Unified.inl | 26 +++++++++++--------------- src/utility/Mic_Class.inl | 35 ++++++++++++++++++++--------------- src/utility/Speaker_Class.inl | 16 +++++++--------- src/utility/m5unified_i2s.h | 26 ++++++++++++++++---------- 4 files changed, 54 insertions(+), 49 deletions(-) diff --git a/src/M5Unified.inl b/src/M5Unified.inl index 14281fd1..491f0d9f 100644 --- a/src/M5Unified.inl +++ b/src/M5Unified.inl @@ -145,9 +145,7 @@ static constexpr const uint8_t _pin_table_i2c_ex_in[][5] = { { board_t::board_M5StampC5 , 255 ,255 , 255 ,255 }, { board_t::board_M5ToughC5 , GPIO_NUM_3 ,GPIO_NUM_2 , GPIO_NUM_3 ,GPIO_NUM_2 }, // PortA は内部バスと同一 (レベルシフタ経由の物理分配) { board_t::board_unknown , 255 ,255 , 255 ,255 }, -#elif defined (CONFIG_IDF_TARGET_ESP32S2) -{ board_t::board_unknown , 255 ,255 , 255 ,255 }, -#else +#elif !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) { board_t::board_M5Stack , GPIO_NUM_22,GPIO_NUM_21 , GPIO_NUM_22,GPIO_NUM_21 }, { board_t::board_M5Paper , GPIO_NUM_22,GPIO_NUM_21 , GPIO_NUM_32,GPIO_NUM_25 }, { board_t::board_M5TimerCam , GPIO_NUM_14,GPIO_NUM_12 , GPIO_NUM_13,GPIO_NUM_4 }, @@ -157,6 +155,10 @@ static constexpr const uint8_t _pin_table_i2c_ex_in[][5] = { { board_t::board_M5AtomU , GPIO_NUM_21,GPIO_NUM_25 , GPIO_NUM_32,GPIO_NUM_26 }, { board_t::board_M5AtomPsram , GPIO_NUM_21,GPIO_NUM_25 , GPIO_NUM_32,GPIO_NUM_26 }, { board_t::board_unknown , GPIO_NUM_22,GPIO_NUM_21 , GPIO_NUM_33,GPIO_NUM_32 }, // Core2,Tough,StickC,CoreInk,Station,StampPico +#else +// A chip without boards of its own: nothing to probe. Kept explicit so that it never +// inherits the ESP32 table above. +{ board_t::board_unknown , 255 ,255 , 255 ,255 }, #endif }; @@ -180,8 +182,7 @@ static constexpr const uint8_t _pin_table_port_bc[][5] = { { board_t::board_M5Tab5 , GPIO_NUM_17,GPIO_NUM_52, GPIO_NUM_7 ,GPIO_NUM_6 }, // Tab5 #elif defined (CONFIG_IDF_TARGET_ESP32C5) { board_t::board_M5ToughC5 , GPIO_NUM_1 ,GPIO_NUM_6 , GPIO_NUM_12,GPIO_NUM_11 }, -#elif defined (CONFIG_IDF_TARGET_ESP32S2) -#else +#elif !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) { board_t::board_M5Stack , GPIO_NUM_36,GPIO_NUM_26 , GPIO_NUM_16,GPIO_NUM_17 }, { board_t::board_M5StackCore2 , GPIO_NUM_36,GPIO_NUM_26 , GPIO_NUM_13,GPIO_NUM_14 }, { board_t::board_M5Tough , GPIO_NUM_36,GPIO_NUM_26 , GPIO_NUM_13,GPIO_NUM_14 }, @@ -202,8 +203,7 @@ static constexpr const uint8_t _pin_table_port_de[][5] = { #elif defined (CONFIG_IDF_TARGET_ESP32C61) #elif defined (CONFIG_IDF_TARGET_ESP32H2) #elif defined (CONFIG_IDF_TARGET_ESP32C5) -#elif defined (CONFIG_IDF_TARGET_ESP32S2) -#else +#elif !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) { board_t::board_M5Stack , GPIO_NUM_34,GPIO_NUM_35 , GPIO_NUM_5 ,GPIO_NUM_13 }, { board_t::board_M5StackCore2 , GPIO_NUM_34,GPIO_NUM_35 , GPIO_NUM_27,GPIO_NUM_19 }, { board_t::board_M5Station , GPIO_NUM_36,GPIO_NUM_26 , GPIO_NUM_16,GPIO_NUM_17 }, // B2 / C2 @@ -235,8 +235,7 @@ static constexpr const uint8_t _pin_table_sd[][7] = { { board_t::board_M5Tab5 , GPIO_NUM_43, GPIO_NUM_44, GPIO_NUM_39, GPIO_NUM_40, GPIO_NUM_41, GPIO_NUM_42 }, #elif defined (CONFIG_IDF_TARGET_ESP32C5) { board_t::board_M5ToughC5 , GPIO_NUM_9 , GPIO_NUM_7 , GPIO_NUM_8 , 255 , 255 , GPIO_NUM_10 }, -#elif defined (CONFIG_IDF_TARGET_ESP32S2) -#else +#elif !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) { board_t::board_M5Stack , GPIO_NUM_18, GPIO_NUM_23, GPIO_NUM_19, 255 , 255 , GPIO_NUM_4 }, { board_t::board_M5StackCore2 , GPIO_NUM_18, GPIO_NUM_23, GPIO_NUM_38, 255 , 255 , GPIO_NUM_4 }, { board_t::board_M5Tough , GPIO_NUM_18, GPIO_NUM_23, GPIO_NUM_38, 255 , 255 , GPIO_NUM_4 }, @@ -269,8 +268,7 @@ static constexpr const uint8_t _pin_table_other0[][2] = { #elif defined (CONFIG_IDF_TARGET_ESP32H2) { board_t::board_M5NanoH2 , GPIO_NUM_11 }, #elif defined (CONFIG_IDF_TARGET_ESP32C5) -#elif defined (CONFIG_IDF_TARGET_ESP32S2) -#else +#elif !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) { board_t::board_M5Stack , GPIO_NUM_15 }, { board_t::board_M5StackCore2 , GPIO_NUM_25 }, { board_t::board_M5Station , GPIO_NUM_4 }, @@ -298,8 +296,7 @@ static constexpr const uint8_t _pin_table_other1[][2] = { #elif defined (CONFIG_IDF_TARGET_ESP32C61) #elif defined (CONFIG_IDF_TARGET_ESP32H2) #elif defined (CONFIG_IDF_TARGET_ESP32C5) -#elif defined (CONFIG_IDF_TARGET_ESP32S2) -#else +#elif !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) { board_t::board_M5StickCPlus2 , GPIO_NUM_4 }, { board_t::board_M5Paper , GPIO_NUM_2 }, @@ -420,8 +417,7 @@ static constexpr const uint8_t _pin_table_mbus[][31] = { }, #elif defined (CONFIG_IDF_TARGET_ESP32H2) #elif defined (CONFIG_IDF_TARGET_ESP32C5) -#elif defined (CONFIG_IDF_TARGET_ESP32S2) -#else +#elif !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) { board_t::board_M5Stack , 255 , GPIO_NUM_35, 255 , GPIO_NUM_36, diff --git a/src/utility/Mic_Class.inl b/src/utility/Mic_Class.inl index 869c8c8d..daa2cecf 100644 --- a/src/utility/Mic_Class.inl +++ b/src/utility/Mic_Class.inl @@ -317,13 +317,18 @@ namespace m5 if (_cfg.pin_bck < 0 || _cfg.pin_ws < 0) { i2s_pdm_rx_config_t i2s_config; memset(&i2s_config, 0, sizeof(i2s_pdm_rx_config_t)); -#if defined ( CONFIG_IDF_TARGET_ESP32P4 ) || defined (CONFIG_IDF_TARGET_ESP32H2) i2s_config.clk_cfg.clk_src = i2s_clock_src_t::I2S_CLK_SRC_DEFAULT; -#else - i2s_config.clk_cfg.clk_src = i2s_clock_src_t::I2S_CLK_SRC_PLL_160M; -#endif +#if defined ( M5UNIFIED_I2S_PLL_D2_HZ ) i2s_config.clk_cfg.sample_rate_hz = 48000; // dummy setting i2s_config.clk_cfg.mclk_multiple = i2s_mclk_multiple_t::I2S_MCLK_MULTIPLE_128; // dummy setting +#else + // No verified raw clock recipe on this chip: the driver keeps the clock, so the real + // port rate (over_sampling included, as the capture task expects) goes in here. + // DSR 16 matches what the raw path selects. + i2s_config.clk_cfg.sample_rate_hz = _calc_rec_rate(); + i2s_config.clk_cfg.mclk_multiple = i2s_mclk_multiple_t::I2S_MCLK_MULTIPLE_256; + i2s_config.clk_cfg.dn_sample_mode = i2s_pdm_dsr_t::I2S_PDM_DSR_16S; +#endif i2s_config.slot_cfg.data_bit_width = i2s_data_bit_width_t::I2S_DATA_BIT_WIDTH_16BIT; i2s_config.slot_cfg.slot_bit_width = i2s_slot_bit_width_t::I2S_SLOT_BIT_WIDTH_16BIT; i2s_config.slot_cfg.slot_mode = (_cfg.stereo) ? i2s_slot_mode_t::I2S_SLOT_MODE_STEREO : i2s_slot_mode_t::I2S_SLOT_MODE_MONO; @@ -336,14 +341,9 @@ if (_cfg.pin_bck < 0 || _cfg.pin_ws < 0) { { i2s_std_config_t i2s_config; memset(&i2s_config, 0, sizeof(i2s_std_config_t)); -#if defined ( CONFIG_IDF_TARGET_ESP32P4 ) || defined (CONFIG_IDF_TARGET_ESP32H2) i2s_config.clk_cfg.clk_src = i2s_clock_src_t::I2S_CLK_SRC_DEFAULT; -#else - i2s_config.clk_cfg.clk_src = i2s_clock_src_t::I2S_CLK_SRC_PLL_160M; -#endif -#if defined ( CONFIG_IDF_TARGET_ESP32P4 ) - { // ESP32-P4 はクロックをドライバ管理で最終値に確定させる (mic_task での raw 分周 - // 上書きを行わない)。クロック源既定 (minimum supported revision < 3 のビルドは +#if M5UNIFIED_I2S_DRIVER_MANAGED_CLK + { // クロックをドライバ管理で最終値に確定させる (mic_task での raw 分周上書きを行わない)。クロック源既定 (minimum supported revision < 3 のビルドは // XTAL 40MHz / rev >= 3 ビルドは PLL_F160M) もドライバに委ねる。 int os = _cfg.over_sampling; if (os < 1) { os = 1; } else if (os > 8) { os = 8; } @@ -458,6 +458,7 @@ if (_cfg.pin_bck < 0 || _cfg.pin_ws < 0) { bool use_pdm = (self->_cfg.pin_bck < 0 && !self->_cfg.use_adc); +#if defined ( M5UNIFIED_I2S_PLL_D2_HZ ) static constexpr uint32_t PLL_D2_CLK = M5UNIFIED_I2S_PLL_D2_HZ; uint32_t bits = (self->_cfg.use_adc) ? 1 : 16; /// 1サンプリング当たりの出力ビット数; @@ -473,6 +474,7 @@ if (_cfg.pin_bck < 0 || _cfg.pin_ws < 0) { // read here: a queued record() with a new rate may overwrite it while // this request is still being captured. calcClockDiv(&div_a, &div_b, &div_n, PLL_D2_CLK / (bits * div_m), self->_rec_sample_rate); +#endif // false when the raw clock latch below times out: the channel must not // report itself active with an unlatched clock configuration. @@ -489,7 +491,10 @@ if (_cfg.pin_bck < 0 || _cfg.pin_ws < 0) { #endif #endif -#if defined (M5UNIFIED_I2S_HW_V2) +#if !defined ( M5UNIFIED_I2S_PLL_D2_HZ ) + // No verified raw recipe for this chip: the driver keeps the mode and clock it configured. + (void)dev; (void)use_pdm; +#elif defined (M5UNIFIED_I2S_HW_V2) dev->rx_conf.rx_pdm_en = use_pdm; dev->rx_conf.rx_tdm_en = !use_pdm; @@ -500,9 +505,9 @@ if (_cfg.pin_bck < 0 || _cfg.pin_ws < 0) { dev->rx_conf.rx_pdm2pcm_en = use_pdm; dev->rx_conf.rx_pdm_sinc_dsr_16_en = 1; #endif -#if defined (CONFIG_IDF_TARGET_ESP32P4) - // ESP32-P4 の std 経路はクロックを _setup_i2s でドライバ管理により最終値に - // 設定済みのため、raw 分周の上書きを行わない (PDM 経路は従来どおり)。 +#if M5UNIFIED_I2S_DRIVER_MANAGED_CLK + // std 経路はクロックを _setup_i2s でドライバ管理により最終値に設定済みのため、 + // raw 分周の上書きを行わない (PDM 経路は従来どおり)。 const bool skip_raw_clk = !use_pdm; #else const bool skip_raw_clk = false; diff --git a/src/utility/Speaker_Class.inl b/src/utility/Speaker_Class.inl index 9c3899fb..cc8e0ef8 100644 --- a/src/utility/Speaker_Class.inl +++ b/src/utility/Speaker_Class.inl @@ -202,14 +202,12 @@ namespace m5 i2s_std_config_t i2s_config; memset(&i2s_config, 0, sizeof(i2s_std_config_t)); -#if defined ( CONFIG_IDF_TARGET_ESP32H2 ) || defined ( CONFIG_IDF_TARGET_ESP32P4 ) + // On every chip whose raw divider path runs (M5UNIFIED_I2S_PLL_D2_HZ), I2S_CLK_SRC_DEFAULT + // is the PLL_160M source that path expects, so no chip list is needed here. (The ESP32-P4 + // default is chosen at run time, which is why its clock stays driver managed.) i2s_config.clk_cfg.clk_src = i2s_clock_src_t::I2S_CLK_SRC_DEFAULT; -#else - i2s_config.clk_cfg.clk_src = i2s_clock_src_t::I2S_CLK_SRC_PLL_160M; -#endif -#if defined ( CONFIG_IDF_TARGET_ESP32P4 ) - // ESP32-P4 はクロックをドライバ管理で最終値に確定させる (spk_task での raw 分周 - // 上書きを行わない)。ここで実レートを渡すことで、begin 中のクロック遷移が +#if M5UNIFIED_I2S_DRIVER_MANAGED_CLK + // クロックをドライバ管理で最終値に確定させる (spk_task での raw 分周上書きを行わない)。ここで実レートを渡すことで、begin 中のクロック遷移が // 一度きりになり、外付け codec (Tab5=ES8388) のロック失敗を防ぐ。 // クロック源既定 (minimum supported revision < 3 のビルドは XTAL 40MHz / // rev >= 3 ビルドは PLL_F160M) もドライバに委ねる。 @@ -400,7 +398,7 @@ namespace m5 #else const i2s_port_t i2s_port = self->_cfg.i2s_port; -#if defined (CONFIG_IDF_TARGET_ESP32P4) +#if M5UNIFIED_I2S_DRIVER_MANAGED_CLK // クロックは _setup_i2s でドライバ管理により最終値に設定済み (実レート + // 256fs、40MHz source ビルドの高レートのみ 128fs)。 // ドライバの分数分周 (a/b ≤ 511) は十分高精度のため、レート換算は公称値でよい。 @@ -537,7 +535,7 @@ namespace m5 dev->conf.tx_fifo_reset = 0; #endif -#endif // !CONFIG_IDF_TARGET_ESP32P4 (raw クロック設定ブロック全体) +#endif // !M5UNIFIED_I2S_DRIVER_MANAGED_CLK (raw クロック設定ブロック全体) // i2s_zero_dma_buffer(i2s_port); enum spk_i2s_state diff --git a/src/utility/m5unified_i2s.h b/src/utility/m5unified_i2s.h index 0be333f0..f56c6008 100644 --- a/src/utility/m5unified_i2s.h +++ b/src/utility/m5unified_i2s.h @@ -65,19 +65,25 @@ /// Source clock frequency assumed by the raw clock divider setup in the speaker/mic /// tasks (the frequency selected by tx/rx_clk_sel = 1 on HW v2, PLL_160M on HW v1). /// This is a per-chip physical property that cannot be derived from a capability -/// macro, so every known target is enumerated explicitly. +/// macro, so it is only defined for the targets on which it has been verified; on +/// any other chip the raw divider is not written and the driver keeps the clock it +/// configured (M5UNIFIED_I2S_DRIVER_MANAGED_CLK). #if defined ( CONFIG_IDF_TARGET_ESP32C3 ) || defined ( CONFIG_IDF_TARGET_ESP32C6 ) || defined ( CONFIG_IDF_TARGET_ESP32C5 ) || defined ( CONFIG_IDF_TARGET_ESP32C61 ) || defined ( CONFIG_IDF_TARGET_ESP32S3 ) #define M5UNIFIED_I2S_PLL_D2_HZ (120*1000*1000) // 240 MHz/2 #elif defined ( CONFIG_IDF_TARGET_ESP32P4 ) - #define M5UNIFIED_I2S_PLL_D2_HZ (20*1000*1000) // 20 MHz -#elif defined ( CONFIG_IDF_TARGET_ESP32H2 ) || defined ( CONFIG_IDF_TARGET_ESP32H4 ) + #define M5UNIFIED_I2S_PLL_D2_HZ (20*1000*1000) // 20 MHz (PDM capture only; the std path is driver managed) +#elif defined ( CONFIG_IDF_TARGET_ESP32H2 ) #define M5UNIFIED_I2S_PLL_D2_HZ (96*1000*1000) // PLL_F96M -#else - /// Unknown I2S-capable targets fall back to the HW v1 value. The message below is - /// intentionally not #warning (which fails -Werror builds); it flags that the - /// frequency must be verified and added to the table above. - #if defined (M5UNIFIED_I2S_PORT_COUNT) && defined (CONFIG_IDF_TARGET) && !defined (CONFIG_IDF_TARGET_ESP32) && !defined (CONFIG_IDF_TARGET_ESP32S2) - #pragma message ("M5Unified: unknown target, assuming a 80 MHz I2S source clock. Verify it and extend the table in m5unified_i2s.h") - #endif +#elif !defined ( CONFIG_IDF_TARGET ) || defined ( CONFIG_IDF_TARGET_ESP32 ) || defined ( CONFIG_IDF_TARGET_ESP32S2 ) #define M5UNIFIED_I2S_PLL_D2_HZ (80*1000*1000) // 160 MHz/2 #endif + +/// The speaker/mic std paths leave the sample clock to the driver (real rate passed at +/// setup, no raw divider override) on chips whose clock tree the raw path has not been +/// verified against, and on ESP32-P4 where the one-shot clock transition is required +/// by the external codec. +#if defined ( CONFIG_IDF_TARGET_ESP32P4 ) || !defined ( M5UNIFIED_I2S_PLL_D2_HZ ) + #define M5UNIFIED_I2S_DRIVER_MANAGED_CLK 1 +#else + #define M5UNIFIED_I2S_DRIVER_MANAGED_CLK 0 +#endif From b926d6474cc0321efa2a8ad048662037a0c29235 Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Fri, 2 Oct 2026 00:23:15 +0000 Subject: [PATCH 3/6] Power/RTC: let the PowerHub STM32 arm the alarm before powering off On PowerHub, timerSleep() powered the board off before the STM32 front-end had armed the RTC alarm, so the board never woke up and only the power button brought it back. The STM32 acknowledges the alarm register writes (D0..D3) at once but applies them from its main loop, behind a single "update pending" flag, so a power-off request right after the alarm-enable write can overtake the update, and an update that lands while an older one is applied can be dropped. Space the alarm register writes by 50 ms and wait 500 ms before the timer power-off request. After the request the existing light sleep and restart path is kept. Backport of the waiting part of #369; the API changes that came with it on the development branch are not included. --- src/utility/Power_Class.hpp | 3 +++ src/utility/Power_Class.inl | 17 ++++++++++++----- src/utility/rtc/RTC_PowerHub_Class.inl | 8 ++++++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/utility/Power_Class.hpp b/src/utility/Power_Class.hpp index 708e7f8d..eb66396e 100644 --- a/src/utility/Power_Class.hpp +++ b/src/utility/Power_Class.hpp @@ -132,9 +132,12 @@ namespace m5 void setLed(uint8_t brightness = 255); /// all power off. + /// @note PowerHub enters light sleep after requesting power-off and restarts if it wakes. void powerOff(void); /// sleep and timer boot. The boot condition can be specified by the argument. + /// @note PowerHub paces alarm writes by 50 ms, settles for 500 ms before requesting + /// power-off, then enters light sleep and restarts if it wakes. /// @param seconds Number of seconds to boot. void timerSleep(int seconds); diff --git a/src/utility/Power_Class.inl b/src/utility/Power_Class.inl index 2ab16458..f201de0f 100644 --- a/src/utility/Power_Class.inl +++ b/src/utility/Power_Class.inl @@ -1693,11 +1693,18 @@ namespace m5 #if defined (CONFIG_IDF_TARGET_ESP32S3) case board_t::board_M5PowerHub: - uint8_t buf[6]={}; - M5.In_I2C.writeRegister(powerhub_i2c_addr, 0x00, buf, sizeof(buf), i2c_freq); - M5.In_I2C.writeRegister8(powerhub_i2c_addr, 0xE0, 1, i2c_freq); - use_deepsleep = false; - break; + { + // The STM32 arms the RTC alarm asynchronously after acknowledging the writes. + // Let it finish before the power-off request can overtake the alarm update. + static constexpr uint32_t powerhub_alarm_settle_ms = 500; + if (withTimer) { m5gfx::delay(powerhub_alarm_settle_ms); } + uint8_t buf[6]={}; + M5.In_I2C.writeRegister(powerhub_i2c_addr, 0x00, buf, sizeof(buf), i2c_freq); + M5.In_I2C.writeRegister8(powerhub_i2c_addr, 0xE0, 1, i2c_freq); + // Do not resume the sketch with outputs off and the alarm still armed. + use_deepsleep = false; + break; + } #endif } diff --git a/src/utility/rtc/RTC_PowerHub_Class.inl b/src/utility/rtc/RTC_PowerHub_Class.inl index 2731f870..d59cc80a 100644 --- a/src/utility/rtc/RTC_PowerHub_Class.inl +++ b/src/utility/rtc/RTC_PowerHub_Class.inl @@ -10,6 +10,10 @@ namespace m5 { + // The STM32 applies D0..D3 from its main loop after acknowledging I2C writes. + // Pace the writes so an older update cannot clear the pending flag for a newer one. + static constexpr std::uint32_t powerhub_alarm_apply_wait_ms = 50; + static std::uint8_t weekdayToPowerHub(std::int8_t weekDay) { static constexpr std::uint8_t weekDayTable[] = { 1, 2, 4, 8, 10, 20, 40 }; @@ -120,6 +124,7 @@ namespace m5 } writeRegister(0xD0, buf, 3); + m5gfx::delay(powerhub_alarm_apply_wait_ms); if (irq_enable) { bitOn(0xB0, irq_enable); @@ -128,6 +133,7 @@ namespace m5 bitOff(0xB0, irq_enable); bitOff(0xD3, irq_enable); } + m5gfx::delay(powerhub_alarm_apply_wait_ms); return irq_enable; } @@ -142,11 +148,13 @@ namespace m5 if (!_init) { return; } static constexpr const std::uint8_t buf[4] = {}; writeRegister(0xD0, buf, sizeof(buf)); + m5gfx::delay(powerhub_alarm_apply_wait_ms); } void RTC_PowerHub_Class::disableIRQ(void) { if (!_init) { return; } bitOff(0xD3, 0); // disable alarm + m5gfx::delay(powerhub_alarm_apply_wait_ms); } } From 057b5628665347a3e6e8c8e10a8b835cfc481ec6 Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Fri, 2 Oct 2026 00:23:15 +0000 Subject: [PATCH 4/6] RTC: allow time-only PowerHub alarms without a date setAlarmIRQ(time) passes a null date pointer, and the PowerHub implementation dereferenced it. Leave the day register at zero (every day) when no date is given. Backport of the corresponding part of #368. --- src/utility/rtc/RTC_PowerHub_Class.inl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utility/rtc/RTC_PowerHub_Class.inl b/src/utility/rtc/RTC_PowerHub_Class.inl index d59cc80a..43e1007f 100644 --- a/src/utility/rtc/RTC_PowerHub_Class.inl +++ b/src/utility/rtc/RTC_PowerHub_Class.inl @@ -117,7 +117,8 @@ namespace m5 } } - if (date->date >= 0) + // A time-only alarm has no date; zero in D2 means every day. + if (date && date->date >= 0) { irq_enable = true; buf[2] = date->date & 0x1f; From 6aab9f57ef9ccb274f8f55ebc2ebabd74bcc9457 Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Fri, 2 Oct 2026 00:23:15 +0000 Subject: [PATCH 5/6] RTC: actually disable the PowerHub alarm disableIRQ() and the disabled branch of setAlarmIRQ() called bitOff() with a zero mask, which changes no bits, so the alarm stayed enabled. Write zero to the alarm enable register instead. Backport of the corresponding part of #368. --- src/utility/rtc/RTC_PowerHub_Class.inl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utility/rtc/RTC_PowerHub_Class.inl b/src/utility/rtc/RTC_PowerHub_Class.inl index 43e1007f..9d3801e1 100644 --- a/src/utility/rtc/RTC_PowerHub_Class.inl +++ b/src/utility/rtc/RTC_PowerHub_Class.inl @@ -132,7 +132,7 @@ namespace m5 bitOn(0xD3, irq_enable); } else { bitOff(0xB0, irq_enable); - bitOff(0xD3, irq_enable); + writeRegister8(0xD3, 0); // A zero bit mask would leave the alarm enabled. } m5gfx::delay(powerhub_alarm_apply_wait_ms); @@ -155,7 +155,7 @@ namespace m5 void RTC_PowerHub_Class::disableIRQ(void) { if (!_init) { return; } - bitOff(0xD3, 0); // disable alarm + writeRegister8(0xD3, 0); // Clear the enable register; bitOff with a zero mask is a no-op. m5gfx::delay(powerhub_alarm_apply_wait_ms); } } From c11ea6c04a36554794b6377f32e15aa074c4d3b1 Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Fri, 2 Oct 2026 00:23:15 +0000 Subject: [PATCH 6/6] Bump version to 0.2.25 --- 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 18d25068..a05d1261 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.24 +version: 0.2.25 dependencies: m5stack/m5gfx: version: ">=0.2.31" diff --git a/library.json b/library.json index ee3cdf51..1d5fba4a 100644 --- a/library.json +++ b/library.json @@ -16,7 +16,7 @@ "version": ">=0.2.31" } ], - "version": "0.2.24", + "version": "0.2.25", "frameworks": ["arduino", "espidf", "*"], "platforms": ["espressif32", "native"], "headers": "M5Unified.h" diff --git a/library.properties b/library.properties index 41ad1346..59a8a635 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=M5Unified -version=0.2.24 +version=0.2.25 author=M5Stack maintainer=M5Stack sentence=Unified library for M5Stack series diff --git a/src/gitTagVersion.h b/src/gitTagVersion.h index 0ecd0c5b..4a82349b 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 24 +#define M5UNIFIED_VERSION_PATCH 25 #define M5UNIFIED_VERSION F( M5UNIFIED_VERSION_MAJOR "." M5UNIFIED_VERSION_MINOR "." M5UNIFIED_VERSION_PATCH )