226 lines
9.2 KiB
C++
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; }
|
|
};
|