mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-10-22 15:21:30 +00:00
Both P4 SKUs render at half resolution and nearest-neighbour-upscale each LVGL band into a scratch buffer before handing it to the panel. That scratch was allocated with MALLOC_CAP_SPIRAM, which put it on the worst possible bus. These are DPI/DSI panels with num_fbs = 1: the ~1.4 MB framebuffer can only live in PSRAM, and the DSI DMA streams it to the glass CONTINUOUSLY. Every draw_bitmap is a copy INTO that framebuffer, so with the scratch also in PSRAM a single flush put three streams on one bus at once — the scratch read, the framebuffer write, and the DSI's own read. Anything else wanting PSRAM in that window can starve the DSI read, and a DPI underrun presents as a whole-screen colour flash for one frame. That is the reported "screen flashes teal on every RX" (#167 flicker): it happens on ANY screen and was there from the start of the port, because it is a display-bus problem and has nothing to do with what is being drawn. The RX correlation is the priority-10 "lora_rx" drain task, which wakes on every received packet and is ON by default (rx_queue defaults to 1). Allocating the scratch in internal DMA RAM removes the scratch read from the PSRAM bus entirely. The cost is bounded and small: one LVGL band is LV_DRAW_BUF_LINES = 24 rows, so AMOLED 284x24 -> 568*48*2 = ~53 KB and LCD 270x24 -> 540*48*2 = ~51 KB, against a 348 KiB internal region. PSRAM stays as the fallback so a fragmented heap degrades to the previous behaviour rather than dropping to the unscaled draw path. Not done here: lowering the drain task's priority. It is created in the vendored core (RadioLibWrappers.cpp), which is not tracked in this repo, so a change there cannot ship from wadamesh — it needs a monorepo core change plus a core-* tag bump. It is also the weaker lever: the core pins that task to core 0 while LVGL and loop run on core 1, so it is not preempting the UI thread, and the contention this fixes is the memory bus. Builds clean and boot-verified on the device (T-Display P4, MAC ...e1:c2:a7); no allocation failures, only rst:0x1 (POWERON). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
237 lines
12 KiB
C++
237 lines
12 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
//
|
|
// HI8561 TFT-LCD DisplayDriver for the LilyGo T-Display P4 (LCD SKU). Mirrors RM69A10Display.cpp
|
|
// almost line-for-line — the ESP32-P4 MIPI-DSI bring-up is identical; only the vendor panel driver,
|
|
// the 540x1168 resolution and the DPI timing differ. The vendor panel driver lives in the vendored
|
|
// esp_lcd_hi8561.{h,cpp} (Espressif's Apache-2.0 esp_lcd_hi8561, from Waveshare-ESP32-components,
|
|
// unmodified except the .c->.cpp rename for our variants/ glob). The 540x1168 @ 48 MHz DPI timing is
|
|
// the vendor's HI8561_540_1168_PANEL_60HZ_DPI_CONFIG macro (hand-inlined here so num_fbs / pixel
|
|
// format stay explicit and this file diffs cleanly against RM69A10Display.cpp). The DSI-PHY LDO is
|
|
// kept at the tested-on-the-board 1.83 V (a deprecated LilyGo P4 bin notes "changed MIPI voltage
|
|
// domain to 1.8V" — same rail for both panels), NOT the 2.5 V a generic HI8561 eval board uses.
|
|
#if defined(HAS_TDISPLAY_P4)
|
|
#include "HI8561Display.h"
|
|
#include <Arduino.h>
|
|
#include "esp_heap_caps.h"
|
|
#include "esp_ldo_regulator.h"
|
|
#include "Xl9535.h" // SCREEN_RST lives on the expander (reset_gpio_num = -1)
|
|
#include "esp_lcd_mipi_dsi.h"
|
|
#include "esp_lcd_panel_io.h" // esp_lcd_panel_io_tx_param (runtime brightness 0x51)
|
|
#include "esp_lcd_panel_ops.h"
|
|
#include "esp_lcd_hi8561.h"
|
|
#include "driver/ledc.h" // GPIO 51 PWM backlight — the TFT-LCD has a real LED backlight
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/semphr.h"
|
|
|
|
// The HI8561 TFT-LCD backlight is a hardware LED driven by PWM on GPIO 51 (LilyGo's
|
|
// hi8561::kScreenBacklight). Unlike the AMOLED (RM69A10, self-emissive, brightness via DCS 0x51),
|
|
// the LCD panel renders but stays dark until this PWM is driven — the "no backlight, works under a
|
|
// flashlight" report on the P4 LCD. Set up LEDC and drive it from setBrightness().
|
|
static constexpr int kBacklightGpio = 51;
|
|
static constexpr ledc_timer_t kBlTimer = LEDC_TIMER_0;
|
|
static constexpr ledc_channel_t kBlChan = LEDC_CHANNEL_0;
|
|
static bool s_hi8561_bl_ready = false;
|
|
|
|
// The DPI panel copies each draw_bitmap band into the internal frame buffer ASYNCHRONOUSLY
|
|
// (DMA2D) and fires on_color_trans_done when the copy is complete. writePixelsRGB565 reuses a
|
|
// single upscale scratch buffer (and LVGL reuses its draw buffer) on the very next flush, so
|
|
// without waiting for that copy the next band overwrites the source mid-transfer — corrupting
|
|
// the frame buffer as horizontal black/garbage bands. This binary semaphore makes each flush
|
|
// synchronous: draw_bitmap -> wait for done. (Same contract as RM69A10Display.)
|
|
static SemaphoreHandle_t s_flush_sem = nullptr;
|
|
static bool IRAM_ATTR hi8561TransDone(esp_lcd_panel_handle_t, esp_lcd_dpi_panel_event_data_t*, void*) {
|
|
BaseType_t hpw = pdFALSE;
|
|
if (s_flush_sem) xSemaphoreGiveFromISR(s_flush_sem, &hpw);
|
|
return hpw == pdTRUE;
|
|
}
|
|
|
|
// HI8561 TFT-LCD, 540x1168 portrait. DPI timing = vendor HI8561_540_1168_PANEL_60HZ_DPI_CONFIG.
|
|
#define HI_W 540
|
|
#define HI_H 1168
|
|
// UI scale: the panel is high-DPI, so LVGL renders at 1/HI_UI_SCALE and writePixelsRGB565
|
|
// nearest-neighbour-upscales each flush to fill the native panel — whole UI HI_UI_SCALE x bigger,
|
|
// uniformly. UITask's HAS_TDP4_LCD LVGL res + the HI8561-touch scale must match (HI_W/HI_UI_SCALE).
|
|
#ifndef HI_UI_SCALE
|
|
#define HI_UI_SCALE 2
|
|
#endif
|
|
#define HI_DPI_MHZ 48 // vendor HI8561_540_1168 DPI clock
|
|
#define HI_LANES 2
|
|
#define HI_BITRATE 1000 // Mbps per lane (vendor HI8561_PANEL_BUS_DSI_2CH_CONFIG)
|
|
#define HI_HSYNC 20 // hsync_pulse_width
|
|
#define HI_HBP 40 // hsync_back_porch
|
|
#define HI_HFP 20 // hsync_front_porch
|
|
#define HI_VSYNC 2 // vsync_pulse_width
|
|
#define HI_VBP 12 // vsync_back_porch
|
|
#define HI_VFP 200 // vsync_front_porch
|
|
// ESP32-P4 MIPI-DSI PHY internal LDO: channel 3 @ 1.83V — the tested LilyGo T-Display P4 value
|
|
// (same rail the RM69A10 SKU uses; the board's MIPI voltage domain is 1.8V, per LilyGo).
|
|
#define DSI_LDO_CHAN 3
|
|
#define DSI_LDO_MV 1830
|
|
|
|
static esp_ldo_channel_handle_t s_ldo = nullptr;
|
|
|
|
bool HI8561Display::begin() {
|
|
// 1. Power the DSI PHY via the P4 internal LDO (BEFORE creating the DSI bus).
|
|
esp_ldo_channel_config_t ldo_cfg = { .chan_id = DSI_LDO_CHAN, .voltage_mv = DSI_LDO_MV };
|
|
if (esp_ldo_acquire_channel(&ldo_cfg, &s_ldo) != ESP_OK) {
|
|
Serial.println("[HI8561] LDO acquire fail"); return false;
|
|
}
|
|
delay(100);
|
|
|
|
// 1b. Panel hardware reset via the XL9535 (reset_gpio_num = -1 so esp_lcd won't do it). Same
|
|
// HIGH -> LOW -> HIGH / 200 ms order as the RM69A10 SKU, done AFTER the LDO and BEFORE the DSI bus.
|
|
xl9535.write(Xl9535::IO_SCREEN_RST, true); delay(200);
|
|
xl9535.write(Xl9535::IO_SCREEN_RST, false); delay(200);
|
|
xl9535.write(Xl9535::IO_SCREEN_RST, true); delay(200);
|
|
|
|
// 2. DSI bus (initialises the DSI PHY).
|
|
esp_lcd_dsi_bus_handle_t dsi_bus = nullptr;
|
|
esp_lcd_dsi_bus_config_t bus_cfg = {
|
|
.bus_id = 0, .num_data_lanes = HI_LANES,
|
|
.phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT, .lane_bit_rate_mbps = HI_BITRATE,
|
|
};
|
|
if (esp_lcd_new_dsi_bus(&bus_cfg, &dsi_bus) != ESP_OK) { Serial.println("[HI8561] dsi_bus fail"); return false; }
|
|
|
|
// 3. DBI IO (LCD command channel).
|
|
esp_lcd_panel_io_handle_t dbi_io = nullptr;
|
|
esp_lcd_dbi_io_config_t dbi_cfg = { .virtual_channel = 0, .lcd_cmd_bits = 8, .lcd_param_bits = 8 };
|
|
if (esp_lcd_new_panel_io_dbi(dsi_bus, &dbi_cfg, &dbi_io) != ESP_OK) { Serial.println("[HI8561] dbi_io fail"); return false; }
|
|
|
|
// 4. DPI (video) config — vendor HI8561_540_1168_PANEL_60HZ_DPI_CONFIG values, inlined.
|
|
esp_lcd_dpi_panel_config_t dpi_cfg = {
|
|
.virtual_channel = 0,
|
|
.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT,
|
|
.dpi_clock_freq_mhz = HI_DPI_MHZ,
|
|
.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565,
|
|
.num_fbs = 1,
|
|
.video_timing = {
|
|
.h_size = HI_W, .v_size = HI_H,
|
|
.hsync_pulse_width = HI_HSYNC, .hsync_back_porch = HI_HBP, .hsync_front_porch = HI_HFP,
|
|
.vsync_pulse_width = HI_VSYNC, .vsync_back_porch = HI_VBP, .vsync_front_porch = HI_VFP,
|
|
},
|
|
.flags = { .use_dma2d = true },
|
|
};
|
|
|
|
// 5. HI8561 vendor panel (reset via the XL9535 -> reset_gpio_num=-1; use the driver's default init).
|
|
hi8561_vendor_config_t vendor = {
|
|
.init_cmds = nullptr, .init_cmds_size = 0,
|
|
.mipi_config = { .dsi_bus = dsi_bus, .dpi_config = &dpi_cfg },
|
|
};
|
|
esp_lcd_panel_dev_config_t dev = {
|
|
.reset_gpio_num = -1,
|
|
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
|
|
.bits_per_pixel = 16,
|
|
.vendor_config = &vendor,
|
|
};
|
|
if (esp_lcd_new_panel_hi8561(dbi_io, &dev, &_panel) != ESP_OK) { Serial.println("[HI8561] new_panel fail"); return false; }
|
|
|
|
esp_lcd_panel_reset(_panel);
|
|
if (esp_lcd_panel_init(_panel) != ESP_OK) { Serial.println("[HI8561] panel_init fail"); return false; }
|
|
esp_lcd_panel_disp_on_off(_panel, true);
|
|
|
|
// Backlight: bring up the LEDC PWM on GPIO 51 (the LCD's real LED backlight). Without this the
|
|
// panel renders but is dark. 20 kHz avoids audible/visible flicker; 8-bit duty maps 0..255 directly.
|
|
{
|
|
ledc_timer_config_t bl_tmr = {};
|
|
bl_tmr.speed_mode = LEDC_LOW_SPEED_MODE;
|
|
bl_tmr.timer_num = kBlTimer;
|
|
bl_tmr.duty_resolution = LEDC_TIMER_8_BIT;
|
|
bl_tmr.freq_hz = 20000;
|
|
bl_tmr.clk_cfg = LEDC_AUTO_CLK;
|
|
ledc_channel_config_t bl_ch = {};
|
|
bl_ch.gpio_num = kBacklightGpio;
|
|
bl_ch.speed_mode = LEDC_LOW_SPEED_MODE;
|
|
bl_ch.channel = kBlChan;
|
|
bl_ch.timer_sel = kBlTimer;
|
|
bl_ch.duty = 0;
|
|
bl_ch.hpoint = 0;
|
|
s_hi8561_bl_ready = (ledc_timer_config(&bl_tmr) == ESP_OK) &&
|
|
(ledc_channel_config(&bl_ch) == ESP_OK);
|
|
if (!s_hi8561_bl_ready) Serial.println("[HI8561] backlight PWM init FAILED");
|
|
}
|
|
// Keep the DBI IO handle so setBrightness() can also send DCS 0x51 (harmless if the panel
|
|
// ignores it); the real light is the PWM above. Start at max so the LCD actually lights.
|
|
_dbi_io = dbi_io;
|
|
setBrightness(255);
|
|
_on = true;
|
|
|
|
// Signal each draw_bitmap's frame-buffer copy completion so writePixelsRGB565 can wait for it.
|
|
s_flush_sem = xSemaphoreCreateBinary();
|
|
esp_lcd_dpi_panel_event_callbacks_t cbs = {};
|
|
cbs.on_color_trans_done = hi8561TransDone;
|
|
esp_lcd_dpi_panel_register_event_callbacks(_panel, &cbs, nullptr);
|
|
|
|
// Clear the panel framebuffer to black once, so nothing stale shows before LVGL's first flush.
|
|
{
|
|
const int SH = 32;
|
|
uint16_t* strip = (uint16_t*)heap_caps_calloc((size_t)HI_W * SH, 2, MALLOC_CAP_SPIRAM);
|
|
if (strip) {
|
|
for (int y0 = 0; y0 < HI_H; y0 += SH) {
|
|
int hh = (y0 + SH <= HI_H) ? SH : (HI_H - y0);
|
|
esp_lcd_panel_draw_bitmap(_panel, 0, y0, HI_W, y0 + hh, strip);
|
|
if (s_flush_sem) xSemaphoreTake(s_flush_sem, pdMS_TO_TICKS(100)); // let the copy finish before reusing/freeing strip
|
|
}
|
|
free(strip);
|
|
}
|
|
}
|
|
|
|
Serial.printf("[HI8561] up %dx%d\n", HI_W, HI_H);
|
|
return true;
|
|
}
|
|
|
|
void HI8561Display::writePixelsRGB565(int x, int y, int w, int h, const uint16_t* pixels) {
|
|
if (!_panel || !pixels || w <= 0 || h <= 0) return;
|
|
#if HI_UI_SCALE > 1
|
|
// Nearest-neighbour upscale the (half-res) LVGL band to the native panel: each source pixel becomes
|
|
// an HI_UI_SCALE x HI_UI_SCALE block. One expanded band -> one draw_bitmap (exclusive end coords).
|
|
const int S = HI_UI_SCALE, dw = w * S, dh = h * S;
|
|
// INTERNAL DMA RAM, not PSRAM — same reasoning as the AMOLED SKU (long note in
|
|
// RM69A10Display::writePixelsRGB565). This is a DPI/DSI panel whose single framebuffer must
|
|
// live in PSRAM and is streamed to the glass continuously, so keeping the upscale scratch in
|
|
// PSRAM too put three streams on one bus: the scratch read, the framebuffer write, and the
|
|
// DSI's own read. Any other PSRAM consumer — the priority-10 "lora_rx" drain task fires on
|
|
// every received packet — could then starve the DSI, and a DPI underrun shows as a
|
|
// whole-screen colour flash for one frame (#167). One band is 270x24 upscaled 2x =
|
|
// 540*48*2 = ~51 KB out of 768 KB SRAM. PSRAM stays the fallback.
|
|
static uint16_t* s_up = nullptr; static size_t s_up_px = 0;
|
|
size_t need = (size_t)dw * dh;
|
|
if (need > s_up_px) {
|
|
if (s_up) heap_caps_free(s_up);
|
|
s_up = (uint16_t*)heap_caps_malloc(need * sizeof(uint16_t),
|
|
MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
|
|
if (!s_up) s_up = (uint16_t*)heap_caps_malloc(need * sizeof(uint16_t), MALLOC_CAP_SPIRAM);
|
|
s_up_px = s_up ? need : 0;
|
|
}
|
|
if (s_up) {
|
|
for (int row = 0; row < h; row++) {
|
|
const uint16_t* src = pixels + (size_t)row * w;
|
|
uint16_t* d0 = s_up + (size_t)(row * S) * dw; // first of S output rows
|
|
for (int i = 0; i < w; i++) { uint16_t c = src[i]; uint16_t* p = d0 + i * S; for (int sx = 0; sx < S; sx++) p[sx] = c; }
|
|
for (int sy = 1; sy < S; sy++) memcpy(d0 + (size_t)sy * dw, d0, (size_t)dw * sizeof(uint16_t));
|
|
}
|
|
esp_lcd_panel_draw_bitmap(_panel, x * S, y * S, x * S + dw, y * S + dh, s_up);
|
|
if (s_flush_sem) xSemaphoreTake(s_flush_sem, pdMS_TO_TICKS(100)); // wait for the FB copy before the next flush reuses s_up
|
|
return;
|
|
}
|
|
// fall through to an unscaled draw if the scratch alloc failed (tiny UI, but visible)
|
|
#endif
|
|
// exclusive end coords (esp_lcd_panel_draw_bitmap contract).
|
|
esp_lcd_panel_draw_bitmap(_panel, x, y, x + w, y + h, pixels);
|
|
if (s_flush_sem) xSemaphoreTake(s_flush_sem, pdMS_TO_TICKS(100)); // wait for the FB copy before LVGL reuses this buffer
|
|
}
|
|
|
|
void HI8561Display::setBrightness(uint8_t b) {
|
|
// The real backlight is the LEDC PWM on GPIO 51 (drives the LCD's LED); the DCS 0x51 is a
|
|
// best-effort no-op the AMOLED path used. Drive both so the CC slider dims the LCD.
|
|
if (s_hi8561_bl_ready) {
|
|
ledc_set_duty(LEDC_LOW_SPEED_MODE, kBlChan, b);
|
|
ledc_update_duty(LEDC_LOW_SPEED_MODE, kBlChan);
|
|
}
|
|
if (_dbi_io) esp_lcd_panel_io_tx_param(_dbi_io, 0x51, &b, 1); // DCS SET_DISPLAY_BRIGHTNESS
|
|
}
|
|
|
|
void HI8561Display::turnOn() { if (_panel) esp_lcd_panel_disp_on_off(_panel, true); _on = true; }
|
|
void HI8561Display::turnOff() { if (_panel) esp_lcd_panel_disp_on_off(_panel, false); _on = false; }
|
|
|
|
#endif // HAS_TDISPLAY_P4
|