mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 19:05:07 +00:00
597 lines
20 KiB
C++
597 lines
20 KiB
C++
// 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 <tactility/log.h>
|
|
|
|
#include <driver/gpio.h>
|
|
#include <driver/rmt_encoder.h>
|
|
#include <driver/rmt_tx.h>
|
|
#include <esp_heap_caps.h>
|
|
#include <esp_lcd_io_i80.h>
|
|
#include <esp_lcd_panel_io.h>
|
|
#include <esp_rom_sys.h>
|
|
#include <esp_timer.h>
|
|
#include <freertos/FreeRTOS.h>
|
|
#include <freertos/task.h>
|
|
#include <hal/gpio_ll.h>
|
|
#include <soc/gpio_struct.h>
|
|
|
|
#include <cstring>
|
|
|
|
#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<uint8_t*>(heap_caps_aligned_alloc(16, T5S3_EPD_FB_BYTES, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
|
|
engine.back = static_cast<uint8_t*>(heap_caps_aligned_alloc(16, T5S3_EPD_FB_BYTES, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
|
|
for (auto& buffer: engine.row_buffer) {
|
|
buffer = static_cast<uint8_t*>(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<uint32_t*>(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<uint32_t*>(engine.changes);
|
|
bool any_changes = false;
|
|
for (int y = y_start; y < y_end; y++) {
|
|
const auto* to_row = reinterpret_cast<const uint32_t*>(engine.front + y * T5S3_EPD_FB_ROW_BYTES);
|
|
const auto* from_row = reinterpret_cast<const uint32_t*>(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<int>((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<int>(y_start), static_cast<int>(y_end), elapsed_ms);
|
|
} else {
|
|
LOG_D(TAG, "Updated rows %d to %d in %d ms", static_cast<int>(y_start), static_cast<int>(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<int>((esp_timer_get_time() - started) / 1000));
|
|
return true;
|
|
}
|
|
|
|
// endregion
|