From 4522ae26b0d1ee82ef947a7b10a98a8f136b36b5 Mon Sep 17 00:00:00 2001 From: luoweiyuan Date: Thu, 22 Oct 2026 15:49:21 +0800 Subject: [PATCH 1/6] Fix CoreP4X IMU X-axis orientation Invert the BMI270 accelerometer and gyroscope X axes in the CoreP4X board mapping. Keep the existing Z inversion and user axis-order handling. (cherry picked from commit e5494bf620efbe91a0d6fcee7da6c8186fcdd5e5) --- src/utility/IMU_Class.inl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utility/IMU_Class.inl b/src/utility/IMU_Class.inl index f214f722..ff255496 100644 --- a/src/utility/IMU_Class.inl +++ b/src/utility/IMU_Class.inl @@ -96,9 +96,9 @@ namespace m5 } #elif defined(CONFIG_IDF_TARGET_ESP32P4) if (board == m5::board_t::board_M5CoreP4X) - { - _internal_axisorder_fixed[sensor_index_accel] = (internal_axisorder_t)(axis_invert_z); - _internal_axisorder_fixed[sensor_index_gyro ] = (internal_axisorder_t)(axis_invert_z); + { // CoreP4X BMI270: X=-X, Y=+Y, Z=-Z. + _internal_axisorder_fixed[sensor_index_accel] = (internal_axisorder_t)(axis_invert_x | axis_invert_z); + _internal_axisorder_fixed[sensor_index_gyro ] = (internal_axisorder_t)(axis_invert_x | axis_invert_z); } #elif defined(CONFIG_IDF_TARGET_ESP32S3) if ((board == m5::board_t::board_M5StackCoreS3 || board == m5::board_t::board_M5StackChan) From 9536a08e2f0c7e3aef69acc4402664d434a99746 Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:09:23 +0000 Subject: [PATCH 2/6] Speaker (internal DAC): remove the pops at the start and end of a sound On the internal 8-bit DAC (M5Stack Core), two steps were audible around every sound: - End: the hold level and the half-cosine ramp to 0 were written as whole DAC steps. At the default volume the bias is only a few steps, so the ramp became a staircase of 1-LSB clicks. The hold level and both ramps now carry the fraction below one step per sample, like the playback path does. The ramps use a smoothstep curve in integer math instead of cosf per sample. - Start: after about 85 ms of silence I2S was stopped and the DAC disabled; restarting them stepped the DAC output by about 40 mV before the next sound. On the DAC, I2S now keeps running at the zero level for about a second before it is stopped, so pauses inside a melody or between key clicks no longer stop it. A sound after a longer silence still steps once. When end() wakes the task during that second, the rising ramp of a fixed dac_zero_level is skipped, so the pin does not jump to the bias just before it is pulled low. External I2S DACs and the buzzer output are unchanged. (cherry picked from commit 0ecbc0c126b0268ac5e1b510db304334552d66ea) --- src/utility/Speaker_Class.inl | 65 +++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/src/utility/Speaker_Class.inl b/src/utility/Speaker_Class.inl index 05982e4c..06cb1325 100644 --- a/src/utility/Speaker_Class.inl +++ b/src/utility/Speaker_Class.inl @@ -562,6 +562,36 @@ namespace m5 int32_t* sound_buf32 = (int32_t*)alloca(dma_buf_len * sizeof(int32_t)); +#if !defined (SDL_h_) + // Fill one write of sound_buf32 with a DAC level: the hold level (dac_offset), or a smoothstep ramp + // (x^2 (3 - 2x): flat at both ends like a half-cosine, in integer math) between dac_offset and 0. + // The fraction below one DAC step is carried per sample like the playback path does; without the + // carry a small bias is written as whole steps, an audible staircase of clicks on the 8-bit DAC. + enum dac_fill_t { dac_hold, dac_fall, dac_rise }; + uint8_t* const carry = surplus; // a lambda cannot capture a member of the anonymous union + auto fill_dac = [&](size_t words, dac_fill_t mode) + { + const uint32_t steps = out_stereo ? dma_buf_len : dma_buf_len << 1; + auto level = [&](size_t t) -> int32_t + { + if (mode == dac_hold) { return dac_offset; } + uint32_t x = (uint32_t)((t << 15) / steps); // 0 .. <1 in Q15 + uint32_t y = ((x * x >> 15) * (98304 - 2 * x)) >> 15; // x^2 (3 - 2x) in Q15 + if (mode == dac_fall) { y = 32768 - y; } + return (int32_t)(((uint32_t)dac_offset * y) >> 15); + }; + for (size_t idx = 0; idx < words; ++idx) + { // mono packs two time steps per word; stereo packs L/R of the same step + size_t t = out_stereo ? idx : idx << 1; + int32_t v1 = level(t) + carry[0]; + carry[0] = v1; + int32_t v2 = level(out_stereo ? t : t + 1) + carry[out_stereo]; + carry[out_stereo] = v2; + sound_buf32[idx] = v1 << 16 | v2; + } + }; +#endif + while (self->_task_running.load(std::memory_order_acquire)) { @@ -583,9 +613,15 @@ namespace m5 { // 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; } + if (self->_cfg.use_dac) + { + fill_dac(words, dac_hold); + } + else + { + memset(sound_buf32, 0, words * sizeof(int32_t)); + } 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 )); @@ -597,12 +633,7 @@ namespace m5 if (self->_cfg.use_dac && dac_offset) { // Gradual transition of DAC output to 0; flg_i2s_started = spk_i2s_mute; - size_t idx = 0; - do - { - auto tmp = (uint32_t)((1.0f + cosf(idx * M_PI / dma_buf_len)) * (dac_offset >> 1)); - sound_buf32[idx] = tmp | tmp << 16; - } while (++idx < dma_buf_len); + fill_dac(dma_buf_len, dac_fall); size_t write_bytes; _spk_i2s_write(i2s_port, sound_buf32, dma_buf_len * sizeof(int32_t), &write_bytes, portMAX_DELAY); if (self->_cfg.dac_zero_level == 0) @@ -615,6 +646,13 @@ namespace m5 memset(sound_buf32, 0, dma_buf_len * sizeof(uint32_t)); // DAC使用時は長めに設定する size_t retry = (self->_cfg.dma_buf_count << self->_cfg.use_dac) + 1; + if (self->_cfg.use_dac && dma_buf_len) + { // Keep I2S running at the zero level for about a second before stopping it. Stopping and + // restarting I2S steps the DAC output (a pop at both ends of a sound), so short pauses + // between sounds (melodies, key clicks) stay free of it. + size_t per_sec = (spk_sample_rate_x256 / SAMPLERATE_MUL) / (out_stereo ? dma_buf_len : dma_buf_len << 1); + if (retry < per_sec) { retry = per_sec; } + } while (!ulTaskNotifyTake( pdTRUE, 0 ) && --retry) { size_t write_bytes; @@ -664,14 +702,11 @@ namespace m5 _spk_i2s_start(i2s_port); } - if (self->_cfg.use_dac && self->_cfg.dac_zero_level != 0) + // not when end() woke the task from the zero level: raising the bias just before the pin + // is pulled low would add a pop + if (self->_cfg.use_dac && self->_cfg.dac_zero_level != 0 && self->_task_running.load(std::memory_order_acquire)) { - size_t idx = 0; - do - { - auto tmp = (uint32_t)((1.0f - cosf(idx * M_PI / dma_buf_len)) * (dac_offset >> 1)); - sound_buf32[idx] = tmp | tmp << 16; - } while (++idx < dma_buf_len); + fill_dac(dma_buf_len, dac_rise); size_t write_bytes; _spk_i2s_write(i2s_port, sound_buf32, dma_buf_len * sizeof(int32_t), &write_bytes, portMAX_DELAY); } From d3e27463dd8a7a0ee3b907ed6ac50389f1ea1ba3 Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:09:23 +0000 Subject: [PATCH 3/6] begin(): external displays follow cfg.clear_display Module Display, Unit OLED / MiniOLED / GLASS / GLASS2 / LCD, Module RCA and Unit RCA were initialized with init() regardless of cfg.clear_display, so they were reset and cleared even when the sketch asked to keep the display (m5stack/M5GFX#370). They now follow the flag like the internal display and Unit PoE-P4: init() when it is true, init_without_reset(false) otherwise. The default is unchanged. (cherry picked from commit 2dc85420a9666dc3bec79a0995f9701c9dc86c8f) --- src/M5Unified.hpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/M5Unified.hpp b/src/M5Unified.hpp index 325aa699..ed9be7bb 100644 --- a/src/M5Unified.hpp +++ b/src/M5Unified.hpp @@ -477,7 +477,7 @@ namespace m5 #endif { M5ModuleDisplay dsp(cfg.module_display); - if (dsp.init()) { + if (cfg.clear_display ? dsp.init() : dsp.init_without_reset(false)) { addDisplay(dsp); } } @@ -520,7 +520,7 @@ namespace m5 #endif { M5UnitOLED dsp(cfg.unit_oled); - if (dsp.init()) { + if (cfg.clear_display ? dsp.init() : dsp.init_without_reset(false)) { addDisplay(dsp); port_a_used = true; } @@ -544,7 +544,7 @@ namespace m5 #endif { M5UnitMiniOLED dsp(cfg.unit_mini_oled); - if (dsp.init()) { + if (cfg.clear_display ? dsp.init() : dsp.init_without_reset(false)) { addDisplay(dsp); port_a_used = true; } @@ -568,7 +568,7 @@ namespace m5 #endif { M5UnitGLASS dsp(cfg.unit_glass); - if (dsp.init()) { + if (cfg.clear_display ? dsp.init() : dsp.init_without_reset(false)) { addDisplay(dsp); port_a_used = true; } @@ -592,7 +592,7 @@ namespace m5 #endif { M5UnitGLASS2 dsp(cfg.unit_glass2); - if (dsp.init()) { + if (cfg.clear_display ? dsp.init() : dsp.init_without_reset(false)) { addDisplay(dsp); port_a_used = true; } @@ -619,7 +619,7 @@ namespace m5 int retry = 8; do { m5gfx::delay(32); - if (dsp.init()) { + if (cfg.clear_display ? dsp.init() : dsp.init_without_reset(false)) { addDisplay(dsp); port_a_used = true; break; @@ -645,7 +645,7 @@ namespace m5 // When ModuleRCA is used, UnitRCA is not used. unit_rca = false; M5ModuleRCA dsp(cfg.module_rca); - if (dsp.init()) { + if (cfg.clear_display ? dsp.init() : dsp.init_without_reset(false)) { addDisplay(dsp); } } @@ -668,7 +668,7 @@ namespace m5 ))) { M5UnitRCA dsp(cfg.unit_rca); - if (dsp.init()) { + if (cfg.clear_display ? dsp.init() : dsp.init_without_reset(false)) { addDisplay(dsp); } } From 34926008d5780276491bedc8cea053473621756b Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:09:23 +0000 Subject: [PATCH 4/6] RX8130: do not charge the RTC backup on boards with a primary cell begin() set CHGEN on every board. ToughC5 has a CR2032 primary cell on VBAT with no series resistor, so M5.begin() enabled charging of a non-rechargeable cell. CHGEN is now a constructor option (default off), set per board in RTC_Class::begin(): on for boards whose VBAT holds something chargeable (Tab5: supercapacitor, StamPLC: rechargeable cell, both through 1 kOhm; StopWatch / PaperMono: capacitor), off for ToughC5 and for boards whose backup is unknown. begin() clears the bit explicitly, because it survives on VBAT after an earlier firmware set it. INIEN stays on for every board (the datasheet sets it for both cell types). --- src/utility/RTC_Class.inl | 16 +++++++++++----- src/utility/rtc/RX8130_Class.hpp | 8 +++++++- src/utility/rtc/RX8130_Class.inl | 7 ++++++- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/utility/RTC_Class.inl b/src/utility/RTC_Class.inl index 71aa0664..0d29f0e0 100644 --- a/src/utility/RTC_Class.inl +++ b/src/utility/RTC_Class.inl @@ -57,15 +57,18 @@ namespace m5 switch (board) { #if defined (CONFIG_IDF_TARGET_ESP32P4) + case board_t::board_M5Tab5: // VBAT: supercapacitor through 1 kOhm + instance.reset(new RX8130_Class(RX8130_Class::DEFAULT_ADDRESS, 400000, i2c, true)); + break; + case board_t::board_M5CoreP4X: - case board_t::board_M5Tab5: case board_t::board_M5Tab5X: instance.reset(new RX8130_Class(RX8130_Class::DEFAULT_ADDRESS, 400000, i2c)); break; #endif #if defined (CONFIG_IDF_TARGET_ESP32C5) - case board_t::board_M5ToughC5: + case board_t::board_M5ToughC5: // VBAT: CR2032 primary cell, never charged instance.reset(new RX8130_Class(RX8130_Class::DEFAULT_ADDRESS, 400000, i2c)); break; #endif @@ -75,11 +78,14 @@ namespace m5 instance.reset(new RTC_PowerHub_Class(RTC_PowerHub_Class::DEFAULT_ADDRESS, 400000)); break; + case board_t::board_M5StampPLC: // VBAT: rechargeable backup cell through 1 kOhm + case board_t::board_M5StopWatch: // VBAT: 10 uF capacitor + case board_t::board_M5PaperMono: // VBAT: 1 uF capacitor + instance.reset(new RX8130_Class(RX8130_Class::DEFAULT_ADDRESS, 400000, i2c, true)); + break; + case board_t::board_M5StickS3: - case board_t::board_M5StopWatch: - case board_t::board_M5StampPLC: case board_t::board_M5PaperColor: - case board_t::board_M5PaperMono: case board_t::board_M5ChainCaptain: instance.reset(new RX8130_Class(RX8130_Class::DEFAULT_ADDRESS, 400000, i2c)); break; diff --git a/src/utility/rtc/RX8130_Class.hpp b/src/utility/rtc/RX8130_Class.hpp index e1df4b6a..9ccb7ec3 100644 --- a/src/utility/rtc/RX8130_Class.hpp +++ b/src/utility/rtc/RX8130_Class.hpp @@ -13,8 +13,11 @@ namespace m5 public: static constexpr std::uint8_t DEFAULT_ADDRESS = 0x32; - RX8130_Class(std::uint8_t i2c_addr = DEFAULT_ADDRESS, std::uint32_t freq = 400000, I2C_Class* i2c = &In_I2C) + /// backup_charge: let the RX8130 charge VBAT from VDD (CHGEN). Only for a rechargeable + /// backup (supercapacitor, rechargeable cell); a primary cell such as a CR2032 must not be charged. + RX8130_Class(std::uint8_t i2c_addr = DEFAULT_ADDRESS, std::uint32_t freq = 400000, I2C_Class* i2c = &In_I2C, bool backup_charge = false) : RTC_Base ( i2c_addr, freq, i2c ) + , _backup_charge ( backup_charge ) {} bool begin(I2C_Class* i2c = nullptr) override; @@ -33,6 +36,9 @@ namespace m5 void disableIRQ(void) override; bool getVoltLow(void) override; + + private: + bool _backup_charge; }; } diff --git a/src/utility/rtc/RX8130_Class.inl b/src/utility/rtc/RX8130_Class.inl index 8efde73d..31b15d18 100644 --- a/src/utility/rtc/RX8130_Class.inl +++ b/src/utility/rtc/RX8130_Class.inl @@ -18,7 +18,12 @@ namespace m5 i2c->begin(); } - bool res = bitOn(0x1F, 0x30); + // Control Register 1: INIEN (bit4) always, CHGEN (bit5) only with a rechargeable backup. + // CHGEN is cleared explicitly: it survives on VBAT, so an earlier firmware may have set it. + std::uint8_t ctrl1 = 0; + bool res = readRegister(0x1F, &ctrl1, 1); + ctrl1 = (ctrl1 & ~0x20) | 0x10 | (_backup_charge ? 0x20 : 0x00); + res = res && writeRegister8(0x1F, ctrl1); res &= writeRegister8(0x30, 0x00); res &= writeRegister8(0x1E, 0x00); From 734767b0392d6eebc1a6d6081caad18da806f98a Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:09:23 +0000 Subject: [PATCH 5/6] Tab5: do not enable periodic RTC timer IRQs On Tab5 the interrupt output of the built-in RX8130 is connected to the power management controller. Periodic timer IRQs are not supported there. - The built-in RX8130 on Tab5 is created with periodic timer IRQs disabled. A positive period passed to setTimerIRQ(), through M5.Rtc or the driver, stops any running timer and returns 0. setTimerIRQ(0) still stops the timer. - Power::timerSleep(int seconds) returns without sleeping on Tab5. Use timerSleep(time) or timerSleep(date, time) instead. RX8130 (all boards): - begin() stops the periodic timer. The IRQ flags are kept for getIRQstatus(). - disableIRQ() also stops the periodic timer and reads the registers back to check it. - setAlarmIRQ() keeps the alarm interrupt disabled while the alarm registers are written, and clears the alarm flag before enabling it. Power::timerSleep(time) and timerSleep(date, time) (all boards) now return without sleeping when no RTC is available or the RTC driver reports that the alarm could not be set. --- src/utility/Power_Class.hpp | 3 ++ src/utility/Power_Class.inl | 25 ++++++++++- src/utility/RTC_Class.hpp | 1 + src/utility/RTC_Class.inl | 3 +- src/utility/rtc/RX8130_Class.hpp | 5 ++- src/utility/rtc/RX8130_Class.inl | 73 +++++++++++++++++++++++++------- 6 files changed, 90 insertions(+), 20 deletions(-) diff --git a/src/utility/Power_Class.hpp b/src/utility/Power_Class.hpp index 68af41bc..bb4d9bf4 100644 --- a/src/utility/Power_Class.hpp +++ b/src/utility/Power_Class.hpp @@ -141,12 +141,14 @@ namespace m5 /// @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. + /// @note On Tab5, returns without sleeping; use timerSleep(time). void timerSleep(int seconds); /// sleep and timer boot. The boot condition can be specified by the argument. /// @param time Time to boot. (only minutes and hours can be specified. Ignore seconds) /// @attention CoreInk and M5Paper can't alarm boot because it can't be turned off while connected to USB. /// @attention CoreInk と M5Paper は USB接続中はRTCタイマー起動が出来ない。; + /// @note Returns without sleeping if no RTC is available or the RTC driver reports that the alarm could not be set. void timerSleep(const rtc_time_t& time); /// sleep and timer boot. The boot condition can be specified by the argument. @@ -154,6 +156,7 @@ namespace m5 /// @param time Time to boot. (only minutes and hours can be specified. Ignore seconds) /// @attention CoreInk and M5Paper can't alarm boot because it can't be turned off while connected to USB. /// @attention CoreInk と M5Paper は USB接続中はRTCタイマー起動が出来ない。; + /// @note Returns without sleeping if no RTC is available or the RTC driver reports that the alarm could not be set. void timerSleep(const rtc_date_t& date, const rtc_time_t& time); /// Value for micro_seconds of deepSleep / lightSleep, meaning "sleep without a timer wakeup". diff --git a/src/utility/Power_Class.inl b/src/utility/Power_Class.inl index cdec4ac3..c69d2e1c 100644 --- a/src/utility/Power_Class.inl +++ b/src/utility/Power_Class.inl @@ -2084,6 +2084,13 @@ namespace m5 void Power_Class::timerSleep( int seconds ) { +#if defined (CONFIG_IDF_TARGET_ESP32P4) + if (M5.getBoard() == board_t::board_M5Tab5) + { + M5_LOGE("timerSleep(seconds) is not supported on Tab5, use timerSleep(time). not sleeping."); + return; + } +#endif M5.Rtc.disableIRQ(); M5.Rtc.clearIRQ(); M5.Rtc.setAlarmIRQ(seconds); @@ -2097,7 +2104,14 @@ namespace m5 { M5.Rtc.disableIRQ(); M5.Rtc.clearIRQ(); - M5.Rtc.setAlarmIRQ(time); + // An alarm setup failure must not enter sleep without an RTC wake source. + if (M5.Rtc.setAlarmIRQ(time) <= 0) + { + M5_LOGE("timerSleep: RTC alarm could not be set. not sleeping."); + M5.Rtc.disableIRQ(); + M5.Rtc.clearIRQ(); + return; + } _timerSleep(); } @@ -2105,7 +2119,14 @@ namespace m5 { M5.Rtc.disableIRQ(); M5.Rtc.clearIRQ(); - M5.Rtc.setAlarmIRQ(date, time); + // An alarm setup failure must not enter sleep without an RTC wake source. + if (M5.Rtc.setAlarmIRQ(date, time) <= 0) + { + M5_LOGE("timerSleep: RTC alarm could not be set. not sleeping."); + M5.Rtc.disableIRQ(); + M5.Rtc.clearIRQ(); + return; + } _timerSleep(); } diff --git a/src/utility/RTC_Class.hpp b/src/utility/RTC_Class.hpp index 5cafa7fb..d9c22439 100644 --- a/src/utility/RTC_Class.hpp +++ b/src/utility/RTC_Class.hpp @@ -44,6 +44,7 @@ namespace m5 /// Set timer IRQ /// @param timer_msec /// @return the set number of msec. 0 == disabled, or the timer could not be set (communication failure). + /// @note The built-in RTC driver on Tab5 rejects positive periods with 0; 0 still requests a stop. std::uint32_t setTimerIRQ(std::uint32_t timer_msec); // deprecated diff --git a/src/utility/RTC_Class.inl b/src/utility/RTC_Class.inl index 0d29f0e0..440908fa 100644 --- a/src/utility/RTC_Class.inl +++ b/src/utility/RTC_Class.inl @@ -58,7 +58,8 @@ namespace m5 { #if defined (CONFIG_IDF_TARGET_ESP32P4) case board_t::board_M5Tab5: // VBAT: supercapacitor through 1 kOhm - instance.reset(new RX8130_Class(RX8130_Class::DEFAULT_ADDRESS, 400000, i2c, true)); + // Periodic timer IRQs are not supported for the built-in RTC on Tab5. + instance.reset(new RX8130_Class(RX8130_Class::DEFAULT_ADDRESS, 400000, i2c, true, false)); break; case board_t::board_M5CoreP4X: diff --git a/src/utility/rtc/RX8130_Class.hpp b/src/utility/rtc/RX8130_Class.hpp index 9ccb7ec3..72490e70 100644 --- a/src/utility/rtc/RX8130_Class.hpp +++ b/src/utility/rtc/RX8130_Class.hpp @@ -15,9 +15,11 @@ namespace m5 /// backup_charge: let the RX8130 charge VBAT from VDD (CHGEN). Only for a rechargeable /// backup (supercapacitor, rechargeable cell); a primary cell such as a CR2032 must not be charged. - RX8130_Class(std::uint8_t i2c_addr = DEFAULT_ADDRESS, std::uint32_t freq = 400000, I2C_Class* i2c = &In_I2C, bool backup_charge = false) + /// timer_irq: allow periodic timer IRQs; false still permits stop requests. + RX8130_Class(std::uint8_t i2c_addr = DEFAULT_ADDRESS, std::uint32_t freq = 400000, I2C_Class* i2c = &In_I2C, bool backup_charge = false, bool timer_irq = true) : RTC_Base ( i2c_addr, freq, i2c ) , _backup_charge ( backup_charge ) + , _timer_irq ( timer_irq ) {} bool begin(I2C_Class* i2c = nullptr) override; @@ -39,6 +41,7 @@ namespace m5 private: bool _backup_charge; + bool _timer_irq; }; } diff --git a/src/utility/rtc/RX8130_Class.inl b/src/utility/rtc/RX8130_Class.inl index 31b15d18..8ef36bc2 100644 --- a/src/utility/rtc/RX8130_Class.inl +++ b/src/utility/rtc/RX8130_Class.inl @@ -10,6 +10,28 @@ namespace m5 { + // flag_clear: W0C mask written to 0x1D once the timer is confirmed stopped + // (0xAF clears TF only, 0xA7 clears TF and AF). + // ctl_bits: the 0x1E bits to clear together with TE (0x10 = TIE, 0x18 = TIE and AIE). + static bool stop_rx8130_timer(RX8130_Class* rtc, std::uint8_t flag_clear, std::uint8_t ctl_bits = 0x10) + { + for (int retry = 0; retry < 3; ++retry) + { + std::uint8_t ext = 0, ctl = 0; + bool te_off = rtc->bitOff(0x1C, 0x10); + bool tie_off = rtc->bitOff(0x1E, ctl_bits); + bool ext_read = rtc->readRegister(0x1C, &ext, 1); + bool ctl_read = rtc->readRegister(0x1E, &ctl, 1); + if (te_off && tie_off && ext_read && ctl_read + && !(ext & 0x10) && !(ctl & ctl_bits) + && rtc->writeRegister8(0x1D, flag_clear)) // W0C: only the selected flags are cleared + { + return true; + } + } + return false; + } + bool RX8130_Class::begin(I2C_Class* i2c) { if (i2c) @@ -26,6 +48,8 @@ namespace m5 res = res && writeRegister8(0x1F, ctrl1); res &= writeRegister8(0x30, 0x00); res &= writeRegister8(0x1E, 0x00); + // Stop the periodic timer; flags are kept for getIRQstatus(). + res &= bitOff(0x1C, 0x10); _init = res; return _init; @@ -115,6 +139,9 @@ namespace m5 std::uint32_t RX8130_Class::setTimerIRQ(std::uint32_t msec) { + // A disallowed positive period follows the same cleanup path as a stop request. + bool timer_refused = msec != 0 && !_timer_irq; + if (timer_refused) { msec = 0; } // Source clocks in the order they are tried. period = mul_ms / div [ms]. // max_ms = 65535 * period, so msec <= max_ms keeps msec * div within uint32. // max_cnt = min(65535, 0xFFFFFFFF / mul_ms), so cnt * mul_ms (the period) stays within uint32. @@ -175,6 +202,9 @@ namespace m5 if (cycle == 0) { stop_timer(); writeRegister8(0x1D, FLAG_CLEAR_TF); + if (timer_refused) { + M5_LOGW("setTimerIRQ: periodic timer IRQ is not supported on this board."); + } return 0; } bool ok = readRegister(0x1C, ®0x1C, 1); @@ -212,6 +242,7 @@ namespace m5 std::uint8_t buf[4] = { 0x80, 0x80, 0x80, 0x00 }; bool irq_enable = false; + int flg_wada = -1; if (time) { if (time->minutes >= 0) { @@ -228,7 +259,6 @@ namespace m5 if (date) { // 0 Sets WEEK as target of alarm function // 1 Sets DAY as target of alarm function - int flg_wada = -1; if (date->date >= 0) { flg_wada = 1; @@ -242,27 +272,39 @@ namespace m5 if (flg_wada >= 0) { irq_enable = true; - if (flg_wada) - { // week alarm / day alarm selector - bitOn(0x1C, 0x08); - } else { - bitOff(0x1C, 0x08); - } } } - // MIN_ALARM_REG 0x17 - writeRegister(0x17, buf, 3); - - if (irq_enable) + // Keep AIE off until WADA and all alarm registers are committed. + // The initial disable is retried: if it never succeeds the old alarm stays armed. + bool disabled = false; + for (int retry = 0; retry < 3 && !disabled; ++retry) { disabled = bitOff(0x1E, 0x08); } + if (!disabled) { return 0; } + if (flg_wada >= 0 + && !(flg_wada ? bitOn(0x1C, 0x08) : bitOff(0x1C, 0x08))) { - bitOn(0x1E, 0x08); + bitOff(0x1E, 0x08); + return 0; } - else + if (!writeRegister(0x17, buf, 3)) { bitOff(0x1E, 0x08); + return 0; + } + // 0x1D is W0C: clear AF for both arming and clearing while retaining TF. + if (!writeRegister8(0x1D, 0xB7)) + { + bitOff(0x1E, 0x08); + return 0; + } + if (irq_enable) + { + if (!bitOn(0x1E, 0x08)) + { + bitOff(0x1E, 0x08); + return 0; + } } - return irq_enable; } @@ -284,8 +326,7 @@ namespace m5 void RX8130_Class::disableIRQ(void) { if (isEnabled()) { - bitOff(0x1E, 0x18); - writeRegister8(0x1D, 0xA7); // W0C: clear TF and AF only + stop_rx8130_timer(this, 0xA7, 0x18); } } From d08e2f62ce3afbdeceedfa7faed871df53b11c78 Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Sun, 25 Oct 2026 00:09:23 +0000 Subject: [PATCH 6/6] Bump version to 0.2.28 --- idf_component.yml | 4 ++-- library.json | 4 ++-- library.properties | 4 ++-- src/gitTagVersion.h | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/idf_component.yml b/idf_component.yml index 1f762144..0369f86c 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.27 +version: 0.2.28 dependencies: m5stack/m5gfx: - version: ">=0.2.31" + version: ">=0.2.35" diff --git a/library.json b/library.json index d38ea172..bff9c054 100644 --- a/library.json +++ b/library.json @@ -13,10 +13,10 @@ "dependencies": [ { "name": "M5GFX", - "version": ">=0.2.31" + "version": ">=0.2.35" } ], - "version": "0.2.27", + "version": "0.2.28", "frameworks": ["arduino", "espidf", "*"], "platforms": ["espressif32", "native"], "headers": "M5Unified.h" diff --git a/library.properties b/library.properties index 960eb3d3..77d02051 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=M5Unified -version=0.2.27 +version=0.2.28 author=M5Stack maintainer=M5Stack sentence=Unified library for M5Stack series @@ -8,4 +8,4 @@ category=Display url=https://github.com/m5stack/M5Unified.git architectures=esp32 includes=M5Unified.h -depends=M5GFX (>=0.2.31) +depends=M5GFX (>=0.2.35) diff --git a/src/gitTagVersion.h b/src/gitTagVersion.h index 535f9e22..0a1eaa36 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 27 +#define M5UNIFIED_VERSION_PATCH 28 #define M5UNIFIED_VERSION F( M5UNIFIED_VERSION_MAJOR "." M5UNIFIED_VERSION_MINOR "." M5UNIFIED_VERSION_PATCH )