Files
JakubandClaude Sonnet 5.5 e03fa7c084 fix(l2): no bright bar on the dark screen at power-on
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>
2026-10-25 21:33:59 +01:00

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;
}
};