Files
MeshCore-Solo/variants/wio-tracker-l2/WioTrackerL2Display.h
T
JakubandClaude Opus 5.5 0a07dc3deb perf(l2): screen sent by DMA while the next part is drawn
The panel takes each 24-line band by DMA while LVGL draws the next one
into the other buffer (two 15 KB buffers in internal RAM; LovyanGFX's
4092-byte DMA descriptors leave PSRAM unaligned for the S3's DMA, which
showed as shifted and smeared bands). LVGL draws in the panel's byte
order, and so do map tiles and the frozen screen under a popup, so
nothing is swapped on the way out. Full frames ~10-25% faster (page1
19.9 -> 14.7 ms, Home 21.4 -> 18.2 ms). Without the internal RAM the
screen is sent as before. The arduino2 env keeps the old way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 20:56:13 +02:00

239 lines
7.7 KiB
C++

#pragma once
#include <helpers/ui/LGFXDisplay.h>
#include <Wire.h>
#define LGFX_USE_V1
#include <LovyanGFX.hpp>
// ---------------------------------------------------------------------------
// Wio Tracker L2 Pro display stack:
// - NV3031B 320x240 panel on quad-SPI (SCLK 42, IO0-3 = 41/40/39/38, CS 46)
// - GT911 capacitive touch on I2C 0x5D (SDA 47 / SCL 48)
// - LP5814 4-channel LED driver on I2C 0x2C used as backlight
// Panel power/reset lines are on the TCA9535 expander and are already
// sequenced by WioTrackerL2Board::begin() before this driver initializes.
// ---------------------------------------------------------------------------
// The SPI clock is 80 MHz / an integer: 75 MHz asked gives 40 MHz on the wire.
#ifndef L2_SPI_FREQUENCY
#define L2_SPI_FREQUENCY 75000000
#endif
#define LP5814_I2C_ADDR 0x2C
// LP5814 used as backlight controller (all 4 channels in parallel)
class WioTrackerL2Backlight : public lgfx::v1::ILight {
static constexpr uint8_t REG_DEVICE_CONFIG0 = 0x00;
static constexpr uint8_t REG_MAX_CURRENT = 0x01;
static constexpr uint8_t REG_ENABLE_CONTROL = 0x02;
static constexpr uint8_t REG_DIM_MODE = 0x04;
static constexpr uint8_t REG_ENGINE_MODE = 0x05;
static constexpr uint8_t REG_UPDATE = 0x0F;
static constexpr uint8_t REG_LED0_DC = 0x14;
static constexpr uint8_t REG_LED0_PWM = 0x18;
uint8_t _brightness = 153; // 60%
bool _standby = false; // chip disabled (the screen is off)
void writeReg(uint8_t reg, uint8_t value) {
Wire.beginTransmission(LP5814_I2C_ADDR);
Wire.write(reg);
Wire.write(value);
Wire.endTransmission();
}
void configure() {
writeReg(REG_DEVICE_CONFIG0, 0x01); // chip enable
writeReg(REG_MAX_CURRENT, 0x01); // 51 mA max current
writeReg(REG_ENABLE_CONTROL, 0x00); // outputs off while configuring
writeReg(REG_DIM_MODE, 0x4E);
writeReg(REG_ENGINE_MODE, 0xF0);
for (uint8_t i = 0; i < 4; i++) {
writeReg(REG_LED0_DC + i, 200);
}
writeReg(REG_ENABLE_CONTROL, 0x0F); // enable all 4 channels
writeReg(REG_UPDATE, 0x55); // latch (LP5814 requires 0x55)
delay(5);
}
public:
bool init(uint8_t brightness) override {
Wire.beginTransmission(LP5814_I2C_ADDR);
if (Wire.endTransmission() != 0) {
return false; // LP5814 not found
}
configure();
setBrightness(brightness);
return true;
}
// 0 (LovyanGFX's sleep()) puts the chip in standby; the next level brings it
// back, set up again.
void setBrightness(uint8_t brightness) override {
if (brightness == 0) {
writeReg(REG_DEVICE_CONFIG0, 0x00); // chip disable: standby
_standby = true;
return;
}
if (_standby) { configure(); _standby = false; }
for (uint8_t i = 0; i < 4; i++) {
writeReg(REG_LED0_PWM + i, brightness);
}
_brightness = brightness;
}
uint8_t getBrightness() const { return _brightness; }
virtual ~WioTrackerL2Backlight() = default;
};
// The panel, plus a write that doesn't wait: an area's pixels (already in the
// panel's byte order, at most 32 KB -- one SPI transaction) go out by DMA
// while the CPU draws the next one; flushWait() ends it. The library's own
// writes wait for the wire every time (the ui-lvgl frame's ~8 ms).
struct Panel_WioTrackerL2 : public lgfx::Panel_NV3031B {
void flushStart(int x, int y, int w, int h, const uint8_t* data, uint32_t len) {
setWindow(x, y, x + w - 1, y + h - 1);
start_qspi();
_bus->writeBytes(data, len, true, true);
}
void flushWait() {
_bus->wait();
end_qspi();
}
};
class LGFX_WioTrackerL2 : public lgfx::LGFX_Device {
Panel_WioTrackerL2 _panel_instance;
lgfx::Bus_SPI _bus_instance;
lgfx::Touch_GT911 _touch_instance;
WioTrackerL2Backlight _light_instance;
public:
Panel_WioTrackerL2& panelL2() { return _panel_instance; }
bool init_impl(bool use_reset, bool use_clear) override {
// bring up backlight controller while the I2C bus is still clean
_light_instance.init(_light_instance.getBrightness());
bool result = LGFX_Device::init_impl(use_reset, use_clear);
// GT911 probe can leave the ESP32 I2C peripheral with a stuck BUSY flag;
// cycling Wire resets it so later LP5814/sensor traffic doesn't time out
Wire.end();
Wire.begin(47, 48);
return result;
}
LGFX_WioTrackerL2(void) {
{
auto cfg = _bus_instance.config();
cfg.spi_host = SPI3_HOST;
cfg.spi_mode = 3;
cfg.freq_write = L2_SPI_FREQUENCY;
cfg.freq_read = 16000000;
cfg.pin_sclk = 42;
// quad SPI data pins
cfg.pin_io0 = 41;
cfg.pin_io1 = 40;
cfg.pin_io2 = 39;
cfg.pin_io3 = 38;
_bus_instance.config(cfg);
_panel_instance.setBus(&_bus_instance);
}
{
auto cfg = _panel_instance.config();
cfg.pin_cs = 46;
cfg.pin_rst = -1; // reset is on the IO expander, done in board init
cfg.pin_busy = -1;
cfg.panel_width = 240; // native portrait orientation
cfg.panel_height = 320;
cfg.memory_width = 240;
cfg.memory_height = 320;
cfg.offset_x = 0;
cfg.offset_y = 0;
cfg.offset_rotation = 1; // panel mounted landscape (320x240)
cfg.invert = true;
cfg.rgb_order = true;
cfg.dlen_16bit = false;
cfg.bus_shared = false;
_panel_instance.config(cfg);
}
{
auto cfg = _touch_instance.config();
cfg.pin_cs = -1;
cfg.x_min = 0;
cfg.x_max = 239;
cfg.y_min = 0;
cfg.y_max = 319;
cfg.pin_int = -1; // INT is on the IO expander
cfg.offset_rotation = 2;
cfg.i2c_port = 0;
cfg.i2c_addr = 0x5D;
cfg.pin_sda = 47;
cfg.pin_scl = 48;
cfg.bus_shared = false;
cfg.freq = 400000;
_touch_instance.config(cfg);
_panel_instance.setTouch(&_touch_instance);
}
_panel_instance.setLight(&_light_instance);
setPanel(&_panel_instance);
}
};
class WioTrackerL2Display : public LGFXDisplay {
LGFX_WioTrackerL2 disp;
public:
WioTrackerL2Display() : LGFXDisplay(320, 240, disp) {}
// NodePrefs::display_brightness level 0-4 -> backlight PWM
void setBrightness(uint8_t level) override {
static const uint8_t PWM[5] = { 20, 60, 120, 190, 255 };
display->setBrightness(PWM[level > 4 ? 4 : level]);
}
// direct access to the LGFX device (LVGL flush/touch glue)
lgfx::LGFX_Device* lgfxDevice() { return &disp; }
Panel_WioTrackerL2& panel() { return disp.panelL2(); }
// LVGL draws straight to the panel: the frame sprite (76 KB of PSRAM) only
// carried the boot "Loading..." -- given back once LVGL takes over.
void releaseFrameBuffer() { buffer.deleteSprite(); }
// reliable touch read in logical (UI_ZOOM-scaled) coords; returns false when
// not touched (LGFXDisplay::getTouch reads an uninitialized point on release)
bool readTouch(int& x, int& y) {
lgfx::touch_point_t tp;
if (disp.getTouch(&tp, 1) == 0) return false;
x = tp.x / UI_ZOOM;
y = tp.y / UI_ZOOM;
return true;
}
// shadows LGFXDisplay::begin() (non-virtual, called on the concrete type in
// main.cpp) - identical except rotation: our panel config already carries
// offset_rotation=1 for the landscape mounting, so no extra rotation here
bool begin() {
turnOn();
display->init();
display->setRotation(0);
display->setColorDepth(8);
// The zoomed sprite blit stops a pixel short of the panel edge, so those
// pixels keep whatever the panel powered up with (they showed as coloured
// lines down the right side and along the bottom). Clear the panel once.
display->fillScreen(TFT_BLACK);
display->setBrightness(153);
display->setTextColor(TFT_WHITE);
buffer.setColorDepth(8);
buffer.setPsram(true);
buffer.createSprite(width(), height());
return true;
}
};