mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
The first frame (the sprite's default white background, black dots) came out as a bright bar across the middle of the screen. A plain black frame now, and the backlight stays off until it's sent: dark through the panel's reset and init (the LP5814 keeps its state across a restart), lit with the first frame. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
285 lines
9.5 KiB
C++
285 lines
9.5 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 the backlight controller while the I2C bus is still clean -- but
|
|
// dark: the panel's memory holds noise until it's initialised and cleared
|
|
// (a white bar across the screen), and a chip left lit by a restart would
|
|
// show it too. begin() switches the light on after the clear.
|
|
_light_instance.init(0);
|
|
|
|
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;
|
|
|
|
// The backlight from one PWM level to another over ~80 ms, on a square
|
|
// curve (the eye sees the low end's steps). Blocks; 0 ends in standby.
|
|
void fade(uint8_t from, uint8_t to) {
|
|
const int STEPS = 8;
|
|
for (int i = 1; i <= STEPS; i++) {
|
|
const int t = i * 256 / STEPS; // 0..256
|
|
const int k = from < to ? t * t / 256 : 256 - (256 - t) * (256 - t) / 256;
|
|
const int v = from + ((int)to - from) * k / 256;
|
|
display->panel()->setBrightness((uint8_t)(i == STEPS ? to : (v < 1 ? 1 : v)));
|
|
delay(8);
|
|
}
|
|
}
|
|
|
|
bool _lit = false; // the light has come on with the first frame
|
|
|
|
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]);
|
|
}
|
|
|
|
// Screen off: the backlight fades out, then the panel sleeps. Screen on: the
|
|
// panel wakes with the light still off -- the caller draws a fresh frame
|
|
// first (the panel shows what's left in its memory for a moment, a white
|
|
// flash) -- and fadeIn() brings the light up.
|
|
void turnOff() override {
|
|
if (_isOn) {
|
|
fade(display->getBrightness(), 0);
|
|
display->sleep();
|
|
}
|
|
_isOn = false;
|
|
}
|
|
void turnOn() override {
|
|
if (!_isOn) {
|
|
display->panel()->setSleep(false);
|
|
delay(10); // the panel takes no pixels just out of sleep
|
|
}
|
|
_isOn = true;
|
|
}
|
|
void fadeIn() { fade(0, display->getBrightness()); }
|
|
|
|
// The first frame goes out with the light still off, then the light comes on:
|
|
// lit any sooner, the panel shows what it holds in memory for as long as the
|
|
// frame takes.
|
|
void endFrame() override {
|
|
LGFXDisplay::endFrame();
|
|
if (!_lit) { _lit = true; display->setBrightness(153); }
|
|
}
|
|
|
|
// 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->setTextColor(TFT_WHITE); // the light comes on with the first frame (endFrame())
|
|
|
|
buffer.setColorDepth(8);
|
|
buffer.setPsram(true);
|
|
buffer.createSprite(width(), height());
|
|
|
|
return true;
|
|
}
|
|
};
|