diff --git a/idf_component.yml b/idf_component.yml index d931684..f5f821d 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.25 +version: 0.2.26 diff --git a/library.json b/library.json index 7ee1b57..caa49f3 100644 --- a/library.json +++ b/library.json @@ -10,7 +10,7 @@ "type": "git", "url": "https://github.com/m5stack/M5GFX.git" }, - "version": "0.2.25", + "version": "0.2.26", "frameworks": ["arduino", "espidf", "*"], "platforms": ["espressif32", "native"], "headers": "M5GFX.h" diff --git a/library.properties b/library.properties index 14e0c35..14e359b 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=M5GFX -version=0.2.25 +version=0.2.26 author=M5Stack maintainer=M5Stack sentence=Library for M5Stack All Display diff --git a/src/M5GFX.cpp b/src/M5GFX.cpp index c21a240..5e8b4fb 100644 --- a/src/M5GFX.cpp +++ b/src/M5GFX.cpp @@ -416,6 +416,8 @@ namespace m5gfx 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; + static constexpr int_fast16_t chain_captain_i2c_sda = GPIO_NUM_3; + static constexpr int_fast16_t chain_captain_i2c_scl = GPIO_NUM_2; struct Panel_M5StackCoreS3 : public lgfx::Panel_ILI9342 { @@ -606,6 +608,38 @@ namespace m5gfx } }; + struct Light_M5ChainCaptain : public lgfx::ILight + { + bool init(uint8_t brightness) override + { + lgfx::i2c::init(i2c_port, chain_captain_i2c_sda, chain_captain_i2c_scl); + + // Disable M5IOE1 I2C idle sleep and configure IO11/PWM_CH3 for the backlight. + lgfx::i2c::writeRegister8(i2c_port, m5ioe1_i2c_addr, 0x23, 0x00, 0, m5ioe1_i2c_freq); + lgfx::i2c::bitOff(i2c_port, m5ioe1_i2c_addr, 0x14, 1u << 2, m5ioe1_i2c_freq); + lgfx::i2c::bitOn( i2c_port, m5ioe1_i2c_addr, 0x04, 1u << 2, m5ioe1_i2c_freq); + + const uint16_t pwm_freq = 1000; + uint8_t freq_buf[] = { 0x25, (uint8_t)pwm_freq, (uint8_t)(pwm_freq >> 8) }; + lgfx::i2c::transactionWrite(i2c_port, m5ioe1_i2c_addr, freq_buf, sizeof(freq_buf), m5ioe1_i2c_freq); + + setBrightness(brightness); + return true; + } + + void setBrightness(uint8_t brightness) override + { + const uint32_t squared = (uint32_t)brightness * brightness; + const uint16_t duty = (squared * 4095u + 32512u) / 65025u; + uint8_t duty_buf[] = { + 0x1F, // PWM_CH3 duty low register + (uint8_t)duty, + (uint8_t)((duty >> 8) | 0x80), // normal polarity, PWM enabled + }; + lgfx::i2c::transactionWrite(i2c_port, m5ioe1_i2c_addr, duty_buf, sizeof(duty_buf), m5ioe1_i2c_freq); + } + }; + struct Panel_StopWatch : public lgfx::Panel_CO5300 { Panel_StopWatch(void) @@ -1832,6 +1866,89 @@ namespace m5gfx } } + if (board == 0 || board == board_t::board_M5ChainCaptain) + { + static constexpr uint8_t chain_captain_i2c_addr_list[] = { + 0x32u, // RX8130 + 0x4Fu, // M5IOE1 + 0x68u, // BMI270 + 0x6Eu, // M5PM1 + 0u + }; + uint32_t i2c_result = _detect_i2c_device(chain_captain_i2c_sda, chain_captain_i2c_scl, chain_captain_i2c_addr_list); + + if (i2c_result == ~0u) { + gpio::pin_backup_t backup_pins[] = { GPIO_NUM_2, GPIO_NUM_3, GPIO_NUM_15, GPIO_NUM_16, GPIO_NUM_45, GPIO_NUM_46 }; + lgfx::i2c::init(i2c_port, chain_captain_i2c_sda, chain_captain_i2c_scl); + if (_check_m5pm1(i2c_port) && _check_m5ioe1(i2c_port)) { + board = board_t::board_M5ChainCaptain; + ESP_LOGI(LIBRARY_NAME, "[Autodetect] board_M5ChainCaptain"); + +#if !(defined(CONFIG_ESP32S3_SPIRAM_SUPPORT)) + ESP_LOGE(LIBRARY_NAME, "M5ChainCaptain needs OPI-PSRAM enabled"); +#elif !defined (CONFIG_SPIRAM_MODE_OCT) + ESP_LOGE(LIBRARY_NAME, "M5ChainCaptain needs OPI-PSRAM enabled"); +#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); + lgfx::i2c::writeRegister8(i2c_port, m5pm1_i2c_addr, 0x0A, 0x00, 0, m5pm1_i2c_freq); + lgfx::i2c::writeRegister8(i2c_port, m5ioe1_i2c_addr, 0x23, 0x00, 0, m5ioe1_i2c_freq); + + // IO12: LCD power, IO1: LCD reset. + lgfx::i2c::bitOff(i2c_port, m5ioe1_i2c_addr, 0x14, 1u << 3, m5ioe1_i2c_freq); + lgfx::i2c::bitOn( i2c_port, m5ioe1_i2c_addr, 0x04, 1u << 3, m5ioe1_i2c_freq); + lgfx::i2c::bitOn( i2c_port, m5ioe1_i2c_addr, 0x06, 1u << 3, m5ioe1_i2c_freq); + lgfx::i2c::bitOff(i2c_port, m5ioe1_i2c_addr, 0x13, 1u << 0, m5ioe1_i2c_freq); + lgfx::i2c::bitOn( i2c_port, m5ioe1_i2c_addr, 0x03, 1u << 0, m5ioe1_i2c_freq); + if (use_reset) { + lgfx::i2c::bitOff(i2c_port, m5ioe1_i2c_addr, 0x05, 1u << 0, m5ioe1_i2c_freq); + lgfx::delay(10); + } + lgfx::i2c::bitOn(i2c_port, m5ioe1_i2c_addr, 0x05, 1u << 0, m5ioe1_i2c_freq); + lgfx::delay(20); + + bus_cfg.pin_mosi = GPIO_NUM_16; + bus_cfg.pin_miso = GPIO_NUM_NC; + bus_cfg.pin_sclk = GPIO_NUM_15; + bus_cfg.pin_dc = GPIO_NUM_46; + bus_cfg.spi_mode = 0; + bus_cfg.spi_3wire = true; + bus_cfg.spi_host = SPI2_HOST; + bus_cfg.freq_write = 40000000; + bus_cfg.freq_read = 16000000; + bus_spi->config(bus_cfg); + bus_spi->init(); + + auto p = new Panel_ST7789(); + p->bus(bus_spi); + { + auto cfg = p->config(); + cfg.pin_cs = GPIO_NUM_45; + cfg.pin_rst = GPIO_NUM_NC; + cfg.memory_width = 240; + cfg.memory_height = 320; + cfg.panel_width = 240; + cfg.panel_height = 240; + cfg.offset_x = 0; + cfg.offset_y = 0; + cfg.offset_rotation = 2; + cfg.readable = false; + cfg.invert = true; + cfg.bus_shared = false; + p->config(cfg); + p->setRotation(0); + } + _panel_last.reset(p); + _set_backlight(new Light_M5ChainCaptain()); + goto init_clear; +#endif + } + lgfx::i2c::release(i2c_port); + bus_spi->release(); + for (auto pin: backup_pins) { pin.restore(); } + } + } + if (board == 0 || board == board_t::board_M5PaperColor) { static constexpr int_fast16_t papercolor_i2c_sda = GPIO_NUM_3; @@ -1915,33 +2032,37 @@ namespace m5gfx } } - if (board == 0 || board == board_t::board_M5PaperS3) + if (board == 0 || board == board_t::board_M5PaperS3 || board == board_t::board_M5PaperDIY) { - static constexpr int_fast16_t papers3_i2c_sda = GPIO_NUM_41; - static constexpr int_fast16_t papers3_i2c_scl = GPIO_NUM_42; + static constexpr int_fast16_t paper_i2c_sda = GPIO_NUM_41; + static constexpr int_fast16_t paper_i2c_scl = GPIO_NUM_42; static constexpr const uint8_t gt911_i2c_addr[] = { 0x14, 0x5D }; - gpio::pin_backup_t backup_pins[] = { papers3_i2c_sda, papers3_i2c_scl }; + gpio::pin_backup_t backup_pins[] = { paper_i2c_sda, paper_i2c_scl }; auto result = lgfx::gpio::command( (const uint8_t[]) { - lgfx::gpio::command_mode_output , papers3_i2c_scl, - lgfx::gpio::command_write_low , papers3_i2c_scl, - lgfx::gpio::command_mode_output , papers3_i2c_sda, - lgfx::gpio::command_write_low , papers3_i2c_sda, - lgfx::gpio::command_write_high , papers3_i2c_scl, - lgfx::gpio::command_write_high , papers3_i2c_sda, - lgfx::gpio::command_mode_input_pulldown, papers3_i2c_scl, - lgfx::gpio::command_mode_input_pulldown, papers3_i2c_sda, + lgfx::gpio::command_mode_output , paper_i2c_scl, + lgfx::gpio::command_write_low , paper_i2c_scl, + lgfx::gpio::command_mode_output , paper_i2c_sda, + lgfx::gpio::command_write_low , paper_i2c_sda, + lgfx::gpio::command_write_high , paper_i2c_scl, + lgfx::gpio::command_write_high , paper_i2c_sda, + lgfx::gpio::command_mode_input_pulldown, paper_i2c_scl, + lgfx::gpio::command_mode_input_pulldown, paper_i2c_sda, lgfx::gpio::command_delay , 1, - lgfx::gpio::command_read , papers3_i2c_scl, - lgfx::gpio::command_read , papers3_i2c_sda, + lgfx::gpio::command_read , paper_i2c_scl, + lgfx::gpio::command_read , paper_i2c_sda, lgfx::gpio::command_end } ); // Check G41,G42 HIGH if (result == 0x03) { - lgfx::i2c::init(i2c_port, papers3_i2c_sda, papers3_i2c_scl); - { - bool gt911_found = false; + lgfx::i2c::init(i2c_port, paper_i2c_sda, paper_i2c_scl); + bool paperdiy_found = false; + bool gt911_found = false; + if (board != board_t::board_M5PaperS3) { + paperdiy_found = _check_m5pm1(i2c_port); + } + if (!paperdiy_found && board != board_t::board_M5PaperDIY) { for (auto addr: gt911_i2c_addr) { if (lgfx::i2c::beginTransaction(i2c_port, addr, 400000).has_value()) { gt911_found = lgfx::i2c::endTransaction(i2c_port).has_value(); @@ -1950,85 +2071,102 @@ namespace m5gfx } } } - if (gt911_found) { + } + if (paperdiy_found || gt911_found) { + if (paperdiy_found) { + board = board_t::board_M5PaperDIY; + ESP_LOGI(LIBRARY_NAME, "[Autodetect] board_M5PaperDIY"); + + // M5PM1 GPIO2 drives EPD_PWR on PaperDIY. + lgfx::i2c::writeRegister8(i2c_port, m5pm1_i2c_addr, 0x09, 0x00, 0, m5pm1_i2c_freq); + lgfx::i2c::writeRegister8(i2c_port, m5pm1_i2c_addr, 0x0A, 0x00, 0, m5pm1_i2c_freq); + lgfx::i2c::bitOff(i2c_port, m5pm1_i2c_addr, 0x16, 0b11 << (2 * 2), m5pm1_i2c_freq); + lgfx::i2c::bitOn (i2c_port, m5pm1_i2c_addr, 0x10, 1 << 2, m5pm1_i2c_freq); + lgfx::i2c::bitOff(i2c_port, m5pm1_i2c_addr, 0x13, 1 << 2, m5pm1_i2c_freq); + lgfx::i2c::bitOn (i2c_port, m5pm1_i2c_addr, 0x11, 1 << 2, m5pm1_i2c_freq); + lgfx::delay(10); + } else { board = board_t::board_M5PaperS3; ESP_LOGI(LIBRARY_NAME, "[Autodetect] board_M5PaperS3"); // PWROFF_PULSE_PIN lgfx::pinMode(GPIO_NUM_44, lgfx::pin_mode_t::output); lgfx::gpio_lo(GPIO_NUM_44); + } #if !(defined(CONFIG_ESP32S3_SPIRAM_SUPPORT)) - ESP_LOGE(LIBRARY_NAME, "M5PaperS3 need OPI-PSRAM enabled"); + ESP_LOGE(LIBRARY_NAME, "%s need OPI-PSRAM enabled", board == board_t::board_M5PaperDIY ? "M5PaperDIY" : "M5PaperS3"); #elif !defined (CONFIG_SPIRAM_MODE_OCT) - ESP_LOGE(LIBRARY_NAME, "M5PaperS3 need OPI-PSRAM enabled"); + ESP_LOGE(LIBRARY_NAME, "%s need OPI-PSRAM enabled", board == board_t::board_M5PaperDIY ? "M5PaperDIY" : "M5PaperS3"); #else - auto bus_epd = new Bus_EPD(); - _bus_last.reset(bus_epd); - auto p = new lgfx::Panel_EPD(); - _panel_last.reset(p); + auto bus_epd = new Bus_EPD(); + _bus_last.reset(bus_epd); + auto p = new lgfx::Panel_EPD(); + _panel_last.reset(p); - { - auto bus_cfg = bus_epd->config(); - bus_cfg.bus_speed = 16000000; - bus_cfg.pin_data[0] = GPIO_NUM_6; - bus_cfg.pin_data[1] = GPIO_NUM_14; - bus_cfg.pin_data[2] = GPIO_NUM_7; - bus_cfg.pin_data[3] = GPIO_NUM_12; - bus_cfg.pin_data[4] = GPIO_NUM_9; - bus_cfg.pin_data[5] = GPIO_NUM_11; - bus_cfg.pin_data[6] = GPIO_NUM_8; - bus_cfg.pin_data[7] = GPIO_NUM_10; - bus_cfg.pin_pwr = GPIO_NUM_46; - bus_cfg.pin_spv = GPIO_NUM_17; - bus_cfg.pin_ckv = GPIO_NUM_18; - bus_cfg.pin_sph = GPIO_NUM_13; - bus_cfg.pin_oe = GPIO_NUM_45; - bus_cfg.pin_le = GPIO_NUM_15; - bus_cfg.pin_cl = GPIO_NUM_16; - bus_cfg.bus_width = 8; - bus_epd->config(bus_cfg); - p->setBus(bus_epd); - } - { - auto cfg_detail = p->config_detail(); - cfg_detail.line_padding = 8; - p->config_detail(cfg_detail); - } - { - auto cfg = p->config(); - cfg.memory_width = 960; - cfg.panel_width = 960; - cfg.memory_height = 540; - cfg.panel_height = 540; - cfg.offset_rotation = 3; - cfg.offset_x = 0; - cfg.offset_y = 0; - cfg.bus_shared = false; - p->config(cfg); - } + { + auto bus_cfg = bus_epd->config(); + bus_cfg.bus_speed = 16000000; + bus_cfg.pin_data[0] = GPIO_NUM_6; + bus_cfg.pin_data[1] = GPIO_NUM_14; + bus_cfg.pin_data[2] = GPIO_NUM_7; + bus_cfg.pin_data[3] = GPIO_NUM_12; + bus_cfg.pin_data[4] = GPIO_NUM_9; + bus_cfg.pin_data[5] = GPIO_NUM_11; + bus_cfg.pin_data[6] = GPIO_NUM_8; + bus_cfg.pin_data[7] = GPIO_NUM_10; + bus_cfg.pin_pwr = GPIO_NUM_46; + bus_cfg.pin_spv = GPIO_NUM_17; + bus_cfg.pin_ckv = GPIO_NUM_18; + bus_cfg.pin_sph = GPIO_NUM_13; + bus_cfg.pin_oe = GPIO_NUM_45; + bus_cfg.pin_le = GPIO_NUM_15; + bus_cfg.pin_cl = GPIO_NUM_16; + bus_cfg.bus_width = 8; + bus_epd->config(bus_cfg); + p->setBus(bus_epd); + } + { + auto cfg_detail = p->config_detail(); + cfg_detail.line_padding = 8; + p->config_detail(cfg_detail); + } + { + auto cfg = p->config(); + cfg.memory_width = 960; + cfg.panel_width = 960; + cfg.memory_height = 540; + cfg.panel_height = 540; + cfg.offset_rotation = 3; + cfg.offset_x = 0; + cfg.offset_y = 0; + cfg.bus_shared = false; + p->config(cfg); + } - { - auto t = new lgfx::Touch_GT911(); - _touch_last.reset(t); - auto cfg = t->config(); - cfg.pin_int = GPIO_NUM_48; - cfg.pin_sda = GPIO_NUM_41; - cfg.pin_scl = GPIO_NUM_42; - cfg.freq = 400000; - cfg.i2c_port = I2C_NUM_1; - cfg.x_min = 0; - cfg.x_max = 539; - cfg.y_min = 0; - cfg.y_max = 959; - cfg.offset_rotation = 1; - cfg.bus_shared = false; - t->config(cfg); - _panel_last->touch(t); - p->touch(t); - } - goto init_clear; -#endif + if (board == board_t::board_M5PaperS3) { + auto t = new lgfx::Touch_GT911(); + _touch_last.reset(t); + auto cfg = t->config(); + cfg.pin_int = GPIO_NUM_48; + cfg.pin_sda = GPIO_NUM_41; + cfg.pin_scl = GPIO_NUM_42; + cfg.freq = 400000; + cfg.i2c_port = I2C_NUM_1; + cfg.x_min = 0; + cfg.x_max = 539; + cfg.y_min = 0; + cfg.y_max = 959; + cfg.offset_rotation = 1; + cfg.bus_shared = false; + t->config(cfg); + _panel_last->touch(t); + p->touch(t); + } + if (board == board_t::board_M5PaperDIY) { + lgfx::i2c::release(i2c_port); } + goto init_clear; +#endif } lgfx::i2c::release(i2c_port); } @@ -3147,6 +3285,7 @@ The usage of each pin is as follows. case board_M5StackCoreInk: title = "M5StackCoreInk"; break; case board_M5Paper: title = "M5Paper"; break; case board_M5PaperS3: title = "M5PaperS3"; break; + case board_M5PaperDIY: title = "M5PaperDIY"; break; case board_M5PaperColor: title = "M5PaperColor"; break; case board_M5PaperMono: title = "M5PaperMono"; break; case board_M5Tough: title = "M5Tough"; break; @@ -3154,6 +3293,7 @@ The usage of each pin is as follows. case board_M5StampC5: title = "M5StampC5"; break; case board_M5Station: title = "M5Station"; break; case board_M5StopWatch: title = "M5StopWatch"; break; + case board_M5ChainCaptain: title = "M5ChainCaptain"; break; case board_M5AtomS3: title = "M5AtomS3"; break; case board_M5AtomS3R: title = "M5AtomS3R"; break; case board_M5Dial: title = "M5Dial"; break; @@ -3179,6 +3319,7 @@ The usage of each pin is as follows. case board_M5Paper: case board_M5PaperS3: + case board_M5PaperDIY: w = 960; h = 540; pnl_cfg.offset_rotation = 3; @@ -3244,6 +3385,7 @@ The usage of each pin is as follows. break; case board_M5Dial: + case board_M5ChainCaptain: w = 240; h = 240; break; diff --git a/src/lgfx/boards.hpp b/src/lgfx/boards.hpp index 6abf4df..8bb8a0a 100644 --- a/src/lgfx/boards.hpp +++ b/src/lgfx/boards.hpp @@ -42,6 +42,7 @@ namespace lgfx // This should not be changed to "m5gfx" , board_M5CoreP4X = 31 , board_M5ChainCaptain = 32 , board_M5ToughC5 = 33 + , board_M5PaperDIY = 34 /// non display boards , board_M5AtomLite = 128 diff --git a/src/lgfx/utility/pgmspace.h b/src/lgfx/utility/pgmspace.h index 7c12ff3..cc8523d 100644 --- a/src/lgfx/utility/pgmspace.h +++ b/src/lgfx/utility/pgmspace.h @@ -61,7 +61,14 @@ Original Source: #endif #if !defined ( pgm_read_3byte_unaligned ) - #define pgm_read_3byte_unaligned(addr) (pgm_read_dword_unaligned(addr) & 0xFFFFFFu) + #if defined ( pgm_read_dword_with_offset ) + #define pgm_read_3byte_unaligned(addr) (pgm_read_dword_unaligned(addr) & 0xFFFFFFu) + #else + #define pgm_read_3byte_unaligned(addr) (uint32_t) \ + ( (uint32_t)pgm_read_byte((const uint8_t *)(addr) ) \ + | (uint32_t)pgm_read_byte((const uint8_t *)(addr) + 1) << 8 \ + | (uint32_t)pgm_read_byte((const uint8_t *)(addr) + 2) << 16 ) + #endif #endif #if defined ( ESP8266 ) || defined (__SAMD21__) || defined(__SAMD21G18A__) || defined(__SAMD21J18A__) || defined(__SAMD21E17A__) || defined(__SAMD21E18A__) || defined(ARDUINO_ARCH_MBED_RP2040) || defined(ARDUINO_ARCH_RP2040) || defined(USE_PICO_SDK) diff --git a/src/lgfx/v1/LGFX_Sprite.cpp b/src/lgfx/v1/LGFX_Sprite.cpp index ecdb12d..6fb91ff 100644 --- a/src/lgfx/v1/LGFX_Sprite.cpp +++ b/src/lgfx/v1/LGFX_Sprite.cpp @@ -709,14 +709,15 @@ namespace lgfx setColorDepth(bpp < 32 ? bpp : 24); uint32_t w = bmpdata.biWidth; int32_t h = bmpdata.biHeight; // bcHeight Image height (pixels) - if (!createSprite(w, h)) return false; //If the value of Height is positive, the image data is from bottom to top //If the value of Height is negative, the image data is from top to bottom. - int32_t flow = (h < 0) ? 1 : -1; - int32_t y = 0; - if (h < 0) h = -h; - else y = h - 1; + if (h == INT32_MIN) return false; + bool top_down = h < 0; + if (top_down) h = -h; + if (!createSprite(w, h)) return false; + int32_t flow = top_down ? 1 : -1; + int32_t y = top_down ? 0 : h - 1; if (bpp <= 8) { if (!_palette) createPalette(); diff --git a/src/lgfx/v1/gitTagVersion.h b/src/lgfx/v1/gitTagVersion.h index b39408d..b392faf 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 25 +#define LGFX_VERSION_PATCH 26 #define LOVYANGFX_VERSION F( LGFX_VERSION_MAJOR "." LGFX_VERSION_MINOR "." LGFX_VERSION_PATCH ) diff --git a/src/lgfx/v1/misc/pixelcopy.hpp b/src/lgfx/v1/misc/pixelcopy.hpp index 9c738d4..368bb52 100644 --- a/src/lgfx/v1/misc/pixelcopy.hpp +++ b/src/lgfx/v1/misc/pixelcopy.hpp @@ -114,6 +114,8 @@ namespace lgfx : copy_rgb_affine) : (dst_depth == grayscale_8bit) ? copy_rgb_affine : (dst_depth == rgb565_nonswapped) ? copy_rgb_affine + : (dst_depth == argb8888_nonswapped) ? copy_rgb_affine + : (dst_depth == argb8888_4Byte) ? copy_rgb_affine : nullptr; } diff --git a/src/lgfx/v1/panel/Panel_SSD1677.cpp b/src/lgfx/v1/panel/Panel_SSD1677.cpp index 508a054..ce7f63e 100644 --- a/src/lgfx/v1/panel/Panel_SSD1677.cpp +++ b/src/lgfx/v1/panel/Panel_SSD1677.cpp @@ -65,23 +65,23 @@ namespace lgfx //-------------------------------------------------------------------------- static constexpr uint8_t lut_fastest[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 + 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 - 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, - 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 - 0x17, 0x41, 0xA8, 0x32, 0x30, // VGH, VSH1, VSH2, VSL, 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). @@ -126,6 +126,49 @@ namespace lgfx 0x17, 0x41, 0xA8, 0x32, 0x30, // VGH, VSH1, VSH2, VSL, 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 + 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, + 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, + 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, + }; + // Write a 110-byte LUT: 105 waveform bytes -> 0x32, voltages -> 0x03/0x04/0x2C. static void send_lut(IBus* bus, const uint8_t* lut) { @@ -149,10 +192,7 @@ namespace lgfx _epd_mode = epd_mode_t::epd_quality; } - Panel_SSD1677::~Panel_SSD1677(void) - { - if (_prev_buf) { heap_free(_prev_buf); _prev_buf = nullptr; } - } + Panel_SSD1677::~Panel_SSD1677(void) = default; color_depth_t Panel_SSD1677::setColorDepth(color_depth_t depth) { @@ -552,14 +592,22 @@ namespace lgfx } while (++y < h); } - bool Panel_SSD1677::_wait_busy(uint32_t timeout) + bool Panel_SSD1677::_wait_busy(uint32_t timeout, bool enforce_refresh_minimum) { _bus->wait(); + // BUSY is not guaranteed to assert in the same instant that the command + // transfer completes. Sampling it immediately can therefore mistake the + // pre-activation LOW level for completion and return before the waveform + // has even started. + delay(2); if (_cfg.pin_busy >= 0 && gpio_in(_cfg.pin_busy)) { uint32_t start_time = millis(); - uint32_t delay_msec = _refresh_msec - (start_time - _send_msec); - if (delay_msec && delay_msec < timeout) { delay(delay_msec); } + if (enforce_refresh_minimum) + { + uint32_t delay_msec = _refresh_msec - (start_time - _send_msec); + if (delay_msec && delay_msec < timeout) { delay(delay_msec); } + } do { if (millis() - start_time > timeout) { return false; } @@ -612,23 +660,442 @@ namespace lgfx // Panel_SSD1677_4Gray //========================================================================== - bool Panel_SSD1677_4Gray::_ensure_prev_buf(void) + Panel_SSD1677_4Gray::~Panel_SSD1677_4Gray(void) + { + if (_displayed_buf) { heap_free(_displayed_buf); } + } + + bool Panel_SSD1677_4Gray::init(bool use_reset) + { + bool result = Panel_SSD1677::init(use_reset); + _invalidate_gray_state(); + if (result) + { + if (_displayed_buf) { heap_free(_displayed_buf); } + _displayed_buf = static_cast(heap_alloc_psram(_get_buffer_length())); + if (!_displayed_buf) { return false; } + memset(_displayed_buf, 0xFF, _get_buffer_length()); + _displayed_valid = false; + // _after_wake() performs a software reset and establishes the first + // (non-swapped) SSD1677 RAM face. + _mode2_face_odd = false; + _mode2_face_known = true; + } + return result; + } + + void Panel_SSD1677_4Gray::_invalidate_gray_state(void) + { + _optical_state = optical_state_t::unknown; + _mode2_face_odd = false; + _mode2_face_known = false; + _displayed_valid = false; + } + + void Panel_SSD1677_4Gray::_clear_modified_range(void) { - if (_prev_buf) { return true; } - size_t len = _get_plane_length() * 2; - _prev_buf = (uint8_t*)heap_alloc_psram(len); - if (!_prev_buf) { _prev_buf = (uint8_t*)heap_alloc(len); } - if (!_prev_buf) { return false; } - memset(_prev_buf, 0xFF, len); // white baseline - _prev_valid = false; + _range_mod.top = INT16_MAX; + _range_mod.left = INT16_MAX; + _range_mod.right = 0; + _range_mod.bottom = 0; + } + + range_rect_t Panel_SSD1677_4Gray::_full_range(void) const + { + range_rect_t result; + result.left = 0; + result.top = 0; + result.right = _cfg.panel_width - 1; + result.bottom = _cfg.panel_height - 1; + return result; + } + + void Panel_SSD1677_4Gray::_send_gray_lut(const uint8_t* lut) + { + send_lut(_bus, lut); + } + + void Panel_SSD1677_4Gray::_remember_displayed(const uint8_t* lsb, + const uint8_t* msb) + { + if (!_displayed_buf) { return; } + memcpy(_displayed_buf, lsb, _buf_x1_len); + memcpy(_displayed_buf + _buf_x1_len, msb, _buf_x1_len); + _displayed_valid = true; + } + + static uint8_t transition_group(uint8_t old_level, uint8_t new_level, + uint8_t step, bool monochrome) + { + 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; + } + + 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 auto full = _full_range(); + const uint32_t row_bytes = ((_cfg.panel_width + 7) & ~7) >> 3; + const uint8_t* old_lsb = _displayed_buf; + const uint8_t* old_msb = _displayed_buf + _buf_x1_len; + uint8_t row[128]; + + _set_ram_area(full.left, full.top, full.right + 1, full.bottom + 1); + _bus->writeCommand(command, 8); + 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); + if (y >= dirty.top && y <= dirty.bottom) + { + for (int32_t x = dirty.left; x <= dirty.right; ++x) + { + 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, + step, monochrome); + 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); } + } + } + _bus->writeBytes(row, row_bytes, true, false); + } + } + + 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) + { + _bus->writeCommand(CMD_DISP_UPDATE_CTRL1, 8); + _bus->writeData(ctrl1, 8); + if (!_screen_on) { ctrl2 |= 0xC0; } + _bus->writeCommand(CMD_DISP_UPDATE_CTRL2, 8); + _bus->writeData(ctrl2, 8); + _bus->writeCommand(CMD_MASTER_ACTIVATION, 8); + _send_msec = millis(); + if (!_wait_busy(10000, enforce_refresh_minimum)) + { + _invalidate_gray_state(); + return false; + } + _screen_on = !powers_down; + if (mode2_activation) + { + if (_mode2_face_known) { _mode2_face_odd = !_mode2_face_odd; } + } + else + { + _mode2_face_odd = false; + _mode2_face_known = true; + } + return true; + } + + bool Panel_SSD1677_4Gray::_reset_controller_for_mode2(void) + { + _bus->writeCommand(0x12, 8); + _send_msec = millis(); + if (!_wait_busy(5000)) { return false; } + for (uint8_t i = 0; auto cmds = getInitCommands(i); ++i) + { + if (!_wait_busy(5000)) { return false; } + command_list(cmds); + } + const auto full = _full_range(); + _set_ram_area(full.left, full.top, full.right + 1, full.bottom + 1); + _bus->writeCommand(0x46, 8); + _bus->writeData(0xF7, 8); + if (!_wait_busy(5000)) { return false; } + _bus->writeCommand(0x47, 8); + _bus->writeData(0xF7, 8); + if (!_wait_busy(5000)) { return false; } + _screen_on = false; + _mode2_face_odd = false; + _mode2_face_known = true; + return true; + } + + bool Panel_SSD1677_4Gray::_refresh_quality(const uint8_t* lsb, const uint8_t* msb) + { + // 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. + 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. + 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; } + _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; + for (int32_t y = dirty.top; y <= dirty.bottom; ++y) + { + for (int32_t x = dirty.left; x <= dirty.right; ++x) + { + 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); + } + } + } + } + + 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; } - void Panel_SSD1677_4Gray::_store_prev(void) + bool Panel_SSD1677_4Gray::_refresh_fastest(const uint8_t* lsb, const uint8_t* msb, + const range_rect_t& current_dirty) { - if (!_prev_buf) { return; } - memcpy(_prev_buf, _buf, _get_plane_length() * 2); - _prev_valid = true; + if (!_displayed_valid) + { + return _refresh_quality(lsb, msb); + } + + if (!_activate_transition_step(lsb, msb, current_dirty, 2, true, true)) + { + return false; + } + + _optical_state = optical_state_t::mono_synchronized; + _remember_mono_dirty(msb, current_dirty); + return true; } void Panel_SSD1677_4Gray::display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h) @@ -645,60 +1112,44 @@ namespace lgfx if (_range_mod.empty()) { return; } auto mode = getEpdMode(); - - // 4-gray always refreshes the full screen. - range_rect_t full; - full.left = 0; full.top = 0; - full.right = _cfg.panel_width - 1; full.bottom = _cfg.panel_height - 1; + const auto current_dirty = _range_mod; const uint8_t* planeL = _buf; const uint8_t* planeM = &_buf[_buf_x1_len]; startWrite(); - // Pick RAM source planes + LUT per mode. All use the factory absolute path - // (Display Mode 1, self-contained 0xC7). - // quality / text : 4-level, factory_quality (cleanest, slow) - // fast : 4-level, factory_fast - // fastest : 1-bit B/W. lut_fastest (differential) cannot drive - // full white<->black transitions, so fastest binarizes: - // feed planeM (v>=2 -> white) to BOTH RAMs, making the - // 2-bit value 00 (black) or 11 (white) only. - const uint8_t* bw_src = planeL; // 4-level: LSB -> BW, MSB -> RED - const uint8_t* red_src = planeM; - const uint8_t* lut = lut_factory_quality; - if (mode == epd_mode_t::epd_fastest) + bool success = false; + switch (mode) { - lut = lut_fastest; - } else - if (mode == epd_mode_t::epd_fast) - { - lut = lut_factory_fast; + case epd_mode_t::epd_quality: + success = _refresh_quality(planeL, planeM); + break; + case epd_mode_t::epd_text: + success = _refresh_text(planeL, planeM); + break; + case epd_mode_t::epd_fast: + success = _refresh_fast(planeL, planeM, current_dirty); + break; + case epd_mode_t::epd_fastest: + success = _refresh_fastest(planeL, planeM, current_dirty); + break; + default: + success = false; + break; } - // The factory LUT groups are reversed in brightness on this panel - // (HW group 00=white .. 11=black, opposite of the datasheet comment), so - // send the complement of both planes to get a correctly-oriented image. - _send_plane(CMD_WRITE_RAM_BW, bw_src, full, true); - _send_plane(CMD_WRITE_RAM_RED, red_src, full, true); - - send_lut(_bus, lut); - _wait_busy(); - _bus->writeCommand(CMD_DISP_UPDATE_CTRL1, 8); - _bus->writeData(CTRL1_NORMAL, 8); - _bus->writeCommand(CMD_DISP_UPDATE_CTRL2, 8); - _bus->writeData(0xC7, 8); // Mode 1, self-contained power cycle (powers down after) - _bus->writeCommand(CMD_MASTER_ACTIVATION, 8); - _send_msec = millis(); - _wait_busy(); - _screen_on = false; - - _initialize_seq = false; - _last_epd_mode = mode; - _range_mod.top = INT16_MAX; - _range_mod.left = INT16_MAX; - _range_mod.right = 0; - _range_mod.bottom = 0; + if (success) + { + if (mode == epd_mode_t::epd_quality || mode == epd_mode_t::epd_text) + { + _remember_displayed(planeL, planeM); + } + _initialize_seq = false; + _last_epd_mode = mode; + _range_old = current_dirty; + _clear_modified_range(); + } endWrite(); } diff --git a/src/lgfx/v1/panel/Panel_SSD1677.hpp b/src/lgfx/v1/panel/Panel_SSD1677.hpp index c178849..14bb3e2 100644 --- a/src/lgfx/v1/panel/Panel_SSD1677.hpp +++ b/src/lgfx/v1/panel/Panel_SSD1677.hpp @@ -41,8 +41,8 @@ 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/fast : factory absolute LUT (planes sent as-is) - fastest : differential vs prev (lut_grayscale codes) + quality/text : absolute four-gray LUT (planes sent as-is) + fast/fastest : monochrome differential LUT vs displayed history */ struct Panel_SSD1677 : public Panel_HasBuffer { @@ -82,15 +82,10 @@ namespace lgfx bool _initialize_seq; bool _screen_on = false; - // Software "previous frame" planes (prevL, prevM). Allocated lazily and used - // only by the 4-gray differential (fastest) path; B/W and factory modes don't - // need it (B/W relies on the controller's RED RAM as the previous frame). - uint8_t* _prev_buf = nullptr; - size_t _get_buffer_length(void) const override; uint32_t _get_plane_length(void) const; - bool _wait_busy(uint32_t timeout = 4096); + bool _wait_busy(uint32_t timeout = 4096, bool enforce_refresh_minimum = true); void _draw_pixel(uint_fast16_t x, uint_fast16_t y, uint32_t value); uint8_t _read_pixel(uint_fast16_t x, uint_fast16_t y); void _update_transferred_rect(uint_fast16_t &xs, uint_fast16_t &ys, uint_fast16_t &xe, uint_fast16_t &ye); @@ -125,14 +120,45 @@ namespace lgfx struct Panel_SSD1677_4Gray : public Panel_SSD1677 { + ~Panel_SSD1677_4Gray(void) override; + bool init(bool use_reset) override; void display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h) override; protected: - bool _prev_valid = false; // _prev_buf holds a meaningful previous frame - epd_mode_t _last_gray_mode = (epd_mode_t)~0u; - - bool _ensure_prev_buf(void); - void _store_prev(void); // copy current _buf planes into _prev_buf + enum class optical_state_t : uint8_t + { + unknown, + gray4, + mono_synchronized, + }; + + optical_state_t _optical_state = optical_state_t::unknown; + bool _mode2_face_odd = false; + bool _mode2_face_known = false; + uint8_t* _displayed_buf = nullptr; + bool _displayed_valid = false; + + void _invalidate_gray_state(void); + void _clear_modified_range(void); + 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, 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); + 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_fastest(const uint8_t* lsb, const uint8_t* msb, const range_rect_t& dirty); }; //----------------------------------------------------------------------------