From f26a16fb5735c94cffe73fa58d2a363d65223738 Mon Sep 17 00:00:00 2001 From: Rob Kirberich Date: Sun, 26 Oct 2025 14:59:16 +0100 Subject: [PATCH 01/56] Use actual glyph width, not the guessed spaceWidth, to determine the width of the space character --- src/lgfx/v1/lgfx_fonts.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/lgfx/v1/lgfx_fonts.cpp b/src/lgfx/v1/lgfx_fonts.cpp index 76157177..4092c483 100644 --- a/src/lgfx/v1/lgfx_fonts.cpp +++ b/src/lgfx/v1/lgfx_fonts.cpp @@ -886,10 +886,7 @@ namespace lgfx int32_t sy = 65536 * style->size_y; y += (metrics->y_offset * sy) >> 16; - if (code == 0x20) { - gNum = 0xFFFF; - buffer[2] = getSwap32(this->spaceWidth); - } else if (!this->getUnicodeIndex(code, &gNum)) { + if (!this->getUnicodeIndex(code, &gNum)) { return drawCharDummy(gfx, x, y, this->spaceWidth, metrics->height, style, filled_x); } else { file->preRead(); From be0a2da343440d011db41b23e016bd4b702ea21b Mon Sep 17 00:00:00 2001 From: Forairaaaaa Date: Wed, 22 Jul 2026 16:56:40 +0800 Subject: [PATCH 02/56] Add CoreS3 ILI9342E panel support --- src/M5GFX.cpp | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index 5e8b4fba..7836fffc 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -413,6 +413,14 @@ namespace m5gfx static constexpr int_fast16_t aw9523_i2c_addr = 0x58; // AW9523B static constexpr int_fast16_t axp_i2c_addr = 0x34; // AXP2101 static constexpr int_fast16_t gc0308_i2c_addr = 0x21; // GC0308 + static constexpr int_fast16_t ft5x06_i2c_addr = 0x38; + static constexpr uint8_t ft5x06_cipher_reg = 0xA3; + static constexpr uint8_t ft5x06_firmid_reg = 0xA6; + static constexpr uint8_t ft5x06_vendid_reg = 0xA8; + static constexpr uint8_t ft5x06_ili9342c_firmid = 0x10; + static constexpr uint8_t ft5x06_ili9342e_firmid = 0x12; + static constexpr uint8_t ft5x06_m5stack_vendor = 0x11; + static constexpr int32_t ft5x06_version_i2c_freq = 100000; static constexpr int_fast16_t i2c_port = I2C_NUM_1; static constexpr int_fast16_t i2c_sda = GPIO_NUM_12; static constexpr int_fast16_t i2c_scl = GPIO_NUM_11; @@ -430,6 +438,61 @@ namespace m5gfx _rotation = 1; // default rotation } + void initPanelByTouchVersion() + { + uint8_t touch_cipher = 0; + uint8_t touch_firmid = 0; + uint8_t touch_vendid = 0; + bool touch_info_valid = false; + for (int retry = 0; retry < 5 && !touch_info_valid; ++retry) + { + auto set_work_mode = lgfx::i2c::writeRegister8( + i2c_port, ft5x06_i2c_addr, 0x00, 0x00, 0, ft5x06_version_i2c_freq); + auto read_cipher = lgfx::i2c::readRegister8( + i2c_port, ft5x06_i2c_addr, ft5x06_cipher_reg, ft5x06_version_i2c_freq); + auto read_firmid = lgfx::i2c::readRegister8( + i2c_port, ft5x06_i2c_addr, ft5x06_firmid_reg, ft5x06_version_i2c_freq); + auto read_vendid = lgfx::i2c::readRegister8( + i2c_port, ft5x06_i2c_addr, ft5x06_vendid_reg, ft5x06_version_i2c_freq); + touch_cipher = read_cipher.has_value() ? read_cipher.value() : 0; + touch_firmid = read_firmid.has_value() ? read_firmid.value() : 0; + touch_vendid = read_vendid.has_value() ? read_vendid.value() : 0; + touch_info_valid = set_work_mode.has_value() + && read_firmid.has_value() + && read_vendid.has_value() + && touch_vendid == ft5x06_m5stack_vendor + && (touch_firmid == ft5x06_ili9342c_firmid + || touch_firmid == ft5x06_ili9342e_firmid); + if (!touch_info_valid) + { + lgfx::delay(20); + } + } + + bool use_ili9342e = touch_info_valid + && touch_firmid == ft5x06_ili9342e_firmid; + if (touch_info_valid) + { + ESP_LOGI(LIBRARY_NAME, + "CoreS3 touch CIPHER:0x%02x / FIRMID:0x%02x / VENDID:0x%02x, panel:%s", + (int)touch_cipher, (int)touch_firmid, (int)touch_vendid, + use_ili9342e ? "ILI9342E" : "ILI9342C"); + } + else + { + ESP_LOGW(LIBRARY_NAME, + "CoreS3 touch version read failed (CIPHER:0x%02x / FIRMID:0x%02x / VENDID:0x%02x), panel:ILI9342C", + (int)touch_cipher, (int)touch_firmid, (int)touch_vendid); + } + + if (use_ili9342e) + { + startWrite(true); + command_list(getIli9342EInitCommands()); + endWrite(); + } + } + void rst_control(bool level) override { static constexpr uint8_t lcd_rst_bit = 1 << 1; // AW9523B P1_1 @@ -454,6 +517,31 @@ namespace m5gfx : GPIO_ENABLE1_W1TS_REG ) = 1u << (GPIO_NUM_35 & 31); } + + protected: + static const uint8_t* getIli9342EInitCommands() + { + static constexpr uint8_t list0[] = + { + 0xDD, 1, 0x01, + 0x3A, 1, 0x55, + 0x21, 0, + 0x36, 1, 0x08, + 0xD5, 1, 0x00, + 0xB1, 1, 0x22, + 0xC2, 9, 0x00,0x64,0x00,0x43,0x3C,0x03,0x03,0x03,0x03, + 0xC8, 1, 0x38, + 0xC9, 1, 0x1D, + 0xCA, 1, 0x1D, + 0xB7, 4, 0x5A,0x41,0x11,0x19, + 0xE4,15, 0x04,0x09,0x12,0x06,0x14,0x08,0x3C,0x77,0x4A,0x06,0x0D,0x09,0x18,0x24,0x07, + 0xE5,15, 0x02,0x03,0x08,0x06,0x14,0x08,0x38,0x56,0x4B,0x05,0x0F,0x0C,0x1F,0x1C,0x0D, + 0x11, 0 + CMD_INIT_DELAY, 120, + 0x29, 0 + CMD_INIT_DELAY, 120, + 0xFF,0xFF, + }; + return list0; + } }; struct Touch_M5StackCoreS3 : public lgfx::Touch_FT5x06 @@ -911,6 +999,13 @@ namespace m5gfx default: break; + case board_t::board_M5StackCoreS3: + case board_t::board_M5StackCoreS3SE: + case board_t::board_M5StackChan: + static_cast(_panel_last.get()) + ->initPanelByTouchVersion(); + break; + case board_t::board_M5StopWatch: auto p = reinterpret_cast(_panel_last.get()); if (p->initPanelFb() ) { From a19ebc5a9c4dc2daf95c8be8f28f2c385c9a4681 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Fri, 24 Jul 2026 21:20:59 +0900 Subject: [PATCH 03/56] Rework SSD1677 4-gray refresh into per-mode single-activation waveforms Panel_SSD1677_4Gray now dispatches quality/text/fast to a shared absolute refresh path (Mode 2 face-parity recovery, parity-aware full-frame transfer, one Mode 1 activation) whose only per-mode difference is a static LUT, and fastest to a differential monochrome update with a static LUT and software-computed transition groups. - quality: oscillation prefix erases image history with a group-balanced net drive, preventing ghost accumulation over repeated refreshes; the tone-forming tail runs twice so the endpoints stay as stable as the vendor waveform ones - text: white-first waveform, forms the four levels without a black flash - fast: absolute 4-gray waveform derived from the community SDK, with the black darken return shortened to 28 frames (the excess overdrive rebounds toward gray during later undriven holds) - fastest: 2-frame reverse-polarity prepulse plus 8-frame dose with state-aware holds outside the updated region; VCOM matches the panel offset so held pixels sit field-free - absolute refreshes wait on the BUSY line without the 400 ms refresh-minimum floor - the fastest fallback now records its display baseline, so a fastest-only usage no longer degrades into a full refresh on every update --- src/lgfx/v1/panel/Panel_SSD1677.cpp | 418 ++++++++-------------------- src/lgfx/v1/panel/Panel_SSD1677.hpp | 11 +- 2 files changed, 113 insertions(+), 316 deletions(-) diff --git a/src/lgfx/v1/panel/Panel_SSD1677.cpp b/src/lgfx/v1/panel/Panel_SSD1677.cpp index ce7f63e2..30c2bf61 100644 --- a/src/lgfx/v1/panel/Panel_SSD1677.cpp +++ b/src/lgfx/v1/panel/Panel_SSD1677.cpp @@ -64,109 +64,112 @@ namespace lgfx // Then voltages [VGH, VSH1, VSH2, VSL, VCOM] (bytes 105..109) -> 0x03/0x04/0x2C. //-------------------------------------------------------------------------- - static constexpr uint8_t lut_fastest[110] = { - 0x55, 0x55, 0x55, 0x55, 0x55, 0x5A, 0xAA, 0xAA, 0x00, 0x00, // LUT0: 00 white - 0xAA, 0x95, 0x55, 0x55, 0x55, 0x5A, 0x82, 0xA0, 0x00, 0x00, // LUT1: 01 light - 0xAA, 0xA5, 0x55, 0x55, 0x55, 0x5A, 0xA0, 0x00, 0x00, 0x00, // LUT2: 10 dark - 0xAA, 0xAA, 0xAA, 0xAA, 0x55, 0x55, 0x55, 0x55, 0x00, 0x00, // LUT3: 11 black - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VCOM - 0x02, 0x02, 0x02, 0x02, 0x00, // TP G0: A, B, C, D, RP - 0x02, 0x02, 0x02, 0x02, 0x00, // TP G1: A, B, C, D, RP - 0x02, 0x02, 0x02, 0x02, 0x00, // TP G2: A, B, C, D, RP - 0x02, 0x02, 0x02, 0x02, 0x00, // TP G3: A, B, C, D, RP - 0x02, 0x02, 0x02, 0x02, 0x00, // TP G4: A, B, C, D, RP - 0x02, 0x02, 0x02, 0x02, 0x00, // TP G5: A, B, C, D, RP - 0x02, 0x02, 0x02, 0x02, 0x00, // TP G6: A, B, C, D, RP - 0x02, 0x02, 0x02, 0x02, 0x00, // TP G7: A, B, C, D, RP - 0x00, 0x00, 0x00, 0x00, 0x00, // TP G8: A, B, C, D, RP - 0x00, 0x00, 0x00, 0x00, 0x00, // TP G9: A, B, C, D, RP - 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, // frame rate - 0x17, 0x41, 0xA8, 0x32, 0x30, // VGH, VSH1, VSH2, VSL, VCOM(-1.2V) - }; - - // Factory absolute LUTs. 2-bit pixel encoding: BW=bit0(LSB), RED=bit1(MSB). - // 00=black, 01=dark, 10=light, 11=white. (i.e. value v(0..3) = (MSB<<1)|LSB.) - static constexpr uint8_t lut_factory_fast[110] = { - 0x00, 0x4A, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT0: 00 black - 0x80, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT1: 01 dark - 0x88, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT2: 10 light - 0xA8, 0x44, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT3: 11 white + // VS codes: 0 = VSS (no drive), 1 = VSH1 (darken), 2 = VSL (lighten), + // 3 = VSH2 (weak darken). + // For the absolute waveforms (quality/text/fast) the RAM group selects the + // target level: group 0 = white, 1 = light gray, 2 = dark gray, 3 = black. + + // Quality: an oscillation prefix (12 strokes, 5 frames each) erases the + // previous image, then the factory 4-gray tail forms the target. + // Keep |VSH1| == |VSL|: the net drive stays identical for all groups, + // which prevents image-correlated ghosting over repeated refreshes. + // The tail runs twice (repeat on its timing groups); a single pass forms + // endpoints shallow enough that whites visibly mottle within a minute. + static constexpr uint8_t lut_quality[110] = { + 0x66, 0x66, 0x00, 0x4A, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, // white + 0x66, 0x66, 0x80, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // light + 0x66, 0x66, 0x88, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // dark + 0x66, 0x66, 0xA8, 0x44, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, // black 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VCOM - 0x09, 0x0C, 0x03, 0x03, 0x00, - 0x0F, 0x03, 0x07, 0x03, 0x00, - 0x03, 0x00, 0x02, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x05, 0x05, 0x05, 0x00, + 0x05, 0x05, 0x05, 0x05, 0x01, + 0x08, 0x0B, 0x02, 0x03, 0x01, + 0x0C, 0x02, 0x07, 0x02, 0x01, + 0x01, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x44, 0x44, 0x44, 0x44, 0x44, // frame rate (faster clock) - 0x17, 0x41, 0xA8, 0x32, 0x50, // VGH, VSH1, VSH2, VSL, VCOM(-2.0V) + 0x22, 0x22, 0x22, 0x22, 0x22, // frame rate + 0x17, 0x46, 0xA8, 0x36, 0x30, // VGH, VSH1(+16V), VSH2, VSL(-16V), VCOM(-1.2V) }; - static constexpr uint8_t lut_factory_quality[110] = { - 0x00, 0x4A, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT0: 00 black - 0x80, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT1: 01 dark - 0x88, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT2: 10 light - 0xA8, 0x44, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT3: 11 white + // Text: drive every pixel to white first (24 lightening frames, no black + // flash), then per-group darkening tails form the four levels. The net + // drive differs by group, so repeated text refreshes can leave + // image-correlated ghosting after power-off. + static constexpr uint8_t lut_text[110] = { + 0xA8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // white + 0xA9, 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // light + 0xA9, 0x55, 0x5C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // dark + 0xA9, 0x55, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // black 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VCOM - 0x08, 0x0B, 0x02, 0x03, 0x00, - 0x0C, 0x02, 0x07, 0x02, 0x00, - 0x01, 0x00, 0x02, 0x00, 0x00, + 0x08, 0x08, 0x08, 0x01, 0x00, + 0x01, 0x01, 0x01, 0x01, 0x00, + 0x01, 0x01, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, - 0x22, 0x22, 0x22, 0x22, 0x22, // frame rate (slower clock) - 0x17, 0x41, 0xA8, 0x32, 0x30, // VGH, VSH1, VSH2, VSL, VCOM(-1.2V) + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x22, 0x22, 0x22, 0x22, 0x22, // frame rate + 0x17, 0x46, 0xA8, 0x36, 0x30, // VGH, VSH1(+16V), VSH2, VSL(-16V), VCOM(-1.2V) }; - // Single-activation Quality waveform. The first eight phases apply the same - // VSL/VSH1 sequence to all four LUT groups (four rapid white/black round - // trips, two frames per phase). The remaining phases are the calibrated - // four-gray target waveform. No intermediate image transfer or separate - // activation is required. - static constexpr uint8_t lut_quality_oscillating[110] = { - 0x99, 0x99, 0x00, 0x4A, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT0 - 0x99, 0x99, 0x80, 0x60, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT1 - 0x99, 0x99, 0x88, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT2 - 0x99, 0x99, 0xA8, 0x44, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT3 + // Fast: absolute 4-gray waveform from community-sdk (oscillating 2-frame + // strokes). The net drive differs by group, so heavy repetition can leave + // image-correlated ghosting. + // The black darken return is 28 frames, not the original 32: the last + // 4 frames overdrive black and the excess rebounds toward gray during + // later undriven holds. + static constexpr uint8_t lut_fast[110] = { + 0x55, 0x55, 0x55, 0x55, 0x55, 0x5A, 0xAA, 0xAA, 0x00, 0x00, // white + 0xAA, 0x95, 0x55, 0x55, 0x55, 0x5A, 0x82, 0xA0, 0x00, 0x00, // light + 0xAA, 0xA5, 0x55, 0x55, 0x55, 0x5A, 0xA0, 0x00, 0x00, 0x00, // dark + 0xAA, 0xAA, 0xAA, 0xAA, 0x55, 0x55, 0x55, 0x50, 0x00, 0x00, // black 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VCOM 0x02, 0x02, 0x02, 0x02, 0x00, 0x02, 0x02, 0x02, 0x02, 0x00, - 0x08, 0x0B, 0x02, 0x03, 0x00, - 0x0C, 0x02, 0x07, 0x02, 0x00, - 0x01, 0x00, 0x02, 0x00, 0x00, + 0x02, 0x02, 0x02, 0x02, 0x00, + 0x02, 0x02, 0x02, 0x02, 0x00, + 0x02, 0x02, 0x02, 0x02, 0x00, + 0x02, 0x02, 0x02, 0x02, 0x00, + 0x02, 0x02, 0x02, 0x02, 0x00, + 0x02, 0x02, 0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, // frame rate + 0x17, 0x41, 0xA8, 0x32, 0x30, // VGH, VSH1(+15V), VSH2, VSL(-15V), VCOM(-1.2V) + }; + + // Fastest: differential monochrome update (Mode 2). Here the RAM group is + // the transition class computed in _send_transition_plane: group 0 holds + // dark pixels, 3 holds light pixels, 1 drives black-to-white and 2 drives + // white-to-black. A 2-frame reverse-polarity prepulse precedes the + // 8-frame dose to curb ghosting from repeated partial updates. + static constexpr uint8_t lut_fastest[110] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // hold dark + 0x6A, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // black -> white + 0x95, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // white -> black + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // hold light + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VCOM + 0x02, 0x02, 0x02, 0x02, 0x00, + 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x22, 0x22, 0x22, 0x22, 0x22, - 0x17, 0x41, 0xA8, 0x36, 0x30, - }; - - // Differential Mode 2 base. Groups 00 and 11 are state-aware holds, - // group 01 lightens, and group 10 darkens. The actual pulse positions and - // durations are installed for each transition step below. - static constexpr uint8_t lut_gray_differential[110] = { - 0,0,0,0,0,0,0,0,0,0, - 0x54,0x54,0x40,0,0,0,0,0,0,0, - 0xAA,0xA0,0xA8,0,0,0,0,0,0,0, - 0xA2,0x22,0x20,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0, - 1,1,1,1,0, 1,1,1,1,0, - 1,1,1,1,0, 0,0,0,0,0, - 0,0,0,0,0, 0,0,0,0,0, - 0,0,0,0,0, 0,0,0,0,0, - 0,0,0,0,0, 0,0,0,0,0, - 0x8F,0x8F,0x8F,0x8F,0x8F, - 0x17,0x41,0xA8,0x32,0x30, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, // frame rate + // VCOM -1.2V matches the panel offset so held pixels sit field-free; + // more negative values slowly darken and mottle non-updated areas. + 0x17, 0x46, 0xA8, 0x32, 0x30, // VGH, VSH1(+16V), VSH2, VSL(-15V), VCOM(-1.2V) }; // Write a 110-byte LUT: 105 waveform bytes -> 0x32, voltages -> 0x03/0x04/0x2C. @@ -724,29 +727,18 @@ namespace lgfx _displayed_valid = true; } - static uint8_t transition_group(uint8_t old_level, uint8_t new_level, - uint8_t step, bool monochrome) + // Transition class for lut_fastest: the new level is thresholded to + // monochrome, unchanged pixels fall into the hold group of their side. + static uint8_t transition_group(uint8_t old_level, uint8_t new_level) { - if (monochrome) - { - new_level = new_level < 2 ? 0 : 3; - if (old_level == new_level) { return old_level < 2 ? 0 : 3; } - return old_level < new_level ? 1 : 2; - } - const uint8_t light_boundary = step; - const uint8_t dark_boundary = 2 - step; - const bool light = old_level < new_level - && old_level <= light_boundary - && new_level > light_boundary; - const bool dark = old_level > new_level - && old_level > dark_boundary - && new_level <= dark_boundary; - return light ? 1 : dark ? 2 : old_level < 2 ? 0 : 3; + new_level = new_level < 2 ? 0 : 3; + if (old_level == new_level) { return old_level < 2 ? 0 : 3; } + return old_level < new_level ? 1 : 2; } void Panel_SSD1677_4Gray::_send_transition_plane( uint8_t command, const uint8_t* new_lsb, const uint8_t* new_msb, - const range_rect_t& dirty, uint8_t step, bool monochrome) + const range_rect_t& dirty) { const auto full = _full_range(); const uint32_t row_bytes = ((_cfg.panel_width + 7) & ~7) >> 3; @@ -773,8 +765,7 @@ namespace lgfx | ((old_msb[index] & mask) ? 2 : 0); const uint8_t new_level = ((new_lsb[index] & mask) ? 1 : 0) | ((new_msb[index] & mask) ? 2 : 0); - const uint8_t group = transition_group(old_level, new_level, - step, monochrome); + const uint8_t group = transition_group(old_level, new_level); const uint8_t group_bit = command == CMD_WRITE_RAM_BW ? 1 : 2; if (group & group_bit) { row[x >> 3] |= mask; } else { row[x >> 3] &= uint8_t(~mask); } @@ -784,132 +775,6 @@ namespace lgfx } } - bool Panel_SSD1677_4Gray::_activate_transition_step( - const uint8_t* new_lsb, const uint8_t* new_msb, - const range_rect_t& dirty, uint8_t step, bool monochrome, - bool shortened, bool stronger_mono) - { - static constexpr uint8_t light_phases[] = {7, 4, 9}; - static constexpr uint8_t vsl[] = {0x36, 0x32, 0x32}; - static constexpr uint8_t dark_phases[] = {7, 6, 4}; - static constexpr uint8_t vsh1[] = {0x46, 0x3F, 0x46}; - static constexpr uint8_t light_position[] = { - 0, 1, 3, 5, 7, 9, 11, 13, 15, - }; - static constexpr uint8_t dark_position[] = { - 0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, - }; - - _send_transition_plane(CMD_WRITE_RAM_BW, new_lsb, new_msb, - dirty, step, monochrome); - _send_transition_plane(CMD_WRITE_RAM_RED, new_lsb, new_msb, - dirty, step, monochrome); - - uint8_t lut[sizeof(lut_gray_differential)]; - memcpy(lut, lut_gray_differential, sizeof(lut)); - memset(lut, 0, 40); // groups 00 and 11 start as no-drive - - if (monochrome || step == 2) - { - // Drive lightening and darkening concurrently. Fast uses the validated - // 12-frame 3+3+3+2+1 dose; Fastest uses the accepted eight-frame - // 2+2+2+1+1 dose. - // The fifth Fastest phase is already present for the darkening group. - // Use that same final frame for lightening as well: this raises the - // black-to-white dose from seven to eight frames without extending the - // eight-frame activation or changing outside-area hold groups. A - // 30-step navigation UI test retained the same 105 ms average and made - // the erased black cursor residue visually negligible. - const uint8_t light_end = shortened ? 5 : 8; - const uint8_t dark_end = shortened ? 5 : 8; - for (uint8_t position = 0; position < light_end; ++position) - { - lut[10 + (position >> 2)] |= - uint8_t{2} << (6 - ((position & 3) << 1)); - } - for (uint8_t position = 0; position < dark_end; ++position) - { - lut[20 + (position >> 2)] |= - uint8_t{1} << (6 - ((position & 3) << 1)); - } - for (uint8_t timing_group = 0; timing_group < 4; ++timing_group) - { - for (uint8_t phase = 0; phase < 4; ++phase) - { - const uint8_t position = timing_group * 4 + phase; - uint8_t tp = 0; - if (shortened) - { - if (stronger_mono) - { - // Fast retains the validated 12-frame dose. - if (position < 3) { tp = 3; } - else if (position == 3) { tp = 2; } - else if (position == 4) { tp = 1; } - } - else - { - // Fastest: keep the same five pulse positions and - // calibrated voltages, but shorten 3+3+3+2+1 to 2+2+2+1+1. - // This isolates the optical effect of an eight-frame dose from - // the already-validated state-aware outside-area holds. - if (position < 3) { tp = 2; } - else if (position < 5) { tp = 1; } - } - } - else - { - if (position < 7) { tp = 3; } - else if (position == 7) { tp = 1; } - } - lut[50 + timing_group * 5 + phase] = tp; - } - } - } - else - { - const uint8_t light_boundary = step; - const uint8_t dark_boundary = 2 - step; - for (uint8_t i = 0; i < light_phases[light_boundary]; ++i) - { - const uint8_t position = light_position[i]; - lut[10 + (position >> 2)] |= - uint8_t{2} << (6 - ((position & 3) << 1)); - } - for (uint8_t i = 0; i < dark_phases[dark_boundary]; ++i) - { - const uint8_t position = dark_position[i]; - lut[20 + (position >> 2)] |= - uint8_t{1} << (6 - ((position & 3) << 1)); - } - for (uint8_t timing_group = 0; timing_group < 4; ++timing_group) - { - for (uint8_t phase = 0; phase < 4; ++phase) - { - lut[50 + timing_group * 5 + phase] = 1; - } - } - } - - // Groups 00 and 11 remain source-no-drive holds. Repeated 12-frame tests - // showed that reducing VCOM from -1.2 V to -0.4 V greatly accelerated - // whiteward drift in untouched black/dark pixels. Move in the opposite - // direction. Hardware validation over 60 consecutive dirty updates found - // the SSD1677 factory-fast value (-2.0 V) neutral for all four untouched - // gray levels, so both monochrome differential modes use it while the - // four-gray transition passes retain -1.2 V. - if (monochrome) { lut[109] = 0x50; } - const uint8_t dark_boundary = monochrome ? 0 : uint8_t(2 - step); - const uint8_t light_boundary = monochrome ? 2 : step; - lut[106] = vsh1[dark_boundary]; - // Fast gives black-to-white transitions the full twelfth frame and the - // calibrated -16 V VSL. Fastest retains the lower-dose -15 V setting. - lut[108] = stronger_mono ? 0x36 : vsl[light_boundary]; - _send_gray_lut(lut); - return _activate(CTRL1_NORMAL, 0x0C, false, true, - !shortened); - } - bool Panel_SSD1677_4Gray::_activate(uint8_t ctrl1, uint8_t ctrl2, bool powers_down, bool mode2_activation, bool enforce_refresh_minimum) { @@ -962,73 +827,31 @@ namespace lgfx return true; } - bool Panel_SSD1677_4Gray::_refresh_quality(const uint8_t* lsb, const uint8_t* msb) + bool Panel_SSD1677_4Gray::_refresh_absolute(const uint8_t* lsb, const uint8_t* msb, + const uint8_t* lut) { // A sleep/power transition can invalidate the controller's Mode 2 face - // parity before the first visible refresh. Quality is an absolute, - // full-screen operation, so recover a deterministic RAM face here rather - // than silently dropping the user's first frame. + // parity before the first visible refresh. Absolute modes are + // full-screen operations, so recover a deterministic RAM face here + // rather than silently dropping the user's first frame. if (!_mode2_face_known && !_reset_controller_for_mode2()) { _invalidate_gray_state(); return false; } - const auto full = _full_range(); - const uint8_t* bw = _mode2_face_odd ? msb : lsb; - const uint8_t* red = _mode2_face_odd ? lsb : msb; - _send_plane(CMD_WRITE_RAM_BW, bw, full, true); - _send_plane(CMD_WRITE_RAM_RED, red, full, true); - _send_gray_lut(lut_quality_oscillating); - if (!_activate(CTRL1_NORMAL, 0x07, true, false)) { return false; } - _optical_state = optical_state_t::gray4; - return true; - } - - bool Panel_SSD1677_4Gray::_refresh_text(const uint8_t* lsb, const uint8_t* msb) - { - const auto full = _full_range(); // The Mode 2 ping-pong face exchanges the physical interpretation of the - // two middle codes. Match Quality's parity-aware plane order before the - // self-contained white-first waveform; 00 black and 11 white are - // symmetric, so a parity error otherwise appears only as dark/light swap. + // two middle codes: swap the plane destinations on the odd face. 00 black + // and 11 white are symmetric, so a parity error would otherwise appear + // only as a dark/light swap. + const auto full = _full_range(); const uint8_t* bw = _mode2_face_odd ? msb : lsb; const uint8_t* red = _mode2_face_odd ? lsb : msb; _send_plane(CMD_WRITE_RAM_BW, bw, full, true); _send_plane(CMD_WRITE_RAM_RED, red, full, true); - uint8_t lut[sizeof(lut_factory_quality)] = {}; - auto set_vs = [&](uint8_t group, uint8_t phase, uint8_t code) - { - const size_t index = size_t(group) * 10 + (phase >> 2); - const uint8_t shift = 6 - 2 * (phase & 3); - lut[index] |= uint8_t(code << shift); - }; - auto set_tp = [&](uint8_t phase, uint8_t frames) - { - lut[50 + size_t(phase >> 2) * 5 + (phase & 3)] = frames; - }; - for (uint8_t phase = 0; phase < 3; ++phase) - { - for (uint8_t group = 0; group < 4; ++group) { set_vs(group, phase, 2); } - set_tp(phase, 8); - } - for (uint8_t phase = 3; phase < 12; ++phase) - { - set_vs(3, phase, 1); - if (phase <= 9) { set_vs(2, phase, 1); } - if (phase <= 4) { set_vs(1, phase, 1); } - set_tp(phase, phase == 10 ? 5 : phase == 11 ? 6 : 1); - } - set_vs(2, 10, 3); - for (uint8_t phase = 5; phase <= 9; ++phase) { set_vs(1, phase, 3); } - for (size_t i = 100; i < 105; ++i) { lut[i] = 0x22; } - lut[105] = 0x17; - lut[106] = 0x46; - lut[107] = 0xA8; - lut[108] = 0x36; - lut[109] = 0x30; _send_gray_lut(lut); - if (!_activate(CTRL1_NORMAL, 0x07, true, false)) { return false; } + // Wait on the BUSY line itself, without the 400 ms refresh-minimum floor. + if (!_activate(CTRL1_NORMAL, 0x07, true, false, false)) { return false; } _optical_state = optical_state_t::gray4; return true; } @@ -1059,39 +882,18 @@ namespace lgfx } } - bool Panel_SSD1677_4Gray::_refresh_fast(const uint8_t* lsb, const uint8_t* msb, - const range_rect_t& current_dirty) - { - if (!_displayed_valid) - { - return _refresh_quality(lsb, msb); - } - - // Fast retains the 12-frame activation and stable outside-area holds, and - // uses a stronger lightening dose than the eight-frame Fastest path to - // reduce black-to-white ghosting in the rewritten rectangle. - if (!_activate_transition_step(lsb, msb, current_dirty, 2, true, true, true)) - { - return false; - } - - _optical_state = optical_state_t::mono_synchronized; - _remember_mono_dirty(msb, current_dirty); - return true; - } - bool Panel_SSD1677_4Gray::_refresh_fastest(const uint8_t* lsb, const uint8_t* msb, const range_rect_t& current_dirty) { if (!_displayed_valid) { - return _refresh_quality(lsb, msb); + return _refresh_absolute(lsb, msb, lut_quality); } - if (!_activate_transition_step(lsb, msb, current_dirty, 2, true, true)) - { - return false; - } + _send_transition_plane(CMD_WRITE_RAM_BW, lsb, msb, current_dirty); + _send_transition_plane(CMD_WRITE_RAM_RED, lsb, msb, current_dirty); + _send_gray_lut(lut_fastest); + if (!_activate(CTRL1_NORMAL, 0x0C, false, true, false)) { return false; } _optical_state = optical_state_t::mono_synchronized; _remember_mono_dirty(msb, current_dirty); @@ -1123,13 +925,13 @@ namespace lgfx switch (mode) { case epd_mode_t::epd_quality: - success = _refresh_quality(planeL, planeM); + success = _refresh_absolute(planeL, planeM, lut_quality); break; case epd_mode_t::epd_text: - success = _refresh_text(planeL, planeM); + success = _refresh_absolute(planeL, planeM, lut_text); break; case epd_mode_t::epd_fast: - success = _refresh_fast(planeL, planeM, current_dirty); + success = _refresh_absolute(planeL, planeM, lut_fast); break; case epd_mode_t::epd_fastest: success = _refresh_fastest(planeL, planeM, current_dirty); @@ -1141,7 +943,9 @@ namespace lgfx if (success) { - if (mode == epd_mode_t::epd_quality || mode == epd_mode_t::epd_text) + // Every absolute refresh (including Fastest's fallback) leaves the full + // 2-bit planes on glass, so record them as the differential baseline. + if (_optical_state == optical_state_t::gray4) { _remember_displayed(planeL, planeM); } diff --git a/src/lgfx/v1/panel/Panel_SSD1677.hpp b/src/lgfx/v1/panel/Panel_SSD1677.hpp index 14bb3e2b..b7fa0431 100644 --- a/src/lgfx/v1/panel/Panel_SSD1677.hpp +++ b/src/lgfx/v1/panel/Panel_SSD1677.hpp @@ -145,19 +145,12 @@ namespace lgfx void _remember_displayed(const uint8_t* lsb, const uint8_t* msb); void _send_transition_plane(uint8_t command, const uint8_t* new_lsb, const uint8_t* new_msb, - const range_rect_t& dirty, uint8_t step, - bool monochrome); - bool _activate_transition_step(const uint8_t* new_lsb, const uint8_t* new_msb, - const range_rect_t& dirty, uint8_t step, - bool monochrome, bool shortened = false, - bool stronger_mono = false); + const range_rect_t& dirty); void _remember_mono_dirty(const uint8_t* msb, const range_rect_t& dirty); bool _activate(uint8_t ctrl1, uint8_t ctrl2, bool powers_down, bool mode2_activation, bool enforce_refresh_minimum = true); bool _reset_controller_for_mode2(void); - bool _refresh_quality(const uint8_t* lsb, const uint8_t* msb); - bool _refresh_text(const uint8_t* lsb, const uint8_t* msb); - bool _refresh_fast(const uint8_t* lsb, const uint8_t* msb, const range_rect_t& dirty); + bool _refresh_absolute(const uint8_t* lsb, const uint8_t* msb, const uint8_t* lut); bool _refresh_fastest(const uint8_t* lsb, const uint8_t* msb, const range_rect_t& dirty); }; From 075331404f69655544137f7413191b59eddd54c9 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 27 Jul 2026 12:51:48 +0900 Subject: [PATCH 04/56] Make QOI_DEBUG opt-in lgfx_qoi.c unconditionally defined QOI_DEBUG, so error paths always called fprintf(stderr). This pulls in newlib stdio and can fail to link on cores that do not provide the _write syscall. The png/jpg decoders reference no stdio; this aligns qoi with them. Define QOI_DEBUG externally to re-enable the debug output. --- src/lgfx/utility/lgfx_qoi.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lgfx/utility/lgfx_qoi.c b/src/lgfx/utility/lgfx_qoi.c index da859b20..478e4683 100644 --- a/src/lgfx/utility/lgfx_qoi.c +++ b/src/lgfx/utility/lgfx_qoi.c @@ -67,8 +67,6 @@ struct _qoi_t uint8_t read_buf[LGFX_QOI_READBUF_LEN]; }; -#define QOI_DEBUG - #ifdef QOI_DEBUG #define debug_printf(...) fprintf(stderr, __VA_ARGS__) #define QOI_ERROR(...) (fprintf(stderr, __VA_ARGS__), -2) From 177f28e31f5dfaa380d3b1e036c6e96be7a3e572 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 27 Jul 2026 12:51:48 +0900 Subject: [PATCH 05/56] Fix -Wsign-compare warning in Touch_CST226::getTouchRaw The loop counter was int while count is uint_fast8_t. On ARM cores uint_fast8_t is unsigned int, so the comparison triggers -Wsign-compare (on xtensa/riscv it is unsigned char, promoted to int, which is why ESP32 builds never warned). Harmless at runtime; use the same type as count. --- src/lgfx/v1/touch/Touch_CSTxxx.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lgfx/v1/touch/Touch_CSTxxx.cpp b/src/lgfx/v1/touch/Touch_CSTxxx.cpp index 07df4ca2..ea8ca19c 100644 --- a/src/lgfx/v1/touch/Touch_CSTxxx.cpp +++ b/src/lgfx/v1/touch/Touch_CSTxxx.cpp @@ -315,7 +315,7 @@ namespace lgfx if( requested_count > 1 ) // an array of touch_point_t was provided { int pIdx = 0; // points index with corrected offset - for (int i=0; i> 4) & 0xf)); From 0c4b8fe68c7b1bab3881d39d687be77574acf326 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 27 Jul 2026 19:34:32 +0900 Subject: [PATCH 06/56] Finalize SSD1677 four-mode refresh handling Use stable absolute Mode 1 waveforms for Quality and Text, absolute Mode 2 for Fast, and differential Mode 2 for Fastest. Preserve ping-pong face parity across mode changes and rebuild display history after sleep or power transitions. Optimize transition-plane generation and keep waveform-tuning helpers out of the public panel API. --- src/lgfx/v1/panel/Panel_SSD1677.cpp | 269 +++++++++++++++++----------- src/lgfx/v1/panel/Panel_SSD1677.hpp | 32 ++-- 2 files changed, 188 insertions(+), 113 deletions(-) diff --git a/src/lgfx/v1/panel/Panel_SSD1677.cpp b/src/lgfx/v1/panel/Panel_SSD1677.cpp index 30c2bf61..232f7005 100644 --- a/src/lgfx/v1/panel/Panel_SSD1677.cpp +++ b/src/lgfx/v1/panel/Panel_SSD1677.cpp @@ -34,7 +34,6 @@ namespace lgfx //---------------------------------------------------------------------------- static constexpr int8_t Bayer[16] = { -30, 18, -22, 26, -14, 2, -6, 10, -18, 30, -26, 22, -2, 14, -10, 6 }; - // static constexpr int8_t Bayer[16] = { 0, }; // SSD1677 commands static constexpr uint8_t CMD_DEEP_SLEEP = 0x10; @@ -58,7 +57,7 @@ namespace lgfx static constexpr uint8_t CTRL1_BYPASS_RED = 0x40; //-------------------------------------------------------------------------- - // LUTs (ported from community-sdk EInkDisplay, non-X3 / GDEQ0426T82). + // SSD1677 LUT layout. // Layout: VS patterns (5 groups x 10 bytes) + TP/RP timing (10 groups x 5 bytes) // + frame rate (5 bytes) = 105 bytes -> command 0x32. // Then voltages [VGH, VSH1, VSH2, VSL, VCOM] (bytes 105..109) -> 0x03/0x04/0x2C. @@ -69,12 +68,12 @@ namespace lgfx // For the absolute waveforms (quality/text/fast) the RAM group selects the // target level: group 0 = white, 1 = light gray, 2 = dark gray, 3 = black. - // Quality: an oscillation prefix (12 strokes, 5 frames each) erases the - // previous image, then the factory 4-gray tail forms the target. + // Quality: an oscillation prefix erases the previous image, then the + // four-gray tail forms the target. // Keep |VSH1| == |VSL|: the net drive stays identical for all groups, // which prevents image-correlated ghosting over repeated refreshes. - // The tail runs twice (repeat on its timing groups); a single pass forms - // endpoints shallow enough that whites visibly mottle within a minute. + // The timing groups repeat the tail to stabilize the black and white + // endpoints. static constexpr uint8_t lut_quality[110] = { 0x66, 0x66, 0x00, 0x4A, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, // white 0x66, 0x66, 0x80, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // light @@ -95,37 +94,8 @@ namespace lgfx 0x17, 0x46, 0xA8, 0x36, 0x30, // VGH, VSH1(+16V), VSH2, VSL(-16V), VCOM(-1.2V) }; - // Text: drive every pixel to white first (24 lightening frames, no black - // flash), then per-group darkening tails form the four levels. The net - // drive differs by group, so repeated text refreshes can leave - // image-correlated ghosting after power-off. + // Text: 64-frame absolute four-gray waveform for Mode 1. static constexpr uint8_t lut_text[110] = { - 0xA8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // white - 0xA9, 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // light - 0xA9, 0x55, 0x5C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // dark - 0xA9, 0x55, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // black - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VCOM - 0x08, 0x08, 0x08, 0x01, 0x00, - 0x01, 0x01, 0x01, 0x01, 0x00, - 0x01, 0x01, 0x05, 0x06, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, - 0x22, 0x22, 0x22, 0x22, 0x22, // frame rate - 0x17, 0x46, 0xA8, 0x36, 0x30, // VGH, VSH1(+16V), VSH2, VSL(-16V), VCOM(-1.2V) - }; - - // Fast: absolute 4-gray waveform from community-sdk (oscillating 2-frame - // strokes). The net drive differs by group, so heavy repetition can leave - // image-correlated ghosting. - // The black darken return is 28 frames, not the original 32: the last - // 4 frames overdrive black and the excess rebounds toward gray during - // later undriven holds. - static constexpr uint8_t lut_fast[110] = { 0x55, 0x55, 0x55, 0x55, 0x55, 0x5A, 0xAA, 0xAA, 0x00, 0x00, // white 0xAA, 0x95, 0x55, 0x55, 0x55, 0x5A, 0x82, 0xA0, 0x00, 0x00, // light 0xAA, 0xA5, 0x55, 0x55, 0x55, 0x5A, 0xA0, 0x00, 0x00, 0x00, // dark @@ -145,8 +115,32 @@ namespace lgfx 0x17, 0x41, 0xA8, 0x32, 0x30, // VGH, VSH1(+15V), VSH2, VSL(-15V), VCOM(-1.2V) }; + // Fast: 48-frame absolute four-gray waveform for Mode 2. + // The first 16 phases run for one frame and the remaining phases for two, + // while the final HOLD phase of each middle-gray row uses VSH2 for a weak + // two-frame darkening trim. + static constexpr uint8_t lut_fast[110] = { + 0x55, 0x55, 0x55, 0x55, 0x55, 0x5A, 0xAA, 0xAA, 0x00, 0x00, // white + 0xAA, 0x95, 0x55, 0x55, 0x55, 0x5A, 0x82, 0xAC, 0x00, 0x00, // light + 0xAA, 0xA5, 0x55, 0x55, 0x55, 0x5A, 0xAC, 0x00, 0x00, 0x00, // dark + 0xAA, 0xAA, 0xAA, 0xAA, 0x55, 0x55, 0x55, 0x50, 0x00, 0x00, // black + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VCOM + 0x01, 0x01, 0x01, 0x01, 0x00, + 0x01, 0x01, 0x01, 0x01, 0x00, + 0x01, 0x01, 0x01, 0x01, 0x00, + 0x01, 0x01, 0x01, 0x01, 0x00, + 0x02, 0x02, 0x02, 0x02, 0x00, + 0x02, 0x02, 0x02, 0x02, 0x00, + 0x02, 0x02, 0x02, 0x02, 0x00, + 0x02, 0x02, 0x02, 0x02, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, // frame rate + 0x17, 0x41, 0xA8, 0x32, 0x30, // VGH, VSH1(+15V), VSH2, VSL(-15V), VCOM(-1.2V) + }; + // Fastest: differential monochrome update (Mode 2). Here the RAM group is - // the transition class computed in _send_transition_plane: group 0 holds + // the transition class computed in _send_transition_planes: group 0 holds // dark pixels, 3 holds light pixels, 1 drives black-to-white and 2 drives // white-to-black. A 2-frame reverse-polarity prepulse precedes the // 8-frame dose to curb ghosting from repeated partial updates. @@ -167,8 +161,7 @@ namespace lgfx 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, // frame rate - // VCOM -1.2V matches the panel offset so held pixels sit field-free; - // more negative values slowly darken and mottle non-updated areas. + // VCOM -1.2V keeps the hold groups near field-free. 0x17, 0x46, 0xA8, 0x32, 0x30, // VGH, VSH1(+16V), VSH2, VSL(-15V), VCOM(-1.2V) }; @@ -404,7 +397,7 @@ namespace lgfx _send_plane(CMD_WRITE_RAM_RED, img, tr); // full/half: same image to both } - // Refresh sequence (community-sdk refreshDisplay, B/W). + // Built-in monochrome refresh sequence. _wait_busy(); _bus->writeCommand(CMD_DISP_UPDATE_CTRL1, 8); _bus->writeData(is_full ? CTRL1_BYPASS_RED : CTRL1_NORMAL, 8); @@ -687,6 +680,26 @@ namespace lgfx return result; } + void Panel_SSD1677_4Gray::setSleep(bool flg) + { + Panel_SSD1677::setSleep(flg); + _invalidate_gray_state(); + if (!flg) + { + // The hardware reset in the wake path establishes the even RAM face. + _mode2_face_odd = false; + _mode2_face_known = true; + } + } + + void Panel_SSD1677_4Gray::setPowerSave(bool flg) + { + Panel_SSD1677::setPowerSave(flg); + // Analog power transitions make the retained Mode 2 face/history unsafe. + // The next Fast/Fastest update will reset and rebuild them. + _invalidate_gray_state(); + } + void Panel_SSD1677_4Gray::_invalidate_gray_state(void) { _optical_state = optical_state_t::unknown; @@ -727,17 +740,23 @@ namespace lgfx _displayed_valid = true; } - // Transition class for lut_fastest: the new level is thresholded to - // monochrome, unchanged pixels fall into the hold group of their side. - static uint8_t transition_group(uint8_t old_level, uint8_t new_level) + static uint8_t dirty_byte_mask(int32_t byte_index, + const range_rect_t& dirty) { - new_level = new_level < 2 ? 0 : 3; - if (old_level == new_level) { return old_level < 2 ? 0 : 3; } - return old_level < new_level ? 1 : 2; + uint8_t mask = 0xFF; + if (byte_index == (dirty.left >> 3)) + { + mask &= uint8_t(0xFFu >> (dirty.left & 7)); + } + if (byte_index == (dirty.right >> 3)) + { + mask &= uint8_t(0xFFu << (7 - (dirty.right & 7))); + } + return mask; } - void Panel_SSD1677_4Gray::_send_transition_plane( - uint8_t command, const uint8_t* new_lsb, const uint8_t* new_msb, + void Panel_SSD1677_4Gray::_send_transition_planes( + const uint8_t* new_msb, const range_rect_t& dirty) { const auto full = _full_range(); @@ -746,29 +765,61 @@ namespace lgfx const uint8_t* old_msb = _displayed_buf + _buf_x1_len; uint8_t row[128]; + // New levels are thresholded by new_msb, while an old middle gray must be + // driven all the way to the requested black/white endpoint. The group + // bits can still be formed bytewise without decoding individual pixels: + // BW = new_msb + // RED = new_msb ? (old_lsb & old_msb) : (old_lsb | old_msb) + const bool full_dirty = dirty.left == full.left + && dirty.top == full.top + && dirty.right == full.right + && dirty.bottom == full.bottom; + if (full_dirty) + { + _send_plane(CMD_WRITE_RAM_BW, new_msb, full); + } + else + { + _set_ram_area(full.left, full.top, full.right + 1, full.bottom + 1); + _bus->writeCommand(CMD_WRITE_RAM_BW, 8); + const int32_t first_byte = dirty.left >> 3; + const int32_t last_byte = dirty.right >> 3; + for (int32_t y = 0; y < (int32_t)_cfg.panel_height; ++y) + { + const size_t row_offset = size_t(y) * row_bytes; + memcpy(row, old_msb + row_offset, row_bytes); + if (y >= dirty.top && y <= dirty.bottom) + { + for (int32_t b = first_byte; b <= last_byte; ++b) + { + const uint8_t mask = dirty_byte_mask(b, dirty); + row[b] = (row[b] & uint8_t(~mask)) + | (new_msb[row_offset + b] & mask); + } + } + _bus->writeBytes(row, row_bytes, true, false); + } + } + _set_ram_area(full.left, full.top, full.right + 1, full.bottom + 1); - _bus->writeCommand(command, 8); + _bus->writeCommand(CMD_WRITE_RAM_RED, 8); + const int32_t first_byte = dirty.left >> 3; + const int32_t last_byte = dirty.right >> 3; for (int32_t y = 0; y < (int32_t)_cfg.panel_height; ++y) { - // Outside the dirty rectangle, preserve black/dark with group 00 and - // light/white with group 11. This distinction is essential because a - // single nominal no-drive group is not optically neutral over repeated - // full-panel Mode 2 scans. - memcpy(row, old_msb + size_t(y) * row_bytes, row_bytes); + const size_t row_offset = size_t(y) * row_bytes; + memcpy(row, old_msb + row_offset, row_bytes); if (y >= dirty.top && y <= dirty.bottom) { - for (int32_t x = dirty.left; x <= dirty.right; ++x) + for (int32_t b = first_byte; b <= last_byte; ++b) { - const size_t index = size_t(y) * row_bytes + (x >> 3); - const uint8_t mask = uint8_t(0x80u >> (x & 7)); - const uint8_t old_level = ((old_lsb[index] & mask) ? 1 : 0) - | ((old_msb[index] & mask) ? 2 : 0); - const uint8_t new_level = ((new_lsb[index] & mask) ? 1 : 0) - | ((new_msb[index] & mask) ? 2 : 0); - const uint8_t group = transition_group(old_level, new_level); - const uint8_t group_bit = command == CMD_WRITE_RAM_BW ? 1 : 2; - if (group & group_bit) { row[x >> 3] |= mask; } - else { row[x >> 3] &= uint8_t(~mask); } + const size_t index = row_offset + b; + const uint8_t mask = dirty_byte_mask(b, dirty); + const uint8_t new_side = new_msb[index]; + const uint8_t red = (new_side & (old_lsb[index] & old_msb[index])) + | (uint8_t(~new_side) + & (old_lsb[index] | old_msb[index])); + row[b] = (row[b] & uint8_t(~mask)) | (red & mask); } } _bus->writeBytes(row, row_bytes, true, false); @@ -803,7 +854,7 @@ namespace lgfx return true; } - bool Panel_SSD1677_4Gray::_reset_controller_for_mode2(void) + bool Panel_SSD1677_4Gray::_reset_controller_and_face(void) { _bus->writeCommand(0x12, 8); _send_msec = millis(); @@ -827,23 +878,24 @@ namespace lgfx return true; } - bool Panel_SSD1677_4Gray::_refresh_absolute(const uint8_t* lsb, const uint8_t* msb, - const uint8_t* lut) + bool Panel_SSD1677_4Gray::_ensure_known_face(void) { - // A sleep/power transition can invalidate the controller's Mode 2 face - // parity before the first visible refresh. Absolute modes are - // full-screen operations, so recover a deterministic RAM face here - // rather than silently dropping the user's first frame. - if (!_mode2_face_known && !_reset_controller_for_mode2()) + if (_mode2_face_known || _reset_controller_and_face()) { return true; } + _invalidate_gray_state(); + return false; + } + + bool Panel_SSD1677_4Gray::_refresh_mode1_absolute( + const uint8_t* lsb, const uint8_t* msb, const uint8_t* lut) + { + // Mode 1 needs a known RAM face to map the two middle gray levels. + if (!_ensure_known_face()) { - _invalidate_gray_state(); return false; } - // The Mode 2 ping-pong face exchanges the physical interpretation of the - // two middle codes: swap the plane destinations on the odd face. 00 black - // and 11 white are symmetric, so a parity error would otherwise appear - // only as a dark/light swap. + // The ping-pong face exchanges the two middle codes in Mode 1. Swap the + // plane destinations on the odd face; 00 black and 11 white are symmetric. const auto full = _full_range(); const uint8_t* bw = _mode2_face_odd ? msb : lsb; const uint8_t* red = _mode2_face_odd ? lsb : msb; @@ -856,42 +908,59 @@ namespace lgfx return true; } + bool Panel_SSD1677_4Gray::_refresh_mode2_absolute( + const uint8_t* lsb, const uint8_t* msb, const uint8_t* lut) + { + if (!lut || !_ensure_known_face()) { return false; } + + // Mode 2 consumes the group bits written to 0x24/0x26 directly on every + // activation. Both RAMs are overwritten, so their command mapping stays + // fixed even though the controller advances its ping-pong face. Swapping + // these destinations on odd faces would exchange the two middle grays. + const auto full = _full_range(); + _send_plane(CMD_WRITE_RAM_BW, lsb, full, true); + _send_plane(CMD_WRITE_RAM_RED, msb, full, true); + _send_gray_lut(lut); + if (!_activate(CTRL1_NORMAL, 0x0C, false, true, false)) { return false; } + + _optical_state = optical_state_t::gray4; + return true; + } + void Panel_SSD1677_4Gray::_remember_mono_dirty( const uint8_t* msb, const range_rect_t& dirty) { const uint32_t row_bytes = ((_cfg.panel_width + 7) & ~7) >> 3; uint8_t* old_lsb = _displayed_buf; uint8_t* old_msb = _displayed_buf + _buf_x1_len; + const int32_t first_byte = dirty.left >> 3; + const int32_t last_byte = dirty.right >> 3; for (int32_t y = dirty.top; y <= dirty.bottom; ++y) { - for (int32_t x = dirty.left; x <= dirty.right; ++x) + const size_t row_offset = size_t(y) * row_bytes; + for (int32_t b = first_byte; b <= last_byte; ++b) { - const size_t index = size_t(y) * row_bytes + (x >> 3); - const uint8_t mask = uint8_t(0x80u >> (x & 7)); - if (msb[index] & mask) - { - old_lsb[index] |= mask; - old_msb[index] |= mask; - } - else - { - old_lsb[index] &= uint8_t(~mask); - old_msb[index] &= uint8_t(~mask); - } + const size_t index = row_offset + b; + const uint8_t mask = dirty_byte_mask(b, dirty); + const uint8_t mono = msb[index] & mask; + old_lsb[index] = (old_lsb[index] & uint8_t(~mask)) | mono; + old_msb[index] = (old_msb[index] & uint8_t(~mask)) | mono; } } } - bool Panel_SSD1677_4Gray::_refresh_fastest(const uint8_t* lsb, const uint8_t* msb, - const range_rect_t& current_dirty) + bool Panel_SSD1677_4Gray::_refresh_mode2_fastest( + const uint8_t* lsb, const uint8_t* msb, + const range_rect_t& current_dirty) { if (!_displayed_valid) { - return _refresh_absolute(lsb, msb, lut_quality); + // Establish a complete optical/history baseline without leaving Mode 2. + return _refresh_mode2_absolute(lsb, msb, lut_fast); } + if (!_ensure_known_face()) { return false; } - _send_transition_plane(CMD_WRITE_RAM_BW, lsb, msb, current_dirty); - _send_transition_plane(CMD_WRITE_RAM_RED, lsb, msb, current_dirty); + _send_transition_planes(msb, current_dirty); _send_gray_lut(lut_fastest); if (!_activate(CTRL1_NORMAL, 0x0C, false, true, false)) { return false; } @@ -925,16 +994,16 @@ namespace lgfx switch (mode) { case epd_mode_t::epd_quality: - success = _refresh_absolute(planeL, planeM, lut_quality); + success = _refresh_mode1_absolute(planeL, planeM, lut_quality); break; case epd_mode_t::epd_text: - success = _refresh_absolute(planeL, planeM, lut_text); + success = _refresh_mode1_absolute(planeL, planeM, lut_text); break; case epd_mode_t::epd_fast: - success = _refresh_absolute(planeL, planeM, lut_fast); + success = _refresh_mode2_absolute(planeL, planeM, lut_fast); break; case epd_mode_t::epd_fastest: - success = _refresh_fastest(planeL, planeM, current_dirty); + success = _refresh_mode2_fastest(planeL, planeM, current_dirty); break; default: success = false; diff --git a/src/lgfx/v1/panel/Panel_SSD1677.hpp b/src/lgfx/v1/panel/Panel_SSD1677.hpp index b7fa0431..5f25b775 100644 --- a/src/lgfx/v1/panel/Panel_SSD1677.hpp +++ b/src/lgfx/v1/panel/Panel_SSD1677.hpp @@ -29,7 +29,7 @@ namespace lgfx /* SSD1677 (GDEQ0426T82 / 800x480) E-Paper panel driver. - Buffer / coordinate model (EPD native, see docs/Panel_SSD1677_rebuild_plan.md): + Buffer / coordinate model (EPD native): - Panel native orientation = landscape 800(X=source) x 480(Y=gate). - 8 pixels per byte are packed along X (source) direction, matching the controller's source-shift scan order. byte = 8 consecutive X pixels. @@ -41,8 +41,9 @@ namespace lgfx - _draw_pixel stores an abstract gray level v(0=black..3=white) split into two bit planes: planeL = v&1 (-> BW RAM 0x24), planeM = v&2 (-> RED RAM 0x26). - The hardware RAM encoding is generated at send time per epd_mode: - quality/text : absolute four-gray LUT (planes sent as-is) - fast/fastest : monochrome differential LUT vs displayed history + quality/text : Mode 1 absolute four-gray update + fast : Mode 2 absolute four-gray update + fastest : Mode 2 monochrome differential update vs history */ struct Panel_SSD1677 : public Panel_HasBuffer { @@ -72,7 +73,7 @@ namespace lgfx protected: - // Longer busy ceiling for the 480x800 panel; full refresh ~3.8s. + // Minimum wait used by the base monochrome refresh path. static constexpr unsigned long _refresh_msec = 400; range_rect_t _range_old; @@ -100,9 +101,8 @@ namespace lgfx const uint8_t* getInitCommands(uint8_t listno) const override { - // SSD1677 power-up sequence (community-sdk EInkDisplay, non-X3). - // panel native: 800(X=source) x 480(Y=gate). 0x01 sets 480 gates. - // RAM window / auto-clear / power-on are issued in _after_wake (computed). + // SSD1677 power-up sequence for the native 800x480 panel. + // 0x01 configures 480 gates; RAM setup and power-on follow in _after_wake. static constexpr uint8_t list0[] = { 0x12, 0 + CMD_INIT_DELAY, 10, // SW Reset + 10 msec 0x18, 1, 0x80, // Temp sensor: internal @@ -122,6 +122,8 @@ namespace lgfx { ~Panel_SSD1677_4Gray(void) override; bool init(bool use_reset) override; + void setSleep(bool flg) override; + void setPowerSave(bool flg) override; void display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h) override; protected: @@ -143,15 +145,19 @@ namespace lgfx range_rect_t _full_range(void) const; void _send_gray_lut(const uint8_t* lut); void _remember_displayed(const uint8_t* lsb, const uint8_t* msb); - void _send_transition_plane(uint8_t command, - const uint8_t* new_lsb, const uint8_t* new_msb, - const range_rect_t& dirty); + void _send_transition_planes(const uint8_t* new_msb, + const range_rect_t& dirty); void _remember_mono_dirty(const uint8_t* msb, const range_rect_t& dirty); bool _activate(uint8_t ctrl1, uint8_t ctrl2, bool powers_down, bool mode2_activation, bool enforce_refresh_minimum = true); - bool _reset_controller_for_mode2(void); - bool _refresh_absolute(const uint8_t* lsb, const uint8_t* msb, const uint8_t* lut); - bool _refresh_fastest(const uint8_t* lsb, const uint8_t* msb, const range_rect_t& dirty); + bool _reset_controller_and_face(void); + bool _ensure_known_face(void); + bool _refresh_mode1_absolute(const uint8_t* lsb, const uint8_t* msb, + const uint8_t* lut); + bool _refresh_mode2_absolute(const uint8_t* lsb, const uint8_t* msb, + const uint8_t* lut); + bool _refresh_mode2_fastest(const uint8_t* lsb, const uint8_t* msb, + const range_rect_t& dirty); }; //---------------------------------------------------------------------------- From 5f8a783f7dbc07e8ce5c19cf8779829d1eefcde1 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Tue, 28 Jul 2026 18:29:37 +0900 Subject: [PATCH 07/56] Fix CardputerADV being misdetected as plain Cardputer The GPIO read used for the VAMeter / Cardputer / CardputerADV split was overwritten by the VAMeter I2C probe result, so the subsequent (result & 0x0C) == 0x0C test saw a bool and could never select CardputerADV. This surfaces whenever an I2C Cap or Unit is attached: on CardputerADV G5/G6 are the external pins, so the Cap pull-ups make (result & 3) == 3 true, the VAMeter probe runs, finds nothing and leaves result at 0. M5Unified then picks the plain Cardputer pin table, whose in_i2c entry is 255/255, and every access to a built-in device on the internal bus fails. Keep the probe result in its own variable so the G8/G9 bits survive. --- src/M5GFX.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index 7836fffc..dea57580 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -2432,10 +2432,12 @@ The usage of each pin is as follows. for (auto &bup : backup_pins2) { bup.restore(); } if ((result & 3) == 3) { m5gfx::i2c::i2c_temporary_switcher_t backup_i2c_setting(1, GPIO_NUM_5, GPIO_NUM_6); - result = (m5gfx::i2c::transactionWrite(1, 0x40, nullptr, 0).has_value() - && m5gfx::i2c::transactionWrite(1, 0x41, nullptr, 0).has_value()); + // Keep the probe result out of `result`; the CardputerADV check below + // still needs the G8/G9 bits of the GPIO read. + bool is_vameter = (m5gfx::i2c::transactionWrite(1, 0x40, nullptr, 0).has_value() + && m5gfx::i2c::transactionWrite(1, 0x41, nullptr, 0).has_value()); backup_i2c_setting.restore(); - if (result) { + if (is_vameter) { board = board_t::board_M5VAMeter; } } From 476302abdf9169d95588f679fec255ddc9cf7763 Mon Sep 17 00:00:00 2001 From: Forairaaaaa Date: Fri, 31 Jul 2026 14:19:39 +0800 Subject: [PATCH 08/56] Update CoreS3 ILI9342E init parameters --- src/M5GFX.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index dea57580..973b7028 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -529,13 +529,13 @@ namespace m5gfx 0x36, 1, 0x08, 0xD5, 1, 0x00, 0xB1, 1, 0x22, - 0xC2, 9, 0x00,0x64,0x00,0x43,0x3C,0x03,0x03,0x03,0x03, 0xC8, 1, 0x38, - 0xC9, 1, 0x1D, - 0xCA, 1, 0x1D, + 0xCB, 1, 0x1C, + 0xC9, 1, 0x1A, + 0xCA, 1, 0x1A, 0xB7, 4, 0x5A,0x41,0x11,0x19, - 0xE4,15, 0x04,0x09,0x12,0x06,0x14,0x08,0x3C,0x77,0x4A,0x06,0x0D,0x09,0x18,0x24,0x07, - 0xE5,15, 0x02,0x03,0x08,0x06,0x14,0x08,0x38,0x56,0x4B,0x05,0x0F,0x0C,0x1F,0x1C,0x0D, + 0xE4,15, 0x04,0x08,0x11,0x06,0x12,0x07,0x3A,0x76,0x47,0x07,0x0F,0x0A,0x11,0x19,0x05, + 0xE5,15, 0x02,0x03,0x07,0x06,0x12,0x07,0x36,0x5F,0x48,0x06,0x10,0x0C,0x16,0x14,0x09, 0x11, 0 + CMD_INIT_DELAY, 120, 0x29, 0 + CMD_INIT_DELAY, 120, 0xFF,0xFF, From bdf2b245dd73eb8e44f760b3c8207feca2b394b0 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Sat, 1 Aug 2026 01:54:26 +0000 Subject: [PATCH 09/56] Derive the per port I2C properties instead of hard coding them The I2C code was written when every port of a chip was the same peripheral, so the FIFO depth and the clock feeding the SCL divider were written as constants, and the number of ports was taken from SOC_I2C_NUM. None of that holds since the ESP32-C6, which carries a low power I2C alongside the normal one. - SOC_I2C_NUM counts the low power port too, so it can no longer tell how many normal ports there are. That is why the conditionals had a list of chip names growing next to it. Ask SOC_HP_I2C_NUM instead, keeping the list only as the fallback for the ESP-IDF versions that predate the macro, so it stops growing. - read the FIFO depth from SOC_I2C_FIFO_LEN through a per port accessor, rather than assuming 32 in writeBytes() and readBytes(). - move the source clock selection into a per port accessor as well. It has to stay a function rather than a value cached at init, because on the older chips it follows the CPU frequency, which changes at run time. No behaviour change on the chips where the normal port is 32 bytes deep, which is all of them except the ESP32-C2 at 16. The transfers on that chip were being chunked past the end of its FIFO and are now chunked correctly. That one is not verified on hardware. This only prepares the structure; the low power port is not reachable yet. --- src/lgfx/v1/platforms/esp32/common.cpp | 113 +++++++++++++++++++------ 1 file changed, 88 insertions(+), 25 deletions(-) diff --git a/src/lgfx/v1/platforms/esp32/common.cpp b/src/lgfx/v1/platforms/esp32/common.cpp index 376d412c..16a0ff48 100644 --- a/src/lgfx/v1/platforms/esp32/common.cpp +++ b/src/lgfx/v1/platforms/esp32/common.cpp @@ -938,6 +938,83 @@ namespace lgfx #define I2C_ACK_ERR_INT_RAW_M I2C_NACK_INT_RAW_M #endif +// From the ESP32-C6 on, some chips carry a low power I2C in addition to the normal ones, +// and SOC_I2C_NUM counts both of them. SOC_HP_I2C_NUM / SOC_LP_I2C_NUM tell them apart, +// but they only exist in recent ESP-IDF, so the chips with a single high power port have +// to be named when they are missing. +#if defined ( SOC_HP_I2C_NUM ) + #define LGFX_HP_I2C_NUM SOC_HP_I2C_NUM +#elif defined ( CONFIG_IDF_TARGET_ESP32C2 ) || defined ( CONFIG_IDF_TARGET_ESP32C5 ) || defined ( CONFIG_IDF_TARGET_ESP32C6 ) || defined ( CONFIG_IDF_TARGET_ESP32C61 ) + #define LGFX_HP_I2C_NUM 1 +#else + #define LGFX_HP_I2C_NUM SOC_I2C_NUM +#endif + +// ESP-IDF numbers the low power ports after all the high power ones, so the first low +// power port index is the number of high power ports. ( see i2c_port_t in hal/i2c_types.h ) +#if defined ( SOC_LP_I2C_NUM ) && ( SOC_LP_I2C_NUM > 0 ) + #define LGFX_LP_I2C_NUM SOC_LP_I2C_NUM + #define LGFX_LP_I2C_PORT LGFX_HP_I2C_NUM +#else + #define LGFX_LP_I2C_NUM 0 +#endif + + /// True if this port index belongs to the low power I2C. + static inline bool isLpPort(int i2c_port) + { +#if LGFX_LP_I2C_NUM > 0 + return i2c_port >= LGFX_LP_I2C_PORT; +#else + (void)i2c_port; + return false; +#endif + } + + /// Hardware FIFO depth of this port. The low power I2C has a shallower FIFO than the + /// normal one, so this cannot be a single constant for the whole chip. + static inline uint32_t getFifoLen(int i2c_port) + { +#if LGFX_LP_I2C_NUM > 0 && defined ( SOC_LP_I2C_FIFO_LEN ) + if (isLpPort(i2c_port)) { return SOC_LP_I2C_FIFO_LEN; } +#else + (void)i2c_port; +#endif +#if defined ( SOC_I2C_FIFO_LEN ) + return SOC_I2C_FIFO_LEN; +#else + return 32; +#endif + } + + /// Clock feeding the SCL divider of this port [Hz]. + /// This is not a property of the chip alone: the low power I2C runs from its own + /// clock, and on the older chips the normal one follows the CPU frequency. + static inline uint32_t getSourceClock(int i2c_port) + { +#if LGFX_LP_I2C_NUM > 0 + if (isLpPort(i2c_port)) + { // Both selectable sources of the low power I2C are 20MHz: + // LP_FAST ( RC fast, nominal 20MHz ) and XTAL_D2 ( 40MHz crystal divided by 2 ). + return 20 * 1000 * 1000; + } +#else + (void)i2c_port; +#endif + +#if defined (CONFIG_IDF_TARGET_ESP32C2) || defined (CONFIG_IDF_TARGET_ESP32C3) || defined ( CONFIG_IDF_TARGET_ESP32C5 ) || defined (CONFIG_IDF_TARGET_ESP32S3) || defined ( CONFIG_IDF_TARGET_ESP32C6 ) || defined ( CONFIG_IDF_TARGET_ESP32C61 ) || defined ( CONFIG_IDF_TARGET_ESP32P4 ) + return 40 * 1000 * 1000; // XTAL clock +#else + rtc_cpu_freq_config_t cpu_freq_conf; + rtc_clk_cpu_freq_get_config(&cpu_freq_conf); + if (cpu_freq_conf.freq_mhz < 80) + { // The source follows the CPU frequency here, so it has to be read every time + // rather than cached when the port is initialized. + return (cpu_freq_conf.source_freq_mhz * 1000000) / cpu_freq_conf.div; + } + return 80 * 1000 * 1000; +#endif + } + #if !defined ( I2C_CLOCK_SRC_ATOMIC ) #if __cplusplus <= 201103L #define LGFX_PERIPH_MODULE_T periph_module_t @@ -948,7 +1025,7 @@ namespace lgfx __attribute__ ((unused)) static LGFX_PERIPH_MODULE_T getPeriphModule(int num) { -#if SOC_I2C_NUM == 1 || defined CONFIG_IDF_TARGET_ESP32C2 || defined CONFIG_IDF_TARGET_ESP32C5 || defined CONFIG_IDF_TARGET_ESP32C6 || defined CONFIG_IDF_TARGET_ESP32C61 +#if LGFX_HP_I2C_NUM == 1 return PERIPH_I2C0_MODULE; #else return num == 0 ? PERIPH_I2C0_MODULE : PERIPH_I2C1_MODULE; @@ -960,7 +1037,7 @@ namespace lgfx static i2c_dev_t* getDev(int num) { -#if SOC_I2C_NUM == 1 || defined CONFIG_IDF_TARGET_ESP32C2 || defined CONFIG_IDF_TARGET_ESP32C5 || defined CONFIG_IDF_TARGET_ESP32C6 || defined CONFIG_IDF_TARGET_ESP32C61 +#if LGFX_HP_I2C_NUM == 1 return &I2C0; #else return num == 0 ? &I2C0 : &I2C1; @@ -1144,7 +1221,7 @@ namespace lgfx #endif #endif -#if SOC_I2C_NUM == 1 || defined CONFIG_IDF_TARGET_ESP32C2 || defined CONFIG_IDF_TARGET_ESP32C5 || defined CONFIG_IDF_TARGET_ESP32C6 || defined CONFIG_IDF_TARGET_ESP32C61 +#if LGFX_HP_I2C_NUM == 1 auto twowire = &Wire; #else auto twowire = ((dev == &I2C0) ? &Wire : &Wire1); @@ -1378,7 +1455,7 @@ namespace lgfx #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) #if defined ARDUINO_ESP32_GIT_VER #if ARDUINO_ESP32_GIT_VER != 0x44c11981 - #if SOC_I2C_NUM == 1 || defined CONFIG_IDF_TARGET_ESP32C2 || defined CONFIG_IDF_TARGET_ESP32C5 || defined CONFIG_IDF_TARGET_ESP32C6 || defined CONFIG_IDF_TARGET_ESP32C61 + #if LGFX_HP_I2C_NUM == 1 auto twowire = &Wire; #else auto twowire = ((i2c_port == 0) ? &Wire : &Wire1); @@ -1437,7 +1514,7 @@ namespace lgfx #endif #if defined ( USE_TWOWIRE_SETPINS ) -#if SOC_I2C_NUM == 1 || defined CONFIG_IDF_TARGET_ESP32C2 || defined CONFIG_IDF_TARGET_ESP32C5 || defined CONFIG_IDF_TARGET_ESP32C6 || defined CONFIG_IDF_TARGET_ESP32C61 +#if LGFX_HP_I2C_NUM == 1 auto twowire = &Wire; #else auto twowire = ((i2c_port == 0) ? &Wire : &Wire1); @@ -1477,7 +1554,7 @@ namespace lgfx i2c_stop(i2c_port); #if defined ( ARDUINO ) && __has_include () -#if SOC_I2C_NUM == 1 || defined CONFIG_IDF_TARGET_ESP32C2 || defined CONFIG_IDF_TARGET_ESP32C5 || defined CONFIG_IDF_TARGET_ESP32C6 || defined CONFIG_IDF_TARGET_ESP32C61 +#if LGFX_HP_I2C_NUM == 1 auto twowire = &Wire; #else auto twowire = ((i2c_port == 0) ? &Wire : &Wire1); @@ -1561,22 +1638,7 @@ namespace lgfx { i2c_context[i2c_port].freq = freq; static constexpr uint32_t MIN_I2C_CYCLE = 35; -#if defined (CONFIG_IDF_TARGET_ESP32C2) || defined (CONFIG_IDF_TARGET_ESP32C3) || defined ( CONFIG_IDF_TARGET_ESP32C5 ) || defined (CONFIG_IDF_TARGET_ESP32S3) || defined ( CONFIG_IDF_TARGET_ESP32C6 ) || defined ( CONFIG_IDF_TARGET_ESP32C61 ) || defined ( CONFIG_IDF_TARGET_ESP32P4 ) - uint32_t src_clock = 40 * 1000 * 1000; // XTAL clock -#else - rtc_cpu_freq_config_t cpu_freq_conf; - rtc_clk_cpu_freq_get_config(&cpu_freq_conf); - uint32_t src_clock = 80 * 1000 * 1000; - if (cpu_freq_conf.freq_mhz < 80) - { - src_clock = (cpu_freq_conf.source_freq_mhz * 1000000) / cpu_freq_conf.div; - } -// ESP_LOGI("LGFX", "i2c::restart : port:%d / addr:%02x / freq:%d / rw:%d", i2c_port, i2c_addr, freq, read); -// ESP_LOGI("LGFX", "cpu_freq_conf.div :%d", cpu_freq_conf.div); -// ESP_LOGI("LGFX", "cpu_freq_conf.freq_mhz :%d", cpu_freq_conf.freq_mhz); -// ESP_LOGI("LGFX", "cpu_freq_conf.source :%d", cpu_freq_conf.source); -// ESP_LOGI("LGFX", "cpu_freq_conf.source_freq_mhz :%d", cpu_freq_conf.source_freq_mhz); -#endif + uint32_t src_clock = getSourceClock(i2c_port); auto cycle = std::min(32767u, std::max(MIN_I2C_CYCLE, (src_clock / (freq + 1) + 1))); freq = src_clock / cycle; @@ -1752,10 +1814,10 @@ namespace lgfx cpp::result res {}; if (!length) return res; - static constexpr int txfifo_limit = 32; + const uint32_t txfifo_limit = getFifoLen(i2c_port); auto dev = getDev(i2c_port); auto fifo_addr = getFifoAddr(i2c_port); - size_t len = ((length - 1) & (txfifo_limit-1)) + 1; + size_t len = ((length - 1) % txfifo_limit) + 1; do { res = i2c_wait(i2c_port); @@ -1791,6 +1853,7 @@ namespace lgfx if (!length) return res; 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; + const uint32_t rxfifo_limit = getFifoLen(i2c_port); auto fifo_addr = getFifoAddr(i2c_port); auto dev = getDev(i2c_port); @@ -1811,7 +1874,7 @@ namespace lgfx break; } - len = length < 32 ? length : 32; + len = length < rxfifo_limit ? length : rxfifo_limit; #if defined ( CONFIG_IDF_TARGET_ESP32 ) || !defined ( CONFIG_IDF_TARGET ) // workaround for ESP32 i2c bug. if (last_nack && len == length && len > 1) { len -= 1; } From 349ab7125b6ae02969cc0581f691e980c6c26abb Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Sat, 1 Aug 2026 01:54:26 +0000 Subject: [PATCH 10/56] Support the low power I2C port on the chips that have one (C5 / C6 / P4) The low power I2C controller shares its register layout with the normal ports and the port numbering places it right after them, so the existing transaction code can drive it once the port is reachable. What differs is everything around the registers: - Bring-up goes through the ESP-IDF master bus driver, which knows how to route the low power pads (fixed IOMUX pair on C5 / C6, LP GPIO matrix on P4) and their clock source. The port is usable when the driver headers and the low power clock definitions are available (ESP-IDF 5.4 or later). - The pads live in the low power IO domain and keep their routing across a reset, so release() hands them back explicitly and set_pin() clears a leftover routing before taking a pin for a normal port. Without this the pins stay bound to the low power port even into the next program. - set_pin() itself is skipped for a low power port: it would re-route the pins through the normal GPIO matrix on every transaction and disconnect the port from its own pads. - The controller reaches the bus only through its internal open drain mode, so the forced outputs the normal ports use are left off. With them on, the state machine runs to completion without ever driving the lines. - The TwoWire pin propagation in setPins() applies only to the normal ports; handing a low power port's pins to a Wire instance would redirect that port instead. Verified on hardware for all three chips: a scan on the low power port finds the same devices as a normal port on the same physical bus, and register reads return real data. --- src/lgfx/v1/platforms/esp32/common.cpp | 136 +++++++++++++++++++++++-- 1 file changed, 128 insertions(+), 8 deletions(-) diff --git a/src/lgfx/v1/platforms/esp32/common.cpp b/src/lgfx/v1/platforms/esp32/common.cpp index 16a0ff48..4f2519d6 100644 --- a/src/lgfx/v1/platforms/esp32/common.cpp +++ b/src/lgfx/v1/platforms/esp32/common.cpp @@ -952,9 +952,20 @@ namespace lgfx // ESP-IDF numbers the low power ports after all the high power ones, so the first low // power port index is the number of high power ports. ( see i2c_port_t in hal/i2c_types.h ) -#if defined ( SOC_LP_I2C_NUM ) && ( SOC_LP_I2C_NUM > 0 ) +// +// Bringing a low power port up is left to the ESP-IDF driver: there is no TwoWire for it, +// and the pads reach the peripheral differently depending on the chip ( a fixed LP IO MUX +// function on some, the LP GPIO matrix on others ). The driver already knows which, so it +// is asked to open the bus and the registers are taken over afterwards, exactly as this +// code does for the normal ports. +#if defined ( SOC_LP_I2C_NUM ) && ( SOC_LP_I2C_NUM > 0 ) && __has_include ( ) \ + && defined ( ESP_IDF_VERSION_VAL ) && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0) #define LGFX_LP_I2C_NUM SOC_LP_I2C_NUM #define LGFX_LP_I2C_PORT LGFX_HP_I2C_NUM + // The default source is the RC oscillator, which drifts with temperature. XTAL_D2 is a + // divided crystal, so the SCL frequency comes out as asked. + #define LGFX_LP_I2C_SCLK LP_I2C_SCLK_XTAL_D2 + #include #else #define LGFX_LP_I2C_NUM 0 #endif @@ -993,8 +1004,15 @@ namespace lgfx { #if LGFX_LP_I2C_NUM > 0 if (isLpPort(i2c_port)) - { // Both selectable sources of the low power I2C are 20MHz: - // LP_FAST ( RC fast, nominal 20MHz ) and XTAL_D2 ( 40MHz crystal divided by 2 ). + { // Ask the clock tree rather than assuming a figure: which source the low power + // port runs from is a choice made when the bus is opened, and the two are not + // the same clock even where they happen to share a nominal frequency. + uint32_t hz = 0; + if (ESP_OK == esp_clk_tree_src_get_freq_hz((soc_module_clk_t)LGFX_LP_I2C_SCLK + , ESP_CLK_TREE_SRC_FREQ_PRECISION_APPROX, &hz) && hz) + { + return hz; + } return 20 * 1000 * 1000; } #else @@ -1037,6 +1055,12 @@ namespace lgfx static i2c_dev_t* getDev(int num) { +#if LGFX_LP_I2C_NUM > 0 + // The register layout of the low power I2C matches the normal one, which is why + // ESP-IDF publishes it as an i2c_dev_t as well. Everything below the port lookup + // therefore works on it unchanged. + if (isLpPort(num)) { return &LP_I2C; } +#endif #if LGFX_HP_I2C_NUM == 1 return &I2C0; #else @@ -1235,8 +1259,10 @@ namespace lgfx #endif } -#if defined ( ARDUINO ) && __has_include () -#elif __has_include() +// A low power port is opened through the ESP-IDF driver even in an Arduino build, where +// the normal ports go through TwoWire, so the handle is needed in both cases. +#if __has_include() \ + && ( LGFX_LP_I2C_NUM > 0 || !( defined ( ARDUINO ) && __has_include () ) ) i2c_master_bus_handle_t i2c_bus_handle = nullptr; #endif private: @@ -1244,8 +1270,32 @@ namespace lgfx }; i2c_context_t i2c_context[I2C_NUM_MAX]; +#if LGFX_LP_I2C_NUM > 0 && SOC_RTCIO_PIN_COUNT > 0 + /// Hand a pin back from the low power IO domain. + /// A pin routed to the low power I2C keeps that routing across a reset, because it is + /// held in the low power domain rather than by the CPU. Any later attempt to drive it + /// from a normal port then finds a pad that no longer reaches the peripheral, and the + /// bus looks dead for reasons nothing in the running program explains. Releasing it + /// here makes taking a pin over idempotent, whether or not the previous user shut + /// down in an orderly way. + static void releaseLpPad(gpio_num_t pin) + { + if ((int)pin >= 0 && rtc_gpio_is_valid_gpio(pin)) + { + rtc_gpio_deinit(pin); + } + } +#endif + static void set_pin(i2c_port_t i2c_num, gpio_num_t pin_sda, gpio_num_t pin_scl) { +#if LGFX_LP_I2C_NUM > 0 && SOC_RTCIO_PIN_COUNT > 0 + // Callers keep the low power ports away from here, so reaching this point means the + // pins are wanted for a normal port and any low power routing left on them, possibly + // by a previous run, has to go. ( see releaseLpPad ) + releaseLpPad(pin_sda); + releaseLpPad(pin_scl); +#endif #if __has_include() if ((int8_t)pin_sda >= 0) { gpio_set_level(pin_sda, true); @@ -1451,6 +1501,25 @@ namespace lgfx if (i2c_context[i2c_port].initialized) { i2c_context[i2c_port].initialized = false; +#if LGFX_LP_I2C_NUM > 0 + if (isLpPort(i2c_port)) + { // Opened through the ESP-IDF driver in init(), so it is closed the same way. + auto bus_handle = i2c_context[i2c_port].i2c_bus_handle; + if (bus_handle) { + i2c_context[i2c_port].i2c_bus_handle = nullptr; + i2c_del_master_bus(bus_handle); + } + #if SOC_RTCIO_PIN_COUNT > 0 + // The pins have to be handed back explicitly: their routing lives in the low + // power domain and would otherwise outlive this program, leaving them unusable + // from a normal port even after a reset. ( see releaseLpPad ) + releaseLpPad(i2c_context[i2c_port].pin_sda); + releaseLpPad(i2c_context[i2c_port].pin_scl); + #endif + } + else +#endif + { #if defined ( ARDUINO ) && __has_include () && defined ( ESP_IDF_VERSION_VAL ) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 0, 0) #if defined ARDUINO_ESP32_GIT_VER @@ -1473,6 +1542,7 @@ namespace lgfx #else i2c_periph_disable(i2c_port); #endif + } if ((int)i2c_context[i2c_port].pin_scl >= 0) { pinMode(i2c_context[i2c_port].pin_scl, pin_mode_t::input_pullup); @@ -1506,6 +1576,11 @@ namespace lgfx release(i2c_port).has_value(); i2c_context[i2c_port].pin_scl = (gpio_num_t)pin_scl; i2c_context[i2c_port].pin_sda = (gpio_num_t)pin_sda; +#if LGFX_LP_I2C_NUM > 0 + // The TwoWire instances belong to the normal ports; a low power port has none, + // and handing these pins to one would redirect that port to them. + if (isLpPort(i2c_port)) { return {}; } +#endif #if defined ( ARDUINO ) && __has_include () #if defined ( ESP_IDF_VERSION_VAL ) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(3, 3, 0) @@ -1553,6 +1628,35 @@ namespace lgfx i2c_stop(i2c_port); +#if LGFX_LP_I2C_NUM > 0 + if (isLpPort(i2c_port)) + { + i2c_master_bus_config_t bus_config; + memset(&bus_config, 0, sizeof(i2c_master_bus_config_t)); + bus_config.i2c_port = i2c_port; + bus_config.sda_io_num = pin_sda; + bus_config.scl_io_num = pin_scl; + bus_config.lp_source_clk = LGFX_LP_I2C_SCLK; + bus_config.glitch_ignore_cnt = 7; + bus_config.flags.enable_internal_pullup = true; + bus_config.intr_priority = 1; + + i2c_master_bus_handle_t bus_handle = nullptr; + if (ESP_OK != i2c_new_master_bus(&bus_config, &bus_handle)) + { // The pins of the low power port are constrained: they have to be LP IO, and on + // the chips without an LP GPIO matrix they are a fixed pair. + return cpp::fail(error_t::invalid_arg); + } + i2c_context[i2c_port].i2c_bus_handle = bus_handle; + i2c_context[i2c_port].initialized = true; + + // The pads are already routed to the peripheral by the call above, and they do not + // go through the normal GPIO matrix that set_pin() drives, so it is not called. + i2c_context[i2c_port].save_reg(getDev(i2c_port)); + return {}; + } +#endif + #if defined ( ARDUINO ) && __has_include () #if LGFX_HP_I2C_NUM == 1 auto twowire = &Wire; @@ -1757,7 +1861,15 @@ namespace lgfx } i2c_context[i2c_port].save_reg(dev); - set_pin((i2c_port_t)i2c_port, i2c_context[i2c_port].pin_sda, i2c_context[i2c_port].pin_scl); +#if LGFX_LP_I2C_NUM > 0 + // A low power port reaches its pads through the low power IO domain, which set_pin() + // knows nothing about: routing them through the normal GPIO matrix here would + // disconnect the port from its own pins on every transaction. + if (!isLpPort(i2c_port)) +#endif + { + set_pin((i2c_port_t)i2c_port, i2c_context[i2c_port].pin_sda, i2c_context[i2c_port].pin_scl); + } #if SOC_I2C_SUPPORT_HW_FSM_RST dev->ctr.fsm_rst = 1; @@ -1778,8 +1890,16 @@ namespace lgfx ctrl_reg.val = 0; ctrl_reg.ms_mode = 1; // master mode ctrl_reg.clk_en = 1; - ctrl_reg.sda_force_out = 1; - ctrl_reg.scl_force_out = 1; +#if LGFX_LP_I2C_NUM > 0 + // The low power controller reaches the bus only through its internal open drain + // mode ( sda_force_out = 0 ): with the outputs forced, the state machine runs to + // completion without ever driving the lines, so no device can respond. + if (!isLpPort(i2c_port)) +#endif + { + ctrl_reg.sda_force_out = 1; + ctrl_reg.scl_force_out = 1; + } dev->ctr.val = ctrl_reg.val; // ---------- i2c_ll_master_init typeof(dev->fifo_conf) fifo_conf_reg; From 69c31ce4de2b9e9713053281c4ff555c563354d0 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Sat, 1 Aug 2026 01:54:26 +0000 Subject: [PATCH 11/56] Reach an I2C bus through plain GPIO with a negative port number A negative port selects a software implementation that drives the pins directly: port -1 is slot 0, port -2 is slot 1. It reaches a bus without claiming a peripheral unit, for when every unit is taken or when touching one is undesirable. The convention matches the existing touch driver setting where a negative spi_host already means bit banged GPIO. Negative ports were never valid before (they fell through the upper bound checks into a negative array index), so no existing meaning changes. The implementation lives in soft_i2c.inl, a code fragment that a platform implementation includes inside its i2c namespace, so every function gets internal linkage there: the only public contract is the negative port number itself. The esp32 implementation consumes it here, delegating at the top of each public function. Platform specifics enter through macro hooks: line driving (esp32 toggles the open drain latches its pinMode() provides, the default switches the pin direction), locking (a FreeRTOS mutex here, single threaded by default) and the yield used while a stretched clock is waited out. Clock stretching is honored wherever SCL is released, and the transaction semantics mirror the peripheral path: an unacknowledged address surfaces at the next operation or at endTransaction, not at beginTransaction. Verified on hardware against the same physical bus: the scan results and register reads of both software slots are identical to the normal port and the low power port, and an out of range slot is rejected at init. --- src/lgfx/v1/platforms/esp32/common.cpp | 44 ++- src/lgfx/v1/platforms/soft_i2c.inl | 409 +++++++++++++++++++++++++ 2 files changed, 447 insertions(+), 6 deletions(-) create mode 100644 src/lgfx/v1/platforms/soft_i2c.inl diff --git a/src/lgfx/v1/platforms/esp32/common.cpp b/src/lgfx/v1/platforms/esp32/common.cpp index 4f2519d6..92bf00f9 100644 --- a/src/lgfx/v1/platforms/esp32/common.cpp +++ b/src/lgfx/v1/platforms/esp32/common.cpp @@ -1270,6 +1270,27 @@ namespace lgfx }; i2c_context_t i2c_context[I2C_NUM_MAX]; + static inline bool isSoftPort(int i2c_port) { return i2c_port < 0; } + +// ------------------------------------------------------------------------ +// Software I2C ( soft_i2c.inl ), reached through the negative port numbers. +// pinMode() here puts a pin in open drain output with the latch high and the +// input stage enabled, so a line is driven and released by toggling the latch +// alone; the direction changing default of the fragment is not needed. + +#define SOFT_I2C_LINE_LO(pin) gpio_lo(pin) +#define SOFT_I2C_LINE_HI(pin) gpio_hi(pin) +#define SOFT_I2C_LOCK(ctx) do { \ + if ((ctx).lock_handle == nullptr) { (ctx).lock_handle = xSemaphoreCreateMutex(); } \ + xSemaphoreTake((SemaphoreHandle_t)(ctx).lock_handle, portMAX_DELAY); \ + } while (0) +#define SOFT_I2C_UNLOCK(ctx) xSemaphoreGive((SemaphoreHandle_t)(ctx).lock_handle) +#define SOFT_I2C_YIELD() taskYIELD() +#define LGFX_INTERNAL_SOFT_I2C +#include "../soft_i2c.inl" + +// ------------------------------------------------------------------------ + #if LGFX_LP_I2C_NUM > 0 && SOC_RTCIO_PIN_COUNT > 0 /// Hand a pin back from the low power IO domain. /// A pin routed to the low power I2C keeps that routing across a reset, because it is @@ -1497,6 +1518,7 @@ namespace lgfx cpp::result release(int i2c_port) { + if (isSoftPort(i2c_port)) { return soft_i2c_release(i2c_port); } if (i2c_port >= I2C_NUM_MAX) { return cpp::fail(error_t::invalid_arg); } if (i2c_context[i2c_port].initialized) { @@ -1558,12 +1580,13 @@ namespace lgfx cpp::result setPins(int i2c_port, int pin_sda, int pin_scl) { - if ((i2c_port >= I2C_NUM_MAX) - || ((uint32_t)pin_scl >= GPIO_NUM_MAX) + if (((uint32_t)pin_scl >= GPIO_NUM_MAX) || ((uint32_t)pin_sda >= GPIO_NUM_MAX)) { return cpp::fail(error_t::invalid_arg); } + if (isSoftPort(i2c_port)) { return soft_i2c_setPins(i2c_port, pin_sda, pin_scl); } + if (i2c_port >= I2C_NUM_MAX) { return cpp::fail(error_t::invalid_arg); } if (i2c_context[i2c_port].initialized && i2c_context[i2c_port].pin_scl == (gpio_num_t)pin_scl @@ -1602,20 +1625,25 @@ namespace lgfx cpp::result getPinSDA(int i2c_port) { + if (isSoftPort(i2c_port)) { return soft_i2c_getPinSDA(i2c_port); } + if (i2c_port >= I2C_NUM_MAX) { return cpp::fail(error_t::invalid_arg); } return i2c_context[i2c_port].pin_sda; } cpp::result getPinSCL(int i2c_port) { + if (isSoftPort(i2c_port)) { return soft_i2c_getPinSCL(i2c_port); } + if (i2c_port >= I2C_NUM_MAX) { return cpp::fail(error_t::invalid_arg); } return i2c_context[i2c_port].pin_scl; } cpp::result init(int i2c_port) { + if (isSoftPort(i2c_port)) { return soft_i2c_init(i2c_port); } + if (i2c_port >= I2C_NUM_MAX) { return cpp::fail(error_t::invalid_arg); } gpio_num_t pin_sda = i2c_context[i2c_port].pin_sda; gpio_num_t pin_scl = i2c_context[i2c_port].pin_scl; - if ((i2c_port >= I2C_NUM_MAX) - || ((uint32_t)pin_scl >= GPIO_NUM_MAX) + if (((uint32_t)pin_scl >= GPIO_NUM_MAX) || ((uint32_t)pin_sda >= GPIO_NUM_MAX)) { return cpp::fail(error_t::invalid_arg); @@ -1706,6 +1734,7 @@ namespace lgfx cpp::result restart(int i2c_port, int i2c_addr, uint32_t freq, bool read) { + if (isSoftPort(i2c_port)) { return soft_i2c_restart(i2c_port, i2c_addr, freq, read); } if (i2c_port >= I2C_NUM_MAX) { return cpp::fail(error_t::invalid_arg); } if (i2c_addr < I2C_7BIT_ADDR_MIN || i2c_addr > I2C_10BIT_ADDR_MAX) return cpp::fail(error_t::invalid_arg); @@ -1832,8 +1861,8 @@ namespace lgfx cpp::result beginTransaction(int i2c_port, int i2c_addr, uint32_t freq, bool read) { + if (isSoftPort(i2c_port)) { return soft_i2c_beginTransaction(i2c_port, i2c_addr, freq, read); } if (i2c_port >= I2C_NUM_MAX) return cpp::fail(error_t::invalid_arg); - if ((uint32_t)i2c_context[i2c_port].pin_sda >= GPIO_NUM_MAX || (uint32_t)i2c_context[i2c_port].pin_scl >= GPIO_NUM_MAX) return cpp::fail(error_t::invalid_arg); //ESP_LOGI("LGFX", "i2c::beginTransaction : port:%d / addr:%02x / freq:%d / rw:%d", i2c_port, i2c_addr, freq, read); @@ -1922,12 +1951,14 @@ namespace lgfx cpp::result endTransaction(int i2c_port) { + if (isSoftPort(i2c_port)) { return soft_i2c_endTransaction(i2c_port); } if (i2c_port >= I2C_NUM_MAX) return cpp::fail(error_t::invalid_arg); return i2c_wait(i2c_port, true); } //*/ cpp::result writeBytes(int i2c_port, const uint8_t *data, size_t length) { + if (isSoftPort(i2c_port)) { return soft_i2c_writeBytes(i2c_port, data, length); } if (i2c_port >= I2C_NUM_MAX) { return cpp::fail(error_t::invalid_arg); } if (i2c_context[i2c_port].state.has_error()) { return cpp::fail(i2c_context[i2c_port].state.error()); } if (i2c_context[i2c_port].state != i2c_context_t::state_write) { return cpp::fail(error_t::mode_mismatch); } @@ -1964,8 +1995,9 @@ namespace lgfx return res; } - cpp::result readBytes(int i2c_port, uint8_t *readdata, size_t length, bool last_nack = false) + cpp::result readBytes(int i2c_port, uint8_t *readdata, size_t length, bool last_nack) { + if (isSoftPort(i2c_port)) { return soft_i2c_readBytes(i2c_port, readdata, length, last_nack); } if (i2c_port >= I2C_NUM_MAX) { return cpp::fail(error_t::invalid_arg); } if (i2c_context[i2c_port].state.has_error()) { return cpp::fail(i2c_context[i2c_port].state.error()); } if (i2c_context[i2c_port].state != i2c_context_t::state_read) { return cpp::fail(error_t::mode_mismatch); } diff --git a/src/lgfx/v1/platforms/soft_i2c.inl b/src/lgfx/v1/platforms/soft_i2c.inl new file mode 100644 index 00000000..8752e205 --- /dev/null +++ b/src/lgfx/v1/platforms/soft_i2c.inl @@ -0,0 +1,409 @@ +/*----------------------------------------------------------------------------/ + Lovyan GFX - Graphics library for embedded devices. + +Original Source: + https://github.com/lovyan03/LovyanGFX/ + +Licence: + [FreeBSD](https://github.com/lovyan03/LovyanGFX/blob/master/license.txt) + +Author: + [lovyan03](https://twitter.com/lovyan03) + +Contributors: + [ciniml](https://github.com/ciniml) + [mongonta0716](https://github.com/mongonta0716) + [tobozo](https://github.com/tobozo) +/----------------------------------------------------------------------------*/ +// I2C on plain GPIO, selected by a negative port number: port -1 is slot 0, +// port -2 is slot 1. It reaches a bus without claiming a peripheral unit, for +// when every unit is taken or when touching one is undesirable. +// +// This file is not a header but a code fragment: a platform implementation +// includes it inside its i2c namespace, and delegates to the soft_i2c_*() +// functions at the top of each public function when the port is negative. +// Everything here has internal linkage, so nothing beyond the negative port +// convention becomes part of the library interface. +// +// The transaction semantics mirror the peripheral paths: an unacknowledged +// address surfaces at the next operation or at endTransaction, not at +// beginTransaction. On a failure the bus is stopped and the lock released +// right away, and the stored error is reported (and cleared) later. +// Clock stretching is honored wherever SCL is released. +#if !defined ( LGFX_INTERNAL_SOFT_I2C ) + #error "soft_i2c.inl is an implementation fragment of the i2c namespace; it cannot be included directly." +#endif + +// 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 +// on any platform. A platform whose pinMode() gives an input mode an open +// drain output stage (esp32) overrides these with plain latch toggles and +// skips the direction changes. +#if !defined ( SOFT_I2C_LINE_LO ) + #define SOFT_I2C_LINE_LO(pin) do { gpio_lo(pin); pinMode(pin, pin_mode_t::output); } while (0) + #define SOFT_I2C_LINE_HI(pin) pinMode(pin, pin_mode_t::input_pullup) +#endif + +// Serialization of the transactions on a slot. A platform with an RTOS wraps +// a mutex over the lock_handle member; the default is single threaded. +#if !defined ( SOFT_I2C_LOCK ) + #define SOFT_I2C_LOCK(ctx) + #define SOFT_I2C_UNLOCK(ctx) +#endif + +// Called while waiting out a stretched clock. +#if !defined ( SOFT_I2C_YIELD ) + #if defined ( ARDUINO ) + #define SOFT_I2C_YIELD() yield() + #else + #define SOFT_I2C_YIELD() + #endif +#endif + +//---------------------------------------------------------------------------- + + static constexpr int soft_i2c_port_count = 2; + + static constexpr int soft_i2c_7bit_addr_min = 0x08; + static constexpr int soft_i2c_7bit_addr_max = 0x77; + static constexpr int soft_i2c_10bit_addr_max = 1023; + + struct soft_i2c_context_t + { + enum state_t + { + state_disconnect, + state_write, + state_read + }; + cpp::result state; + void* lock_handle = nullptr; // storage for the SOFT_I2C_LOCK hooks + int pin_sda = -1; + int pin_scl = -1; + uint32_t freq = 0; + uint32_t half_us = 0; // 0 = unthrottled + bool initialized = false; + }; + static soft_i2c_context_t soft_i2c_context[soft_i2c_port_count]; + + 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]; } + + static inline void soft_i2c_half_wait(uint32_t half_us) + { if (half_us) { delayMicroseconds(half_us); } } + + static inline void soft_i2c_set_freq(soft_i2c_context_t& ctx, uint32_t freq) + { + if (freq == 0) { freq = 100000; } // an unset frequency falls back to the usual default + ctx.freq = freq; + // Rounded up: the throttled clock must not come out above the request. + ctx.half_us = (freq >= 500000) ? 0 : (500000 + freq - 1) / freq; + } + + /// 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) + { + SOFT_I2C_LINE_HI(ctx.pin_scl); + soft_i2c_half_wait(ctx.half_us); + if (gpio_in(ctx.pin_scl)) { return true; } + auto us = micros(); + do + { + SOFT_I2C_YIELD(); + if (gpio_in(ctx.pin_scl)) { return true; } + } while ((micros() - us) < 13000); // the same order as a peripheral timeout + return false; + } + + /// Returns true when the byte was acknowledged. + 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); } + 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); + 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; + } + + static inline bool soft_i2c_read_byte(const soft_i2c_context_t& ctx, uint8_t* data, bool ack) + { + uint_fast8_t byte = 0; + SOFT_I2C_LINE_HI(ctx.pin_sda); // released: the device drives the data line + size_t i = 0; + do + { + SOFT_I2C_LINE_LO(ctx.pin_scl); + 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); } + soft_i2c_half_wait(ctx.half_us); + if (!soft_i2c_scl_hi(ctx)) { return false; } + SOFT_I2C_LINE_LO(ctx.pin_scl); + SOFT_I2C_LINE_HI(ctx.pin_sda); + *data = byte; + return true; + } + + /// 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); + soft_i2c_half_wait(ctx.half_us); + bool ok = soft_i2c_scl_hi(ctx); + soft_i2c_half_wait(ctx.half_us); + SOFT_I2C_LINE_HI(ctx.pin_sda); + soft_i2c_half_wait(ctx.half_us); + return ok; + } + + /// Free a bus whose device was left mid transfer (e.g. by a reset) and holds + /// the data line, by clocking the leftover bits out. + static inline void soft_i2c_recover(const soft_i2c_context_t& ctx) + { + 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); + soft_i2c_half_wait(ctx.half_us); + soft_i2c_scl_hi(ctx); + } + soft_i2c_stop_cond(ctx); + } + + /// Ends a transfer on a failure. The bus is stopped and the lock released + /// right away; the error is stored so that it surfaces from the following + /// operation or from endTransaction, as it does on a peripheral path. + static inline void soft_i2c_abort(soft_i2c_context_t& ctx) + { + soft_i2c_stop_cond(ctx); + ctx.state = cpp::fail(error_t::connection_lost); + SOFT_I2C_UNLOCK(ctx); + } + + static inline cpp::result soft_i2c_send_address(soft_i2c_context_t& ctx, int i2c_addr, bool read) + { + bool ack; + if (i2c_addr <= soft_i2c_7bit_addr_max) + { + ack = soft_i2c_write_byte(ctx, i2c_addr << 1 | (read ? 1 : 0)); + } + else + { + ack = soft_i2c_write_byte(ctx, 0xF0 | (i2c_addr >> 8) << 1) + && 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); + soft_i2c_half_wait(ctx.half_us); + 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); + } + } + } + if (!ack) + { + soft_i2c_abort(ctx); + return {}; + } + ctx.state = read ? soft_i2c_context_t::state_t::state_read : soft_i2c_context_t::state_t::state_write; + return {}; + } + +// ------------------------------------------------------------------------ +// Mirrors of the public functions, for the negative ports. + + static inline cpp::result soft_i2c_release(int i2c_port) + { + if (!soft_i2c_valid_port(i2c_port)) { return cpp::fail(error_t::invalid_arg); } + auto& ctx = soft_i2c_ctx(i2c_port); + if (ctx.initialized) + { + ctx.initialized = false; + // Park the pins the same way the peripheral paths do. + if (ctx.pin_scl >= 0) { pinMode(ctx.pin_scl, pin_mode_t::input_pullup); } + if (ctx.pin_sda >= 0) { pinMode(ctx.pin_sda, pin_mode_t::input_pullup); } + } + return {}; + } + + static inline cpp::result soft_i2c_setPins(int i2c_port, int pin_sda, int pin_scl) + { + if (!soft_i2c_valid_port(i2c_port) || pin_sda < 0 || pin_scl < 0) + { + return cpp::fail(error_t::invalid_arg); + } + auto& ctx = soft_i2c_ctx(i2c_port); + if (ctx.initialized && ctx.pin_sda == pin_sda && ctx.pin_scl == pin_scl) + { + return {}; + } + soft_i2c_release(i2c_port).has_value(); + ctx.pin_sda = pin_sda; + ctx.pin_scl = pin_scl; + return {}; + } + + static inline cpp::result soft_i2c_getPinSDA(int i2c_port) + { + if (!soft_i2c_valid_port(i2c_port)) { return cpp::fail(error_t::invalid_arg); } + return soft_i2c_ctx(i2c_port).pin_sda; + } + + static inline cpp::result soft_i2c_getPinSCL(int i2c_port) + { + if (!soft_i2c_valid_port(i2c_port)) { return cpp::fail(error_t::invalid_arg); } + return soft_i2c_ctx(i2c_port).pin_scl; + } + + static inline cpp::result soft_i2c_init(int i2c_port) + { + if (!soft_i2c_valid_port(i2c_port)) { return cpp::fail(error_t::invalid_arg); } + auto& ctx = soft_i2c_ctx(i2c_port); + if (ctx.pin_sda < 0 || ctx.pin_scl < 0) { return cpp::fail(error_t::invalid_arg); } + if (ctx.initialized) + { + soft_i2c_release(i2c_port).has_value(); + } + soft_i2c_set_freq(ctx, 100000); // until a transfer brings its own + // Both lines are released; the pullup carries them high when the bus is + // healthy. ( see SOFT_I2C_LINE_HI for how a line is driven afterwards ) + pinMode(ctx.pin_sda, pin_mode_t::input_pullup); + pinMode(ctx.pin_scl, pin_mode_t::input_pullup); + ctx.state = soft_i2c_context_t::state_t::state_disconnect; + ctx.initialized = true; + return {}; + } + + static inline cpp::result soft_i2c_beginTransaction(int i2c_port, int i2c_addr, uint32_t freq, bool read) + { + if (!soft_i2c_valid_port(i2c_port)) { return cpp::fail(error_t::invalid_arg); } + auto& ctx = soft_i2c_ctx(i2c_port); + if (ctx.pin_sda < 0 || ctx.pin_scl < 0) { return cpp::fail(error_t::invalid_arg); } + if (i2c_addr < soft_i2c_7bit_addr_min || i2c_addr > soft_i2c_10bit_addr_max) { return cpp::fail(error_t::invalid_arg); } + 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); + if (!soft_i2c_scl_hi(ctx)) + { // The clock never rose: no start condition can be made on this bus. + soft_i2c_abort(ctx); + return {}; + } + if (!gpio_in(ctx.pin_sda)) + { + soft_i2c_recover(ctx); + if (!gpio_in(ctx.pin_sda)) + { // The data line is still held; without it no start condition can be + // made, and the acknowledge bits would read as permanently asserted. + soft_i2c_abort(ctx); + return {}; + } + } + SOFT_I2C_LINE_LO(ctx.pin_sda); // start condition + soft_i2c_half_wait(ctx.half_us); + SOFT_I2C_LINE_LO(ctx.pin_scl); + return soft_i2c_send_address(ctx, i2c_addr, read); + } + + static inline cpp::result soft_i2c_restart(int i2c_port, int i2c_addr, uint32_t freq, bool read) + { + if (!soft_i2c_valid_port(i2c_port)) { return cpp::fail(error_t::invalid_arg); } + if (i2c_addr < soft_i2c_7bit_addr_min || i2c_addr > soft_i2c_10bit_addr_max) { return cpp::fail(error_t::invalid_arg); } + auto& ctx = soft_i2c_ctx(i2c_port); + if (ctx.state.has_error()) { return cpp::fail(ctx.state.error()); } + if (ctx.state == soft_i2c_context_t::state_disconnect) + { // Only an open transaction can be restarted; the lock is not held here. + return cpp::fail(error_t::mode_mismatch); + } + soft_i2c_set_freq(ctx, freq); + SOFT_I2C_LINE_HI(ctx.pin_sda); // repeated start + soft_i2c_half_wait(ctx.half_us); + if (!soft_i2c_scl_hi(ctx)) + { + soft_i2c_abort(ctx); + return {}; + } + SOFT_I2C_LINE_LO(ctx.pin_sda); + soft_i2c_half_wait(ctx.half_us); + SOFT_I2C_LINE_LO(ctx.pin_scl); + return soft_i2c_send_address(ctx, i2c_addr, read); + } + + static inline cpp::result soft_i2c_endTransaction(int i2c_port) + { + if (!soft_i2c_valid_port(i2c_port)) { return cpp::fail(error_t::invalid_arg); } + auto& ctx = soft_i2c_ctx(i2c_port); + if (ctx.state.has_error()) + { // The failing operation has already stopped the bus and released the lock; + // report its error here and clear it. + auto err = ctx.state.error(); + ctx.state = soft_i2c_context_t::state_t::state_disconnect; + return cpp::fail(err); + } + if (ctx.state == soft_i2c_context_t::state_disconnect) { return {}; } + bool stopped = soft_i2c_stop_cond(ctx); + ctx.state = soft_i2c_context_t::state_t::state_disconnect; + SOFT_I2C_UNLOCK(ctx); + if (!stopped) { return cpp::fail(error_t::connection_lost); } + return {}; + } + + static inline cpp::result soft_i2c_writeBytes(int i2c_port, const uint8_t *data, size_t length) + { + if (!soft_i2c_valid_port(i2c_port)) { return cpp::fail(error_t::invalid_arg); } + auto& ctx = soft_i2c_ctx(i2c_port); + if (ctx.state.has_error()) { return cpp::fail(ctx.state.error()); } + if (ctx.state != soft_i2c_context_t::state_write) { return cpp::fail(error_t::mode_mismatch); } + if (!length) { return {}; } + do + { + if (!soft_i2c_write_byte(ctx, *data++)) + { + soft_i2c_abort(ctx); + return cpp::fail(error_t::connection_lost); + } + } while (--length); + return {}; + } + + static inline cpp::result soft_i2c_readBytes(int i2c_port, uint8_t *readdata, size_t length, bool last_nack) + { + if (!soft_i2c_valid_port(i2c_port)) { return cpp::fail(error_t::invalid_arg); } + auto& ctx = soft_i2c_ctx(i2c_port); + if (ctx.state.has_error()) { return cpp::fail(ctx.state.error()); } + if (ctx.state != soft_i2c_context_t::state_read) { return cpp::fail(error_t::mode_mismatch); } + if (!length) { return {}; } + do + { + if (!soft_i2c_read_byte(ctx, readdata++, !(last_nack && length == 1))) + { + soft_i2c_abort(ctx); + return cpp::fail(error_t::connection_lost); + } + } while (--length); + return {}; + } + +//---------------------------------------------------------------------------- From 740199032c0bea6bff97a1127d973a120bd8882c Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Sat, 1 Aug 2026 01:54:26 +0000 Subject: [PATCH 12/56] Reach an SPI bus through plain GPIO with a negative host number A negative host selects a software bus driven on the pins directly, without claiming an SPI peripheral: host -1 is slot 0, host -2 is slot 1. The convention matches the touch driver configs, where a negative host has always meant bit banged GPIO. Like the software I2C, the implementation is an internal fragment (soft_spi.inl) included inside the spi namespace of a platform implementation, and the esp32 implementation consumes it here by delegating at the top of each public function. Only the portable pin functions are used, so the fragment is platform independent. Verified with a loopback on hardware: every SPI mode passes at throttled and unthrottled speeds, both through a single open drain pin and through a jumper between two pins, and an out of range host is rejected at init. The quad SPI init refuses a negative host: the software slots are single bit only. --- src/lgfx/v1/platforms/esp32/common.cpp | 40 ++++++ src/lgfx/v1/platforms/soft_spi.inl | 168 +++++++++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 src/lgfx/v1/platforms/soft_spi.inl diff --git a/src/lgfx/v1/platforms/esp32/common.cpp b/src/lgfx/v1/platforms/esp32/common.cpp index 92bf00f9..7c9203ce 100644 --- a/src/lgfx/v1/platforms/esp32/common.cpp +++ b/src/lgfx/v1/platforms/esp32/common.cpp @@ -630,6 +630,14 @@ namespace lgfx #endif static spi_device_handle_t _spi_dev_handle[spi_periph_num] = {nullptr}; +// ------------------------------------------------------------------------ +// Software SPI ( soft_spi.inl ), reached through the negative host numbers. + +#define LGFX_INTERNAL_SOFT_SPI +#include "../soft_spi.inl" + +// ------------------------------------------------------------------------ + 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; @@ -638,6 +646,11 @@ namespace lgfx cpp::result init(int spi_host, int spi_sclk, int spi_miso, int spi_mosi, int dma_channel) { //ESP_LOGI("LGFX","spi::init host:%d, sclk:%d, miso:%d, mosi:%d, dma:%d", spi_host, spi_sclk, spi_miso, spi_mosi, dma_channel); + if (spi_host < 0) + { + return soft_spi_init(spi_host, spi_sclk, spi_miso, spi_mosi); + } + uint32_t spi_port = (spi_host + 1); (void)spi_port; @@ -725,6 +738,7 @@ namespace lgfx cpp::result initQuad(int spi_host, int spi_sclk, int spi_io0, int spi_io1, int spi_io2, int spi_io3, int dma_channel) { //ESP_LOGI("LGFX","spi::init host:%d, sclk:%d, miso:%d, mosi:%d, dma:%d", spi_host, spi_sclk, spi_miso, spi_mosi, dma_channel); + if (spi_host < 0) { return cpp::fail(error_t::invalid_arg); } // the software hosts are single bit only uint32_t spi_port = (spi_host + 1); (void)spi_port; @@ -800,6 +814,11 @@ namespace lgfx void release(int spi_host) { //ESP_LOGI("LGFX","spi::release"); + if (spi_host < 0) + { + soft_spi_release(spi_host); + return; + } #if defined (ARDUINO) && __has_include () // Arduino ESP32 if (_spi_handle[spi_host] != nullptr) { @@ -824,6 +843,11 @@ namespace lgfx void beginTransaction(int spi_host) { + if (spi_host < 0) + { + soft_spi_beginTransaction(spi_host); + return; + } #if defined (ARDUINO) // Arduino ESP32 spiSimpleTransaction(_spi_handle[spi_host]); #else // ESP-IDF @@ -840,6 +864,11 @@ namespace lgfx void beginTransaction(int spi_host, uint32_t freq, int spi_mode) { + if (spi_host < 0) + { + soft_spi_beginTransaction(spi_host, freq, spi_mode); + return; + } uint32_t spi_port = (spi_host + 1); (void)spi_port; uint32_t clkdiv = FreqToClockDiv(getApbFrequency(), freq); @@ -885,6 +914,7 @@ namespace lgfx void endTransaction(int spi_host) { + if (spi_host < 0) { return; } // a software host holds nothing to release if (_spi_dev_handle[spi_host]) { #if defined (ARDUINO) // Arduino ESP32 spiEndTransaction(_spi_handle[spi_host]); @@ -902,6 +932,11 @@ namespace lgfx void writeBytes(int spi_host, const uint8_t* data, size_t len) { + if (spi_host < 0) + { + soft_spi_writeBytes(spi_host, data, len); + return; + } uint32_t spi_port = (spi_host + 1); (void)spi_port; if (len > 64) len = 64; @@ -913,6 +948,11 @@ namespace lgfx void readBytes(int spi_host, uint8_t* data, size_t len) { + if (spi_host < 0) + { + soft_spi_readBytes(spi_host, data, len); + return; + } uint32_t spi_port = (spi_host + 1); (void)spi_port; if (len > 64) len = 64; diff --git a/src/lgfx/v1/platforms/soft_spi.inl b/src/lgfx/v1/platforms/soft_spi.inl new file mode 100644 index 00000000..c6d847c5 --- /dev/null +++ b/src/lgfx/v1/platforms/soft_spi.inl @@ -0,0 +1,168 @@ +/*----------------------------------------------------------------------------/ + Lovyan GFX - Graphics library for embedded devices. + +Original Source: + https://github.com/lovyan03/LovyanGFX/ + +Licence: + [FreeBSD](https://github.com/lovyan03/LovyanGFX/blob/master/license.txt) + +Author: + [lovyan03](https://twitter.com/lovyan03) + +Contributors: + [ciniml](https://github.com/ciniml) + [mongonta0716](https://github.com/mongonta0716) + [tobozo](https://github.com/tobozo) +/----------------------------------------------------------------------------*/ +// SPI on plain GPIO, selected by a negative host number: host -1 is slot 0, +// host -2 is slot 1. It reaches a bus without claiming an SPI peripheral, and +// the convention matches the touch driver configs, where a negative host has +// always meant bit banged GPIO. +// +// This file is not a header but a code fragment: a platform implementation +// includes it inside its spi namespace, and delegates to the soft_spi_*() +// functions at the top of each public function when the host is negative. +// Everything here has internal linkage, so nothing beyond the negative host +// convention becomes part of the library interface. Only the portable pin +// functions are used, which keeps the fragment platform independent. +#if !defined ( LGFX_INTERNAL_SOFT_SPI ) + #error "soft_spi.inl is an implementation fragment of the spi namespace; it cannot be included directly." +#endif + +//---------------------------------------------------------------------------- + + static constexpr int soft_spi_host_count = 2; + + struct soft_spi_context_t + { + int pin_sclk = -1; + int pin_miso = -1; + int pin_mosi = -1; + uint32_t half_us = 0; // 0 = unthrottled + uint8_t mode = 0; + }; + static soft_spi_context_t soft_spi_context[soft_spi_host_count]; + + static inline bool soft_spi_valid_host(int spi_host) { return -soft_spi_host_count <= spi_host && spi_host < 0; } + static inline soft_spi_context_t& soft_spi_ctx(int spi_host) { return soft_spi_context[~spi_host]; } + + static inline void soft_spi_half_wait(uint32_t half_us) + { + if (half_us) { delayMicroseconds(half_us); } + } + + static inline void soft_spi_clock_idle(int pin_sclk, uint8_t spi_mode) + { + if (pin_sclk < 0) { return; } + if (spi_mode & 2) { gpio_hi(pin_sclk); } else { gpio_lo(pin_sclk); } + } + + /// Full duplex byte shift, MSB first. read_data may be the write_data + /// buffer (in place), or nullptr to discard the incoming bits. + /// half_us throttles the clock (0 = unthrottled); the clock is left at its + /// idle level, which the caller has to establish before the first call + /// ( see soft_spi_clock_idle ). + static inline void soft_spi_transfer(uint8_t* read_data, const uint8_t* write_data, size_t len, const soft_spi_context_t& ctx) + { + if (!len) { return; } + const int pin_sclk = ctx.pin_sclk; + const int pin_miso = ctx.pin_miso; + const int pin_mosi = ctx.pin_mosi; + const uint32_t half_us = ctx.half_us; + const bool cpha = ctx.mode & 1; + const bool lead = !(ctx.mode & 2); // the clock level of the leading edge + do + { + uint_fast8_t w = *write_data++; + uint_fast8_t r = 0; + uint_fast8_t mask = 0x80; + do + { + if (!cpha) + { // The data is set while the clock idles and sampled on the leading edge. + if (pin_mosi >= 0) { if (w & mask) { gpio_hi(pin_mosi); } else { gpio_lo(pin_mosi); } } + soft_spi_half_wait(half_us); + if (lead) { gpio_hi(pin_sclk); } else { gpio_lo(pin_sclk); } + if (pin_miso >= 0 && gpio_in(pin_miso)) { r += mask; } + soft_spi_half_wait(half_us); + if (lead) { gpio_lo(pin_sclk); } else { gpio_hi(pin_sclk); } + } + else + { // The data is set on the leading edge and sampled on the trailing edge. + if (lead) { gpio_hi(pin_sclk); } else { gpio_lo(pin_sclk); } + if (pin_mosi >= 0) { if (w & mask) { gpio_hi(pin_mosi); } else { gpio_lo(pin_mosi); } } + soft_spi_half_wait(half_us); + if (lead) { gpio_lo(pin_sclk); } else { gpio_hi(pin_sclk); } + if (pin_miso >= 0 && gpio_in(pin_miso)) { r += mask; } + soft_spi_half_wait(half_us); + } + } while (mask >>= 1); + if (read_data) { *read_data++ = r; } + } while (--len); + } + +// ------------------------------------------------------------------------ +// Mirrors of the public functions, for the negative hosts. + + static inline cpp::result soft_spi_init(int spi_host, int spi_sclk, int spi_miso, int spi_mosi) + { + if (!soft_spi_valid_host(spi_host)) { return cpp::fail(error_t::invalid_arg); } + if (spi_sclk < 0) { return cpp::fail(error_t::invalid_arg); } // the clock is not optional + auto& ctx = soft_spi_ctx(spi_host); + ctx.pin_sclk = spi_sclk; + ctx.pin_miso = spi_miso; + ctx.pin_mosi = spi_mosi; + ctx.mode = 0; + ctx.half_us = 0; + if (spi_sclk >= 0) + { + gpio_lo(spi_sclk); // low first, so that no HIGH pulse leaves the pin (panels without CS) + pinMode(spi_sclk, pin_mode_t::output); + } + if (spi_mosi >= 0) { pinMode(spi_mosi, pin_mode_t::output); } + if (spi_miso >= 0) { pinMode(spi_miso, pin_mode_t::input_pullup); } + return {}; + } + + static inline void soft_spi_release(int spi_host) + { + if (!soft_spi_valid_host(spi_host)) { return; } + auto& ctx = soft_spi_ctx(spi_host); + if (ctx.pin_sclk >= 0) { pinMode(ctx.pin_sclk, pin_mode_t::input); } + if (ctx.pin_miso >= 0) { pinMode(ctx.pin_miso, pin_mode_t::input); } + if (ctx.pin_mosi >= 0) { pinMode(ctx.pin_mosi, pin_mode_t::input); } + ctx = soft_spi_context_t(); + } + + static inline void soft_spi_beginTransaction(int spi_host) + { // Nothing to acquire; just make sure the clock rests at its idle level. + if (!soft_spi_valid_host(spi_host)) { return; } + auto& ctx = soft_spi_ctx(spi_host); + soft_spi_clock_idle(ctx.pin_sclk, ctx.mode); + } + + static inline void soft_spi_beginTransaction(int spi_host, uint32_t freq, int spi_mode) + { + if (!soft_spi_valid_host(spi_host)) { return; } + auto& ctx = soft_spi_ctx(spi_host); + ctx.mode = spi_mode & 3; + if (freq == 0) { freq = 100000; } // an unset frequency falls back to a safe default + // Best effort, rounded up: the throttled clock must not come out above the request. + ctx.half_us = (freq >= 500000) ? 0 : (500000 + freq - 1) / freq; + soft_spi_beginTransaction(spi_host); + } + + static inline void soft_spi_writeBytes(int spi_host, const uint8_t* data, size_t length) + { + if (!soft_spi_valid_host(spi_host)) { return; } + soft_spi_transfer(nullptr, data, length, soft_spi_ctx(spi_host)); + } + + static inline void soft_spi_readBytes(int spi_host, uint8_t* data, size_t length) + { // Full duplex, in place: the same semantics as the peripheral paths. + if (!soft_spi_valid_host(spi_host)) { return; } + soft_spi_transfer(data, data, length, soft_spi_ctx(spi_host)); + } + +//---------------------------------------------------------------------------- From 11fad0d466cf0a1297b76439df69146fb6e0f861 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Sat, 1 Aug 2026 01:54:26 +0000 Subject: [PATCH 13/56] Probe with the software I2C port while the board is still unknown The autodetect probes now go over the software I2C port, so nothing on the hardware side is claimed or reconfigured until a board is confirmed; a candidate that does not match leaves no trace beyond the pins, which the existing pin backups already restore. Once a board is confirmed, the bus is handed over to the hardware port that its backlight and touch use at run time, preserving the open-port state those consumers rely on. _detect_i2c_device() carried its own bit banged address probe; it now rides on the same software port. This also removes the ACK contention it had (the probe drove the lines push-pull, so a device pulling the acknowledge low fought the driver), and the pull-up plausibility check it performs beforehand stays as it was. The paths where no board matched did not release the hardware port on the C6 and P4 sections; with the probe on the software port there is no hardware claim to leak, and the software slot is returned explicitly. Verified on hardware for the three affected chips (C5 / C6 / P4 boards): detection, display, backlight and touch all work, and the internal I2C scan results are byte for byte identical to builds without this change. --- src/M5GFX.cpp | 115 +++++++++++++++++++++++++++++--------------------- 1 file changed, 66 insertions(+), 49 deletions(-) diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index 973b7028..18f8833c 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -82,6 +82,13 @@ namespace m5gfx } } + // ボード未確定段階の I2C プローブに使うソフトウェア I2C ポート (GPIO ビットバン)。 + // ハードウェアのペリフェラルを一切確保・設定しないため、候補ボードの試行が + // ペリフェラルやピンの状態を汚さない。ボード確定後の常用バスは従来どおり + // ハードウェアポートを使う。 + __attribute__ ((unused)) + static constexpr int_fast16_t probe_i2c_port = -1; + // I2Cデバイスの存在をチェックする。 // SDA,SCLのプルアップが確認できない場合は0を返す。 // プルアップが確認できた場合は ~0u を返すが、存在しないデバイスに対応するビットは 0 となる。 @@ -134,31 +141,17 @@ namespace m5gfx // 全ビットを立てる result = ~0u; - // for (int addr7bit = 0x08; addr7bit < 0x78; ++addr7bit) { + // アドレスの存在確認はソフトウェア I2C ポートで行う (オープンドレイン駆動で + // ACK 競合が起きず、ハードウェアのペリフェラルにも触れない) + lgfx::i2c::init(probe_i2c_port, pin_sda, pin_scl); for (; addr_list[0] != 0; ++addr_list) { - // 7bit addr + write flag + ack check; uint_fast8_t addr7bit = addr_list[0]; - // ACKチェック用ビットも含め左2bitシフトし、9bit相当にする - uint_fast16_t sda_bits = (addr7bit << 2u) + 1u; - lgfx::delayMicroseconds(5); - lgfx::gpio_lo(pin_sda); - lgfx::delayMicroseconds(5); - for (int i = 0; i < 9; ++i) { - lgfx::gpio_lo(pin_scl); - if (sda_bits & 0x100u) { lgfx::gpio_hi(pin_sda); } - else { lgfx::gpio_lo(pin_sda); } - sda_bits <<= 1; - lgfx::delayMicroseconds(3); - lgfx::gpio_hi(pin_scl); - lgfx::delayMicroseconds(6); - } - bool hit = !lgfx::gpio_in(pin_sda); + bool hit = lgfx::i2c::beginTransaction(probe_i2c_port, addr7bit, 100000, false).has_value() + && lgfx::i2c::endTransaction(probe_i2c_port).has_value(); result = (result << 1) + hit; ESP_LOGV(LIBRARY_NAME, "[Autodetect] i2c addr:%02x = %s", (int)addr7bit, hit ? "hit" : "--"); - - // i2c stop - lgfx::gpio::command(cmd_i2c_stop_list); } + lgfx::i2c::release(probe_i2c_port); } else { result = 0; } @@ -2763,10 +2756,11 @@ The usage of each pin is as follows. // TP INT = GPIO_NUM_23 lgfx::pinMode(GPIO_NUM_23, lgfx::pin_mode_t::output); // TP INT lgfx::gpio_hi(GPIO_NUM_23); // select I2C Addr (high=0x14 / low=0x5D) - lgfx::i2c::init(in_i2c_port, GPIO_NUM_31, GPIO_NUM_32); + // ボード確定まではソフトウェア I2C で通信し、ハードウェアポートを温存する + lgfx::i2c::init(probe_i2c_port, GPIO_NUM_31, GPIO_NUM_32); - id = lgfx::i2c::readRegister8(in_i2c_port, pi4io1_i2c_addr, 0x01).has_value() - && lgfx::i2c::readRegister8(in_i2c_port, pi4io2_i2c_addr, 0x01).has_value(); + 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"); @@ -2795,10 +2789,10 @@ The usage of each pin is as follows. 0xFF,0xFF,0xFF, }; - i2c_write_register8_array(in_i2c_port, pi4io1_i2c_addr, reg_data_io1_1, 100000); - i2c_write_register8_array(in_i2c_port, pi4io2_i2c_addr, reg_data_io2, 100000); + i2c_write_register8_array(probe_i2c_port, pi4io1_i2c_addr, reg_data_io1_1, 100000); + i2c_write_register8_array(probe_i2c_port, pi4io2_i2c_addr, reg_data_io2, 100000); lgfx::delay(10); - i2c_write_register8_array(in_i2c_port, pi4io1_i2c_addr, reg_data_io1_2, 100000); + i2c_write_register8_array(probe_i2c_port, pi4io1_i2c_addr, reg_data_io1_2, 100000); lgfx::pinMode(GPIO_NUM_23, lgfx::pin_mode_t::input); // TP INT lgfx::delay(100); @@ -2821,7 +2815,7 @@ The usage of each pin is as follows. for (int i = 0; i < 3; ++i) { uint8_t fw_version = 0; uint8_t fw_reg[2] = { 0, 0 }; - if (lgfx::i2c::transactionWriteRead(in_i2c_port, Touch_ST7123::default_addr, fw_reg, sizeof(fw_reg), &fw_version, 1, 100000).has_value()) { + if (lgfx::i2c::transactionWriteRead(probe_i2c_port, Touch_ST7123::default_addr, fw_reg, sizeof(fw_reg), &fw_version, 1, 100000).has_value()) { read_st_touch_fw = true; ESP_LOGI(LIBRARY_NAME, "M5Tab5 ST touch FW version %02x", fw_version); if (fw_version == 1) { @@ -2840,6 +2834,11 @@ The usage of each pin is as follows. ESP_LOGW(LIBRARY_NAME, "M5Tab5 ST touch FW version read failed"); } + // ボードが確定し I2C の用は済んだので、常用するハードウェアポートへ + // バスを引き継ぐ (タッチがこのポートを使う) + lgfx::i2c::release(probe_i2c_port); + lgfx::i2c::init(in_i2c_port, GPIO_NUM_31, GPIO_NUM_32); + auto bus_dsi = new Bus_DSI(); _bus_last.reset(bus_dsi); auto bus_cfg = bus_dsi->config(); @@ -2964,6 +2963,8 @@ The usage of each pin is as follows. } goto init_clear; } + // ボード不成立時のみここへ来る。プローブに使ったソフトウェアポートを返す + lgfx::i2c::release(probe_i2c_port); } } @@ -3065,7 +3066,8 @@ The usage of each pin is as follows. } else if (result == 0x03) { // NessoN1 ? - lgfx::i2c::init(i2c_port, GPIO_NUM_10, GPIO_NUM_8); + // パネル ID で確定するまではソフトウェア I2C で通信する + lgfx::i2c::init(probe_i2c_port, GPIO_NUM_10, GPIO_NUM_8); // PI4IO E0 // P0 BTN1 // P1 BTN2 @@ -3101,8 +3103,8 @@ The usage of each pin is as follows. 0x05, 0b10000010, 0, // PI4IO_REG_OUT_SET 0xFF,0xFF,0xFF, }; - i2c_write_register8_array(i2c_port, pi4io2_i2c_addr, reg_data_io2, 100000); - i2c_write_register8_array(i2c_port, pi4io1_i2c_addr, reg_data_io1, 100000); + i2c_write_register8_array(probe_i2c_port, pi4io2_i2c_addr, reg_data_io2, 100000); + i2c_write_register8_array(probe_i2c_port, pi4io1_i2c_addr, reg_data_io1, 100000); bus_cfg.pin_mosi = GPIO_NUM_21; bus_cfg.pin_miso = GPIO_NUM_22; @@ -3120,6 +3122,11 @@ The usage of each pin is as follows. board = board_t::board_ArduinoNessoN1; ESP_LOGI(LIBRARY_NAME, "[Autodetect] board_ArduinoNessoN1"); + // ボードが確定したので、常用するハードウェアポートへバスを引き継ぐ + // (バックライトとタッチがこのポートを使う) + lgfx::i2c::release(probe_i2c_port); + lgfx::i2c::init(i2c_port, GPIO_NUM_10, GPIO_NUM_8); + bus_spi->release(); bus_cfg.freq_write = 40000000; bus_cfg.freq_read = 16000000; @@ -3174,6 +3181,9 @@ The usage of each pin is as follows. } bus_spi->release(); } + // ボード不成立時のみここへ来る (成立時は goto で抜けている)。 + // プローブに使ったソフトウェアポートを返し、ピンを元へ戻す + lgfx::i2c::release(probe_i2c_port); for (auto &bup : backup_pins) { bup.restore(); } } } @@ -3213,16 +3223,17 @@ The usage of each pin is as follows. , GPIO_NUM_26 }; - lgfx::i2c::init(i2c_port, toughc5_i2c_sda, toughc5_i2c_scl); + // ボード確定まではソフトウェア I2C で通信し、ハードウェアポートを温存する + lgfx::i2c::init(probe_i2c_port, toughc5_i2c_sda, toughc5_i2c_scl); - if (_check_m5pm1(i2c_port) && _check_m5ioe1(i2c_port)) { + if (_check_m5pm1(probe_i2c_port) && _check_m5ioe1(probe_i2c_port)) { // M5PM1 初期化 - lgfx::i2c::writeRegister8(i2c_port, m5pm1_i2c_addr, 0x09, 0x00, 0, m5pm1_i2c_freq); // I2C sleep disable - lgfx::i2c::writeRegister8(i2c_port, m5pm1_i2c_addr, 0x0A, 0x00, 0, m5pm1_i2c_freq); // WDT disable - lgfx::i2c::bitOn( i2c_port, m5pm1_i2c_addr, 0x06, 0x17, m5pm1_i2c_freq); // PWR_CFG: LED_CTRL, LDO, DCDC, CHG enable + lgfx::i2c::writeRegister8(probe_i2c_port, m5pm1_i2c_addr, 0x09, 0x00, 0, m5pm1_i2c_freq); // I2C sleep disable + lgfx::i2c::writeRegister8(probe_i2c_port, m5pm1_i2c_addr, 0x0A, 0x00, 0, m5pm1_i2c_freq); // WDT disable + lgfx::i2c::bitOn( probe_i2c_port, m5pm1_i2c_addr, 0x06, 0x17, m5pm1_i2c_freq); // PWR_CFG: LED_CTRL, LDO, DCDC, CHG enable // M5IOE1 初期化 - lgfx::i2c::writeRegister8(i2c_port, m5ioe1_i2c_addr, 0x23, 0x00, 0, m5ioe1_i2c_freq); // I2C sleep disable + lgfx::i2c::writeRegister8(probe_i2c_port, m5ioe1_i2c_addr, 0x23, 0x00, 0, m5ioe1_i2c_freq); // I2C sleep disable // M5IOE1 PIN4(LCD_RST), PIN5(LCD_EN), PIN10(LCD_BL) を出力に設定 // PIN4=bit3, PIN5=bit4 → low register (0x03/0x05/0x13) @@ -3230,30 +3241,30 @@ The usage of each pin is as follows. static constexpr uint8_t IOE1_PIN_10 = 9; static constexpr uint8_t IOE1_BIT_10_H = (1u << (IOE1_PIN_10 - 8)); - lgfx::i2c::bitOff(i2c_port, m5ioe1_i2c_addr, 0x13, 0b00011000, m5ioe1_i2c_freq); // PIN4,5 push-pull - lgfx::i2c::bitOff(i2c_port, m5ioe1_i2c_addr, 0x14, IOE1_BIT_10_H, m5ioe1_i2c_freq); // PIN10 push-pull - lgfx::i2c::bitOn( i2c_port, m5ioe1_i2c_addr, 0x03, 0b00011000, m5ioe1_i2c_freq); // PIN4,5 output - lgfx::i2c::bitOn( i2c_port, m5ioe1_i2c_addr, 0x04, IOE1_BIT_10_H, m5ioe1_i2c_freq); // PIN10 output + lgfx::i2c::bitOff(probe_i2c_port, m5ioe1_i2c_addr, 0x13, 0b00011000, m5ioe1_i2c_freq); // PIN4,5 push-pull + lgfx::i2c::bitOff(probe_i2c_port, m5ioe1_i2c_addr, 0x14, IOE1_BIT_10_H, m5ioe1_i2c_freq); // PIN10 push-pull + lgfx::i2c::bitOn( probe_i2c_port, m5ioe1_i2c_addr, 0x03, 0b00011000, m5ioe1_i2c_freq); // PIN4,5 output + lgfx::i2c::bitOn( probe_i2c_port, m5ioe1_i2c_addr, 0x04, IOE1_BIT_10_H, m5ioe1_i2c_freq); // PIN10 output // LCD enable - lgfx::i2c::bitOn( i2c_port, m5ioe1_i2c_addr, 0x05, 1 << 4, m5ioe1_i2c_freq); // PIN5(LCD_EN) HIGH + lgfx::i2c::bitOn( probe_i2c_port, m5ioe1_i2c_addr, 0x05, 1 << 4, m5ioe1_i2c_freq); // PIN5(LCD_EN) HIGH // LCD reset if (use_reset) { - lgfx::i2c::bitOff(i2c_port, m5ioe1_i2c_addr, 0x05, 1 << 3, m5ioe1_i2c_freq); // PIN4(LCD_RST) LOW + lgfx::i2c::bitOff(probe_i2c_port, m5ioe1_i2c_addr, 0x05, 1 << 3, m5ioe1_i2c_freq); // PIN4(LCD_RST) LOW lgfx::delay(2); - lgfx::i2c::bitOn( i2c_port, m5ioe1_i2c_addr, 0x05, 1 << 3, m5ioe1_i2c_freq); // PIN4(LCD_RST) HIGH + lgfx::i2c::bitOn( probe_i2c_port, m5ioe1_i2c_addr, 0x05, 1 << 3, m5ioe1_i2c_freq); // PIN4(LCD_RST) HIGH lgfx::delay(10); } // M5PM1 GPIO2(TP_RST) を出力に設定してリセット - lgfx::i2c::bitOff(i2c_port, m5pm1_i2c_addr, 0x16, 0b11 << (2*2), m5pm1_i2c_freq); // GPIO2 → GPIO function - lgfx::i2c::bitOn( i2c_port, m5pm1_i2c_addr, 0x10, 1 << 2, m5pm1_i2c_freq); // GPIO2 output - lgfx::i2c::bitOff(i2c_port, m5pm1_i2c_addr, 0x13, 1 << 2, m5pm1_i2c_freq); // GPIO2 push-pull + lgfx::i2c::bitOff(probe_i2c_port, m5pm1_i2c_addr, 0x16, 0b11 << (2*2), m5pm1_i2c_freq); // GPIO2 → GPIO function + lgfx::i2c::bitOn( probe_i2c_port, m5pm1_i2c_addr, 0x10, 1 << 2, m5pm1_i2c_freq); // GPIO2 output + lgfx::i2c::bitOff(probe_i2c_port, m5pm1_i2c_addr, 0x13, 1 << 2, m5pm1_i2c_freq); // GPIO2 push-pull if (use_reset) { - lgfx::i2c::bitOff(i2c_port, m5pm1_i2c_addr, 0x11, 1 << 2, m5pm1_i2c_freq); // GPIO2(TP_RST) LOW + lgfx::i2c::bitOff(probe_i2c_port, m5pm1_i2c_addr, 0x11, 1 << 2, m5pm1_i2c_freq); // GPIO2(TP_RST) LOW lgfx::delay(2); - lgfx::i2c::bitOn( i2c_port, m5pm1_i2c_addr, 0x11, 1 << 2, m5pm1_i2c_freq); // GPIO2(TP_RST) HIGH + lgfx::i2c::bitOn( probe_i2c_port, m5pm1_i2c_addr, 0x11, 1 << 2, m5pm1_i2c_freq); // GPIO2(TP_RST) HIGH lgfx::delay(10); } @@ -3273,6 +3284,11 @@ The usage of each pin is as follows. board = board_t::board_M5ToughC5; ESP_LOGI(LIBRARY_NAME, "[Autodetect] board_M5ToughC5"); + // ボードが確定したので、常用するハードウェアポートへバスを引き継ぐ + // (バックライトとタッチがこのポートを使う) + lgfx::i2c::release(probe_i2c_port); + lgfx::i2c::init(i2c_port, toughc5_i2c_sda, toughc5_i2c_scl); + bus_spi->release(); bus_cfg.freq_write = 20000000; bus_cfg.freq_read = 16000000; @@ -3319,7 +3335,8 @@ The usage of each pin is as follows. } bus_spi->release(); } - lgfx::i2c::release(i2c_port); + // ここへ来るのはボード不成立の場合のみ。ソフトウェアポートしか触れていない + lgfx::i2c::release(probe_i2c_port); for (auto &bup : backup_pins) { bup.restore(); } } From 8dc9333a9624b273d682078e0c0aefabff32f3a2 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Sat, 1 Aug 2026 01:54:26 +0000 Subject: [PATCH 14/56] Let Bus_I2C take a negative port number The config carried the port in an unsigned byte, so a software port could not be named there. Store it signed, the way the touch configs already do, and skip the peripheral busy checks for a software port, whose transfers are synchronous. The values in actual use (0 and 1) read the same either way. --- src/lgfx/v1/platforms/esp32/Bus_I2C.cpp | 2 ++ src/lgfx/v1/platforms/esp32/Bus_I2C.hpp | 2 +- src/lgfx/v1/platforms/sdl/Bus_I2C.hpp | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lgfx/v1/platforms/esp32/Bus_I2C.cpp b/src/lgfx/v1/platforms/esp32/Bus_I2C.cpp index d079d6e4..9acc74e7 100644 --- a/src/lgfx/v1/platforms/esp32/Bus_I2C.cpp +++ b/src/lgfx/v1/platforms/esp32/Bus_I2C.cpp @@ -119,6 +119,7 @@ namespace lgfx void Bus_I2C::wait(void) { + if (_cfg.i2c_port < 0) { return; } // a software port transfers synchronously #if I2C_NUM_MAX > 1 auto dev = (_cfg.i2c_port == 0) ? &I2C0 : &I2C1; #else @@ -130,6 +131,7 @@ namespace lgfx bool Bus_I2C::busy(void) const { + if (_cfg.i2c_port < 0) { return false; } // a software port transfers synchronously #if I2C_NUM_MAX > 1 auto dev = (_cfg.i2c_port == 0) ? &I2C0 : &I2C1; #else diff --git a/src/lgfx/v1/platforms/esp32/Bus_I2C.hpp b/src/lgfx/v1/platforms/esp32/Bus_I2C.hpp index 21a52441..fa4330d2 100644 --- a/src/lgfx/v1/platforms/esp32/Bus_I2C.hpp +++ b/src/lgfx/v1/platforms/esp32/Bus_I2C.hpp @@ -35,7 +35,7 @@ namespace lgfx uint32_t freq_read = 400000; int16_t pin_scl = 22; int16_t pin_sda = 21; - uint8_t i2c_port = 0; // e.g. ESP32 0=I2C_NUM_0 / 1=I2C_NUM_1 + int8_t i2c_port = 0; // e.g. ESP32 0=I2C_NUM_0 / 1=I2C_NUM_1 / negative=software uint8_t i2c_addr = 0x3C; uint32_t prefix_cmd = 0x00; uint32_t prefix_data = 0x40; diff --git a/src/lgfx/v1/platforms/sdl/Bus_I2C.hpp b/src/lgfx/v1/platforms/sdl/Bus_I2C.hpp index 2662b455..a55557bb 100644 --- a/src/lgfx/v1/platforms/sdl/Bus_I2C.hpp +++ b/src/lgfx/v1/platforms/sdl/Bus_I2C.hpp @@ -35,7 +35,7 @@ namespace lgfx uint32_t freq_read = 400000; int16_t pin_scl = 22; int16_t pin_sda = 21; - uint8_t i2c_port = 0; + int8_t i2c_port = 0; uint8_t i2c_addr = 0x3C; uint32_t prefix_cmd = 0x00; uint32_t prefix_data = 0x40; From 14aa93c4948a395f00604bcb1fb656d03cb957da Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Sat, 1 Aug 2026 02:25:41 +0000 Subject: [PATCH 15/56] Poll the port the transfer is on in Bus_I2C::wait() and busy() Both functions picked the register block with #if I2C_NUM_MAX > 1, but I2C_NUM_MAX is an enum constant, not a macro: the preprocessor evaluates the condition as 0 > 1, the I2C1 branch never compiled, and every port was polled as I2C0 (verified on ESP-IDF 4.4 and 5.x). On a second hardware port, and on the low power ports now that they are reachable, the functions report an idle bus while a transfer is still on the wire. Resolve the device through the i2c implementation instead, which already knows every port kind, as new i2c::wait() / i2c::busy() entry points; a software port reports not busy there, since its transfers are synchronous. The bus busy bit moves into a getBusBusy() accessor next to the other per-target register accessors, replacing both the duplicated target lists in beginTransaction() and the member detection templates that Bus_I2C.cpp carried for the same purpose. --- src/lgfx/v1/platforms/esp32/Bus_I2C.cpp | 41 ++----------------------- src/lgfx/v1/platforms/esp32/common.cpp | 36 ++++++++++++++++------ src/lgfx/v1/platforms/esp32/common.hpp | 2 ++ 3 files changed, 30 insertions(+), 49 deletions(-) diff --git a/src/lgfx/v1/platforms/esp32/Bus_I2C.cpp b/src/lgfx/v1/platforms/esp32/Bus_I2C.cpp index 9acc74e7..118fea57 100644 --- a/src/lgfx/v1/platforms/esp32/Bus_I2C.cpp +++ b/src/lgfx/v1/platforms/esp32/Bus_I2C.cpp @@ -21,35 +21,12 @@ Original Source: #include "Bus_I2C.hpp" #include "../../misc/pixelcopy.hpp" -#include -#include - namespace lgfx { inline namespace v1 { //---------------------------------------------------------------------------- - // status_reg メンバーの存在を検出 - template - struct has_status_reg : std::false_type {}; - - template - struct has_status_reg().status_reg))> : std::true_type {}; - - // bus_busy 取得 - template - static inline typename std::enable_if::value, bool>::type - i2c_dev_bus_busy(const T& dev) { - return dev.status_reg.bus_busy; - } - - template - static inline typename std::enable_if::value, bool>::type - i2c_dev_bus_busy(const T& dev) { - return dev.sr.bus_busy; - } - void Bus_I2C::config(const config_t& config) { _cfg = config; @@ -119,26 +96,12 @@ namespace lgfx void Bus_I2C::wait(void) { - if (_cfg.i2c_port < 0) { return; } // a software port transfers synchronously -#if I2C_NUM_MAX > 1 - auto dev = (_cfg.i2c_port == 0) ? &I2C0 : &I2C1; -#else - auto dev = &I2C0; -#endif - - while (i2c_dev_bus_busy(*dev)) { taskYIELD(); } + lgfx::i2c::wait(_cfg.i2c_port); } bool Bus_I2C::busy(void) const { - if (_cfg.i2c_port < 0) { return false; } // a software port transfers synchronously -#if I2C_NUM_MAX > 1 - auto dev = (_cfg.i2c_port == 0) ? &I2C0 : &I2C1; -#else - auto dev = &I2C0; -#endif - - return i2c_dev_bus_busy(*dev); + return lgfx::i2c::busy(_cfg.i2c_port); } void Bus_I2C::dc_control(bool dc) diff --git a/src/lgfx/v1/platforms/esp32/common.cpp b/src/lgfx/v1/platforms/esp32/common.cpp index 7c9203ce..26d9e334 100644 --- a/src/lgfx/v1/platforms/esp32/common.cpp +++ b/src/lgfx/v1/platforms/esp32/common.cpp @@ -1398,6 +1398,15 @@ namespace lgfx #endif } + static bool getBusBusy(i2c_dev_t* dev) + { +#if defined ( CONFIG_IDF_TARGET_ESP32C3 ) || 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 ) + return dev->sr.bus_busy; +#else + return dev->status_reg.bus_busy; +#endif + } + static void i2c_set_cmd(i2c_dev_t* dev, uint8_t index, uint8_t op_code, uint8_t byte_num, bool flg_nack = false) { /* @@ -1910,11 +1919,7 @@ namespace lgfx auto dev = getDev(i2c_port); i2c_context[i2c_port].lock(); -#if defined ( CONFIG_IDF_TARGET_ESP32C3 ) || 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 ) - if (dev->sr.bus_busy) -#else - if (dev->status_reg.bus_busy) -#endif + if (getBusBusy(dev)) { //ESP_LOGI("LGFX", "i2c::begin wait"); auto ms = micros(); @@ -1922,11 +1927,7 @@ namespace lgfx { taskYIELD(); } -#if defined ( CONFIG_IDF_TARGET_ESP32C3 ) || 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 ) - while (dev->sr.bus_busy && micros() - ms < 128); -#else - while (dev->status_reg.bus_busy && micros() - ms < 128); -#endif + while (getBusBusy(dev) && micros() - ms < 128); } i2c_context[i2c_port].save_reg(dev); @@ -1996,6 +1997,21 @@ namespace lgfx return i2c_wait(i2c_port, true); } //*/ + bool busy(int i2c_port) + { + if (isSoftPort(i2c_port)) { return false; } // a software port transfers synchronously + if (i2c_port >= I2C_NUM_MAX) { return false; } + return getBusBusy(getDev(i2c_port)); + } + + void wait(int i2c_port) + { + if (isSoftPort(i2c_port)) { return; } // a software port transfers synchronously + if (i2c_port >= I2C_NUM_MAX) { return; } + auto dev = getDev(i2c_port); + while (getBusBusy(dev)) { taskYIELD(); } + } + cpp::result writeBytes(int i2c_port, const uint8_t *data, size_t length) { if (isSoftPort(i2c_port)) { return soft_i2c_writeBytes(i2c_port, data, length); } diff --git a/src/lgfx/v1/platforms/esp32/common.hpp b/src/lgfx/v1/platforms/esp32/common.hpp index 4effeda5..9f780d69 100644 --- a/src/lgfx/v1/platforms/esp32/common.hpp +++ b/src/lgfx/v1/platforms/esp32/common.hpp @@ -355,6 +355,8 @@ namespace lgfx cpp::result init(int i2c_port); cpp::result getPinSDA(int i2c_port); cpp::result getPinSCL(int i2c_port); + bool busy(int i2c_port); + void wait(int i2c_port); struct i2c_temporary_switcher_t { From dfd96e381f7a0460163c78aab09c438f84e6ed4f Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Sat, 1 Aug 2026 02:48:52 +0000 Subject: [PATCH 16/56] Refuse a low power port number when the low power support is absent The port arguments were bounds checked against I2C_NUM_MAX, which counts every port the ESP-IDF headers know of. On an ESP-IDF recent enough to number the low power port but older than the 5.4 that the low power support here requires, that bound accepts the low power port number while getDev() has no low power branch and folds it onto a high power port: the functions silently drive that peripheral instead, alongside its legitimate user and without its lock. Check against the ports this implementation can actually drive instead (the high power ports, plus the low power ones when compiled in), so an unsupported low power port number fails with invalid_arg. On the configurations where the support is present, and on the ESP-IDF versions that predate the low power port entirely, both bounds are equal and nothing changes. --- src/lgfx/v1/platforms/esp32/common.cpp | 33 ++++++++++++++++---------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/lgfx/v1/platforms/esp32/common.cpp b/src/lgfx/v1/platforms/esp32/common.cpp index 26d9e334..c9703058 100644 --- a/src/lgfx/v1/platforms/esp32/common.cpp +++ b/src/lgfx/v1/platforms/esp32/common.cpp @@ -1010,6 +1010,15 @@ namespace lgfx #define LGFX_LP_I2C_NUM 0 #endif +// The ports this implementation can drive: the high power ones, plus the low power ones +// when they are supported. This is the bound the port arguments are checked against. +// I2C_NUM_MAX cannot serve there: on an ESP-IDF recent enough to count the low power +// port but too old for the support above, a low power port number would pass the check +// and getDev() would fold it onto a high power port, silently driving that one instead. +// ( SOC_*_NUM carry unsigned suffixes, so the sum is brought back to int for the +// comparisons against a port number. ) +#define LGFX_I2C_PORT_NUM ( (int)( LGFX_HP_I2C_NUM + LGFX_LP_I2C_NUM ) ) + /// True if this port index belongs to the low power I2C. static inline bool isLpPort(int i2c_port) { @@ -1568,7 +1577,7 @@ namespace lgfx cpp::result release(int i2c_port) { if (isSoftPort(i2c_port)) { return soft_i2c_release(i2c_port); } - if (i2c_port >= I2C_NUM_MAX) { return cpp::fail(error_t::invalid_arg); } + if (i2c_port >= LGFX_I2C_PORT_NUM) { return cpp::fail(error_t::invalid_arg); } if (i2c_context[i2c_port].initialized) { i2c_context[i2c_port].initialized = false; @@ -1635,7 +1644,7 @@ namespace lgfx return cpp::fail(error_t::invalid_arg); } if (isSoftPort(i2c_port)) { return soft_i2c_setPins(i2c_port, pin_sda, pin_scl); } - if (i2c_port >= I2C_NUM_MAX) { return cpp::fail(error_t::invalid_arg); } + if (i2c_port >= LGFX_I2C_PORT_NUM) { return cpp::fail(error_t::invalid_arg); } if (i2c_context[i2c_port].initialized && i2c_context[i2c_port].pin_scl == (gpio_num_t)pin_scl @@ -1675,21 +1684,21 @@ namespace lgfx cpp::result getPinSDA(int i2c_port) { if (isSoftPort(i2c_port)) { return soft_i2c_getPinSDA(i2c_port); } - if (i2c_port >= I2C_NUM_MAX) { return cpp::fail(error_t::invalid_arg); } + if (i2c_port >= LGFX_I2C_PORT_NUM) { return cpp::fail(error_t::invalid_arg); } return i2c_context[i2c_port].pin_sda; } cpp::result getPinSCL(int i2c_port) { if (isSoftPort(i2c_port)) { return soft_i2c_getPinSCL(i2c_port); } - if (i2c_port >= I2C_NUM_MAX) { return cpp::fail(error_t::invalid_arg); } + if (i2c_port >= LGFX_I2C_PORT_NUM) { return cpp::fail(error_t::invalid_arg); } return i2c_context[i2c_port].pin_scl; } cpp::result init(int i2c_port) { if (isSoftPort(i2c_port)) { return soft_i2c_init(i2c_port); } - if (i2c_port >= I2C_NUM_MAX) { return cpp::fail(error_t::invalid_arg); } + if (i2c_port >= LGFX_I2C_PORT_NUM) { return cpp::fail(error_t::invalid_arg); } gpio_num_t pin_sda = i2c_context[i2c_port].pin_sda; gpio_num_t pin_scl = i2c_context[i2c_port].pin_scl; if (((uint32_t)pin_scl >= GPIO_NUM_MAX) @@ -1784,7 +1793,7 @@ namespace lgfx cpp::result restart(int i2c_port, int i2c_addr, uint32_t freq, bool read) { if (isSoftPort(i2c_port)) { return soft_i2c_restart(i2c_port, i2c_addr, freq, read); } - if (i2c_port >= I2C_NUM_MAX) { return cpp::fail(error_t::invalid_arg); } + if (i2c_port >= LGFX_I2C_PORT_NUM) { return cpp::fail(error_t::invalid_arg); } if (i2c_addr < I2C_7BIT_ADDR_MIN || i2c_addr > I2C_10BIT_ADDR_MAX) return cpp::fail(error_t::invalid_arg); auto res = i2c_wait(i2c_port); @@ -1911,7 +1920,7 @@ namespace lgfx cpp::result beginTransaction(int i2c_port, int i2c_addr, uint32_t freq, bool read) { if (isSoftPort(i2c_port)) { return soft_i2c_beginTransaction(i2c_port, i2c_addr, freq, read); } - if (i2c_port >= I2C_NUM_MAX) return cpp::fail(error_t::invalid_arg); + if (i2c_port >= LGFX_I2C_PORT_NUM) return cpp::fail(error_t::invalid_arg); if ((uint32_t)i2c_context[i2c_port].pin_sda >= GPIO_NUM_MAX || (uint32_t)i2c_context[i2c_port].pin_scl >= GPIO_NUM_MAX) return cpp::fail(error_t::invalid_arg); //ESP_LOGI("LGFX", "i2c::beginTransaction : port:%d / addr:%02x / freq:%d / rw:%d", i2c_port, i2c_addr, freq, read); @@ -1993,21 +2002,21 @@ namespace lgfx cpp::result endTransaction(int i2c_port) { if (isSoftPort(i2c_port)) { return soft_i2c_endTransaction(i2c_port); } - if (i2c_port >= I2C_NUM_MAX) return cpp::fail(error_t::invalid_arg); + if (i2c_port >= LGFX_I2C_PORT_NUM) return cpp::fail(error_t::invalid_arg); return i2c_wait(i2c_port, true); } //*/ bool busy(int i2c_port) { if (isSoftPort(i2c_port)) { return false; } // a software port transfers synchronously - if (i2c_port >= I2C_NUM_MAX) { return false; } + if (i2c_port >= LGFX_I2C_PORT_NUM) { return false; } return getBusBusy(getDev(i2c_port)); } void wait(int i2c_port) { if (isSoftPort(i2c_port)) { return; } // a software port transfers synchronously - if (i2c_port >= I2C_NUM_MAX) { return; } + if (i2c_port >= LGFX_I2C_PORT_NUM) { return; } auto dev = getDev(i2c_port); while (getBusBusy(dev)) { taskYIELD(); } } @@ -2015,7 +2024,7 @@ namespace lgfx cpp::result writeBytes(int i2c_port, const uint8_t *data, size_t length) { if (isSoftPort(i2c_port)) { return soft_i2c_writeBytes(i2c_port, data, length); } - if (i2c_port >= I2C_NUM_MAX) { return cpp::fail(error_t::invalid_arg); } + if (i2c_port >= LGFX_I2C_PORT_NUM) { return cpp::fail(error_t::invalid_arg); } if (i2c_context[i2c_port].state.has_error()) { return cpp::fail(i2c_context[i2c_port].state.error()); } if (i2c_context[i2c_port].state != i2c_context_t::state_write) { return cpp::fail(error_t::mode_mismatch); } cpp::result res {}; @@ -2054,7 +2063,7 @@ namespace lgfx cpp::result readBytes(int i2c_port, uint8_t *readdata, size_t length, bool last_nack) { if (isSoftPort(i2c_port)) { return soft_i2c_readBytes(i2c_port, readdata, length, last_nack); } - if (i2c_port >= I2C_NUM_MAX) { return cpp::fail(error_t::invalid_arg); } + if (i2c_port >= LGFX_I2C_PORT_NUM) { return cpp::fail(error_t::invalid_arg); } if (i2c_context[i2c_port].state.has_error()) { return cpp::fail(i2c_context[i2c_port].state.error()); } if (i2c_context[i2c_port].state != i2c_context_t::state_read) { return cpp::fail(error_t::mode_mismatch); } cpp::result res {}; From 4ad26d0fedebf1185a853aa9154bd1592301d242 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Wed, 5 Aug 2026 08:17:49 +0000 Subject: [PATCH 17/56] Report the last reading when Touch_CHSC6540 runs out of retries The consecutive reads exist to reject a frame whose bytes changed mid-read, but while a finger is moving no two reads agree: the retries run out and the function reported no touch at all, so drags and flicks kept breaking up mid-gesture. Adopt the most recent reading instead, the way Touch_FT5x06 already does. A frame whose coordinate payload could not be read is dropped rather than adopted, so a torn read does not turn into a touch at (0,0). --- src/lgfx/v1/touch/Touch_CHSC6540.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lgfx/v1/touch/Touch_CHSC6540.cpp b/src/lgfx/v1/touch/Touch_CHSC6540.cpp index f82dbf16..2f619512 100644 --- a/src/lgfx/v1/touch/Touch_CHSC6540.cpp +++ b/src/lgfx/v1/touch/Touch_CHSC6540.cpp @@ -72,13 +72,17 @@ namespace lgfx { flip = !flip; size_t points = std::min(count, tmp[0]); - if (points && lgfx::i2c::readBytes(_cfg.i2c_port, &tmp[1], points * 6 - 2)) - {} + if (points && !lgfx::i2c::readBytes(_cfg.i2c_port, &tmp[1], points * 6 - 2)) + { // 座標を読み切れなかったフレームは破棄する (ゼロ埋めの座標を有効タッチとして返さないため); + tmp[0] = 0; + } } if (lgfx::i2c::endTransaction(_cfg.i2c_port)) {} if (tmp[0] == 0 || memcmp(buf[0], buf[1], len) == 0) break; } - if (0 == --retry) return 0; + // リトライを使い切った場合はタッチ無しとせず直近の読み値を採用する。 + // (指の移動中は連続読出しが一致しないため、return 0 だとフリックが取れない) + if (0 == --retry) { break; } } if (count > tmp[0]) count = tmp[0]; From 851dcf9b5034f51bfa1332e87aa67a0299777b08 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Wed, 5 Aug 2026 08:17:49 +0000 Subject: [PATCH 18/56] Write the DMA descriptor address where AHB_DMA (C5/C61) expects it On the GDMA chips (C3 / S3 / C6) the OUT_LINK register carries the descriptor address bits [19:0] together with the START bit, and Bus_SPI ORs the address into its START writes. The AHB_DMA generation dropped the address bits from OUT_LINK and reads them from a separate OUT_LINK_ADDR_CHn register: the OR is silently ignored, the DMA starts with no descriptor, and the first DMA transfer hangs waiting for an OUT FIFO that never fills. The P4 (AXI_DMA) already needed a split address register; reuse that two-register path on the C5 and C61, whose OUT_LINK_ADDR_CHn registers sit 4 bytes apart. The H4 carries an AHB_DMA too but lays these registers out inside each channel block, so it keeps the old path. Verified on an ESP32-C5. --- src/lgfx/v1/platforms/esp32/Bus_SPI.cpp | 14 +++++++++++--- src/lgfx/v1/platforms/esp32/Bus_SPI.hpp | 3 ++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/lgfx/v1/platforms/esp32/Bus_SPI.cpp b/src/lgfx/v1/platforms/esp32/Bus_SPI.cpp index 673686b6..aa2d0c9e 100644 --- a/src/lgfx/v1/platforms/esp32/Bus_SPI.cpp +++ b/src/lgfx/v1/platforms/esp32/Bus_SPI.cpp @@ -103,6 +103,12 @@ Original Source: #define DMA_OUTLINK_START_CH0 AHB_DMA_OUTLINK_START_CH0 #define DMA_OUTFIFO_EMPTY_CH0 AHB_DMA_OUTFIFO_EMPTY_CH0 #define SIZE_OF_DMA_OUT_CH (sizeof(AHB_DMA.channel[0])) + // AHB_DMA 世代のうち C5/C61 は OUT_LINK レジスタが制御ビットのみになり、 + // ディスクリプタアドレスは別レジスタ (チャンネルごとに 4 バイト刻み) へ書く; + // (同じ AHB_DMA でも H4 はアドレスレジスタをチャンネルブロック内に持つため対象外) + #if defined (CONFIG_IDF_TARGET_ESP32C5) || defined (CONFIG_IDF_TARGET_ESP32C61) + #define DMA_OUT_LINK_ADDR_CH0_REG AHB_DMA_OUT_LINK_ADDR_CH0_REG + #endif #else #if __has_include() #if !defined DMA_OUT_LINK_CH0_REG @@ -224,6 +230,8 @@ namespace lgfx _spi_dma_outstatus_reg = reg(DMA_OUTFIFO_STATUS_CH0_REG + assigned_dma_ch * SIZE_OF_DMA_OUT_CH); #if defined ( CONFIG_IDF_TARGET_ESP32P4 ) _spi_dma_out_link2_reg = reg(AXI_DMA_OUT_LINK2_CH0_REG + assigned_dma_ch * SIZE_OF_DMA_OUT_CH); + #elif defined ( DMA_OUT_LINK_ADDR_CH0_REG ) + _spi_dma_out_link2_reg = reg(DMA_OUT_LINK_ADDR_CH0_REG + assigned_dma_ch * sizeof(uint32_t)); #endif } #elif defined ( CONFIG_IDF_TARGET_ESP32 ) || !defined ( CONFIG_IDF_TARGET ) @@ -712,7 +720,7 @@ namespace lgfx if (use_dma) { auto spi_dma_out_link_reg = _spi_dma_out_link_reg; - #if defined ( CONFIG_IDF_TARGET_ESP32P4 ) + #if defined ( CONFIG_IDF_TARGET_ESP32P4 ) || defined ( DMA_OUT_LINK_ADDR_CH0_REG ) auto spi_dma_out_link2_reg = _spi_dma_out_link2_reg; #endif auto cmd = _spi_cmd_reg; @@ -728,7 +736,7 @@ namespace lgfx auto dma = reg(SPI_DMA_CONF_REG(_spi_port)); *dma = 0; /// Clear previous transfer uint32_t len = ((length - 1) & ((SPI_MS_DATA_BITLEN)>>3)) + 1; - #if defined ( CONFIG_IDF_TARGET_ESP32P4 ) + #if defined ( CONFIG_IDF_TARGET_ESP32P4 ) || defined ( DMA_OUT_LINK_ADDR_CH0_REG ) *spi_dma_out_link2_reg = ((uint32_t)(_dmadesc)); *spi_dma_out_link_reg = DMA_OUTLINK_START_CH0 ; #else @@ -946,7 +954,7 @@ namespace lgfx *_spi_dma_out_link_reg = 0; #if defined ( SOC_GDMA_SUPPORTED ) && defined ( DMA_OUTLINK_START_CH0 ) - #if defined ( CONFIG_IDF_TARGET_ESP32P4 ) + #if defined ( CONFIG_IDF_TARGET_ESP32P4 ) || defined ( DMA_OUT_LINK_ADDR_CH0_REG ) *_spi_dma_out_link2_reg = ((uint32_t)(_dmadesc)); *_spi_dma_out_link_reg = DMA_OUTLINK_START_CH0; #else diff --git a/src/lgfx/v1/platforms/esp32/Bus_SPI.hpp b/src/lgfx/v1/platforms/esp32/Bus_SPI.hpp index e0c75598..996c8260 100644 --- a/src/lgfx/v1/platforms/esp32/Bus_SPI.hpp +++ b/src/lgfx/v1/platforms/esp32/Bus_SPI.hpp @@ -193,7 +193,8 @@ namespace lgfx volatile uint32_t* _spi_cmd_reg = nullptr; volatile uint32_t* _spi_user_reg = nullptr; volatile uint32_t* _spi_dma_out_link_reg = nullptr; - #if defined (CONFIG_IDF_TARGET_ESP32P4) + // P4 (AXI_DMA) と C5/C61 (AHB_DMA) はディスクリプタアドレスを OUT_LINK と別のレジスタへ書く; + #if defined (CONFIG_IDF_TARGET_ESP32P4) || defined (CONFIG_IDF_TARGET_ESP32C5) || defined (CONFIG_IDF_TARGET_ESP32C61) volatile uint32_t* _spi_dma_out_link2_reg = nullptr; #endif volatile uint32_t* _spi_dma_outstatus_reg = nullptr; From 4ae71a136960ce2056e8a3090e1317c1922fd915 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Wed, 5 Aug 2026 08:19:06 +0000 Subject: [PATCH 19/56] Drive the ToughC5 internal I2C devices on the low power port The internal bus (SDA=G2 / SCL=G3) sits exactly on the C5's fixed LP_I2C pads. Move the backlight, the touch panel and the post-detection bus onto the low power port; Port A is a level-shifted branch of the same bus, so this leaves the C5's single high power I2C controller entirely free. On Arduino builds this also takes the internal devices off TwoWire, since the low power port is driven by the ESP-IDF driver directly. The port choice follows the exact condition the common I2C code compiles its low power support under (ESP-IDF 5.4 with the new driver); anything older stays on the high power port. Pairs with the M5Unified change that moves In_I2C to the same port. --- src/M5GFX.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index 18f8833c..b37f5811 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -753,7 +753,18 @@ namespace m5gfx #elif defined (CONFIG_IDF_TARGET_ESP32C5) static constexpr int32_t i2c_freq = 400000; + // 内部 I2C (G2/G3) は LP_I2C の固定パッドと一致するため LP ポートへ割り当てる。 + // PortA も同じバスの物理分配のため、これで HP の I2C0 が丸ごと空く。 + // (ポート番号は HP ポート数の次 = C5 では 1。Arduino ビルドでも LP は + // TwoWire を介さず ESP-IDF ドライバで直接駆動される) + // 条件は common.cpp が LP 対応をコンパイルする条件と同一に保つこと + // (条件を満たさない SDK では LP ポートを開けないため HP へフォールバックする) +#if defined ( SOC_LP_I2C_NUM ) && ( SOC_LP_I2C_NUM > 0 ) && __has_include ( ) \ + && defined ( ESP_IDF_VERSION_VAL ) && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0) + static constexpr int_fast16_t i2c_port = LP_I2C_NUM_0; +#else static constexpr int_fast16_t i2c_port = I2C_NUM_0; +#endif struct Light_M5ToughC5 : public lgfx::ILight { @@ -3320,7 +3331,7 @@ The usage of each pin is as follows. cfg.pin_sda = toughc5_i2c_sda; cfg.pin_scl = toughc5_i2c_scl; cfg.i2c_addr = 0x2E; - cfg.i2c_port = I2C_NUM_0; + cfg.i2c_port = i2c_port; cfg.freq = 400000; cfg.x_min = 0; cfg.x_max = 319; From bcbe859d2ec151e39f2e3d49720ef126f808a881 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Wed, 5 Aug 2026 08:19:06 +0000 Subject: [PATCH 20/56] Raise the ToughC5 SPI write clock to 40MHz The same ILI9342 panel already runs at this rate on the Core2 and Tough; verified on the actual board. --- src/M5GFX.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index b37f5811..7fce57f6 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -3301,7 +3301,7 @@ The usage of each pin is as follows. lgfx::i2c::init(i2c_port, toughc5_i2c_sda, toughc5_i2c_scl); bus_spi->release(); - bus_cfg.freq_write = 20000000; + bus_cfg.freq_write = 40000000; bus_cfg.freq_read = 16000000; bus_spi->config(bus_cfg); From 87b556933191bff7ff3b3f860b2679f10d95ae82 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Fri, 7 Aug 2026 03:17:30 +0000 Subject: [PATCH 21/56] Release leftover low power routing when a software I2C port is initialized A pin routed to the low power I2C keeps that routing across a reset, because it is held in the low power domain rather than by the CPU. set_pin() already hands such pins back before taking them for a hardware port, but the software port path did not: the bit-banged lines stayed bound to the low power peripheral and never reached the pads, so the board autodetect probe (which runs on a software port) failed on every warm boot until a power cycle reset the routing. Release the pads in init() for a software port as well, the same way the hardware port path does. --- src/lgfx/v1/platforms/esp32/common.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/lgfx/v1/platforms/esp32/common.cpp b/src/lgfx/v1/platforms/esp32/common.cpp index c9703058..bf22c21c 100644 --- a/src/lgfx/v1/platforms/esp32/common.cpp +++ b/src/lgfx/v1/platforms/esp32/common.cpp @@ -1697,7 +1697,20 @@ namespace lgfx cpp::result init(int i2c_port) { - if (isSoftPort(i2c_port)) { return soft_i2c_init(i2c_port); } + if (isSoftPort(i2c_port)) + { +#if LGFX_LP_I2C_NUM > 0 && SOC_RTCIO_PIN_COUNT > 0 + if (soft_i2c_valid_port(i2c_port)) + { // 低電力ポートのルーティングはリセットを跨いで残るため、前回実行が + // LP ポートとして使ったピンをソフトポートで取り直す場合にも返却が + // 必要になる。( see releaseLpPad ) + auto& ctx = soft_i2c_ctx(i2c_port); + releaseLpPad((gpio_num_t)ctx.pin_sda); + releaseLpPad((gpio_num_t)ctx.pin_scl); + } +#endif + return soft_i2c_init(i2c_port); + } if (i2c_port >= LGFX_I2C_PORT_NUM) { return cpp::fail(error_t::invalid_arg); } gpio_num_t pin_sda = i2c_context[i2c_port].pin_sda; gpio_num_t pin_scl = i2c_context[i2c_port].pin_scl; From a230cd82f0e15cdd3ab1e38616697a1f0317155c Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Fri, 7 Aug 2026 08:01:08 +0000 Subject: [PATCH 22/56] Reset a low power I2C port through its own reset register The HP helper i2c_ll_reset_register() indexes the PCR I2C register array with the port number, and a low power port number is out of range for that array (it holds only the high power ports), so a forced stop on the low power port was resetting an unrelated register. Route low power ports to lp_i2c_ll_reset_register() instead. --- src/lgfx/v1/platforms/esp32/common.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/lgfx/v1/platforms/esp32/common.cpp b/src/lgfx/v1/platforms/esp32/common.cpp index bf22c21c..b8528dde 100644 --- a/src/lgfx/v1/platforms/esp32/common.cpp +++ b/src/lgfx/v1/platforms/esp32/common.cpp @@ -1149,6 +1149,13 @@ namespace lgfx __attribute__ ((unused)) static void i2c_periph_reset(int i2c_num) { +#if LGFX_LP_I2C_NUM > 0 + if (isLpPort(i2c_num)) + { // HP 用の i2c_ll_reset_register は SoC の PCR I2C 配列を範囲外参照するため LP 専用関数を使う; + lp_i2c_ll_reset_register(i2c_num - LGFX_HP_I2C_NUM); + return; + } +#endif I2C_RCC_ATOMIC() { i2c_ll_reset_register(i2c_num); (void)__DECLARE_RCC_ATOMIC_ENV; From 52638d0f18fc0b44bafc478a06514b120f0a7f0c Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Fri, 7 Aug 2026 08:01:48 +0000 Subject: [PATCH 23/56] Keep an errored I2C transaction on the forced stop path i2c_wait() chose between the normal STOP command and the forced stop by reading int_raw, which was never initialized when the ack-wait stage had been skipped, so the choice was undefined after a failed read. The normal STOP path waits for a completion interrupt that never fires on a peripheral that has already been force-stopped by the error handling, costing the full 14ms timeout on every failed transaction. Initialize int_raw, treat an errored context as needing the forced stop, and skip the redundant hardware stop when the error handler has already issued one. --- src/lgfx/v1/platforms/esp32/common.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/lgfx/v1/platforms/esp32/common.cpp b/src/lgfx/v1/platforms/esp32/common.cpp index b8528dde..593071f7 100644 --- a/src/lgfx/v1/platforms/esp32/common.cpp +++ b/src/lgfx/v1/platforms/esp32/common.cpp @@ -1498,6 +1498,7 @@ namespace lgfx if (i2c_context[i2c_port].state == i2c_context_t::state_disconnect) { return res; } 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; if (i2c_context[i2c_port].wait_ack_stage) @@ -1541,12 +1542,17 @@ namespace lgfx if (flg_stop || res.has_error()) { #if 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 ) - if (res.has_error() || i2c_context[i2c_port].state == i2c_context_t::state_read || !int_raw.end_detect_int_raw) +// エラー発生後はペリフェラルが強制停止済みの場合があり、通常のSTOPコマンド発行では完了割り込みが来ずタイムアウトまで待たされるため強制STOP側へ分岐する; + if (res.has_error() || i2c_context[i2c_port].state.has_error() || i2c_context[i2c_port].state == i2c_context_t::state_read || !int_raw.end_detect_int_raw) #else - if (res.has_error() || i2c_context[i2c_port].state == i2c_context_t::state_read || !int_raw.end_detect) + if (res.has_error() || i2c_context[i2c_port].state.has_error() || i2c_context[i2c_port].state == i2c_context_t::state_read || !int_raw.end_detect) #endif { // force stop - i2c_stop(i2c_port); + // state が既にエラーの場合はエラー検出箇所で停止済みのため再停止しない (res のエラーはこの呼び出しで検出されたもので未停止); + if (res.has_error() || !i2c_context[i2c_port].state.has_error()) + { + i2c_stop(i2c_port); + } } else { From 98a68f5cc3a10b2207246bf4fb13f90d0be7aae0 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Fri, 7 Aug 2026 08:02:13 +0000 Subject: [PATCH 24/56] Report a pure address NACK on read as NACK instead of timeout A read from an absent address ended in the "read timeout" warning, although the NACK is the expected outcome when probing for devices, and printing the warning costs several milliseconds per probe on a 115200bps console, slowing bus scans considerably. Report it at verbose level as a NACK, but only when neither the timeout flag nor the arbitration-lost flag accompanies it. --- src/lgfx/v1/platforms/esp32/common.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/lgfx/v1/platforms/esp32/common.cpp b/src/lgfx/v1/platforms/esp32/common.cpp index 593071f7..272e2460 100644 --- a/src/lgfx/v1/platforms/esp32/common.cpp +++ b/src/lgfx/v1/platforms/esp32/common.cpp @@ -2150,8 +2150,17 @@ namespace lgfx if (0 == getRxFifoCount(dev)) { + uint32_t int_raw_val = dev->int_raw.val; i2c_stop(i2c_port); - ESP_LOGW("LGFX", "i2c read error : read timeout"); + if ((int_raw_val & I2C_ACK_ERR_INT_RAW_M) + && !(int_raw_val & (I2C_TIME_OUT_INT_RAW_M | I2C_ARBITRATION_LOST_INT_RAW_M))) + { // Pure address NACK means no device is present: not a timeout, so no warning. + ESP_LOGV("LGFX", "i2c read error : nack"); + } + else + { + ESP_LOGW("LGFX", "i2c read error : read timeout"); + } res = cpp::fail(error_t::connection_lost); i2c_context[i2c_port].state = cpp::fail(error_t::connection_lost); i2c_context[i2c_port].wait_ack_stage = 0; From 44336bcffee357215dcdc08838e03a98b9636c64 Mon Sep 17 00:00:00 2001 From: jyy Date: Fri, 7 Aug 2026 21:07:24 +0800 Subject: [PATCH 25/56] Add Core2 ILI9342E panel support --- src/M5GFX.cpp | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index 7fce57f6..8cbcc4c3 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -248,6 +248,71 @@ namespace m5gfx _rotation = 1; // default rotation } + void initPanelByTouchVersion() + { + static constexpr uint8_t touch_addr = 0x38; + static constexpr uint8_t cipher_reg = 0xA3; + static constexpr uint8_t firmid_reg = 0xA6; + static constexpr uint8_t vendid_reg = 0xA8; + static constexpr uint8_t ili9342c_firmid = 0x10; + static constexpr uint8_t ili9342e_firmid = 0x12; + static constexpr uint8_t m5stack_vendor = 0x11; + static constexpr int32_t version_i2c_freq = 100000; + + uint8_t touch_cipher = 0; + uint8_t touch_firmid = 0; + uint8_t touch_vendid = 0; + bool touch_info_valid = false; + for (int retry = 0; retry < 5 && !touch_info_valid; ++retry) + { + auto set_work_mode = lgfx::i2c::writeRegister8( + axp_i2c_port, touch_addr, 0x00, 0x00, 0, version_i2c_freq); + auto read_cipher = lgfx::i2c::readRegister8( + axp_i2c_port, touch_addr, cipher_reg, version_i2c_freq); + auto read_firmid = lgfx::i2c::readRegister8( + axp_i2c_port, touch_addr, firmid_reg, version_i2c_freq); + auto read_vendid = lgfx::i2c::readRegister8( + axp_i2c_port, touch_addr, vendid_reg, version_i2c_freq); + + touch_cipher = read_cipher.has_value() ? read_cipher.value() : 0; + touch_firmid = read_firmid.has_value() ? read_firmid.value() : 0; + touch_vendid = read_vendid.has_value() ? read_vendid.value() : 0; + touch_info_valid = set_work_mode.has_value() + && read_firmid.has_value() + && read_vendid.has_value() + && touch_vendid == m5stack_vendor + && (touch_firmid == ili9342c_firmid + || touch_firmid == ili9342e_firmid); + if (!touch_info_valid) + { + lgfx::delay(20); + } + } + + bool use_ili9342e = touch_info_valid + && touch_firmid == ili9342e_firmid; + if (touch_info_valid) + { + ESP_LOGI(LIBRARY_NAME, + "Core2 touch CIPHER:0x%02x / FIRMID:0x%02x / VENDID:0x%02x, panel:%s", + (int)touch_cipher, (int)touch_firmid, (int)touch_vendid, + use_ili9342e ? "ILI9342E" : "ILI9342C"); + } + else + { + ESP_LOGW(LIBRARY_NAME, + "Core2 touch version read failed (CIPHER:0x%02x / FIRMID:0x%02x / VENDID:0x%02x), panel:ILI9342C", + (int)touch_cipher, (int)touch_firmid, (int)touch_vendid); + } + + if (use_ili9342e) + { + startWrite(true); + command_list(getIli9342EInitCommands()); + endWrite(); + } + } + void rst_control(bool level) override { uint8_t bits = level ? 2 : 0; @@ -255,6 +320,31 @@ namespace m5gfx // AXP192 reg 0x96 = GPIO3&4 control lgfx::i2c::writeRegister8(axp_i2c_port, axp_i2c_addr, 0x96, bits, mask, axp_i2c_freq); } + + protected: + static const uint8_t* getIli9342EInitCommands() + { + static constexpr uint8_t list0[] = + { + 0xDD, 1, 0x01, + 0x3A, 1, 0x55, + 0x21, 0, + 0x36, 1, 0x08, + 0xD5, 1, 0x00, + 0xB1, 1, 0x22, + 0xC8, 1, 0x38, + 0xCB, 1, 0x1C, + 0xC9, 1, 0x1A, + 0xCA, 1, 0x1A, + 0xB7, 4, 0x5A,0x41,0x11,0x19, + 0xE4,15, 0x04,0x08,0x11,0x06,0x12,0x07,0x3A,0x76,0x47,0x07,0x0F,0x0A,0x11,0x19,0x05, + 0xE5,15, 0x02,0x03,0x07,0x06,0x12,0x07,0x36,0x5F,0x48,0x06,0x10,0x0C,0x16,0x14,0x09, + 0x11, 0 + CMD_INIT_DELAY, 120, + 0x29, 0 + CMD_INIT_DELAY, 120, + 0xFF,0xFF, + }; + return list0; + } }; struct Light_M5StackCore2 : public lgfx::ILight @@ -998,6 +1088,14 @@ namespace m5gfx return false; } +#if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) + if (board == board_t::board_M5StackCore2) + { + static_cast(_panel_last.get()) + ->initPanelByTouchVersion(); + } +#endif + #if defined (CONFIG_IDF_TARGET_ESP32S3) switch (board) { default: From bd96c127f0fbb5ba96a347ddb2d08958ee215e21 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 10 Aug 2026 01:59:42 +0000 Subject: [PATCH 26/56] Add Panel_TM1680 LED matrix driver I2C driver for the Titan Micro TM1680 in 24ROW x 16COM mode, used as a 16x16 monochrome LED matrix. - The frame buffer keeps the Panel_1bitOLED page layout and is converted to the TM1680 RAM layout (row bits A3-A0 in the upper nibble half) when pushed by display(). - The whole frame is only 32 bytes, so display() always sends it in full as a single transaction. - The controller does not respond to I2C reads, so init() skips the read probe used by other 1bit panels. - There is no display-invert command; inversion is applied while packing the buffer. - Brightness maps to the global 16-step PWM duty. --- src/lgfx/v1/panel/Panel_TM1680.cpp | 142 +++++++++++++++++++++++++++++ src/lgfx/v1/panel/Panel_TM1680.hpp | 76 +++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 src/lgfx/v1/panel/Panel_TM1680.cpp create mode 100644 src/lgfx/v1/panel/Panel_TM1680.hpp diff --git a/src/lgfx/v1/panel/Panel_TM1680.cpp b/src/lgfx/v1/panel/Panel_TM1680.cpp new file mode 100644 index 00000000..04f4dda9 --- /dev/null +++ b/src/lgfx/v1/panel/Panel_TM1680.cpp @@ -0,0 +1,142 @@ +/*----------------------------------------------------------------------------/ + Lovyan GFX - Graphics library for embedded devices. + +Original Source: + https://github.com/lovyan03/LovyanGFX/ + +Licence: + [FreeBSD](https://github.com/lovyan03/LovyanGFX/blob/master/license.txt) + +Author: + [lovyan03](https://twitter.com/lovyan03) + +Contributors: + [ciniml](https://github.com/ciniml) + [mongonta0716](https://github.com/mongonta0716) + [tobozo](https://github.com/tobozo) +/----------------------------------------------------------------------------*/ +#include "Panel_TM1680.hpp" +#include "../Bus.hpp" +#include "../platforms/common.hpp" + +#include + +namespace lgfx +{ + inline namespace v1 + { +//---------------------------------------------------------------------------- + + bool Panel_TM1680::init(bool use_reset) + { + // The TM1680 does not respond to I2C reads, so the read probe of + // Panel_1bitOLED::init cannot be used; send the init commands without probing. + if (!Panel_HasBuffer::init(use_reset)) + { + return false; + } + // Panel_HasBuffer::init does not clear the allocated buffer; + // clear it here so setInvert below does not push garbage to the LEDs. + memset(_buf, 0, _get_buffer_length()); + + startWrite(true); + for (size_t i = 0; auto cmds = getInitCommands(i); i++) + { + size_t idx = 0; + while (cmds[idx] != 0xFF || cmds[idx + 1] != 0xFF) ++idx; + if (idx) { _bus->writeBytes(cmds, idx, false, true); } + } + setInvert(_invert); + setRotation(_rotation); + endWrite(); + + return true; + } + + void Panel_TM1680::setInvert(bool invert) + { + _invert = invert; + // The TM1680 has no display-invert command; invert while packing in display(). + if (_buf) + { + startWrite(); + display(0, 0, _cfg.panel_width, _cfg.panel_height); + endWrite(); + } + } + + void Panel_TM1680::setSleep(bool flg) + { + startWrite(); + if (flg) + { + _bus->writeCommand(CMD_LED_OFF, 8); + _bus->writeCommand(CMD_SYS_DIS, 8); + } + else + { + _bus->writeCommand(CMD_SYS_EN, 8); + _bus->writeCommand(CMD_LED_ON, 8); + } + endWrite(); + } + + void Panel_TM1680::setBrightness(uint8_t brightness) + { + startWrite(); + if (brightness) + { + _bus->writeCommand(CMD_PWM_DUTY + (((brightness * 16) >> 8) & 0x0F), 8); + _bus->writeCommand(CMD_LED_ON, 8); + } + else + { + _bus->writeCommand(CMD_LED_OFF, 8); + } + endWrite(); + } + + void Panel_TM1680::display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h) + { + if (0 < w && 0 < h) + { + uint_fast16_t xs = x, xe = x + w - 1; + uint_fast16_t ys = y, ye = y + h - 1; + _update_transferred_rect(xs, ys, xe, ye); + } + if (_range_mod.empty()) { return; } + + // The whole frame is only 32 bytes; always send it in full. + // The leading byte is the RAM write address. + uint8_t sendbuf[1 + ((16 * 16) >> 3)]; + sendbuf[0] = 0x00; + bool invert = _invert ^ _cfg.invert; + + for (uint_fast8_t py = 0; py < 16; ++py) + { + uint32_t mask = 1 << (py & 7); + auto src = &_buf[(py >> 3) * 16]; + for (uint_fast8_t xb = 0; xb < 2; ++xb) + { + uint_fast8_t v = 0; + for (uint_fast8_t bit = 0; bit < 8; ++bit) + { + if (src[(xb << 3) + bit] & mask) + { // in TM1680 RAM, rows A3-A0 occupy the upper nibble half + v |= 1 << ((bit > 3) ? (bit & 3) : (4 + bit)); + } + } + sendbuf[1 + py * 2 + xb] = invert ? (v ^ 0xFF) : v; + } + } + + _bus->writeBytes(sendbuf, sizeof(sendbuf), true, true); + _range_mod.top = INT16_MAX; + _range_mod.left = INT16_MAX; + _range_mod.right = 0; + _range_mod.bottom = 0; + } + +//---------------------------------------------------------------------------- + } +} diff --git a/src/lgfx/v1/panel/Panel_TM1680.hpp b/src/lgfx/v1/panel/Panel_TM1680.hpp new file mode 100644 index 00000000..d07dbdb8 --- /dev/null +++ b/src/lgfx/v1/panel/Panel_TM1680.hpp @@ -0,0 +1,76 @@ +/*----------------------------------------------------------------------------/ + Lovyan GFX - Graphics library for embedded devices. + +Original Source: + https://github.com/lovyan03/LovyanGFX/ + +Licence: + [FreeBSD](https://github.com/lovyan03/LovyanGFX/blob/master/license.txt) + +Author: + [lovyan03](https://twitter.com/lovyan03) + +Contributors: + [ciniml](https://github.com/ciniml) + [mongonta0716](https://github.com/mongonta0716) + [tobozo](https://github.com/tobozo) +/----------------------------------------------------------------------------*/ +#pragma once + +#include "Panel_SSD1306.hpp" + +namespace lgfx +{ + inline namespace v1 + { +//---------------------------------------------------------------------------- + + // Titan Micro TM1680 LED matrix driver (I2C, 24ROW x 16COM mode). + // The frame buffer keeps the Panel_1bitOLED page layout and is converted + // to the TM1680 RAM layout when pushed by display(). + // The protocol has no command/data prefix byte: configure Bus_I2C with prefix_len = 0. + struct Panel_TM1680 : public Panel_1bitOLED + { + Panel_TM1680(void) + { + _cfg.memory_width = _cfg.panel_width = 16; + _cfg.memory_height = _cfg.panel_height = 16; + _auto_display = true; + } + + bool init(bool use_reset) override; + void setInvert(bool invert) override; + void setSleep(bool flg) override; + void setBrightness(uint8_t brightness) override; + void display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h) override; + + protected: + static constexpr uint8_t CMD_SYS_DIS = 0x80; + static constexpr uint8_t CMD_SYS_EN = 0x81; + static constexpr uint8_t CMD_LED_OFF = 0x82; + static constexpr uint8_t CMD_LED_ON = 0x83; + static constexpr uint8_t CMD_RC_MASTER = 0x98; // internal RC oscillator, master mode + static constexpr uint8_t CMD_COM_16NMOS = 0xA4; // 24ROW x 16COM (NMOS) + static constexpr uint8_t CMD_PWM_DUTY = 0xB0; // +0..15 = duty 1/16..16/16 + + const uint8_t* getInitCommands(uint8_t listno) const override + { + static constexpr uint8_t list0[] = { + CMD_SYS_DIS, + CMD_COM_16NMOS, + CMD_RC_MASTER, + CMD_SYS_EN, + CMD_PWM_DUTY + 1, + CMD_LED_ON, + 0xFF, 0xFF, // end + }; + switch (listno) { + case 0: return list0; + default: return nullptr; + } + } + }; + +//---------------------------------------------------------------------------- + } +} From 12976dec020817778cce60eceee6695cb0a3c7c5 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 10 Aug 2026 01:59:42 +0000 Subject: [PATCH 27/56] Add support for M5Stack CoreMatrix (ESP32-C61) Autodetect probes the M5PM1 and M5IOE1 over software I2C first, then powers the LED matrix rail through M5IOE1 PIN4 before checking for the TM1680: the controller sits on that rail and does not respond on I2C until powered. The TM1680 has no ID register, so presence is confirmed by an address ACK only. The default rotation is 1 with offset_rotation 3, which shows upright with the ABC buttons at the top (verified on hardware). --- src/M5GFX.cpp | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index 8cbcc4c3..861f6c69 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -19,6 +19,7 @@ #include "lgfx/v1/panel/Panel_ILI9342.hpp" #include "lgfx/v1/panel/Panel_SSD1306.hpp" #include "lgfx/v1/panel/Panel_SSD1677.hpp" +#include "lgfx/v1/panel/Panel_TM1680.hpp" #include "lgfx/v1/panel/Panel_ST7735.hpp" #include "lgfx/v1/panel/Panel_ST7789.hpp" #include "lgfx/v1/panel/Panel_GC9A01.hpp" @@ -50,6 +51,11 @@ static constexpr int_fast16_t in_i2c_port = I2C_NUM_1; #include #endif + +#elif defined ( CONFIG_IDF_TARGET_ESP32C61 ) + +#include "lgfx/v1/platforms/esp32/Bus_I2C.hpp" + #endif #else @@ -879,6 +885,12 @@ namespace m5gfx // Touch_M5ToughC5 は lgfx::Touch_CHSC6540 に統合済み +#elif defined (CONFIG_IDF_TARGET_ESP32C61) + + static constexpr int32_t i2c_freq = 400000; + static constexpr int_fast16_t i2c_port = I2C_NUM_0; + static constexpr std::uint8_t tm1680_i2c_addr = 0x72; // CoreMatrix LED matrix driver + #elif defined (CONFIG_IDF_TARGET_ESP32C6) static constexpr int32_t i2c_freq = 400000; @@ -3449,6 +3461,81 @@ The usage of each pin is as follows. for (auto &bup : backup_pins) { bup.restore(); } } +#elif defined (CONFIG_IDF_TARGET_ESP32C61) + + if (board == 0 || board == board_t::board_M5CoreMatrix) + { + // CoreMatrix: system I2C SDA=G0 / SCL=G1 (physically shared with Grove and M-Bus) + static constexpr int_fast16_t corematrix_i2c_sda = GPIO_NUM_0; + static constexpr int_fast16_t corematrix_i2c_scl = GPIO_NUM_1; + + gpio::pin_backup_t backup_pins[] = + { GPIO_NUM_0 + , GPIO_NUM_1 + }; + + // Probe over software I2C; the hardware port is left untouched until the board is confirmed + lgfx::i2c::init(probe_i2c_port, corematrix_i2c_sda, corematrix_i2c_scl); + + if (_check_m5pm1(probe_i2c_port) && _check_m5ioe1(probe_i2c_port)) { + lgfx::i2c::writeRegister8(probe_i2c_port, m5pm1_i2c_addr, 0x09, 0x00, 0, m5pm1_i2c_freq); // I2C sleep disable + lgfx::i2c::writeRegister8(probe_i2c_port, m5pm1_i2c_addr, 0x0A, 0x00, 0, m5pm1_i2c_freq); // WDT disable + + // Drive M5IOE1 PIN4 (LEDS_EN) high to power the LED matrix rail. + // The TM1680 sits on that rail and does not respond on I2C until powered. + lgfx::i2c::writeRegister8(probe_i2c_port, m5ioe1_i2c_addr, 0x23, 0x00, 0, m5ioe1_i2c_freq); // I2C sleep disable + lgfx::i2c::bitOff(probe_i2c_port, m5ioe1_i2c_addr, 0x13, 1 << 3, m5ioe1_i2c_freq); // PIN4 push-pull + lgfx::i2c::bitOn (probe_i2c_port, m5ioe1_i2c_addr, 0x03, 1 << 3, m5ioe1_i2c_freq); // PIN4 output + lgfx::i2c::bitOn (probe_i2c_port, m5ioe1_i2c_addr, 0x05, 1 << 3, m5ioe1_i2c_freq); // PIN4 HIGH + lgfx::delay(20); // wait for the rail to rise and the TM1680 to complete POR + + // The TM1680 has no ID register; check for an address ACK only + bool hit = lgfx::i2c::beginTransaction(probe_i2c_port, tm1680_i2c_addr, 100000, false).has_value() + && lgfx::i2c::endTransaction(probe_i2c_port).has_value(); + if (hit) + { + board = board_t::board_M5CoreMatrix; + ESP_LOGI(LIBRARY_NAME, "[Autodetect] board_M5CoreMatrix"); + + // Board confirmed: hand the bus over to the hardware I2C port + lgfx::i2c::release(probe_i2c_port); + + auto bus_i2c = new Bus_I2C(); + { + auto cfg = bus_i2c->config(); + cfg.i2c_port = i2c_port; + cfg.freq_write = i2c_freq; + cfg.freq_read = i2c_freq; + cfg.pin_sda = corematrix_i2c_sda; + cfg.pin_scl = corematrix_i2c_scl; + cfg.i2c_addr = tm1680_i2c_addr; + cfg.prefix_len = 0; // the TM1680 protocol has no command/data prefix byte + bus_i2c->config(cfg); + } + _bus_last.reset(bus_i2c); + + auto p = new lgfx::Panel_TM1680(); + { + auto cfg = p->config(); + cfg.bus_shared = false; + // rotation 1 (the default) shows upright with the buttons at the top + cfg.offset_rotation = 3; + p->config(cfg); + p->setRotation(1); + } + p->bus(bus_i2c); + _panel_last.reset(p); + + goto init_clear; + } + // Not this board: restore the LED matrix power rail + lgfx::i2c::bitOff(probe_i2c_port, m5ioe1_i2c_addr, 0x05, 1 << 3, m5ioe1_i2c_freq); + } + // Reached only when no board was detected; only the software port was touched + lgfx::i2c::release(probe_i2c_port); + for (auto &bup : backup_pins) { bup.restore(); } + } + #elif defined (CONFIG_IDF_TARGET_ESP32H2) #endif @@ -3514,6 +3601,7 @@ The usage of each pin is as follows. case board_M5Tough: title = "M5Tough"; break; case board_M5ToughC5: title = "M5ToughC5"; break; case board_M5StampC5: title = "M5StampC5"; break; + case board_M5CoreMatrix: title = "M5CoreMatrix"; break; case board_M5Station: title = "M5Station"; break; case board_M5StopWatch: title = "M5StopWatch"; break; case board_M5ChainCaptain: title = "M5ChainCaptain"; break; From 9a97c8f087b11032a3f88c7abd5da9c008c4fa60 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 10 Aug 2026 06:05:33 +0000 Subject: [PATCH 28/56] Probe the ToughC5 with minimal writes and restore them when not confirmed The ToughC5 probe configured the PM1 power rails, watchdog, IO expander pins and both reset lines before the panel ID had confirmed the board, and a failed confirmation left all of that behind on whatever device had answered the address checks. Restrict the pre-confirmation writes to what the panel ID read needs: the LDO rail that powers the panel, and LCD_EN / LCD_RST driven high. The previous register values are recorded first and written back when the panel does not answer, so an unconfirmed candidate is left as it was found. Everything else (watchdog, I2C sleep, charge and LED bits, backlight pin, TP_RST, and the reset pulse itself) moves after the board is confirmed. The reset lines are also preloaded high in the output latch before the pins are switched to push-pull output. LCD_RST has no pull-up on the board, so with use_reset=false the pin used to drive whatever the power-on latch value happened to be, which could hold the panel or the touch controller in reset after a cold start. --- src/M5GFX.cpp | 99 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 62 insertions(+), 37 deletions(-) diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index 861f6c69..57ae9181 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -3348,46 +3348,26 @@ The usage of each pin is as follows. lgfx::i2c::init(probe_i2c_port, toughc5_i2c_sda, toughc5_i2c_scl); if (_check_m5pm1(probe_i2c_port) && _check_m5ioe1(probe_i2c_port)) { - // M5PM1 初期化 - lgfx::i2c::writeRegister8(probe_i2c_port, m5pm1_i2c_addr, 0x09, 0x00, 0, m5pm1_i2c_freq); // I2C sleep disable - lgfx::i2c::writeRegister8(probe_i2c_port, m5pm1_i2c_addr, 0x0A, 0x00, 0, m5pm1_i2c_freq); // WDT disable - lgfx::i2c::bitOn( probe_i2c_port, m5pm1_i2c_addr, 0x06, 0x17, m5pm1_i2c_freq); // PWR_CFG: LED_CTRL, LDO, DCDC, CHG enable - - // M5IOE1 初期化 - lgfx::i2c::writeRegister8(probe_i2c_port, m5ioe1_i2c_addr, 0x23, 0x00, 0, m5ioe1_i2c_freq); // I2C sleep disable - - // M5IOE1 PIN4(LCD_RST), PIN5(LCD_EN), PIN10(LCD_BL) を出力に設定 + // ボード確定 (パネル ID 確認) 前の書き込みは、ID 読みに必要な最小限 + // (LCD 電源とリセット解除) に留め、不成立時に復元できるよう元値を控える + auto pm1_06 = lgfx::i2c::readRegister8(probe_i2c_port, m5pm1_i2c_addr, 0x06, m5pm1_i2c_freq); + auto ioe1_03 = lgfx::i2c::readRegister8(probe_i2c_port, m5ioe1_i2c_addr, 0x03, m5ioe1_i2c_freq); + auto ioe1_05 = lgfx::i2c::readRegister8(probe_i2c_port, m5ioe1_i2c_addr, 0x05, m5ioe1_i2c_freq); + auto ioe1_13 = lgfx::i2c::readRegister8(probe_i2c_port, m5ioe1_i2c_addr, 0x13, m5ioe1_i2c_freq); + if (pm1_06.has_value() && ioe1_03.has_value() && ioe1_05.has_value() && ioe1_13.has_value()) { + + // M5IOE1 PIN4(LCD_RST), PIN5(LCD_EN) を High で出力に設定 // PIN4=bit3, PIN5=bit4 → low register (0x03/0x05/0x13) - // PIN10=bit1(high) → high register (0x04/0x06/0x14) - static constexpr uint8_t IOE1_PIN_10 = 9; - static constexpr uint8_t IOE1_BIT_10_H = (1u << (IOE1_PIN_10 - 8)); - + // リセット線 (PIN4, 基板プルアップ無し) を Low のまま駆動しないよう、 + // 出力ラッチへ High を先に書いてから push-pull / 出力化する + lgfx::i2c::bitOn( probe_i2c_port, m5ioe1_i2c_addr, 0x05, 0b00011000, m5ioe1_i2c_freq); // PIN4,5 latch HIGH lgfx::i2c::bitOff(probe_i2c_port, m5ioe1_i2c_addr, 0x13, 0b00011000, m5ioe1_i2c_freq); // PIN4,5 push-pull - lgfx::i2c::bitOff(probe_i2c_port, m5ioe1_i2c_addr, 0x14, IOE1_BIT_10_H, m5ioe1_i2c_freq); // PIN10 push-pull lgfx::i2c::bitOn( probe_i2c_port, m5ioe1_i2c_addr, 0x03, 0b00011000, m5ioe1_i2c_freq); // PIN4,5 output - lgfx::i2c::bitOn( probe_i2c_port, m5ioe1_i2c_addr, 0x04, IOE1_BIT_10_H, m5ioe1_i2c_freq); // PIN10 output - - // LCD enable - lgfx::i2c::bitOn( probe_i2c_port, m5ioe1_i2c_addr, 0x05, 1 << 4, m5ioe1_i2c_freq); // PIN5(LCD_EN) HIGH - - // LCD reset - if (use_reset) { - lgfx::i2c::bitOff(probe_i2c_port, m5ioe1_i2c_addr, 0x05, 1 << 3, m5ioe1_i2c_freq); // PIN4(LCD_RST) LOW - lgfx::delay(2); - lgfx::i2c::bitOn( probe_i2c_port, m5ioe1_i2c_addr, 0x05, 1 << 3, m5ioe1_i2c_freq); // PIN4(LCD_RST) HIGH - lgfx::delay(10); - } - // M5PM1 GPIO2(TP_RST) を出力に設定してリセット - lgfx::i2c::bitOff(probe_i2c_port, m5pm1_i2c_addr, 0x16, 0b11 << (2*2), m5pm1_i2c_freq); // GPIO2 → GPIO function - lgfx::i2c::bitOn( probe_i2c_port, m5pm1_i2c_addr, 0x10, 1 << 2, m5pm1_i2c_freq); // GPIO2 output - lgfx::i2c::bitOff(probe_i2c_port, m5pm1_i2c_addr, 0x13, 1 << 2, m5pm1_i2c_freq); // GPIO2 push-pull - if (use_reset) { - lgfx::i2c::bitOff(probe_i2c_port, m5pm1_i2c_addr, 0x11, 1 << 2, m5pm1_i2c_freq); // GPIO2(TP_RST) LOW - lgfx::delay(2); - lgfx::i2c::bitOn( probe_i2c_port, m5pm1_i2c_addr, 0x11, 1 << 2, m5pm1_i2c_freq); // GPIO2(TP_RST) HIGH - lgfx::delay(10); - } + // LCD のロジック電源 (PM1 LDO → PYB_LCD_EN 経由)。PM1 リセット後は + // PWR_CFG がクリアされているため、コールドブートではここで入れる + lgfx::i2c::bitOn( probe_i2c_port, m5pm1_i2c_addr, 0x06, 0x04, m5pm1_i2c_freq); // PWR_CFG: LDO enable + lgfx::delay(10); // SPI バスを設定して LCD パネル ID を確認 bus_cfg.pin_mosi = GPIO_NUM_7; @@ -3405,6 +3385,43 @@ The usage of each pin is as follows. board = board_t::board_M5ToughC5; ESP_LOGI(LIBRARY_NAME, "[Autodetect] board_M5ToughC5"); + // --- ボードが確定したので、確認前に行えなかった初期化をまとめて行う + // M5PM1 初期化 + lgfx::i2c::writeRegister8(probe_i2c_port, m5pm1_i2c_addr, 0x09, 0x00, 0, m5pm1_i2c_freq); // I2C sleep disable + lgfx::i2c::writeRegister8(probe_i2c_port, m5pm1_i2c_addr, 0x0A, 0x00, 0, m5pm1_i2c_freq); // WDT disable + lgfx::i2c::bitOn( probe_i2c_port, m5pm1_i2c_addr, 0x06, 0x17, m5pm1_i2c_freq); // PWR_CFG: LED_CTRL, LDO, DCDC, CHG enable + + // M5IOE1 初期化 + lgfx::i2c::writeRegister8(probe_i2c_port, m5ioe1_i2c_addr, 0x23, 0x00, 0, m5ioe1_i2c_freq); // I2C sleep disable + + // M5IOE1 PIN10(LCD_BL) を出力に設定 + // PIN10=bit1(high) → high register (0x04/0x06/0x14) + static constexpr uint8_t IOE1_PIN_10 = 9; + static constexpr uint8_t IOE1_BIT_10_H = (1u << (IOE1_PIN_10 - 8)); + lgfx::i2c::bitOff(probe_i2c_port, m5ioe1_i2c_addr, 0x14, IOE1_BIT_10_H, m5ioe1_i2c_freq); // PIN10 push-pull + lgfx::i2c::bitOn( probe_i2c_port, m5ioe1_i2c_addr, 0x04, IOE1_BIT_10_H, m5ioe1_i2c_freq); // PIN10 output + + // LCD reset + if (use_reset) { + lgfx::i2c::bitOff(probe_i2c_port, m5ioe1_i2c_addr, 0x05, 1 << 3, m5ioe1_i2c_freq); // PIN4(LCD_RST) LOW + lgfx::delay(2); + lgfx::i2c::bitOn( probe_i2c_port, m5ioe1_i2c_addr, 0x05, 1 << 3, m5ioe1_i2c_freq); // PIN4(LCD_RST) HIGH + lgfx::delay(10); + } + + // M5PM1 GPIO2(TP_RST) を出力に設定してリセット + // ラッチへ High (リセット解除) を先に書いてから出力化する + lgfx::i2c::bitOn( probe_i2c_port, m5pm1_i2c_addr, 0x11, 1 << 2, m5pm1_i2c_freq); // GPIO2 latch HIGH + lgfx::i2c::bitOff(probe_i2c_port, m5pm1_i2c_addr, 0x16, 0b11 << (2*2), m5pm1_i2c_freq); // GPIO2 → GPIO function + lgfx::i2c::bitOn( probe_i2c_port, m5pm1_i2c_addr, 0x10, 1 << 2, m5pm1_i2c_freq); // GPIO2 output + lgfx::i2c::bitOff(probe_i2c_port, m5pm1_i2c_addr, 0x13, 1 << 2, m5pm1_i2c_freq); // GPIO2 push-pull + if (use_reset) { + lgfx::i2c::bitOff(probe_i2c_port, m5pm1_i2c_addr, 0x11, 1 << 2, m5pm1_i2c_freq); // GPIO2(TP_RST) LOW + lgfx::delay(2); + lgfx::i2c::bitOn( probe_i2c_port, m5pm1_i2c_addr, 0x11, 1 << 2, m5pm1_i2c_freq); // GPIO2(TP_RST) HIGH + lgfx::delay(10); + } + // ボードが確定したので、常用するハードウェアポートへバスを引き継ぐ // (バックライトとタッチがこのポートを使う) lgfx::i2c::release(probe_i2c_port); @@ -3455,8 +3472,16 @@ The usage of each pin is as follows. goto init_clear; } bus_spi->release(); + + // 不成立: 確定前に書いた最小限のレジスタを元値へ復元する + lgfx::i2c::writeRegister8(probe_i2c_port, m5ioe1_i2c_addr, 0x03, ioe1_03.value(), 0, m5ioe1_i2c_freq); + lgfx::i2c::writeRegister8(probe_i2c_port, m5ioe1_i2c_addr, 0x13, ioe1_13.value(), 0, m5ioe1_i2c_freq); + lgfx::i2c::writeRegister8(probe_i2c_port, m5ioe1_i2c_addr, 0x05, ioe1_05.value(), 0, m5ioe1_i2c_freq); + lgfx::i2c::writeRegister8(probe_i2c_port, m5pm1_i2c_addr, 0x06, pm1_06.value(), 0, m5pm1_i2c_freq); + } } - // ここへ来るのはボード不成立の場合のみ。ソフトウェアポートしか触れていない + // ここへ来るのはボード不成立の場合のみ。デバイスへ書いた分は復元済みで、 + // ソフトウェア I2C と ESP 側のピン状態を返す lgfx::i2c::release(probe_i2c_port); for (auto &bup : backup_pins) { bup.restore(); } } From 949489354104c3451f699cfc2760c51999b796e0 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 10 Aug 2026 06:05:43 +0000 Subject: [PATCH 29/56] Retry the ToughC5 panel ID read once A transient failure of the single ID read left the board undetected for the rest of the run, with the C5 default fallback applied to hardware it does not match. --- src/M5GFX.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index 57ae9181..889cdca2 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -3380,6 +3380,11 @@ The usage of each pin is as follows. bus_spi->init(); std::uint32_t id = _read_panel_id(bus_spi, GPIO_NUM_25); + if ((id & 0xFF) != 0xE3) + { // 一時的な読み損ないでボード不成立に落ちないよう一度だけ再試行する + lgfx::delay(2); + id = _read_panel_id(bus_spi, GPIO_NUM_25); + } if ((id & 0xFF) == 0xE3) { // ILI9342c board = board_t::board_M5ToughC5; From 5f5bb1f1a31b595085089de194f83dc5f4d33c8b Mon Sep 17 00:00:00 2001 From: Tinyu Date: Mon, 10 Aug 2026 14:25:18 +0800 Subject: [PATCH 30/56] bring SDL window to foreground on macOS. --- examples/PlatformIO_SDL/src/sdl_main.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/PlatformIO_SDL/src/sdl_main.cpp b/examples/PlatformIO_SDL/src/sdl_main.cpp index 87abf61b..7137e416 100644 --- a/examples/PlatformIO_SDL/src/sdl_main.cpp +++ b/examples/PlatformIO_SDL/src/sdl_main.cpp @@ -17,6 +17,9 @@ int user_func(bool* running) int main(int, char**) { +#if defined(__APPLE__) + SDL_SetHint(SDL_HINT_MAC_BACKGROUND_APP, "0"); +#endif // The second argument is effective for step execution with breakpoints. // You can specify the time in milliseconds to perform slow execution that ensures screen updates. return lgfx::Panel_sdl::main(user_func, 128); From 1892d315db3e41ead668eadfe46cb6a975b7737e Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 10 Aug 2026 08:11:52 +0000 Subject: [PATCH 31/56] Bring the SDL simulator window to the foreground on macOS Since SDL 2.30.4, non-bundled binaries are no longer brought to the foreground on macOS 14 or later. Setting SDL_HINT_MAC_BACKGROUND_APP to "0" before SDL_Init restores the previous behaviour, so every application using Panel_sdl::main benefits without modifying its own entry point. The hint is set with normal priority, so users can still override it via the SDL_MAC_BACKGROUND_APP environment variable. --- src/lgfx/v1/platforms/sdl/Panel_sdl.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lgfx/v1/platforms/sdl/Panel_sdl.cpp b/src/lgfx/v1/platforms/sdl/Panel_sdl.cpp index 13b5cc58..2052d98b 100644 --- a/src/lgfx/v1/platforms/sdl/Panel_sdl.cpp +++ b/src/lgfx/v1/platforms/sdl/Panel_sdl.cpp @@ -257,6 +257,11 @@ namespace lgfx _update_in_semaphore = SDL_CreateSemaphore(0); _update_out_semaphore = SDL_CreateSemaphore(0); for (size_t pin = 0; pin < EMULATED_GPIO_MAX; ++pin) { gpio_hi(pin); } +#if defined ( __APPLE__ ) + /// Since SDL 2.30.4, non-bundled apps are no longer brought to the foreground on macOS 14 or later. + /// Set the hint before SDL_Init so that the window appears in front. (can be overridden by the environment variable) + SDL_SetHint(SDL_HINT_MAC_BACKGROUND_APP, "0"); +#endif /*Initialize the SDL*/ SDL_Init(SDL_INIT_VIDEO); SDL_StartTextInput(); From d0f9c8213857c47812b064f3a7ca18ac4a0f0f06 Mon Sep 17 00:00:00 2001 From: thecybershotguy Date: Wed, 12 Aug 2026 13:35:39 -0600 Subject: [PATCH 32/56] Fix unbounded BUSY-pin spin in Panel_IT8951::_write_args() _write_args() polled the IT8951's BUSY GPIO between argument words with a raw while(!gpio_in(pin)) loop, unlike every other BUSY wait in this file. If BUSY gets stuck asserted (seen after a panel power brownout, e.g. switching an M5Paper's e-paper supply between USB and battery), this spins forever with no recovery short of a power cycle, hanging the whole calling stack. _write_args() can't just call the existing _wait_busy() helper because it toggles chip-select, while _write_args() must hold CS asserted continuously across a multi-word burst. Extract the bounded, timeout-based poll out of _wait_busy() into a new CS-free helper, _wait_busy_pin(timeout = 4096), matching the timeout already used elsewhere in this file. _wait_busy() now wraps it with CS handling exactly as before; _write_args() calls it directly per word and bails out instead of spinning forever. Co-Authored-By: Claude Sonnet 5 --- src/lgfx/v1/panel/Panel_IT8951.cpp | 29 +++++++++++++++++++++++++---- src/lgfx/v1/panel/Panel_IT8951.hpp | 1 + 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/lgfx/v1/panel/Panel_IT8951.cpp b/src/lgfx/v1/panel/Panel_IT8951.cpp index 223b4a0f..d3beaef4 100644 --- a/src/lgfx/v1/panel/Panel_IT8951.cpp +++ b/src/lgfx/v1/panel/Panel_IT8951.cpp @@ -241,10 +241,14 @@ IT8951 Registers defines _bus->endTransaction(); } - bool Panel_IT8951::_wait_busy(uint32_t timeout) + // Bounded poll of the BUSY pin, without touching chip-select. + // Returns false after `timeout` ms if BUSY never deasserts (e.g. the + // panel's boost converter browned out and left BUSY stuck), instead of + // spinning forever. Shared by _wait_busy() below and by _write_args(), + // which must keep CS asserted continuously across a multi-word burst + // and therefore can't use _wait_busy() directly. + bool Panel_IT8951::_wait_busy_pin(uint32_t timeout) { - _bus->wait(); - cs_control(true); if (_cfg.pin_busy >= 0 && !lgfx::gpio_in(_cfg.pin_busy)) { auto start_ms = millis(); @@ -264,6 +268,17 @@ IT8951 Registers defines } } while (!lgfx::gpio_in(_cfg.pin_busy)); } + return true; + } + + bool Panel_IT8951::_wait_busy(uint32_t timeout) + { + _bus->wait(); + cs_control(true); + if (!_wait_busy_pin(timeout)) + { + return false; + } cs_control(false); return true; } @@ -339,7 +354,13 @@ IT8951 Registers defines { uint32_t buf = getSwap16(args[i]); _bus->wait(); - while (!lgfx::gpio_in(_cfg.pin_busy)); + // CS must stay asserted for the whole multi-word burst, so this + // can't go through _wait_busy() (it toggles CS). A stuck BUSY + // line must still fail out here rather than hang forever. + if (!_wait_busy_pin()) + { + return false; + } _bus->writeData(buf, 16); } while ( ++i < length ); return true; diff --git a/src/lgfx/v1/panel/Panel_IT8951.hpp b/src/lgfx/v1/panel/Panel_IT8951.hpp index 0a44a700..89033e56 100644 --- a/src/lgfx/v1/panel/Panel_IT8951.hpp +++ b/src/lgfx/v1/panel/Panel_IT8951.hpp @@ -88,6 +88,7 @@ namespace lgfx uint16_t _vcom = 0; bool _wait_busy( uint32_t timeout = 4096); + bool _wait_busy_pin( uint32_t timeout = 4096); bool _write_command( uint16_t cmd); bool _write_word( uint16_t data); bool _write_args( uint16_t cmd, uint16_t *args, int32_t length); From 99a47952a989f462d67b38f0dc5ab266c0ccdbc3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Aug 2026 01:10:23 +0000 Subject: [PATCH 33/56] Initial plan From 7502f29622dff7e19dd5c24cddbf1d3b58786ff0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Aug 2026 01:11:42 +0000 Subject: [PATCH 34/56] Deassert CS before returning false on busy timeout in _write_args Co-authored-by: lovyan03 <42724151+lovyan03@users.noreply.github.com> --- src/lgfx/v1/panel/Panel_IT8951.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lgfx/v1/panel/Panel_IT8951.cpp b/src/lgfx/v1/panel/Panel_IT8951.cpp index d3beaef4..325ffd4c 100644 --- a/src/lgfx/v1/panel/Panel_IT8951.cpp +++ b/src/lgfx/v1/panel/Panel_IT8951.cpp @@ -359,6 +359,7 @@ IT8951 Registers defines // line must still fail out here rather than hang forever. if (!_wait_busy_pin()) { + cs_control(true); return false; } _bus->writeData(buf, 16); From 915d99842dd0ff1ba1e59f77b1595a3553542e7c Mon Sep 17 00:00:00 2001 From: Peter Edwards Date: Wed, 12 Aug 2026 07:14:43 +0200 Subject: [PATCH 35/56] fix(BFFfont): correct first-pixel guard in decode_rle_bitmap --- src/lgfx/v1/lgfx_fonts.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lgfx/v1/lgfx_fonts.cpp b/src/lgfx/v1/lgfx_fonts.cpp index 8709cdf2..6965fc9c 100644 --- a/src/lgfx/v1/lgfx_fonts.cpp +++ b/src/lgfx/v1/lgfx_fonts.cpp @@ -941,7 +941,7 @@ namespace lgfx if (bs->bit_pos + bpp > bs->bit_length) break; ret = (uint8_t)bs->read_bits(bpp); - if (bs->bit_pos != bpp && prev_v == ret) + if (out != 0 && prev_v == ret) { count = 0; state = RLE_REPEATED; From 76c5f374ea4a02de14859244bbf542e686a59e28 Mon Sep 17 00:00:00 2001 From: John Laur Date: Wed, 5 Aug 2026 01:46:34 -0500 Subject: [PATCH 36/56] Add mirrored rotation support (modes 4..7) to Panel_AMOLED --- src/lgfx/v1/panel/Panel_AMOLED.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lgfx/v1/panel/Panel_AMOLED.cpp b/src/lgfx/v1/panel/Panel_AMOLED.cpp index 4533b038..195327cc 100644 --- a/src/lgfx/v1/panel/Panel_AMOLED.cpp +++ b/src/lgfx/v1/panel/Panel_AMOLED.cpp @@ -45,6 +45,10 @@ namespace lgfx case 1: madctl = 0x60; break; case 2: madctl = 0xc0; break; case 3: madctl = 0xa0; break; + case 4: madctl = 0xc2; break; + case 5: madctl = 0x62; break; + case 6: madctl = 0x02; break; + case 7: madctl = 0xa2; break; } startWrite(); From 1884372064f488a2a1f568555fa400d28e963205 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Thu, 13 Aug 2026 05:00:03 +0000 Subject: [PATCH 37/56] Light_PWM: consolidate LEDC conditional blocks and correct the IDF v6 intr_type guard ledc_channel_config_t.intr_type carries a deprecated attribute from ESP-IDF v6 (interrupts are handled inside the driver), so it is now assigned only when building against an earlier IDF. The Arduino and IDF code paths are consolidated behind LGFX_LEDCINIT / LGFX_LEDC_SPEED_MODE macros selected once at the top of the file. --- src/lgfx/v1/platforms/esp32/Light_PWM.cpp | 85 +++++++++++------------ 1 file changed, 42 insertions(+), 43 deletions(-) diff --git a/src/lgfx/v1/platforms/esp32/Light_PWM.cpp b/src/lgfx/v1/platforms/esp32/Light_PWM.cpp index f70bf148..e36892e6 100644 --- a/src/lgfx/v1/platforms/esp32/Light_PWM.cpp +++ b/src/lgfx/v1/platforms/esp32/Light_PWM.cpp @@ -30,6 +30,29 @@ Original Source: #include #endif +#if defined ( ARDUINO ) + + #if defined ESP_ARDUINO_VERSION // use ledc* functions shipped with arduino-esp32 core + #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) + #define LGFX_LEDCINIT(pin_bl, freq, bits, pwm_channel) ledcAttach(pin_bl, freq, bits) + #define LGFX_LEDCWRITE(pin_bl, pwm_channel, duty) ledcWrite(pin_bl, duty) + #elif ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(2, 0, 0) + #define LGFX_LEDCINIT(pin_bl, freq, bits, pwm_channel) { ledcSetup(pwm_channel, freq, bits); ledcAttachPin(pin_bl, pwm_channel); } + #define LGFX_LEDCWRITE(pin_bl, pwm_channel, duty) ledcWrite(pwm_channel, duty) + #endif + #endif + +#else // esp-idf + + #if SOC_LEDC_SUPPORT_HS_MODE + #define LGFX_LEDC_SPEED_MODE LEDC_HIGH_SPEED_MODE + #else + #define LGFX_LEDC_SPEED_MODE LEDC_LOW_SPEED_MODE + #endif + +#endif + + namespace lgfx { inline namespace v1 @@ -44,59 +67,38 @@ namespace lgfx #if defined ( ARDUINO ) -#if defined ESP_ARDUINO_VERSION - #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) - #define LEDC_USE_IDF_V5 // esp32-arduino core 3.x.x uses the new ledC syntax - #endif - #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(6, 0, 0) - #define LEDC_USE_IDF_V6 // esp32-arduino core 6.x.x - #endif -#endif + LGFX_LEDCINIT(_cfg.pin_bl, _cfg.freq, PWM_BITS, _cfg.pwm_channel); -#if defined LEDC_USE_IDF_V5 - ledcAttach(_cfg.pin_bl, _cfg.freq, PWM_BITS); - setBrightness(brightness); -#else - ledcSetup(_cfg.pwm_channel, _cfg.freq, PWM_BITS); - ledcAttachPin(_cfg.pin_bl, _cfg.pwm_channel); - setBrightness(brightness); -#endif -#else +#else // esp-idf static ledc_channel_config_t ledc_channel; { ledc_channel.gpio_num = (gpio_num_t)_cfg.pin_bl; -#if SOC_LEDC_SUPPORT_HS_MODE - ledc_channel.speed_mode = LEDC_HIGH_SPEED_MODE; -#else - ledc_channel.speed_mode = LEDC_LOW_SPEED_MODE; -#endif + ledc_channel.speed_mode = LGFX_LEDC_SPEED_MODE; ledc_channel.channel = (ledc_channel_t)_cfg.pwm_channel; -#if !defined LEDC_USE_IDF_V6 // ledc_channel_config_t.intr_type is deprecated, no need to explicitly configure interrupt, handled in the driver - ledc_channel.intr_type = LEDC_INTR_DISABLE; -#endif ledc_channel.timer_sel = (ledc_timer_t)((_cfg.pwm_channel >> 1) & 3); ledc_channel.duty = _cfg.invert ? (1 << PWM_BITS) : 0; ledc_channel.hpoint = 0; + #if defined ESP_IDF_VERSION_VAL + // when ledc_channel_config_t.intr_type is deprecated, no need to explicitly configure interrupt, handled in the driver + #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0) + ledc_channel.intr_type = LEDC_INTR_DISABLE; + #endif + #endif }; ledc_channel_config(&ledc_channel); static ledc_timer_config_t ledc_timer; { -#if SOC_LEDC_SUPPORT_HS_MODE - ledc_timer.speed_mode = LEDC_HIGH_SPEED_MODE; // timer mode -#else - ledc_timer.speed_mode = LEDC_LOW_SPEED_MODE; -#endif + ledc_timer.speed_mode = LGFX_LEDC_SPEED_MODE; // timer mode ledc_timer.duty_resolution = (ledc_timer_bit_t)PWM_BITS; // resolution of PWM duty 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); - setBrightness(brightness); - #endif + setBrightness(brightness); return true; } @@ -115,17 +117,14 @@ namespace lgfx if (_cfg.invert) duty = (1 << PWM_BITS) - duty; #if defined ( ARDUINO ) -#if defined LEDC_USE_IDF_V5 - ledcWrite(_cfg.pin_bl, duty); -#else - ledcWrite(_cfg.pwm_channel, duty); -#endif -#elif SOC_LEDC_SUPPORT_HS_MODE - ledc_set_duty(LEDC_HIGH_SPEED_MODE, (ledc_channel_t)_cfg.pwm_channel, duty); - ledc_update_duty(LEDC_HIGH_SPEED_MODE, (ledc_channel_t)_cfg.pwm_channel); -#else - ledc_set_duty(LEDC_LOW_SPEED_MODE, (ledc_channel_t)_cfg.pwm_channel, duty); - ledc_update_duty(LEDC_LOW_SPEED_MODE, (ledc_channel_t)_cfg.pwm_channel); + + LGFX_LEDCWRITE(_cfg.pin_bl, _cfg.pwm_channel, duty); + +#else // esp-idf + + ledc_set_duty(LGFX_LEDC_SPEED_MODE, (ledc_channel_t)_cfg.pwm_channel, duty); + ledc_update_duty(LGFX_LEDC_SPEED_MODE, (ledc_channel_t)_cfg.pwm_channel); + #endif } From adf9a591b9fa7b6a1bce0166582206715833ba27 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Thu, 13 Aug 2026 05:00:03 +0000 Subject: [PATCH 38/56] Guard optional soc headers with __has_include --- src/lgfx/v1/platforms/esp32/common.hpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lgfx/v1/platforms/esp32/common.hpp b/src/lgfx/v1/platforms/esp32/common.hpp index 9f780d69..fa8b4c80 100644 --- a/src/lgfx/v1/platforms/esp32/common.hpp +++ b/src/lgfx/v1/platforms/esp32/common.hpp @@ -30,9 +30,13 @@ Original Source: #include #include #if __has_include() -#include -#include -#include + #include +#endif +#if __has_include() + #include +#endif +#if __has_include() + #include #endif #include #include From 83d0b755cd1042c3150f93a5380fde8adac504c6 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Thu, 13 Aug 2026 05:27:34 +0000 Subject: [PATCH 39/56] Light_PWM: fall back to the legacy LEDC API when the core version macro is absent ESP_ARDUINO_VERSION was introduced with arduino-esp32 2.0.0, so on the 1.x cores neither branch defined LGFX_LEDCINIT / LGFX_LEDCWRITE and the calls below failed to compile. Define the new API variant only when the version is known to be 3.0.0 or later, and fall back to ledcSetup/ledcAttachPin/ledcWrite(channel) everywhere else, which is the only API those old cores provide. --- src/lgfx/v1/platforms/esp32/Light_PWM.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lgfx/v1/platforms/esp32/Light_PWM.cpp b/src/lgfx/v1/platforms/esp32/Light_PWM.cpp index e36892e6..676b90e6 100644 --- a/src/lgfx/v1/platforms/esp32/Light_PWM.cpp +++ b/src/lgfx/v1/platforms/esp32/Light_PWM.cpp @@ -36,11 +36,12 @@ Original Source: #if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0) #define LGFX_LEDCINIT(pin_bl, freq, bits, pwm_channel) ledcAttach(pin_bl, freq, bits) #define LGFX_LEDCWRITE(pin_bl, pwm_channel, duty) ledcWrite(pin_bl, duty) - #elif ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(2, 0, 0) - #define LGFX_LEDCINIT(pin_bl, freq, bits, pwm_channel) { ledcSetup(pwm_channel, freq, bits); ledcAttachPin(pin_bl, pwm_channel); } - #define LGFX_LEDCWRITE(pin_bl, pwm_channel, duty) ledcWrite(pwm_channel, duty) #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_LEDCWRITE(pin_bl, pwm_channel, duty) ledcWrite(pwm_channel, duty) + #endif #else // esp-idf From e84795cdffffbd71f1992c60faea72e045bb4850 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Thu, 13 Aug 2026 07:30:20 +0000 Subject: [PATCH 40/56] Use SPI2_HOST/SPI3_HOST instead of the removed HSPI/VSPI aliases ESP-IDF v6 drops the ESP32-classic HSPI_HOST/VSPI_HOST aliases, which broke the autodetect path and the Atom/Module display headers when building against it. SPI2_HOST/SPI3_HOST are the canonical names since IDF v4, so this stays compatible with every Arduino core and IDF version the CI floor covers. --- src/M5AtomDisplay.h | 2 +- src/M5GFX.cpp | 10 +++++----- src/M5ModuleDisplay.h | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/M5AtomDisplay.h b/src/M5AtomDisplay.h index 62f003a1..7c311040 100644 --- a/src/M5AtomDisplay.h +++ b/src/M5AtomDisplay.h @@ -138,7 +138,7 @@ class M5AtomDisplay : public M5GFX int spi_sclk = GPIO_NUM_7; #elif !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) - #define M5GFX_SPI_HOST VSPI_HOST + #define M5GFX_SPI_HOST SPI3_HOST int i2c_port = 1; int i2c_sda = GPIO_NUM_25; diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index 889cdca2..4c19d71e 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -1153,7 +1153,7 @@ namespace m5gfx #if !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) - bus_cfg.spi_host = VSPI_HOST; + bus_cfg.spi_host = SPI3_HOST; bus_cfg.dma_channel = 1; std::uint32_t id; @@ -1180,7 +1180,7 @@ namespace m5gfx board = board_t::board_M5StickCPlus; ESP_LOGI(LIBRARY_NAME, "[Autodetect] M5StickCPlus"); bus_spi->release(); - bus_cfg.spi_host = HSPI_HOST; + bus_cfg.spi_host = SPI2_HOST; bus_cfg.freq_write = 40000000; bus_cfg.freq_read = 15000000; bus_spi->config(bus_cfg); @@ -1195,7 +1195,7 @@ namespace m5gfx board = board_t::board_M5StickC; ESP_LOGI(LIBRARY_NAME, "[Autodetect] M5StickC"); bus_spi->release(); - bus_cfg.spi_host = HSPI_HOST; + bus_cfg.spi_host = SPI2_HOST; bus_cfg.freq_write = 27000000; bus_cfg.freq_read = 14000000; bus_spi->config(bus_cfg); @@ -1280,7 +1280,7 @@ namespace m5gfx board = board_t::board_M5StickCPlus2; ESP_LOGI(LIBRARY_NAME, "[Autodetect] M5StickCPlus2"); bus_spi->release(); - bus_cfg.spi_host = HSPI_HOST; + bus_cfg.spi_host = SPI2_HOST; bus_cfg.freq_write = 40000000; bus_cfg.freq_read = 15000000; bus_spi->config(bus_cfg); @@ -1350,7 +1350,7 @@ namespace m5gfx board = board_t::board_M5Station; bus_spi->release(); - bus_cfg.spi_host = HSPI_HOST; + bus_cfg.spi_host = SPI2_HOST; bus_cfg.freq_write = 40000000; bus_cfg.freq_read = 15000000; bus_spi->config(bus_cfg); diff --git a/src/M5ModuleDisplay.h b/src/M5ModuleDisplay.h index f26e296a..c6969f08 100644 --- a/src/M5ModuleDisplay.h +++ b/src/M5ModuleDisplay.h @@ -149,7 +149,7 @@ class M5ModuleDisplay : public M5GFX int spi_sclk = GPIO_NUM_36; #elif !defined (CONFIG_IDF_TARGET) || defined (CONFIG_IDF_TARGET_ESP32) - #define M5GFX_SPI_HOST VSPI_HOST + #define M5GFX_SPI_HOST SPI3_HOST int i2c_port = 1; int i2c_sda = GPIO_NUM_21; From 506d2711e254699a20fd6d1543dd7098db7ff96f Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Thu, 13 Aug 2026 07:30:48 +0000 Subject: [PATCH 41/56] CI: add esp32c5 coverage, IDF 6.0, and run superseding - The IDF matrix gains esp32c5 (5.5.2), esp32c6 on 5.5.2 so the low power peripheral paths compile, and esp32 on 6.0.2 to surface next major deprecations early. - The Arduino matrix gains esp32c5 on arduino-esp32 3.3.11 (C5 support landed in the 3.3 series). - Both workflows now cancel superseded runs of the same branch or pull request, keyed by event name and PR number. --- .github/workflows/ArduinoBuild.yml | 9 +++++++++ .github/workflows/IDFBuild.yml | 12 ++++++++++++ 2 files changed, 21 insertions(+) diff --git a/.github/workflows/ArduinoBuild.yml b/.github/workflows/ArduinoBuild.yml index 6eb8f73f..81c6d0e6 100644 --- a/.github/workflows/ArduinoBuild.yml +++ b/.github/workflows/ArduinoBuild.yml @@ -12,6 +12,11 @@ on: pull_request: workflow_dispatch: +# supersede queued/running builds of the same branch or PR +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: build: name: ${{ matrix.board }} (${{ matrix.esp32_version }}) @@ -44,6 +49,10 @@ jobs: - board: esp32:esp32:esp32c3 esp32_version: 3.1.1 board_manager_url: https://espressif.github.io/arduino-esp32/package_esp32_index.json + # ESP32-C5 (v3.3+ only) + - board: esp32:esp32:esp32c5 + esp32_version: 3.3.11 + board_manager_url: https://espressif.github.io/arduino-esp32/package_esp32_index.json # ESP32-C6 (v3 only) - board: esp32:esp32:esp32c6 esp32_version: 3.1.1 diff --git a/.github/workflows/IDFBuild.yml b/.github/workflows/IDFBuild.yml index 7e7101c0..110786c9 100644 --- a/.github/workflows/IDFBuild.yml +++ b/.github/workflows/IDFBuild.yml @@ -12,6 +12,11 @@ on: pull_request: workflow_dispatch: +# supersede queued/running builds of the same branch or PR +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: build: name: ${{ matrix.target }} (IDF ${{ matrix.idf_version }}) @@ -27,6 +32,8 @@ jobs: idf_version: "5.3" - target: esp32 idf_version: "5.1.6" + - target: esp32 + idf_version: "6.0.2" # ESP32-S3 - target: esp32s3 idf_version: "5.3" @@ -37,9 +44,14 @@ jobs: idf_version: "5.3" - target: esp32c3 idf_version: "5.1.6" + # ESP32-C5 (IDF 5.5+ only) + - target: esp32c5 + idf_version: "5.5.2" # ESP32-C6 (IDF 5.3+ only) - target: esp32c6 idf_version: "5.3" + - target: esp32c6 + idf_version: "5.5.2" # ESP32-H2 - target: esp32h2 idf_version: "5.3" From 2b3c6d654bedd2958fbb68ab8242659ee9e6e820 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Fri, 14 Aug 2026 13:22:12 +0000 Subject: [PATCH 42/56] Add PWM brightness control for the M5ToughC5 backlight The backlight was previously on/off only, so setBrightness had no dimming effect. Drive it with the IO expander PWM channel in the same way as the ChainCaptain, with a gamma 2.0 curve so the perceived brightness follows the setting. --- src/M5GFX.cpp | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index 4c19d71e..b704c717 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -864,22 +864,29 @@ namespace m5gfx struct Light_M5ToughC5 : public lgfx::ILight { + // LCD backlight = M5IOE1 PIN10, driven by the expander's PWM channel 4. + // Registers: 0x21/0x22 = PWM4 duty (12bit, H[7]=enable), 0x25/0x26 = shared PWM frequency (Hz). bool init(uint8_t brightness) override { + static constexpr uint8_t freq_1khz[] = { 0x25, 0xE8, 0x03 }; + lgfx::i2c::transactionWrite(i2c_port, m5ioe1_i2c_addr, freq_1khz, sizeof(freq_1khz), m5ioe1_i2c_freq); + lgfx::i2c::bitOn(i2c_port, m5ioe1_i2c_addr, 0x04, 0x02, m5ioe1_i2c_freq); // PIN10 output mode setBrightness(brightness); return true; } + void writeDuty(uint_fast16_t duty12) + { + uint8_t buf[] = { 0x21, (uint8_t)duty12, (uint8_t)(0x80 | (duty12 >> 8)) }; + lgfx::i2c::transactionWrite(i2c_port, m5ioe1_i2c_addr, buf, sizeof(buf), m5ioe1_i2c_freq); + } + void setBrightness(uint8_t brightness) override { - // M5IOE1 PIN10 (bit1 in high register 0x06) controls LCD backlight - static constexpr uint8_t IOE1_PIN_10 = 9; - static constexpr uint8_t IOE1_BIT_10_H = (1u << (IOE1_PIN_10 - 8)); - if (brightness) { - lgfx::i2c::bitOn(i2c_port, m5ioe1_i2c_addr, 0x06, IOE1_BIT_10_H, m5ioe1_i2c_freq); - } else { - lgfx::i2c::bitOff(i2c_port, m5ioe1_i2c_addr, 0x06, IOE1_BIT_10_H, m5ioe1_i2c_freq); - } + // gamma 2.0: perceived brightness tracks the setting instead of the raw duty + uint_fast16_t duty12 = ((uint32_t)brightness * brightness * 4095u + 32512u) / 65025u; + if (brightness && duty12 == 0) { duty12 = 1; } + writeDuty(duty12); } }; From b48269dc884ca1c660f0a4db89cd99362971dbf2 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Sat, 15 Aug 2026 03:48:36 +0000 Subject: [PATCH 43/56] Fix SPI bus pins left in open-drain mode on ESP-IDF v6 (#192) ESP-IDF v6 spi_bus_initialize() routes bus pins with gpio_matrix_output() and no longer clears the pad open-drain flag (v5 used gpio_set_direction(), which did). If the pads were previously configured as open-drain outputs (e.g. by the autodetect pull-up probing of the bus pins), SCLK/MOSI stay open-drain, the waveform cannot rise at MHz clock rates through the weak pull-up, and the panel never responds. Explicitly restore the bus pins to push-pull after bus initialization. Verified on M5Stack CoreS3SE: ESP-IDF v6.0.1 autodetect and draw/readRect round-trip now pass; no regression on ESP-IDF v5.5.4 (CoreS3SE) or M5StickS3 (v6.0.1). --- src/lgfx/v1/platforms/esp32/common.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/lgfx/v1/platforms/esp32/common.cpp b/src/lgfx/v1/platforms/esp32/common.cpp index 272e2460..9c2c03d5 100644 --- a/src/lgfx/v1/platforms/esp32/common.cpp +++ b/src/lgfx/v1/platforms/esp32/common.cpp @@ -92,6 +92,10 @@ Original Source: #include #endif +#if __has_include() + #include +#endif + #if defined (ESP_IDF_VERSION_VAL) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) #include @@ -713,6 +717,22 @@ 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 + } + } + 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 12cf3462237498b02617b82d47203cdfd48a1b31 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Sat, 15 Aug 2026 04:18:42 +0000 Subject: [PATCH 44/56] Use gpio_ll / esp_rom helpers instead of per-chip register field names in pinMode The pad_driver and func_out_sel register layouts are named differently on some chips (e.g. ESP32-C61), which required chip-specific #ifdef branches and SFINAE helpers. Delegate to gpio_ll_od_enable/od_disable and esp_rom_gpio_connect_out_signal, whose per-chip implementations absorb the naming differences, so future chips need no new branches here. Direct register access remains only as a fallback for ESP-IDF v3. esp_rom_gpio_connect_out_signal also normalizes the inv/oen matrix bits, which matches the intended plain-GPIO-output contract of pinMode. Build-verified on esp32s3 (ESP-IDF v5.5.4 / v6.0.1), esp32c61, esp32 (v5.5.4), and Arduino-ESP32 2.x (ESP-IDF v4.4); runtime-verified on M5Stack CoreS3SE with ESP-IDF v6.0.1. --- src/lgfx/v1/platforms/esp32/common.cpp | 43 ++++++++------------------ 1 file changed, 13 insertions(+), 30 deletions(-) diff --git a/src/lgfx/v1/platforms/esp32/common.cpp b/src/lgfx/v1/platforms/esp32/common.cpp index 9c2c03d5..bdf0e82b 100644 --- a/src/lgfx/v1/platforms/esp32/common.cpp +++ b/src/lgfx/v1/platforms/esp32/common.cpp @@ -96,6 +96,10 @@ Original Source: #include #endif +#if __has_include() + #include +#endif + #if defined (ESP_IDF_VERSION_VAL) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) #include @@ -234,29 +238,6 @@ namespace lgfx static inline typename std::enable_if::value && !has_func_in_sel::value, uint32_t>::type get_gpio_func_in_sel(T& cfg) { return cfg.in_sel; } - // ------- func_out_sel detect ------- - // func_sel/funcn_out_sel/out_sel: A type characteristic that detects the presence of members - template - struct has_funcn_out_sel : std::false_type {}; - - template - struct has_funcn_out_sel().funcn_out_sel))> : std::true_type {}; - - // func_sel exist - template - static inline typename std::enable_if::value>::type - set_gpio_func_out_sel(T& cfg, uint32_t val) { cfg.func_sel = val; } - - // func_sel does not exist, funcn_out_sel exist - template - static inline typename std::enable_if::value && has_funcn_out_sel::value>::type - set_gpio_func_out_sel(T& cfg, uint32_t val) { cfg.funcn_out_sel = val; } - - // func_sel && funcn_out_sel does not exist (use out_sel) - template - static inline typename std::enable_if::value && !has_funcn_out_sel::value>::type - set_gpio_func_out_sel(T& cfg, uint32_t val) { cfg.out_sel = val; } - static int search_pin_number(int peripheral_sig) { uint32_t result = get_gpio_func_in_sel(GPIO.func_in_sel_cfg[peripheral_sig]); @@ -460,9 +441,11 @@ namespace lgfx *io_mux_reg = io_mux_val; -#if defined(CONFIG_IDF_TARGET_ESP32C61) - GPIO.pinn[pin].pinn_pad_driver = (mode == pin_mode_t::output) ? 0 : 1; // 1 = OpenDrain / 0 = normal output -#else + // レジスタ構造体のメンバ名はチップ毎に異なるため、命名差を吸収する LL/ROM API へ委譲する +#if __has_include() + if (mode == pin_mode_t::output) { gpio_ll_od_disable(&GPIO, (gpio_num_t)pin); } + else { gpio_ll_od_enable (&GPIO, (gpio_num_t)pin); } // OpenDrain +#else // ESP-IDF v3 系 (無印 ESP32 のみ) GPIO.pin[pin].pad_driver = (mode == pin_mode_t::output) ? 0 : 1; // 1 = OpenDrain / 0 = normal output #endif if (mode != pin_mode_t::output) { @@ -470,10 +453,10 @@ namespace lgfx } auto gpio_en_reg = gpio_en_regs[((pin >> 5) << 1) + 1]; *gpio_en_reg = 1u << (pin & 31); -#if defined(CONFIG_IDF_TARGET_ESP32C61) - set_gpio_func_out_sel(GPIO.funcn_out_sel_cfg[pin], SIG_GPIO_OUT_IDX); -#else - set_gpio_func_out_sel(GPIO.func_out_sel_cfg[pin], SIG_GPIO_OUT_IDX); +#if __has_include() + esp_rom_gpio_connect_out_signal(pin, SIG_GPIO_OUT_IDX, false, false); +#else // ESP-IDF v3 系 (無印 ESP32 のみ) + GPIO.func_out_sel_cfg[pin].func_sel = SIG_GPIO_OUT_IDX; #endif } From 57df72a61cc0428dc9fb328cbc9e25e181c98697 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Sat, 15 Aug 2026 06:11:02 +0000 Subject: [PATCH 45/56] 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 bdf0e82b..063e6a28 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 46/56] 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 676b90e6..82f3fff2 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 47/56] 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 4e46c2e3..0a778a21 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 5fc1312d..1c89f482 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 8dd0a399..96aae269 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 bdcfa058..a69ceacd 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 48/56] 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 82f3fff2..2286881c 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; } From ff5e91e332a4a01fb2cb542416213956f6a00e69 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Sat, 15 Aug 2026 09:40:22 +0000 Subject: [PATCH 49/56] Fix pinMode pulls not driven on ESP-IDF v6 (SOC_GPIO_SUPPORT_RTC_INDEPENDENT removed) ESP-IDF v6 removed the SOC_GPIO_SUPPORT_RTC_INDEPENDENT soc capability macro. pinMode treated the missing macro as 0, which routes RTC-capable pins through the RTC-domain pull path meant for the plain ESP32. On ESP32-S2/S3 (and other chips with independent digital pads) that path does not drive the pad pulls: input_pullup left the IO_MUX FUN_PU bit clear and the line did not rise. Map the capability to an internal LGFX_GPIO_RTC_INDEPENDENT macro that uses the SoC value when available and otherwise derives it from the build target: only the plain ESP32 needs the RTC path. Verified on ESP32-S3 (floating RTC-capable pin, ESP-IDF v6.0.1): input_pullup previously read back low with FUN_PU clear; with this fix the pull-up is driven and the pin reads high. No regression on v5.5.4 (macro still provided by the SoC caps) and no behavior change for the plain ESP32. --- src/lgfx/v1/platforms/esp32/common.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/lgfx/v1/platforms/esp32/common.cpp b/src/lgfx/v1/platforms/esp32/common.cpp index 063e6a28..8a944f6c 100644 --- a/src/lgfx/v1/platforms/esp32/common.cpp +++ b/src/lgfx/v1/platforms/esp32/common.cpp @@ -84,8 +84,16 @@ Original Source: #include #endif -#ifndef SOC_GPIO_SUPPORT_RTC_INDEPENDENT -#define SOC_GPIO_SUPPORT_RTC_INDEPENDENT 0 +// Whether the digital pads (pulls in IO_MUX) work independently of the RTC IO +// domain; only the plain ESP32 needs the RTC-domain path for RTC-capable pins. +// ESP-IDF v6 removed the SOC_GPIO_SUPPORT_RTC_INDEPENDENT macro, so derive +// the value from the build target when it is absent. +#if defined (SOC_GPIO_SUPPORT_RTC_INDEPENDENT) + #define LGFX_GPIO_RTC_INDEPENDENT SOC_GPIO_SUPPORT_RTC_INDEPENDENT +#elif defined (CONFIG_IDF_TARGET) && !defined (CONFIG_IDF_TARGET_ESP32) + #define LGFX_GPIO_RTC_INDEPENDENT 1 +#else + #define LGFX_GPIO_RTC_INDEPENDENT 0 #endif #if __has_include() @@ -416,7 +424,7 @@ namespace lgfx auto io_mux_val = *io_mux_reg; // & ~(FUN_PU_M | FUN_PD_M | SLP_PU_M | SLP_PD_M | MCU_SEL_M); #if SOC_RTCIO_INPUT_OUTPUT_SUPPORTED - if (!SOC_GPIO_SUPPORT_RTC_INDEPENDENT && rtc_gpio_is_valid_gpio(gpio_num)) { + if (!LGFX_GPIO_RTC_INDEPENDENT && rtc_gpio_is_valid_gpio(gpio_num)) { rtc_gpio_deinit(gpio_num); if (mode == pin_mode_t::input_pulldown) { rtc_gpio_pulldown_en((gpio_num_t)pin); } From 2d1d2682fc48e19529dfff5a64010b3840b9acb5 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Sun, 16 Aug 2026 03:04:58 +0000 Subject: [PATCH 50/56] Remove the OPI-PSRAM requirement from the M5StopWatch detection Panel_CO5300 supports direct drawing: the optional PSRAM frame buffer (initPanelFb) may fail to allocate and is not required, so the PSRAM guard (introduced alongside the EPD boards, which genuinely need the buffer) is unnecessary here. Worse, with PSRAM disabled the guard skipped the panel setup after the board had already been identified, and the autodetection then fell through and misidentified the device as an AtomS3Lite. Direct drawing has a limitation: drawing whose origin is at an odd coordinate may render incorrectly, so a warning is logged when the build lacks OPI-PSRAM. Verified on the real device: with PSRAM disabled the board is now detected as M5StopWatch and the display works via direct drawing; builds with and without OPI-PSRAM are both green. --- src/M5GFX.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index b704c717..0ffbd02e 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -1888,11 +1888,14 @@ namespace m5gfx board = board_t::board_M5StopWatch; ESP_LOGI(LIBRARY_NAME, "[Autodetect] board_M5StopWatch"); -#if !(defined(CONFIG_ESP32S3_SPIRAM_SUPPORT)) - ESP_LOGE(LIBRARY_NAME, "M5StopWatch need OPI-PSRAM enabled"); -#elif !defined (CONFIG_SPIRAM_MODE_OCT) - ESP_LOGE(LIBRARY_NAME, "M5StopWatch need OPI-PSRAM enabled"); -#else + // Panel_CO5300 supports direct drawing; the optional PSRAM frame + // buffer (initPanelFb) may fail to allocate and is not required, + // so no PSRAM requirement applies here (unlike the EPD boards). +#if !(defined(CONFIG_ESP32S3_SPIRAM_SUPPORT)) || !defined (CONFIG_SPIRAM_MODE_OCT) + // Without the frame buffer, drawing whose origin is at an odd + // coordinate may render incorrectly (this affects e.g. text glyphs). + ESP_LOGW(LIBRARY_NAME, "M5StopWatch: OPI-PSRAM is disabled; the display falls back to direct drawing, which may render incorrectly when the drawing origin is at an odd coordinate. Enable OPI-PSRAM for correct rendering."); +#endif // GPIO39:OLED CS Pin lgfx::pinMode(GPIO_NUM_39, lgfx::pin_mode_t::output); @@ -1983,7 +1986,6 @@ namespace m5gfx } goto init_clear; -#endif } if (is_papermono) { From 12002e98f6b6514f5d301f7f5a61986f3da04416 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Sun, 16 Aug 2026 03:38:36 +0000 Subject: [PATCH 51/56] Keep the board identification when the EPD boards lack OPI-PSRAM When PSRAM was disabled in the build, the EPD board blocks (PaperMono, PaperS3/PaperDIY, PaperColor, ChainCaptain) logged the requirement but skipped the panel setup and fell through the rest of the autodetection, which ended with board_unknown and let the caller misidentify the device as a display-less model. Jump to init_clear instead so the board identification is kept; the display simply stays unavailable. Clear _panel_last and _touch_last before the jump: autodetect() only detaches the active panel at entry, so a stale panel object from a previous detection round would otherwise be re-attached at init_clear. --- src/M5GFX.cpp | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index 0ffbd02e..830fac75 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -1994,8 +1994,14 @@ namespace m5gfx #if !(defined(CONFIG_ESP32S3_SPIRAM_SUPPORT)) ESP_LOGE(LIBRARY_NAME, "M5PaperMono need OPI-PSRAM enabled"); + _panel_last.reset(); + _touch_last.reset(); + goto init_clear; // keep the board identification; the display stays unavailable #elif !defined (CONFIG_SPIRAM_MODE_OCT) ESP_LOGE(LIBRARY_NAME, "M5PaperMono need OPI-PSRAM enabled"); + _panel_last.reset(); + _touch_last.reset(); + goto init_clear; // keep the board identification; the display stays unavailable #else // GPIO16:EINK CS Pin @@ -2104,8 +2110,14 @@ namespace m5gfx #if !(defined(CONFIG_ESP32S3_SPIRAM_SUPPORT)) ESP_LOGE(LIBRARY_NAME, "M5ChainCaptain needs OPI-PSRAM enabled"); + _panel_last.reset(); + _touch_last.reset(); + goto init_clear; // keep the board identification; the display stays unavailable #elif !defined (CONFIG_SPIRAM_MODE_OCT) ESP_LOGE(LIBRARY_NAME, "M5ChainCaptain needs OPI-PSRAM enabled"); + _panel_last.reset(); + _touch_last.reset(); + goto init_clear; // keep the board identification; the display stays unavailable #else // M5PM1 and M5IOE1 may retain their idle-sleep settings across battery-powered shutdown. lgfx::i2c::writeRegister8(i2c_port, m5pm1_i2c_addr, 0x09, 0x00, 0, m5pm1_i2c_freq); @@ -2187,8 +2199,14 @@ namespace m5gfx #if !(defined(CONFIG_ESP32S3_SPIRAM_SUPPORT)) ESP_LOGE(LIBRARY_NAME, "M5PaperColor need OPI-PSRAM enabled"); + _panel_last.reset(); + _touch_last.reset(); + goto init_clear; // keep the board identification; the display stays unavailable #elif !defined (CONFIG_SPIRAM_MODE_OCT) ESP_LOGE(LIBRARY_NAME, "M5PaperColor need OPI-PSRAM enabled"); + _panel_last.reset(); + _touch_last.reset(); + goto init_clear; // keep the board identification; the display stays unavailable #else // Disable watchdog (WDT_CNT=0 disables) lgfx::i2c::writeRegister8(i2c_port, m5pm1_i2c_addr, 0x0A, 0x00, 0x00, m5pm1_i2c_freq); @@ -2313,8 +2331,14 @@ namespace m5gfx #if !(defined(CONFIG_ESP32S3_SPIRAM_SUPPORT)) ESP_LOGE(LIBRARY_NAME, "%s need OPI-PSRAM enabled", board == board_t::board_M5PaperDIY ? "M5PaperDIY" : "M5PaperS3"); + _panel_last.reset(); + _touch_last.reset(); + goto init_clear; // keep the board identification; the display stays unavailable #elif !defined (CONFIG_SPIRAM_MODE_OCT) ESP_LOGE(LIBRARY_NAME, "%s need OPI-PSRAM enabled", board == board_t::board_M5PaperDIY ? "M5PaperDIY" : "M5PaperS3"); + _panel_last.reset(); + _touch_last.reset(); + goto init_clear; // keep the board identification; the display stays unavailable #else auto bus_epd = new Bus_EPD(); _bus_last.reset(bus_epd); From 9d3a8505c0a79ec9cd07275151f7d2eca08e0028 Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Sun, 16 Aug 2026 11:41:00 +0000 Subject: [PATCH 52/56] Wait for the touch controller response when identifying the M5Tab5 panel The ST71xx touch controller needs several tens of milliseconds to respond again when it is reset during scan operation. The previous 3-attempt probe gave up too early, so a warm reset from a running state (esp_restart or the reset caused by connecting a serial monitor) left the panel type undetected and the screen blank. Poll until a recognized ST71xx firmware version is read or the GT911 (ILI9881C variant) ACKs, up to roughly 600 ms. The ST variants still require the touch FW version to distinguish their DSI timings; a unit whose touch never responds falls back to the previous behavior (the ILI9881C variant is still identified by its DSI ID). --- src/M5GFX.cpp | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index b704c717..5a9a074a 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -2940,12 +2940,26 @@ The usage of each pin is as follows. bool hit_st7121 = false; bool hit_st7123 = false; bool read_st_touch_fw = false; - for (int i = 0; i < 3; ++i) { + bool found_gt911 = false; + // ST71xx のタッチコントローラは、スキャン動作中にリセットされた場合、 + // 応答可能になるまで数十 ms を要する (実測: 稼働中からのソフトリセット後 + // およそ 50ms)。待ちを打ち切ると ST パネルの判別に失敗して表示が出ない + // (ST7121/ST7123 は lane 速度が異なり DSI ID では安全に区別できない) ため、 + // ST touch の既知 FW 版数 (1/3) を認識するか GT911 (ILI9881C 個体) が + // ACK するまで待つ。 + int last_logged_fw = -1; + for (int i = 0; i < 60; ++i) { uint8_t fw_version = 0; uint8_t fw_reg[2] = { 0, 0 }; if (lgfx::i2c::transactionWriteRead(probe_i2c_port, Touch_ST7123::default_addr, fw_reg, sizeof(fw_reg), &fw_version, 1, 100000).has_value()) { read_st_touch_fw = true; - ESP_LOGI(LIBRARY_NAME, "M5Tab5 ST touch FW version %02x", fw_version); + if (fw_version != last_logged_fw) { // 未知値のリトライ継続で同じログを繰り返さない + last_logged_fw = fw_version; + ESP_LOGI(LIBRARY_NAME, "M5Tab5 ST touch FW version %02x", fw_version); + if (fw_version != 1 && fw_version != 3) { + ESP_LOGW(LIBRARY_NAME, "M5Tab5 unknown ST touch FW version %02x", fw_version); + } + } if (fw_version == 1) { hit_st7121 = true; break; @@ -2954,11 +2968,18 @@ The usage of each pin is as follows. hit_st7123 = true; break; } - ESP_LOGW(LIBRARY_NAME, "M5Tab5 unknown ST touch FW version %02x", fw_version); + } else { + // GT911 はアドレス ACK の確認のみ (実読すると内部ポインタを進めてしまう) + if (lgfx::i2c::beginTransaction(probe_i2c_port, Touch_GT911::default_addr_1, 100000, false).has_value() + && lgfx::i2c::endTransaction(probe_i2c_port).has_value()) { + found_gt911 = true; + ESP_LOGI(LIBRARY_NAME, "M5Tab5 GT911 touch detected"); + break; + } } lgfx::delay(10); } - if (!read_st_touch_fw) { + if (!read_st_touch_fw && !found_gt911) { ESP_LOGW(LIBRARY_NAME, "M5Tab5 ST touch FW version read failed"); } From d20b138593f593016af9997b6e627fed8f86bf2b Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Sun, 16 Aug 2026 12:42:33 +0000 Subject: [PATCH 53/56] Keep the guessed space advance for VLW fonts without a space glyph Some VLW exporters (e.g. the Processing font converter) omit the space glyph from the file. Falling through to drawCharDummy would render the missing-glyph box for every space, so restore the previous silent guessed-width behavior for that case only. Fonts that do contain a space glyph keep using the actual glyph metrics. --- src/lgfx/v1/lgfx_fonts.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lgfx/v1/lgfx_fonts.cpp b/src/lgfx/v1/lgfx_fonts.cpp index 4092c483..5ed465d8 100644 --- a/src/lgfx/v1/lgfx_fonts.cpp +++ b/src/lgfx/v1/lgfx_fonts.cpp @@ -887,7 +887,13 @@ namespace lgfx y += (metrics->y_offset * sy) >> 16; if (!this->getUnicodeIndex(code, &gNum)) { - return drawCharDummy(gfx, x, y, this->spaceWidth, metrics->height, style, filled_x); + if (code != 0x20) { + return drawCharDummy(gfx, x, y, this->spaceWidth, metrics->height, style, filled_x); + } + // Some VLW exporters omit the space glyph; for such fonts, keep the + // guessed advance instead of rendering the missing-glyph box. + gNum = 0xFFFF; + buffer[2] = getSwap32(this->spaceWidth); } else { file->preRead(); file->seek(28 + gNum * 28); From f04087dfd6337ed3761ca8e0d4a6e32ba717aedd Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Sun, 16 Aug 2026 13:56:25 +0000 Subject: [PATCH 54/56] AtomS3R: re-probe the panel ID at low clock speed before giving up Some GC9107 batches return a valid ID over the RDDID command only at low clock rates; at the 8 MHz probe speed the readout is garbage, so autodetect rejected the panel and the unit appeared dead (issue #222). When the first probe matches neither known ID, re-probe once at 100 kHz. Verified on a healthy GC9107 unit that the 100 kHz readout returns the correct ID (0x079100), and on both GC9107 and ST7735 units that the normal first-probe path is unchanged. The ST7735 does not answer the RDDID readout at low clock rates at all (verified: 0xFFFFFFFF at 1 MHz and below, correct at 8 MHz), so the slow retry cannot misidentify it; a healthy ST7735 has already been caught by the first probe. --- src/M5GFX.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index ccfd0b81..f6f3bac8 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -2760,6 +2760,24 @@ The usage of each pin is as follows. bool is_st7735 = ((id & 0xFFFF) == 0x7683 || (id & 0xFFFF) == 0x897C); bool is_gc9107 = (id & 0xFFFFFF) == 0x079100; // ESP_LOGI(LIBRARY_NAME, "[Autodetect] panel_id: 0x%08x", id); + if (!is_st7735 && !is_gc9107) + { // Some GC9107 batches return a valid ID only at low clock rates; + // re-probe slowly before giving up. + // (ST7735 does not answer at this rate, but a healthy one has + // already been caught by the first probe above.) + bus_spi->release(); + bus_cfg.freq_write = 100000; + bus_cfg.freq_read = 100000; + bus_spi->config(bus_cfg); + bus_spi->init(); + id = _read_panel_id(bus_spi, GPIO_NUM_14); + // restore the probe speed in bus_cfg: later board probes reuse it + // without setting the frequency themselves. + bus_cfg.freq_write = 8000000; + bus_cfg.freq_read = 8000000; + is_st7735 = ((id & 0xFFFF) == 0x7683 || (id & 0xFFFF) == 0x897C); + is_gc9107 = (id & 0xFFFFFF) == 0x079100; + } if (is_st7735 || is_gc9107) { board = board_t::board_M5AtomS3R; From fe54e8393369523117124ad87a34099057cc26ab Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 17 Aug 2026 01:08:58 +0000 Subject: [PATCH 55/56] Bump version to 0.2.27 --- idf_component.yml | 2 +- library.json | 2 +- library.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/idf_component.yml b/idf_component.yml index f5f821dc..61c53d38 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.26 +version: 0.2.27 diff --git a/library.json b/library.json index caa49f33..b6821377 100644 --- a/library.json +++ b/library.json @@ -10,7 +10,7 @@ "type": "git", "url": "https://github.com/m5stack/M5GFX.git" }, - "version": "0.2.26", + "version": "0.2.27", "frameworks": ["arduino", "espidf", "*"], "platforms": ["espressif32", "native"], "headers": "M5GFX.h" diff --git a/library.properties b/library.properties index 14e359b9..70d7309e 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=M5GFX -version=0.2.26 +version=0.2.27 author=M5Stack maintainer=M5Stack sentence=Library for M5Stack All Display From c40c85324aed8979319da2d477b67309a7a085ec Mon Sep 17 00:00:00 2001 From: ainyan03 Date: Mon, 17 Aug 2026 01:30:10 +0000 Subject: [PATCH 56/56] Update the version define missed in the 0.2.27 bump --- src/lgfx/v1/gitTagVersion.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lgfx/v1/gitTagVersion.h b/src/lgfx/v1/gitTagVersion.h index b392faf7..187a0872 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 26 +#define LGFX_VERSION_PATCH 27 #define LOVYANGFX_VERSION F( LGFX_VERSION_MAJOR "." LGFX_VERSION_MINOR "." LGFX_VERSION_PATCH )