Files
tactility/Drivers/gdeq031t10-module/source/gdeq031t10.cpp
Ken Van Hoeylandt 29e80cfd65 T-Deck Max & Pro support updates (#582)
- Created kernel driver for T-Deck Max & Pro display (needs work, doesn't refresh reliably on T-Deck Pro)
- Created T-Deck Pro device implementation (incubating)
2026-07-24 00:17:48 +02:00

656 lines
25 KiB
C++

// SPDX-License-Identifier: Apache-2.0
#include <drivers/gdeq031t10.h>
#include <gdeq031t10_module.h>
#include <tactility/check.h>
#include <tactility/delay.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/display.h>
#include <tactility/drivers/esp32_spi.h>
#include <tactility/drivers/gpio_controller.h>
#include <tactility/drivers/spi_controller.h>
#include <tactility/error.h>
#include <tactility/log.h>
#include <tactility/time.h>
#include <driver/spi_master.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <cstdlib>
#include <cstring>
constexpr auto* TAG = "GDEQ031T10";
#define GET_CONFIG(device) (static_cast<const Gdeq031t10Config*>((device)->config))
static constexpr int WIDTH = 240;
static constexpr int HEIGHT = 320;
static constexpr size_t FRAMEBUFFER_SIZE = (WIDTH * HEIGHT) / 8; // 1 bpp packed
static constexpr int BYTES_PER_ROW = WIDTH / 8;
// UC8253-family commands, ported from Xinyuan-LilyGO/T-Deck-MAX's
// Display_EPD_W21.cpp reference driver.
static constexpr uint8_t CMD_PANEL_SETTING = 0x00;
static constexpr uint8_t CMD_POWER_ON_OFF = 0x02; // shared opcode: power on when followed by 0x04, power off as standalone 0x02
static constexpr uint8_t CMD_POWER_ON = 0x04;
static constexpr uint8_t CMD_DEEP_SLEEP = 0x07;
static constexpr uint8_t CMD_DATA_START_OLD = 0x10;
static constexpr uint8_t CMD_DISPLAY_REFRESH = 0x12;
static constexpr uint8_t CMD_DATA_START_NEW = 0x13;
static constexpr uint8_t CMD_VCOM_DATA_INTERVAL = 0x50;
static constexpr uint8_t CMD_PARTIAL_WINDOW = 0x90;
static constexpr uint8_t CMD_PARTIAL_IN = 0x91;
static constexpr uint8_t CMD_PARTIAL_OUT = 0x92;
static constexpr uint8_t CMD_FAST_MODE_ENABLE = 0xE0;
static constexpr uint8_t CMD_FAST_MODE_TIMING = 0xE5;
static constexpr uint8_t DEEP_SLEEP_CHECK_CODE = 0xA5;
extern "C" {
struct Gdeq031t10Internal {
spi_device_handle_t spi_device;
struct GpioDescriptor* dc;
struct GpioDescriptor* reset; // optional
struct GpioDescriptor* busy;
/** Mirrors what the panel currently holds, required by the controller's
* "old data" + "new data" double-buffered refresh protocol. Panel polarity
* matches the LVGL 1bpp render data directly (bit 1 = white, bit 0 = black). */
uint8_t* shadow_framebuffer;
/** Scratch buffer used to gather a windowed region's bytes for one SPI write. */
uint8_t* region_buffer;
/** Serializes panel/SPI access between draw_bitmap and power on/off calls. */
SemaphoreHandle_t panel_mutex;
/** Waveform mode the panel registers currently hold; valid=false when the
* registers are in an unknown state (before first init, or after deep sleep,
* which requires a reset that restores defaults). */
enum Gdeq031t10RefreshMode panel_mode;
bool panel_mode_valid;
/** True while the panel's charge pump is on (CMD_POWER_ON issued, no power-off since). */
bool panel_power_on;
/** disp_on_off state; the panel is in deep sleep while false. */
bool display_on;
/** Forces the next refresh to be a full-screen refresh (set at boot and by reset()). */
bool force_full_refresh;
};
// region Panel protocol
static bool write_command(Gdeq031t10Internal* internal, uint8_t command) {
gpio_descriptor_set_level(internal->dc, false);
spi_transaction_t transaction = {};
transaction.length = 8;
transaction.tx_buffer = &command;
if (spi_device_polling_transmit(internal->spi_device, &transaction) != ESP_OK) {
LOG_E(TAG, "SPI command transfer failed");
return false;
}
return true;
}
static bool write_data(Gdeq031t10Internal* internal, const uint8_t* data, size_t length) {
gpio_descriptor_set_level(internal->dc, true);
spi_transaction_t transaction = {};
transaction.length = length * 8;
transaction.tx_buffer = data;
if (spi_device_polling_transmit(internal->spi_device, &transaction) != ESP_OK) {
LOG_E(TAG, "SPI data transfer failed");
return false;
}
return true;
}
static bool write_data_byte(Gdeq031t10Internal* internal, uint8_t data) {
return write_data(internal, &data, 1);
}
static bool wait_while_busy(Gdeq031t10Internal* internal) {
// gpio_descriptor_get_level() reports the panel's busy state directly (true = busy).
const TickType_t timeout = pdMS_TO_TICKS(5000);
const TickType_t start = get_ticks();
bool busy = true;
while (gpio_descriptor_get_level(internal->busy, &busy) == ERROR_NONE && busy) {
if (get_ticks() - start > timeout) {
LOG_E(TAG, "Timed out waiting for panel BUSY");
return false;
}
delay_millis(2);
}
LOG_I(TAG, "waited %lu ms until not busy", get_ticks() - start);
return !busy;
}
static void hardware_reset(Gdeq031t10Internal* internal) {
if (internal->reset != nullptr) {
gpio_descriptor_set_level(internal->reset, false);
delay_millis(10);
gpio_descriptor_set_level(internal->reset, true);
delay_millis(10);
}
}
static bool init_full(Gdeq031t10Internal* internal, bool mirror_180) {
bool ok = write_command(internal, CMD_PANEL_SETTING);
ok = ok && write_data_byte(internal, mirror_180 ? 0x13 : 0x1F);
ok = ok && write_command(internal, CMD_POWER_ON);
ok = ok && wait_while_busy(internal);
if (!ok) {
LOG_E(TAG, "Full init failed");
return false;
}
internal->panel_mode = GDEQ031T10_REFRESH_FULL;
internal->panel_mode_valid = true;
internal->panel_power_on = true;
return true;
}
static bool init_with_fast_lut(Gdeq031t10Internal* internal, bool mirror_180, enum Gdeq031t10RefreshMode mode) {
if (!init_full(internal, mirror_180)) {
return false;
}
// Fast-LUT timing values from the vendor reference driver: ~1.0s (fast),
// ~1.5s (slow, extra settling) and the partial-mode value.
uint8_t timing;
switch (mode) {
case GDEQ031T10_REFRESH_FAST: timing = 0x5A; break;
case GDEQ031T10_REFRESH_SLOW: timing = 0x6E; break;
case GDEQ031T10_REFRESH_PARTIAL: timing = 0x79; break;
default: return true; // GDEQ031T10_REFRESH_FULL: init_full() already did everything
}
bool ok = write_command(internal, CMD_FAST_MODE_ENABLE);
ok = ok && write_data_byte(internal, 0x02);
ok = ok && write_command(internal, CMD_FAST_MODE_TIMING);
ok = ok && write_data_byte(internal, timing);
if (ok && mode == GDEQ031T10_REFRESH_PARTIAL) {
ok = write_command(internal, CMD_VCOM_DATA_INTERVAL);
ok = ok && write_data_byte(internal, 0xD7);
}
if (!ok) {
LOG_E(TAG, "Mode init failed");
return false;
}
internal->panel_mode = mode;
return true;
}
static bool ensure_panel_ready(Gdeq031t10Internal* internal, bool mirror_180, enum Gdeq031t10RefreshMode mode) {
if (!internal->panel_mode_valid || internal->panel_mode != mode) {
return init_with_fast_lut(internal, mirror_180, mode);
} else if (!internal->panel_power_on) {
// Registers still hold the mode; only the charge pump was idled.
if (!write_command(internal, CMD_POWER_ON) || !wait_while_busy(internal)) {
LOG_E(TAG, "Panel did not become ready after power-on");
return false;
}
internal->panel_power_on = true;
}
return true;
}
static bool panel_power_off(Gdeq031t10Internal* internal) {
// Command the panel off regardless of whether BUSY confirms it: retrying
// forever here would just as likely hang, and a stuck-BUSY panel is
// already unusable either way.
bool ok = write_command(internal, CMD_POWER_ON_OFF); // 0x02 standalone = power off
if (!ok || !wait_while_busy(internal)) {
LOG_E(TAG, "Panel did not confirm power-off");
ok = false;
}
internal->panel_power_on = false;
return ok;
}
static void refresh_full(Gdeq031t10Internal* internal, bool mirror_180, enum Gdeq031t10RefreshMode mode, const uint8_t* render_bitmap) {
// Always do a full register reload before this update, and put the panel back into
// deep sleep after it, instead of caching/reusing state across updates the way
// ensure_panel_ready() does when the mode hasn't changed. This matches the vendor reference
// driver (Xinyuan-LilyGO/T-Deck-MAX's Display_EPD_W21.cpp) exactly: it calls
// EPD_Init()/EPD_Init_Fast() before every single update and EPD_DeepSleep() after every
// single update, never reusing controller state between them. Reusing state - or tracking
// real old-data without also doing this, or vice versa - each independently left the panel
// showing an intermittent/alternating blank screen; only the combination matches what the
// panel's internal state machine tolerates.
if (!init_with_fast_lut(internal, mirror_180, mode)) {
LOG_E(TAG, "Skipping full refresh: panel not ready");
return;
}
// shadow_framebuffer holds the panel's actual current content (matches the vendor's tracked
// oldData[] buffer) - send it as "old" data so the controller computes correct per-pixel
// transitions.
LOG_I(TAG, "write old data from shadow_buffer");
if (!write_command(internal, CMD_DATA_START_OLD) || !write_data(internal, internal->shadow_framebuffer, FRAMEBUFFER_SIZE)) {
LOG_E(TAG, "Failed to send old frame data");
return;
}
LOG_I(TAG, "memcpy render_bitmap -> shadow_buffer");
std::memcpy(internal->shadow_framebuffer, render_bitmap, FRAMEBUFFER_SIZE);
if (!write_command(internal, CMD_DATA_START_NEW) || !write_data(internal, internal->shadow_framebuffer, FRAMEBUFFER_SIZE)) {
LOG_E(TAG, "Failed to send new frame data");
return;
}
if (!write_command(internal, CMD_DISPLAY_REFRESH)) {
LOG_E(TAG, "Failed to trigger display refresh");
return;
}
delay_millis(1); // datasheet requires >=200us settle before polling BUSY
if (!wait_while_busy(internal)) {
LOG_E(TAG, "Full refresh did not complete");
}
// EPD_DeepSleep(): power off, then actually deep-sleep rather than just idling the charge
// pump. panel_mode_valid=false forces the next refresh_full() call back through a fresh
// init above instead of reusing (possibly stale) controller state.
if (internal->panel_power_on) {
panel_power_off(internal);
}
}
/** Windowed partial refresh of the given byte-column/row bounding box. */
static void refresh_window(Gdeq031t10Internal* internal, bool mirror_180, const uint8_t* render_bitmap, int first_byte_col, int last_byte_col, int first_row, int last_row) {
// Partial LUT (fast waveform via temperature force) on a sub-region only. The
// RAM window must be byte-aligned in X, which it already is (byte columns).
if (!ensure_panel_ready(internal, mirror_180, GDEQ031T10_REFRESH_PARTIAL)) {
LOG_E(TAG, "Skipping window refresh: panel not ready");
return;
}
const int width_bytes = last_byte_col - first_byte_col + 1;
const uint16_t x = static_cast<uint16_t>(first_byte_col * 8);
const uint16_t xe = static_cast<uint16_t>(last_byte_col * 8 + 7);
const uint16_t y = static_cast<uint16_t>(first_row);
const uint16_t ye = static_cast<uint16_t>(last_row);
// Set the partial RAM window (GxEPD2 GDEQ031T10 sequence).
bool ok = write_command(internal, CMD_PARTIAL_IN);
ok = ok && write_command(internal, CMD_PARTIAL_WINDOW);
ok = ok && write_data_byte(internal, static_cast<uint8_t>(x));
ok = ok && write_data_byte(internal, static_cast<uint8_t>(xe));
ok = ok && write_data_byte(internal, static_cast<uint8_t>(y >> 8));
ok = ok && write_data_byte(internal, static_cast<uint8_t>(y & 0xFF));
ok = ok && write_data_byte(internal, static_cast<uint8_t>(ye >> 8));
ok = ok && write_data_byte(internal, static_cast<uint8_t>(ye & 0xFF));
ok = ok && write_data_byte(internal, 0x01);
if (!ok) {
LOG_E(TAG, "Failed to set partial refresh window");
write_command(internal, CMD_PARTIAL_OUT); // best-effort: leave partial-window mode
return;
}
// Old region: the region's current panel contents (shadow), gathered contiguously.
size_t n = 0;
for (int row = first_row; row <= last_row; row++) {
const size_t base = static_cast<size_t>(row) * BYTES_PER_ROW + first_byte_col;
std::memcpy(&internal->region_buffer[n], &internal->shadow_framebuffer[base], width_bytes);
n += width_bytes;
}
if (!write_command(internal, CMD_DATA_START_OLD) || !write_data(internal, internal->region_buffer, n)) {
LOG_E(TAG, "Failed to send old window data");
write_command(internal, CMD_PARTIAL_OUT);
return;
}
// New region: render data, and update the shadow for this region as we go.
n = 0;
for (int row = first_row; row <= last_row; row++) {
const size_t base = static_cast<size_t>(row) * BYTES_PER_ROW + first_byte_col;
for (int c = 0; c < width_bytes; c++) {
const uint8_t value = render_bitmap[base + c];
internal->region_buffer[n++] = value;
internal->shadow_framebuffer[base + c] = value;
}
}
if (!write_command(internal, CMD_DATA_START_NEW) || !write_data(internal, internal->region_buffer, n)) {
LOG_E(TAG, "Failed to send new window data");
write_command(internal, CMD_PARTIAL_OUT);
return;
}
if (write_command(internal, CMD_DISPLAY_REFRESH)) {
delay_millis(1);
if (!wait_while_busy(internal)) {
LOG_E(TAG, "Window refresh did not complete");
}
} else {
LOG_E(TAG, "Failed to trigger window refresh");
}
write_command(internal, CMD_PARTIAL_OUT);
}
// endregion
// region DisplayApi
static error_t gdeq031t10_reset(Device* device) {
auto* internal = static_cast<Gdeq031t10Internal*>(device_get_driver_data(device));
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
// The reset pulse restores register defaults and drops the charge pump.
hardware_reset(internal);
internal->panel_mode_valid = false;
internal->panel_power_on = false;
internal->force_full_refresh = true;
xSemaphoreGive(internal->panel_mutex);
return ERROR_NONE;
}
static error_t gdeq031t10_init(Device* device) {
auto* internal = static_cast<Gdeq031t10Internal*>(device_get_driver_data(device));
const auto* config = GET_CONFIG(device);
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
bool ok = init_full(internal, config->mirror_180);
xSemaphoreGive(internal->panel_mutex);
return ok ? ERROR_NONE : ERROR_RESOURCE;
}
// LVGL only ever calls this with the full frame: DISPLAY_COLOR_FORMAT_MONOCHROME forces
// LV_DISPLAY_RENDER_MODE_FULL in the generic kernel LVGL bridge (lvgl_display.c), and FULL mode
// only presents (calls draw_bitmap) once per render cycle, with the complete 0,0..hres,vres rect.
// color_data is row-major, MSB-first 1bpp (LVGL's LV_COLOR_FORMAT_I1 with the palette header
// already stripped by the caller); bit 1 = white, bit 0 = black. The panel expects the same
// polarity, so color_data is written to the panel unmodified.
static error_t gdeq031t10_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<Gdeq031t10Internal*>(device_get_driver_data(device));
const auto* config = GET_CONFIG(device);
LOG_I(TAG, "draw_bitmap %d %d - %d %d", x_start, y_start, x_end, y_end);
if (x_start != 0 || y_start != 0 || x_end != WIDTH || y_end != HEIGHT) {
LOG_I(TAG, "draw_bitmap: Only full-frame draws are supported (got %ld,%ld..%ld,%ld)", (long)x_start, (long)y_start, (long)x_end, (long)y_end);
return ERROR_NOT_SUPPORTED;
}
const auto* render_bitmap = static_cast<const uint8_t*>(color_data);
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
// Work-around until partial updates are working
internal->force_full_refresh = true;
if (!internal->display_on) {
// Display is off (deep sleep). Drop the frame without touching the shadow: the
// mismatch it leaves behind makes the frame redraw after the next power-on.
xSemaphoreGive(internal->panel_mutex);
LOG_W(TAG, "draw_bitmap: ignoring drawing, display is off");
return ERROR_NONE;
}
// Find the bounding box of bytes that differ from what the panel holds (shadow
// holds the same polarity as render_bitmap).
int first_col = BYTES_PER_ROW, last_col = -1, first_row = HEIGHT, last_row = -1;
for (int row = 0; row < HEIGHT; row++) {
const size_t base = static_cast<size_t>(row) * BYTES_PER_ROW;
for (int col = 0; col < BYTES_PER_ROW; col++) {
if (render_bitmap[base + col] != internal->shadow_framebuffer[base + col]) {
if (col < first_col) first_col = col;
if (col > last_col) last_col = col;
if (row < first_row) first_row = row;
if (row > last_row) last_row = row;
}
}
}
const bool nothing_changed = (last_col < 0);
if (nothing_changed && !internal->force_full_refresh) {
xSemaphoreGive(internal->panel_mutex);
return ERROR_NONE; // panel already shows this frame; don't refresh needlessly
}
if (internal->force_full_refresh) {
LOG_I(TAG, "draw_bitmap: refresh_full");
refresh_full(internal, config->mirror_180, config->refresh_mode, render_bitmap);
internal->force_full_refresh = false;
} else {
LOG_I(TAG, "draw_bitmap: refresh_window");
refresh_window(internal, config->mirror_180, render_bitmap, first_col, last_col, first_row, last_row);
}
// The refresh is synchronous, so the pipeline is idle here: drop the charge pump rather
// than leave it running. The mode registers survive a power-off, so the next refresh
// only pays a short power-on wait.
if (internal->panel_power_on) {
panel_power_off(internal);
}
xSemaphoreGive(internal->panel_mutex);
return ERROR_NONE;
}
static error_t gdeq031t10_disp_on_off(Device* device, bool on_off) {
auto* internal = static_cast<Gdeq031t10Internal*>(device_get_driver_data(device));
const auto* config = GET_CONFIG(device);
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
if (on_off == internal->display_on) {
xSemaphoreGive(internal->panel_mutex);
return ERROR_NONE;
}
bool ok = true;
if (on_off) {
// Toggling RST (in init_full) also wakes the panel from deep sleep. The panel kept
// its image through deep sleep and the shadow still matches it, so no forced
// refresh is needed: any frame dropped while off left a shadow mismatch that the
// next draw picks up.
ok = init_full(internal, config->mirror_180);
if (ok && internal->panel_power_on) {
// init_full left the charge pump on; idle it until the next draw needs it.
panel_power_off(internal);
}
} else {
if (internal->panel_power_on) {
panel_power_off(internal);
}
delay_millis(100);
write_command(internal, CMD_DEEP_SLEEP);
write_data_byte(internal, DEEP_SLEEP_CHECK_CODE);
// Deep sleep needs a reset to wake, which restores register defaults.
internal->panel_mode_valid = false;
}
internal->display_on = on_off;
xSemaphoreGive(internal->panel_mutex);
return ok ? ERROR_NONE : ERROR_RESOURCE;
}
static DisplayColorFormat gdeq031t10_get_color_format(Device*) {
return DISPLAY_COLOR_FORMAT_MONOCHROME;
}
static uint16_t gdeq031t10_get_resolution_x(Device*) {
return WIDTH;
}
static uint16_t gdeq031t10_get_resolution_y(Device*) {
return HEIGHT;
}
static void gdeq031t10_get_frame_buffer(Device*, uint8_t, void** out_buffer) {
*out_buffer = nullptr;
}
static uint8_t gdeq031t10_get_frame_buffer_count(Device*) {
return 0;
}
// endregion
static const DisplayApi gdeq031t10_display_api = {
.capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLOW_REFRESH,
.reset = gdeq031t10_reset,
.init = gdeq031t10_init,
.draw_bitmap = gdeq031t10_draw_bitmap,
.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 = gdeq031t10_disp_on_off,
.disp_sleep = nullptr,
.get_color_format = gdeq031t10_get_color_format,
.get_resolution_x = gdeq031t10_get_resolution_x,
.get_resolution_y = gdeq031t10_get_resolution_y,
.get_frame_buffer = gdeq031t10_get_frame_buffer,
.get_frame_buffer_count = gdeq031t10_get_frame_buffer_count,
.get_backlight = nullptr,
.has_capability = nullptr,
};
// region Driver lifecycle
static void free_internal(Gdeq031t10Internal* internal) {
if (internal->spi_device != nullptr) {
spi_bus_remove_device(internal->spi_device);
}
if (internal->dc != nullptr) {
gpio_descriptor_release(internal->dc);
}
if (internal->reset != nullptr) {
gpio_descriptor_release(internal->reset);
}
if (internal->busy != nullptr) {
gpio_descriptor_release(internal->busy);
}
if (internal->panel_mutex != nullptr) {
vSemaphoreDelete(internal->panel_mutex);
}
free(internal->shadow_framebuffer);
free(internal->region_buffer);
free(internal);
}
static error_t start(Device* device) {
auto* parent = device_get_parent(device);
check(device_get_type(parent) == &SPI_CONTROLLER_TYPE);
const auto* spi_config = static_cast<const Esp32SpiConfig*>(parent->config);
const auto* config = GET_CONFIG(device);
struct GpioPinSpec cs_pin;
if (esp32_spi_get_cs_pin(device, &cs_pin) != ERROR_NONE) {
LOG_E(TAG, "Failed to resolve CS pin");
return ERROR_RESOURCE;
}
auto* internal = static_cast<Gdeq031t10Internal*>(calloc(1, sizeof(Gdeq031t10Internal)));
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
internal->dc = gpio_descriptor_acquire(
config->pin_dc.gpio_controller,
config->pin_dc.pin,
config->pin_dc.flags | GPIO_FLAG_DIRECTION_OUTPUT,
GPIO_OWNER_GPIO
);
if (config->pin_reset.gpio_controller != nullptr) {
internal->reset = gpio_descriptor_acquire(
config->pin_reset.gpio_controller,
config->pin_reset.pin,
config->pin_reset.flags | GPIO_FLAG_DIRECTION_OUTPUT,
GPIO_OWNER_GPIO
);
} else {
internal->reset = nullptr;
}
internal->busy = gpio_descriptor_acquire(
config->pin_busy.gpio_controller,
config->pin_busy.pin,
config->pin_busy.flags | GPIO_FLAG_DIRECTION_INPUT | GPIO_FLAG_ACTIVE_LOW,
GPIO_OWNER_GPIO
);
internal->panel_mutex = xSemaphoreCreateMutex();
internal->shadow_framebuffer = static_cast<uint8_t*>(malloc(FRAMEBUFFER_SIZE));
internal->region_buffer = static_cast<uint8_t*>(malloc(FRAMEBUFFER_SIZE));
if (
internal->dc == nullptr ||
internal->busy == nullptr ||
internal->panel_mutex == nullptr ||
internal->shadow_framebuffer == nullptr ||
internal->region_buffer == nullptr
// Note: reset pin is not checked as it is optional
) {
LOG_E(TAG, "Failed to acquire GPIO pins or allocate buffers");
free_internal(internal);
return ERROR_OUT_OF_MEMORY;
}
gpio_descriptor_set_flags(internal->dc, GPIO_FLAG_DIRECTION_OUTPUT);
gpio_descriptor_set_flags(internal->busy, GPIO_FLAG_DIRECTION_INPUT);
if (internal->reset != nullptr) {
gpio_descriptor_set_flags(internal->reset, GPIO_FLAG_DIRECTION_OUTPUT);
}
spi_device_interface_config_t device_config = {};
device_config.mode = 0;
device_config.clock_speed_hz = config->clock_speed_hz;
device_config.spics_io_num = cs_pin.gpio_controller == nullptr ? -1 : static_cast<int>(cs_pin.pin);
device_config.queue_size = 1;
if (spi_bus_add_device(spi_config->host, &device_config, &internal->spi_device) != ESP_OK) {
LOG_E(TAG, "Failed to add SPI device");
free_internal(internal);
return ERROR_RESOURCE;
}
// 0xFF matches an all-white render (bit 1 = white), consistent with the panel's blank
// power-up state and with the forced first full refresh below.
std::memset(internal->shadow_framebuffer, 0xFF, FRAMEBUFFER_SIZE);
if (!init_full(internal, config->mirror_180)) {
free_internal(internal);
return ERROR_RESOURCE;
}
internal->display_on = true;
internal->force_full_refresh = true;
device_set_driver_data(device, internal);
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<Gdeq031t10Internal*>(device_get_driver_data(device));
xSemaphoreTake(internal->panel_mutex, portMAX_DELAY);
if (internal->display_on) {
// Same sequence as disp_on_off(false): leave the panel in deep sleep.
if (internal->panel_power_on) {
panel_power_off(internal);
}
delay_millis(100);
write_command(internal, CMD_DEEP_SLEEP);
write_data_byte(internal, DEEP_SLEEP_CHECK_CODE);
internal->display_on = false;
}
xSemaphoreGive(internal->panel_mutex);
free_internal(internal);
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
// endregion
Driver gdeq031t10_driver = {
.name = "gdeq031t10",
.compatible = (const char*[]) { "gooddisplay,gdeq031t10", nullptr },
.start_device = start,
.stop_device = stop,
.api = &gdeq031t10_display_api,
.device_type = &DISPLAY_TYPE,
.owner = &gdeq031t10_module,
.internal = nullptr
};
}