// 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