Ken Van Hoeylandt 97a5b0eb74
Refactor e-paper drivers and remove chat app (#699)
Replaces the EPDiy and esp_epaper based display drivers with purpose-built drivers, and removes the Chat app.
2026-10-17 22:27:51 +02:00

668 lines
23 KiB
C++

// SPDX-License-Identifier: Apache-2.0
#include "papers3_display.h"
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/display.h>
#include <tactility/error.h>
#include <tactility/log.h>
#include <tactility/memory.h>
#include <tactility/module.h>
#include <driver/gpio.h>
#include <esp_attr.h>
#include <esp_heap_caps.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_panel_ops.h>
#include <esp_rom_sys.h>
#include <esp_timer.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <algorithm>
#include <cstring>
#define TAG "Papers3Display"
#define GET_CONFIG(device) (static_cast<const Papers3DisplayConfig*>((device)->config))
// The panel has no waveform tables. A software state machine decides per pixel and per frame
// what to drive, and a free running task streams every frame over an 8 bit parallel bus.
// 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 = 960;
static constexpr int PANEL_HEIGHT = 540;
static constexpr gpio_num_t PIN_PWR_EN = GPIO_NUM_45;
static constexpr gpio_num_t PIN_BST_EN = GPIO_NUM_46;
static constexpr gpio_num_t PIN_SDCE = GPIO_NUM_13;
static constexpr gpio_num_t PIN_SDLE = GPIO_NUM_15;
static constexpr gpio_num_t PIN_SDCK = GPIO_NUM_16;
static constexpr gpio_num_t PIN_GDSP = GPIO_NUM_17;
static constexpr gpio_num_t PIN_GDCK = GPIO_NUM_18;
static constexpr gpio_num_t PIN_DATA[8] = { GPIO_NUM_6, GPIO_NUM_14, GPIO_NUM_7, GPIO_NUM_12, GPIO_NUM_9, GPIO_NUM_11, GPIO_NUM_8, GPIO_NUM_10 };
// The i80 peripheral needs a data/command pin even though the panel has none
static constexpr gpio_num_t PIN_DUMMY_DC = static_cast<gpio_num_t>(49);
static constexpr uint32_t PIXEL_CLOCK_HZ = 24000000;
// A row is 2 bits per pixel, plus padding for the line latch
static constexpr int LINE_PAD_BYTES = 16;
static constexpr size_t ROW_BYTES = PANEL_WIDTH / 4 + LINE_PAD_BYTES;
// 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;
// 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 POWER_SETTLE_MS = 100;
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" {
extern Module m5stack_papers3_module;
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 Papers3DisplayInternal {
esp_lcd_i80_bus_handle_t bus;
esp_lcd_panel_io_handle_t io;
TaskHandle_t task;
SemaphoreHandle_t request_mutex;
SemaphoreHandle_t request_done;
portMUX_TYPE lock;
volatile bool dma_done;
volatile bool exited;
bool powered;
uint32_t rotation;
// 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;
// 1 bit per pixel in native panel orientation, 1 is black
uint8_t* target;
uint8_t* state;
// Internal DMA capable rows
uint8_t* row_buf[2];
uint8_t* zero_row;
uint8_t* solid_row;
};
// region Panel signalling
static bool IRAM_ATTR on_row_sent(esp_lcd_panel_io_handle_t, esp_lcd_panel_io_event_data_t*, void* user_ctx) {
static_cast<Papers3DisplayInternal*>(user_ctx)->dma_done = true;
return false;
}
static void power_on(Papers3DisplayInternal* internal) {
if (internal->powered) return;
gpio_set_level(PIN_PWR_EN, 1);
esp_rom_delay_us(100);
gpio_set_level(PIN_BST_EN, 1);
esp_rom_delay_us(100);
gpio_set_level(PIN_GDCK, 1);
gpio_set_level(PIN_GDSP, 1);
vTaskDelay(pdMS_TO_TICKS(POWER_SETTLE_MS));
internal->powered = true;
}
static void power_off(Papers3DisplayInternal* internal) {
if (!internal->powered) return;
gpio_set_level(PIN_GDCK, 0);
gpio_set_level(PIN_GDSP, 0);
gpio_set_level(PIN_BST_EN, 0);
esp_rom_delay_us(100);
gpio_set_level(PIN_PWR_EN, 0);
esp_rom_delay_us(100);
internal->powered = false;
}
static void IRAM_ATTR send_row(Papers3DisplayInternal* internal, const uint8_t* data) {
while (!internal->dma_done) {}
gpio_set_level(PIN_GDCK, 0);
gpio_set_level(PIN_SDLE, 1);
gpio_set_level(PIN_SDLE, 0);
gpio_set_level(PIN_GDCK, 1);
internal->dma_done = false;
esp_lcd_panel_io_tx_color(internal->io, -1, data, ROW_BYTES);
}
static void IRAM_ATTR begin_frame() {
gpio_set_level(PIN_GDCK, 1);
esp_rom_delay_us(7);
gpio_set_level(PIN_GDSP, 0);
esp_rom_delay_us(10);
gpio_set_level(PIN_GDCK, 0);
gpio_set_level(PIN_GDCK, 1);
esp_rom_delay_us(8);
gpio_set_level(PIN_GDSP, 1);
esp_rom_delay_us(10);
gpio_set_level(PIN_GDCK, 0);
for (int i = 0; i < 2; i++) {
gpio_set_level(PIN_GDCK, 1);
gpio_set_level(PIN_GDCK, 0);
}
}
static void IRAM_ATTR end_frame(Papers3DisplayInternal* internal) {
send_row(internal, internal->zero_row);
while (!internal->dma_done) {}
}
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) {}
}
// endregion
// region Pixel state machine
// A state byte holds two pixels. The first (left) 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 [x0, x1) of a row, both multiples of 8. Other columns of out stay untouched.
static void IRAM_ATTR encode_row(const Papers3DisplayInternal* internal, int y, int x0, int x1, uint8_t* out) {
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 = (x1 - x0) / 8; count > 0; count--) {
const uint8_t pixels = *target++;
out[0] = static_cast<uint8_t>((drive_pair(state, pixels >> 6) << 4) | drive_pair(state + 1, (pixels >> 4) & 0x3));
out[1] = static_cast<uint8_t>((drive_pair(state + 2, (pixels >> 2) & 0x3) << 4) | drive_pair(state + 3, pixels & 0x3));
state += 4;
out += 2;
}
}
static void IRAM_ATTR run_update_frame(Papers3DisplayInternal* 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
memset(internal->row_buf[0], 0, ROW_BYTES);
memset(internal->row_buf[1], 0, ROW_BYTES);
begin_frame();
int current = 0;
for (int y = 0; y < PANEL_HEIGHT; y++) {
if (y >= region.y0 && y < region.y1) {
// The other buffer may still be in flight while this one is encoded
uint8_t* row = internal->row_buf[current];
encode_row(internal, y, region.x0, region.x1, row);
send_row(internal, row);
current ^= 1;
} else {
send_row(internal, internal->zero_row);
}
}
end_frame(internal);
pad_frame(start_us);
}
// Drives every pixel toward one color. Pattern 0x55 is black and 0xAA is white.
static void run_solid_frames(Papers3DisplayInternal* internal, uint8_t pattern, int frames) {
while (!internal->dma_done) {}
memset(internal->solid_row, pattern, PANEL_WIDTH / 4);
for (int frame = 0; frame < frames; frame++) {
begin_frame();
for (int y = 0; y < PANEL_HEIGHT; y++) {
send_row(internal, internal->solid_row);
}
end_frame(internal);
}
}
static void run_flash(Papers3DisplayInternal* internal) {
for (int cycle = 0; cycle < FLASH_CYCLES; cycle++) {
run_solid_frames(internal, 0x55, FLASH_FIELDS);
run_solid_frames(internal, 0xAA, FLASH_FIELDS);
}
}
static void display_task(void* arg) {
auto* internal = static_cast<Papers3DisplayInternal*>(arg);
Region active = EMPTY_REGION;
int frames_left = 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) {
power_on(internal);
run_flash(internal);
memset(internal->state, STATE_SETTLED_WHITE, STATE_SIZE);
active = EMPTY_REGION;
frames_left = 0;
portENTER_CRITICAL(&internal->lock);
if (!internal->flash_wipes_content) {
internal->pending = FULL_REGION;
}
portEXIT_CRITICAL(&internal->lock);
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);
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;
run_update_frame(internal, aligned);
if (--frames_left == 0) {
active = EMPTY_REGION;
}
} else if (!notified) {
power_off(internal);
}
}
power_off(internal);
internal->exited = true;
vTaskDelete(nullptr);
}
// endregion
// region Helpers
static void notify_task(Papers3DisplayInternal* internal, uint32_t command) {
xTaskNotify(internal->task, command, eSetBits);
}
// Rotated coordinates to native panel coordinates
static inline void to_native(uint32_t rotation, int x, int y, int* native_x, int* native_y) {
switch (rotation) {
case 90:
*native_x = PANEL_WIDTH - 1 - y;
*native_y = x;
break;
case 180:
*native_x = PANEL_WIDTH - 1 - x;
*native_y = PANEL_HEIGHT - 1 - y;
break;
case 270:
*native_x = y;
*native_y = PANEL_HEIGHT - 1 - x;
break;
default:
*native_x = x;
*native_y = y;
break;
}
}
static bool is_portrait(uint32_t rotation) {
return rotation == 90 || rotation == 270;
}
static error_t flash_blocking(Papers3DisplayInternal* 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 papers3_display_reset(Device* device) {
auto* internal = static_cast<Papers3DisplayInternal*>(device_get_driver_data(device));
notify_task(internal, CMD_POWER_OFF);
return ERROR_NONE;
}
static error_t papers3_display_init(Device*) {
return ERROR_NONE;
}
static error_t papers3_display_clear(Device* device) {
auto* internal = static_cast<Papers3DisplayInternal*>(device_get_driver_data(device));
memset(internal->target, 0, TARGET_SIZE);
return flash_blocking(internal, true);
}
static error_t papers3_display_refresh(Device* device) {
auto* internal = static_cast<Papers3DisplayInternal*>(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 papers3_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<Papers3DisplayInternal*>(device_get_driver_data(device));
const uint32_t rotation = internal->rotation;
const int32_t width = x_end - x_start;
const auto* source = static_cast<const uint8_t*>(color_data);
for (int32_t row = 0; row < width * (y_end - y_start); row += width) {
const int32_t y = y_start + row / width;
for (int32_t col = 0; col < width; col++) {
const int32_t x = x_start + col;
int native_x, native_y;
to_native(rotation, x, y, &native_x, &native_y);
uint8_t* byte = internal->target + native_y * TARGET_ROW_BYTES + native_x / 8;
const uint8_t mask = 0x80 >> (native_x & 7);
if (dither_is_black(source[row + col], x, y)) {
*byte |= mask;
} else {
*byte &= ~mask;
}
}
}
int x0, y0, x1, y1;
to_native(rotation, x_start, y_start, &x0, &y0);
to_native(rotation, x_end - 1, y_end - 1, &x1, &y1);
const Region changed = { std::min(x0, x1), std::min(y0, y1), std::max(x0, x1) + 1, std::max(y0, y1) + 1 };
portENTER_CRITICAL(&internal->lock);
internal->pending.merge(changed);
portEXIT_CRITICAL(&internal->lock);
notify_task(internal, CMD_UPDATE);
return ERROR_NONE;
}
static error_t papers3_display_disp_on_off(Device* device, bool on_off) {
auto* internal = static_cast<Papers3DisplayInternal*>(device_get_driver_data(device));
if (!on_off) {
notify_task(internal, CMD_POWER_OFF);
}
return ERROR_NONE;
}
static DisplayColorFormat papers3_display_get_color_format(Device*) {
return DISPLAY_COLOR_FORMAT_GRAYSCALE8;
}
static uint16_t papers3_display_get_resolution_x(Device* device) {
return is_portrait(GET_CONFIG(device)->rotation) ? PANEL_HEIGHT : PANEL_WIDTH;
}
static uint16_t papers3_display_get_resolution_y(Device* device) {
return is_portrait(GET_CONFIG(device)->rotation) ? PANEL_WIDTH : PANEL_HEIGHT;
}
// endregion
static constexpr DisplayApi papers3_display_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 = papers3_display_reset,
.init = papers3_display_init,
.draw_bitmap = papers3_display_draw_bitmap,
.clear = papers3_display_clear,
.refresh = papers3_display_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 = papers3_display_disp_on_off,
.disp_sleep = nullptr,
.get_color_format = papers3_display_get_color_format,
.get_resolution_x = papers3_display_get_resolution_x,
.get_resolution_y = papers3_display_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 free_buffers(Papers3DisplayInternal* internal) {
memory_free(internal->target);
memory_free(internal->state);
heap_caps_free(internal->row_buf[0]);
heap_caps_free(internal->row_buf[1]);
heap_caps_free(internal->zero_row);
heap_caps_free(internal->solid_row);
}
static bool allocate_buffers(Papers3DisplayInternal* internal) {
static const MemoryPolicy external_policy = { .required = 0, .desired = MEMORY_CAPABILITY_EXTERNAL, .alignment = 16 };
internal->target = static_cast<uint8_t*>(memory_calloc_with_policy(1, TARGET_SIZE, &external_policy));
internal->state = static_cast<uint8_t*>(memory_alloc_with_policy(STATE_SIZE, &external_policy));
// Pad so DMA bursts never read past the row
constexpr size_t dma_row_size = ROW_BYTES + 16;
uint8_t** dma_rows[] = { &internal->row_buf[0], &internal->row_buf[1], &internal->zero_row, &internal->solid_row };
for (uint8_t** row : dma_rows) {
*row = static_cast<uint8_t*>(heap_caps_aligned_calloc(16, 1, dma_row_size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL));
}
return internal->target && internal->state && internal->row_buf[0] && internal->row_buf[1] && internal->zero_row && internal->solid_row;
}
static void configure_gpio() {
gpio_config_t config = {
.pin_bit_mask = (1ULL << PIN_PWR_EN) | (1ULL << PIN_BST_EN) | (1ULL << PIN_SDLE) | (1ULL << PIN_GDSP) | (1ULL << PIN_GDCK),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&config);
}
static esp_err_t create_bus(Papers3DisplayInternal* internal) {
esp_lcd_i80_bus_config_t bus_config = {};
bus_config.dc_gpio_num = PIN_DUMMY_DC;
bus_config.wr_gpio_num = PIN_SDCK;
bus_config.clk_src = LCD_CLK_SRC_PLL160M;
for (int i = 0; i < 8; i++) {
bus_config.data_gpio_nums[i] = PIN_DATA[i];
}
bus_config.bus_width = 8;
bus_config.max_transfer_bytes = ROW_BYTES;
bus_config.dma_burst_size = 32;
esp_err_t result = esp_lcd_new_i80_bus(&bus_config, &internal->bus);
if (result != ESP_OK) return result;
esp_lcd_panel_io_i80_config_t io_config = {};
io_config.cs_gpio_num = PIN_SDCE;
io_config.pclk_hz = PIXEL_CLOCK_HZ;
io_config.trans_queue_depth = 4;
io_config.on_color_trans_done = on_row_sent;
io_config.user_ctx = internal;
io_config.lcd_cmd_bits = 8;
io_config.lcd_param_bits = 8;
io_config.dc_levels.dc_data_level = 1;
result = esp_lcd_new_panel_io_i80(internal->bus, &io_config, &internal->io);
if (result != ESP_OK) {
esp_lcd_del_i80_bus(internal->bus);
}
return result;
}
static void destroy(Papers3DisplayInternal* internal) {
if (internal->io) esp_lcd_panel_io_del(internal->io);
if (internal->bus) esp_lcd_del_i80_bus(internal->bus);
if (internal->request_mutex) vSemaphoreDelete(internal->request_mutex);
if (internal->request_done) vSemaphoreDelete(internal->request_done);
free_buffers(internal);
memory_free(internal);
}
static error_t start(Device* device) {
static const MemoryPolicy internal_policy = { .required = MEMORY_CAPABILITY_INTERNAL, .desired = 0, .alignment = 0 };
auto* internal = static_cast<Papers3DisplayInternal*>(memory_calloc_with_policy(1, sizeof(Papers3DisplayInternal), &internal_policy));
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
internal->rotation = GET_CONFIG(device)->rotation;
internal->lock = portMUX_INITIALIZER_UNLOCKED;
internal->dma_done = true;
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);
configure_gpio();
if (create_bus(internal) != ESP_OK) {
LOG_E(TAG, "Failed to create parallel bus");
internal->bus = nullptr;
internal->io = nullptr;
destroy(internal);
return ERROR_RESOURCE;
}
if (xTaskCreatePinnedToCore(display_task, "papers3_display", 4096, internal, 1, &internal->task, 1) != pdPASS) {
LOG_E(TAG, "Failed to create display task");
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, rotation %lu)", PANEL_WIDTH, PANEL_HEIGHT, internal->rotation);
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<Papers3DisplayInternal*>(device_get_driver_data(device));
notify_task(internal, CMD_EXIT);
while (!internal->exited) {
vTaskDelay(1);
}
device_set_driver_data(device, nullptr);
destroy(internal);
return ERROR_NONE;
}
// endregion
Driver papers3_display_driver = {
.name = "papers3-display",
.compatible = (const char*[]) { "m5stack,papers3-display", nullptr },
.start_device = start,
.stop_device = stop,
.probe = nullptr,
.api = &papers3_display_api,
.device_type = &DISPLAY_TYPE,
.internal = nullptr
};
}