Files
tactility/Drivers/GDEQ031T10/Source/Gdeq031t10Display.h
T

226 lines
9.2 KiB
C++

#pragma once
#include <Tactility/hal/display/DisplayDevice.h>
#include <Tactility/hal/touch/TouchDevice.h>
#include <atomic>
#include <driver/gpio.h>
#include <driver/spi_master.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <lvgl.h>
#include <memory>
#include <optional>
/**
* Driver for the GoodDisplay GDEQ031T10 (UC8253-family controller) 3.1" 320x240
* SPI e-paper panel, as used on the LilyGO T-Deck Pro/Max.
*
* Command sequence and timings are ported from the vendor reference driver in
* Xinyuan-LilyGO/T-Deck-MAX (examples/Elink_paper/GDEQ031T10_Arduino).
*/
class Gdeq031t10Display final : public tt::hal::display::DisplayDevice {
public:
enum class RefreshMode {
Full, // ~3s, best quality
Fast, // ~1.0s
Slow, // ~1.5s, fast LUT with extra settling
Partial // ~0.5s, full-frame partial-mode refresh (more ghosting)
};
class Configuration {
public:
Configuration(
spi_host_device_t spiHost,
gpio_num_t pinCs,
gpio_num_t pinDc,
gpio_num_t pinReset,
gpio_num_t pinBusy,
std::shared_ptr<tt::hal::touch::TouchDevice> touch = nullptr,
int clockSpeedHz = 4'000'000,
RefreshMode defaultRefreshMode = RefreshMode::Full,
bool mirror180 = false
) : spiHost(spiHost),
pinCs(pinCs),
pinDc(pinDc),
pinReset(pinReset),
pinBusy(pinBusy),
touch(std::move(touch)),
clockSpeedHz(clockSpeedHz),
defaultRefreshMode(defaultRefreshMode),
mirror180(mirror180)
{}
spi_host_device_t spiHost;
gpio_num_t pinCs;
gpio_num_t pinDc;
gpio_num_t pinReset;
gpio_num_t pinBusy;
std::shared_ptr<tt::hal::touch::TouchDevice> touch;
int clockSpeedHz;
RefreshMode defaultRefreshMode;
/** Panel is mounted upside down relative to the reference orientation */
bool mirror180;
};
static constexpr uint16_t WIDTH = 240;
static constexpr uint16_t HEIGHT = 320;
static constexpr size_t FRAMEBUFFER_SIZE = (WIDTH * HEIGHT) / 8; // 1 bpp packed
// LVGL 9 stores a 2-colour palette (2 x lv_color32_t = 8 bytes) at the start
// of an LV_COLOR_FORMAT_I1 buffer, before the packed 1bpp bitmap.
static constexpr size_t LVGL_I1_PALETTE_SIZE = 8;
private:
std::unique_ptr<Configuration> configuration;
spi_device_handle_t spiDevice = nullptr;
lv_display_t* _Nullable lvglDisplay = nullptr;
/** Mirrors what the panel currently holds, required by the controller's
* "old data" + "new data" double-buffered refresh protocol. */
std::unique_ptr<uint8_t[]> shadowFramebuffer;
/** Render target for LVGL; copied to the panel on flush */
std::unique_ptr<uint8_t[]> renderFramebuffer;
bool initialized = false;
std::atomic<bool> powered = false;
std::atomic<RefreshMode> currentRefreshMode = RefreshMode::Full;
/** Number of windowed partial refreshes since the last full refresh. A full
* refresh is forced once this reaches MAX_PARTIAL_REFRESHES to clear the
* ghosting that partial updates accumulate. */
uint8_t partialRefreshCount = 0;
/** Forces the next refresh to be a full-screen refresh (set at boot and by
* requestFullRefresh, read on the refresh task). */
std::atomic<bool> forceFullRefresh = true;
/** Scratch buffer used to gather a windowed region's bytes for one SPI write. */
std::unique_ptr<uint8_t[]> regionBuffer;
static constexpr uint8_t MAX_PARTIAL_REFRESHES = 8;
/** Union bounding box (byte-column/row space) of all windowed partial
* refreshes since the last ghost-clear. When the partial-refresh gate
* expires, this decides between a localized scrub and a full-screen
* refresh: small repeated updates (a text cursor, a typing line) shouldn't
* flash the whole panel. */
bool ghostAreaValid = false;
int ghostFirstCol = 0;
int ghostLastCol = 0;
int ghostFirstRow = 0;
int ghostLastRow = 0;
/** Waveform mode the panel registers currently hold. Empty when the
* registers are in an unknown state (before first init, or after deep sleep,
* which requires a reset that restores defaults). */
std::optional<RefreshMode> panelMode;
/** True while the panel's charge pump is on (CMD_POWER_ON issued, no
* power-off since). */
bool panelPowerOn = false;
/** Serializes panel/SPI access between the refresh task and external
* callers (setPowerOn, stop). */
SemaphoreHandle_t panelMutex = nullptr;
// The physical refresh takes hundreds of ms to over a second of ink
// movement. Running it inside LVGL's flush callback would block the LVGL
// task (and with it all input handling) for that long, so flushes only
// stage the frame and a dedicated task drives the panel. Frames coalesce
// latest-wins: however many flushes arrive during a refresh, only the
// newest staged frame is drawn next.
static constexpr uint32_t REFRESH_TASK_PRIORITY = 3; // below LVGL (6), above idle
static constexpr uint32_t REFRESH_TASK_STACK_SIZE = 4096;
TaskHandle_t refreshTask = nullptr;
/** Signalled by the refresh task right before it exits; stop() joins on it. */
SemaphoreHandle_t refreshTaskExited = nullptr;
/** Guards pendingFramebuffer and framePending (LVGL flush vs refresh task). */
SemaphoreHandle_t bufferMutex = nullptr;
/** Latest complete frame staged by the LVGL flush callback. */
std::unique_ptr<uint8_t[]> pendingFramebuffer;
/** The refresh task's working copy of the frame it is drawing (swapped with
* pendingFramebuffer under bufferMutex; no copy on the task side). */
std::unique_ptr<uint8_t[]> taskFramebuffer;
bool framePending = false;
std::atomic<bool> refreshTaskShouldExit = false;
/** Returns false if the SPI transfer failed; the panel/shadow state should
* not be advanced as if it succeeded. */
bool writeCommand(uint8_t command);
bool writeData(const uint8_t* data, size_t length);
bool writeData(uint8_t data) { return writeData(&data, 1); }
/** Waits for the BUSY pin to report ready, up to a fixed timeout. Returns
* false (and logs) if the panel never became ready, e.g. bad wiring or no
* panel attached — callers must not assume the operation completed. */
bool waitWhileBusy() const;
void reset() const;
bool initFull();
bool initFast();
bool initSlow();
bool initPartial();
/** Puts the panel in the given waveform mode with the charge pump on,
* skipping the (expensive) init sequence when it already is. */
bool ensurePanelReady(RefreshMode mode);
bool powerOff();
/** Stages the LVGL-rendered frame for the refresh task (called on flush). */
void queueRefresh();
static void refreshTaskMain(void* parameter);
void runRefreshTask();
void refresh();
void refreshFull(RefreshMode mode);
/** Windowed partial refresh. With scrubGhosts the controller is fed "old"
* data that is the complement of the new frame, so it drives every pixel in
* the window through a transition — a localized ghost-clear that only
* flashes the window itself. */
void refreshWindow(int firstByteCol, int lastByteCol, int firstRow, int lastRow, bool scrubGhosts);
static void flushCallback(lv_display_t* display, const lv_area_t* area, uint8_t* pixelMap);
/** Theme hook: disables the textarea cursor blink (see startLvgl). */
static void themeApplyCallback(lv_theme_t* theme, lv_obj_t* obj);
public:
explicit Gdeq031t10Display(std::unique_ptr<Configuration> inConfiguration) : configuration(std::move(inConfiguration)) {
assert(configuration != nullptr);
}
~Gdeq031t10Display() override {
if (panelMutex != nullptr) {
vSemaphoreDelete(panelMutex);
}
if (bufferMutex != nullptr) {
vSemaphoreDelete(bufferMutex);
}
if (refreshTaskExited != nullptr) {
vSemaphoreDelete(refreshTaskExited);
}
}
std::string getName() const override { return "GDEQ031T10"; }
std::string getDescription() const override { return "GoodDisplay GDEQ031T10 e-paper display"; }
bool start() override;
bool stop() override;
void setPowerOn(bool turnOn) override;
bool isPoweredOn() const override { return powered; }
bool supportsPowerControl() const override { return true; }
void requestFullRefresh() override;
std::shared_ptr<tt::hal::touch::TouchDevice> _Nullable getTouchDevice() override {
return configuration->touch;
}
bool supportsLvgl() const override { return true; }
bool startLvgl() override;
bool stopLvgl() override;
lv_display_t* _Nullable getLvglDisplay() const override { return lvglDisplay; }
bool supportsDisplayDriver() const override { return false; }
std::shared_ptr<tt::hal::display::DisplayDriver> _Nullable getDisplayDriver() override { return nullptr; }
/** Sets the refresh mode used for automatic (non-full) refreshes from now on,
* until changed again by a subsequent call. */
void setRefreshMode(RefreshMode mode) { currentRefreshMode = mode; }
};