From f6bf92e5d001d808bf2ea4b8fe821fd380862118 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Wed, 19 Aug 2026 03:51:06 +0000 Subject: [PATCH 01/11] Clamp SPI clock dividers to their register widths FreqToClockDiv could produce a CLKDIV_PRE value wider than the register field when the requested frequency was far below the base clock, spilling into adjacent fields. Clamp both divider components with the target register definitions so each SoC retains its supported range without corrupting neighboring bits. --- src/lgfx/v1/platforms/esp32/common.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lgfx/v1/platforms/esp32/common.cpp b/src/lgfx/v1/platforms/esp32/common.cpp index 8a944f6..1ff3c8a 100644 --- a/src/lgfx/v1/platforms/esp32/common.cpp +++ b/src/lgfx/v1/platforms/esp32/common.cpp @@ -272,8 +272,8 @@ namespace lgfx { if (fapb <= hz) return SPI_CLK_EQU_SYSCLK; uint32_t div_num = fapb / (1 + hz); - uint32_t pre = div_num / 64u; - div_num = div_num / (pre+1); + uint32_t pre = std::min(div_num / (SPI_CLKCNT_N_V + 1u), SPI_CLKDIV_PRE_V); + div_num = std::min(div_num / (pre+1), SPI_CLKCNT_N_V); return div_num << 12 | ((div_num-1)>>1) << 6 | div_num | pre << 18; } From db4ebea0b900d52e43851afe4c48134964805568 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Wed, 19 Aug 2026 03:51:06 +0000 Subject: [PATCH 02/11] Use the active GPSPI source clock for divider calculations Decode each supported ESP32 target's live GPSPI clock source and pre-divider instead of assuming an 80 MHz APB clock. Acquire the bus before reading that state because the driver may change it, allowing the existing cache to recalculate whenever another owner selects a different source. Unknown selectors use a safe upper bound so divider calculation does not accidentally overspeed the request. --- src/lgfx/v1/platforms/esp32/Bus_SPI.cpp | 9 +- src/lgfx/v1/platforms/esp32/common.cpp | 144 +++++++++++++++++++++++- src/lgfx/v1/platforms/esp32/common.hpp | 1 + 3 files changed, 148 insertions(+), 6 deletions(-) diff --git a/src/lgfx/v1/platforms/esp32/Bus_SPI.cpp b/src/lgfx/v1/platforms/esp32/Bus_SPI.cpp index aa2d0c9..d5e9e63 100644 --- a/src/lgfx/v1/platforms/esp32/Bus_SPI.cpp +++ b/src/lgfx/v1/platforms/esp32/Bus_SPI.cpp @@ -288,7 +288,12 @@ namespace lgfx void Bus_SPI::beginTransaction(void) { //ESP_LOGI("LGFX","Bus_SPI::beginTransaction"); - uint32_t freq_apb = getApbFrequency(); + // Bus acquisition can change the SPI source or its pre-dividers. + if (_cfg.use_lock) + { + spi::beginTransaction(_cfg.spi_host); + } + uint32_t freq_apb = getSpiClockFrequency(_cfg.spi_host); uint32_t clkdiv_write = _clkdiv_write; if (_last_freq_apb != freq_apb) { @@ -323,8 +328,6 @@ namespace lgfx #endif ; - if (_cfg.use_lock) spi::beginTransaction(_cfg.spi_host); - *_spi_user_reg = _user_reg; auto spi_port = _spi_port; (void)spi_port; diff --git a/src/lgfx/v1/platforms/esp32/common.cpp b/src/lgfx/v1/platforms/esp32/common.cpp index 1ff3c8a..41ed701 100644 --- a/src/lgfx/v1/platforms/esp32/common.cpp +++ b/src/lgfx/v1/platforms/esp32/common.cpp @@ -38,6 +38,17 @@ Original Source: #include #include #include +#if defined ( CONFIG_IDF_TARGET_ESP32P4 ) + #include +#elif defined ( CONFIG_IDF_TARGET_ESP32C5 ) || defined ( CONFIG_IDF_TARGET_ESP32C6 ) \ + || defined ( CONFIG_IDF_TARGET_ESP32C61 ) || defined ( CONFIG_IDF_TARGET_ESP32H2 ) + #include +#endif +#if __has_include() && __has_include() + #include + #include + #define LGFX_HAS_ESP_CLK_TREE +#endif #include #include #if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)) @@ -268,6 +279,133 @@ namespace lgfx #endif } + uint32_t getSpiClockFrequency(int spi_host) + { +#if defined ( CONFIG_IDF_TARGET_ESP32 ) || defined ( CONFIG_IDF_TARGET_ESP32S2 ) \ + || !defined ( CONFIG_IDF_TARGET ) + (void)spi_host; + return getApbFrequency(); +#else + const auto get_xtal_frequency = []() -> uint32_t + { + return static_cast(rtc_clk_xtal_freq_get()) * 1000000u; + }; + const auto get_rc_fast_frequency = []() -> uint32_t + { +#if defined ( LGFX_HAS_ESP_CLK_TREE ) && defined ( SOC_MOD_CLK_RC_FAST ) + uint32_t frequency = 0; + if (ESP_OK == esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_RC_FAST + , ESP_CLK_TREE_SRC_FREQ_PRECISION_APPROX, &frequency) && frequency) + { + return frequency; + } +#endif +#if defined ( SOC_CLK_RC_FAST_FREQ_APPROX ) + return SOC_CLK_RC_FAST_FREQ_APPROX; +#else + return 17500000u; +#endif + }; + +#if defined ( CONFIG_IDF_TARGET_ESP32S3 ) || defined ( CONFIG_IDF_TARGET_ESP32C2 ) \ + || defined ( CONFIG_IDF_TARGET_ESP32C3 ) + static_assert(SPI_MST_CLK_SEL_V == 1u, "SPI clock source selector must be one bit"); + // SPI_MST_CLK_SEL is a shifted mask in the older SPI register headers; + // decode with the explicit value mask rather than VALUE_GET_FIELD. + const uint32_t source_sel = (REG_READ(SPI_CLK_GATE_REG(spi_host + 1)) + >> SPI_MST_CLK_SEL_S) & SPI_MST_CLK_SEL_V; + if (source_sel == 0) { return get_xtal_frequency(); } + #if defined ( CONFIG_IDF_TARGET_ESP32C2 ) + return 40000000u; + #else + return 80000000u; // PLL_F80M is independent of CPU/APB frequency scaling. + #endif + +#elif defined ( CONFIG_IDF_TARGET_ESP32C5 ) || defined ( CONFIG_IDF_TARGET_ESP32C61 ) + if (spi_host != SPI2_HOST) { return 160000000u; } + const uint32_t clkm = REG_READ(PCR_SPI2_CLKM_CONF_REG); + const uint32_t source_sel = VALUE_GET_FIELD(clkm, PCR_SPI2_CLKM_SEL); + const uint32_t source_div = VALUE_GET_FIELD(clkm, PCR_SPI2_CLKM_DIV_NUM) + 1u; + uint32_t source_hz; + switch (source_sel) + { + case 0: source_hz = get_xtal_frequency(); break; + case 1: source_hz = 160000000u; break; + case 2: source_hz = get_rc_fast_frequency(); break; +#if defined ( CONFIG_IDF_TARGET_ESP32C5 ) + case 3: source_hz = 120000000u; break; +#endif + default: source_hz = 160000000u; break; // Safe upper bound for an unknown source. + } + return source_hz / source_div; + +#elif defined ( CONFIG_IDF_TARGET_ESP32C6 ) || defined ( CONFIG_IDF_TARGET_ESP32H2 ) + if (spi_host != SPI2_HOST) + { + #if defined ( CONFIG_IDF_TARGET_ESP32C6 ) + return 80000000u; + #else + return 48000000u; + #endif + } + const uint32_t clkm = REG_READ(PCR_SPI2_CLKM_CONF_REG); + const uint32_t source_sel = VALUE_GET_FIELD(clkm, PCR_SPI2_CLKM_SEL); + switch (source_sel) + { + case 0: return get_xtal_frequency(); + #if defined ( CONFIG_IDF_TARGET_ESP32C6 ) + case 1: return 80000000u; + #else + case 1: return 48000000u; + #endif + case 2: return get_rc_fast_frequency(); + default: + #if defined ( CONFIG_IDF_TARGET_ESP32C6 ) + return 80000000u; // Safe upper bound for an unknown source. + #else + return 48000000u; // Safe upper bound for an unknown source. + #endif + } + +#elif defined ( CONFIG_IDF_TARGET_ESP32P4 ) + const uint32_t ctrl116 = REG_READ(HP_SYS_CLKRST_PERI_CLK_CTRL116_REG); + uint32_t source_sel; + uint32_t hs_div; + uint32_t mst_div; + if (spi_host == SPI2_HOST) + { + source_sel = VALUE_GET_FIELD(ctrl116, HP_SYS_CLKRST_REG_GPSPI2_CLK_SRC_SEL); + hs_div = VALUE_GET_FIELD(ctrl116, HP_SYS_CLKRST_REG_GPSPI2_HS_CLK_DIV_NUM); + mst_div = VALUE_GET_FIELD(ctrl116, HP_SYS_CLKRST_REG_GPSPI2_MST_CLK_DIV_NUM); + } + else if (spi_host == SPI3_HOST) + { + const uint32_t ctrl117 = REG_READ(HP_SYS_CLKRST_PERI_CLK_CTRL117_REG); + source_sel = VALUE_GET_FIELD(ctrl116, HP_SYS_CLKRST_REG_GPSPI3_CLK_SRC_SEL); + hs_div = VALUE_GET_FIELD(ctrl117, HP_SYS_CLKRST_REG_GPSPI3_HS_CLK_DIV_NUM); + mst_div = VALUE_GET_FIELD(ctrl117, HP_SYS_CLKRST_REG_GPSPI3_MST_CLK_DIV_NUM); + } + else + { + return getApbFrequency(); + } + + uint32_t source_hz; + switch (source_sel) + { + case 0: source_hz = get_xtal_frequency(); break; + case 1: source_hz = get_rc_fast_frequency(); break; + case 4: source_hz = 480000000u; break; // SPLL + default: source_hz = 480000000u; break; // Safe upper bound for an unknown source. + } + return source_hz / (hs_div + 1) / (mst_div + 1); +#else + (void)spi_host; + return getApbFrequency(); +#endif +#endif + } + uint32_t FreqToClockDiv(uint32_t fapb, uint32_t hz) { if (fapb <= hz) return SPI_CLK_EQU_SYSCLK; @@ -891,7 +1029,9 @@ namespace lgfx } uint32_t spi_port = (spi_host + 1); (void)spi_port; - uint32_t clkdiv = FreqToClockDiv(getApbFrequency(), freq); + // Bus acquisition may select a different source or pre-divider. + beginTransaction(spi_host); + uint32_t clkdiv = FreqToClockDiv(getSpiClockFrequency(spi_host), freq); uint32_t user = SPI_USR_MOSI | SPI_USR_MISO | SPI_DOUTDIN; if (spi_mode == 1 || spi_mode == 2) user |= SPI_CK_OUT_EDGE; @@ -917,8 +1057,6 @@ namespace lgfx #endif ; - beginTransaction(spi_host); - writereg(SPI_USER_REG(spi_port), user); #if defined (SPI_PIN_REG) writereg(SPI_PIN_REG(spi_port), pin); diff --git a/src/lgfx/v1/platforms/esp32/common.hpp b/src/lgfx/v1/platforms/esp32/common.hpp index fa8b4c8..8ce48ad 100644 --- a/src/lgfx/v1/platforms/esp32/common.hpp +++ b/src/lgfx/v1/platforms/esp32/common.hpp @@ -192,6 +192,7 @@ namespace lgfx static inline void gpio_lo(int_fast8_t pin) { if (pin >= 0) *get_gpio_lo_reg(pin) = 1 << (pin & 31); } // ESP_LOGI("LGFX", "gpio_lo: %d", pin); } uint32_t getApbFrequency(void); + uint32_t getSpiClockFrequency(int spi_host); uint32_t FreqToClockDiv(uint32_t fapb, uint32_t hz); /// for I2S and LCD_CAM peripheral clock From 5bce6a2d52828e0ccf165e460735072da83163ae Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Wed, 19 Aug 2026 03:51:06 +0000 Subject: [PATCH 03/11] Select faster GPSPI sources for Arduino transactions On ESP32-C5, C61, C6, and P4 Arduino builds, temporarily select an 80 MHz GPSPI base only when it produces a strictly closer write clock without overspeeding either requested rate. Equal write results keep the current source; read throughput may decrease when write throughput improves. Save and restore only the owned clock fields inside the RCC atomic section, and clean up active ownership when a bus is released or destroyed. ESP-IDF transactions retain driver ownership, and the Arduino mutex does not serialize mixed API users. --- src/lgfx/v1/platforms/esp32/Bus_SPI.cpp | 222 ++++++++++++++++++++++++ src/lgfx/v1/platforms/esp32/Bus_SPI.hpp | 5 + 2 files changed, 227 insertions(+) diff --git a/src/lgfx/v1/platforms/esp32/Bus_SPI.cpp b/src/lgfx/v1/platforms/esp32/Bus_SPI.cpp index d5e9e63..c08fb59 100644 --- a/src/lgfx/v1/platforms/esp32/Bus_SPI.cpp +++ b/src/lgfx/v1/platforms/esp32/Bus_SPI.cpp @@ -39,6 +39,17 @@ Original Source: #include #include +#if defined (ARDUINO) && (defined (CONFIG_IDF_TARGET_ESP32P4) \ + || defined (CONFIG_IDF_TARGET_ESP32C5) || defined (CONFIG_IDF_TARGET_ESP32C6) \ + || defined (CONFIG_IDF_TARGET_ESP32C61)) + #define LGFX_SPI_CLOCK_TAKEOVER + #if defined (CONFIG_IDF_TARGET_ESP32P4) + #include + #else + #include + #endif +#endif + #if __has_include () #include #else @@ -149,6 +160,199 @@ namespace lgfx static __attribute__ ((always_inline)) inline void writereg(uint32_t addr, uint32_t value) { *(volatile uint32_t*)addr = value; } #pragma GCC diagnostic pop +#if defined (LGFX_SPI_CLOCK_TAKEOVER) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-variable" + struct spi_clock_state_t + { + uint32_t saved0 = 0; + uint32_t saved1 = 0; + const Bus_SPI* owner = nullptr; + bool active = false; + }; + + static spi_clock_state_t spi_clock_state[SOC_SPI_PERIPH_NUM]; + + static bool spi_clock_host_supported(int spi_host) + { +#if defined (CONFIG_IDF_TARGET_ESP32P4) + return spi_host == SPI2_HOST || spi_host == SPI3_HOST; +#else + return spi_host == SPI2_HOST; +#endif + } + + static uint32_t spi_clock_output_frequency(uint32_t source_hz, uint32_t requested_hz) + { + const uint32_t clock_div = FreqToClockDiv(source_hz, requested_hz); + if (clock_div & SPI_CLK_EQU_SYSCLK) { return source_hz; } + const uint32_t pre = VALUE_GET_FIELD(clock_div, SPI_CLKDIV_PRE) + 1u; + const uint32_t n = VALUE_GET_FIELD(clock_div, SPI_CLKCNT_N) + 1u; + return source_hz / pre / n; + } + + static uint32_t spi_clock_error(uint32_t actual_hz, uint32_t requested_hz) + { + return actual_hz > requested_hz ? actual_hz - requested_hz : requested_hz - actual_hz; + } + + struct spi_clock_target_t + { + uint32_t base_hz; + uint32_t source_div; + }; + + static spi_clock_target_t spi_clock_find_target(uint32_t requested_hz) + { +#if defined (CONFIG_IDF_TARGET_ESP32C5) || defined (CONFIG_IDF_TARGET_ESP32C61) + (void)requested_hz; + // The 80 MHz root preserves both common write and read rates (for example + // 40 and 16 MHz), while a 40 MHz root would make 16 MHz inexact. + return { 80000000u, 2u }; +#else + (void)requested_hz; + return { 80000000u, 1u }; +#endif + } + + static bool spi_clock_acquire(const Bus_SPI* owner, int spi_host + , uint32_t write_hz, uint32_t read_hz) + { + if (!spi_clock_host_supported(spi_host) || write_hz == 0 || read_hz == 0) { return false; } + + const auto target = spi_clock_find_target(std::max(write_hz, read_hz)); + const uint32_t current_hz = getSpiClockFrequency(spi_host); + const uint32_t current_write = spi_clock_output_frequency(current_hz, write_hz); + const uint32_t target_write = spi_clock_output_frequency(target.base_hz, write_hz); + const uint32_t target_read = spi_clock_output_frequency(target.base_hz, read_hz); + const uint32_t current_write_error = spi_clock_error(current_write, write_hz); + const uint32_t target_write_error = spi_clock_error(target_write, write_hz); + // Prefer write throughput: a slower read clock is acceptable, but neither + // clock may exceed its request. Equal write results leave the current owner + // untouched to avoid an unnecessary source change. + if (target_write > write_hz || target_read > read_hz + || target_write_error >= current_write_error) + { + return false; + } + + auto& state = spi_clock_state[spi_host]; + if (state.active) { return false; } // Host-scoped ownership is not nestable. + + // The Arduino bus mutex does not serialize ESP-IDF SPI driver users on the + // same host; mixing the two APIs cannot provide transaction-wide exclusion. + PERIPH_RCC_ATOMIC() + { +#if defined (CONFIG_IDF_TARGET_ESP32P4) + if (spi_host == SPI2_HOST) + { + constexpr uint32_t mask = HP_SYS_CLKRST_REG_GPSPI2_CLK_SRC_SEL_M + | HP_SYS_CLKRST_REG_GPSPI2_HS_CLK_DIV_NUM_M + | HP_SYS_CLKRST_REG_GPSPI2_MST_CLK_DIV_NUM_M; + constexpr uint32_t target_value = (4u << HP_SYS_CLKRST_REG_GPSPI2_CLK_SRC_SEL_S) + | (2u << HP_SYS_CLKRST_REG_GPSPI2_HS_CLK_DIV_NUM_S) + | (1u << HP_SYS_CLKRST_REG_GPSPI2_MST_CLK_DIV_NUM_S); + const uint32_t current = REG_READ(HP_SYS_CLKRST_PERI_CLK_CTRL116_REG); + state.saved0 = current & mask; + REG_WRITE(HP_SYS_CLKRST_PERI_CLK_CTRL116_REG, (current & ~mask) | target_value); + } + else + { + constexpr uint32_t source_mask = HP_SYS_CLKRST_REG_GPSPI3_CLK_SRC_SEL_M; + constexpr uint32_t source_target = 4u << HP_SYS_CLKRST_REG_GPSPI3_CLK_SRC_SEL_S; + constexpr uint32_t divider_mask = HP_SYS_CLKRST_REG_GPSPI3_HS_CLK_DIV_NUM_M + | HP_SYS_CLKRST_REG_GPSPI3_MST_CLK_DIV_NUM_M; + constexpr uint32_t divider_target = (2u << HP_SYS_CLKRST_REG_GPSPI3_HS_CLK_DIV_NUM_S) + | (1u << HP_SYS_CLKRST_REG_GPSPI3_MST_CLK_DIV_NUM_S); + const uint32_t ctrl116 = REG_READ(HP_SYS_CLKRST_PERI_CLK_CTRL116_REG); + const uint32_t ctrl117 = REG_READ(HP_SYS_CLKRST_PERI_CLK_CTRL117_REG); + state.saved0 = ctrl116 & source_mask; + state.saved1 = ctrl117 & divider_mask; + // Install safe dividers before selecting the 480 MHz source. + REG_WRITE(HP_SYS_CLKRST_PERI_CLK_CTRL117_REG, (ctrl117 & ~divider_mask) | divider_target); + REG_WRITE(HP_SYS_CLKRST_PERI_CLK_CTRL116_REG, (ctrl116 & ~source_mask) | source_target); + } +#elif defined (CONFIG_IDF_TARGET_ESP32C5) || defined (CONFIG_IDF_TARGET_ESP32C61) + constexpr uint32_t mask = PCR_SPI2_CLKM_SEL_M | PCR_SPI2_CLKM_DIV_NUM_M; + const uint32_t target_value = (1u << PCR_SPI2_CLKM_SEL_S) + | ((target.source_div - 1u) << PCR_SPI2_CLKM_DIV_NUM_S); + const uint32_t current = REG_READ(PCR_SPI2_CLKM_CONF_REG); + state.saved0 = current & mask; + REG_WRITE(PCR_SPI2_CLKM_CONF_REG, (current & ~mask) | target_value); +#elif defined (CONFIG_IDF_TARGET_ESP32C6) + constexpr uint32_t mask = PCR_SPI2_CLKM_SEL_M; + constexpr uint32_t target_value = 1u << PCR_SPI2_CLKM_SEL_S; + const uint32_t current = REG_READ(PCR_SPI2_CLKM_CONF_REG); + state.saved0 = current & mask; + REG_WRITE(PCR_SPI2_CLKM_CONF_REG, (current & ~mask) | target_value); +#endif + state.owner = owner; + state.active = true; + } + return true; + } + + static bool spi_clock_owned_by(const Bus_SPI* owner, int spi_host) + { + if (!spi_clock_host_supported(spi_host)) { return false; } + const auto& state = spi_clock_state[spi_host]; + return state.active && state.owner == owner; + } + + static bool spi_clock_restore(const Bus_SPI* owner, int spi_host) + { + if (!spi_clock_host_supported(spi_host)) { return false; } + + auto& state = spi_clock_state[spi_host]; + if (!state.active || state.owner != owner) { return false; } + + PERIPH_RCC_ATOMIC() + { +#if defined (CONFIG_IDF_TARGET_ESP32P4) + if (spi_host == SPI2_HOST) + { + constexpr uint32_t mask = HP_SYS_CLKRST_REG_GPSPI2_CLK_SRC_SEL_M + | HP_SYS_CLKRST_REG_GPSPI2_HS_CLK_DIV_NUM_M + | HP_SYS_CLKRST_REG_GPSPI2_MST_CLK_DIV_NUM_M; + const uint32_t current = REG_READ(HP_SYS_CLKRST_PERI_CLK_CTRL116_REG); + REG_WRITE(HP_SYS_CLKRST_PERI_CLK_CTRL116_REG, (current & ~mask) | state.saved0); + } + else + { + constexpr uint32_t source_mask = HP_SYS_CLKRST_REG_GPSPI3_CLK_SRC_SEL_M; + constexpr uint32_t divider_mask = HP_SYS_CLKRST_REG_GPSPI3_HS_CLK_DIV_NUM_M + | HP_SYS_CLKRST_REG_GPSPI3_MST_CLK_DIV_NUM_M; + const uint32_t ctrl116 = REG_READ(HP_SYS_CLKRST_PERI_CLK_CTRL116_REG); + const uint32_t ctrl117 = REG_READ(HP_SYS_CLKRST_PERI_CLK_CTRL117_REG); + // Leave the 480 MHz source before restoring potentially smaller dividers. + REG_WRITE(HP_SYS_CLKRST_PERI_CLK_CTRL116_REG, (ctrl116 & ~source_mask) | state.saved0); + REG_WRITE(HP_SYS_CLKRST_PERI_CLK_CTRL117_REG, (ctrl117 & ~divider_mask) | state.saved1); + } +#elif defined (CONFIG_IDF_TARGET_ESP32C5) || defined (CONFIG_IDF_TARGET_ESP32C61) + constexpr uint32_t mask = PCR_SPI2_CLKM_SEL_M | PCR_SPI2_CLKM_DIV_NUM_M; + const uint32_t current = REG_READ(PCR_SPI2_CLKM_CONF_REG); + REG_WRITE(PCR_SPI2_CLKM_CONF_REG, (current & ~mask) | state.saved0); +#elif defined (CONFIG_IDF_TARGET_ESP32C6) + constexpr uint32_t mask = PCR_SPI2_CLKM_SEL_M; + const uint32_t current = REG_READ(PCR_SPI2_CLKM_CONF_REG); + REG_WRITE(PCR_SPI2_CLKM_CONF_REG, (current & ~mask) | state.saved0); +#endif + state.active = false; + state.owner = nullptr; + } + return true; + } +#pragma GCC diagnostic pop + + Bus_SPI::~Bus_SPI(void) + { + if (spi_clock_owned_by(this, _cfg.spi_host)) + { + release(); + } + } +#endif + void Bus_SPI::config(const config_t& cfg) { _cfg = cfg; @@ -262,6 +466,18 @@ namespace lgfx void Bus_SPI::release(void) { //ESP_LOGI("LGFX","Bus_SPI::release"); +#if defined (LGFX_SPI_CLOCK_TAKEOVER) + if (spi_clock_owned_by(this, _cfg.spi_host)) + { + // An active takeover implies this instance still owns the Arduino bus + // mutex. Finish the transfer and restore the host before releasing it. + dc_control(true); + if (spi_clock_restore(this, _cfg.spi_host)) + { + spi::endTransaction(_cfg.spi_host); + } + } +#endif if (!_inited) return; _inited = false; spi::release(_cfg.spi_host); @@ -292,6 +508,9 @@ namespace lgfx if (_cfg.use_lock) { spi::beginTransaction(_cfg.spi_host); +#if defined (LGFX_SPI_CLOCK_TAKEOVER) + spi_clock_acquire(this, _cfg.spi_host, _cfg.freq_write, _cfg.freq_read); +#endif } uint32_t freq_apb = getSpiClockFrequency(_cfg.spi_host); uint32_t clkdiv_write = _clkdiv_write; @@ -343,6 +562,9 @@ namespace lgfx dc_control(true); #if defined ( LGFX_SPIDMA_WORKAROUND ) if (_dma_ch) { spicommon_dmaworkaround_idle(_dma_ch); } +#endif +#if defined (LGFX_SPI_CLOCK_TAKEOVER) + if (_cfg.use_lock) { spi_clock_restore(this, _cfg.spi_host); } #endif if (_cfg.use_lock) spi::endTransaction(_cfg.spi_host); #if defined (ARDUINO) // Arduino ESP32 diff --git a/src/lgfx/v1/platforms/esp32/Bus_SPI.hpp b/src/lgfx/v1/platforms/esp32/Bus_SPI.hpp index 996c826..c144b7b 100644 --- a/src/lgfx/v1/platforms/esp32/Bus_SPI.hpp +++ b/src/lgfx/v1/platforms/esp32/Bus_SPI.hpp @@ -103,6 +103,11 @@ namespace lgfx }; constexpr Bus_SPI(void) = default; +#if defined (ARDUINO) && (defined (CONFIG_IDF_TARGET_ESP32P4) \ + || defined (CONFIG_IDF_TARGET_ESP32C5) || defined (CONFIG_IDF_TARGET_ESP32C6) \ + || defined (CONFIG_IDF_TARGET_ESP32C61)) + ~Bus_SPI(void) override; +#endif const config_t& config(void) const { return _cfg; } From 597a108b587c13cad6194ac93fddfdcf681cf9ad Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Wed, 19 Aug 2026 12:39:59 +0000 Subject: [PATCH 04/11] Hold the data line until the clock is actually down The software I2C moved SDA in the same breath as pulling SCL low. A data line that changes while the clock still reads high is a start or a stop to every device on the bus, so the transfer ended instead of carrying a bit and no device ever acknowledged its address. Wait for each line to reach the level it was just given, rather than budgeting a fixed time for it: the clock is driven low, so waiting for it costs the fall time of the bus and nothing more, and the released data line is given the time its own rise actually takes. A fixed hold would have to come out of the setup time, which is what the slow rise of a released line needs at the higher clock rates. The same wait now precedes the start, the repeated start and the stop, where the rise of the released data line decides whether the condition appears on the bus at all. A line that never reaches its level is reported the way a clock that will not rise already was, so the transfer ends instead of carrying on with a bus that is not there. The recovery path is the one exception: a clock that will not settle is the condition it exists to clear. An acknowledge needs one more distinction. This master drives the data line low for the bit before it, so a low reading is either a device holding the line or a rise that has not finished. A device holds it for the whole pulse while a rise is over within the time the bus is allowed to take for one, so the level is read again to separate them. --- src/lgfx/v1/platforms/soft_i2c.inl | 140 ++++++++++++++++++++++++----- 1 file changed, 116 insertions(+), 24 deletions(-) diff --git a/src/lgfx/v1/platforms/soft_i2c.inl b/src/lgfx/v1/platforms/soft_i2c.inl index 8752e20..1c6f85d 100644 --- a/src/lgfx/v1/platforms/soft_i2c.inl +++ b/src/lgfx/v1/platforms/soft_i2c.inl @@ -34,6 +34,11 @@ Contributors: #error "soft_i2c.inl is an implementation fragment of the i2c namespace; it cannot be included directly." #endif +// Port -1 is the probe slot. Both M5GFX board autodetection and the M5Unified +// board check open it, and they run one after the other, so whoever calls init +// on it must call release before returning. Nothing enforces that; keep new +// users of the negative ports on -2, which no library takes. +// // An I2C line is only ever driven low or released, never driven high. The // default implementation releases by turning the pin back into an input and // drives by turning it into an output whose latch was parked low, which works @@ -90,6 +95,10 @@ Contributors: static inline bool soft_i2c_valid_port(int i2c_port) { return -soft_i2c_port_count <= i2c_port && i2c_port < 0; } static inline soft_i2c_context_t& soft_i2c_ctx(int i2c_port) { return soft_i2c_context[~i2c_port]; } + /// Spin limit for the settle waits. A line that never reaches its level is + /// handled by the checks that follow, not by spinning here forever. + static constexpr size_t soft_i2c_settle_guard = 4096; + static inline void soft_i2c_half_wait(uint32_t half_us) { if (half_us) { delayMicroseconds(half_us); } } @@ -101,6 +110,47 @@ Contributors: ctx.half_us = (freq >= 500000) ? 0 : (500000 + freq - 1) / freq; } + /// Drive SCL low and wait until the line actually reads low. + /// The data line may only move once the clock is under its low threshold: + /// a data change while the clock still reads high is a start or a stop to + /// every device on the bus, which ends the transfer instead of carrying a + /// bit. The clock is driven low rather than released, so waiting for it + /// costs the fall time of this bus and nothing more - a fixed hold would + /// instead have to come out of the setup time, which is exactly what the + /// slow rise of a released data line needs at the higher clock rates. + /// @return false when the clock never reached its low level. Reported the + /// same way as a clock that will not rise: carrying on regardless would + /// move the data line while the clock still reads high, which is the very + /// thing this wait exists to prevent. + static inline bool soft_i2c_scl_lo(const soft_i2c_context_t& ctx) + { + SOFT_I2C_LINE_LO(ctx.pin_scl); + size_t guard = 0; + while (gpio_in(ctx.pin_scl)) + { + if (++guard >= soft_i2c_settle_guard) { return false; } + } + return true; + } + + /// Release SDA and wait for the pullup to carry it high. + /// Only for the data bits: it is the rise that is slow, and giving it the + /// time it actually takes keeps the setup time intact where a fixed wait + /// would fall short on a loaded bus. Not for the acknowledge, where the + /// device holds the line low on purpose. + /// @return false when the data line stayed low. Something else is holding + /// it, so the bit about to be clocked out would not be the bit intended. + static inline bool soft_i2c_sda_hi(const soft_i2c_context_t& ctx) + { + SOFT_I2C_LINE_HI(ctx.pin_sda); + size_t guard = 0; + while (!gpio_in(ctx.pin_sda)) + { + if (++guard >= soft_i2c_settle_guard) { return false; } + } + return true; + } + /// Release SCL and wait for it to actually rise, honoring clock stretching. static inline bool soft_i2c_scl_hi(const soft_i2c_context_t& ctx) { @@ -116,25 +166,41 @@ Contributors: return false; } - /// Returns true when the byte was acknowledged. + /// Returns true when the byte was acknowledged and the clock could be taken + /// low again afterwards. A clock that will not settle is reported the same + /// way as a missing acknowledge, which ends the transfer either way. static inline bool soft_i2c_write_byte(const soft_i2c_context_t& ctx, uint8_t data) { size_t i = 0; do { - SOFT_I2C_LINE_LO(ctx.pin_scl); - if (data & 0x80) { SOFT_I2C_LINE_HI(ctx.pin_sda); } else { SOFT_I2C_LINE_LO(ctx.pin_sda); } + if (!soft_i2c_scl_lo(ctx)) { return false; } + if (data & 0x80) { if (!soft_i2c_sda_hi(ctx)) { return false; } } + else { SOFT_I2C_LINE_LO(ctx.pin_sda); } data <<= 1; soft_i2c_half_wait(ctx.half_us); if (!soft_i2c_scl_hi(ctx)) { return false; } } while (++i < 8); - SOFT_I2C_LINE_LO(ctx.pin_scl); + if (!soft_i2c_scl_lo(ctx)) { return false; } SOFT_I2C_LINE_HI(ctx.pin_sda); // release the data line for the acknowledge soft_i2c_half_wait(ctx.half_us); if (!soft_i2c_scl_hi(ctx)) { return false; } bool ack = !gpio_in(ctx.pin_sda); - SOFT_I2C_LINE_LO(ctx.pin_scl); - return ack; + if (ack) + { // This master drove the data line low for the bit before the + // acknowledge, so a low here is either a device holding the line or a + // rise that has not finished. A device holds it for the whole pulse, + // while a rise is over within the time the bus is allowed to take for + // one ( 1us for the slowest mode ), so looking again separates them. + // A bus slower than the specification allows is read as an acknowledge + // that is not there; the limit is the specified rise time, not a + // measurement of this bus. + auto us = micros(); + while (!gpio_in(ctx.pin_sda) && (micros() - us) <= 1) {} + ack = !gpio_in(ctx.pin_sda); + } + bool low = soft_i2c_scl_lo(ctx); + return ack && low; } static inline bool soft_i2c_read_byte(const soft_i2c_context_t& ctx, uint8_t* data, bool ack) @@ -144,16 +210,17 @@ Contributors: size_t i = 0; do { - SOFT_I2C_LINE_LO(ctx.pin_scl); + if (!soft_i2c_scl_lo(ctx)) { return false; } soft_i2c_half_wait(ctx.half_us); if (!soft_i2c_scl_hi(ctx)) { return false; } byte = (byte << 1) + (gpio_in(ctx.pin_sda) ? 1 : 0); } while (++i < 8); - SOFT_I2C_LINE_LO(ctx.pin_scl); - if (ack) { SOFT_I2C_LINE_LO(ctx.pin_sda); } else { SOFT_I2C_LINE_HI(ctx.pin_sda); } + if (!soft_i2c_scl_lo(ctx)) { return false; } + if (ack) { SOFT_I2C_LINE_LO(ctx.pin_sda); } + else { if (!soft_i2c_sda_hi(ctx)) { return false; } } soft_i2c_half_wait(ctx.half_us); if (!soft_i2c_scl_hi(ctx)) { return false; } - SOFT_I2C_LINE_LO(ctx.pin_scl); + if (!soft_i2c_scl_lo(ctx)) { return false; } SOFT_I2C_LINE_HI(ctx.pin_sda); *data = byte; return true; @@ -162,12 +229,16 @@ Contributors: /// Returns false when the clock could not be released for the stop condition. static inline bool soft_i2c_stop_cond(const soft_i2c_context_t& ctx) { - SOFT_I2C_LINE_LO(ctx.pin_scl); - SOFT_I2C_LINE_LO(ctx.pin_sda); + bool low = soft_i2c_scl_lo(ctx); + // Taking the data line low while the clock is still high is a start, not + // the beginning of a stop, so it is only done once the clock is down. + if (low) { SOFT_I2C_LINE_LO(ctx.pin_sda); } soft_i2c_half_wait(ctx.half_us); - bool ok = soft_i2c_scl_hi(ctx); + bool ok = soft_i2c_scl_hi(ctx) && low; soft_i2c_half_wait(ctx.half_us); - SOFT_I2C_LINE_HI(ctx.pin_sda); + // The stop is the rise of the data line while the clock is high, so it + // is not made until the line has actually risen. + ok = soft_i2c_sda_hi(ctx) && ok; soft_i2c_half_wait(ctx.half_us); return ok; } @@ -179,8 +250,9 @@ Contributors: SOFT_I2C_LINE_HI(ctx.pin_sda); size_t i = 0; while (!gpio_in(ctx.pin_sda) && ++i <= 9) - { - SOFT_I2C_LINE_LO(ctx.pin_scl); + { // this is the attempt to free a stuck bus: a clock that will not settle + // is the condition being recovered from, so it does not end the loop. + (void)soft_i2c_scl_lo(ctx); soft_i2c_half_wait(ctx.half_us); soft_i2c_scl_hi(ctx); } @@ -210,15 +282,17 @@ Contributors: && soft_i2c_write_byte(ctx, i2c_addr & 0xFF); if (ack && read) { // A 10 bit read re-addresses the high byte in read mode. - SOFT_I2C_LINE_HI(ctx.pin_sda); + // The repeated start needs the data line to be high first, the same + // as the first start does. + ack = soft_i2c_sda_hi(ctx); soft_i2c_half_wait(ctx.half_us); - ack = soft_i2c_scl_hi(ctx); + ack = ack && soft_i2c_scl_hi(ctx); if (ack) { SOFT_I2C_LINE_LO(ctx.pin_sda); soft_i2c_half_wait(ctx.half_us); - SOFT_I2C_LINE_LO(ctx.pin_scl); - ack = soft_i2c_write_byte(ctx, 0xF0 | (i2c_addr >> 8) << 1 | 1); + ack = soft_i2c_scl_lo(ctx) + && soft_i2c_write_byte(ctx, 0xF0 | (i2c_addr >> 8) << 1 | 1); } } } @@ -305,7 +379,9 @@ Contributors: SOFT_I2C_LOCK(ctx); ctx.state = soft_i2c_context_t::state_t::state_disconnect; soft_i2c_set_freq(ctx, freq); - SOFT_I2C_LINE_HI(ctx.pin_sda); + // Wait for the release to take effect before reading the line below: + // a rise still in progress is not a bus that someone else is holding. + (void)soft_i2c_sda_hi(ctx); if (!soft_i2c_scl_hi(ctx)) { // The clock never rose: no start condition can be made on this bus. soft_i2c_abort(ctx); @@ -323,7 +399,11 @@ Contributors: } SOFT_I2C_LINE_LO(ctx.pin_sda); // start condition soft_i2c_half_wait(ctx.half_us); - SOFT_I2C_LINE_LO(ctx.pin_scl); + if (!soft_i2c_scl_lo(ctx)) + { + soft_i2c_abort(ctx); + return {}; + } return soft_i2c_send_address(ctx, i2c_addr, read); } @@ -338,7 +418,15 @@ Contributors: return cpp::fail(error_t::mode_mismatch); } soft_i2c_set_freq(ctx, freq); - SOFT_I2C_LINE_HI(ctx.pin_sda); // repeated start + // Unlike the first start there is no independent recheck below, so the + // release is judged here: without the data line going high first, the + // falling edge that makes the repeated start never appears on the bus + // and the address that follows is sent into a frame no one opened. + if (!soft_i2c_sda_hi(ctx)) + { + soft_i2c_abort(ctx); + return {}; + } soft_i2c_half_wait(ctx.half_us); if (!soft_i2c_scl_hi(ctx)) { @@ -347,7 +435,11 @@ Contributors: } SOFT_I2C_LINE_LO(ctx.pin_sda); soft_i2c_half_wait(ctx.half_us); - SOFT_I2C_LINE_LO(ctx.pin_scl); + if (!soft_i2c_scl_lo(ctx)) + { + soft_i2c_abort(ctx); + return {}; + } return soft_i2c_send_address(ctx, i2c_addr, read); } From 6f1bd4768336e7f10dce5374a8aba46d82e7247e Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Wed, 19 Aug 2026 22:34:31 +0000 Subject: [PATCH 05/11] Put the output latch back with the rest of the pin A backup restored the mux, the routing and the output enable, but not the level the pin was driving. A pin that had been held low came back driving high, because whoever borrowed it left the latch there - releasing an open drain line means letting the latch go high, and every probe ends that way. Restoring in place was not enough on its own: putting the pad configuration back can turn an open drain output into a push-pull one while the enable is still set and the latch is still high, and the pin drives that high before the latch is ever reached. So the output is taken down first, then the latch goes back, then the configuration, and the pin is only enabled again at the end if that is how it was found. The enable is set and cleared through its own registers rather than read back and written, so a pin being restored elsewhere is not caught in between. --- src/lgfx/v1/platforms/esp32/common.cpp | 41 +++++++++++++++----------- src/lgfx/v1/platforms/esp32/common.hpp | 1 + 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/lgfx/v1/platforms/esp32/common.cpp b/src/lgfx/v1/platforms/esp32/common.cpp index 41ed701..aeac241 100644 --- a/src/lgfx/v1/platforms/esp32/common.cpp +++ b/src/lgfx/v1/platforms/esp32/common.cpp @@ -628,8 +628,10 @@ namespace lgfx _gpio_func_out_reg = *reinterpret_cast(GPIO_FUNC0_OUT_SEL_CFG_REG + (pin_num * 4)); #if defined ( GPIO_ENABLE1_REG ) _gpio_enable = *reinterpret_cast(((pin_num & 32) ? GPIO_ENABLE1_REG : GPIO_ENABLE_REG)) & (1 << (pin_num & 31)); + _gpio_out = *reinterpret_cast(((pin_num & 32) ? GPIO_OUT1_REG : GPIO_OUT_REG )) & (1 << (pin_num & 31)); #else _gpio_enable = *reinterpret_cast(GPIO_ENABLE_REG) & (1 << (pin_num & 31)); + _gpio_out = *reinterpret_cast(GPIO_OUT_REG ) & (1 << (pin_num & 31)); #endif _in_func_num = -1; @@ -655,6 +657,22 @@ namespace lgfx auto pin_num = (size_t)_pin_num; if (pin_num < GPIO_NUM_MAX) { + uint32_t pin_mask = 1 << (pin_num & 31); +#if defined ( GPIO_ENABLE1_REG ) + auto gpio_enable_w1ts = reinterpret_cast((pin_num & 32) ? GPIO_ENABLE1_W1TS_REG : GPIO_ENABLE_W1TS_REG); + auto gpio_enable_w1tc = reinterpret_cast((pin_num & 32) ? GPIO_ENABLE1_W1TC_REG : GPIO_ENABLE_W1TC_REG); +#else + auto gpio_enable_w1ts = reinterpret_cast(GPIO_ENABLE_W1TS_REG); + auto gpio_enable_w1tc = reinterpret_cast(GPIO_ENABLE_W1TC_REG); +#endif + // Stop driving before anything else changes what driving would mean. + // Restoring the pad configuration can turn an open drain output back + // into a push-pull one, and the latch left behind by whoever borrowed + // the pin is usually high - the pin would drive that high for as long + // as it takes to reach the latch below. + *gpio_enable_w1tc = pin_mask; + *(_gpio_out ? get_gpio_hi_reg(_pin_num) : get_gpio_lo_reg(_pin_num)) = pin_mask; + if ((uint16_t)_in_func_num < 256) { GPIO.func_in_sel_cfg[_in_func_num].val = _gpio_func_in_reg; // ESP_LOGD("DEBUG","pin:%d in_func_num:%d", (int)pin_num, (int)_in_func_num); @@ -668,24 +686,11 @@ namespace lgfx *reinterpret_cast(GPIO_PIN0_REG + (pin_num * 4)) = _gpio_pin_reg; *reinterpret_cast(GPIO_FUNC0_OUT_SEL_CFG_REG + (pin_num * 4)) = _gpio_func_out_reg; -#if defined ( GPIO_ENABLE1_REG ) - auto gpio_enable_reg = reinterpret_cast(((pin_num & 32) ? GPIO_ENABLE1_REG : GPIO_ENABLE_REG)); -#else - auto gpio_enable_reg = reinterpret_cast(GPIO_ENABLE_REG); -#endif - - uint32_t pin_mask = 1 << (pin_num & 31); - uint32_t val = *gpio_enable_reg; - // ESP_LOGD("DEBUG","restore GPIO_ENABLE_REG:%08x", (int)*gpio_enable_reg); - if (_gpio_enable) - { - val |= pin_mask; - } - else - { - val &= ~pin_mask; - } - *gpio_enable_reg = val; + // The pin drives again only once it is configured and holding the level + // it held before. Set and clear go through their own registers so a pin + // being restored on another core is not caught in a read-modify-write. + if (_gpio_enable) { *gpio_enable_w1ts = pin_mask; } + else { *gpio_enable_w1tc = pin_mask; } } } diff --git a/src/lgfx/v1/platforms/esp32/common.hpp b/src/lgfx/v1/platforms/esp32/common.hpp index 8ce48ad..b8c31f4 100644 --- a/src/lgfx/v1/platforms/esp32/common.hpp +++ b/src/lgfx/v1/platforms/esp32/common.hpp @@ -322,6 +322,7 @@ namespace lgfx int16_t _in_func_num = -1; int8_t _pin_num = -1; //GPIO_NUM_NC bool _gpio_enable; + bool _gpio_out; }; enum command_t : uint8_t From fe98dddb2ab32ed06c6c91dae4fe32ea5f35813d Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Wed, 19 Aug 2026 22:34:41 +0000 Subject: [PATCH 06/11] Say which negative ports the libraries take The note told a sketch to use port -2 because no library takes it. Board identification in M5Unified takes it, so the advice would have handed a sketch the slot a probe is about to reopen on other pins - and a transfer does not check that the slot is still the one it was given, so the wrong pins would move with no error to show for it. Describe what actually happens instead: both slots are borrowed while a board is being brought up, and a sketch that wants one should open it afterwards. --- src/lgfx/v1/platforms/soft_i2c.inl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/lgfx/v1/platforms/soft_i2c.inl b/src/lgfx/v1/platforms/soft_i2c.inl index 1c6f85d..9ef168c 100644 --- a/src/lgfx/v1/platforms/soft_i2c.inl +++ b/src/lgfx/v1/platforms/soft_i2c.inl @@ -34,10 +34,13 @@ Contributors: #error "soft_i2c.inl is an implementation fragment of the i2c namespace; it cannot be included directly." #endif -// Port -1 is the probe slot. Both M5GFX board autodetection and the M5Unified -// board check open it, and they run one after the other, so whoever calls init -// on it must call release before returning. Nothing enforces that; keep new -// users of the negative ports on -2, which no library takes. +// The negative ports are shared while a board is being brought up: M5GFX board +// autodetection opens -1 and the M5Unified board check opens -2, both from +// inside begin(). Neither slot carries ownership - opening one takes over the +// pins the last user left behind, and a transfer does not check whether the +// slot is still the one it was given - so a sketch that wants a bus of its own +// should open it once begin() is done, and open it again if it runs detection +// a second time. // // An I2C line is only ever driven low or released, never driven high. The // default implementation releases by turning the pin back into an input and From bfb84f20aa5aeadfb32ab7f494f1b7ed27c39104 Mon Sep 17 00:00:00 2001 From: luoweiyuan Date: Thu, 20 Aug 2026 11:38:46 +0800 Subject: [PATCH 07/11] Add M5Tab5X display support Tab5X upgrades the ESP32-P4 silicon revision used by Tab5. --- src/M5GFX.cpp | 10 +++++++--- src/lgfx/boards.hpp | 1 + src/lgfx/v1/platforms/esp32p4/Bus_DSI.cpp | 3 +-- src/picture_frame/picture_frame.h | 3 ++- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index f6f3bac..cfd1cd0 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -2921,7 +2921,7 @@ The usage of each pin is as follows. if (pkg_ver == 0) // pkg_ver == EFUSE_RD_CHIP_VER_PKG_ { - if (board == 0 || board == board_t::board_M5Tab5) + if (board == 0 || board == board_t::board_M5Tab5 || board == board_t::board_M5Tab5X) { // SDA = GPIO_NUM_31 // SCL = GPIO_NUM_32 @@ -2934,8 +2934,10 @@ The usage of each pin is as follows. id = lgfx::i2c::readRegister8(probe_i2c_port, pi4io1_i2c_addr, 0x01).has_value() && lgfx::i2c::readRegister8(probe_i2c_port, pi4io2_i2c_addr, 0x01).has_value(); if (id != 0) { - board = board_t::board_M5Tab5; - ESP_LOGI(LIBRARY_NAME, "[Autodetect] board_M5Tab5"); + if (board == 0) + board = board_t::board_M5Tab5; + ESP_LOGI(LIBRARY_NAME, "[Autodetect] %s", + board == board_t::board_M5Tab5X ? "board_M5Tab5X" : "board_M5Tab5"); static constexpr const uint8_t reg_data_io1_1[] = { 0x03, 0b01111111, 0, // PI4IO_REG_IO_DIR @@ -3717,6 +3719,7 @@ The usage of each pin is as follows. case board_M5VAMeter: title = "M5VAMeter"; break; case board_M5StampPLC: title = "M5StampPLC"; break; case board_M5Tab5: title = "M5Tab5"; break; + case board_M5Tab5X: title = "M5Tab5X"; break; case board_M5UnitPoEP4: title = "M5UnitPoEP4"; break; case board_ArduinoNessoN1: title = "ArduinoNessoN1"; break; default: title = "M5GFX"; break; @@ -3809,6 +3812,7 @@ The usage of each pin is as follows. break; case board_M5Tab5: + case board_M5Tab5X: w = 720; h = 1280; break; diff --git a/src/lgfx/boards.hpp b/src/lgfx/boards.hpp index 8bb8a0a..88a3bda 100644 --- a/src/lgfx/boards.hpp +++ b/src/lgfx/boards.hpp @@ -43,6 +43,7 @@ namespace lgfx // This should not be changed to "m5gfx" , board_M5ChainCaptain = 32 , board_M5ToughC5 = 33 , board_M5PaperDIY = 34 + , board_M5Tab5X = 35 /// non display boards , board_M5AtomLite = 128 diff --git a/src/lgfx/v1/platforms/esp32p4/Bus_DSI.cpp b/src/lgfx/v1/platforms/esp32p4/Bus_DSI.cpp index 3936d74..98edfbd 100644 --- a/src/lgfx/v1/platforms/esp32p4/Bus_DSI.cpp +++ b/src/lgfx/v1/platforms/esp32p4/Bus_DSI.cpp @@ -31,10 +31,9 @@ namespace lgfx if (_mipi_dsi_bus) { return true; } - esp_lcd_dsi_bus_config_t bus_config; + esp_lcd_dsi_bus_config_t bus_config = {}; bus_config.bus_id = _cfg.bus_id; bus_config.num_data_lanes = _cfg.lane_num; - bus_config.phy_clk_src = static_cast(MIPI_DSI_PHY_CLK_SRC_DEFAULT); bus_config.lane_bit_rate_mbps = _cfg.lane_mbps; esp_ldo_channel_config_t ldo_cfg; diff --git a/src/picture_frame/picture_frame.h b/src/picture_frame/picture_frame.h index f09400b..27f34ba 100644 --- a/src/picture_frame/picture_frame.h +++ b/src/picture_frame/picture_frame.h @@ -42,7 +42,8 @@ namespace m5gfx case board_M5StickCPlus: return &picture_frame_M5StickCPlus; case board_M5StickCPlus2: return &picture_frame_M5StickCPlus2; case board_M5Dial: return &picture_frame_M5Dial; - case board_M5Tab5: return &picture_frame_M5Tab5; + case board_M5Tab5: + case board_M5Tab5X: return &picture_frame_M5Tab5; default: return nullptr; } } From 0313d2617453ff2835d4a8d4d3debf193fee1b9e Mon Sep 17 00:00:00 2001 From: luoweiyuan Date: Fri, 21 Aug 2026 11:20:31 +0800 Subject: [PATCH 08/11] Add M5Stack CoreP4X display support 1. Detect CoreP4X on the internal I2C bus using GPIO11 (SDA) and GPIO9 (SCL), with M5IOE1 at 0x4F and M5PM1 at 0x6E. 2. Add ST7102 MIPI DSI display and Touch_CST3530 touch support. --- CMakeLists.txt | 18 ++- src/M5GFX.cpp | 141 ++++++++++++++++++ src/lgfx/v1/platforms/esp32p4/Bus_DSI.cpp | 2 +- src/lgfx/v1/platforms/esp32p4/Panel_DSI.cpp | 44 ++++-- src/lgfx/v1/platforms/esp32p4/Panel_DSI.hpp | 3 + .../v1/platforms/esp32p4/Panel_ST7102.hpp | 118 +++++++++++++++ src/lgfx/v1/touch/Touch_CSTxxx.cpp | 96 ++++++++++++ src/lgfx/v1/touch/Touch_CSTxxx.hpp | 28 ++++ 8 files changed, 431 insertions(+), 19 deletions(-) create mode 100644 src/lgfx/v1/platforms/esp32p4/Panel_ST7102.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c349e0b..d4b121e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,12 +10,22 @@ file(GLOB SRCS src/lgfx/v1/misc/*.cpp src/lgfx/v1/panel/*.cpp src/lgfx/v1/platforms/esp32/*.cpp - src/lgfx/v1/platforms/esp32c3/*.cpp - src/lgfx/v1/platforms/esp32s2/*.cpp - src/lgfx/v1/platforms/esp32s3/*.cpp - src/lgfx/v1/platforms/esp32p4/*.cpp src/lgfx/v1/touch/*.cpp ) + +# Keep common ESP32 sources, then add only the active chip's implementation. +# This also prevents PlatformIO's flat object layout from colliding on same-named +# sources that live in different platform directories. +if(IDF_TARGET STREQUAL "esp32c3") + file(GLOB PLATFORM_SRCS src/lgfx/v1/platforms/esp32c3/*.cpp) +elseif(IDF_TARGET STREQUAL "esp32s2") + file(GLOB PLATFORM_SRCS src/lgfx/v1/platforms/esp32s2/*.cpp) +elseif(IDF_TARGET STREQUAL "esp32s3") + file(GLOB PLATFORM_SRCS src/lgfx/v1/platforms/esp32s3/*.cpp) +elseif(IDF_TARGET STREQUAL "esp32p4") + file(GLOB PLATFORM_SRCS src/lgfx/v1/platforms/esp32p4/*.cpp) +endif() +list(APPEND SRCS ${PLATFORM_SRCS}) set(COMPONENT_SRCS ${SRCS}) if (IDF_VERSION_MAJOR GREATER_EQUAL 6) diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index cfd1cd0..a64627e 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -35,6 +35,7 @@ #include "lgfx/v1/platforms/esp32p4/Bus_DSI.hpp" #include "lgfx/v1/platforms/esp32p4/Panel_ILI9881C.hpp" +#include "lgfx/v1/platforms/esp32p4/Panel_ST7102.hpp" #include "lgfx/v1/platforms/esp32p4/Panel_ST7121.hpp" #include "lgfx/v1/platforms/esp32p4/Panel_ST7123.hpp" #include "lgfx/v1/platforms/esp32p4/Touch_ST7123.hpp" @@ -209,6 +210,34 @@ namespace m5gfx return false; } +#if defined (CONFIG_IDF_TARGET_ESP32P4) + struct Light_M5CoreP4X : public lgfx::ILight + { + bool init(uint8_t brightness) override + { + static constexpr uint16_t pwm_freq = 1000; + const uint8_t freq_data[] = { + 0x25, static_cast(pwm_freq), static_cast(pwm_freq >> 8) + }; + lgfx::i2c::transactionWrite(in_i2c_port, m5ioe1_i2c_addr, + freq_data, sizeof(freq_data), m5ioe1_i2c_freq); + lgfx::i2c::bitOn(in_i2c_port, m5ioe1_i2c_addr, 0x06, 1u << 0, m5ioe1_i2c_freq); + setBrightness(brightness); + return true; + } + + void setBrightness(uint8_t brightness) override + { + uint16_t duty = (brightness << 4) | (brightness >> 4); + const uint8_t duty_data[] = { + 0x1B, static_cast(duty), static_cast(0x80 | (duty >> 8)) + }; + lgfx::i2c::transactionWrite(in_i2c_port, m5ioe1_i2c_addr, + duty_data, sizeof(duty_data), m5ioe1_i2c_freq); + } + }; +#endif + #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) static constexpr std::int32_t axp_i2c_freq = 400000; static constexpr std::uint_fast8_t axp_i2c_addr = 0x34; @@ -2921,6 +2950,117 @@ The usage of each pin is as follows. if (pkg_ver == 0) // pkg_ver == EFUSE_RD_CHIP_VER_PKG_ { + if (board == 0 || board == board_t::board_M5CoreP4X) + { + static constexpr uint8_t corep4x_i2c_addr_list[] = { + 0x4Fu, // M5IOE1 + 0x6Eu, // M5PM1 + 0u + }; + uint32_t i2c_result = _detect_i2c_device(GPIO_NUM_11, GPIO_NUM_9, corep4x_i2c_addr_list); + if (i2c_result == ~0u) { + lgfx::i2c::init(in_i2c_port, GPIO_NUM_11, GPIO_NUM_9); + board = board_t::board_M5CoreP4X; + ESP_LOGI(LIBRARY_NAME, "[Autodetect] board_M5CoreP4X"); + + lgfx::i2c::writeRegister8(in_i2c_port, m5pm1_i2c_addr, 0x09, 0x00, 0, m5pm1_i2c_freq); + lgfx::i2c::writeRegister8(in_i2c_port, m5pm1_i2c_addr, 0x0A, 0x00, 0, m5pm1_i2c_freq); + lgfx::i2c::bitOn(in_i2c_port, m5pm1_i2c_addr, 0x06, 1u << 3, m5pm1_i2c_freq); + + // M5IOE1 G8/G9/G10/G11 control touch reset, backlight, LCD power, + // and LCD reset. G12 supplies the shared 3V3 rail for MBUS, TF card, IMU, + // infrared and Ethernet. + static constexpr uint8_t touch_reset_bit = 1u << 7; + static constexpr uint8_t display_bits_h = 0b00000111; + static constexpr uint8_t shared_power_bit = 1u << 3; + lgfx::i2c::writeRegister8(in_i2c_port, m5ioe1_i2c_addr, 0x23, 0x00, 0, m5ioe1_i2c_freq); + lgfx::i2c::bitOff(in_i2c_port, m5ioe1_i2c_addr, 0x13, touch_reset_bit, m5ioe1_i2c_freq); + lgfx::i2c::bitOff(in_i2c_port, m5ioe1_i2c_addr, 0x14, display_bits_h, m5ioe1_i2c_freq); + lgfx::i2c::bitOff(in_i2c_port, m5ioe1_i2c_addr, 0x14, shared_power_bit, m5ioe1_i2c_freq); + lgfx::i2c::bitOn( in_i2c_port, m5ioe1_i2c_addr, 0x03, touch_reset_bit, m5ioe1_i2c_freq); + lgfx::i2c::bitOn( in_i2c_port, m5ioe1_i2c_addr, 0x04, display_bits_h, m5ioe1_i2c_freq); + lgfx::i2c::bitOn( in_i2c_port, m5ioe1_i2c_addr, 0x04, shared_power_bit, m5ioe1_i2c_freq); + lgfx::i2c::bitOn(in_i2c_port, m5ioe1_i2c_addr, 0x05, touch_reset_bit, m5ioe1_i2c_freq); + lgfx::i2c::bitOn(in_i2c_port, m5ioe1_i2c_addr, 0x06, display_bits_h, m5ioe1_i2c_freq); + lgfx::i2c::bitOn(in_i2c_port, m5ioe1_i2c_addr, 0x06, shared_power_bit, m5ioe1_i2c_freq); + + static constexpr uint16_t pwm_freq = 1000; + const uint8_t freq_data[] = { + 0x25, static_cast(pwm_freq), static_cast(pwm_freq >> 8) + }; + const uint8_t duty_data[] = { 0x1B, 0x00, 0x80 }; + lgfx::i2c::transactionWrite(in_i2c_port, m5ioe1_i2c_addr, + freq_data, sizeof(freq_data), m5ioe1_i2c_freq); + lgfx::i2c::transactionWrite(in_i2c_port, m5ioe1_i2c_addr, + duty_data, sizeof(duty_data), m5ioe1_i2c_freq); + lgfx::delay(150); + +#if !CONFIG_SPIRAM + ESP_LOGE(LIBRARY_NAME, "M5CoreP4X needs PSRAM enabled"); +#else + auto bus_dsi = new Bus_DSI(); + _bus_last.reset(bus_dsi); + auto bus_cfg = bus_dsi->config(); + bus_cfg.bus_id = 0; + bus_cfg.lane_num = 2; + bus_cfg.lane_mbps = 600; + bus_cfg.ldo_chan_id = 3; + bus_cfg.ldo_voltage_mv = 2500; + bus_dsi->config(bus_cfg); + if (bus_dsi->init()) { + lgfx::delay(50); + auto p = new Panel_ST7102(); + _panel_last.reset(p); + auto det = p->config_detail(); + det.dpi_freq_mhz = 24; + det.hsync_back_porch = 40; + det.hsync_pulse_width = 2; + det.hsync_front_porch = 40; + det.vsync_back_porch = 8; + det.vsync_pulse_width = 4; + det.vsync_front_porch = 200; + p->config_detail(det); + + auto cfg = p->config(); + cfg.memory_width = 480; + cfg.memory_height = 480; + cfg.panel_width = 480; + cfg.panel_height = 480; + cfg.readable = false; + cfg.rgb_order = true; + cfg.bus_shared = false; + cfg.offset_x = 0; + cfg.offset_y = 0; + cfg.offset_rotation = 2; + cfg.pin_cs = GPIO_NUM_NC; + cfg.pin_rst = GPIO_NUM_NC; + p->config(cfg); + p->setBus(bus_dsi); + + auto t = new lgfx::Touch_CST3530(); + _touch_last.reset(t); + auto tcfg = t->config(); + tcfg.pin_rst = -1; + tcfg.pin_sda = GPIO_NUM_11; + tcfg.pin_scl = GPIO_NUM_9; + tcfg.pin_int = GPIO_NUM_1; + tcfg.freq = 400000; + tcfg.x_min = 0; + tcfg.x_max = 479; + tcfg.y_min = 0; + tcfg.y_max = 479; + tcfg.i2c_port = I2C_NUM_1; + tcfg.bus_shared = false; + tcfg.offset_rotation = 2; + t->config(tcfg); + _panel_last->setTouch(t); + } + _set_backlight(new Light_M5CoreP4X()); +#endif + goto init_clear; + } + } + if (board == 0 || board == board_t::board_M5Tab5 || board == board_t::board_M5Tab5X) { // SDA = GPIO_NUM_31 @@ -3709,6 +3849,7 @@ The usage of each pin is as follows. case board_M5Station: title = "M5Station"; break; case board_M5StopWatch: title = "M5StopWatch"; break; case board_M5ChainCaptain: title = "M5ChainCaptain"; break; + case board_M5CoreP4X: title = "M5CoreP4X"; break; case board_M5AtomS3: title = "M5AtomS3"; break; case board_M5AtomS3R: title = "M5AtomS3R"; break; case board_M5Dial: title = "M5Dial"; break; diff --git a/src/lgfx/v1/platforms/esp32p4/Bus_DSI.cpp b/src/lgfx/v1/platforms/esp32p4/Bus_DSI.cpp index 98edfbd..7daf8ce 100644 --- a/src/lgfx/v1/platforms/esp32p4/Bus_DSI.cpp +++ b/src/lgfx/v1/platforms/esp32p4/Bus_DSI.cpp @@ -41,7 +41,7 @@ namespace lgfx ldo_cfg.chan_id = _cfg.ldo_chan_id; ldo_cfg.voltage_mv = _cfg.ldo_voltage_mv; - esp_lcd_dbi_io_config_t dbi_config; + esp_lcd_dbi_io_config_t dbi_config = {}; dbi_config.virtual_channel = 0; dbi_config.lcd_cmd_bits = _cfg.lcd_cmd_bits; dbi_config.lcd_param_bits = _cfg.lcd_param_bits; diff --git a/src/lgfx/v1/platforms/esp32p4/Panel_DSI.cpp b/src/lgfx/v1/platforms/esp32p4/Panel_DSI.cpp index 9462adc..58ce42d 100644 --- a/src/lgfx/v1/platforms/esp32p4/Panel_DSI.cpp +++ b/src/lgfx/v1/platforms/esp32p4/Panel_DSI.cpp @@ -38,16 +38,10 @@ namespace lgfx auto bus = getBusDSI(); if (bus == nullptr) { return false; } - const uint8_t* params; - for (size_t i = 0; nullptr != (params = getInitParams(i)); ++i) - { - size_t len; - while (0 != (len = params[0])) { -// printf("cmd: %02x, len: %d\n", params[1], len); - bus->writeParams(params[1], ¶ms[2], len - 1); - params += len + 1; - } - vTaskDelay(pdMS_TO_TICKS(getInitDelay(i))); + const bool init_in_command_mode = initInCommandMode(); + if (!init_in_command_mode + && ESP_OK != esp_lcd_panel_init(_disp_panel_handle)) { + return false; } uint8_t madctl_val = 0; @@ -60,10 +54,24 @@ namespace lgfx colmod_val = 0x77; } - bus->writeParams(CMD_MADCTL, &(madctl_val), 1); - bus->writeParams(CMD_COLMOD, &(colmod_val), 1); + const uint8_t* params; + for (size_t i = 0; nullptr != (params = getInitParams(i)); ++i) + { + size_t len; + while (0 != (len = params[0])) { +// printf("cmd: %02x, len: %d\n", params[1], len); + bus->writeParams(params[1], ¶ms[2], len - 1); + params += len + 1; + } + vTaskDelay(pdMS_TO_TICKS(getInitDelay(i))); + } - return (ESP_OK == esp_lcd_panel_init(_disp_panel_handle)); + if (init_in_command_mode) { + bus->writeParams(CMD_MADCTL, &(madctl_val), 1); + bus->writeParams(CMD_COLMOD, &(colmod_val), 1); + return (ESP_OK == esp_lcd_panel_init(_disp_panel_handle)); + } + return true; } @@ -82,7 +90,7 @@ namespace lgfx #else dpi_config.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; #endif - dpi_config.num_fbs = 1; + dpi_config.num_fbs = 2; dpi_config.video_timing.h_size = _cfg.panel_width; dpi_config.video_timing.v_size = _cfg.panel_height; dpi_config.video_timing.hsync_back_porch = _config_detail.hsync_back_porch; @@ -122,6 +130,14 @@ namespace lgfx auto bus = getBusDSI(); if (bus == nullptr) { return false; } + const size_t reset_delay = getResetDelayBeforeDpi(); + if (reset_delay + && (!bus->writeParams(CMD_SWRESET, nullptr, 0))) { + return false; + } + if (reset_delay) { + vTaskDelay(pdMS_TO_TICKS(reset_delay)); + } if (init_dpi(bus) && init_panel()) { esp_lcd_dpi_panel_get_frame_buffer(_disp_panel_handle, 1, &(_config_detail.buffer)); diff --git a/src/lgfx/v1/platforms/esp32p4/Panel_DSI.hpp b/src/lgfx/v1/platforms/esp32p4/Panel_DSI.hpp index 2316b53..0c1fc54 100644 --- a/src/lgfx/v1/platforms/esp32p4/Panel_DSI.hpp +++ b/src/lgfx/v1/platforms/esp32p4/Panel_DSI.hpp @@ -74,6 +74,7 @@ namespace lgfx protected: + static constexpr uint8_t CMD_SWRESET = 0x01; static constexpr uint8_t CMD_SLPIN = 0x10; static constexpr uint8_t CMD_SLPOUT = 0x11; static constexpr uint8_t CMD_INVOFF = 0x20; @@ -87,6 +88,8 @@ namespace lgfx virtual const uint8_t* getInitParams(size_t listno) const { return nullptr; } virtual size_t getInitDelay(size_t listno) const { return 0; } + virtual size_t getResetDelayBeforeDpi(void) const { return 0; } + virtual bool initInCommandMode(void) const { return true; } bool write_params(uint32_t cmd, const uint8_t* data = nullptr, size_t length = 0); diff --git a/src/lgfx/v1/platforms/esp32p4/Panel_ST7102.hpp b/src/lgfx/v1/platforms/esp32p4/Panel_ST7102.hpp new file mode 100644 index 0000000..d7884af --- /dev/null +++ b/src/lgfx/v1/platforms/esp32p4/Panel_ST7102.hpp @@ -0,0 +1,118 @@ +/*----------------------------------------------------------------------------/ + Lovyan GFX - Graphics library for embedded devices. + + ST7102 MIPI DSI panel parameters for M5Stack CoreP4X. +/----------------------------------------------------------------------------*/ +#pragma once + +#include "Panel_DSI.hpp" +#if SOC_MIPI_DSI_SUPPORTED + +namespace lgfx +{ + inline namespace v1 + { + + struct Panel_ST7102 : public Panel_DSI + { + protected: + + bool initInCommandMode(void) const override { return false; } + size_t getResetDelayBeforeDpi(void) const override { return 120; } + + const uint8_t* getInitParams(size_t listno) const override + { + static constexpr uint8_t list0[] = + {//len(cmd+params), cmd, params + 4, 0x99, 0x71, 0x02, 0xA2, + 4, 0x99, 0x71, 0x02, 0xA3, + 4, 0x99, 0x71, 0x02, 0xA4, + 2, 0x78, 0x21, + 2, 0x79, 0xCF, + 8, 0xB0, 0x22, 0x43, 0x1E, 0x43, 0x2F, 0x57, 0x57, + 3, 0xB7, 0x7D, 0x7D, + 3, 0xBF, 0x7A, 0x7A, + 38, 0xC8, 0x00, 0x00, 0x13, 0x23, 0x3E, 0x00, 0x6A, 0x03, 0xB0, 0x06, + 0x11, 0x0F, 0x07, 0x85, 0x03, 0x21, 0xD5, 0x01, 0x18, 0x00, + 0x22, 0x56, 0x0F, 0x98, 0x0A, 0x32, 0xF8, 0x0D, 0x48, 0x0F, + 0xF3, 0x80, 0x0F, 0xAC, 0xC1, 0x03, 0xC4, + 38, 0xC9, 0x00, 0x00, 0x13, 0x23, 0x3E, 0x00, 0x6A, 0x03, 0xB0, 0x06, + 0x11, 0x0F, 0x07, 0x85, 0x03, 0x21, 0xD5, 0x01, 0x18, 0x00, + 0x22, 0x56, 0x0F, 0x98, 0x0A, 0x32, 0xF8, 0x0D, 0x48, 0x0F, + 0xF3, 0x80, 0x0F, 0xAC, 0xC1, 0x03, 0xC4, + 7, 0xD7, 0x10, 0x0C, 0x02, 0x19, 0x40, 0x40, + 33, 0xA3, 0x40, 0x03, 0x80, 0xCF, 0x44, 0x00, 0x00, 0x00, 0x02, 0x05, + 0x6F, 0x6F, 0x00, 0x1A, 0x00, 0x45, 0x05, 0x00, 0x00, 0x00, + 0x00, 0x46, 0x00, 0x00, 0x02, 0x20, 0x52, 0x00, 0x05, 0x00, + 0x00, 0xFF, + 45, 0xA6, 0x02, 0x00, 0x24, 0x55, 0x35, 0x00, 0x38, 0x00, 0x97, 0x97, + 0x00, 0x24, 0x55, 0x36, 0x00, 0x37, 0x00, 0x97, 0x97, 0x02, + 0xAC, 0x51, 0x3A, 0x00, 0x00, 0x00, 0x97, 0x97, 0x00, 0xAC, + 0x21, 0x00, 0x0B, 0x00, 0x00, 0x97, 0x97, 0x00, 0x00, 0x06, + 0x00, 0x00, 0x00, 0x00, + 49, 0xA7, 0x19, 0x19, 0x00, 0x64, 0x40, 0x07, 0x16, 0x40, 0x00, 0x04, + 0x03, 0x97, 0x97, 0x00, 0x64, 0x40, 0x25, 0x34, 0x00, 0x00, + 0x02, 0x01, 0x97, 0x97, 0x00, 0x64, 0x40, 0x4B, 0x5A, 0x00, + 0x00, 0x02, 0x01, 0x97, 0x97, 0x00, 0x24, 0x40, 0x69, 0x78, + 0x00, 0x00, 0x00, 0x00, 0x97, 0x97, 0x00, 0x44, + 38, 0xAC, 0x11, 0x08, 0x13, 0x0A, 0x18, 0x1A, 0x1B, 0x00, 0x06, 0x03, + 0x19, 0x1B, 0x1B, 0x1B, 0x18, 0x1B, 0x10, 0x09, 0x12, 0x0B, + 0x18, 0x1A, 0x1B, 0x02, 0x06, 0x01, 0x19, 0x1B, 0x1B, 0x1B, + 0x18, 0x1B, 0xFF, 0x67, 0xFF, 0x67, 0x00, + 8, 0xAD, 0xCC, 0x40, 0x46, 0x11, 0x04, 0x6F, 0x6F, + 15, 0xE8, 0x30, 0x07, 0x00, 0xB3, 0xB3, 0x9C, 0x00, 0xE2, 0x04, 0x00, + 0x00, 0x00, 0x00, 0xEF, + 3, 0x75, 0x03, 0x04, + 34, 0xE7, 0x8B, 0x3C, 0x00, 0x0C, 0xF0, 0x5D, 0x00, 0x5D, 0x00, 0x5D, + 0x00, 0x5D, 0x00, 0xFF, 0x00, 0x08, 0x7B, 0x00, 0x00, 0xC8, + 0x6A, 0x5A, 0x08, 0x1A, 0x3C, 0x00, 0x71, 0x01, 0x8C, 0x01, + 0x7F, 0xF0, 0x22, + 10, 0xE9, 0x3C, 0x7F, 0x08, 0x07, 0x1A, 0x7A, 0x22, 0x1A, 0x33, + 0, + }; + + static constexpr uint8_t list1[] = + { + 1, CMD_SLPOUT, + 0, + }; + + static constexpr uint8_t list2[] = + { + 2, CMD_MADCTL, 0b11, + 1, CMD_DISPON, + 0, + }; + + static constexpr uint8_t list3[] = + { + 2, 0x35, 0x00, + 1, CMD_DISPON, + 0, + }; + + switch (listno) + { + case 0: return list0; + case 1: return list1; + case 2: return list2; + case 3: return list3; + default: return nullptr; + } + } + + size_t getInitDelay(size_t listno) const override + { + switch (listno) + { + case 1: return 20; + case 2: return 20; + default: return 0; + } + } + }; + + } +} + +#endif diff --git a/src/lgfx/v1/touch/Touch_CSTxxx.cpp b/src/lgfx/v1/touch/Touch_CSTxxx.cpp index ea8ca19..e1e0a4c 100644 --- a/src/lgfx/v1/touch/Touch_CSTxxx.cpp +++ b/src/lgfx/v1/touch/Touch_CSTxxx.cpp @@ -27,6 +27,102 @@ namespace lgfx { inline namespace v1 { +//---------------------------------------------------------------------------- + + bool Touch_CST3530::_write_reg32(uint32_t reg) + { + uint8_t data[4] = + { static_cast(reg >> 24) + , static_cast(reg >> 16) + , static_cast(reg >> 8) + , static_cast(reg) + }; + return i2c::transactionWrite(_cfg.i2c_port, _cfg.i2c_addr, data, sizeof(data), _cfg.freq).has_value(); + } + + bool Touch_CST3530::_read_reg32(uint32_t reg, uint8_t* data, size_t length) + { + uint8_t address[4] = + { static_cast(reg >> 24) + , static_cast(reg >> 16) + , static_cast(reg >> 8) + , static_cast(reg) + }; + return i2c::transactionWriteRead(_cfg.i2c_port, _cfg.i2c_addr, address, sizeof(address), data, length, _cfg.freq).has_value(); + } + + bool Touch_CST3530::_check_init(void) + { + if (_inited) return true; + + bool ok = _write_reg32(0xD0000400); + lgfx::delay(20); + ok = _write_reg32(0xD0000400) && ok; + lgfx::delay(20); + ok = _write_reg32(0xD0000000) && ok; + ok = _write_reg32(0xD0000C00) && ok; + ok = _write_reg32(0xD0000100) && ok; + _inited = ok; + return _inited; + } + + bool Touch_CST3530::init(void) + { + _inited = false; + if (_cfg.pin_int >= 0) { + lgfx::pinMode(_cfg.pin_int, pin_mode_t::input); + } + i2c::init(_cfg.i2c_port, _cfg.pin_sda, _cfg.pin_scl).has_value(); + return true; + } + + void Touch_CST3530::wakeup(void) + { + _inited = false; + _check_init(); + } + + void Touch_CST3530::sleep(void) + { + } + + uint_fast8_t Touch_CST3530::getTouchRaw(touch_point_t* tp, uint_fast8_t count) + { + if (count == 0 || !_check_init()) return 0; + + uint8_t data[50] = { 0 }; + if (!_read_reg32(0xD0070000, data, 9)) return 0; + + uint8_t finger_num = data[3] & 0x0F; + uint8_t key_num = (data[3] >> 4) & 0x0F; + uint8_t total_num = key_num + finger_num; + if (total_num > 1) { + size_t extra_length = (total_num - 1) * 5; + if (extra_length > sizeof(data) - 9) extra_length = sizeof(data) - 9; + if (!i2c::transactionRead(_cfg.i2c_port, _cfg.i2c_addr, &data[9], extra_length, _cfg.freq).has_value()) { + _write_reg32(0xD00002AB); + return 0; + } + } + _write_reg32(0xD00002AB); + + if (finger_num == 0 || (data[8] >> 4) == 0) return 0; + + uint_fast8_t points = finger_num; + if (points > max_touch_points) points = max_touch_points; + if (points > count) points = count; + for (uint_fast8_t i = 0; i < points; ++i) { + uint16_t index = (key_num + i) * 5; + uint16_t x = data[index + 4] | ((uint16_t)(data[index + 7] & 0x0F) << 8); + uint16_t y = data[index + 5] | ((uint16_t)(data[index + 7] & 0xF0) << 4); + tp[i].id = i; + tp[i].size = 1; + tp[i].x = x; + tp[i].y = y; + } + return points; + } + //---------------------------------------------------------------------------- static constexpr uint8_t CST816S_TOUCH_REG = 0x01; diff --git a/src/lgfx/v1/touch/Touch_CSTxxx.hpp b/src/lgfx/v1/touch/Touch_CSTxxx.hpp index 15b78d6..9390c2d 100644 --- a/src/lgfx/v1/touch/Touch_CSTxxx.hpp +++ b/src/lgfx/v1/touch/Touch_CSTxxx.hpp @@ -28,6 +28,34 @@ namespace lgfx struct Touch_CST226; // CST226/CST226SE struct Touch_CST816S; + struct Touch_CST3530; + + +//---------------------------------------------------------------------------- + + struct Touch_CST3530 : public ITouch + { + Touch_CST3530(void) + { + _cfg.i2c_addr = 0x58; + _cfg.x_min = 0; + _cfg.x_max = 479; + _cfg.y_min = 0; + _cfg.y_max = 479; + } + + bool init(void) override; + void wakeup(void) override; + void sleep(void) override; + uint_fast8_t getTouchRaw(touch_point_t* tp, uint_fast8_t count) override; + + private: + static constexpr uint_fast8_t max_touch_points = 5; + + bool _check_init(void); + bool _write_reg32(uint32_t reg); + bool _read_reg32(uint32_t reg, uint8_t* data, size_t length); + }; //---------------------------------------------------------------------------- From 6c371433fa10d906b2ea3cde7fe96c5b254b7ce4 Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:03:40 +0000 Subject: [PATCH 09/11] Wait for chunked ESP32 I2C reads to complete readBytes splits transfers at the hardware RX FIFO boundary. Record the same data-transfer wait stage used by writeBytes after starting each read command, so the next chunk or endTransaction waits for and clears that command completion instead of racing a stale END interrupt. --- src/lgfx/v1/platforms/esp32/common.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lgfx/v1/platforms/esp32/common.cpp b/src/lgfx/v1/platforms/esp32/common.cpp index aeac241..b8f6ac8 100644 --- a/src/lgfx/v1/platforms/esp32/common.cpp +++ b/src/lgfx/v1/platforms/esp32/common.cpp @@ -2296,6 +2296,7 @@ namespace lgfx updateDev(dev); dev->int_clr.val = intmask; dev->ctr.trans_start = 1; + i2c_context[i2c_port].wait_ack_stage = 2; uint32_t us = lgfx::micros(); taskYIELD(); From 90dc8db79ccf1693f8f06fe042f61e3572e378c4 Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:42:03 +0000 Subject: [PATCH 10/11] Survive slaves that stretch the clock for milliseconds The software wait limits gave a stalled transfer 1-2ms before declaring connection_lost: the FIFO poll in readBytes allowed us_limit+1024us and the END wait in i2c_wait allowed 512< i2c_wait(int i2c_port, bool flg_stop = false) { if (flg_stop == false && i2c_context[i2c_port].state.has_error()) { return cpp::fail(i2c_context[i2c_port].state.error()); } @@ -1662,7 +1667,7 @@ namespace lgfx auto dev = getDev(i2c_port); typeof(dev->int_raw) int_raw; int_raw.val = dev->int_raw.val; // ACK待ちステージをスキップした場合も後段の分岐で参照されるため必ず初期化する; - static constexpr uint32_t intmask = I2C_ACK_ERR_INT_RAW_M | I2C_END_DETECT_INT_RAW_M | I2C_ARBITRATION_LOST_INT_RAW_M; + static constexpr uint32_t intmask = I2C_ACK_ERR_INT_RAW_M | I2C_TIME_OUT_INT_RAW_M | I2C_END_DETECT_INT_RAW_M | I2C_ARBITRATION_LOST_INT_RAW_M; if (i2c_context[i2c_port].wait_ack_stage) { @@ -1676,7 +1681,7 @@ namespace lgfx #else uint32_t us_limit = (dev->scl_high_period.period + dev->scl_low_period.period + 16 ) * (1 + dev->status_reg.tx_fifo_cnt); #endif - us_limit += 512 << i2c_context[i2c_port].wait_ack_stage; + us_limit += i2c_stall_limit_us; do { @@ -1688,6 +1693,13 @@ namespace lgfx int_raw.val = dev->int_raw.val; dev->int_clr.val = int_raw.val; + // A timeout or lost arbitration is fatal even when END is also set. + if (int_raw.val & (I2C_TIME_OUT_INT_RAW_M | I2C_ARBITRATION_LOST_INT_RAW_M)) + { + res = cpp::fail(error_t::connection_lost); + i2c_context[i2c_port].state = cpp::fail(error_t::connection_lost); + } + else #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) if (!int_raw.end_detect || int_raw.ack_err) #elif defined ( CONFIG_IDF_TARGET_ESP32C2 ) || defined ( CONFIG_IDF_TARGET_ESP32S3 ) || defined ( CONFIG_IDF_TARGET_ESP32C5 ) || defined ( CONFIG_IDF_TARGET_ESP32C6 ) || defined ( CONFIG_IDF_TARGET_ESP32C61 ) || defined ( CONFIG_IDF_TARGET_ESP32P4 ) || defined ( CONFIG_IDF_TARGET_ESP32H2 ) @@ -1721,13 +1733,27 @@ namespace lgfx { i2c_set_cmd(dev, 0, i2c_cmd_stop, 0); i2c_set_cmd(dev, 1, i2c_cmd_end, 0); - static constexpr uint32_t intmask_ = I2C_ACK_ERR_INT_RAW_M | I2C_TIME_OUT_INT_RAW_M | I2C_END_DETECT_INT_RAW_M | I2C_ARBITRATION_LOST_INT_RAW_M | I2C_TRANS_COMPLETE_INT_RAW_M; + // Wake only on events that end the STOP; a NACK is evaluated after it completes. + static constexpr uint32_t intmask_ = I2C_TIME_OUT_INT_RAW_M | I2C_ARBITRATION_LOST_INT_RAW_M | I2C_TRANS_COMPLETE_INT_RAW_M; updateDev(dev); - dev->int_clr.val = intmask_; + dev->int_clr.val = intmask_ | I2C_ACK_ERR_INT_RAW_M | I2C_END_DETECT_INT_RAW_M; dev->ctr.trans_start = 1; uint32_t ms = lgfx::millis(); taskYIELD(); - while (!(dev->int_raw.val & intmask_) && ((millis() - ms) < 14)); + while (!(dev->int_raw.val & intmask_) && ((millis() - ms) < (i2c_stall_limit_us / 1000))) { taskYIELD(); } + // A STOP that did not complete leaves the bus in an unknown state: + // recover it and refuse further use of this transaction. + { + uint32_t stop_raw = dev->int_raw.val; + if (res.has_value() + && ((stop_raw & (I2C_TIME_OUT_INT_RAW_M | I2C_ARBITRATION_LOST_INT_RAW_M)) + || !(stop_raw & I2C_TRANS_COMPLETE_INT_RAW_M))) + { + res = cpp::fail(error_t::connection_lost); + i2c_context[i2c_port].state = cpp::fail(error_t::connection_lost); + i2c_stop(i2c_port); + } + } #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) if (res.has_value() && dev->int_raw.ack_err) #elif defined ( CONFIG_IDF_TARGET_ESP32C2 ) || defined ( CONFIG_IDF_TARGET_ESP32S3 ) || defined ( CONFIG_IDF_TARGET_ESP32C5 ) || defined ( CONFIG_IDF_TARGET_ESP32C6 ) || defined ( CONFIG_IDF_TARGET_ESP32C61 ) || defined ( CONFIG_IDF_TARGET_ESP32P4 ) || defined ( CONFIG_IDF_TARGET_ESP32H2 ) @@ -1735,8 +1761,9 @@ namespace lgfx #else if (res.has_value() && dev->int_raw.nack) #endif - { + { // The STOP completed but a byte went unacknowledged. res = cpp::fail(error_t::connection_lost); + i2c_context[i2c_port].state = cpp::fail(error_t::connection_lost); } //ESP_LOGI("LGFX", "I2C stop"); } @@ -2143,11 +2170,14 @@ namespace lgfx dev->ctr.fsm_rst = 1; #endif +// SCL-low (clock stretch) watchdog. 2^21 source clocks stays past +// i2c_stall_limit_us on every supported source; the ESP32 register below is at +// its ceiling, about 13ms. #if defined ( CONFIG_IDF_TARGET_ESP32C3 ) - dev->timeout.time_out_value = 31; + dev->timeout.time_out_value = 21; dev->timeout.time_out_en = 1; #elif defined ( CONFIG_IDF_TARGET_ESP32C2 ) || defined ( CONFIG_IDF_TARGET_ESP32S3 ) || defined ( CONFIG_IDF_TARGET_ESP32C5 ) || defined ( CONFIG_IDF_TARGET_ESP32C6 ) || defined ( CONFIG_IDF_TARGET_ESP32C61 ) || defined ( CONFIG_IDF_TARGET_ESP32P4 ) || defined ( CONFIG_IDF_TARGET_ESP32H2 ) - dev->to.time_out_value = 31; + dev->to.time_out_value = 21; dev->to.time_out_en = 1; #else dev->timeout.tout = 0xFFFFF; // max 13ms @@ -2310,7 +2340,7 @@ namespace lgfx do { taskYIELD(); - } while ((len>>1) >= getRxFifoCount(dev) && !(dev->int_raw.val & intmask) && ((lgfx::micros() - us) <= us_limit + 1024)); + } while ((len>>1) >= getRxFifoCount(dev) && !(dev->int_raw.val & intmask) && ((lgfx::micros() - us) <= us_limit + i2c_stall_limit_us)); if (0 == getRxFifoCount(dev)) { diff --git a/src/lgfx/v1/platforms/soft_i2c.inl b/src/lgfx/v1/platforms/soft_i2c.inl index 9ef168c..1281e2f 100644 --- a/src/lgfx/v1/platforms/soft_i2c.inl +++ b/src/lgfx/v1/platforms/soft_i2c.inl @@ -165,7 +165,7 @@ Contributors: { SOFT_I2C_YIELD(); if (gpio_in(ctx.pin_scl)) { return true; } - } while ((micros() - us) < 13000); // the same order as a peripheral timeout + } while ((micros() - us) < 25000); // the same stall allowance as the peripheral ports return false; } From 06a7a00b84fa6f43b1e30031499359603c458037 Mon Sep 17 00:00:00 2001 From: ainyan03 <205502311+ainyan03@users.noreply.github.com> Date: Tue, 25 Aug 2026 01:50:22 +0000 Subject: [PATCH 11/11] Bump version to 0.2.28 --- idf_component.yml | 2 +- library.json | 2 +- library.properties | 2 +- src/lgfx/v1/gitTagVersion.h | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/idf_component.yml b/idf_component.yml index 61c53d3..c06ca05 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -2,4 +2,4 @@ description: Graphics library for M5Stack series issues: https://github.com/m5stack/M5GFX/issues repository: https://github.com/m5stack/M5GFX.git url: https://github.com/m5stack/M5GFX.git -version: 0.2.27 +version: 0.2.28 diff --git a/library.json b/library.json index b682137..4fcefb7 100644 --- a/library.json +++ b/library.json @@ -10,7 +10,7 @@ "type": "git", "url": "https://github.com/m5stack/M5GFX.git" }, - "version": "0.2.27", + "version": "0.2.28", "frameworks": ["arduino", "espidf", "*"], "platforms": ["espressif32", "native"], "headers": "M5GFX.h" diff --git a/library.properties b/library.properties index 70d7309..8e8ce71 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=M5GFX -version=0.2.27 +version=0.2.28 author=M5Stack maintainer=M5Stack sentence=Library for M5Stack All Display diff --git a/src/lgfx/v1/gitTagVersion.h b/src/lgfx/v1/gitTagVersion.h index 187a087..3519c98 100644 --- a/src/lgfx/v1/gitTagVersion.h +++ b/src/lgfx/v1/gitTagVersion.h @@ -1,4 +1,4 @@ #define LGFX_VERSION_MAJOR 1 #define LGFX_VERSION_MINOR 2 -#define LGFX_VERSION_PATCH 27 +#define LGFX_VERSION_PATCH 28 #define LOVYANGFX_VERSION F( LGFX_VERSION_MAJOR "." LGFX_VERSION_MINOR "." LGFX_VERSION_PATCH )