#pragma once #include #include #include #include #include #include #include #include #include #include #include /** * 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 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 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; 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 shadowFramebuffer; /** Render target for LVGL; copied to the panel on flush */ std::unique_ptr renderFramebuffer; bool initialized = false; std::atomic powered = false; std::atomic 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 forceFullRefresh = true; /** Scratch buffer used to gather a windowed region's bytes for one SPI write. */ std::unique_ptr 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 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 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 taskFramebuffer; bool framePending = false; std::atomic 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 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 _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 _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; } };