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 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..63731dc28 100644 --- a/Devices/lilygo-t5-epd47-s3/lilygo,t5-epd47-s3.dts +++ b/Devices/lilygo-t5-epd47-s3/lilygo,t5-epd47-s3.dts @@ -12,6 +12,7 @@ #include #include #include +// #include / { compatible = "root"; @@ -33,6 +34,8 @@ // Listed before the I2C bus on purpose, the touch controller was only tested with the display started first display { 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/CMakeLists.txt b/Drivers/lilygo-t5s3-epd-module/CMakeLists.txt index 14415ac4b..001a3d22e 100644 --- a/Drivers/lilygo-t5s3-epd-module/CMakeLists.txt +++ b/Drivers/lilygo-t5s3-epd-module/CMakeLists.txt @@ -12,7 +12,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..096b5eda2 100644 --- a/Drivers/lilygo-t5s3-epd-module/README.md +++ b/Drivers/lilygo-t5s3-epd-module/README.md @@ -1,28 +1,30 @@ # 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. -- `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. +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: -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. +- `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: + +- `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. + +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 engine and its tables 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_rows.h` -- `source/t5s3_epd_waveform.h` +- `source/t5s3_epd_board.cpp` +- `source/t5s3_epd_board.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/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. 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 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/bindings/lilygo,t5s3-display.yaml b/Drivers/lilygo-t5s3-epd-module/bindings/lilygo,t5s3-display.yaml index 47f6ac120..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,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 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. 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/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..934f5723e 100644 --- a/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp +++ b/Drivers/lilygo-t5s3-epd-module/source/t5s3_display.cpp @@ -6,31 +6,117 @@ #include #include #include +#include #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)) -// 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; - -// 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; - -// 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; - -// Maximum time that consecutive quality updates keep extending the hold window -static constexpr uint32_t QUALITY_HOLD_SESSION_MAX_MS = 200; - -// 4x4 ordered (Bayer) dither thresholds, spread evenly across a 0-15 nibble range. +// 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 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. + +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 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 = 16; +static constexpr uint8_t SHADE_BLACK = 0; +static constexpr uint8_t SHADE_WHITE = SHADES - 1; +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; +// 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. +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_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] = { { 0, 8, 2, 10 }, { 12, 4, 14, 6 }, @@ -38,49 +124,527 @@ 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 shades and is dithered between them +struct Tone { + uint8_t darker; + uint8_t lighter; + // Share of the lighter shade, 0 to 16 + uint8_t share; +}; -// 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 void build_tones(Tone* tones) { + for (int luminance = 0; luminance < 256; luminance++) { + int shade = 0; + while (shade < SHADES - 2 && luminance > SHADE_BRIGHTNESS[shade + 1]) { + shade++; + } + 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)); + } } extern "C" { +// Result of encoding a frame +struct Encoded { + bool driven; + // Rows [0, rows) hold all driven pixels + int 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; + int budget; + Encoded result; + int64_t duration_us; +}; + struct T5s3DisplayInternal { - uint8_t* framebuffer; + TaskHandle_t task; + SemaphoreHandle_t request_mutex; + SemaphoreHandle_t request_done; + 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; + // 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; + // 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 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) { + 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)]; + } + 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) { + // 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); + 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) { - if (!internal->powered) { - t5s3_epd_power_on(); - internal->powered = true; + 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; +} + +// 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; +} + +// 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); + 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)); + } + 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)); } +// 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); + } +} + +// 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)); + } + while (esp_timer_get_time() < time_us) {} +} + +static void display_task(void* arg) { + auto* internal = static_cast(arg); + 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; + 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; + + if (commands & CMD_EXIT) { + break; + } + + if (commands & CMD_POWER_OFF) { + active = false; + power_off(internal); + } + + if (commands & CMD_FLASH) { + run_flash(internal); + active = false; + changed_since_refresh = false; + last_refresh_us = esp_timer_get_time(); + xSemaphoreGive(internal->request_done); + } + + 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(); + } + + if (!active) { + current = 0; + frame = encode_now(internal, internal->bus[current]); + if (!frame.driven) { + if (!notified) { + power_off(internal); + } + continue; + } + 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); + 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++; + 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)); + } + 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); + internal->exited = true; + vTaskDelete(nullptr); +} + +// 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); + 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_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(); - internal->powered = false; - power_on(internal); + // The panel has no reset line, it powers up again with the next update + notify_task(internal, CMD_POWER_OFF); return ERROR_NONE; } @@ -89,115 +653,56 @@ static error_t t5s3_display_init(Device*) { return ERROR_NONE; } -/** Fills the panel white with one full update, without the flashing cycles of the initial clear */ +/** Flashes the panel to white */ static error_t t5s3_display_clear(Device* device) { auto* internal = static_cast(device_get_driver_data(device)); - power_on(internal); - 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; - } - return success ? ERROR_NONE : ERROR_RESOURCE; + memset(internal->target, WHITE_PAIR, SHADE_SIZE); + return flash_blocking(internal); } -/** - * 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. - */ +/** 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)); - 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; - } - return success ? ERROR_NONE : ERROR_RESOURCE; -} - -// 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; -} - -// 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; - } - } - } else { - internal->partial_count_since_quality++; - } + 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 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(); + const auto* source = static_cast(color_data); 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); - - // 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 - 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 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* 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++) { + 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); } - 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); + 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); + 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) { - power_on(internal); - } else if (internal->powered) { - t5s3_epd_power_off(); - internal->powered = false; + if (!on_off) { + notify_task(internal, CMD_POWER_OFF); } return ERROR_NONE; } @@ -207,17 +712,17 @@ 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 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 +748,116 @@ 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 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->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); +} + 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->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 (!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->target, WHITE_PAIR, SHADE_SIZE); + memset(internal->goal, WHITE_PAIR, SHADE_SIZE); + build_tones(internal->tones); - if (!t5s3_epd_init(config->pixel_clock_hz)) { - free(internal); + 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; } - internal->framebuffer = t5s3_epd_framebuffer(); - internal->partial_count_since_quality = 0; - internal->quality_hold_until_tick = 0; - internal->quality_hold_session_start_tick = 0; + 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, TASK_PRIORITY, &internal->task, TASK_CORE) != pdPASS) { + LOG_E(TAG, "Failed to create display task"); + stop_helper(internal); + t5s3_epd_board_deinit(); + destroy(internal); + return ERROR_RESOURCE; + } 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(); + // The boot splash leaves residue that a regular update would never clear + flash_blocking(internal); - LOG_I(TAG, "Initialized (%dx%d)", T5S3_EPD_WIDTH, T5S3_EPD_HEIGHT); + 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)); - - if (internal->powered) { - t5s3_epd_power_off(); - internal->powered = false; + notify_task(internal, CMD_EXIT); + while (!internal->exited) { + vTaskDelay(1); } - - t5s3_epd_deinit(); - - free(internal); + stop_helper(internal); device_set_driver_data(device, nullptr); + t5s3_epd_board_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 deleted file mode 100644 index 39f9f83fb..000000000 --- a/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd.cpp +++ /dev/null @@ -1,596 +0,0 @@ -// 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). -// -// 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.h" - -#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; -// 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; - 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 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; - } - - // 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; -} - -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; -} - -bool t5s3_epd_init(uint32_t pixel_clock_hz) { - if (engine.front != 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)); - } - if (engine.front == nullptr || engine.back == nullptr || engine.row_buffer[0] == nullptr || engine.row_buffer[1] == 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); - - if (!setup_register_pins() || !setup_bus(pixel_clock_hz) || !setup_pulse()) { - 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; - } - 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(); -} - -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(); -} - -// endregion - -// region Updates - -uint8_t* t5s3_epd_framebuffer() { - return engine.front; -} - -void t5s3_epd_fill_white() { - memset(engine.front, 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]; - } -} - -bool t5s3_epd_update(T5s3EpdMode mode, int32_t y_start, int32_t y_end) { - 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; - 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); - 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; - } - if (!any_changes) { - return true; - } - t5s3_epd_build_line_mask(engine.line_mask, engine.changes); - - 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(); - } - } 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); - } - } - - // 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; - } - - 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); - } - } - - 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); - } - 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); - vTaskDelay(1); - } -} - -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)) { - 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); - } - if (engine.failed) { - 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 deleted file mode 100644 index f6dd6b898..000000000 --- a/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd.h +++ /dev/null @@ -1,54 +0,0 @@ -// 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. - -#pragma once - -#include "t5s3_epd_rows.h" - -#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 -}; - -// 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 t5s3_epd_update(). -uint8_t* t5s3_epd_framebuffer(); - -void t5s3_epd_power_on(); - -void t5s3_epd_power_off(); - -// 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); - -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 deleted file mode 100644 index 34f36befc..000000000 --- a/Drivers/lilygo-t5s3-epd-module/source/t5s3_epd_rows.h +++ /dev/null @@ -1,106 +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_waveform.h" - -#include - -#ifdef ESP_PLATFORM -#include -#define T5S3_EPD_IRAM IRAM_ATTR -#else -#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 -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) { - action = T5S3_EPD_ACTION_DARK; - } else if (to == 15 && from != 15) { - action = T5S3_EPD_ACTION_LIGHT; - } - table[(to << 4) | from] = action; - } - } -} - -// 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) { - 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; - } - } - if (!flash_white) { - table[(15 << 4) | 15] = 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, - }, -};