touch: P4 — move the flush upscale scratch out of PSRAM (#167 teal flash on RX)

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>
This commit is contained in:
Kaj SchittecatandClaude Opus 4.8 committed 2026-07-30 15:35:09 +02:00
1 parent 471a522d8f
commit 7aaff80742
2 files changed
+27 -2

No files matched your search

+11 -1
View File
@@ -185,11 +185,21 @@ void HI8561Display::writePixelsRGB565(int x, int y, int w, int h, const uint16_t
// 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_SPIRAM);
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) {
+16 -1
View File
@@ -149,11 +149,26 @@ void RM69A10Display::writePixelsRGB565(int x, int y, int w, int h, const uint16_
// Nearest-neighbour upscale the (half-res) LVGL band to the native panel: each source pixel becomes
// an RM_UI_SCALE x RM_UI_SCALE block. One expanded band -> one draw_bitmap (exclusive end coords).
const int S = RM_UI_SCALE, dw = w * S, dh = h * S;
// The scratch band lives in INTERNAL DMA RAM, not PSRAM. This panel is a DPI/DSI one with a
// single ~1.4 MB framebuffer that can only live in PSRAM, and the DSI DMA streams that
// framebuffer out to the glass CONTINUOUSLY. Every draw_bitmap below is a copy INTO that
// framebuffer, so with the scratch also in PSRAM a flush put three streams on the same bus at
// once: the scratch read, the framebuffer write, and the DSI's own read. Anything else that
// wants PSRAM at that moment — notably the priority-10 "lora_rx" drain task, which wakes on
// every received packet — can then starve the DSI read, and a DPI underrun shows up as a
// whole-screen colour flash for one frame. That is the P4 "flicker on every RX" report
// (issue #167): it appears on any screen, because it is a display-bus problem and nothing to
// do with what is being drawn. Internal RAM removes the scratch read from the PSRAM bus.
// Cost is bounded and small: one LVGL band is 284x24 (LV_DRAW_BUF_LINES), so upscaled 2x this
// is 568*48*2 = ~53 KB out of the P4's 768 KB SRAM. PSRAM stays as the fallback so a
// fragmented heap degrades to the old behaviour instead of dropping to the unscaled path.
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_SPIRAM);
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) {