diff --git a/idf_component.yml b/idf_component.yml index 1f76214..0369f86 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 d38ea17..bff9c05 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 960eb3d..77d0205 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/M5Unified.hpp b/src/M5Unified.hpp index 325aa69..ed9be7b 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); } } diff --git a/src/gitTagVersion.h b/src/gitTagVersion.h index 535f9e2..0a1eaa3 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 ) diff --git a/src/utility/IMU_Class.inl b/src/utility/IMU_Class.inl index f214f72..ff25549 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) diff --git a/src/utility/Power_Class.hpp b/src/utility/Power_Class.hpp index 68af41b..bb4d9bf 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 cdec4ac..c69d2e1 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 5cafa7f..d9c2243 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 71aa066..440908f 100644 --- a/src/utility/RTC_Class.inl +++ b/src/utility/RTC_Class.inl @@ -57,15 +57,19 @@ namespace m5 switch (board) { #if defined (CONFIG_IDF_TARGET_ESP32P4) + case board_t::board_M5Tab5: // VBAT: supercapacitor through 1 kOhm + // 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: - 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 +79,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/Speaker_Class.inl b/src/utility/Speaker_Class.inl index 05982e4..06cb132 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); } diff --git a/src/utility/rtc/RX8130_Class.hpp b/src/utility/rtc/RX8130_Class.hpp index e1df4b6..72490e7 100644 --- a/src/utility/rtc/RX8130_Class.hpp +++ b/src/utility/rtc/RX8130_Class.hpp @@ -13,8 +13,13 @@ 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. + /// 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; @@ -33,6 +38,10 @@ namespace m5 void disableIRQ(void) override; bool getVoltLow(void) override; + + private: + bool _backup_charge; + bool _timer_irq; }; } diff --git a/src/utility/rtc/RX8130_Class.inl b/src/utility/rtc/RX8130_Class.inl index 8efde73..8ef36bc 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) @@ -18,9 +40,16 @@ 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); + // Stop the periodic timer; flags are kept for getIRQstatus(). + res &= bitOff(0x1C, 0x10); _init = res; return _init; @@ -110,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. @@ -170,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); @@ -207,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) { @@ -223,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; @@ -237,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; } @@ -279,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); } }