From 57df72a61cc0428dc9fb328cbc9e25e181c98697 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Sat, 15 Aug 2026 06:11:02 +0000 Subject: [PATCH 1/4] Restore push-pull mode on QSPI bus pins in spi::initQuad The fix for SPI bus pins left in open-drain mode on ESP-IDF v6 (#192) covered only the single-bit spi::init path. spi::initQuad (used by QSPI panels) calls spi_bus_initialize the same way and is affected by the same behavior: ESP-IDF v6 routes bus pins with gpio_matrix_output() and no longer clears the pad open-drain flag. Factor the pad restoration out of spi::init into a shared helper and apply it to SCLK/IO0-3 in spi::initQuad as well. Verified on M5StopWatch (CO5300, QSPI): with the bus pins deliberately set to open-drain before init, the pads remained open-drain after init on ESP-IDF v6.0.1 without this fix, and are restored to push-pull with it. No regression on ESP-IDF v5.5.4. --- src/lgfx/v1/platforms/esp32/common.cpp | 39 ++++++++++++++++---------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/lgfx/v1/platforms/esp32/common.cpp b/src/lgfx/v1/platforms/esp32/common.cpp index bdf0e82..063e6a2 100644 --- a/src/lgfx/v1/platforms/esp32/common.cpp +++ b/src/lgfx/v1/platforms/esp32/common.cpp @@ -92,6 +92,8 @@ Original Source: #include #endif +#include + #if __has_include() #include #endif @@ -625,6 +627,25 @@ namespace lgfx // ------------------------------------------------------------------------ + // Clear stale open-drain state on the bus pins. + // spi_bus_initialize does not clear the pad open-drain flag (ESP-IDF v6 + // no longer configures it at all), so a pin left in open-drain mode by a + // previous use may not rise fast enough at typical SPI clock rates. + static void clear_open_drain(std::initializer_list pins) + { + for (int pin : pins) + { + if ((size_t)pin < GPIO_NUM_MAX) + { +#if __has_include() + gpio_ll_od_disable(&GPIO, (gpio_num_t)pin); +#else // ESP-IDF v3 (plain ESP32 only) + GPIO.pin[pin].pad_driver = 0; +#endif + } + } + } + cpp::result init(int spi_host, int spi_sclk, int spi_miso, int spi_mosi) { return init(spi_host, spi_sclk, spi_miso, spi_mosi, 0); // SPI_DMA_CH_AUTO; @@ -700,21 +721,7 @@ namespace lgfx } } - // SPIバスのピンを push-pull 出力へ明示的に戻す。 - // pinMode の input 系設定はパッドを open-drain にするが、ESP-IDF v6 以降の - // spi_bus_initialize はピンの open-drain 設定を解除しないため、直前のピン状態が - // 残っていると SCLK/MOSI が open-drain 駆動となり、高クロックで波形が立ち上がらない。 - for (int pin : { spi_sclk, spi_mosi, spi_miso }) - { - if ((size_t)pin < GPIO_NUM_MAX) - { -#if __has_include() - gpio_ll_od_disable(&GPIO, (gpio_num_t)pin); -#else // ESP-IDF v3 系 (無印 ESP32 のみ) - GPIO.pin[pin].pad_driver = 0; -#endif - } - } + clear_open_drain({ spi_sclk, spi_mosi, spi_miso }); writereg(SPI_USER_REG(spi_port), SPI_USR_MOSI | SPI_USR_MISO | SPI_DOUTDIN); // need SD card access (full duplex setting) writereg(SPI_CTRL_REG(spi_port), 0); @@ -797,6 +804,8 @@ namespace lgfx #pragma GCC diagnostic pop + clear_open_drain({ spi_sclk, spi_io0, spi_io1, spi_io2, spi_io3 }); + writereg(SPI_USER_REG(spi_port), SPI_USR_MOSI | SPI_USR_MISO | SPI_DOUTDIN); // need SD card access (full duplex setting) writereg(SPI_CTRL_REG(spi_port), 0); #if defined ( SPI_CTRL1_REG ) From 599fe1858ed76af5821e68c9aab33cc83a59d227 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Sat, 15 Aug 2026 06:11:11 +0000 Subject: [PATCH 2/4] Normalize backlight pin as push-pull GPIO output in Light_PWM::init The LEDC pin attach does not fully normalize the pad state: the pad open-drain flag is never cleared (on any IDF version), and starting with ESP-IDF v6 the ledc driver no longer selects the GPIO function in IO_MUX either. If the backlight pin was previously configured as an input or open-drain output (e.g. by probing code or a previous application state), the PWM output cannot drive the pin high and the backlight stays off. Explicitly normalize the pin as a push-pull GPIO output at the start of init. The pin is first latched to the "off" level to avoid a visible glitch while the pin is temporarily a plain GPIO output. Verified on M5Stack Core FIRE (backlight on GPIO32) with ESP-IDF v6.0.1: with the pin deliberately set to open-drain before init, the pad remained open-drain after init without this fix, and is restored to push-pull with it. --- src/lgfx/v1/platforms/esp32/Light_PWM.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/lgfx/v1/platforms/esp32/Light_PWM.cpp b/src/lgfx/v1/platforms/esp32/Light_PWM.cpp index 676b90e..82f3fff 100644 --- a/src/lgfx/v1/platforms/esp32/Light_PWM.cpp +++ b/src/lgfx/v1/platforms/esp32/Light_PWM.cpp @@ -19,6 +19,7 @@ Original Source: #include #include "Light_PWM.hpp" +#include "common.hpp" #if defined ( ARDUINO ) #include @@ -65,6 +66,15 @@ namespace lgfx bool Light_PWM::init(uint8_t brightness) { + // The LEDC driver does not fully normalize the pad state: the open-drain + // flag is never cleared, and ESP-IDF v6 no longer selects the GPIO + // function in IO_MUX. Reconfigure the pin as a push-pull GPIO output + // first, latching it to the "off" level to avoid a visible glitch. + if ((size_t)_cfg.pin_bl < GPIO_NUM_MAX) + { + if (_cfg.invert) { gpio_hi(_cfg.pin_bl); } else { gpio_lo(_cfg.pin_bl); } + lgfx::pinMode(_cfg.pin_bl, pin_mode_t::output); + } #if defined ( ARDUINO ) From d049983bf5fa7d359ba434eb59e6549f4b294f59 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Sat, 15 Aug 2026 06:11:23 +0000 Subject: [PATCH 3/4] Normalize parallel bus data pins as push-pull output (ESP32-S2/S3) Neither esp_lcd_new_i80_bus (ESP32-S3, since ESP-IDF v5.4 removed its gpio_set_direction call) nor the plain GPIO matrix routing used on ESP32-S2 configures the pad direction or clears the open-drain flag on the data pins. If a data pin was previously configured as an input or open-drain output, the bus cannot drive it high and the panel receives corrupted data. Bus_EPD already guards against this by calling pinMode(output) on its data pins; apply the same guard to Bus_Parallel8/16. The WR/RD/RS control pins were already normalized via gpio_set_direction. Verified on ESP32-S3 (ESP-IDF v6.0.1) by pulse-counting bus edges with PCNT on a scope-free rig: with the data pins deliberately set to open-drain before init, a 0x00/0xFF pattern produced 0 edges on D0/D7 (WR counting normally) without this fix, and the expected edge count with it. --- src/lgfx/v1/platforms/esp32s2/Bus_Parallel16.cpp | 7 +++++++ src/lgfx/v1/platforms/esp32s2/Bus_Parallel8.cpp | 7 +++++++ src/lgfx/v1/platforms/esp32s3/Bus_Parallel16.cpp | 8 ++++++++ src/lgfx/v1/platforms/esp32s3/Bus_Parallel8.cpp | 8 ++++++++ 4 files changed, 30 insertions(+) diff --git a/src/lgfx/v1/platforms/esp32s2/Bus_Parallel16.cpp b/src/lgfx/v1/platforms/esp32s2/Bus_Parallel16.cpp index 4e46c2e..0a778a2 100644 --- a/src/lgfx/v1/platforms/esp32s2/Bus_Parallel16.cpp +++ b/src/lgfx/v1/platforms/esp32s2/Bus_Parallel16.cpp @@ -96,6 +96,13 @@ namespace lgfx bool Bus_Parallel16::init(void) { + // The GPIO matrix routing used below does not configure pad direction / + // open-drain, so normalize the data pins as push-pull GPIO outputs first. + for (int pin : _cfg.pin_data) + { + lgfx::pinMode(pin, pin_mode_t::output); + } + _init_pin(); for (size_t i = 0; i < 3; ++i) diff --git a/src/lgfx/v1/platforms/esp32s2/Bus_Parallel8.cpp b/src/lgfx/v1/platforms/esp32s2/Bus_Parallel8.cpp index 5fc1312..1c89f48 100644 --- a/src/lgfx/v1/platforms/esp32s2/Bus_Parallel8.cpp +++ b/src/lgfx/v1/platforms/esp32s2/Bus_Parallel8.cpp @@ -96,6 +96,13 @@ namespace lgfx bool Bus_Parallel8::init(void) { + // The GPIO matrix routing used below does not configure pad direction / + // open-drain, so normalize the data pins as push-pull GPIO outputs first. + for (int pin : _cfg.pin_data) + { + lgfx::pinMode(pin, pin_mode_t::output); + } + _init_pin(); for (size_t i = 0; i < 3; ++i) diff --git a/src/lgfx/v1/platforms/esp32s3/Bus_Parallel16.cpp b/src/lgfx/v1/platforms/esp32s3/Bus_Parallel16.cpp index 8dd0a39..96aae26 100644 --- a/src/lgfx/v1/platforms/esp32s3/Bus_Parallel16.cpp +++ b/src/lgfx/v1/platforms/esp32s3/Bus_Parallel16.cpp @@ -80,6 +80,14 @@ namespace lgfx bool Bus_Parallel16::init(void) { + // esp_lcd_new_i80_bus does not configure pad direction / open-drain, so + // normalize the data pins as push-pull GPIO outputs first (same + // normalization used by Bus_EPD). + for (int pin : _cfg.pin_data) + { + lgfx::pinMode(pin, pin_mode_t::output); + } + _init_pin(); for (size_t i = 0; i < 3; ++i) diff --git a/src/lgfx/v1/platforms/esp32s3/Bus_Parallel8.cpp b/src/lgfx/v1/platforms/esp32s3/Bus_Parallel8.cpp index bdcfa05..a69ceac 100644 --- a/src/lgfx/v1/platforms/esp32s3/Bus_Parallel8.cpp +++ b/src/lgfx/v1/platforms/esp32s3/Bus_Parallel8.cpp @@ -80,6 +80,14 @@ namespace lgfx bool Bus_Parallel8::init(void) { + // esp_lcd_new_i80_bus does not configure pad direction / open-drain, so + // normalize the data pins as push-pull GPIO outputs first (same + // normalization used by Bus_EPD). + for (int pin : _cfg.pin_data) + { + lgfx::pinMode(pin, pin_mode_t::output); + } + _init_pin(); for (size_t i = 0; i < 3; ++i) From 69cf83ce9f905fee07c0774d84221139ee0e90eb Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Sat, 15 Aug 2026 08:25:15 +0000 Subject: [PATCH 4/4] Report LEDC setup failure from Light_PWM::init Light_PWM::init ignored the result of the LEDC configuration calls and always returned true. Propagate failures instead: init now fails early when the configured pin cannot be driven as a GPIO output, the ESP-IDF path checks both ledc_channel_config and ledc_timer_config, the Arduino 3.x path returns the ledcAttach result, and the pre-3.x path checks the frequency returned by ledcSetup (0 on failure) and only attaches the pin when setup succeeded. On failure the initial setBrightness call is skipped. The caller (Panel_Device::init) ignores the return value, so panel initialization is unaffected; the result is now meaningful for callers that choose to check it. --- src/lgfx/v1/platforms/esp32/Light_PWM.cpp | 24 +++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/lgfx/v1/platforms/esp32/Light_PWM.cpp b/src/lgfx/v1/platforms/esp32/Light_PWM.cpp index 82f3fff..2286881 100644 --- a/src/lgfx/v1/platforms/esp32/Light_PWM.cpp +++ b/src/lgfx/v1/platforms/esp32/Light_PWM.cpp @@ -21,13 +21,14 @@ Original Source: #include "Light_PWM.hpp" #include "common.hpp" +#include + #if defined ( ARDUINO ) #include #if __has_include() #include #endif #else - #include #include #endif @@ -40,7 +41,7 @@ Original Source: #endif #endif #if !defined LGFX_LEDCINIT // pre-3.x cores, including 1.x where ESP_ARDUINO_VERSION is absent - #define LGFX_LEDCINIT(pin_bl, freq, bits, pwm_channel) { ledcSetup(pwm_channel, freq, bits); ledcAttachPin(pin_bl, pwm_channel); } + #define LGFX_LEDCINIT(pin_bl, freq, bits, pwm_channel) ( ledcSetup(pwm_channel, freq, bits) > 0 && (ledcAttachPin(pin_bl, pwm_channel), true) ) #define LGFX_LEDCWRITE(pin_bl, pwm_channel, duty) ledcWrite(pwm_channel, duty) #endif @@ -66,19 +67,21 @@ namespace lgfx bool Light_PWM::init(uint8_t brightness) { + if ((size_t)_cfg.pin_bl >= GPIO_NUM_MAX || !GPIO_IS_VALID_OUTPUT_GPIO((gpio_num_t)_cfg.pin_bl)) + { + return false; + } + // The LEDC driver does not fully normalize the pad state: the open-drain // flag is never cleared, and ESP-IDF v6 no longer selects the GPIO // function in IO_MUX. Reconfigure the pin as a push-pull GPIO output // first, latching it to the "off" level to avoid a visible glitch. - if ((size_t)_cfg.pin_bl < GPIO_NUM_MAX) - { - if (_cfg.invert) { gpio_hi(_cfg.pin_bl); } else { gpio_lo(_cfg.pin_bl); } - lgfx::pinMode(_cfg.pin_bl, pin_mode_t::output); - } + if (_cfg.invert) { gpio_hi(_cfg.pin_bl); } else { gpio_lo(_cfg.pin_bl); } + lgfx::pinMode(_cfg.pin_bl, pin_mode_t::output); #if defined ( ARDUINO ) - LGFX_LEDCINIT(_cfg.pin_bl, _cfg.freq, PWM_BITS, _cfg.pwm_channel); + bool result = LGFX_LEDCINIT(_cfg.pin_bl, _cfg.freq, PWM_BITS, _cfg.pwm_channel); #else // esp-idf @@ -97,7 +100,7 @@ namespace lgfx #endif #endif }; - ledc_channel_config(&ledc_channel); + bool result = (ESP_OK == ledc_channel_config(&ledc_channel)); static ledc_timer_config_t ledc_timer; { ledc_timer.speed_mode = LGFX_LEDC_SPEED_MODE; // timer mode @@ -105,10 +108,11 @@ namespace lgfx ledc_timer.freq_hz = _cfg.freq; // frequency of PWM signal ledc_timer.timer_num = ledc_channel.timer_sel; // timer index }; - ledc_timer_config(&ledc_timer); + result = (ESP_OK == ledc_timer_config(&ledc_timer)) && result; #endif + if (!result) { return false; } setBrightness(brightness); return true; }