From 605030a551a4622c0f1234d7e55747d855ef0d3b Mon Sep 17 00:00:00 2001 From: AdaSzi115026 <81822538+AdaSzi@users.noreply.github.com> Date: Wed, 21 Oct 2026 21:23:54 +0200 Subject: [PATCH 1/6] T5S3 e-paper: non-blocking drivers in the PaperS3 style - Split the board signalling into t5s3_epd_board.cpp, shared by both drivers - lilygo,t5s3-display (default): draw_bitmap() no longer waits for the panel. Changes are shown at once with a black and white dither, each changed area gets its 16 gray levels 3 s after its last change. Only changed pixels are driven, refresh() no longer blocks touch input - lilygo,t5s3-display-bw (new): black and white pixel state machine like the PaperS3 driver --- .../lilygo-t5-epd47-s3/lilygo,t5-epd47-s3.dts | 4 +- Drivers/lilygo-t5s3-epd-module/CMakeLists.txt | 2 + Drivers/lilygo-t5s3-epd-module/README.md | 27 +- .../bindings/lilygo,t5s3-display-bw.yaml | 12 + .../include/bindings/t5s3_display_bw.h | 15 + .../include/drivers/t5s3_display_bw.h | 16 + .../lilygo-t5s3-epd-module/source/module.cpp | 2 + .../source/t5s3_display.cpp | 530 ++++++++---- .../source/t5s3_display_bw.cpp | 534 ++++++++++++ .../source/t5s3_epd.cpp | 788 +++++++----------- .../lilygo-t5s3-epd-module/source/t5s3_epd.h | 44 +- .../source/t5s3_epd_board.cpp | 433 ++++++++++ .../source/t5s3_epd_board.h | 39 + .../source/t5s3_epd_rows.h | 22 +- 14 files changed, 1789 insertions(+), 679 deletions(-) create mode 100644 Drivers/lilygo-t5s3-epd-module/bindings/lilygo,t5s3-display-bw.yaml create mode 100644 Drivers/lilygo-t5s3-epd-module/include/bindings/t5s3_display_bw.h create mode 100644 Drivers/lilygo-t5s3-epd-module/include/drivers/t5s3_display_bw.h create mode 100644 Drivers/lilygo-t5s3-epd-module/source/t5s3_display_bw.cpp create mode 100644 Drivers/lilygo-t5s3-epd-module/source/t5s3_epd_board.cpp create mode 100644 Drivers/lilygo-t5s3-epd-module/source/t5s3_epd_board.h diff --git a/Devices/lilygo-t5-epd47-s3/lilygo,t5-epd47-s3.dts b/Devices/lilygo-t5-epd47-s3/lilygo,t5-epd47-s3.dts index c38777838..6bb347af3 100644 --- a/Devices/lilygo-t5-epd47-s3/lilygo,t5-epd47-s3.dts +++ b/Devices/lilygo-t5-epd47-s3/lilygo,t5-epd47-s3.dts @@ -11,7 +11,7 @@ #include #include #include -#include +#include / { compatible = "root"; @@ -32,7 +32,7 @@ // Listed before the I2C bus on purpose, the touch controller was only tested with the display started first display { - compatible = "lilygo,t5s3-display"; + compatible = "lilygo,t5s3-display-bw"; }; i2c_internal { diff --git a/Drivers/lilygo-t5s3-epd-module/CMakeLists.txt b/Drivers/lilygo-t5s3-epd-module/CMakeLists.txt index 14415ac4b..0a357f317 100644 --- a/Drivers/lilygo-t5s3-epd-module/CMakeLists.txt +++ b/Drivers/lilygo-t5s3-epd-module/CMakeLists.txt @@ -13,6 +13,8 @@ tactility_add_module(lilygo-t5s3-epd-module # The pixel loops of the display are too slow with the debug optimization level of the project set_source_files_properties( source/t5s3_epd.cpp + source/t5s3_epd_board.cpp + source/t5s3_display_bw.cpp source/t5s3_display.cpp PROPERTIES COMPILE_OPTIONS -O2 ) diff --git a/Drivers/lilygo-t5s3-epd-module/README.md b/Drivers/lilygo-t5s3-epd-module/README.md index ad0f1239c..805ad3d02 100644 --- a/Drivers/lilygo-t5s3-epd-module/README.md +++ b/Drivers/lilygo-t5s3-epd-module/README.md @@ -1,28 +1,37 @@ # lilygo-t5s3-epd-module -A kernel driver for the 4.7" 960x540 `ED047TC1` e-paper panel of the LILYGO T5 4.7 Inch E-Paper S3 (V2.3/V2.4). +Kernel drivers for the 4.7" 960x540 `ED047TC1` e-paper panel of the LILYGO T5 4.7 Inch E-Paper S3 (V2.3/V2.4). -The driver only works on this board. The pins of the panel, the 74HCT4094 shift register that carries the panel supplies and the sequence of the panel are fixed in the code, so the devicetree node only has the pixel clock as property. +The drivers only work on this board. The pins of the panel, the 74HCT4094 shift register that carries the panel supplies and the sequence of the panel are fixed in the code, so the devicetree node only has the pixel clock as property. -- `t5s3_display.cpp`: the Tactility display driver. It decides between fast and quality updates. -- `t5s3_epd.cpp`: the engine. It controls the shift register, the CKV gate clock (RMT peripheral), the row transfer (LCD peripheral in i80 mode), the update loop and the clear sequence. +There are two drivers. A devicetree uses one of them, the device `lilygo-t5-epd47-s3` uses the black and white one: + +- `lilygo,t5s3-display` (`t5s3_display.cpp`): 16 gray levels. `draw_bitmap()` returns without waiting for the panel. A task shows each change at once with a black and white dither and draws the gray levels of a changed area once it had no change for 3 seconds. Only the pixels that need it are driven, so there is no flash of the whole screen. +- `lilygo,t5s3-display-bw` (`t5s3_display_bw.cpp`): black and white with a pixel state machine instead of waveform tables, like the M5Stack PaperS3 driver. Gray levels are dithered. It is the fastest and does not flash outside of `clear()` and `refresh()`. + +Shared and internal files: + +- `t5s3_epd_board.cpp`: the signalling of the board. It controls the shift register, the CKV gate clock (RMT peripheral) and the row transfer (LCD peripheral in i80 mode), and sends one frame at a time. +- `t5s3_epd.cpp`: the updates of the grayscale driver: the fast black and white update and the refresh with the waveform. - `t5s3_epd_rows.h`: the conversion of framebuffer rows into bus data. It has no hardware access. -- `t5s3_epd_waveform.h`: the 30 phase drive table of the quality updates. +- `t5s3_epd_waveform.h`: the 30 phase drive table for 16 gray levels. -There are three update modes. Fast draws black and white only. Quality draws 16 levels and leaves white pixels that stay white alone, which is used for partial updates. Full also flashes those white pixels, which is used for the clear and the refresh. +Frames are padded to a fixed period, because a pixel keeps its drive until its row is addressed again. The frame counts and the gray levels in the code were measured with a camera on the panel. ## License -The engine and its tables are derived from [epdiy](https://github.com/vroland/epdiy) and are licensed under the +The board signalling, the update logic of the grayscale driver and the waveform are derived from [epdiy](https://github.com/vroland/epdiy) and are licensed under the [LGPL v3.0 or later](LICENSE-LGPL-3.0.md) (the LGPL supplements the [GPL v3.0](../../Documentation/LICENSE-GPL-3.0.md)): - `source/t5s3_epd.cpp` - `source/t5s3_epd.h` +- `source/t5s3_epd_board.cpp` +- `source/t5s3_epd_board.h` - `source/t5s3_epd_rows.h` - `source/t5s3_epd_waveform.h` These files follow the sequences and timings of epdiy and contain its ED047TC1 waveform data. Each of them has an `SPDX-License-Identifier: LGPL-3.0-or-later` header. -The rest of the module (`source/t5s3_display.cpp`, `source/module.cpp`, the bindings and the headers in `include/`) is licensed under [Apache License v2.0](LICENSE-Apache-2.0.md). +The rest of the module (`source/t5s3_display.cpp`, `source/t5s3_display_bw.cpp`, `source/module.cpp`, the bindings and the headers in `include/`) is licensed under [Apache License v2.0](LICENSE-Apache-2.0.md). The pixel state machine in `t5s3_display_bw.cpp` follows the M5Stack PaperS3 display driver of Tactility. -A project that contains this module has to follow the LGPL for the four files above. +A project that contains this module has to follow the LGPL for the files above. diff --git a/Drivers/lilygo-t5s3-epd-module/bindings/lilygo,t5s3-display-bw.yaml b/Drivers/lilygo-t5s3-epd-module/bindings/lilygo,t5s3-display-bw.yaml new file mode 100644 index 000000000..f7263c7b7 --- /dev/null +++ b/Drivers/lilygo-t5s3-epd-module/bindings/lilygo,t5s3-display-bw.yaml @@ -0,0 +1,12 @@ +description: > + LILYGO T5 4.7 Inch E-Paper S3 (V2.3/V2.4) display with an ED047TC1 panel, driven in black and white with a pixel + state machine instead of waveform tables. Updates are fast and do not block. Gray levels are dithered. + The pins of the panel are fixed in the driver, so this node only has the pixel clock as property. + +compatible: "lilygo,t5s3-display-bw" + +properties: + pixel-clock-hz: + type: int + default: 10000000 + description: Clock of the bus that sends the pixel data of a row. Faster speeds up updates if the panel accepts it. diff --git a/Drivers/lilygo-t5s3-epd-module/include/bindings/t5s3_display_bw.h b/Drivers/lilygo-t5s3-epd-module/include/bindings/t5s3_display_bw.h new file mode 100644 index 000000000..de9ce9c7b --- /dev/null +++ b/Drivers/lilygo-t5s3-epd-module/include/bindings/t5s3_display_bw.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +DEFINE_DEVICETREE(t5s3_display_bw, struct T5s3DisplayBwConfig) + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/lilygo-t5s3-epd-module/include/drivers/t5s3_display_bw.h b/Drivers/lilygo-t5s3-epd-module/include/drivers/t5s3_display_bw.h new file mode 100644 index 000000000..6d0b7797d --- /dev/null +++ b/Drivers/lilygo-t5s3-epd-module/include/drivers/t5s3_display_bw.h @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct T5s3DisplayBwConfig { + uint32_t pixel_clock_hz; +}; + +#ifdef __cplusplus +} +#endif diff --git a/Drivers/lilygo-t5s3-epd-module/source/module.cpp b/Drivers/lilygo-t5s3-epd-module/source/module.cpp index ac1344199..3ef5c8187 100644 --- a/Drivers/lilygo-t5s3-epd-module/source/module.cpp +++ b/Drivers/lilygo-t5s3-epd-module/source/module.cpp @@ -5,9 +5,11 @@ extern "C" { extern Driver t5s3_display_driver; +extern Driver t5s3_display_bw_driver; static Driver* const lilygo_t5s3_epd_drivers[] = { &t5s3_display_driver, + &t5s3_display_bw_driver, nullptr }; diff --git a/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp b/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp index b6ea56976..1eff78ed7 100644 --- a/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp +++ b/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp @@ -6,31 +6,51 @@ #include #include #include +#include #include -#include #include "t5s3_epd.h" -#include +#include +#include +#include +#include + +#include +#include #include #define TAG "T5s3Display" #define GET_CONFIG(device) (static_cast((device)->config)) -// An update covering at least this fraction of the panel is a full-screen change and gets a quality refresh -static constexpr float FULL_AREA_QUALITY_THRESHOLD = 0.6f; +// draw_bitmap() writes 16 gray levels into a staging buffer and returns. A task shows each change with a fast update +// that dithers the changed pixels to black and white. Once an area had no change for REFRESH_DELAY_MS, a refresh +// draws its gray levels and clears the ghosting. + +// Areas that wait for a refresh, more are merged +static constexpr int MAX_AREAS = 8; + +// A refresh takes more than a second and delays the changes that come during it, so it waits for a pause. +// Until then the changed areas show their dither. +static constexpr uint32_t REFRESH_DELAY_MS = 3000; + +// Brightness of the levels that look different, measured with a camera. Levels 10 to 14 look like white. +static constexpr int TONE_STEPS = 11; +static constexpr uint8_t TONE_LEVELS[TONE_STEPS] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 15 }; +static constexpr uint8_t TONE_BRIGHTNESS[TONE_STEPS] = { 14, 17, 26, 52, 81, 111, 129, 143, 151, 155, 158 }; -// Fast updates allowed before a quality refresh limits ghosting. A full-screen redraw takes about 10 updates, so this must be higher -static constexpr uint32_t QUALITY_REFRESH_PARTIAL_COUNT = 20; +// Darker luminance is drawn black. The edges of text then keep the weight that the dither of the fast update gives them. +static constexpr int BLACK_LUMINANCE = 64; -// Updates within this time after a quality update also use quality mode, so the tiles of one redraw stay consistent. -// It must stay well below the duration of a quality update. -static constexpr uint32_t QUALITY_HOLD_MS = 50; +static constexpr uint32_t IDLE_POWER_OFF_MS = 2000; -// Maximum time that consecutive quality updates keep extending the hold window -static constexpr uint32_t QUALITY_HOLD_SESSION_MAX_MS = 200; +static constexpr uint32_t CMD_UPDATE = 1U << 0; +static constexpr uint32_t CMD_CLEAR = 1U << 1; +static constexpr uint32_t CMD_REFRESH = 1U << 2; +static constexpr uint32_t CMD_POWER_OFF = 1U << 3; +static constexpr uint32_t CMD_EXIT = 1U << 4; -// 4x4 ordered (Bayer) dither thresholds, spread evenly across a 0-15 nibble range. +// 4x4 ordered (Bayer) dither thresholds static constexpr uint8_t BAYER_4X4[4][4] = { { 0, 8, 2, 10 }, { 12, 4, 14, 6 }, @@ -38,166 +58,366 @@ static constexpr uint8_t BAYER_4X4[4][4] = { { 15, 7, 13, 5 }, }; -// Dithers an 8-bit luminance (0x00 black to 0xFF white) to a 4-bit level (0x0 black to 0xF white) -static inline uint8_t dither_to_nibble(uint8_t luminance, int32_t x, int32_t y) { - // Scaling by 17 spreads the thresholds over one full quantization step - const uint32_t threshold = BAYER_4X4[y & 3][x & 3] * 17U; - const uint32_t level = (static_cast(luminance) * 15U + threshold) / 255U; - return static_cast(level > 15U ? 15U : level); +// A luminance lies between two levels of the panel and is dithered between them +struct Tone { + uint8_t darker; + uint8_t lighter; + // Share of the lighter level, 0 to 16 + uint8_t share; +}; + +static void build_tones(Tone* tones) { + for (int luminance = 0; luminance < 256; luminance++) { + if (luminance < BLACK_LUMINANCE) { + tones[luminance] = { 0, 0, 0 }; + continue; + } + const float brightness = TONE_BRIGHTNESS[0] + (TONE_BRIGHTNESS[TONE_STEPS - 1] - TONE_BRIGHTNESS[0]) * luminance / 255.0f; + int step = 0; + while (step < TONE_STEPS - 2 && brightness > TONE_BRIGHTNESS[step + 1]) { + step++; + } + const float share = (brightness - TONE_BRIGHTNESS[step]) / (TONE_BRIGHTNESS[step + 1] - TONE_BRIGHTNESS[step]); + tones[luminance].darker = TONE_LEVELS[step]; + tones[luminance].lighter = TONE_LEVELS[step + 1]; + tones[luminance].share = static_cast(std::clamp(static_cast(share * 16.0f + 0.5f), 0, 16)); + } +} + +// Share of white pixels, 0 to 16, that the black and white dither of a level gets +static constexpr uint8_t white_share(int level) { + for (int step = 0; step < TONE_STEPS; step++) { + if (TONE_LEVELS[step] == level) { + return static_cast((TONE_BRIGHTNESS[step] - TONE_BRIGHTNESS[0]) * 16 / (TONE_BRIGHTNESS[TONE_STEPS - 1] - TONE_BRIGHTNESS[0])); + } + } + // Unused levels look like white + return 16; } -// Binary variant for fast updates, which only draw black and white -static inline uint8_t dither_to_bw_nibble(uint8_t luminance, int32_t x, int32_t y) { - // Scaling by 16 keeps the highest threshold at 240, so pure white (0xFF) is never classified as black - const uint32_t threshold = BAYER_4X4[y & 3][x & 3] * 16U; - return luminance > threshold ? 0xF : 0x0; +static constexpr uint8_t WHITE_SHARES[16] = { + white_share(0), white_share(1), white_share(2), white_share(3), white_share(4), white_share(5), white_share(6), white_share(7), + white_share(8), white_share(9), white_share(10), white_share(11), white_share(12), white_share(13), white_share(14), white_share(15), +}; + +static inline uint8_t dither_to_level(const Tone& tone, int32_t x, int32_t y) { + return tone.share > BAYER_4X4[y & 3][x & 3] ? tone.lighter : tone.darker; } extern "C" { +// Rows [y0, y1) +struct Rows { + int y0, y1; + + bool empty() const { return y1 <= y0; } + + void merge(const Rows& other) { + if (other.empty()) return; + if (empty()) { + *this = other; + return; + } + y0 = std::min(y0, other.y0); + y1 = std::max(y1, other.y1); + } +}; + +static constexpr Rows NO_ROWS = { 0, 0 }; + struct T5s3DisplayInternal { - uint8_t* framebuffer; + TaskHandle_t task; + SemaphoreHandle_t request_mutex; + SemaphoreHandle_t request_done; + // Guards staging + SemaphoreHandle_t staging_mutex; + portMUX_TYPE lock; + volatile bool exited; bool powered; - // Fast updates since the last quality refresh - uint32_t partial_count_since_quality; - // Until this tick every update uses quality mode - TickType_t quality_hold_until_tick; - // Tick at which the current hold window started - TickType_t quality_hold_session_start_tick; + // Result of the last clear, set by the task + bool clear_succeeded; + // Rows changed in staging and not yet picked up by the task, guarded by lock + Rows pending; + // Time of the oldest change not yet shown and the longest draw_bitmap() call since, guarded by lock + int64_t pending_since_us; + int64_t draw_max_us; + // 4 bit levels written by draw_bitmap(), copied into the frame buffer of the panel by the task + uint8_t* staging; + // Panel levels for each luminance + Tone* tones; }; -static void power_on(T5s3DisplayInternal* internal) { - if (!internal->powered) { - t5s3_epd_power_on(); - internal->powered = true; +static inline void set_level(uint8_t* buffer, int32_t x, int32_t y, uint8_t level) { + uint8_t* byte = &buffer[y * T5S3_EPD_FB_ROW_BYTES + x / 2]; + if (x % 2) { + *byte = (*byte & 0x0F) | static_cast(level << 4); + } else { + *byte = (*byte & 0xF0) | (level & 0x0F); } } -// region DisplayApi +// region Task -static error_t t5s3_display_reset(Device* device) { - auto* internal = static_cast(device_get_driver_data(device)); - // The panel has no reset line, so a power cycle is the closest equivalent - t5s3_epd_power_off(); +static void power_on(T5s3DisplayInternal* internal) { + if (internal->powered) return; + t5s3_epd_board_power_on(); + internal->powered = true; +} + +static void power_off(T5s3DisplayInternal* internal) { + if (!internal->powered) return; + t5s3_epd_board_power_off(); internal->powered = false; - power_on(internal); - return ERROR_NONE; } -/** Initialization is done in start() */ -static error_t t5s3_display_init(Device*) { - return ERROR_NONE; +// Copies the rows into the frame buffer and their black and white dither into the dither buffer +static void copy_staging(T5s3DisplayInternal* internal, const Rows& rows) { + const size_t offset = rows.y0 * T5S3_EPD_FB_ROW_BYTES; + uint8_t* framebuffer = t5s3_epd_framebuffer(); + xSemaphoreTake(internal->staging_mutex, portMAX_DELAY); + memcpy(framebuffer + offset, internal->staging + offset, (rows.y1 - rows.y0) * T5S3_EPD_FB_ROW_BYTES); + xSemaphoreGive(internal->staging_mutex); + + uint8_t* dither = t5s3_epd_dither_buffer(); + for (int y = rows.y0; y < rows.y1; y++) { + const uint8_t* levels = framebuffer + y * T5S3_EPD_FB_ROW_BYTES; + uint8_t* out = dither + y * T5S3_EPD_FB_ROW_BYTES; + const uint8_t* thresholds = BAYER_4X4[y & 3]; + for (int i = 0; i < T5S3_EPD_FB_ROW_BYTES; i++) { + const int x = i * 2; + const uint8_t first = WHITE_SHARES[levels[i] & 0x0F] > thresholds[x & 3] ? 0x0F : 0x00; + const uint8_t second = WHITE_SHARES[levels[i] >> 4] > thresholds[(x + 1) & 3] ? 0xF0 : 0x00; + out[i] = first | second; + } + } } -/** Fills the panel white with one full update, without the flashing cycles of the initial clear */ -static error_t t5s3_display_clear(Device* device) { - auto* internal = static_cast(device_get_driver_data(device)); +static bool run_clear(T5s3DisplayInternal* internal) { power_on(internal); + xSemaphoreTake(internal->staging_mutex, portMAX_DELAY); + memset(internal->staging, 0xFF, T5S3_EPD_FB_BYTES); + xSemaphoreGive(internal->staging_mutex); t5s3_epd_fill_white(); - const bool success = t5s3_epd_update(T5s3EpdMode::Full, 0, T5S3_EPD_HEIGHT); - if (success) { - internal->partial_count_since_quality = 0; - internal->quality_hold_until_tick = 0; + const T5s3EpdRect panel = { 0, 0, T5S3_EPD_WIDTH, T5S3_EPD_HEIGHT }; + return t5s3_epd_refresh(&panel, 1); +} + +// Areas that got fast updates and still need a refresh for their gray levels +struct Areas { + T5s3EpdRect rects[MAX_AREAS]; + // Time of the last change of each area + int64_t changed_us[MAX_AREAS]; + int count; +}; + +static int64_t rect_size(const T5s3EpdRect& rect) { + return static_cast(rect.x1 - rect.x0) * (rect.y1 - rect.y0); +} + +static T5s3EpdRect rect_union(const T5s3EpdRect& a, const T5s3EpdRect& b) { + return { std::min(a.x0, b.x0), std::min(a.y0, b.y0), std::max(a.x1, b.x1), std::max(a.y1, b.y1) }; +} + +// Merges the area into the one whose union grows the least, unless it is far from all of them and there is room +static void add_area(Areas* areas, const T5s3EpdRect& rect, int64_t now_us) { + int best = -1; + int64_t best_growth = 0; + for (int i = 0; i < areas->count; i++) { + const T5s3EpdRect& other = areas->rects[i]; + const int64_t growth = rect_size(rect_union(other, rect)) - rect_size(other) - rect_size(rect); + if (best < 0 || growth < best_growth) { + best = i; + best_growth = growth; + } + } + if (best >= 0 && (best_growth <= 0 || areas->count == MAX_AREAS)) { + areas->rects[best] = rect_union(areas->rects[best], rect); + areas->changed_us[best] = now_us; + } else { + areas->rects[areas->count] = rect; + areas->changed_us[areas->count] = now_us; + areas->count++; } - return success ? ERROR_NONE : ERROR_RESOURCE; } -/** - * Redraws the current content with a quality update to remove ghosting. - * The update skips pixels that did not change, so every pixel is invalidated first. - */ -static error_t t5s3_display_refresh(Device* device) { - auto* internal = static_cast(device_get_driver_data(device)); - power_on(internal); - t5s3_epd_invalidate(); - const bool success = t5s3_epd_update(T5s3EpdMode::Full, 0, T5S3_EPD_HEIGHT); - if (success) { - internal->partial_count_since_quality = 0; - internal->quality_hold_until_tick = 0; +// Each area waits for its own pause, so changes elsewhere do not hold it back +static int64_t next_refresh_us(const Areas& areas) { + int64_t next_us = INT64_MAX; + for (int i = 0; i < areas.count; i++) { + next_us = std::min(next_us, areas.changed_us[i] + REFRESH_DELAY_MS * 1000LL); } - return success ? ERROR_NONE : ERROR_RESOURCE; + return next_us; } -// Decides between a quality refresh and a fast update, *out_within_hold tells if the hold window applied -static bool should_use_quality_mode(T5s3DisplayInternal* internal, int32_t width, int32_t height, bool* out_within_hold) { - const TickType_t now = get_ticks(); - - const uint32_t area = static_cast(width) * static_cast(height); - const bool is_full_screen_change = area >= static_cast( - static_cast(T5S3_EPD_WIDTH * T5S3_EPD_HEIGHT) * FULL_AREA_QUALITY_THRESHOLD - ); - const bool partial_count_exceeded = internal->partial_count_since_quality >= QUALITY_REFRESH_PARTIAL_COUNT; - // Signed difference stays correct across the tick counter wrap, 0 means no hold - const TickType_t hold_until = internal->quality_hold_until_tick; - const bool within_hold = hold_until != 0 && static_cast(hold_until - now) > 0; - *out_within_hold = within_hold; - - return is_full_screen_change || partial_count_exceeded || within_hold; +// Refreshes the areas that are due, or all of them +static void refresh_areas(Areas* areas, bool all) { + Areas due = {}; + Areas waiting = {}; + const int64_t now_us = esp_timer_get_time(); + for (int i = 0; i < areas->count; i++) { + Areas* target = all || areas->changed_us[i] + REFRESH_DELAY_MS * 1000LL <= now_us ? &due : &waiting; + target->rects[target->count] = areas->rects[i]; + target->changed_us[target->count] = areas->changed_us[i]; + target->count++; + } + if (due.count == 0) { + return; + } + // A failed refresh keeps its areas, so they are tried again + if (!t5s3_epd_refresh(due.rects, due.count)) { + return; + } + *areas = waiting; } -// Updates the counters after an update. A failed quality update leaves them unchanged and a failed fast update still counts. -// A quality update starts or extends the hold window, up to QUALITY_HOLD_SESSION_MAX_MS. -static void commit_quality_mode_decision(T5s3DisplayInternal* internal, bool used_quality, bool draw_succeeded, bool was_within_hold) { - if (used_quality) { - if (draw_succeeded) { - internal->partial_count_since_quality = 0; - const TickType_t now = get_ticks(); - if (!was_within_hold) { - internal->quality_hold_session_start_tick = now; - } - const TickType_t session_elapsed = now - internal->quality_hold_session_start_tick; - if (session_elapsed < millis_to_ticks(QUALITY_HOLD_SESSION_MAX_MS)) { - internal->quality_hold_until_tick = now + millis_to_ticks(QUALITY_HOLD_MS); - } else { - internal->quality_hold_until_tick = 0; +static void display_task(void* arg) { + auto* internal = static_cast(arg); + Areas areas = {}; + + while (true) { + uint32_t commands = 0; + TickType_t timeout = portMAX_DELAY; + if (areas.count > 0) { + const int64_t wait_us = std::max(0, next_refresh_us(areas) - esp_timer_get_time()); + timeout = pdMS_TO_TICKS(wait_us / 1000 + 1); + } else if (internal->powered) { + timeout = pdMS_TO_TICKS(IDLE_POWER_OFF_MS); + } + const bool notified = xTaskNotifyWait(0, UINT32_MAX, &commands, timeout) == pdTRUE; + + if (commands & CMD_EXIT) { + break; + } + + if (commands & CMD_POWER_OFF) { + power_off(internal); + } + + if (commands & CMD_CLEAR) { + // The clear draws all rows, so earlier changes are included + portENTER_CRITICAL(&internal->lock); + internal->pending = NO_ROWS; + internal->pending_since_us = 0; + internal->draw_max_us = 0; + portEXIT_CRITICAL(&internal->lock); + internal->clear_succeeded = run_clear(internal); + areas.count = 0; + xSemaphoreGive(internal->request_done); + } + + // A refresh of the whole panel only drives the pixels that do not show their level + const bool refresh_all = (commands & CMD_REFRESH) != 0; + if (refresh_all) { + areas.rects[0] = { 0, 0, T5S3_EPD_WIDTH, T5S3_EPD_HEIGHT }; + areas.changed_us[0] = esp_timer_get_time(); + areas.count = 1; + } + + Rows pending; + int64_t pending_since_us; + int64_t draw_max_us; + portENTER_CRITICAL(&internal->lock); + pending = internal->pending; + pending_since_us = internal->pending_since_us; + draw_max_us = internal->draw_max_us; + internal->pending = NO_ROWS; + internal->pending_since_us = 0; + internal->draw_max_us = 0; + portEXIT_CRITICAL(&internal->lock); + + if (!pending.empty()) { + copy_staging(internal, pending); + power_on(internal); + T5s3EpdRect changed; + if (t5s3_epd_show_changes(pending.y0, pending.y1, &changed) && changed.x1 > changed.x0) { + add_area(&areas, changed, esp_timer_get_time()); } + LOG_D(TAG, "Rows %d to %d shown after %d ms (draw_bitmap up to %d us)", pending.y0, pending.y1, + static_cast((esp_timer_get_time() - pending_since_us) / 1000), static_cast(draw_max_us)); + } else if (areas.count > 0 && (refresh_all || next_refresh_us(areas) <= esp_timer_get_time())) { + power_on(internal); + refresh_areas(&areas, refresh_all); + } else if (!notified && areas.count == 0) { + power_off(internal); } - } else { - internal->partial_count_since_quality++; } + + power_off(internal); + internal->exited = true; + vTaskDelete(nullptr); +} + +static error_t clear_blocking(T5s3DisplayInternal* internal) { + xSemaphoreTake(internal->request_mutex, portMAX_DELAY); + xTaskNotify(internal->task, CMD_CLEAR, eSetBits); + xSemaphoreTake(internal->request_done, portMAX_DELAY); + const bool success = internal->clear_succeeded; + xSemaphoreGive(internal->request_mutex); + return success ? ERROR_NONE : ERROR_RESOURCE; +} + +// endregion + +// region DisplayApi + +static error_t t5s3_display_reset(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + // The panel has no reset line, it powers up again with the next update + xTaskNotify(internal->task, CMD_POWER_OFF, eSetBits); + return ERROR_NONE; +} + +/** Initialization is done in start() */ +static error_t t5s3_display_init(Device*) { + return ERROR_NONE; +} + +/** Fills the panel white, without the flashing cycles of the initial clear */ +static error_t t5s3_display_clear(Device* device) { + return clear_blocking(static_cast(device_get_driver_data(device))); +} + +/** Draws the gray levels of all pixels that show a dither now, without waiting for it */ +static error_t t5s3_display_refresh(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + xTaskNotify(internal->task, CMD_REFRESH, eSetBits); + return ERROR_NONE; } // Reports GRAYSCALE8 so that draw_bitmap() is called per changed tile static error_t t5s3_display_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) { auto* internal = static_cast(device_get_driver_data(device)); - - const int32_t width = x_end - x_start; - const int32_t height = y_end - y_start; - bool within_hold = false; - const bool use_quality = should_use_quality_mode(internal, width, height, &within_hold); + const int64_t start_us = esp_timer_get_time(); // color_data is 8-bit luminance (0x00 black to 0xFF white) and the panel wants 4-bit levels (0x0 black to 0xF white) - // Dithering uses 16 levels for quality updates and 2 levels for fast updates + // Level 15 is white, levels 10 to 14 are not used const auto* src = static_cast(color_data); - const size_t src_stride = static_cast(width); - - for (int32_t row = 0; row < height; row++) { - const uint8_t* src_row = src + static_cast(row) * src_stride; - const int32_t display_y = y_start + row; - - for (int32_t col = 0; col < width; col++) { - const int32_t display_x = x_start + col; - const uint8_t nibble = use_quality - ? dither_to_nibble(src_row[col], display_x, display_y) - : dither_to_bw_nibble(src_row[col], display_x, display_y); - t5s3_epd_set_pixel(internal->framebuffer, display_x, display_y, nibble); + const int32_t width = x_end - x_start; + xSemaphoreTake(internal->staging_mutex, portMAX_DELAY); + for (int32_t y = y_start; y < y_end; y++) { + const uint8_t* src_row = src + (y - y_start) * width; + for (int32_t x = x_start; x < x_end; x++) { + set_level(internal->staging, x, y, dither_to_level(internal->tones[src_row[x - x_start]], x, y)); } } + xSemaphoreGive(internal->staging_mutex); - power_on(internal); - const bool success = t5s3_epd_update(use_quality ? T5s3EpdMode::Quality : T5s3EpdMode::Fast, y_start, y_end); - - commit_quality_mode_decision(internal, use_quality, success, within_hold); - return success ? ERROR_NONE : ERROR_RESOURCE; + const int64_t draw_us = esp_timer_get_time() - start_us; + portENTER_CRITICAL(&internal->lock); + internal->pending.merge({ y_start, y_end }); + if (internal->pending_since_us == 0) { + internal->pending_since_us = start_us; + } + internal->draw_max_us = std::max(internal->draw_max_us, draw_us); + portEXIT_CRITICAL(&internal->lock); + xTaskNotify(internal->task, CMD_UPDATE, eSetBits); + return ERROR_NONE; } static error_t t5s3_display_disp_on_off(Device* device, bool on_off) { auto* internal = static_cast(device_get_driver_data(device)); - if (on_off) { - power_on(internal); - } else if (internal->powered) { - t5s3_epd_power_off(); - internal->powered = false; + if (!on_off) { + xTaskNotify(internal->task, CMD_POWER_OFF, eSetBits); } return ERROR_NONE; } @@ -217,7 +437,7 @@ static uint16_t t5s3_display_get_resolution_y(Device*) { // endregion static const DisplayApi t5s3_display_api = { - // The driver converts into its own framebuffer, so the LVGL draw buffers may live in external RAM + // The driver converts into its own buffer, so the LVGL draw buffers may live in external RAM .capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLOW_REFRESH | DISPLAY_CAPABILITY_PREFER_EXTERNAL_RAM, .reset = t5s3_display_reset, .init = t5s3_display_init, @@ -243,51 +463,73 @@ static const DisplayApi t5s3_display_api = { .get_frame_buffer_count = nullptr, .get_backlight = nullptr, .has_capability = nullptr, + .get_shape = nullptr, }; // region Driver lifecycle +static void destroy(T5s3DisplayInternal* internal) { + if (internal->request_mutex) vSemaphoreDelete(internal->request_mutex); + if (internal->request_done) vSemaphoreDelete(internal->request_done); + if (internal->staging_mutex) vSemaphoreDelete(internal->staging_mutex); + memory_free(internal->staging); + memory_free(internal->tones); + memory_free(internal); +} + static error_t start(Device* device) { const auto* config = GET_CONFIG(device); - auto* internal = static_cast(malloc(sizeof(T5s3DisplayInternal))); + static const MemoryPolicy internal_policy = { .required = MEMORY_CAPABILITY_INTERNAL, .desired = 0, .alignment = 0 }; + static const MemoryPolicy external_policy = { .required = 0, .desired = MEMORY_CAPABILITY_EXTERNAL, .alignment = 16 }; + auto* internal = static_cast(memory_calloc_with_policy(1, sizeof(T5s3DisplayInternal), &internal_policy)); if (internal == nullptr) { return ERROR_OUT_OF_MEMORY; } - internal->powered = false; + internal->lock = portMUX_INITIALIZER_UNLOCKED; + internal->request_mutex = xSemaphoreCreateMutex(); + internal->request_done = xSemaphoreCreateBinary(); + internal->staging_mutex = xSemaphoreCreateMutex(); + internal->staging = static_cast(memory_alloc_with_policy(T5S3_EPD_FB_BYTES, &external_policy)); + internal->tones = static_cast(memory_alloc_with_policy(256 * sizeof(Tone), &external_policy)); + if (!internal->request_mutex || !internal->request_done || !internal->staging_mutex || !internal->staging || !internal->tones) { + destroy(internal); + return ERROR_OUT_OF_MEMORY; + } + memset(internal->staging, 0xFF, T5S3_EPD_FB_BYTES); + build_tones(internal->tones); if (!t5s3_epd_init(config->pixel_clock_hz)) { - free(internal); + destroy(internal); return ERROR_RESOURCE; } - internal->framebuffer = t5s3_epd_framebuffer(); - - internal->partial_count_since_quality = 0; - internal->quality_hold_until_tick = 0; - internal->quality_hold_session_start_tick = 0; - - device_set_driver_data(device, internal); // The boot splash leaves ghosting, so clear the panel with the full flash cycle before LVGL draws power_on(internal); t5s3_epd_clear(); + if (xTaskCreatePinnedToCore(display_task, "t5s3_display", 4096, internal, 1, &internal->task, 1) != pdPASS) { + LOG_E(TAG, "Failed to create display task"); + power_off(internal); + t5s3_epd_deinit(); + destroy(internal); + return ERROR_RESOURCE; + } + + device_set_driver_data(device, internal); LOG_I(TAG, "Initialized (%dx%d)", T5S3_EPD_WIDTH, T5S3_EPD_HEIGHT); return ERROR_NONE; } static error_t stop(Device* device) { auto* internal = static_cast(device_get_driver_data(device)); - - if (internal->powered) { - t5s3_epd_power_off(); - internal->powered = false; + xTaskNotify(internal->task, CMD_EXIT, eSetBits); + while (!internal->exited) { + vTaskDelay(1); } - - t5s3_epd_deinit(); - - free(internal); device_set_driver_data(device, nullptr); + t5s3_epd_deinit(); + destroy(internal); return ERROR_NONE; } diff --git a/Drivers/lilygo-t5s3-epd-module/source/t5s3_display_bw.cpp b/Drivers/lilygo-t5s3-epd-module/source/t5s3_display_bw.cpp new file mode 100644 index 000000000..b9761084f --- /dev/null +++ b/Drivers/lilygo-t5s3-epd-module/source/t5s3_display_bw.cpp @@ -0,0 +1,534 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "t5s3_epd_board.h" + +#include +#include +#include +#include +#include + +#include +#include + +#define TAG "T5s3DisplayBw" +#define GET_CONFIG(device) (static_cast((device)->config)) + +// The panel is driven without waveform tables. A software state machine decides per pixel and +// per frame what to drive, and a task streams the frames. Each pixel is driven toward its target color for +// DRIVE_FRAMES frames after the target changes and is left alone afterwards. Only the bounding box of recent +// changes is processed. + +static constexpr int PANEL_WIDTH = T5S3_EPD_WIDTH; +static constexpr int PANEL_HEIGHT = T5S3_EPD_HEIGHT; + +// The target is 1 bit per pixel and the state is 4 bits per pixel +static constexpr size_t TARGET_ROW_BYTES = PANEL_WIDTH / 8; +static constexpr size_t STATE_ROW_BYTES = PANEL_WIDTH / 2; +static constexpr size_t TARGET_SIZE = TARGET_ROW_BYTES * PANEL_HEIGHT; +static constexpr size_t STATE_SIZE = STATE_ROW_BYTES * PANEL_HEIGHT; + +// Frames a pixel is driven after its target changed +static constexpr int DRIVE_FRAMES = 4; + +// Pixel state with both pixels white and fully driven, so nothing is driven +static constexpr uint8_t STATE_SETTLED_WHITE = 0x90; + +// Frames are padded to this duration so every drive step has the same length +static constexpr int64_t FRAME_PERIOD_US = 16667; + +// Gate pulse of a row in units of 0.1 us +static constexpr uint32_t ROW_HOLD_TICKS = 120; + +// Full panel black and white passes that remove ghosting +static constexpr int FLASH_CYCLES = 1; +static constexpr int FLASH_FIELDS = 8; + +static constexpr uint32_t IDLE_POWER_OFF_MS = 2000; + +static constexpr uint32_t CMD_UPDATE = 1U << 0; +static constexpr uint32_t CMD_FLASH = 1U << 1; +static constexpr uint32_t CMD_POWER_OFF = 1U << 2; +static constexpr uint32_t CMD_EXIT = 1U << 3; + +// 4x4 ordered (Bayer) dither thresholds +static constexpr uint8_t BAYER_4X4[4][4] = { + { 0, 8, 2, 10 }, + { 12, 4, 14, 6 }, + { 3, 11, 1, 9 }, + { 15, 7, 13, 5 }, +}; + +// Thresholds scaled by 16 stay below 255, so pure white is never classified as black +static inline bool dither_is_black(uint8_t luminance, int32_t x, int32_t y) { + return luminance <= BAYER_4X4[y & 3][x & 3] * 16U; +} + +extern "C" { + +struct Region { + int x0, y0, x1, y1; + + bool empty() const { return x1 <= x0 || y1 <= y0; } + + void merge(const Region& other) { + if (other.empty()) return; + if (empty()) { + *this = other; + return; + } + x0 = std::min(x0, other.x0); + y0 = std::min(y0, other.y0); + x1 = std::max(x1, other.x1); + y1 = std::max(y1, other.y1); + } +}; + +static constexpr Region EMPTY_REGION = { 0, 0, 0, 0 }; +static constexpr Region FULL_REGION = { 0, 0, PANEL_WIDTH, PANEL_HEIGHT }; + +struct T5s3DisplayBwInternal { + TaskHandle_t task; + SemaphoreHandle_t request_mutex; + SemaphoreHandle_t request_done; + portMUX_TYPE lock; + volatile bool exited; + bool powered; + // Set by the caller of a flash request, read by the task + bool flash_wipes_content; + // Changes not yet picked up by the task, guarded by lock + Region pending; + // Time of the first change of the current burst of updates and the longest draw_bitmap() call, guarded by lock + int64_t burst_start_us; + int64_t draw_max_us; + // Columns [x0, x1) that the frame being sent encodes + int encode_x0; + int encode_x1; + // 1 bit per pixel with the first pixel of a byte in the lowest bit, 1 is black + uint8_t* target; + uint8_t* state; +}; + +// region Pixel state machine + +// A state byte holds two pixels. The first pixel uses bits 7:5 for its drive counter and bit 1 for its color, +// the second uses bits 4:2 and bit 0. A counter of 4 means fully driven. target_pair holds the 2 target color +// bits of the pixel pair. Returns 4 output bits for 2 pixels, where 01 drives to black and 10 drives to white. +static inline uint8_t IRAM_ATTR drive_pair(uint8_t* state_ptr, uint8_t target_pair) { + // Indexed by which pixels changed color, keeps the counters of the unchanged ones + static constexpr uint8_t KEEP_COUNTERS[4] = { 0xFC, 0xE0, 0x1C, 0x00 }; + + uint8_t state = *state_ptr; + state &= KEEP_COUNTERS[(state ^ target_pair) & 0x3]; + state |= target_pair; + + uint8_t out = 0; + out |= (state & 0x80) ? 0x0 : (target_pair & 0x2) ? 0x4 : 0x8; + out |= (state & 0x10) ? 0x0 : (target_pair & 0x1) ? 0x1 : 0x2; + + // Increment the counters that have not reached 4 + state += ((~state) >> 2) & 0x24; + *state_ptr = state; + return out; +} + +// Encodes the columns [encode_x0, encode_x1) of a row, both multiples of 8. Other columns of out stay untouched. +// The panel wants the first pixel in the lowest bits, so the second pixel of a pair goes into the first slot of drive_pair(). +static void IRAM_ATTR encode_row(int y, uint8_t* out, void* context) { + auto* internal = static_cast(context); + const int x0 = internal->encode_x0; + const uint8_t* target = internal->target + y * TARGET_ROW_BYTES + x0 / 8; + uint8_t* state = internal->state + y * STATE_ROW_BYTES + x0 / 2; + out += x0 / 4; + + for (int count = (internal->encode_x1 - x0) / 8; count > 0; count--) { + const uint8_t pixels = *target++; + out[0] = static_cast(drive_pair(state, pixels & 0x3) | (drive_pair(state + 1, (pixels >> 2) & 0x3) << 4)); + out[1] = static_cast(drive_pair(state + 2, (pixels >> 4) & 0x3) | (drive_pair(state + 3, pixels >> 6) << 4)); + state += 4; + out += 2; + } +} + +static void pad_frame(int64_t frame_start_us) { + const int64_t remaining_us = FRAME_PERIOD_US - (esp_timer_get_time() - frame_start_us); + if (remaining_us > 2000) { + vTaskDelay(pdMS_TO_TICKS((remaining_us - 1000) / 1000)); + } + while (esp_timer_get_time() - frame_start_us < FRAME_PERIOD_US) {} +} + +static bool run_update_frame(T5s3DisplayBwInternal* internal, const Region& region) { + const int64_t start_us = esp_timer_get_time(); + // Columns outside the region must stay zero so they are not driven + t5s3_epd_board_fill_rows(0); + internal->encode_x0 = region.x0; + internal->encode_x1 = region.x1; + const bool success = t5s3_epd_board_frame(region.y0, region.y1, nullptr, ROW_HOLD_TICKS, encode_row, internal); + pad_frame(start_us); + return success; +} + +// Drives every pixel toward one color +static bool run_solid_frames(uint8_t pattern, int frames) { + t5s3_epd_board_fill_rows(pattern); + bool success = true; + for (int frame = 0; frame < frames && success; frame++) { + success = t5s3_epd_board_frame(0, PANEL_HEIGHT, nullptr, ROW_HOLD_TICKS, nullptr, nullptr); + } + return success; +} + +static bool run_flash() { + bool success = true; + for (int cycle = 0; cycle < FLASH_CYCLES && success; cycle++) { + success = run_solid_frames(T5S3_EPD_ALL_DARK, FLASH_FIELDS) && run_solid_frames(T5S3_EPD_ALL_LIGHT, FLASH_FIELDS); + } + return success; +} + +static void power_on(T5s3DisplayBwInternal* internal) { + if (internal->powered) return; + t5s3_epd_board_power_on(); + internal->powered = true; +} + +static void power_off(T5s3DisplayBwInternal* internal) { + if (!internal->powered) return; + t5s3_epd_board_power_off(); + internal->powered = false; +} + +static void run_flash_request(T5s3DisplayBwInternal* internal) { + const int64_t start_us = esp_timer_get_time(); + power_on(internal); + const bool success = run_flash(); + memset(internal->state, STATE_SETTLED_WHITE, STATE_SIZE); + if (!success) { + LOG_E(TAG, "Flash failed, a peripheral did not respond"); + } + + const int64_t now_us = esp_timer_get_time(); + portENTER_CRITICAL(&internal->lock); + if (!internal->flash_wipes_content) { + internal->pending = FULL_REGION; + internal->burst_start_us = now_us; + } + internal->draw_max_us = 0; + portEXIT_CRITICAL(&internal->lock); + LOG_I(TAG, "Flashed in %d ms", static_cast((esp_timer_get_time() - start_us) / 1000)); +} + +static void display_task(void* arg) { + auto* internal = static_cast(arg); + Region active = EMPTY_REGION; + Region burst = EMPTY_REGION; + int frames_left = 0; + int burst_frames = 0; + int64_t burst_longest_frame_us = 0; + + while (true) { + uint32_t commands = 0; + TickType_t timeout = portMAX_DELAY; + if (frames_left > 0) { + timeout = 0; + } else if (internal->powered) { + timeout = pdMS_TO_TICKS(IDLE_POWER_OFF_MS); + } + const bool notified = xTaskNotifyWait(0, UINT32_MAX, &commands, timeout) == pdTRUE; + + if (commands & CMD_EXIT) { + break; + } + + if (commands & CMD_POWER_OFF) { + active = EMPTY_REGION; + frames_left = 0; + power_off(internal); + } + + if (commands & CMD_FLASH) { + run_flash_request(internal); + active = EMPTY_REGION; + burst = EMPTY_REGION; + frames_left = 0; + burst_frames = 0; + burst_longest_frame_us = 0; + xSemaphoreGive(internal->request_done); + } + + Region pending; + portENTER_CRITICAL(&internal->lock); + pending = internal->pending; + internal->pending = EMPTY_REGION; + portEXIT_CRITICAL(&internal->lock); + + if (!pending.empty()) { + active.merge(pending); + burst.merge(pending); + frames_left = DRIVE_FRAMES; + } + + if (frames_left > 0) { + power_on(internal); + // Columns are processed in groups of 8 pixels + Region aligned = active; + aligned.x0 &= ~7; + aligned.x1 = (aligned.x1 + 7) & ~7; + const int64_t frame_start_us = esp_timer_get_time(); + if (!run_update_frame(internal, aligned)) { + LOG_E(TAG, "Update failed, a peripheral did not respond"); + } + // The padding is not part of the work of a frame + burst_longest_frame_us = std::max(burst_longest_frame_us, esp_timer_get_time() - frame_start_us); + burst_frames++; + if (--frames_left == 0) { + active = EMPTY_REGION; + int64_t burst_start_us; + int64_t draw_max_us; + portENTER_CRITICAL(&internal->lock); + burst_start_us = internal->burst_start_us; + draw_max_us = internal->draw_max_us; + internal->burst_start_us = 0; + internal->draw_max_us = 0; + portEXIT_CRITICAL(&internal->lock); + LOG_D(TAG, "Updated %dx%d at %d,%d in %d ms (%d frames, longest %d ms, draw_bitmap up to %d us)", + burst.x1 - burst.x0, burst.y1 - burst.y0, burst.x0, burst.y0, + burst_start_us != 0 ? static_cast((esp_timer_get_time() - burst_start_us) / 1000) : -1, + burst_frames, static_cast(burst_longest_frame_us / 1000), static_cast(draw_max_us) + ); + burst = EMPTY_REGION; + burst_frames = 0; + burst_longest_frame_us = 0; + } + } else if (!notified) { + power_off(internal); + } + } + + power_off(internal); + internal->exited = true; + vTaskDelete(nullptr); +} + +// endregion + +// region Helpers + +static void notify_task(T5s3DisplayBwInternal* internal, uint32_t command) { + xTaskNotify(internal->task, command, eSetBits); +} + +static error_t flash_blocking(T5s3DisplayBwInternal* internal, bool wipe_content) { + xSemaphoreTake(internal->request_mutex, portMAX_DELAY); + internal->flash_wipes_content = wipe_content; + notify_task(internal, CMD_FLASH); + xSemaphoreTake(internal->request_done, portMAX_DELAY); + xSemaphoreGive(internal->request_mutex); + return ERROR_NONE; +} + +// endregion + +// region DisplayApi + +static error_t t5s3_display_bw_reset(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + notify_task(internal, CMD_POWER_OFF); + return ERROR_NONE; +} + +static error_t t5s3_display_bw_init(Device*) { + return ERROR_NONE; +} + +static error_t t5s3_display_bw_clear(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + memset(internal->target, 0, TARGET_SIZE); + return flash_blocking(internal, true); +} + +static error_t t5s3_display_bw_refresh(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + return flash_blocking(internal, false); +} + +// Reports GRAYSCALE8 so LVGL renders partial tiles. The luminance is dithered to black and white. +static error_t t5s3_display_bw_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) { + auto* internal = static_cast(device_get_driver_data(device)); + const int64_t start_us = esp_timer_get_time(); + const int32_t width = x_end - x_start; + const auto* source = static_cast(color_data); + + for (int32_t y = y_start; y < y_end; y++) { + const uint8_t* source_row = source + (y - y_start) * width; + uint8_t* target_row = internal->target + y * TARGET_ROW_BYTES; + for (int32_t x = x_start; x < x_end; x++) { + const uint8_t mask = 1U << (x & 7); + if (dither_is_black(source_row[x - x_start], x, y)) { + target_row[x / 8] |= mask; + } else { + target_row[x / 8] &= ~mask; + } + } + } + + const Region changed = { x_start, y_start, x_end, y_end }; + const int64_t draw_us = esp_timer_get_time() - start_us; + portENTER_CRITICAL(&internal->lock); + internal->pending.merge(changed); + if (internal->burst_start_us == 0) { + internal->burst_start_us = start_us; + } + internal->draw_max_us = std::max(internal->draw_max_us, draw_us); + portEXIT_CRITICAL(&internal->lock); + notify_task(internal, CMD_UPDATE); + return ERROR_NONE; +} + +static error_t t5s3_display_bw_disp_on_off(Device* device, bool on_off) { + auto* internal = static_cast(device_get_driver_data(device)); + if (!on_off) { + notify_task(internal, CMD_POWER_OFF); + } + return ERROR_NONE; +} + +static DisplayColorFormat t5s3_display_bw_get_color_format(Device*) { + return DISPLAY_COLOR_FORMAT_GRAYSCALE8; +} + +static uint16_t t5s3_display_bw_get_resolution_x(Device*) { + return PANEL_WIDTH; +} + +static uint16_t t5s3_display_bw_get_resolution_y(Device*) { + return PANEL_HEIGHT; +} + +// endregion + +static constexpr DisplayApi t5s3_display_bw_api = { + // draw_bitmap() converts into its own target buffer and never DMAs from the given pointer + .capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLOW_REFRESH | DISPLAY_CAPABILITY_PREFER_EXTERNAL_RAM, + .reset = t5s3_display_bw_reset, + .init = t5s3_display_bw_init, + .draw_bitmap = t5s3_display_bw_draw_bitmap, + .clear = t5s3_display_bw_clear, + .refresh = t5s3_display_bw_refresh, + .mirror = nullptr, + .swap_xy = nullptr, + .get_swap_xy = nullptr, + .get_mirror_x = nullptr, + .get_mirror_y = nullptr, + .set_gap = nullptr, + .get_gap_x = nullptr, + .get_gap_y = nullptr, + .invert_color = nullptr, + .disp_on_off = t5s3_display_bw_disp_on_off, + .disp_sleep = nullptr, + .get_color_format = t5s3_display_bw_get_color_format, + .get_resolution_x = t5s3_display_bw_get_resolution_x, + .get_resolution_y = t5s3_display_bw_get_resolution_y, + .get_frame_buffer = nullptr, + .get_frame_buffer_count = nullptr, + .get_backlight = nullptr, + .has_capability = nullptr, + .get_shape = nullptr, +}; + +// region Driver lifecycle + +static void destroy(T5s3DisplayBwInternal* internal) { + if (internal->request_mutex) vSemaphoreDelete(internal->request_mutex); + if (internal->request_done) vSemaphoreDelete(internal->request_done); + memory_free(internal->target); + memory_free(internal->state); + memory_free(internal); +} + +static bool allocate_buffers(T5s3DisplayBwInternal* internal) { + static const MemoryPolicy external_policy = { .required = 0, .desired = MEMORY_CAPABILITY_EXTERNAL, .alignment = 16 }; + internal->target = static_cast(memory_calloc_with_policy(1, TARGET_SIZE, &external_policy)); + internal->state = static_cast(memory_alloc_with_policy(STATE_SIZE, &external_policy)); + if (internal->target == nullptr || internal->state == nullptr) { + return false; + } + return true; +} + +static error_t start(Device* device) { + const auto* config = GET_CONFIG(device); + static const MemoryPolicy internal_policy = { .required = MEMORY_CAPABILITY_INTERNAL, .desired = 0, .alignment = 0 }; + auto* internal = static_cast(memory_calloc_with_policy(1, sizeof(T5s3DisplayBwInternal), &internal_policy)); + if (internal == nullptr) { + return ERROR_OUT_OF_MEMORY; + } + + internal->lock = portMUX_INITIALIZER_UNLOCKED; + internal->request_mutex = xSemaphoreCreateMutex(); + internal->request_done = xSemaphoreCreateBinary(); + + if (!internal->request_mutex || !internal->request_done || !allocate_buffers(internal)) { + destroy(internal); + return ERROR_OUT_OF_MEMORY; + } + memset(internal->state, STATE_SETTLED_WHITE, STATE_SIZE); + + if (!t5s3_epd_board_init(config->pixel_clock_hz)) { + LOG_E(TAG, "Failed to set up the pins and peripherals"); + destroy(internal); + return ERROR_RESOURCE; + } + + if (xTaskCreatePinnedToCore(display_task, "t5s3_display_bw", 4096, internal, 1, &internal->task, 1) != pdPASS) { + LOG_E(TAG, "Failed to create display task"); + t5s3_epd_board_deinit(); + destroy(internal); + return ERROR_RESOURCE; + } + + device_set_driver_data(device, internal); + + // The boot splash leaves residue that a regular update would never clear + flash_blocking(internal, true); + + LOG_I(TAG, "Started (%dx%d)", PANEL_WIDTH, PANEL_HEIGHT); + return ERROR_NONE; +} + +static error_t stop(Device* device) { + auto* internal = static_cast(device_get_driver_data(device)); + notify_task(internal, CMD_EXIT); + while (!internal->exited) { + vTaskDelay(1); + } + device_set_driver_data(device, nullptr); + t5s3_epd_board_deinit(); + destroy(internal); + return ERROR_NONE; +} + +// endregion + +Driver t5s3_display_bw_driver = { + .name = "t5s3-display-bw", + .compatible = (const char*[]) { "lilygo,t5s3-display-bw", nullptr }, + .start_device = start, + .stop_device = stop, + .probe = nullptr, + .api = &t5s3_display_bw_api, + .device_type = &DISPLAY_TYPE, + .internal = nullptr +}; + +} diff --git a/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd.cpp b/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd.cpp index 39f9f83fb..6251871c2 100644 --- a/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd.cpp +++ b/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd.cpp @@ -1,596 +1,422 @@ // SPDX-License-Identifier: LGPL-3.0-or-later // -// Minimal driver for the ED047TC1 panel of the LILYGO T5 4.7 Inch E-Paper S3. -// The sequences and timings follow the epdiy project (https://github.com/vroland/epdiy). +// Waveform updates of the ED047TC1 panel of the LILYGO T5 4.7 Inch E-Paper S3. +// The update logic follows the epdiy project (https://github.com/vroland/epdiy). // -// The panel is controlled by a 74HCT4094 shift register (latch, output enable, mode, start of frame and the -// panel supplies), the CKV gate clock (RMT peripheral) and an 8 bit bus (LCD peripheral in i80 mode) that -// carries the 2 bit actions of 4 pixels per byte. Rows are sent one at a time. The data of a row is applied by the -// CKV pulse after the one that was running during its transfer. +// Each update diffs the frame buffer (front) against what the panel shows (back), sends only the rows that changed +// and only drives the columns that changed. #include "t5s3_epd.h" #include +#include -#include -#include -#include -#include -#include -#include -#include +#include #include #include #include -#include -#include #include #define TAG "T5s3Epd" -static constexpr gpio_num_t PIN_REGISTER_DATA = GPIO_NUM_13; -static constexpr gpio_num_t PIN_REGISTER_CLOCK = GPIO_NUM_12; -static constexpr gpio_num_t PIN_REGISTER_STROBE = GPIO_NUM_0; -static constexpr gpio_num_t PIN_CKV = GPIO_NUM_38; -// Start pulse of a row, driven as the data/command line of the bus -static constexpr gpio_num_t PIN_STH = GPIO_NUM_40; -// Pixel clock, driven as the write line of the bus -static constexpr gpio_num_t PIN_CKH = GPIO_NUM_41; -static constexpr gpio_num_t BUS_PINS[8] = {GPIO_NUM_6, GPIO_NUM_7, GPIO_NUM_4, GPIO_NUM_5, GPIO_NUM_2, GPIO_NUM_3, GPIO_NUM_8, GPIO_NUM_1}; - -// A row has headroom after the pixel data for the timing of the source driver -static constexpr int BUS_TRANSFER_BYTES = ((T5S3_EPD_WIDTH + 32) / 4 + 3) & ~3; - -// Time base of the CKV pulses, 10 MHz -static constexpr uint32_t PULSE_RESOLUTION_HZ = 10000000; -// Low time after the pulse of a row in ticks -static constexpr uint32_t ROW_LOW_TICKS = 50; -// More phases drive the pixels further into black or white, which leaves less ghosting but makes fast updates slower -static constexpr uint32_t FAST_PHASES = 10; -// Hold time of a phase of the fast update in ticks -static constexpr uint32_t FAST_HOLD_TICKS = 1000; +// A pixel keeps its drive until its row is addressed again, so the drive of a phase depends on the length of the frame. +// Frames are padded to a fixed period, which makes an update of a few rows drive as long as one of the whole panel. +// Black needs about 32 frames to get as dark as the waveform makes it and white about 8 to leave black. The fast update +// stops at a dark gray for black, the refresh completes it. +static constexpr uint32_t FAST_PHASES = 8; +static constexpr int64_t FAST_FRAME_US = 16667; +// Gate pulse of a row of the fast update in units of 0.1 us +static constexpr uint32_t FAST_HOLD_TICKS = 120; +// Duration of a frame of the whole panel, the gray levels of the waveform were measured with it +static constexpr int64_t QUALITY_FRAME_US = 40000; +// Frames that settle the pixels of a refresh before the waveform. Black pixels get the rest of their drive and white +// pixels go further toward white. Gray pixels are shaken: REFRESH_SHAKE_FRAMES frames toward black and the rest toward +// white, which erases what they showed before, also a black that was shown for a long time. The waveform then draws +// their gray levels like from a clean white. +static constexpr int REFRESH_SETTLE_FRAMES = 24; +static constexpr int REFRESH_SHAKE_FRAMES = 8; + +// 1 bit per pixel +static constexpr int TOUCHED_ROW_BYTES = T5S3_EPD_WIDTH / 8; +static constexpr int TOUCHED_BYTES = TOUCHED_ROW_BYTES * T5S3_EPD_HEIGHT; // Hold time of the frames that flash the panel to white static constexpr uint32_t CLEAR_HOLD_TICKS = 120; static constexpr int CLEAR_CYCLES = 3; static constexpr int CLEAR_DARK_FRAMES = 10; static constexpr int CLEAR_LIGHT_FRAMES = 10; static constexpr int CLEAR_REST_FRAMES = 2; -static constexpr uint8_t ALL_DARK = 0x55; -static constexpr uint8_t ALL_LIGHT = 0xAA; - -// Limit for waiting on a flag that an interrupt handler sets, so that a lost interrupt fails the update instead of spinning forever -static constexpr uint32_t WAIT_SPIN_LIMIT = 20000000; struct Engine { - uint8_t* front = nullptr; - uint8_t* back = nullptr; - uint8_t* row_buffer[2] = {nullptr, nullptr}; - int current = 0; - esp_lcd_i80_bus_handle_t bus = nullptr; - esp_lcd_panel_io_handle_t io = nullptr; - rmt_channel_handle_t pulse_channel = nullptr; - rmt_encoder_handle_t pulse_encoder = nullptr; - volatile bool transfer_done = true; - volatile bool pulse_done = true; - bool failed = false; - // Shift register - bool latch = false; - bool output_enable = false; - bool mode = false; - bool start_of_frame = true; - bool power_disable = true; - bool positive_power = false; - bool negative_power = false; - // Rows written since the last row that carried data - int rows_skipped = 0; + uint8_t* front; + uint8_t* back; + // Black and white targets of the fast update + uint8_t* dither; + // Pixels that a fast update drove since their last refresh, 1 bit per pixel + uint8_t* touched; + // Targets of the fast update being sent, dithered where front differs from back + uint8_t* fast; + // Targets of the update being sent + const uint8_t* to; + // Actions of the phase being sent + uint8_t table[256]; + // Areas of the refresh being sent + const T5s3EpdRect* rects; + int rect_count; + int settle_frame; bool row_dirty[T5S3_EPD_HEIGHT]; alignas(4) uint8_t changes[T5S3_EPD_FB_ROW_BYTES]; alignas(4) uint8_t line_mask[T5S3_EPD_BUS_ROW_BYTES]; }; -static Engine engine; -static rmt_symbol_word_t pulse_symbol; - -static inline void IRAM_ATTR pin_set(gpio_num_t pin, uint32_t level) { - gpio_ll_set_level(GPIO_LL_GET_HW(GPIO_PORT_0), pin, level); -} - -static bool IRAM_ATTR on_pulse_done(rmt_channel_handle_t, const rmt_tx_done_event_data_t*, void*) { - engine.pulse_done = true; - return false; -} - -static bool IRAM_ATTR on_transfer_done(esp_lcd_panel_io_handle_t, esp_lcd_panel_io_event_data_t*, void*) { - engine.transfer_done = true; - return false; -} - -// After a failure nothing is waited for anymore, so the rest of the update ends quickly -static void IRAM_ATTR wait_for(volatile bool& flag) { - uint32_t spins = 0; - while (!flag) { - if (engine.failed || ++spins > WAIT_SPIN_LIMIT) { - engine.failed = true; - flag = true; - } - } -} - -// region Shift register - -static void IRAM_ATTR push_register_bit(bool bit) { - pin_set(PIN_REGISTER_CLOCK, 0); - pin_set(PIN_REGISTER_DATA, bit ? 1 : 0); - pin_set(PIN_REGISTER_CLOCK, 1); -} - -static void IRAM_ATTR push_register() { - pin_set(PIN_REGISTER_STROBE, 0); - push_register_bit(engine.output_enable); - push_register_bit(engine.mode); - // Always set - push_register_bit(true); - push_register_bit(engine.start_of_frame); - push_register_bit(engine.negative_power); - push_register_bit(engine.positive_power); - push_register_bit(engine.power_disable); - push_register_bit(engine.latch); - pin_set(PIN_REGISTER_STROBE, 1); -} - -// endregion - -// region CKV pulses and row transfer - -// Pulses the gate clock for high_ticks and keeps it low for low_ticks, repeated for the given number of pulses. -// Without a high time the line is high for low_ticks. -static void IRAM_ATTR pulse_ckv(uint32_t high_ticks, uint32_t low_ticks, bool wait, int count = 1) { - wait_for(engine.pulse_done); - if (high_ticks > 0) { - pulse_symbol.duration0 = high_ticks; - pulse_symbol.level0 = 1; - pulse_symbol.duration1 = low_ticks; - pulse_symbol.level1 = 0; - } else { - pulse_symbol.duration0 = low_ticks; - pulse_symbol.level0 = 1; - pulse_symbol.duration1 = 0; - pulse_symbol.level1 = 0; - } - engine.pulse_done = false; - rmt_transmit_config_t config = {}; - config.loop_count = count; - if (rmt_transmit(engine.pulse_channel, engine.pulse_encoder, &pulse_symbol, sizeof(pulse_symbol), &config) != ESP_OK) { - engine.pulse_done = true; - engine.failed = true; - return; - } - if (wait) { - wait_for(engine.pulse_done); - } -} - -static void IRAM_ATTR pulse_ckv_us(uint32_t high_us, uint32_t low_us, bool wait) { - pulse_ckv(high_us * 10, low_us * 10, wait); -} - -static void IRAM_ATTR start_transfer() { - engine.transfer_done = false; - if (esp_lcd_panel_io_tx_color(engine.io, 0, engine.row_buffer[engine.current], BUS_TRANSFER_BYTES) != ESP_OK) { - engine.transfer_done = true; - engine.failed = true; - } -} - -// Applies the data that was sent last and starts the transfer of the current row buffer while the CKV pulse runs -static void IRAM_ATTR output_row(uint32_t hold_ticks) { - wait_for(engine.transfer_done); - wait_for(engine.pulse_done); - engine.latch = true; - push_register(); - engine.latch = false; - push_register(); - pulse_ckv(hold_ticks, ROW_LOW_TICKS, false); - start_transfer(); - engine.current ^= 1; -} - -static void IRAM_ATTR write_row(uint32_t hold_ticks) { - output_row(hold_ticks); - engine.rows_skipped = 0; -} - -// Skips a row after rows with data. The first two skipped rows push blank data through the pipeline. -static void IRAM_ATTR skip_row(uint32_t hold_ticks) { - memset(engine.row_buffer[engine.current], 0, T5S3_EPD_BUS_ROW_BYTES); - output_row(hold_ticks); - engine.rows_skipped++; -} - -// Clocks the gate driver through rows that carry no data with one train of pulses -static void IRAM_ATTR skip_rows(int count) { - pulse_ckv(45, 5, false, count); - engine.rows_skipped += count; -} - -static void IRAM_ATTR start_frame() { - wait_for(engine.transfer_done); - wait_for(engine.pulse_done); - engine.rows_skipped = 0; - - engine.mode = true; - push_register(); - pulse_ckv_us(1, 1, true); - - // The start of frame line must go low and high again while the long pulse runs - engine.start_of_frame = false; - push_register(); - pulse_ckv_us(1000, 100, false); - engine.start_of_frame = true; - push_register(); - for (int i = 0; i < 4; i++) { - pulse_ckv_us(1, 1, true); - } - - engine.output_enable = true; - push_register(); -} - -static void IRAM_ATTR end_frame() { - engine.start_of_frame = false; - push_register(); - for (int i = 0; i < 5; i++) { - pulse_ckv_us(1, 1, true); - } - engine.mode = false; - push_register(); - pulse_ckv_us(0, 10, true); - engine.output_enable = false; - push_register(); - for (int i = 0; i < 3; i++) { - pulse_ckv_us(1, 1, true); - } -} - -// endregion - -// region Frames - -static void IRAM_ATTR draw_frame(const uint8_t* table, uint32_t hold_ticks) { - start_frame(); - int y = 0; - bool ends_with_train = false; - while (y < T5S3_EPD_HEIGHT && !engine.failed) { - ends_with_train = false; - if (engine.row_dirty[y]) { - t5s3_epd_prepare_row( - engine.row_buffer[engine.current], - engine.front + y * T5S3_EPD_FB_ROW_BYTES, - engine.back + y * T5S3_EPD_FB_ROW_BYTES, - table, - engine.line_mask - ); - write_row(hold_ticks); - y++; - } else if (engine.rows_skipped < 2) { - skip_row(hold_ticks); - y++; - } else { - int run = 1; - while (y + run < T5S3_EPD_HEIGHT && !engine.row_dirty[y + run]) { - run++; - } - skip_rows(run); - ends_with_train = true; - y += run; - } - } - // The last row is applied by one more pulse - if (engine.rows_skipped == 0 && !engine.failed) { - write_row(hold_ticks); - } else if (ends_with_train) { - // The end of the frame must not overlap the train of pulses of the skipped rows - wait_for(engine.pulse_done); - } - end_frame(); -} - -static void IRAM_ATTR draw_uniform_frame(uint8_t pattern, uint32_t hold_ticks) { - memset(engine.row_buffer[0], pattern, T5S3_EPD_BUS_ROW_BYTES); - memset(engine.row_buffer[1], pattern, T5S3_EPD_BUS_ROW_BYTES); - start_frame(); - for (int y = 0; y < T5S3_EPD_HEIGHT && !engine.failed; y++) { - write_row(hold_ticks); - } - if (!engine.failed) { - write_row(hold_ticks); - } - end_frame(); -} - -// endregion - -// region Setup +static Engine* engine = nullptr; -static bool setup_register_pins() { - gpio_config_t config = {}; - config.pin_bit_mask = (1ULL << PIN_REGISTER_DATA) | (1ULL << PIN_REGISTER_CLOCK) | (1ULL << PIN_REGISTER_STROBE); - config.mode = GPIO_MODE_OUTPUT; - if (gpio_config(&config) != ESP_OK) { - return false; - } - pin_set(PIN_REGISTER_STROBE, 0); - return true; -} - -static bool setup_bus(uint32_t pixel_clock_hz) { - esp_lcd_i80_bus_config_t bus_config = {}; - bus_config.dc_gpio_num = PIN_STH; - bus_config.wr_gpio_num = PIN_CKH; - bus_config.clk_src = LCD_CLK_SRC_DEFAULT; - for (int i = 0; i < 8; i++) { - bus_config.data_gpio_nums[i] = BUS_PINS[i]; - } - bus_config.bus_width = 8; - bus_config.max_transfer_bytes = BUS_TRANSFER_BYTES; - if (esp_lcd_new_i80_bus(&bus_config, &engine.bus) != ESP_OK) { - return false; +// Sleeps for most of the rest of the period, which also lets the idle task feed the watchdog +static void pad_frame(int64_t frame_start_us, int64_t period_us) { + const int64_t remaining_us = period_us - (esp_timer_get_time() - frame_start_us); + if (remaining_us > 2000) { + vTaskDelay(pdMS_TO_TICKS((remaining_us - 1000) / 1000)); } - - // The data/command line is low when idle, high during a dummy command phase and low during the data of a row - esp_lcd_panel_io_i80_config_t io_config = {}; - io_config.cs_gpio_num = GPIO_NUM_NC; - io_config.pclk_hz = pixel_clock_hz; - io_config.trans_queue_depth = 4; - io_config.on_color_trans_done = on_transfer_done; - io_config.lcd_cmd_bits = 10; - io_config.lcd_param_bits = 0; - io_config.dc_levels.dc_idle_level = 0; - io_config.dc_levels.dc_cmd_level = 1; - io_config.dc_levels.dc_dummy_level = 0; - io_config.dc_levels.dc_data_level = 0; - return esp_lcd_new_panel_io_i80(engine.bus, &io_config, &engine.io) == ESP_OK; + while (esp_timer_get_time() - frame_start_us < period_us) {} } -static bool setup_pulse() { - rmt_tx_channel_config_t channel_config = {}; - channel_config.gpio_num = PIN_CKV; - channel_config.clk_src = RMT_CLK_SRC_DEFAULT; - channel_config.resolution_hz = PULSE_RESOLUTION_HZ; - channel_config.mem_block_symbols = 48; - channel_config.trans_queue_depth = 1; - if (rmt_new_tx_channel(&channel_config, &engine.pulse_channel) != ESP_OK) { - return false; - } - rmt_copy_encoder_config_t encoder_config = {}; - if (rmt_new_copy_encoder(&encoder_config, &engine.pulse_encoder) != ESP_OK) { - return false; - } - rmt_tx_event_callbacks_t callbacks = {}; - callbacks.on_trans_done = on_pulse_done; - if (rmt_tx_register_event_callbacks(engine.pulse_channel, &callbacks, nullptr) != ESP_OK) { - return false; - } - return rmt_enable(engine.pulse_channel) == ESP_OK; +static void IRAM_ATTR prepare_row(int y, uint8_t* out, void*) { + t5s3_epd_prepare_row( + out, + engine->to + y * T5S3_EPD_FB_ROW_BYTES, + engine->back + y * T5S3_EPD_FB_ROW_BYTES, + engine->table, + engine->line_mask + ); } bool t5s3_epd_init(uint32_t pixel_clock_hz) { - if (engine.front != nullptr) { + if (engine != nullptr) { return true; } - - engine.front = static_cast(heap_caps_aligned_alloc(16, T5S3_EPD_FB_BYTES, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)); - engine.back = static_cast(heap_caps_aligned_alloc(16, T5S3_EPD_FB_BYTES, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)); - for (auto& buffer: engine.row_buffer) { - buffer = static_cast(heap_caps_aligned_calloc(16, 1, BUS_TRANSFER_BYTES, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)); + static const MemoryPolicy external_policy = { .required = 0, .desired = MEMORY_CAPABILITY_EXTERNAL, .alignment = 16 }; + engine = static_cast(memory_calloc_with_policy(1, sizeof(Engine), &external_policy)); + if (engine == nullptr) { + LOG_E(TAG, "Out of memory"); + return false; } - if (engine.front == nullptr || engine.back == nullptr || engine.row_buffer[0] == nullptr || engine.row_buffer[1] == nullptr) { + engine->front = static_cast(memory_alloc_with_policy(T5S3_EPD_FB_BYTES, &external_policy)); + engine->back = static_cast(memory_alloc_with_policy(T5S3_EPD_FB_BYTES, &external_policy)); + engine->dither = static_cast(memory_alloc_with_policy(T5S3_EPD_FB_BYTES, &external_policy)); + engine->fast = static_cast(memory_alloc_with_policy(T5S3_EPD_FB_BYTES, &external_policy)); + engine->touched = static_cast(memory_calloc_with_policy(1, TOUCHED_BYTES, &external_policy)); + if (engine->front == nullptr || engine->back == nullptr || engine->dither == nullptr || engine->fast == nullptr || engine->touched == nullptr) { LOG_E(TAG, "Out of memory"); t5s3_epd_deinit(); return false; } - memset(engine.front, 0xFF, T5S3_EPD_FB_BYTES); - memset(engine.back, 0xFF, T5S3_EPD_FB_BYTES); + memset(engine->front, 0xFF, T5S3_EPD_FB_BYTES); + memset(engine->back, 0xFF, T5S3_EPD_FB_BYTES); + memset(engine->dither, 0xFF, T5S3_EPD_FB_BYTES); - if (!setup_register_pins() || !setup_bus(pixel_clock_hz) || !setup_pulse()) { + if (!t5s3_epd_board_init(pixel_clock_hz)) { LOG_E(TAG, "Failed to set up the pins and peripherals"); t5s3_epd_deinit(); return false; } - - engine.latch = false; - engine.output_enable = false; - engine.mode = false; - engine.start_of_frame = true; - engine.power_disable = true; - engine.positive_power = false; - engine.negative_power = false; - engine.current = 0; - engine.transfer_done = true; - engine.pulse_done = true; - push_register(); return true; } void t5s3_epd_deinit() { - if (engine.pulse_channel != nullptr) { - // Both calls fail harmlessly when the channel was not enabled - rmt_disable(engine.pulse_channel); - rmt_del_channel(engine.pulse_channel); - engine.pulse_channel = nullptr; - } - if (engine.pulse_encoder != nullptr) { - rmt_del_encoder(engine.pulse_encoder); - engine.pulse_encoder = nullptr; - } - if (engine.io != nullptr) { - esp_lcd_panel_io_del(engine.io); - engine.io = nullptr; - } - if (engine.bus != nullptr) { - esp_lcd_del_i80_bus(engine.bus); - engine.bus = nullptr; + if (engine == nullptr) { + return; } - heap_caps_free(engine.front); - heap_caps_free(engine.back); - heap_caps_free(engine.row_buffer[0]); - heap_caps_free(engine.row_buffer[1]); - engine.front = nullptr; - engine.back = nullptr; - engine.row_buffer[0] = nullptr; - engine.row_buffer[1] = nullptr; -} - -// endregion - -// region Power - -void t5s3_epd_power_on() { - engine.power_disable = false; - push_register(); - esp_rom_delay_us(100); - engine.negative_power = true; - push_register(); - esp_rom_delay_us(500); - engine.positive_power = true; - push_register(); - esp_rom_delay_us(100); - engine.start_of_frame = true; - push_register(); + t5s3_epd_board_deinit(); + memory_free(engine->front); + memory_free(engine->back); + memory_free(engine->dither); + memory_free(engine->fast); + memory_free(engine->touched); + memory_free(engine); + engine = nullptr; } -void t5s3_epd_power_off() { - engine.positive_power = false; - push_register(); - esp_rom_delay_us(10); - engine.negative_power = false; - push_register(); - esp_rom_delay_us(100); - engine.start_of_frame = false; - engine.output_enable = false; - engine.mode = false; - engine.power_disable = true; - push_register(); +uint8_t* t5s3_epd_framebuffer() { + return engine->front; } -// endregion - -// region Updates - -uint8_t* t5s3_epd_framebuffer() { - return engine.front; +uint8_t* t5s3_epd_dither_buffer() { + return engine->dither; } void t5s3_epd_fill_white() { - memset(engine.front, 0xFF, T5S3_EPD_FB_BYTES); + memset(engine->front, 0xFF, T5S3_EPD_FB_BYTES); + memset(engine->dither, 0xFF, T5S3_EPD_FB_BYTES); } -void t5s3_epd_invalidate() { - auto* words = reinterpret_cast(engine.back); - for (int i = 0; i < T5S3_EPD_FB_BYTES / 4; i++) { - words[i] = ~words[i]; +// Pixels whose level changed get their black or white target, the others keep what the panel shows. +// first and last get the first and last group of 8 pixels with a change, or -1 for a row without changes. +static void build_fast_row(int y, int* first, int* last) { + const size_t offset = y * T5S3_EPD_FB_ROW_BYTES; + const uint8_t* front = engine->front + offset; + const uint8_t* back = engine->back + offset; + const uint8_t* dither = engine->dither + offset; + uint8_t* fast = engine->fast + offset; + uint8_t* touched = engine->touched + y * TOUCHED_ROW_BYTES; + *first = -1; + *last = -1; + for (int i = 0; i < T5S3_EPD_FB_ROW_BYTES; i++) { + const uint8_t difference = front[i] ^ back[i]; + uint8_t changed = 0; + changed |= (difference & 0x0F) != 0 ? 0x0F : 0x00; + changed |= (difference & 0xF0) != 0 ? 0xF0 : 0x00; + fast[i] = static_cast((dither[i] & changed) | (back[i] & ~changed)); + // Byte i holds the pixels 2i and 2i + 1 + const int bit = (i & 3) * 2; + touched[i / 4] |= static_cast((((changed & 0x0F) != 0 ? 1 : 0) | ((changed & 0xF0) != 0 ? 2 : 0)) << bit); + if (changed != 0) { + *first = *first < 0 ? i / 4 : *first; + *last = i / 4; + } } } -bool t5s3_epd_update(T5s3EpdMode mode, int32_t y_start, int32_t y_end) { +bool t5s3_epd_show_changes(int32_t y_start, int32_t y_end, T5s3EpdRect* changed) { + *changed = { 0, 0, 0, 0 }; if (y_start < 0) { y_start = 0; } if (y_end > T5S3_EPD_HEIGHT) { y_end = T5S3_EPD_HEIGHT; } - if (y_start >= y_end) { - return true; - } const int64_t started = esp_timer_get_time(); // Find the rows and columns that changed - memset(engine.row_dirty, 0, sizeof(engine.row_dirty)); - memset(engine.changes, 0, sizeof(engine.changes)); - auto* changes = reinterpret_cast(engine.changes); - bool any_changes = false; + memset(engine->row_dirty, 0, sizeof(engine->row_dirty)); + memset(engine->changes, 0, sizeof(engine->changes)); + auto* changes = reinterpret_cast(engine->changes); + int changed_rows = 0; + int driven_rows = 0; + int first_group = T5S3_EPD_WIDTH; + int last_group = -1; for (int y = y_start; y < y_end; y++) { - const auto* to_row = reinterpret_cast(engine.front + y * T5S3_EPD_FB_ROW_BYTES); - const auto* from_row = reinterpret_cast(engine.back + y * T5S3_EPD_FB_ROW_BYTES); + int first; + int last; + build_fast_row(y, &first, &last); + if (first < 0) { + continue; + } + if (changed_rows == 0) { + changed->y0 = y; + } + changed->y1 = y + 1; + first_group = first < first_group ? first : first_group; + last_group = last > last_group ? last : last_group; + changed_rows++; + + const auto* to_row = reinterpret_cast(engine->fast + y * T5S3_EPD_FB_ROW_BYTES); + const auto* from_row = reinterpret_cast(engine->back + y * T5S3_EPD_FB_ROW_BYTES); uint32_t row_changes = 0; for (int i = 0; i < T5S3_EPD_FB_ROW_BYTES / 4; i++) { const uint32_t difference = to_row[i] ^ from_row[i]; changes[i] |= difference; row_changes |= difference; } - engine.row_dirty[y] = row_changes != 0; - any_changes = any_changes || row_changes != 0; + engine->row_dirty[y] = row_changes != 0; + driven_rows += row_changes != 0 ? 1 : 0; } - if (!any_changes) { + if (changed_rows == 0) { return true; } - t5s3_epd_build_line_mask(engine.line_mask, engine.changes); + changed->x0 = first_group * 8; + changed->x1 = (last_group + 1) * 8; + + // Pixels whose dither is what the panel shows already are only drawn by the refresh + if (driven_rows > 0) { + t5s3_epd_build_line_mask(engine->line_mask, engine->changes); + t5s3_epd_build_fast_table(engine->table); + engine->to = engine->fast; + bool success = true; + for (uint32_t phase = 0; phase < FAST_PHASES && success; phase++) { + const int64_t frame_start_us = esp_timer_get_time(); + success = t5s3_epd_board_frame(changed->y0, changed->y1, engine->row_dirty, FAST_HOLD_TICKS, prepare_row, nullptr); + pad_frame(frame_start_us, FAST_FRAME_US); + } + // A failed update leaves the back buffer alone, so the next update draws the same rows again + if (!success) { + LOG_E(TAG, "Update failed, a peripheral did not respond"); + return false; + } + for (int y = changed->y0; y < changed->y1; y++) { + if (engine->row_dirty[y]) { + memcpy(engine->back + y * T5S3_EPD_FB_ROW_BYTES, engine->fast + y * T5S3_EPD_FB_ROW_BYTES, T5S3_EPD_FB_ROW_BYTES); + } + } + } - engine.failed = false; - uint8_t table[256]; - if (mode == T5s3EpdMode::Fast) { - t5s3_epd_build_fast_table(table); - for (uint32_t phase = 0; phase < FAST_PHASES && !engine.failed; phase++) { - draw_frame(table, FAST_HOLD_TICKS); - taskYIELD(); + LOG_D(TAG, "Fast update of %dx%d at %d,%d took %d ms", static_cast(changed->x1 - changed->x0), static_cast(changed->y1 - changed->y0), + static_cast(changed->x0), static_cast(changed->y0), static_cast((esp_timer_get_time() - started) / 1000)); + return true; +} + +// Drives only the columns of the areas that cover the row +static void IRAM_ATTR prepare_refresh_row(int y, uint8_t* out, void*) { + memset(engine->line_mask, 0, sizeof(engine->line_mask)); + for (int i = 0; i < engine->rect_count; i++) { + const T5s3EpdRect& rect = engine->rects[i]; + if (y >= rect.y0 && y < rect.y1) { + // A mask byte covers 4 pixels + memset(engine->line_mask + rect.x0 / 4, 0xFF, (rect.x1 + 3) / 4 - rect.x0 / 4); + } + } + prepare_row(y, out, nullptr); +} + +static inline bool needs_refresh(int y, int x) { + const uint8_t front = engine->front[y * T5S3_EPD_FB_ROW_BYTES + x / 2]; + const uint8_t back = engine->back[y * T5S3_EPD_FB_ROW_BYTES + x / 2]; + const int shift = (x & 1) * 4; + const bool differs = ((front ^ back) >> shift & 0x0F) != 0; + return differs || (engine->touched[y * TOUCHED_ROW_BYTES + x / 8] >> (x & 7) & 1) != 0; +} + +// Bus rows of the settle frames during and after the shake, kept in the buffer of the fast update, which a refresh +// does not use. A row of that buffer holds both. +static inline uint8_t* settle_row(int y, bool shaking) { + return engine->fast + y * T5S3_EPD_FB_ROW_BYTES + (shaking ? 0 : T5S3_EPD_BUS_ROW_BYTES); +} + +static void build_settle_rows(int y) { + uint8_t* shake = settle_row(y, true); + uint8_t* after = settle_row(y, false); + memset(shake, 0, T5S3_EPD_BUS_ROW_BYTES); + memset(after, 0, T5S3_EPD_BUS_ROW_BYTES); + for (int i = 0; i < engine->rect_count; i++) { + const T5s3EpdRect& rect = engine->rects[i]; + if (y < rect.y0 || y >= rect.y1) { + continue; } - } else { - // Long updates sleep between frames so that the idle task can feed the watchdog - for (int phase = 0; phase < T5S3_EPD_QUALITY_PHASES && !engine.failed; phase++) { - t5s3_epd_build_quality_table(table, phase, mode == T5s3EpdMode::Full); - draw_frame(table, T5S3_EPD_QUALITY_HOLD[phase]); - vTaskDelay(1); + for (int x = rect.x0 / 4 * 4; x < (rect.x1 + 3) / 4 * 4; x++) { + if (!needs_refresh(y, x)) { + continue; + } + const uint8_t level = engine->front[y * T5S3_EPD_FB_ROW_BYTES + x / 2] >> ((x & 1) * 4) & 0x0F; + const int shift = (x & 3) * 2; + const uint8_t shake_action = level == 15 ? T5S3_EPD_ACTION_LIGHT : T5S3_EPD_ACTION_DARK; + const uint8_t after_action = level == 0 ? T5S3_EPD_ACTION_DARK : T5S3_EPD_ACTION_LIGHT; + shake[x / 4] |= static_cast(shake_action << shift); + after[x / 4] |= static_cast(after_action << shift); } } +} - // A failed update leaves the back buffer alone, so the next update draws the same rows again - if (engine.failed) { - LOG_E(TAG, "Update failed, a peripheral did not respond"); - return false; +static void IRAM_ATTR prepare_settle_row(int y, uint8_t* out, void*) { + memcpy(out, settle_row(y, engine->settle_frame < REFRESH_SHAKE_FRAMES), T5S3_EPD_BUS_ROW_BYTES); +} + +bool t5s3_epd_refresh(const T5s3EpdRect* rects, int count) { + const int64_t started = esp_timer_get_time(); + // Only rows with pixels that need a refresh are sent + int y_start = T5S3_EPD_HEIGHT; + int y_end = 0; + for (int i = 0; i < count; i++) { + const T5s3EpdRect& rect = rects[i]; + for (int y = rect.y0; y < rect.y1; y++) { + bool needed = false; + for (int x = rect.x0; x < rect.x1 && !needed; x++) { + needed = needs_refresh(y, x); + } + if (needed) { + y_start = y < y_start ? y : y_start; + y_end = y + 1 > y_end ? y + 1 : y_end; + } + } + } + if (y_start >= y_end) { + return true; } + engine->rects = rects; + engine->rect_count = count; for (int y = y_start; y < y_end; y++) { - if (engine.row_dirty[y]) { - memcpy(engine.back + y * T5S3_EPD_FB_ROW_BYTES, engine.front + y * T5S3_EPD_FB_ROW_BYTES, T5S3_EPD_FB_ROW_BYTES); + build_settle_rows(y); + } + bool success = true; + for (int frame = 0; frame < REFRESH_SETTLE_FRAMES && success; frame++) { + engine->settle_frame = frame; + const int64_t frame_start_us = esp_timer_get_time(); + success = t5s3_epd_board_frame(y_start, y_end, nullptr, FAST_HOLD_TICKS, prepare_settle_row, nullptr); + pad_frame(frame_start_us, FAST_FRAME_US); + } + + // Gray pixels are white now, black and white pixels show their level. The waveform starts from the white that its + // gray levels were measured from. + for (int i = 0; i < count && success; i++) { + const T5s3EpdRect& rect = rects[i]; + for (int y = rect.y0; y < rect.y1; y++) { + for (int x = rect.x0 / 4 * 4; x < (rect.x1 + 3) / 4 * 4; x++) { + if (!needs_refresh(y, x)) { + continue; + } + uint8_t* byte = &engine->back[y * T5S3_EPD_FB_ROW_BYTES + x / 2]; + const int shift = (x & 1) * 4; + const uint8_t level = engine->front[y * T5S3_EPD_FB_ROW_BYTES + x / 2] >> shift & 0x0F; + const uint8_t shown = level == 0 ? 0 : 15; + *byte = static_cast((*byte & ~(0x0F << shift)) | (shown << shift)); + engine->touched[y * TOUCHED_ROW_BYTES + x / 8] &= static_cast(~(1 << (x & 7))); + } } } - const int elapsed_ms = static_cast((esp_timer_get_time() - started) / 1000); - // Quality and slow updates are logged at info level, frequent fast updates would flood the log - if (mode != T5s3EpdMode::Fast || elapsed_ms > 150) { - LOG_I(TAG, "Updated rows %d to %d in %d ms", static_cast(y_start), static_cast(y_end), elapsed_ms); - } else { - LOG_D(TAG, "Updated rows %d to %d in %d ms", static_cast(y_start), static_cast(y_end), elapsed_ms); + engine->to = engine->front; + for (int phase = 0; phase < T5S3_EPD_QUALITY_PHASES && success; phase++) { + t5s3_epd_build_quality_table(engine->table, phase); + const int64_t frame_start_us = esp_timer_get_time(); + success = t5s3_epd_board_frame(y_start, y_end, nullptr, T5S3_EPD_QUALITY_HOLD[phase], prepare_refresh_row, nullptr); + pad_frame(frame_start_us, QUALITY_FRAME_US); + } + if (!success) { + LOG_E(TAG, "Refresh failed, a peripheral did not respond"); + return false; + } + + // The mask covers groups of 4 pixels, so whole groups were driven + for (int i = 0; i < count; i++) { + const T5s3EpdRect& rect = rects[i]; + const int x0 = rect.x0 / 4 * 2; + const int x1 = (rect.x1 + 3) / 4 * 2; + for (int y = rect.y0; y < rect.y1; y++) { + const size_t offset = y * T5S3_EPD_FB_ROW_BYTES + x0; + memcpy(engine->back + offset, engine->front + offset, x1 - x0); + } } + + LOG_D(TAG, "Refresh of %d areas in rows %d to %d took %d ms", count, y_start, y_end, static_cast((esp_timer_get_time() - started) / 1000)); return true; } -static void draw_clear_frames(uint8_t pattern, int count) { - for (int i = 0; i < count && !engine.failed; i++) { - draw_uniform_frame(pattern, CLEAR_HOLD_TICKS); +static bool draw_clear_frames(uint8_t pattern, int count) { + t5s3_epd_board_fill_rows(pattern); + bool success = true; + for (int i = 0; i < count && success; i++) { + success = t5s3_epd_board_frame(0, T5S3_EPD_HEIGHT, nullptr, CLEAR_HOLD_TICKS, nullptr, nullptr); vTaskDelay(1); } + return success; } bool t5s3_epd_clear() { const int64_t started = esp_timer_get_time(); t5s3_epd_fill_white(); - if (!t5s3_epd_update(T5s3EpdMode::Full, 0, T5S3_EPD_HEIGHT)) { + const T5s3EpdRect panel = { 0, 0, T5S3_EPD_WIDTH, T5S3_EPD_HEIGHT }; + if (!t5s3_epd_refresh(&panel, 1)) { return false; } - engine.failed = false; - for (int cycle = 0; cycle < CLEAR_CYCLES; cycle++) { - draw_clear_frames(ALL_DARK, CLEAR_DARK_FRAMES); - draw_clear_frames(ALL_LIGHT, CLEAR_LIGHT_FRAMES); - draw_clear_frames(0, CLEAR_REST_FRAMES); + bool success = true; + for (int cycle = 0; cycle < CLEAR_CYCLES && success; cycle++) { + success = draw_clear_frames(T5S3_EPD_ALL_DARK, CLEAR_DARK_FRAMES) && + draw_clear_frames(T5S3_EPD_ALL_LIGHT, CLEAR_LIGHT_FRAMES) && + draw_clear_frames(0, CLEAR_REST_FRAMES); } - if (engine.failed) { + if (!success) { LOG_E(TAG, "Clear failed, a peripheral did not respond"); return false; } LOG_I(TAG, "Cleared in %d ms", static_cast((esp_timer_get_time() - started) / 1000)); return true; } - -// endregion diff --git a/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd.h b/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd.h index f6dd6b898..e3355e704 100644 --- a/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd.h +++ b/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd.h @@ -1,7 +1,8 @@ // SPDX-License-Identifier: LGPL-3.0-or-later // -// Minimal driver for the ED047TC1 panel of the LILYGO T5 4.7 Inch E-Paper S3. -// The calls must be serialized by the user, the driver has no locking. +// Waveform updates of the ED047TC1 panel of the LILYGO T5 4.7 Inch E-Paper S3. +// The update logic follows the epdiy project (https://github.com/vroland/epdiy). +// The calls must come from one task, there is no locking. #pragma once @@ -9,13 +10,9 @@ #include -enum class T5s3EpdMode { - // Black and white only - Fast, - // 16 levels in 30 phases, white pixels that stay white are not touched - Quality, - // 16 levels in 30 phases, white pixels that stay white are flashed too, which clears ghosting - Full +// Columns [x0, x1) of the rows [y0, y1) +struct T5s3EpdRect { + int32_t x0, y0, x1, y1; }; // Allocates the frame buffers and sets up the pins and peripherals. The panel stays unpowered. @@ -23,32 +20,21 @@ bool t5s3_epd_init(uint32_t pixel_clock_hz); void t5s3_epd_deinit(); -// Frame buffer to draw in, 4 bits per pixel. The panel shows it after t5s3_epd_update(). +// Frame buffer to draw in, 4 bits per pixel. The panel shows it after an update. uint8_t* t5s3_epd_framebuffer(); -void t5s3_epd_power_on(); +// Black and white version of the frame buffer with levels 0 and 15 only, shown by fast updates +uint8_t* t5s3_epd_dither_buffer(); -void t5s3_epd_power_off(); +// Shows the pixels of the rows [y_start, y_end) whose level changed with their black and white dither, in a few frames. +// changed gets the area of the changed pixels, which still needs a refresh for its gray levels. Needs the panel to be powered. +bool t5s3_epd_show_changes(int32_t y_start, int32_t y_end, T5s3EpdRect* changed); -// Drives all pixels that changed in the rows [y_start, y_end) since the last update. Needs the panel to be powered. -bool t5s3_epd_update(T5s3EpdMode mode, int32_t y_start, int32_t y_end); +// Draws the pixels of the areas that do not show their level yet with the 16 level waveform, after driving them to white. +// Needs the panel to be powered. +bool t5s3_epd_refresh(const T5s3EpdRect* rects, int count); void t5s3_epd_fill_white(); -// Makes the next update drive every pixel -void t5s3_epd_invalidate(); - // Flashes the whole panel to white. Needs the panel to be powered. bool t5s3_epd_clear(); - -inline void t5s3_epd_set_pixel(uint8_t* framebuffer, int32_t x, int32_t y, uint8_t level) { - if (x < 0 || x >= T5S3_EPD_WIDTH || y < 0 || y >= T5S3_EPD_HEIGHT) { - return; - } - uint8_t* byte = &framebuffer[y * T5S3_EPD_FB_ROW_BYTES + x / 2]; - if (x % 2) { - *byte = (*byte & 0x0F) | static_cast(level << 4); - } else { - *byte = (*byte & 0xF0) | (level & 0x0F); - } -} diff --git a/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd_board.cpp b/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd_board.cpp new file mode 100644 index 000000000..49a326afd --- /dev/null +++ b/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd_board.cpp @@ -0,0 +1,433 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// +// Signalling of the ED047TC1 panel on the LILYGO T5 4.7 Inch E-Paper S3. +// The sequences and timings follow the epdiy project (https://github.com/vroland/epdiy). +// +// The panel is controlled by a 74HCT4094 shift register (latch, output enable, mode, start of frame and the +// panel supplies), the CKV gate clock (RMT peripheral) and an 8 bit bus (LCD peripheral in i80 mode) that +// carries the 2 bit actions of 4 pixels per byte. Rows are sent one at a time. The data of a row is applied by the +// CKV pulse after the one that was running during its transfer. + +#include "t5s3_epd_board.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +static constexpr gpio_num_t PIN_REGISTER_DATA = GPIO_NUM_13; +static constexpr gpio_num_t PIN_REGISTER_CLOCK = GPIO_NUM_12; +static constexpr gpio_num_t PIN_REGISTER_STROBE = GPIO_NUM_0; +static constexpr gpio_num_t PIN_CKV = GPIO_NUM_38; +// Start pulse of a row, driven as the data/command line of the bus +static constexpr gpio_num_t PIN_STH = GPIO_NUM_40; +// Pixel clock, driven as the write line of the bus +static constexpr gpio_num_t PIN_CKH = GPIO_NUM_41; +static constexpr gpio_num_t BUS_PINS[8] = {GPIO_NUM_6, GPIO_NUM_7, GPIO_NUM_4, GPIO_NUM_5, GPIO_NUM_2, GPIO_NUM_3, GPIO_NUM_8, GPIO_NUM_1}; + +// A row has headroom after the pixel data for the timing of the source driver +static constexpr int BUS_TRANSFER_BYTES = ((T5S3_EPD_WIDTH + 32) / 4 + 3) & ~3; + +// Time base of the CKV pulses, 10 MHz +static constexpr uint32_t PULSE_RESOLUTION_HZ = 10000000; +// Low time after the pulse of a row in ticks +static constexpr uint32_t ROW_LOW_TICKS = 50; + +// Limit for waiting on a flag that an interrupt handler sets, so that a lost interrupt fails the frame instead of spinning forever +static constexpr uint32_t WAIT_SPIN_LIMIT = 20000000; + +struct Board { + uint8_t* row_buffer[2]; + int current; + esp_lcd_i80_bus_handle_t bus; + esp_lcd_panel_io_handle_t io; + rmt_channel_handle_t pulse_channel; + rmt_encoder_handle_t pulse_encoder; + volatile bool transfer_done; + volatile bool pulse_done; + bool failed; + // Shift register + bool latch; + bool output_enable; + bool mode; + bool start_of_frame; + bool power_disable; + bool positive_power; + bool negative_power; + // Rows written since the last row that carried data + int rows_skipped; + rmt_symbol_word_t pulse_symbol; +}; + +// Interrupt handlers use it, so it stays in internal RAM +static Board board; + +static inline void IRAM_ATTR pin_set(gpio_num_t pin, uint32_t level) { + gpio_ll_set_level(GPIO_LL_GET_HW(GPIO_PORT_0), pin, level); +} + +static bool IRAM_ATTR on_pulse_done(rmt_channel_handle_t, const rmt_tx_done_event_data_t*, void*) { + board.pulse_done = true; + return false; +} + +static bool IRAM_ATTR on_transfer_done(esp_lcd_panel_io_handle_t, esp_lcd_panel_io_event_data_t*, void*) { + board.transfer_done = true; + return false; +} + +// After a failure nothing is waited for anymore, so the rest of the frame ends quickly +static void IRAM_ATTR wait_for(volatile bool& flag) { + uint32_t spins = 0; + while (!flag) { + if (board.failed || ++spins > WAIT_SPIN_LIMIT) { + board.failed = true; + flag = true; + } + } +} + +// region Shift register + +static void IRAM_ATTR push_register_bit(bool bit) { + pin_set(PIN_REGISTER_CLOCK, 0); + pin_set(PIN_REGISTER_DATA, bit ? 1 : 0); + pin_set(PIN_REGISTER_CLOCK, 1); +} + +static void IRAM_ATTR push_register() { + pin_set(PIN_REGISTER_STROBE, 0); + push_register_bit(board.output_enable); + push_register_bit(board.mode); + // Always set + push_register_bit(true); + push_register_bit(board.start_of_frame); + push_register_bit(board.negative_power); + push_register_bit(board.positive_power); + push_register_bit(board.power_disable); + push_register_bit(board.latch); + pin_set(PIN_REGISTER_STROBE, 1); +} + +// endregion + +// region CKV pulses and row transfer + +// Pulses the gate clock for high_ticks and keeps it low for low_ticks, repeated for the given number of pulses. +// Without a high time the line is high for low_ticks. +static void IRAM_ATTR pulse_ckv(uint32_t high_ticks, uint32_t low_ticks, bool wait, int count = 1) { + wait_for(board.pulse_done); + rmt_symbol_word_t& symbol = board.pulse_symbol; + if (high_ticks > 0) { + symbol.duration0 = high_ticks; + symbol.level0 = 1; + symbol.duration1 = low_ticks; + symbol.level1 = 0; + } else { + symbol.duration0 = low_ticks; + symbol.level0 = 1; + symbol.duration1 = 0; + symbol.level1 = 0; + } + board.pulse_done = false; + rmt_transmit_config_t config = {}; + config.loop_count = count; + if (rmt_transmit(board.pulse_channel, board.pulse_encoder, &symbol, sizeof(symbol), &config) != ESP_OK) { + board.pulse_done = true; + board.failed = true; + return; + } + if (wait) { + wait_for(board.pulse_done); + } +} + +static void IRAM_ATTR pulse_ckv_us(uint32_t high_us, uint32_t low_us, bool wait) { + pulse_ckv(high_us * 10, low_us * 10, wait); +} + +static void IRAM_ATTR start_transfer() { + board.transfer_done = false; + if (esp_lcd_panel_io_tx_color(board.io, 0, board.row_buffer[board.current], BUS_TRANSFER_BYTES) != ESP_OK) { + board.transfer_done = true; + board.failed = true; + } +} + +// Applies the data that was sent last and starts the transfer of the current row buffer while the CKV pulse runs +static void IRAM_ATTR output_row(uint32_t hold_ticks) { + wait_for(board.transfer_done); + wait_for(board.pulse_done); + board.latch = true; + push_register(); + board.latch = false; + push_register(); + pulse_ckv(hold_ticks, ROW_LOW_TICKS, false); + start_transfer(); + board.current ^= 1; +} + +static void IRAM_ATTR write_row(uint32_t hold_ticks) { + output_row(hold_ticks); + board.rows_skipped = 0; +} + +// Skips a row after rows with data. The first two skipped rows push blank data through the pipeline. +static void IRAM_ATTR skip_row(uint32_t hold_ticks) { + memset(board.row_buffer[board.current], 0, T5S3_EPD_BUS_ROW_BYTES); + output_row(hold_ticks); + board.rows_skipped++; +} + +// Clocks the gate driver through rows that carry no data with one train of pulses +static void IRAM_ATTR skip_rows(int count) { + pulse_ckv(45, 5, false, count); + board.rows_skipped += count; +} + +static void IRAM_ATTR start_frame() { + wait_for(board.transfer_done); + wait_for(board.pulse_done); + board.rows_skipped = 0; + + board.mode = true; + push_register(); + pulse_ckv_us(1, 1, true); + + // The start of frame line must go low and high again while the long pulse runs + board.start_of_frame = false; + push_register(); + pulse_ckv_us(1000, 100, false); + board.start_of_frame = true; + push_register(); + for (int i = 0; i < 4; i++) { + pulse_ckv_us(1, 1, true); + } + + board.output_enable = true; + push_register(); +} + +static void IRAM_ATTR end_frame() { + board.start_of_frame = false; + push_register(); + for (int i = 0; i < 5; i++) { + pulse_ckv_us(1, 1, true); + } + board.mode = false; + push_register(); + pulse_ckv_us(0, 10, true); + board.output_enable = false; + push_register(); + for (int i = 0; i < 3; i++) { + pulse_ckv_us(1, 1, true); + } +} + +// endregion + +// region Frames + +void t5s3_epd_board_fill_rows(uint8_t value) { + wait_for(board.transfer_done); + memset(board.row_buffer[0], value, T5S3_EPD_BUS_ROW_BYTES); + memset(board.row_buffer[1], value, T5S3_EPD_BUS_ROW_BYTES); +} + +static inline bool IRAM_ATTR has_data(int y, int y0, int y1, const bool* rows) { + return y >= y0 && y < y1 && (rows == nullptr || rows[y]); +} + +bool IRAM_ATTR t5s3_epd_board_frame(int y0, int y1, const bool* rows, uint32_t hold_ticks, T5s3EpdRowFn fill, void* context) { + board.failed = false; + start_frame(); + int y = 0; + bool ends_with_train = false; + while (y < T5S3_EPD_HEIGHT && !board.failed) { + ends_with_train = false; + if (has_data(y, y0, y1, rows)) { + // The other buffer may still be in flight while this one is filled + if (fill != nullptr) { + fill(y, board.row_buffer[board.current], context); + } + write_row(hold_ticks); + y++; + } else if (board.rows_skipped < 2) { + skip_row(hold_ticks); + y++; + } else { + int run = 1; + while (y + run < T5S3_EPD_HEIGHT && !has_data(y + run, y0, y1, rows)) { + run++; + } + skip_rows(run); + ends_with_train = true; + y += run; + } + } + // The last row is applied by one more pulse + if (board.rows_skipped == 0 && !board.failed) { + write_row(hold_ticks); + } else if (ends_with_train) { + // The end of the frame must not overlap the train of pulses of the skipped rows + wait_for(board.pulse_done); + } + end_frame(); + return !board.failed; +} + +// endregion + +// region Setup + +static bool setup_register_pins() { + gpio_config_t config = {}; + config.pin_bit_mask = (1ULL << PIN_REGISTER_DATA) | (1ULL << PIN_REGISTER_CLOCK) | (1ULL << PIN_REGISTER_STROBE); + config.mode = GPIO_MODE_OUTPUT; + if (gpio_config(&config) != ESP_OK) { + return false; + } + pin_set(PIN_REGISTER_STROBE, 0); + return true; +} + +static bool setup_bus(uint32_t pixel_clock_hz) { + esp_lcd_i80_bus_config_t bus_config = {}; + bus_config.dc_gpio_num = PIN_STH; + bus_config.wr_gpio_num = PIN_CKH; + bus_config.clk_src = LCD_CLK_SRC_DEFAULT; + for (int i = 0; i < 8; i++) { + bus_config.data_gpio_nums[i] = BUS_PINS[i]; + } + bus_config.bus_width = 8; + bus_config.max_transfer_bytes = BUS_TRANSFER_BYTES; + if (esp_lcd_new_i80_bus(&bus_config, &board.bus) != ESP_OK) { + return false; + } + + // The data/command line is low when idle, high during a dummy command phase and low during the data of a row + esp_lcd_panel_io_i80_config_t io_config = {}; + io_config.cs_gpio_num = GPIO_NUM_NC; + io_config.pclk_hz = pixel_clock_hz; + io_config.trans_queue_depth = 4; + io_config.on_color_trans_done = on_transfer_done; + io_config.lcd_cmd_bits = 10; + io_config.lcd_param_bits = 0; + io_config.dc_levels.dc_idle_level = 0; + io_config.dc_levels.dc_cmd_level = 1; + io_config.dc_levels.dc_dummy_level = 0; + io_config.dc_levels.dc_data_level = 0; + return esp_lcd_new_panel_io_i80(board.bus, &io_config, &board.io) == ESP_OK; +} + +static bool setup_pulse() { + rmt_tx_channel_config_t channel_config = {}; + channel_config.gpio_num = PIN_CKV; + channel_config.clk_src = RMT_CLK_SRC_DEFAULT; + channel_config.resolution_hz = PULSE_RESOLUTION_HZ; + channel_config.mem_block_symbols = 48; + channel_config.trans_queue_depth = 1; + if (rmt_new_tx_channel(&channel_config, &board.pulse_channel) != ESP_OK) { + return false; + } + rmt_copy_encoder_config_t encoder_config = {}; + if (rmt_new_copy_encoder(&encoder_config, &board.pulse_encoder) != ESP_OK) { + return false; + } + rmt_tx_event_callbacks_t callbacks = {}; + callbacks.on_trans_done = on_pulse_done; + if (rmt_tx_register_event_callbacks(board.pulse_channel, &callbacks, nullptr) != ESP_OK) { + return false; + } + return rmt_enable(board.pulse_channel) == ESP_OK; +} + +bool t5s3_epd_board_init(uint32_t pixel_clock_hz) { + for (auto& buffer: board.row_buffer) { + buffer = static_cast(heap_caps_aligned_calloc(16, 1, BUS_TRANSFER_BYTES, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)); + } + if (board.row_buffer[0] == nullptr || board.row_buffer[1] == nullptr || !setup_register_pins() || !setup_bus(pixel_clock_hz) || !setup_pulse()) { + t5s3_epd_board_deinit(); + return false; + } + + board.latch = false; + board.output_enable = false; + board.mode = false; + board.start_of_frame = true; + board.power_disable = true; + board.positive_power = false; + board.negative_power = false; + board.current = 0; + board.transfer_done = true; + board.pulse_done = true; + push_register(); + return true; +} + +void t5s3_epd_board_deinit() { + if (board.pulse_channel != nullptr) { + // Both calls fail harmlessly when the channel was not enabled + rmt_disable(board.pulse_channel); + rmt_del_channel(board.pulse_channel); + board.pulse_channel = nullptr; + } + if (board.pulse_encoder != nullptr) { + rmt_del_encoder(board.pulse_encoder); + board.pulse_encoder = nullptr; + } + if (board.io != nullptr) { + esp_lcd_panel_io_del(board.io); + board.io = nullptr; + } + if (board.bus != nullptr) { + esp_lcd_del_i80_bus(board.bus); + board.bus = nullptr; + } + heap_caps_free(board.row_buffer[0]); + heap_caps_free(board.row_buffer[1]); + board.row_buffer[0] = nullptr; + board.row_buffer[1] = nullptr; +} + +// endregion + +// region Power + +void t5s3_epd_board_power_on() { + board.power_disable = false; + push_register(); + esp_rom_delay_us(100); + board.negative_power = true; + push_register(); + esp_rom_delay_us(500); + board.positive_power = true; + push_register(); + esp_rom_delay_us(100); + board.start_of_frame = true; + push_register(); +} + +void t5s3_epd_board_power_off() { + board.positive_power = false; + push_register(); + esp_rom_delay_us(10); + board.negative_power = false; + push_register(); + esp_rom_delay_us(100); + board.start_of_frame = false; + board.output_enable = false; + board.mode = false; + board.power_disable = true; + push_register(); +} + +// endregion diff --git a/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd_board.h b/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd_board.h new file mode 100644 index 000000000..bc6d87714 --- /dev/null +++ b/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd_board.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// +// Signalling of the ED047TC1 panel on the LILYGO T5 4.7 Inch E-Paper S3. +// The sequences and timings follow the epdiy project (https://github.com/vroland/epdiy). +// The calls must come from one task, there is no locking. + +#pragma once + +#include + +constexpr int T5S3_EPD_WIDTH = 960; +constexpr int T5S3_EPD_HEIGHT = 540; + +// A bus row holds 2 bits per pixel with the first of four pixels in the lowest bits. +// 00 and 11 hold a pixel, 01 drives it dark and 10 drives it light. +constexpr int T5S3_EPD_BUS_ROW_BYTES = T5S3_EPD_WIDTH / 4; + +constexpr uint8_t T5S3_EPD_ALL_DARK = 0x55; +constexpr uint8_t T5S3_EPD_ALL_LIGHT = 0xAA; + +// Fills the bus row of panel row y. Bytes the callback does not write keep their content. +typedef void (*T5s3EpdRowFn)(int y, uint8_t* row, void* context); + +// Sets up the pins and peripherals. The panel stays unpowered. +bool t5s3_epd_board_init(uint32_t pixel_clock_hz); + +void t5s3_epd_board_deinit(); + +void t5s3_epd_board_power_on(); + +void t5s3_epd_board_power_off(); + +// Sets every byte of both row buffers, which is what rows that the callback leaves alone contain +void t5s3_epd_board_fill_rows(uint8_t value); + +// Drives one frame. The rows [y0, y1) that are set in rows get data from fill and are held for hold_ticks of 0.1 us, +// all other rows are skipped without driving their pixels. Without rows, all rows of the range get data. +// Returns false when a peripheral did not respond. +bool t5s3_epd_board_frame(int y0, int y1, const bool* rows, uint32_t hold_ticks, T5s3EpdRowFn fill, void* context); diff --git a/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd_rows.h b/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd_rows.h index 34f36befc..8cacdc22a 100644 --- a/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd_rows.h +++ b/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd_rows.h @@ -5,6 +5,7 @@ #pragma once +#include "t5s3_epd_board.h" #include "t5s3_epd_waveform.h" #include @@ -16,30 +17,24 @@ #define T5S3_EPD_IRAM #endif -constexpr int T5S3_EPD_WIDTH = 960; -constexpr int T5S3_EPD_HEIGHT = 540; - // Framebuffer rows hold 4 bits per pixel with the even pixel of a pair in the low nibble, 0x0 is black and 0xF is white constexpr int T5S3_EPD_FB_ROW_BYTES = T5S3_EPD_WIDTH / 2; constexpr int T5S3_EPD_FB_BYTES = T5S3_EPD_FB_ROW_BYTES * T5S3_EPD_HEIGHT; -// Rows on the bus hold 2 bits per pixel with the first of four pixels in the lowest bits -constexpr int T5S3_EPD_BUS_ROW_BYTES = T5S3_EPD_WIDTH / 4; - constexpr uint8_t T5S3_EPD_ACTION_HOLD = 0; constexpr uint8_t T5S3_EPD_ACTION_DARK = 1; constexpr uint8_t T5S3_EPD_ACTION_LIGHT = 2; // Tables map (target level << 4 | source level) to the action for one phase of an update. -// Fast update: pixels that end up black are driven dark and pixels that end up white are driven light +// Fast update: changed pixels with a black target are driven dark and changed pixels with a white target light. inline void t5s3_epd_build_fast_table(uint8_t* table) { for (int to = 0; to < 16; to++) { for (int from = 0; from < 16; from++) { uint8_t action = T5S3_EPD_ACTION_HOLD; - if (to == 0 && from != 0) { + if (to != from && to == 0) { action = T5S3_EPD_ACTION_DARK; - } else if (to == 15 && from != 15) { + } else if (to != from && to == 15) { action = T5S3_EPD_ACTION_LIGHT; } table[(to << 4) | from] = action; @@ -47,9 +42,8 @@ inline void t5s3_epd_build_fast_table(uint8_t* table) { } } -// The waveform drives white pixels that stay white with a dark and a light pulse, which flashes them. -// Holding them instead leaves the rest of an area alone, so an update of a part of the screen does not leave visible columns. -inline void t5s3_epd_build_quality_table(uint8_t* table, int phase, bool flash_white) { +// Pixels that keep their level are held, so a refresh only drives the pixels that need it +inline void t5s3_epd_build_quality_table(uint8_t* table, int phase) { const uint8_t* packed = T5S3_EPD_QUALITY_LUT[phase]; for (int to = 0; to < 16; to++) { for (int group = 0; group < 4; group++) { @@ -61,8 +55,8 @@ inline void t5s3_epd_build_quality_table(uint8_t* table, int phase, bool flash_w table[index + 3] = actions & 3; } } - if (!flash_white) { - table[(15 << 4) | 15] = T5S3_EPD_ACTION_HOLD; + for (int level = 0; level < 16; level++) { + table[(level << 4) | level] = T5S3_EPD_ACTION_HOLD; } } From e17f918b3a0e609117aac9a8d39709373ad392a8 Mon Sep 17 00:00:00 2001 From: AdaSzi115026 <81822538+AdaSzi@users.noreply.github.com> Date: Sun, 25 Oct 2026 23:40:35 +0100 Subject: [PATCH 2/6] T5S3 e-paper: grayscale driver with per pixel doses instead of the waveform - lilygo,t5s3-display draws 7 shades with a dose of frames per pixel, in the manner of InkBoy - Frames of 40 ms cover the whole screen, so all changes show at once, the next frame is encoded on the other core - Every gray is reached the same way (15 light frames, a frame of rest, then its dark frames), so moving content leaves almost no trails - A frame that stalls makes the grays it interrupted start again - Only pixels whose shade changes are driven. refresh() flashes and draws the content again, the driver also does that when the content changed without touches (at most every 10 minutes) - The epdiy waveform and the update logic derived from epdiy are removed, only the board signalling stays LGPL - Doses, frame counts and shade brightness were measured with a camera on the panel --- Drivers/lilygo-t5s3-epd-module/CMakeLists.txt | 1 - Drivers/lilygo-t5s3-epd-module/README.md | 19 +- .../bindings/lilygo,t5s3-display.yaml | 11 +- .../source/t5s3_display.cpp | 840 ++++++++++++------ .../source/t5s3_epd.cpp | 422 --------- .../lilygo-t5s3-epd-module/source/t5s3_epd.h | 40 - .../source/t5s3_epd_rows.h | 100 --- .../source/t5s3_epd_waveform.h | 201 ----- 8 files changed, 592 insertions(+), 1042 deletions(-) delete mode 100644 Drivers/lilygo-t5s3-epd-module/source/t5s3_epd.cpp delete mode 100644 Drivers/lilygo-t5s3-epd-module/source/t5s3_epd.h delete mode 100644 Drivers/lilygo-t5s3-epd-module/source/t5s3_epd_rows.h delete mode 100644 Drivers/lilygo-t5s3-epd-module/source/t5s3_epd_waveform.h diff --git a/Drivers/lilygo-t5s3-epd-module/CMakeLists.txt b/Drivers/lilygo-t5s3-epd-module/CMakeLists.txt index 0a357f317..001a3d22e 100644 --- a/Drivers/lilygo-t5s3-epd-module/CMakeLists.txt +++ b/Drivers/lilygo-t5s3-epd-module/CMakeLists.txt @@ -12,7 +12,6 @@ tactility_add_module(lilygo-t5s3-epd-module # The pixel loops of the display are too slow with the debug optimization level of the project set_source_files_properties( - source/t5s3_epd.cpp source/t5s3_epd_board.cpp source/t5s3_display_bw.cpp source/t5s3_display.cpp diff --git a/Drivers/lilygo-t5s3-epd-module/README.md b/Drivers/lilygo-t5s3-epd-module/README.md index 805ad3d02..81aa7399c 100644 --- a/Drivers/lilygo-t5s3-epd-module/README.md +++ b/Drivers/lilygo-t5s3-epd-module/README.md @@ -6,32 +6,25 @@ The drivers only work on this board. The pins of the panel, the 74HCT4094 shift There are two drivers. A devicetree uses one of them, the device `lilygo-t5-epd47-s3` uses the black and white one: -- `lilygo,t5s3-display` (`t5s3_display.cpp`): 16 gray levels. `draw_bitmap()` returns without waiting for the panel. A task shows each change at once with a black and white dither and draws the gray levels of a changed area once it had no change for 3 seconds. Only the pixels that need it are driven, so there is no flash of the whole screen. -- `lilygo,t5s3-display-bw` (`t5s3_display_bw.cpp`): black and white with a pixel state machine instead of waveform tables, like the M5Stack PaperS3 driver. Gray levels are dithered. It is the fastest and does not flash outside of `clear()` and `refresh()`. +- `lilygo,t5s3-display` (`t5s3_display.cpp`): 7 shades from black to white, dithered between them. Each pixel whose target shade changes gets a dose of frames, in the manner of [InkBoy](https://github.com/kuslota/InkBoy): black is driven dark for 12 frames, white and the grays are driven light for 15 frames, which erases what the pixel showed before, and the grays then rest for a frame and are driven dark for 1 to 6 frames. Every gray is reached the same way, so moving content leaves almost no trails. Frames of 40 ms cover the whole screen, so all changes show at once: black and white within a few frames, a gray after about 0.9 s. `refresh()` flashes the panel and draws the content again, and the driver does that by itself when the content changed without touches for 10 minutes. A frame that stalls, for example while the flash is written, makes the grays it interrupted start again. `draw_bitmap()` does not wait for the panel and pixels that do not change are never driven. It needs the pixel clock of 20 MHz of its binding. +- `lilygo,t5s3-display-bw` (`t5s3_display_bw.cpp`): black and white with a pixel state machine, like the M5Stack PaperS3 driver. Gray levels are dithered. It is the fastest. Shared and internal files: - `t5s3_epd_board.cpp`: the signalling of the board. It controls the shift register, the CKV gate clock (RMT peripheral) and the row transfer (LCD peripheral in i80 mode), and sends one frame at a time. -- `t5s3_epd.cpp`: the updates of the grayscale driver: the fast black and white update and the refresh with the waveform. -- `t5s3_epd_rows.h`: the conversion of framebuffer rows into bus data. It has no hardware access. -- `t5s3_epd_waveform.h`: the 30 phase drive table for 16 gray levels. -Frames are padded to a fixed period, because a pixel keeps its drive until its row is addressed again. The frame counts and the gray levels in the code were measured with a camera on the panel. +A pixel keeps its drive until its row is addressed again, so frames are padded to a fixed period. The doses, the frame counts and the shade brightness in the code were measured with a camera on the panel. ## License -The board signalling, the update logic of the grayscale driver and the waveform are derived from [epdiy](https://github.com/vroland/epdiy) and are licensed under the +The board signalling is derived from [epdiy](https://github.com/vroland/epdiy) and is licensed under the [LGPL v3.0 or later](LICENSE-LGPL-3.0.md) (the LGPL supplements the [GPL v3.0](../../Documentation/LICENSE-GPL-3.0.md)): -- `source/t5s3_epd.cpp` -- `source/t5s3_epd.h` - `source/t5s3_epd_board.cpp` - `source/t5s3_epd_board.h` -- `source/t5s3_epd_rows.h` -- `source/t5s3_epd_waveform.h` -These files follow the sequences and timings of epdiy and contain its ED047TC1 waveform data. Each of them has an `SPDX-License-Identifier: LGPL-3.0-or-later` header. +These files follow the sequences and timings of epdiy. Each of them has an `SPDX-License-Identifier: LGPL-3.0-or-later` header. -The rest of the module (`source/t5s3_display.cpp`, `source/t5s3_display_bw.cpp`, `source/module.cpp`, the bindings and the headers in `include/`) is licensed under [Apache License v2.0](LICENSE-Apache-2.0.md). The pixel state machine in `t5s3_display_bw.cpp` follows the M5Stack PaperS3 display driver of Tactility. +The rest of the module (`source/t5s3_display.cpp`, `source/t5s3_display_bw.cpp`, `source/module.cpp`, the bindings and the headers in `include/`) is licensed under [Apache License v2.0](LICENSE-Apache-2.0.md). The pixel state machine in `t5s3_display_bw.cpp` follows the M5Stack PaperS3 display driver of Tactility. The dose method of `t5s3_display.cpp` follows the description of InkBoy (MIT), no code of InkBoy is used. A project that contains this module has to follow the LGPL for the files above. diff --git a/Drivers/lilygo-t5s3-epd-module/bindings/lilygo,t5s3-display.yaml b/Drivers/lilygo-t5s3-epd-module/bindings/lilygo,t5s3-display.yaml index 47f6ac120..2aff212e8 100644 --- a/Drivers/lilygo-t5s3-epd-module/bindings/lilygo,t5s3-display.yaml +++ b/Drivers/lilygo-t5s3-epd-module/bindings/lilygo,t5s3-display.yaml @@ -1,11 +1,14 @@ description: > - LILYGO T5 4.7 Inch E-Paper S3 (V2.3/V2.4) display with an ED047TC1 panel. The pins of the panel - are fixed in the driver, so this node only has the pixel clock as property. + LILYGO T5 4.7 Inch E-Paper S3 (V2.3/V2.4) display with an ED047TC1 panel, driven in 7 shades with a dose of + frames per pixel instead of waveform tables. Updates do not block and leave no trails on gray backgrounds. + The pins of the panel are fixed in the driver, so this node only has the pixel clock as property. compatible: "lilygo,t5s3-display" properties: pixel-clock-hz: type: int - default: 10000000 - description: Clock of the bus that sends the pixel data of a row. Faster speeds up updates if the panel accepts it. + default: 20000000 + description: > + Clock of the bus that sends the pixel data of a row. The doses of the shades were measured with 20 MHz, + which lets a frame of all rows fit in its period. diff --git a/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp b/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp index 1eff78ed7..af51d9c29 100644 --- a/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp +++ b/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp @@ -9,46 +9,109 @@ #include #include -#include "t5s3_epd.h" +#include "t5s3_epd_board.h" +#include #include #include #include #include #include -#include +#include #include #define TAG "T5s3Display" #define GET_CONFIG(device) (static_cast((device)->config)) -// draw_bitmap() writes 16 gray levels into a staging buffer and returns. A task shows each change with a fast update -// that dithers the changed pixels to black and white. Once an area had no change for REFRESH_DELAY_MS, a refresh -// draws its gray levels and clears the ghosting. - -// Areas that wait for a refresh, more are merged -static constexpr int MAX_AREAS = 8; - -// A refresh takes more than a second and delays the changes that come during it, so it waits for a pause. -// Until then the changed areas show their dither. -static constexpr uint32_t REFRESH_DELAY_MS = 3000; - -// Brightness of the levels that look different, measured with a camera. Levels 10 to 14 look like white. -static constexpr int TONE_STEPS = 11; -static constexpr uint8_t TONE_LEVELS[TONE_STEPS] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 15 }; -static constexpr uint8_t TONE_BRIGHTNESS[TONE_STEPS] = { 14, 17, 26, 52, 81, 111, 129, 143, 151, 155, 158 }; - -// Darker luminance is drawn black. The edges of text then keep the weight that the dither of the fast update gives them. -static constexpr int BLACK_LUMINANCE = 64; +// Gray levels are drawn without waveform tables, with a dose of frames per pixel in the manner of InkBoy +// (https://github.com/kuslota/InkBoy). A pixel whose target shade changes gets a program: black is driven dark for +// BLACK_FRAMES frames. White and the grays are driven light for LIGHT_FRAMES frames, which erases what the pixel showed +// before. The grays then rest for HOLD_FRAMES frames and are driven dark for the dose of their shade. A gray then looks +// the same whatever the pixel came from and moving content leaves no trails. Pixels whose target does not change are +// never driven. +// +// Every frame covers the rows from the top down to the lowest row of the current updates, so all of them change at once. +// The helper task encodes the next frame while the display task sends the current one. + +static constexpr int PANEL_WIDTH = T5S3_EPD_WIDTH; +static constexpr int PANEL_HEIGHT = T5S3_EPD_HEIGHT; + +// Shades are 4 bits per pixel with the even pixel of a pair in the low nibble. Programs are 1 byte per pixel. +static constexpr size_t SHADE_ROW_BYTES = PANEL_WIDTH / 2; +static constexpr size_t SHADE_SIZE = SHADE_ROW_BYTES * PANEL_HEIGHT; +static constexpr size_t PROGRAM_SIZE = static_cast(PANEL_WIDTH) * PANEL_HEIGHT; +static constexpr size_t BUS_SIZE = static_cast(T5S3_EPD_BUS_ROW_BYTES) * PANEL_HEIGHT; + +// Rows are split in chunks of 32 pixels, a bit per chunk tells which ones have work +static constexpr int CHUNK_PIXELS = 32; +static constexpr int CHUNKS = PANEL_WIDTH / CHUNK_PIXELS; +static_assert(CHUNKS <= 32); + +// Shades from black to white. The dose of a gray is its number of dark frames after the light frames. +// The brightness of each shade was measured with a camera, 0 is black and 255 is white. +static constexpr int SHADES = 7; +static constexpr uint8_t SHADE_BLACK = 0; +static constexpr uint8_t SHADE_WHITE = SHADES - 1; +static constexpr uint8_t SHADE_DOSE[SHADES] = { 0, 6, 4, 3, 2, 1, 0 }; +static constexpr uint8_t SHADE_BRIGHTNESS[SHADES] = { 0, 28, 54, 94, 145, 199, 255 }; + +// Between 10 and 30 light frames the grays come out the same whatever the pixel showed before, fewer leave traces +static constexpr uint8_t LIGHT_FRAMES = 15; +static constexpr uint8_t BLACK_FRAMES = 12; +static constexpr uint8_t HOLD_FRAMES = 1; + +// A program byte is 0 for a pixel at rest, the dark frames left below HOLD_BASE, HOLD_BASE plus the frames of rest left +// below LIGHT_BASE and LIGHT_BASE plus the light frames left above. Rest and the dark frames of the goal follow the light frames. +static constexpr uint8_t HOLD_BASE = 32; +static constexpr uint8_t LIGHT_BASE = 64; + +// Shade nibbles of white pixels +static constexpr uint8_t WHITE_PAIR = SHADE_WHITE | (SHADE_WHITE << 4); + +// The dose of a frame depends on its period, so every frame has this period, measured from start to start. A frame of +// all rows takes about 34 ms with a pixel clock of 20 MHz, the doses were measured with that clock. +static constexpr int64_t FRAME_PERIOD_US = 40000; + +// Gate pulse of a row in units of 0.1 us +static constexpr uint32_t ROW_HOLD_TICKS = 120; + +// Groups of 8 pixels that get new programs in a frame. A larger change takes a few frames to start, so its encoding +// does not delay the next frame. The first frame of an update has no limit. +static constexpr int RETARGET_WORDS = 6000; + +// A frame that comes this much late or takes longer than the period gave some pixels a clearly wrong dose +static constexpr int64_t STALL_US = 10000; +// A frame can also come late because its encoding took longer than the period, for example when many pixels end their +// light frames at once. That only lengthens one frame, so it counts as a stall from this lateness on. +static constexpr int64_t ENCODING_STALL_US = 150000; + +// Frames whose chunks a restart of the doses covers. A stall extends the drive of the frame before it and of the stalled +// frame, and the encoding runs one frame ahead. +static constexpr int RECENT_FRAMES = 3; + +// Rows are timed by busy waiting, so the display task runs above the LVGL task. The helper task encodes on the other core. +static constexpr UBaseType_t TASK_PRIORITY = 7; +static constexpr BaseType_t TASK_CORE = 0; +static constexpr BaseType_t HELPER_CORE = 1; + +// Frames of the flash that clear() runs on the whole panel +static constexpr int FLASH_DARK_FRAMES = 12; +static constexpr int FLASH_LIGHT_FRAMES = 16; static constexpr uint32_t IDLE_POWER_OFF_MS = 2000; +// Pixels that rested for minutes keep faint traces of what they showed, which only a flash removes. Tactility calls +// refresh() after the user stopped touching the screen. Content that changes without touches gets a refresh from the +// driver once it was quiet for AUTO_REFRESH_QUIET_MS, at most every MINIMAL_AUTO_REFRESH_INTERVAL_MS. +static constexpr int64_t MINIMAL_AUTO_REFRESH_INTERVAL_MS = 10 * 60 * 1000; +static constexpr int64_t AUTO_REFRESH_QUIET_MS = 5000; + static constexpr uint32_t CMD_UPDATE = 1U << 0; -static constexpr uint32_t CMD_CLEAR = 1U << 1; -static constexpr uint32_t CMD_REFRESH = 1U << 2; -static constexpr uint32_t CMD_POWER_OFF = 1U << 3; -static constexpr uint32_t CMD_EXIT = 1U << 4; +static constexpr uint32_t CMD_FLASH = 1U << 1; +static constexpr uint32_t CMD_POWER_OFF = 1U << 2; +static constexpr uint32_t CMD_EXIT = 1U << 3; +static constexpr uint32_t CMD_REFRESH = 1U << 4; // 4x4 ordered (Bayer) dither thresholds static constexpr uint8_t BAYER_4X4[4][4] = { @@ -58,104 +121,301 @@ static constexpr uint8_t BAYER_4X4[4][4] = { { 15, 7, 13, 5 }, }; -// A luminance lies between two levels of the panel and is dithered between them +// A luminance lies between two shades and is dithered between them struct Tone { uint8_t darker; uint8_t lighter; - // Share of the lighter level, 0 to 16 + // Share of the lighter shade, 0 to 16 uint8_t share; }; static void build_tones(Tone* tones) { for (int luminance = 0; luminance < 256; luminance++) { - if (luminance < BLACK_LUMINANCE) { - tones[luminance] = { 0, 0, 0 }; - continue; + int shade = 0; + while (shade < SHADES - 2 && luminance > SHADE_BRIGHTNESS[shade + 1]) { + shade++; } - const float brightness = TONE_BRIGHTNESS[0] + (TONE_BRIGHTNESS[TONE_STEPS - 1] - TONE_BRIGHTNESS[0]) * luminance / 255.0f; - int step = 0; - while (step < TONE_STEPS - 2 && brightness > TONE_BRIGHTNESS[step + 1]) { - step++; - } - const float share = (brightness - TONE_BRIGHTNESS[step]) / (TONE_BRIGHTNESS[step + 1] - TONE_BRIGHTNESS[step]); - tones[luminance].darker = TONE_LEVELS[step]; - tones[luminance].lighter = TONE_LEVELS[step + 1]; - tones[luminance].share = static_cast(std::clamp(static_cast(share * 16.0f + 0.5f), 0, 16)); + const int range = SHADE_BRIGHTNESS[shade + 1] - SHADE_BRIGHTNESS[shade]; + const int share = ((luminance - SHADE_BRIGHTNESS[shade]) * 16 + range / 2) / range; + tones[luminance].darker = static_cast(shade); + tones[luminance].lighter = static_cast(shade + 1); + tones[luminance].share = static_cast(std::clamp(share, 0, 16)); } } -// Share of white pixels, 0 to 16, that the black and white dither of a level gets -static constexpr uint8_t white_share(int level) { - for (int step = 0; step < TONE_STEPS; step++) { - if (TONE_LEVELS[step] == level) { - return static_cast((TONE_BRIGHTNESS[step] - TONE_BRIGHTNESS[0]) * 16 / (TONE_BRIGHTNESS[TONE_STEPS - 1] - TONE_BRIGHTNESS[0])); - } - } - // Unused levels look like white - return 16; -} +extern "C" { -static constexpr uint8_t WHITE_SHARES[16] = { - white_share(0), white_share(1), white_share(2), white_share(3), white_share(4), white_share(5), white_share(6), white_share(7), - white_share(8), white_share(9), white_share(10), white_share(11), white_share(12), white_share(13), white_share(14), white_share(15), +// Result of encoding a frame +struct Encoded { + bool driven; + // Rows [0, rows) hold all driven pixels + int rows; }; -static inline uint8_t dither_to_level(const Tone& tone, int32_t x, int32_t y) { - return tone.share > BAYER_4X4[y & 3][x & 3] ? tone.lighter : tone.darker; -} - -extern "C" { - -// Rows [y0, y1) -struct Rows { +// Work of one encoding call: rows [y0, y1) into bus, with a budget of RETARGET_WORDS or none when negative +struct EncodeJob { + uint8_t* bus; int y0, y1; - - bool empty() const { return y1 <= y0; } - - void merge(const Rows& other) { - if (other.empty()) return; - if (empty()) { - *this = other; - return; - } - y0 = std::min(y0, other.y0); - y1 = std::max(y1, other.y1); - } + int budget; + Encoded result; + int64_t duration_us; }; -static constexpr Rows NO_ROWS = { 0, 0 }; - struct T5s3DisplayInternal { TaskHandle_t task; SemaphoreHandle_t request_mutex; SemaphoreHandle_t request_done; - // Guards staging - SemaphoreHandle_t staging_mutex; portMUX_TYPE lock; volatile bool exited; bool powered; - // Result of the last clear, set by the task - bool clear_succeeded; - // Rows changed in staging and not yet picked up by the task, guarded by lock - Rows pending; - // Time of the oldest change not yet shown and the longest draw_bitmap() call since, guarded by lock + // Time of the oldest change not yet picked up and the longest draw_bitmap() call since, guarded by lock int64_t pending_since_us; int64_t draw_max_us; - // 4 bit levels written by draw_bitmap(), copied into the frame buffer of the panel by the task - uint8_t* staging; - // Panel levels for each luminance + // Chunks of each row with new targets, set by draw_bitmap() and taken by the encoder + std::atomic* dirty; + // Chunks of each row with running programs, only used by the encoder + uint32_t* running; + // Chunks of each row that ran in each of the last frames, only used by the encoder + uint32_t* recent[RECENT_FRAMES]; + int recent_index; + // Shade that draw_bitmap() wants for each pixel + uint8_t* target; + // Shade that the program of each pixel draws or has drawn + uint8_t* goal; + // Program of each pixel, see LIGHT_BASE + uint8_t* program; + // Bus rows of two frames, the one being sent and the next one + uint8_t* bus[2]; + // Bus rows of the frame being sent + const uint8_t* sending; + // Encoding on the other core + TaskHandle_t helper; + SemaphoreHandle_t helper_start; + SemaphoreHandle_t helper_done; + volatile bool helper_exit; + volatile bool helper_exited; + EncodeJob helper_job; + // Set by the display task after a late or stalled frame, the helper task then restarts the doses + bool restart; + // Shades for each luminance Tone* tones; }; -static inline void set_level(uint8_t* buffer, int32_t x, int32_t y, uint8_t level) { - uint8_t* byte = &buffer[y * T5S3_EPD_FB_ROW_BYTES + x / 2]; - if (x % 2) { - *byte = (*byte & 0x0F) | static_cast(level << 4); - } else { - *byte = (*byte & 0xF0) | (level & 0x0F); +static inline uint8_t get_nibble(const uint8_t* buffer, int x) { + return (buffer[x / 2] >> ((x & 1) * 4)) & 0x0F; +} + +static inline void set_nibble(uint8_t* buffer, int x, uint8_t value) { + uint8_t* byte = &buffer[x / 2]; + const int shift = (x & 1) * 4; + *byte = static_cast((*byte & ~(0x0F << shift)) | (value << shift)); +} + +// region Pixel programs + +// Gives the pixels of a chunk whose target changed a new program. A pixel that is still in its light frames keeps them, +// so its gray is reached the same way. Returns the groups of 8 pixels that had changes. +static int retarget_chunk(T5s3DisplayInternal* internal, int y, int chunk) { + const int x0 = chunk * CHUNK_PIXELS; + uint8_t* program = internal->program + y * PANEL_WIDTH + x0; + uint8_t* goal = internal->goal + y * SHADE_ROW_BYTES + x0 / 2; + const auto* target_words = reinterpret_cast(internal->target + y * SHADE_ROW_BYTES + x0 / 2); + auto* goal_words = reinterpret_cast(goal); + int changed = 0; + for (int word = 0; word < CHUNK_PIXELS / 8; word++) { + // Most chunks that LVGL draws again keep their shades + const uint32_t wanted = target_words[word]; + const uint32_t current = goal_words[word]; + if (wanted == current) continue; + changed++; + for (int i = 0; i < 8; i++) { + const uint8_t shade = (wanted >> (i * 4)) & 0x0F; + if (shade == ((current >> (i * 4)) & 0x0F)) continue; + uint8_t& frames = program[word * 8 + i]; + if (shade == SHADE_BLACK) { + frames = BLACK_FRAMES; + } else if (frames <= LIGHT_BASE) { + frames = LIGHT_BASE + LIGHT_FRAMES; + } + } + goal_words[word] = wanted; } + return changed; +} + +// Advances the programs of the pixels of a chunk by one frame and writes their actions, 2 bits per pixel with the first +// pixel in the lowest bits, 01 drives dark and 10 drives light. Sets driven when a pixel is driven and returns whether +// a program still runs. Four programs are stepped at once in a 32 bit word, all program bytes are below 128. +static bool step_chunk(T5s3DisplayInternal* internal, int y, int chunk, uint8_t* out, bool* driven) { + const int x0 = chunk * CHUNK_PIXELS; + auto* words = reinterpret_cast(internal->program + y * PANEL_WIDTH + x0); + const uint8_t* goal = internal->goal + y * SHADE_ROW_BYTES + x0 / 2; + uint32_t running = 0; + for (int word = 0; word < CHUNK_PIXELS / 4; word++) { + uint32_t value = words[word]; + if (value == 0) { + out[word] = 0; + continue; + } + // Top bit of each byte: the program runs. Bit 6: the pixel is driven light. Bit 5 without bit 6: it rests. + const uint32_t running_bits = ((value & 0x7F7F7F7F) + 0x7F7F7F7F) & 0x80808080; + const uint32_t light_bits = value & 0x40404040; + const uint32_t hold_bits = value & 0x20202020 & ~(light_bits >> 1); + value -= running_bits >> 7; + // Pixels whose light frames end rest, pixels whose rest ends get the dark frames of their goal + const uint32_t light_ended = value ^ 0x40404040; + const uint32_t hold_ended = value ^ 0x20202020; + if ((light_bits != 0 && ((light_ended - 0x01010101) & ~light_ended & 0x80808080) != 0) || + (hold_bits != 0 && ((hold_ended - 0x01010101) & ~hold_ended & 0x80808080) != 0)) { + for (int i = 0; i < 4; i++) { + const uint32_t frames = (value >> (i * 8)) & 0xFF; + const int x = word * 4 + i; + uint32_t next = frames; + if (frames == LIGHT_BASE) { + next = SHADE_DOSE[get_nibble(goal, x)] == 0 ? 0 : HOLD_BASE + HOLD_FRAMES; + } else if (frames == HOLD_BASE) { + next = SHADE_DOSE[get_nibble(goal, x)]; + } + value = (value & ~(0xFFU << (i * 8))) | (next << (i * 8)); + } + } + words[word] = value; + running |= value; + // 2 bits per pixel at the bottom of each byte, then packed into one byte + const uint32_t light = light_bits >> 6; + const uint32_t dark = (running_bits >> 7) & ~light & ~(hold_bits >> 5); + const uint32_t actions = (light << 1) | dark; + *driven |= (light | dark) != 0; + out[word] = static_cast((actions & 0x03) | ((actions >> 6) & 0x0C) | ((actions >> 12) & 0x30) | ((actions >> 18) & 0xC0)); + } + return running != 0; +} + +// Encodes the rows of a job for the next frame. Chunks with new targets beyond the budget keep them for a later frame. +static void encode(T5s3DisplayInternal* internal, EncodeJob* job) { + const int64_t start_us = esp_timer_get_time(); + uint32_t* recent = internal->recent[internal->recent_index]; + int budget = job->budget; + job->result = { false, 0 }; + for (int y = job->y0; y < job->y1; y++) { + uint8_t* out = job->bus + y * T5S3_EPD_BUS_ROW_BYTES; + const uint32_t dirty = budget != 0 ? internal->dirty[y].exchange(0) : 0; + uint32_t deferred = 0; + uint32_t work = dirty; + while (work != 0) { + const int chunk = __builtin_ctz(work); + work &= work - 1; + if (budget == 0) { + deferred |= 1U << chunk; + continue; + } + const int changed = retarget_chunk(internal, y, chunk); + if (budget > 0) { + budget = std::max(0, budget - changed); + } + internal->running[y] |= 1U << chunk; + } + if (deferred != 0) { + internal->dirty[y].fetch_or(deferred); + } + work = internal->running[y]; + recent[y] = work; + if (work == 0) { + memset(out, 0, T5S3_EPD_BUS_ROW_BYTES); + continue; + } + uint32_t running = 0; + bool driven = false; + for (int chunk = 0; chunk < CHUNKS; chunk++) { + uint8_t* chunk_out = out + chunk * CHUNK_PIXELS / 4; + if ((work & (1U << chunk)) == 0) { + memset(chunk_out, 0, CHUNK_PIXELS / 4); + } else if (step_chunk(internal, y, chunk, chunk_out, &driven)) { + running |= 1U << chunk; + } + } + internal->running[y] = running; + if (driven) { + job->result.driven = true; + job->result.rows = y + 1; + } + } + job->duration_us = esp_timer_get_time() - start_us; +} + +// The encoders of a frame write to the same slot of recent chunks, the next frame uses the next slot +static void next_recent(T5s3DisplayInternal* internal) { + internal->recent_index = (internal->recent_index + 1) % RECENT_FRAMES; +} + +// Starts the programs of the gray pixels of the chunks that ran in the last frames again. A frame that came late or +// stalled, for example while the flash was written, gave the ones in their rest or dark frames a wrong dose, also the +// ones whose program ended since. +static void restart_doses(T5s3DisplayInternal* internal) { + for (int y = 0; y < PANEL_HEIGHT; y++) { + uint32_t work = 0; + for (const uint32_t* recent : internal->recent) { + work |= recent[y]; + } + internal->running[y] |= work; + while (work != 0) { + const int chunk = __builtin_ctz(work); + work &= work - 1; + const int x0 = chunk * CHUNK_PIXELS; + uint8_t* program = internal->program + y * PANEL_WIDTH + x0; + const uint8_t* goal = internal->goal + y * SHADE_ROW_BYTES + x0 / 2; + for (int x = 0; x < CHUNK_PIXELS; x++) { + const uint8_t shade = get_nibble(goal, x); + if (program[x] > LIGHT_BASE || shade == SHADE_BLACK || shade == SHADE_WHITE) continue; + program[x] = LIGHT_BASE + LIGHT_FRAMES; + } + } + } +} + +static void helper_task(void* arg) { + auto* internal = static_cast(arg); + while (true) { + xSemaphoreTake(internal->helper_start, portMAX_DELAY); + if (internal->helper_exit) break; + if (internal->restart) { + internal->restart = false; + restart_doses(internal); + } + encode(internal, &internal->helper_job); + xSemaphoreGive(internal->helper_done); + } + internal->helper_exited = true; + vTaskDelete(nullptr); +} + +static void start_helper(T5s3DisplayInternal* internal, uint8_t* bus, int y0, int y1, int budget) { + next_recent(internal); + internal->helper_job = { bus, y0, y1, budget, { false, 0 }, 0 }; + xSemaphoreGive(internal->helper_start); } +static Encoded finish_helper(T5s3DisplayInternal* internal) { + xSemaphoreTake(internal->helper_done, portMAX_DELAY); + return internal->helper_job.result; +} + +// Encodes a whole frame on both cores without a budget +static Encoded encode_now(T5s3DisplayInternal* internal, uint8_t* bus) { + start_helper(internal, bus, PANEL_HEIGHT / 2, PANEL_HEIGHT, -1); + EncodeJob job = { bus, 0, PANEL_HEIGHT / 2, -1, { false, 0 }, 0 }; + encode(internal, &job); + const Encoded lower = finish_helper(internal); + return { job.result.driven || lower.driven, lower.driven ? lower.rows : job.result.rows }; +} + +static void IRAM_ATTR copy_bus_row(int y, uint8_t* out, void* context) { + auto* internal = static_cast(context); + memcpy(out, internal->sending + y * T5S3_EPD_BUS_ROW_BYTES, T5S3_EPD_BUS_ROW_BYTES); +} + +// endregion + // region Task static void power_on(T5s3DisplayInternal* internal) { @@ -170,118 +430,92 @@ static void power_off(T5s3DisplayInternal* internal) { internal->powered = false; } -// Copies the rows into the frame buffer and their black and white dither into the dither buffer -static void copy_staging(T5s3DisplayInternal* internal, const Rows& rows) { - const size_t offset = rows.y0 * T5S3_EPD_FB_ROW_BYTES; - uint8_t* framebuffer = t5s3_epd_framebuffer(); - xSemaphoreTake(internal->staging_mutex, portMAX_DELAY); - memcpy(framebuffer + offset, internal->staging + offset, (rows.y1 - rows.y0) * T5S3_EPD_FB_ROW_BYTES); - xSemaphoreGive(internal->staging_mutex); - - uint8_t* dither = t5s3_epd_dither_buffer(); - for (int y = rows.y0; y < rows.y1; y++) { - const uint8_t* levels = framebuffer + y * T5S3_EPD_FB_ROW_BYTES; - uint8_t* out = dither + y * T5S3_EPD_FB_ROW_BYTES; - const uint8_t* thresholds = BAYER_4X4[y & 3]; - for (int i = 0; i < T5S3_EPD_FB_ROW_BYTES; i++) { - const int x = i * 2; - const uint8_t first = WHITE_SHARES[levels[i] & 0x0F] > thresholds[x & 3] ? 0x0F : 0x00; - const uint8_t second = WHITE_SHARES[levels[i] >> 4] > thresholds[(x + 1) & 3] ? 0xF0 : 0x00; - out[i] = first | second; - } +// Drives every pixel toward one color +static bool run_solid_frames(uint8_t pattern, int frames) { + t5s3_epd_board_fill_rows(pattern); + bool success = true; + for (int frame = 0; frame < frames && success; frame++) { + success = t5s3_epd_board_frame(0, PANEL_HEIGHT, nullptr, ROW_HOLD_TICKS, nullptr, nullptr); } + return success; } -static bool run_clear(T5s3DisplayInternal* internal) { +// Flashes the panel to white, all pixels show white afterwards +static void run_flash(T5s3DisplayInternal* internal) { + const int64_t start_us = esp_timer_get_time(); power_on(internal); - xSemaphoreTake(internal->staging_mutex, portMAX_DELAY); - memset(internal->staging, 0xFF, T5S3_EPD_FB_BYTES); - xSemaphoreGive(internal->staging_mutex); - t5s3_epd_fill_white(); - const T5s3EpdRect panel = { 0, 0, T5S3_EPD_WIDTH, T5S3_EPD_HEIGHT }; - return t5s3_epd_refresh(&panel, 1); -} - -// Areas that got fast updates and still need a refresh for their gray levels -struct Areas { - T5s3EpdRect rects[MAX_AREAS]; - // Time of the last change of each area - int64_t changed_us[MAX_AREAS]; - int count; -}; - -static int64_t rect_size(const T5s3EpdRect& rect) { - return static_cast(rect.x1 - rect.x0) * (rect.y1 - rect.y0); -} - -static T5s3EpdRect rect_union(const T5s3EpdRect& a, const T5s3EpdRect& b) { - return { std::min(a.x0, b.x0), std::min(a.y0, b.y0), std::max(a.x1, b.x1), std::max(a.y1, b.y1) }; -} - -// Merges the area into the one whose union grows the least, unless it is far from all of them and there is room -static void add_area(Areas* areas, const T5s3EpdRect& rect, int64_t now_us) { - int best = -1; - int64_t best_growth = 0; - for (int i = 0; i < areas->count; i++) { - const T5s3EpdRect& other = areas->rects[i]; - const int64_t growth = rect_size(rect_union(other, rect)) - rect_size(other) - rect_size(rect); - if (best < 0 || growth < best_growth) { - best = i; - best_growth = growth; - } + const bool success = run_solid_frames(T5S3_EPD_ALL_DARK, FLASH_DARK_FRAMES) && + run_solid_frames(T5S3_EPD_ALL_LIGHT, FLASH_LIGHT_FRAMES) && + run_solid_frames(0, 1); + if (!success) { + LOG_E(TAG, "Flash failed, a peripheral did not respond"); + } + memset(internal->goal, WHITE_PAIR, SHADE_SIZE); + memset(internal->program, 0, PROGRAM_SIZE); + memset(internal->running, 0, PANEL_HEIGHT * sizeof(uint32_t)); + for (uint32_t* recent : internal->recent) { + memset(recent, 0, PANEL_HEIGHT * sizeof(uint32_t)); } - if (best >= 0 && (best_growth <= 0 || areas->count == MAX_AREAS)) { - areas->rects[best] = rect_union(areas->rects[best], rect); - areas->changed_us[best] = now_us; - } else { - areas->rects[areas->count] = rect; - areas->changed_us[areas->count] = now_us; - areas->count++; + for (int y = 0; y < PANEL_HEIGHT; y++) { + internal->dirty[y].store(0); } + LOG_I(TAG, "Flashed in %d ms", static_cast((esp_timer_get_time() - start_us) / 1000)); } -// Each area waits for its own pause, so changes elsewhere do not hold it back -static int64_t next_refresh_us(const Areas& areas) { - int64_t next_us = INT64_MAX; - for (int i = 0; i < areas.count; i++) { - next_us = std::min(next_us, areas.changed_us[i] + REFRESH_DELAY_MS * 1000LL); +// Flashes the panel and draws all pixels that are not white again +static void run_refresh(T5s3DisplayInternal* internal) { + run_flash(internal); + constexpr uint32_t ALL_CHUNKS = CHUNKS == 32 ? ~0U : (1U << CHUNKS) - 1; + for (int y = 0; y < PANEL_HEIGHT; y++) { + internal->dirty[y].store(ALL_CHUNKS); } - return next_us; -} - -// Refreshes the areas that are due, or all of them -static void refresh_areas(Areas* areas, bool all) { - Areas due = {}; - Areas waiting = {}; - const int64_t now_us = esp_timer_get_time(); - for (int i = 0; i < areas->count; i++) { - Areas* target = all || areas->changed_us[i] + REFRESH_DELAY_MS * 1000LL <= now_us ? &due : &waiting; - target->rects[target->count] = areas->rects[i]; - target->changed_us[target->count] = areas->changed_us[i]; - target->count++; - } - if (due.count == 0) { - return; - } - // A failed refresh keeps its areas, so they are tried again - if (!t5s3_epd_refresh(due.rects, due.count)) { - return; +} + +// Waits for the start of the next frame, mostly asleep so other tasks run and the idle task feeds the watchdog +static void wait_until(int64_t time_us) { + const int64_t remaining_us = time_us - esp_timer_get_time(); + if (remaining_us > 2000) { + vTaskDelay(pdMS_TO_TICKS((remaining_us - 1000) / 1000)); } - *areas = waiting; + while (esp_timer_get_time() < time_us) {} } static void display_task(void* arg) { auto* internal = static_cast(arg); - Areas areas = {}; + bool active = false; + int current = 0; + Encoded frame = { false, 0 }; + // Rows of the frames of the current update, they only grow so that every row keeps its timing + int rows = 0; + int64_t next_frame_us = 0; + // Statistics of the current update + int frames = 0; + int64_t since_us = 0; + int64_t longest_frame_us = 0; + int64_t longest_encode_us = 0; + // The encoding of the next frame ended after its start time + bool encoding_late = false; + // Updates since the last refresh, except the one that draws the refreshed content + bool changed_since_refresh = false; + bool refresh_update = false; + int64_t last_refresh_us = esp_timer_get_time(); + int64_t last_change_us = 0; while (true) { uint32_t commands = 0; TickType_t timeout = portMAX_DELAY; - if (areas.count > 0) { - const int64_t wait_us = std::max(0, next_refresh_us(areas) - esp_timer_get_time()); - timeout = pdMS_TO_TICKS(wait_us / 1000 + 1); - } else if (internal->powered) { - timeout = pdMS_TO_TICKS(IDLE_POWER_OFF_MS); + const int64_t refresh_due_us = changed_since_refresh ? + std::max(last_refresh_us + MINIMAL_AUTO_REFRESH_INTERVAL_MS * 1000, last_change_us + AUTO_REFRESH_QUIET_MS * 1000) : INT64_MAX; + if (active) { + timeout = 0; + } else { + int64_t wait_us = internal->powered ? IDLE_POWER_OFF_MS * 1000LL : INT64_MAX; + if (refresh_due_us != INT64_MAX) { + wait_us = std::min(wait_us, std::max(0, refresh_due_us - esp_timer_get_time())); + } + if (wait_us != INT64_MAX) { + timeout = pdMS_TO_TICKS(wait_us / 1000 + 1); + } } const bool notified = xTaskNotifyWait(0, UINT32_MAX, &commands, timeout) == pdTRUE; @@ -290,56 +524,86 @@ static void display_task(void* arg) { } if (commands & CMD_POWER_OFF) { + active = false; power_off(internal); } - if (commands & CMD_CLEAR) { - // The clear draws all rows, so earlier changes are included - portENTER_CRITICAL(&internal->lock); - internal->pending = NO_ROWS; - internal->pending_since_us = 0; - internal->draw_max_us = 0; - portEXIT_CRITICAL(&internal->lock); - internal->clear_succeeded = run_clear(internal); - areas.count = 0; + if (commands & CMD_FLASH) { + run_flash(internal); + active = false; + changed_since_refresh = false; + last_refresh_us = esp_timer_get_time(); xSemaphoreGive(internal->request_done); } - // A refresh of the whole panel only drives the pixels that do not show their level - const bool refresh_all = (commands & CMD_REFRESH) != 0; - if (refresh_all) { - areas.rects[0] = { 0, 0, T5S3_EPD_WIDTH, T5S3_EPD_HEIGHT }; - areas.changed_us[0] = esp_timer_get_time(); - areas.count = 1; + if ((commands & CMD_REFRESH) || (!active && esp_timer_get_time() >= refresh_due_us)) { + run_refresh(internal); + active = false; + changed_since_refresh = false; + refresh_update = true; + last_refresh_us = esp_timer_get_time(); } - Rows pending; - int64_t pending_since_us; - int64_t draw_max_us; - portENTER_CRITICAL(&internal->lock); - pending = internal->pending; - pending_since_us = internal->pending_since_us; - draw_max_us = internal->draw_max_us; - internal->pending = NO_ROWS; - internal->pending_since_us = 0; - internal->draw_max_us = 0; - portEXIT_CRITICAL(&internal->lock); - - if (!pending.empty()) { - copy_staging(internal, pending); - power_on(internal); - T5s3EpdRect changed; - if (t5s3_epd_show_changes(pending.y0, pending.y1, &changed) && changed.x1 > changed.x0) { - add_area(&areas, changed, esp_timer_get_time()); + if (!active) { + current = 0; + frame = encode_now(internal, internal->bus[current]); + if (!frame.driven) { + if (!notified) { + power_off(internal); + } + continue; } - LOG_D(TAG, "Rows %d to %d shown after %d ms (draw_bitmap up to %d us)", pending.y0, pending.y1, - static_cast((esp_timer_get_time() - pending_since_us) / 1000), static_cast(draw_max_us)); - } else if (areas.count > 0 && (refresh_all || next_refresh_us(areas) <= esp_timer_get_time())) { + active = true; + changed_since_refresh |= !refresh_update; + refresh_update = false; + rows = 0; + frames = 0; + longest_frame_us = 0; + longest_encode_us = 0; + portENTER_CRITICAL(&internal->lock); + since_us = internal->pending_since_us; + const int64_t draw_max_us = internal->draw_max_us; + internal->pending_since_us = 0; + internal->draw_max_us = 0; + portEXIT_CRITICAL(&internal->lock); + LOG_D(TAG, "Update starts %d ms after the first change (draw_bitmap up to %d us)", + since_us != 0 ? static_cast((esp_timer_get_time() - since_us) / 1000) : -1, static_cast(draw_max_us)); power_on(internal); - refresh_areas(&areas, refresh_all); - } else if (!notified && areas.count == 0) { - power_off(internal); + next_frame_us = esp_timer_get_time(); + } + + // The next frame is encoded while this one is sent. A frame that drives nothing ends the drive of the one before. + start_helper(internal, internal->bus[current ^ 1], 0, PANEL_HEIGHT, RETARGET_WORDS); + rows = std::max(rows, frame.rows); + internal->sending = internal->bus[current]; + wait_until(next_frame_us); + const int64_t frame_start_us = esp_timer_get_time(); + const int64_t late_us = frame_start_us - next_frame_us; + if (!t5s3_epd_board_frame(0, rows, nullptr, ROW_HOLD_TICKS, copy_bus_row, internal)) { + LOG_E(TAG, "Frame failed, a peripheral did not respond"); + } + const int64_t frame_us = esp_timer_get_time() - frame_start_us; + longest_frame_us = std::max(longest_frame_us, frame_us); + next_frame_us = frame_start_us + FRAME_PERIOD_US; + frames++; + // A late or stalled frame drives some pixels for longer than their dose + if (late_us > (encoding_late ? ENCODING_STALL_US : STALL_US) || frame_us > FRAME_PERIOD_US + STALL_US) { + internal->restart = true; + LOG_W(TAG, "Frame %d us late and %d us long, doses start again", static_cast(late_us), static_cast(frame_us)); } + const Encoded next = finish_helper(internal); + encoding_late = esp_timer_get_time() > next_frame_us; + longest_encode_us = std::max(longest_encode_us, internal->helper_job.duration_us); + + if (!frame.driven && !next.driven) { + LOG_D(TAG, "Drew rows 0 to %d in %d frames, longest frame %d us, longest encoding %d us", rows, frames, + static_cast(longest_frame_us), static_cast(longest_encode_us)); + active = false; + last_change_us = esp_timer_get_time(); + continue; + } + current ^= 1; + frame = next; } power_off(internal); @@ -347,13 +611,20 @@ static void display_task(void* arg) { vTaskDelete(nullptr); } -static error_t clear_blocking(T5s3DisplayInternal* internal) { +// endregion + +// region Helpers + +static void notify_task(T5s3DisplayInternal* internal, uint32_t command) { + xTaskNotify(internal->task, command, eSetBits); +} + +static error_t flash_blocking(T5s3DisplayInternal* internal) { xSemaphoreTake(internal->request_mutex, portMAX_DELAY); - xTaskNotify(internal->task, CMD_CLEAR, eSetBits); + notify_task(internal, CMD_FLASH); xSemaphoreTake(internal->request_done, portMAX_DELAY); - const bool success = internal->clear_succeeded; xSemaphoreGive(internal->request_mutex); - return success ? ERROR_NONE : ERROR_RESOURCE; + return ERROR_NONE; } // endregion @@ -363,7 +634,7 @@ static error_t clear_blocking(T5s3DisplayInternal* internal) { static error_t t5s3_display_reset(Device* device) { auto* internal = static_cast(device_get_driver_data(device)); // The panel has no reset line, it powers up again with the next update - xTaskNotify(internal->task, CMD_POWER_OFF, eSetBits); + notify_task(internal, CMD_POWER_OFF); return ERROR_NONE; } @@ -372,52 +643,56 @@ static error_t t5s3_display_init(Device*) { return ERROR_NONE; } -/** Fills the panel white, without the flashing cycles of the initial clear */ +/** Flashes the panel to white */ static error_t t5s3_display_clear(Device* device) { - return clear_blocking(static_cast(device_get_driver_data(device))); + auto* internal = static_cast(device_get_driver_data(device)); + memset(internal->target, WHITE_PAIR, SHADE_SIZE); + return flash_blocking(internal); } -/** Draws the gray levels of all pixels that show a dither now, without waiting for it */ +/** Flashes the panel and draws the content again, which removes all ghosting. Does not wait for the panel. */ static error_t t5s3_display_refresh(Device* device) { auto* internal = static_cast(device_get_driver_data(device)); - xTaskNotify(internal->task, CMD_REFRESH, eSetBits); + notify_task(internal, CMD_REFRESH); return ERROR_NONE; } -// Reports GRAYSCALE8 so that draw_bitmap() is called per changed tile +// Reports GRAYSCALE8 so that draw_bitmap() is called per changed tile. The luminance is dithered between the shades. static error_t t5s3_display_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) { auto* internal = static_cast(device_get_driver_data(device)); const int64_t start_us = esp_timer_get_time(); - - // color_data is 8-bit luminance (0x00 black to 0xFF white) and the panel wants 4-bit levels (0x0 black to 0xF white) - // Level 15 is white, levels 10 to 14 are not used - const auto* src = static_cast(color_data); + const auto* source = static_cast(color_data); const int32_t width = x_end - x_start; - xSemaphoreTake(internal->staging_mutex, portMAX_DELAY); + const int first_chunk = x_start / CHUNK_PIXELS; + const int last_chunk = (x_end - 1) / CHUNK_PIXELS; + const uint32_t chunks = (last_chunk == 31 ? ~0U : (1U << (last_chunk + 1)) - 1) & ~((1U << first_chunk) - 1); for (int32_t y = y_start; y < y_end; y++) { - const uint8_t* src_row = src + (y - y_start) * width; + const uint8_t* source_row = source + (y - y_start) * width; + const uint8_t* thresholds = BAYER_4X4[y & 3]; + uint8_t* target_row = internal->target + y * SHADE_ROW_BYTES; for (int32_t x = x_start; x < x_end; x++) { - set_level(internal->staging, x, y, dither_to_level(internal->tones[src_row[x - x_start]], x, y)); + const Tone& tone = internal->tones[source_row[x - x_start]]; + set_nibble(target_row, x, tone.share > thresholds[x & 3] ? tone.lighter : tone.darker); } + // The targets are written before their chunks are marked + internal->dirty[y].fetch_or(chunks); } - xSemaphoreGive(internal->staging_mutex); const int64_t draw_us = esp_timer_get_time() - start_us; portENTER_CRITICAL(&internal->lock); - internal->pending.merge({ y_start, y_end }); if (internal->pending_since_us == 0) { internal->pending_since_us = start_us; } internal->draw_max_us = std::max(internal->draw_max_us, draw_us); portEXIT_CRITICAL(&internal->lock); - xTaskNotify(internal->task, CMD_UPDATE, eSetBits); + notify_task(internal, CMD_UPDATE); return ERROR_NONE; } static error_t t5s3_display_disp_on_off(Device* device, bool on_off) { auto* internal = static_cast(device_get_driver_data(device)); if (!on_off) { - xTaskNotify(internal->task, CMD_POWER_OFF, eSetBits); + notify_task(internal, CMD_POWER_OFF); } return ERROR_NONE; } @@ -427,11 +702,11 @@ static DisplayColorFormat t5s3_display_get_color_format(Device*) { } static uint16_t t5s3_display_get_resolution_x(Device*) { - return T5S3_EPD_WIDTH; + return PANEL_WIDTH; } static uint16_t t5s3_display_get_resolution_y(Device*) { - return T5S3_EPD_HEIGHT; + return PANEL_HEIGHT; } // endregion @@ -468,11 +743,29 @@ static const DisplayApi t5s3_display_api = { // region Driver lifecycle +static void stop_helper(T5s3DisplayInternal* internal) { + internal->helper_exit = true; + xSemaphoreGive(internal->helper_start); + while (!internal->helper_exited) { + vTaskDelay(1); + } +} + static void destroy(T5s3DisplayInternal* internal) { if (internal->request_mutex) vSemaphoreDelete(internal->request_mutex); if (internal->request_done) vSemaphoreDelete(internal->request_done); - if (internal->staging_mutex) vSemaphoreDelete(internal->staging_mutex); - memory_free(internal->staging); + if (internal->helper_start) vSemaphoreDelete(internal->helper_start); + if (internal->helper_done) vSemaphoreDelete(internal->helper_done); + memory_free(internal->dirty); + memory_free(internal->running); + for (uint32_t* recent : internal->recent) { + memory_free(recent); + } + memory_free(internal->target); + memory_free(internal->goal); + memory_free(internal->program); + memory_free(internal->bus[0]); + memory_free(internal->bus[1]); memory_free(internal->tones); memory_free(internal); } @@ -489,46 +782,71 @@ static error_t start(Device* device) { internal->lock = portMUX_INITIALIZER_UNLOCKED; internal->request_mutex = xSemaphoreCreateMutex(); internal->request_done = xSemaphoreCreateBinary(); - internal->staging_mutex = xSemaphoreCreateMutex(); - internal->staging = static_cast(memory_alloc_with_policy(T5S3_EPD_FB_BYTES, &external_policy)); + internal->helper_start = xSemaphoreCreateBinary(); + internal->helper_done = xSemaphoreCreateBinary(); + // The chunk masks are read for every row of every frame, so they stay in internal RAM + internal->dirty = static_cast*>(memory_calloc_with_policy(PANEL_HEIGHT, sizeof(std::atomic), &internal_policy)); + internal->running = static_cast(memory_calloc_with_policy(PANEL_HEIGHT, sizeof(uint32_t), &internal_policy)); + bool recent_allocated = true; + for (uint32_t*& recent : internal->recent) { + recent = static_cast(memory_calloc_with_policy(PANEL_HEIGHT, sizeof(uint32_t), &internal_policy)); + recent_allocated &= recent != nullptr; + } + internal->target = static_cast(memory_alloc_with_policy(SHADE_SIZE, &external_policy)); + internal->goal = static_cast(memory_alloc_with_policy(SHADE_SIZE, &external_policy)); + internal->program = static_cast(memory_calloc_with_policy(1, PROGRAM_SIZE, &external_policy)); + internal->bus[0] = static_cast(memory_alloc_with_policy(BUS_SIZE, &external_policy)); + internal->bus[1] = static_cast(memory_alloc_with_policy(BUS_SIZE, &external_policy)); internal->tones = static_cast(memory_alloc_with_policy(256 * sizeof(Tone), &external_policy)); - if (!internal->request_mutex || !internal->request_done || !internal->staging_mutex || !internal->staging || !internal->tones) { + if (!recent_allocated || !internal->request_mutex || !internal->request_done || !internal->helper_start || !internal->helper_done || !internal->dirty || + !internal->running || !internal->target || !internal->goal || !internal->program || !internal->bus[0] || !internal->bus[1] || + !internal->tones) { destroy(internal); return ERROR_OUT_OF_MEMORY; } - memset(internal->staging, 0xFF, T5S3_EPD_FB_BYTES); + memset(internal->target, WHITE_PAIR, SHADE_SIZE); + memset(internal->goal, WHITE_PAIR, SHADE_SIZE); build_tones(internal->tones); - if (!t5s3_epd_init(config->pixel_clock_hz)) { + if (!t5s3_epd_board_init(config->pixel_clock_hz)) { + LOG_E(TAG, "Failed to set up the pins and peripherals"); destroy(internal); return ERROR_RESOURCE; } - // The boot splash leaves ghosting, so clear the panel with the full flash cycle before LVGL draws - power_on(internal); - t5s3_epd_clear(); + if (xTaskCreatePinnedToCore(helper_task, "t5s3_display_helper", 3072, internal, TASK_PRIORITY, &internal->helper, HELPER_CORE) != pdPASS) { + LOG_E(TAG, "Failed to create display helper task"); + t5s3_epd_board_deinit(); + destroy(internal); + return ERROR_RESOURCE; + } - if (xTaskCreatePinnedToCore(display_task, "t5s3_display", 4096, internal, 1, &internal->task, 1) != pdPASS) { + if (xTaskCreatePinnedToCore(display_task, "t5s3_display", 4096, internal, TASK_PRIORITY, &internal->task, TASK_CORE) != pdPASS) { LOG_E(TAG, "Failed to create display task"); - power_off(internal); - t5s3_epd_deinit(); + stop_helper(internal); + t5s3_epd_board_deinit(); destroy(internal); return ERROR_RESOURCE; } device_set_driver_data(device, internal); - LOG_I(TAG, "Initialized (%dx%d)", T5S3_EPD_WIDTH, T5S3_EPD_HEIGHT); + + // The boot splash leaves residue that a regular update would never clear + flash_blocking(internal); + + LOG_I(TAG, "Started (%dx%d)", PANEL_WIDTH, PANEL_HEIGHT); return ERROR_NONE; } static error_t stop(Device* device) { auto* internal = static_cast(device_get_driver_data(device)); - xTaskNotify(internal->task, CMD_EXIT, eSetBits); + notify_task(internal, CMD_EXIT); while (!internal->exited) { vTaskDelay(1); } + stop_helper(internal); device_set_driver_data(device, nullptr); - t5s3_epd_deinit(); + t5s3_epd_board_deinit(); destroy(internal); return ERROR_NONE; } diff --git a/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd.cpp b/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd.cpp deleted file mode 100644 index 6251871c2..000000000 --- a/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd.cpp +++ /dev/null @@ -1,422 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -// -// Waveform updates of the ED047TC1 panel of the LILYGO T5 4.7 Inch E-Paper S3. -// The update logic follows the epdiy project (https://github.com/vroland/epdiy). -// -// Each update diffs the frame buffer (front) against what the panel shows (back), sends only the rows that changed -// and only drives the columns that changed. - -#include "t5s3_epd.h" - -#include -#include - -#include -#include -#include -#include - -#include - -#define TAG "T5s3Epd" - -// A pixel keeps its drive until its row is addressed again, so the drive of a phase depends on the length of the frame. -// Frames are padded to a fixed period, which makes an update of a few rows drive as long as one of the whole panel. -// Black needs about 32 frames to get as dark as the waveform makes it and white about 8 to leave black. The fast update -// stops at a dark gray for black, the refresh completes it. -static constexpr uint32_t FAST_PHASES = 8; -static constexpr int64_t FAST_FRAME_US = 16667; -// Gate pulse of a row of the fast update in units of 0.1 us -static constexpr uint32_t FAST_HOLD_TICKS = 120; -// Duration of a frame of the whole panel, the gray levels of the waveform were measured with it -static constexpr int64_t QUALITY_FRAME_US = 40000; -// Frames that settle the pixels of a refresh before the waveform. Black pixels get the rest of their drive and white -// pixels go further toward white. Gray pixels are shaken: REFRESH_SHAKE_FRAMES frames toward black and the rest toward -// white, which erases what they showed before, also a black that was shown for a long time. The waveform then draws -// their gray levels like from a clean white. -static constexpr int REFRESH_SETTLE_FRAMES = 24; -static constexpr int REFRESH_SHAKE_FRAMES = 8; - -// 1 bit per pixel -static constexpr int TOUCHED_ROW_BYTES = T5S3_EPD_WIDTH / 8; -static constexpr int TOUCHED_BYTES = TOUCHED_ROW_BYTES * T5S3_EPD_HEIGHT; -// Hold time of the frames that flash the panel to white -static constexpr uint32_t CLEAR_HOLD_TICKS = 120; -static constexpr int CLEAR_CYCLES = 3; -static constexpr int CLEAR_DARK_FRAMES = 10; -static constexpr int CLEAR_LIGHT_FRAMES = 10; -static constexpr int CLEAR_REST_FRAMES = 2; - -struct Engine { - uint8_t* front; - uint8_t* back; - // Black and white targets of the fast update - uint8_t* dither; - // Pixels that a fast update drove since their last refresh, 1 bit per pixel - uint8_t* touched; - // Targets of the fast update being sent, dithered where front differs from back - uint8_t* fast; - // Targets of the update being sent - const uint8_t* to; - // Actions of the phase being sent - uint8_t table[256]; - // Areas of the refresh being sent - const T5s3EpdRect* rects; - int rect_count; - int settle_frame; - bool row_dirty[T5S3_EPD_HEIGHT]; - alignas(4) uint8_t changes[T5S3_EPD_FB_ROW_BYTES]; - alignas(4) uint8_t line_mask[T5S3_EPD_BUS_ROW_BYTES]; -}; - -static Engine* engine = nullptr; - -// Sleeps for most of the rest of the period, which also lets the idle task feed the watchdog -static void pad_frame(int64_t frame_start_us, int64_t period_us) { - const int64_t remaining_us = period_us - (esp_timer_get_time() - frame_start_us); - if (remaining_us > 2000) { - vTaskDelay(pdMS_TO_TICKS((remaining_us - 1000) / 1000)); - } - while (esp_timer_get_time() - frame_start_us < period_us) {} -} - -static void IRAM_ATTR prepare_row(int y, uint8_t* out, void*) { - t5s3_epd_prepare_row( - out, - engine->to + y * T5S3_EPD_FB_ROW_BYTES, - engine->back + y * T5S3_EPD_FB_ROW_BYTES, - engine->table, - engine->line_mask - ); -} - -bool t5s3_epd_init(uint32_t pixel_clock_hz) { - if (engine != nullptr) { - return true; - } - static const MemoryPolicy external_policy = { .required = 0, .desired = MEMORY_CAPABILITY_EXTERNAL, .alignment = 16 }; - engine = static_cast(memory_calloc_with_policy(1, sizeof(Engine), &external_policy)); - if (engine == nullptr) { - LOG_E(TAG, "Out of memory"); - return false; - } - engine->front = static_cast(memory_alloc_with_policy(T5S3_EPD_FB_BYTES, &external_policy)); - engine->back = static_cast(memory_alloc_with_policy(T5S3_EPD_FB_BYTES, &external_policy)); - engine->dither = static_cast(memory_alloc_with_policy(T5S3_EPD_FB_BYTES, &external_policy)); - engine->fast = static_cast(memory_alloc_with_policy(T5S3_EPD_FB_BYTES, &external_policy)); - engine->touched = static_cast(memory_calloc_with_policy(1, TOUCHED_BYTES, &external_policy)); - if (engine->front == nullptr || engine->back == nullptr || engine->dither == nullptr || engine->fast == nullptr || engine->touched == nullptr) { - LOG_E(TAG, "Out of memory"); - t5s3_epd_deinit(); - return false; - } - memset(engine->front, 0xFF, T5S3_EPD_FB_BYTES); - memset(engine->back, 0xFF, T5S3_EPD_FB_BYTES); - memset(engine->dither, 0xFF, T5S3_EPD_FB_BYTES); - - if (!t5s3_epd_board_init(pixel_clock_hz)) { - LOG_E(TAG, "Failed to set up the pins and peripherals"); - t5s3_epd_deinit(); - return false; - } - return true; -} - -void t5s3_epd_deinit() { - if (engine == nullptr) { - return; - } - t5s3_epd_board_deinit(); - memory_free(engine->front); - memory_free(engine->back); - memory_free(engine->dither); - memory_free(engine->fast); - memory_free(engine->touched); - memory_free(engine); - engine = nullptr; -} - -uint8_t* t5s3_epd_framebuffer() { - return engine->front; -} - -uint8_t* t5s3_epd_dither_buffer() { - return engine->dither; -} - -void t5s3_epd_fill_white() { - memset(engine->front, 0xFF, T5S3_EPD_FB_BYTES); - memset(engine->dither, 0xFF, T5S3_EPD_FB_BYTES); -} - -// Pixels whose level changed get their black or white target, the others keep what the panel shows. -// first and last get the first and last group of 8 pixels with a change, or -1 for a row without changes. -static void build_fast_row(int y, int* first, int* last) { - const size_t offset = y * T5S3_EPD_FB_ROW_BYTES; - const uint8_t* front = engine->front + offset; - const uint8_t* back = engine->back + offset; - const uint8_t* dither = engine->dither + offset; - uint8_t* fast = engine->fast + offset; - uint8_t* touched = engine->touched + y * TOUCHED_ROW_BYTES; - *first = -1; - *last = -1; - for (int i = 0; i < T5S3_EPD_FB_ROW_BYTES; i++) { - const uint8_t difference = front[i] ^ back[i]; - uint8_t changed = 0; - changed |= (difference & 0x0F) != 0 ? 0x0F : 0x00; - changed |= (difference & 0xF0) != 0 ? 0xF0 : 0x00; - fast[i] = static_cast((dither[i] & changed) | (back[i] & ~changed)); - // Byte i holds the pixels 2i and 2i + 1 - const int bit = (i & 3) * 2; - touched[i / 4] |= static_cast((((changed & 0x0F) != 0 ? 1 : 0) | ((changed & 0xF0) != 0 ? 2 : 0)) << bit); - if (changed != 0) { - *first = *first < 0 ? i / 4 : *first; - *last = i / 4; - } - } -} - -bool t5s3_epd_show_changes(int32_t y_start, int32_t y_end, T5s3EpdRect* changed) { - *changed = { 0, 0, 0, 0 }; - if (y_start < 0) { - y_start = 0; - } - if (y_end > T5S3_EPD_HEIGHT) { - y_end = T5S3_EPD_HEIGHT; - } - const int64_t started = esp_timer_get_time(); - - // Find the rows and columns that changed - memset(engine->row_dirty, 0, sizeof(engine->row_dirty)); - memset(engine->changes, 0, sizeof(engine->changes)); - auto* changes = reinterpret_cast(engine->changes); - int changed_rows = 0; - int driven_rows = 0; - int first_group = T5S3_EPD_WIDTH; - int last_group = -1; - for (int y = y_start; y < y_end; y++) { - int first; - int last; - build_fast_row(y, &first, &last); - if (first < 0) { - continue; - } - if (changed_rows == 0) { - changed->y0 = y; - } - changed->y1 = y + 1; - first_group = first < first_group ? first : first_group; - last_group = last > last_group ? last : last_group; - changed_rows++; - - const auto* to_row = reinterpret_cast(engine->fast + y * T5S3_EPD_FB_ROW_BYTES); - const auto* from_row = reinterpret_cast(engine->back + y * T5S3_EPD_FB_ROW_BYTES); - uint32_t row_changes = 0; - for (int i = 0; i < T5S3_EPD_FB_ROW_BYTES / 4; i++) { - const uint32_t difference = to_row[i] ^ from_row[i]; - changes[i] |= difference; - row_changes |= difference; - } - engine->row_dirty[y] = row_changes != 0; - driven_rows += row_changes != 0 ? 1 : 0; - } - if (changed_rows == 0) { - return true; - } - changed->x0 = first_group * 8; - changed->x1 = (last_group + 1) * 8; - - // Pixels whose dither is what the panel shows already are only drawn by the refresh - if (driven_rows > 0) { - t5s3_epd_build_line_mask(engine->line_mask, engine->changes); - t5s3_epd_build_fast_table(engine->table); - engine->to = engine->fast; - bool success = true; - for (uint32_t phase = 0; phase < FAST_PHASES && success; phase++) { - const int64_t frame_start_us = esp_timer_get_time(); - success = t5s3_epd_board_frame(changed->y0, changed->y1, engine->row_dirty, FAST_HOLD_TICKS, prepare_row, nullptr); - pad_frame(frame_start_us, FAST_FRAME_US); - } - // A failed update leaves the back buffer alone, so the next update draws the same rows again - if (!success) { - LOG_E(TAG, "Update failed, a peripheral did not respond"); - return false; - } - for (int y = changed->y0; y < changed->y1; y++) { - if (engine->row_dirty[y]) { - memcpy(engine->back + y * T5S3_EPD_FB_ROW_BYTES, engine->fast + y * T5S3_EPD_FB_ROW_BYTES, T5S3_EPD_FB_ROW_BYTES); - } - } - } - - LOG_D(TAG, "Fast update of %dx%d at %d,%d took %d ms", static_cast(changed->x1 - changed->x0), static_cast(changed->y1 - changed->y0), - static_cast(changed->x0), static_cast(changed->y0), static_cast((esp_timer_get_time() - started) / 1000)); - return true; -} - -// Drives only the columns of the areas that cover the row -static void IRAM_ATTR prepare_refresh_row(int y, uint8_t* out, void*) { - memset(engine->line_mask, 0, sizeof(engine->line_mask)); - for (int i = 0; i < engine->rect_count; i++) { - const T5s3EpdRect& rect = engine->rects[i]; - if (y >= rect.y0 && y < rect.y1) { - // A mask byte covers 4 pixels - memset(engine->line_mask + rect.x0 / 4, 0xFF, (rect.x1 + 3) / 4 - rect.x0 / 4); - } - } - prepare_row(y, out, nullptr); -} - -static inline bool needs_refresh(int y, int x) { - const uint8_t front = engine->front[y * T5S3_EPD_FB_ROW_BYTES + x / 2]; - const uint8_t back = engine->back[y * T5S3_EPD_FB_ROW_BYTES + x / 2]; - const int shift = (x & 1) * 4; - const bool differs = ((front ^ back) >> shift & 0x0F) != 0; - return differs || (engine->touched[y * TOUCHED_ROW_BYTES + x / 8] >> (x & 7) & 1) != 0; -} - -// Bus rows of the settle frames during and after the shake, kept in the buffer of the fast update, which a refresh -// does not use. A row of that buffer holds both. -static inline uint8_t* settle_row(int y, bool shaking) { - return engine->fast + y * T5S3_EPD_FB_ROW_BYTES + (shaking ? 0 : T5S3_EPD_BUS_ROW_BYTES); -} - -static void build_settle_rows(int y) { - uint8_t* shake = settle_row(y, true); - uint8_t* after = settle_row(y, false); - memset(shake, 0, T5S3_EPD_BUS_ROW_BYTES); - memset(after, 0, T5S3_EPD_BUS_ROW_BYTES); - for (int i = 0; i < engine->rect_count; i++) { - const T5s3EpdRect& rect = engine->rects[i]; - if (y < rect.y0 || y >= rect.y1) { - continue; - } - for (int x = rect.x0 / 4 * 4; x < (rect.x1 + 3) / 4 * 4; x++) { - if (!needs_refresh(y, x)) { - continue; - } - const uint8_t level = engine->front[y * T5S3_EPD_FB_ROW_BYTES + x / 2] >> ((x & 1) * 4) & 0x0F; - const int shift = (x & 3) * 2; - const uint8_t shake_action = level == 15 ? T5S3_EPD_ACTION_LIGHT : T5S3_EPD_ACTION_DARK; - const uint8_t after_action = level == 0 ? T5S3_EPD_ACTION_DARK : T5S3_EPD_ACTION_LIGHT; - shake[x / 4] |= static_cast(shake_action << shift); - after[x / 4] |= static_cast(after_action << shift); - } - } -} - -static void IRAM_ATTR prepare_settle_row(int y, uint8_t* out, void*) { - memcpy(out, settle_row(y, engine->settle_frame < REFRESH_SHAKE_FRAMES), T5S3_EPD_BUS_ROW_BYTES); -} - -bool t5s3_epd_refresh(const T5s3EpdRect* rects, int count) { - const int64_t started = esp_timer_get_time(); - // Only rows with pixels that need a refresh are sent - int y_start = T5S3_EPD_HEIGHT; - int y_end = 0; - for (int i = 0; i < count; i++) { - const T5s3EpdRect& rect = rects[i]; - for (int y = rect.y0; y < rect.y1; y++) { - bool needed = false; - for (int x = rect.x0; x < rect.x1 && !needed; x++) { - needed = needs_refresh(y, x); - } - if (needed) { - y_start = y < y_start ? y : y_start; - y_end = y + 1 > y_end ? y + 1 : y_end; - } - } - } - if (y_start >= y_end) { - return true; - } - - engine->rects = rects; - engine->rect_count = count; - for (int y = y_start; y < y_end; y++) { - build_settle_rows(y); - } - bool success = true; - for (int frame = 0; frame < REFRESH_SETTLE_FRAMES && success; frame++) { - engine->settle_frame = frame; - const int64_t frame_start_us = esp_timer_get_time(); - success = t5s3_epd_board_frame(y_start, y_end, nullptr, FAST_HOLD_TICKS, prepare_settle_row, nullptr); - pad_frame(frame_start_us, FAST_FRAME_US); - } - - // Gray pixels are white now, black and white pixels show their level. The waveform starts from the white that its - // gray levels were measured from. - for (int i = 0; i < count && success; i++) { - const T5s3EpdRect& rect = rects[i]; - for (int y = rect.y0; y < rect.y1; y++) { - for (int x = rect.x0 / 4 * 4; x < (rect.x1 + 3) / 4 * 4; x++) { - if (!needs_refresh(y, x)) { - continue; - } - uint8_t* byte = &engine->back[y * T5S3_EPD_FB_ROW_BYTES + x / 2]; - const int shift = (x & 1) * 4; - const uint8_t level = engine->front[y * T5S3_EPD_FB_ROW_BYTES + x / 2] >> shift & 0x0F; - const uint8_t shown = level == 0 ? 0 : 15; - *byte = static_cast((*byte & ~(0x0F << shift)) | (shown << shift)); - engine->touched[y * TOUCHED_ROW_BYTES + x / 8] &= static_cast(~(1 << (x & 7))); - } - } - } - - engine->to = engine->front; - for (int phase = 0; phase < T5S3_EPD_QUALITY_PHASES && success; phase++) { - t5s3_epd_build_quality_table(engine->table, phase); - const int64_t frame_start_us = esp_timer_get_time(); - success = t5s3_epd_board_frame(y_start, y_end, nullptr, T5S3_EPD_QUALITY_HOLD[phase], prepare_refresh_row, nullptr); - pad_frame(frame_start_us, QUALITY_FRAME_US); - } - if (!success) { - LOG_E(TAG, "Refresh failed, a peripheral did not respond"); - return false; - } - - // The mask covers groups of 4 pixels, so whole groups were driven - for (int i = 0; i < count; i++) { - const T5s3EpdRect& rect = rects[i]; - const int x0 = rect.x0 / 4 * 2; - const int x1 = (rect.x1 + 3) / 4 * 2; - for (int y = rect.y0; y < rect.y1; y++) { - const size_t offset = y * T5S3_EPD_FB_ROW_BYTES + x0; - memcpy(engine->back + offset, engine->front + offset, x1 - x0); - } - } - - LOG_D(TAG, "Refresh of %d areas in rows %d to %d took %d ms", count, y_start, y_end, static_cast((esp_timer_get_time() - started) / 1000)); - return true; -} - -static bool draw_clear_frames(uint8_t pattern, int count) { - t5s3_epd_board_fill_rows(pattern); - bool success = true; - for (int i = 0; i < count && success; i++) { - success = t5s3_epd_board_frame(0, T5S3_EPD_HEIGHT, nullptr, CLEAR_HOLD_TICKS, nullptr, nullptr); - vTaskDelay(1); - } - return success; -} - -bool t5s3_epd_clear() { - const int64_t started = esp_timer_get_time(); - t5s3_epd_fill_white(); - const T5s3EpdRect panel = { 0, 0, T5S3_EPD_WIDTH, T5S3_EPD_HEIGHT }; - if (!t5s3_epd_refresh(&panel, 1)) { - return false; - } - bool success = true; - for (int cycle = 0; cycle < CLEAR_CYCLES && success; cycle++) { - success = draw_clear_frames(T5S3_EPD_ALL_DARK, CLEAR_DARK_FRAMES) && - draw_clear_frames(T5S3_EPD_ALL_LIGHT, CLEAR_LIGHT_FRAMES) && - draw_clear_frames(0, CLEAR_REST_FRAMES); - } - if (!success) { - LOG_E(TAG, "Clear failed, a peripheral did not respond"); - return false; - } - LOG_I(TAG, "Cleared in %d ms", static_cast((esp_timer_get_time() - started) / 1000)); - return true; -} diff --git a/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd.h b/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd.h deleted file mode 100644 index e3355e704..000000000 --- a/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd.h +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -// -// Waveform updates of the ED047TC1 panel of the LILYGO T5 4.7 Inch E-Paper S3. -// The update logic follows the epdiy project (https://github.com/vroland/epdiy). -// The calls must come from one task, there is no locking. - -#pragma once - -#include "t5s3_epd_rows.h" - -#include - -// Columns [x0, x1) of the rows [y0, y1) -struct T5s3EpdRect { - int32_t x0, y0, x1, y1; -}; - -// Allocates the frame buffers and sets up the pins and peripherals. The panel stays unpowered. -bool t5s3_epd_init(uint32_t pixel_clock_hz); - -void t5s3_epd_deinit(); - -// Frame buffer to draw in, 4 bits per pixel. The panel shows it after an update. -uint8_t* t5s3_epd_framebuffer(); - -// Black and white version of the frame buffer with levels 0 and 15 only, shown by fast updates -uint8_t* t5s3_epd_dither_buffer(); - -// Shows the pixels of the rows [y_start, y_end) whose level changed with their black and white dither, in a few frames. -// changed gets the area of the changed pixels, which still needs a refresh for its gray levels. Needs the panel to be powered. -bool t5s3_epd_show_changes(int32_t y_start, int32_t y_end, T5s3EpdRect* changed); - -// Draws the pixels of the areas that do not show their level yet with the 16 level waveform, after driving them to white. -// Needs the panel to be powered. -bool t5s3_epd_refresh(const T5s3EpdRect* rects, int count); - -void t5s3_epd_fill_white(); - -// Flashes the whole panel to white. Needs the panel to be powered. -bool t5s3_epd_clear(); diff --git a/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd_rows.h b/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd_rows.h deleted file mode 100644 index 8cacdc22a..000000000 --- a/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd_rows.h +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -// -// Conversion of framebuffer rows into the data the source driver of the ED047TC1 panel expects. -// This code has no hardware access. - -#pragma once - -#include "t5s3_epd_board.h" -#include "t5s3_epd_waveform.h" - -#include - -#ifdef ESP_PLATFORM -#include -#define T5S3_EPD_IRAM IRAM_ATTR -#else -#define T5S3_EPD_IRAM -#endif - -// Framebuffer rows hold 4 bits per pixel with the even pixel of a pair in the low nibble, 0x0 is black and 0xF is white -constexpr int T5S3_EPD_FB_ROW_BYTES = T5S3_EPD_WIDTH / 2; -constexpr int T5S3_EPD_FB_BYTES = T5S3_EPD_FB_ROW_BYTES * T5S3_EPD_HEIGHT; - -constexpr uint8_t T5S3_EPD_ACTION_HOLD = 0; -constexpr uint8_t T5S3_EPD_ACTION_DARK = 1; -constexpr uint8_t T5S3_EPD_ACTION_LIGHT = 2; - -// Tables map (target level << 4 | source level) to the action for one phase of an update. - -// Fast update: changed pixels with a black target are driven dark and changed pixels with a white target light. -inline void t5s3_epd_build_fast_table(uint8_t* table) { - for (int to = 0; to < 16; to++) { - for (int from = 0; from < 16; from++) { - uint8_t action = T5S3_EPD_ACTION_HOLD; - if (to != from && to == 0) { - action = T5S3_EPD_ACTION_DARK; - } else if (to != from && to == 15) { - action = T5S3_EPD_ACTION_LIGHT; - } - table[(to << 4) | from] = action; - } - } -} - -// Pixels that keep their level are held, so a refresh only drives the pixels that need it -inline void t5s3_epd_build_quality_table(uint8_t* table, int phase) { - const uint8_t* packed = T5S3_EPD_QUALITY_LUT[phase]; - for (int to = 0; to < 16; to++) { - for (int group = 0; group < 4; group++) { - const uint8_t actions = *packed++; - const int index = (to << 4) | (group * 4); - table[index] = (actions >> 6) & 3; - table[index + 1] = (actions >> 4) & 3; - table[index + 2] = (actions >> 2) & 3; - table[index + 3] = actions & 3; - } - } - for (int level = 0; level < 16; level++) { - table[(level << 4) | level] = T5S3_EPD_ACTION_HOLD; - } -} - -// Keeps the pixels of changed columns and holds all others. A framebuffer byte of the changes holds the XOR of two pixels. -inline void t5s3_epd_build_line_mask(uint8_t* mask, const uint8_t* changes) { - for (int i = 0; i < T5S3_EPD_BUS_ROW_BYTES; i++) { - const uint8_t first = changes[2 * i]; - const uint8_t second = changes[2 * i + 1]; - uint8_t value = 0; - value |= (first & 0x0F) != 0 ? 0x03 : 0x00; - value |= (first & 0xF0) != 0 ? 0x0C : 0x00; - value |= (second & 0x0F) != 0 ? 0x30 : 0x00; - value |= (second & 0xF0) != 0 ? 0xC0 : 0x00; - mask[i] = value; - } -} - -// Converts one row of target levels and one row of source levels into bus data. -// All pointers must be 4-byte aligned. Four framebuffer bytes (8 pixels) become two bus bytes per step. -inline void T5S3_EPD_IRAM t5s3_epd_prepare_row( - uint8_t* out, - const uint8_t* to_row, - const uint8_t* from_row, - const uint8_t* table, - const uint8_t* mask -) { - const auto* to_words = reinterpret_cast(to_row); - const auto* from_words = reinterpret_cast(from_row); - const auto* mask_words = reinterpret_cast(mask); - auto* out_words = reinterpret_cast(out); - for (int i = 0; i < T5S3_EPD_FB_ROW_BYTES / 4; i++) { - const uint32_t to = to_words[i]; - const uint32_t from = from_words[i]; - // Table index of a pixel is (target level << 4) | source level, the even pixel of a byte is in its low nibble - const uint32_t even = ((to & 0x0F0F0F0F) << 4) | (from & 0x0F0F0F0F); - const uint32_t odd = (to & 0xF0F0F0F0) | ((from >> 4) & 0x0F0F0F0F); - const uint32_t first = table[even & 0xFF] | (table[odd & 0xFF] << 2) | (table[(even >> 8) & 0xFF] << 4) | (table[(odd >> 8) & 0xFF] << 6); - const uint32_t second = table[(even >> 16) & 0xFF] | (table[(odd >> 16) & 0xFF] << 2) | (table[even >> 24] << 4) | (table[odd >> 24] << 6); - out_words[i] = static_cast((first | (second << 8)) & mask_words[i]); - } -} diff --git a/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd_waveform.h b/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd_waveform.h deleted file mode 100644 index 464540911..000000000 --- a/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd_waveform.h +++ /dev/null @@ -1,201 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later -// -// Drive data of the ED047TC1 panel for the 16 level update, taken from the epdiy project -// (https://github.com/vroland/epdiy, built-in waveform epdiy_ED047TC1, mode GC16). -// This file is under the LGPL v3.0 or later like epdiy itself. - -#pragma once - -#include - -constexpr int T5S3_EPD_QUALITY_PHASES = 30; - -// Hold time of each phase in units of 0.1 microsecond -constexpr uint16_t T5S3_EPD_QUALITY_HOLD[T5S3_EPD_QUALITY_PHASES] = { - 30, 30, 20, 20, 30, 30, 30, 40, 40, 50, 50, 50, 100, 200, 300, 10, 10, 8, 8, 8, 8, 8, 10, 10, 15, 15, 20, 20, 100, 300 -}; - -// Per phase: 16 target levels with 4 bytes each. A byte holds the 2 bit actions of 4 source levels, -// the lowest source level in the top bits. Action 0 holds the pixel, 1 drives it dark and 2 light. -constexpr uint8_t T5S3_EPD_QUALITY_LUT[T5S3_EPD_QUALITY_PHASES][64] = { - { - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, - }, - { - 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, - 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, - 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, - 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, - }, - { - 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, - 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, - 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, - 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, - }, - { - 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, - 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, - 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, - 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, - }, - { - 0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55, - 0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55, - 0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55, - 0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55, - }, - { - 0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55, - 0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55, - 0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55, - 0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55, - }, - { - 0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55, - 0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55, - 0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55, - 0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55, - }, - { - 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55, - 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55, - 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55, - 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55, - }, - { - 0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55, - 0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55, - 0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55, - 0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55, - }, - { - 0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55, - 0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55, - 0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55, - 0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55, - }, - { - 0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55, - 0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55, - 0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55, - 0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55, - }, - { - 0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, - 0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, - 0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, - 0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, - }, - { - 0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55, - 0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55, - 0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55, - 0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55, - }, - { - 0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55, - 0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55, - 0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55, - 0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55, - }, - { - 0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55, - 0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55, - 0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55, - 0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55, - }, - { - 0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - }, - { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - }, - { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - }, - { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - }, - { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - }, - { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - }, - { - 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, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - }, - { - 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, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - }, - { - 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, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - }, - { - 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, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - }, - { - 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - }, - { - 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - }, - { - 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - }, - { - 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - }, - { - 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, - }, -}; From f50a185d903eaff6559149cdcb899b9cc04983e7 Mon Sep 17 00:00:00 2001 From: AdaSzi115026 <81822538+AdaSzi@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:37:49 +0100 Subject: [PATCH 3/6] T5S3 e-paper: 16 shades and grayscale driver as default - Grays rest for 0 to 3 frames before their dark frames, each frame of rest makes a gray a little darker - That gives 16 shades close to each other, so the dither between neighbouring shades is hardly visible - AUTO_REFRESH_INTERVAL_MS is renamed to MINIMAL_AUTO_REFRESH_INTERVAL_MS - lilygo-t5-epd47-s3 uses lilygo,t5s3-display, the black and white driver stays in the devicetree as commented out lines in case someone prefers it --- .../lilygo-t5-epd47-s3/lilygo,t5-epd47-s3.dts | 7 ++++-- Drivers/lilygo-t5s3-epd-module/README.md | 4 ++-- .../bindings/lilygo,t5s3-display.yaml | 2 +- .../source/t5s3_display.cpp | 22 ++++++++++--------- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/Devices/lilygo-t5-epd47-s3/lilygo,t5-epd47-s3.dts b/Devices/lilygo-t5-epd47-s3/lilygo,t5-epd47-s3.dts index 6bb347af3..63731dc28 100644 --- a/Devices/lilygo-t5-epd47-s3/lilygo,t5-epd47-s3.dts +++ b/Devices/lilygo-t5-epd47-s3/lilygo,t5-epd47-s3.dts @@ -11,7 +11,8 @@ #include #include #include -#include +#include +// #include / { compatible = "root"; @@ -32,7 +33,9 @@ // Listed before the I2C bus on purpose, the touch controller was only tested with the display started first display { - compatible = "lilygo,t5s3-display-bw"; + compatible = "lilygo,t5s3-display"; + // Black and white only with faster updates, enable it together with its include above + // compatible = "lilygo,t5s3-display-bw"; }; i2c_internal { diff --git a/Drivers/lilygo-t5s3-epd-module/README.md b/Drivers/lilygo-t5s3-epd-module/README.md index 81aa7399c..2a8d77ce3 100644 --- a/Drivers/lilygo-t5s3-epd-module/README.md +++ b/Drivers/lilygo-t5s3-epd-module/README.md @@ -4,9 +4,9 @@ Kernel drivers for the 4.7" 960x540 `ED047TC1` e-paper panel of the LILYGO T5 4. The drivers only work on this board. The pins of the panel, the 74HCT4094 shift register that carries the panel supplies and the sequence of the panel are fixed in the code, so the devicetree node only has the pixel clock as property. -There are two drivers. A devicetree uses one of them, the device `lilygo-t5-epd47-s3` uses the black and white one: +There are two drivers. A devicetree uses one of them, the device `lilygo-t5-epd47-s3` uses the grayscale one and has the black and white one commented out in its devicetree: -- `lilygo,t5s3-display` (`t5s3_display.cpp`): 7 shades from black to white, dithered between them. Each pixel whose target shade changes gets a dose of frames, in the manner of [InkBoy](https://github.com/kuslota/InkBoy): black is driven dark for 12 frames, white and the grays are driven light for 15 frames, which erases what the pixel showed before, and the grays then rest for a frame and are driven dark for 1 to 6 frames. Every gray is reached the same way, so moving content leaves almost no trails. Frames of 40 ms cover the whole screen, so all changes show at once: black and white within a few frames, a gray after about 0.9 s. `refresh()` flashes the panel and draws the content again, and the driver does that by itself when the content changed without touches for 10 minutes. A frame that stalls, for example while the flash is written, makes the grays it interrupted start again. `draw_bitmap()` does not wait for the panel and pixels that do not change are never driven. It needs the pixel clock of 20 MHz of its binding. +- `lilygo,t5s3-display` (`t5s3_display.cpp`): 16 shades from black to white, dithered between neighbouring shades. Each pixel whose target shade changes gets a dose of frames, in the manner of [InkBoy](https://github.com/kuslota/InkBoy): black is driven dark for 12 frames, white and the grays are driven light for 15 frames, which erases what the pixel showed before, and the grays then rest for 0 to 3 frames and are driven dark for 1 to 8 frames. Each frame of rest makes a gray a little darker, which gives 14 grays close to each other, so the dither is hardly visible. Every gray is reached the same way, so moving content leaves almost no trails. Frames of 40 ms cover the whole screen, so all changes show at once: black and white within a few frames, a gray after about 0.9 s. `refresh()` flashes the panel and draws the content again, and the driver does that by itself when the content changed without touches for 10 minutes. A frame that stalls, for example while the flash is written, makes the grays it interrupted start again. `draw_bitmap()` does not wait for the panel and pixels that do not change are never driven. It needs the pixel clock of 20 MHz of its binding. - `lilygo,t5s3-display-bw` (`t5s3_display_bw.cpp`): black and white with a pixel state machine, like the M5Stack PaperS3 driver. Gray levels are dithered. It is the fastest. Shared and internal files: diff --git a/Drivers/lilygo-t5s3-epd-module/bindings/lilygo,t5s3-display.yaml b/Drivers/lilygo-t5s3-epd-module/bindings/lilygo,t5s3-display.yaml index 2aff212e8..881bddc7e 100644 --- a/Drivers/lilygo-t5s3-epd-module/bindings/lilygo,t5s3-display.yaml +++ b/Drivers/lilygo-t5s3-epd-module/bindings/lilygo,t5s3-display.yaml @@ -1,5 +1,5 @@ description: > - LILYGO T5 4.7 Inch E-Paper S3 (V2.3/V2.4) display with an ED047TC1 panel, driven in 7 shades with a dose of + LILYGO T5 4.7 Inch E-Paper S3 (V2.3/V2.4) display with an ED047TC1 panel, driven in 16 shades with a dose of frames per pixel instead of waveform tables. Updates do not block and leave no trails on gray backgrounds. The pins of the panel are fixed in the driver, so this node only has the pixel clock as property. diff --git a/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp b/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp index af51d9c29..633a634fa 100644 --- a/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp +++ b/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp @@ -27,9 +27,9 @@ // Gray levels are drawn without waveform tables, with a dose of frames per pixel in the manner of InkBoy // (https://github.com/kuslota/InkBoy). A pixel whose target shade changes gets a program: black is driven dark for // BLACK_FRAMES frames. White and the grays are driven light for LIGHT_FRAMES frames, which erases what the pixel showed -// before. The grays then rest for HOLD_FRAMES frames and are driven dark for the dose of their shade. A gray then looks -// the same whatever the pixel came from and moving content leaves no trails. Pixels whose target does not change are -// never driven. +// before. The grays then rest for a few frames and are driven dark for the dose of their shade. A gray then looks the +// same whatever the pixel came from and moving content leaves no trails. Pixels whose target does not change are never +// driven. // // Every frame covers the rows from the top down to the lowest row of the current updates, so all of them change at once. // The helper task encodes the next frame while the display task sends the current one. @@ -48,18 +48,19 @@ static constexpr int CHUNK_PIXELS = 32; static constexpr int CHUNKS = PANEL_WIDTH / CHUNK_PIXELS; static_assert(CHUNKS <= 32); -// Shades from black to white. The dose of a gray is its number of dark frames after the light frames. +// Shades from black to white. The dose of a gray is its number of dark frames after the light frames and the frames of +// rest before them. Each frame of rest makes the dose a little darker, which gives shades between those of whole frames. // The brightness of each shade was measured with a camera, 0 is black and 255 is white. -static constexpr int SHADES = 7; +static constexpr int SHADES = 16; static constexpr uint8_t SHADE_BLACK = 0; static constexpr uint8_t SHADE_WHITE = SHADES - 1; -static constexpr uint8_t SHADE_DOSE[SHADES] = { 0, 6, 4, 3, 2, 1, 0 }; -static constexpr uint8_t SHADE_BRIGHTNESS[SHADES] = { 0, 28, 54, 94, 145, 199, 255 }; +static constexpr uint8_t SHADE_DOSE[SHADES] = { 0, 8, 6, 5, 5, 4, 4, 3, 3, 3, 2, 2, 2, 1, 1, 0 }; +static constexpr uint8_t SHADE_REST[SHADES] = { 0, 0, 1, 2, 0, 1, 0, 2, 1, 0, 3, 1, 0, 3, 0, 0 }; +static constexpr uint8_t SHADE_BRIGHTNESS[SHADES] = { 0, 21, 35, 47, 59, 70, 82, 94, 106, 120, 135, 155, 173, 196, 223, 255 }; // Between 10 and 30 light frames the grays come out the same whatever the pixel showed before, fewer leave traces static constexpr uint8_t LIGHT_FRAMES = 15; static constexpr uint8_t BLACK_FRAMES = 12; -static constexpr uint8_t HOLD_FRAMES = 1; // A program byte is 0 for a pixel at rest, the dark frames left below HOLD_BASE, HOLD_BASE plus the frames of rest left // below LIGHT_BASE and LIGHT_BASE plus the light frames left above. Rest and the dark frames of the goal follow the light frames. @@ -272,7 +273,8 @@ static bool step_chunk(T5s3DisplayInternal* internal, int y, int chunk, uint8_t* const int x = word * 4 + i; uint32_t next = frames; if (frames == LIGHT_BASE) { - next = SHADE_DOSE[get_nibble(goal, x)] == 0 ? 0 : HOLD_BASE + HOLD_FRAMES; + const uint8_t shade = get_nibble(goal, x); + next = SHADE_REST[shade] != 0 ? HOLD_BASE + SHADE_REST[shade] : SHADE_DOSE[shade]; } else if (frames == HOLD_BASE) { next = SHADE_DOSE[get_nibble(goal, x)]; } @@ -657,7 +659,7 @@ static error_t t5s3_display_refresh(Device* device) { return ERROR_NONE; } -// Reports GRAYSCALE8 so that draw_bitmap() is called per changed tile. The luminance is dithered between the shades. +// Reports GRAYSCALE8 so that draw_bitmap() is called per changed tile. The luminance is dithered between the two nearest shades. static error_t t5s3_display_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) { auto* internal = static_cast(device_get_driver_data(device)); const int64_t start_us = esp_timer_get_time(); From 1a57faa0b3a53288513b9629de1d30ec3c233199 Mon Sep 17 00:00:00 2001 From: AdaSzi115026 <81822538+AdaSzi@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:11:41 +0100 Subject: [PATCH 4/6] T5S3 e-paper: keep thin black lines on gray and fix a restart race - Black is driven for 18 frames, until grays drawn with it end their light frames. Before, a black line of 1 pixel on gray faded after a refresh. - A pending restart of the doses runs on the display task before both cores encode, so it no longer races with the encoding of the upper half. --- Drivers/lilygo-t5s3-epd-module/README.md | 2 +- Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Drivers/lilygo-t5s3-epd-module/README.md b/Drivers/lilygo-t5s3-epd-module/README.md index 2a8d77ce3..096b5eda2 100644 --- a/Drivers/lilygo-t5s3-epd-module/README.md +++ b/Drivers/lilygo-t5s3-epd-module/README.md @@ -6,7 +6,7 @@ The drivers only work on this board. The pins of the panel, the 74HCT4094 shift There are two drivers. A devicetree uses one of them, the device `lilygo-t5-epd47-s3` uses the grayscale one and has the black and white one commented out in its devicetree: -- `lilygo,t5s3-display` (`t5s3_display.cpp`): 16 shades from black to white, dithered between neighbouring shades. Each pixel whose target shade changes gets a dose of frames, in the manner of [InkBoy](https://github.com/kuslota/InkBoy): black is driven dark for 12 frames, white and the grays are driven light for 15 frames, which erases what the pixel showed before, and the grays then rest for 0 to 3 frames and are driven dark for 1 to 8 frames. Each frame of rest makes a gray a little darker, which gives 14 grays close to each other, so the dither is hardly visible. Every gray is reached the same way, so moving content leaves almost no trails. Frames of 40 ms cover the whole screen, so all changes show at once: black and white within a few frames, a gray after about 0.9 s. `refresh()` flashes the panel and draws the content again, and the driver does that by itself when the content changed without touches for 10 minutes. A frame that stalls, for example while the flash is written, makes the grays it interrupted start again. `draw_bitmap()` does not wait for the panel and pixels that do not change are never driven. It needs the pixel clock of 20 MHz of its binding. +- `lilygo,t5s3-display` (`t5s3_display.cpp`): 16 shades from black to white, dithered between neighbouring shades. Each pixel whose target shade changes gets a dose of frames, in the manner of [InkBoy](https://github.com/kuslota/InkBoy): black is driven dark for 18 frames, white and the grays are driven light for 15 frames, which erases what the pixel showed before, and the grays then rest for 0 to 3 frames and are driven dark for 1 to 8 frames. Each frame of rest makes a gray a little darker, which gives 14 grays close to each other, so the dither is hardly visible. Every gray is reached the same way, so moving content leaves almost no trails. Frames of 40 ms cover the whole screen, so all changes show at once: black and white within a few frames, a gray after about 0.9 s. `refresh()` flashes the panel and draws the content again, and the driver does that by itself when the content changed without touches for 10 minutes. A frame that stalls, for example while the flash is written, makes the grays it interrupted start again. `draw_bitmap()` does not wait for the panel and pixels that do not change are never driven. It needs the pixel clock of 20 MHz of its binding. - `lilygo,t5s3-display-bw` (`t5s3_display_bw.cpp`): black and white with a pixel state machine, like the M5Stack PaperS3 driver. Gray levels are dithered. It is the fastest. Shared and internal files: diff --git a/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp b/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp index 633a634fa..d1373213a 100644 --- a/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp +++ b/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp @@ -60,7 +60,9 @@ static constexpr uint8_t SHADE_BRIGHTNESS[SHADES] = { 0, 21, 35, 47, 59, 70, 82, // Between 10 and 30 light frames the grays come out the same whatever the pixel showed before, fewer leave traces static constexpr uint8_t LIGHT_FRAMES = 15; -static constexpr uint8_t BLACK_FRAMES = 12; +// Black is driven until grays that started with it ended their light frames. Pixels driven light wash out a thin black +// line next to them, for example a line of 1 pixel on a gray area that is drawn again after a refresh. +static constexpr uint8_t BLACK_FRAMES = LIGHT_FRAMES + 3; // A program byte is 0 for a pixel at rest, the dark frames left below HOLD_BASE, HOLD_BASE plus the frames of rest left // below LIGHT_BASE and LIGHT_BASE plus the light frames left above. Rest and the dark frames of the goal follow the light frames. @@ -404,6 +406,11 @@ static Encoded finish_helper(T5s3DisplayInternal* internal) { // Encodes a whole frame on both cores without a budget static Encoded encode_now(T5s3DisplayInternal* internal, uint8_t* bus) { + // restart_doses() covers all rows, so it must not run on the helper while this task encodes the upper half + if (internal->restart) { + internal->restart = false; + restart_doses(internal); + } start_helper(internal, bus, PANEL_HEIGHT / 2, PANEL_HEIGHT, -1); EncodeJob job = { bus, 0, PANEL_HEIGHT / 2, -1, { false, 0 }, 0 }; encode(internal, &job); From 9c2480cd6c0960c4c68e21c82c207985d25d8f17 Mon Sep 17 00:00:00 2001 From: AdaSzi115026 <81822538+AdaSzi@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:25:19 +0100 Subject: [PATCH 5/6] T5S3: thin black lines on gray, restart race fix, user data on SD card - Black is driven for 18 frames, until grays drawn with it end their light frames. Before, a black line of 1 pixel on gray faded after a refresh. - A pending restart of the doses runs on the display task before both cores encode, so it no longer races with the encoding of the upper half. - User data is stored on the SD card --- Devices/lilygo-t5-epd47-s3/device.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Devices/lilygo-t5-epd47-s3/device.properties b/Devices/lilygo-t5-epd47-s3/device.properties index ba0436a20..0e3337150 100644 --- a/Devices/lilygo-t5-epd47-s3/device.properties +++ b/Devices/lilygo-t5-epd47-s3/device.properties @@ -10,7 +10,7 @@ hardware.spiRamSpeed=80M hardware.esptoolFlashFreq=80M hardware.bluetooth=true -storage.userDataLocation=Internal +storage.userDataLocation=SD display.size=4.7" display.dpi=235 From ba7c89f15e714f75e5e4796614f0c612ef13cfa0 Mon Sep 17 00:00:00 2001 From: AdaSzi115026 <81822538+AdaSzi@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:49:04 +0100 Subject: [PATCH 6/6] T5S3 e-paper: set the restart flag while the helper is idle The display task set the restart flag while the helper task could read and clear it. It is now set after finish_helper(), so the helper sees it with its next job. --- Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp b/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp index d1373213a..934f5723e 100644 --- a/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp +++ b/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp @@ -595,12 +595,13 @@ static void display_task(void* arg) { longest_frame_us = std::max(longest_frame_us, frame_us); next_frame_us = frame_start_us + FRAME_PERIOD_US; frames++; - // A late or stalled frame drives some pixels for longer than their dose + const Encoded next = finish_helper(internal); + // A late or stalled frame drives some pixels for longer than their dose. The flag is set while the helper is + // idle, it restarts the doses with its next job. if (late_us > (encoding_late ? ENCODING_STALL_US : STALL_US) || frame_us > FRAME_PERIOD_US + STALL_US) { internal->restart = true; LOG_W(TAG, "Frame %d us late and %d us long, doses start again", static_cast(late_us), static_cast(frame_us)); } - const Encoded next = finish_helper(internal); encoding_late = esp_timer_get_time() > next_frame_us; longest_encode_us = std::max(longest_encode_us, internal->helper_job.duration_us);