The artefacts went from occasional to continuous, and the cause was my own
mitigation for them.
lv_obj_set_local_style_prop() does NOT skip identical values -- it calls
lv_obj_refresh_style() unconditionally, which invalidates the object. A
comment in ui_set_stale() claimed the opposite and that assumption spread.
ui_update_thermostat() is called once a second by the control task and was
writing seven style properties every time, so the panel was redrawing
something forever, and glitched lines were being regenerated as fast as
anything could clear them.
Two full-screen invalidates made it worse still: one after every HA poll,
one 600ms after every touch. The premise was that LVGL never reinvalidates
pixels it does not know are wrong, so repainting would clean them up. But
the README already had the answer -- the corruption scales with how much is
written, because LVGL's PSRAM writes compete with the panel's DMA reads out
of the same PSRAM. A full repaint is 768KB of exactly the contention that
causes the problem. Both are removed.
What is left is the thing that actually helps: write less.
- ui_update_thermostat() returns without taking the lock when nothing
moved, and otherwise touches only the widgets whose values changed.
- ui_set_stale() is guarded on the previous value.
- Condition and trend icons only get a colour write when the glyph itself
changed.
Also raises the bounce buffer from 16 to 20 scanlines. The comment saying
20 "fails to allocate once WiFi is up" was true when display_init() ran
after wifi_init_sta(); it does not any more. display_alloc() takes the
buffers from a virgin heap with ~200KB free, so the extra 12.8KB is
affordable, and each larger chunk buys the refill ISR more time to fetch the
next one before the LCD needs it. That attacks the mechanism rather than the
symptom. ~24KB internal heap remains once WiFi is up; the "Free internal
heap" log line is the thing to watch if it needs to come back down.
Both gotchas are now in the README so the next person does not reach for a
repaint again.
Co-Authored-By: Claude <noreply@anthropic.com>
268 lines
10 KiB
C
268 lines
10 KiB
C
/*
|
|
* RGB parallel panel driver for the Elecrow CrowPanel 7" ADVANCE (800x480).
|
|
*
|
|
* NOTE: the Advance and the Basic CrowPanel 7" share almost no pins. Every
|
|
* data line, every sync line, the pixel clock, the porches and the sync
|
|
* polarities differ, and the Advance drives its backlight over I2C rather
|
|
* than from a GPIO. Configuration below is the Advance; do not mix the two.
|
|
*
|
|
* Reference: Elecrow Advance-series wiki and the LovyanGFX board config in
|
|
* the FluidTouch project (elecrow-crowpanel-7-advance-v13).
|
|
*/
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "driver/gpio.h"
|
|
#include "esp_heap_caps.h"
|
|
#include "esp_check.h"
|
|
#include "esp_lcd_panel_ops.h"
|
|
#include "esp_lcd_panel_rgb.h"
|
|
#include "esp_log.h"
|
|
#include "lvgl.h"
|
|
|
|
#include "display_rgb.h"
|
|
|
|
static const char *TAG = "display_rgb";
|
|
|
|
/* --- Sync / control pins (Advance) --- */
|
|
#define RGB_DE_GPIO 42
|
|
#define RGB_VSYNC_GPIO 41
|
|
#define RGB_HSYNC_GPIO 40
|
|
#define RGB_PCLK_GPIO 39
|
|
|
|
/*
|
|
* --- Data pins (Advance), 16-bit RGB565 ---
|
|
* data_gpio_nums[0..4] = B0-B4, [5..10] = G0-G5, [11..15] = R0-R4.
|
|
*/
|
|
#define RGB_B0_GPIO 21
|
|
#define RGB_B1_GPIO 47
|
|
#define RGB_B2_GPIO 48
|
|
#define RGB_B3_GPIO 45
|
|
#define RGB_B4_GPIO 38
|
|
|
|
#define RGB_G0_GPIO 9
|
|
#define RGB_G1_GPIO 10
|
|
#define RGB_G2_GPIO 11
|
|
#define RGB_G3_GPIO 12
|
|
#define RGB_G4_GPIO 13
|
|
#define RGB_G5_GPIO 14
|
|
|
|
#define RGB_R0_GPIO 7
|
|
#define RGB_R1_GPIO 17
|
|
#define RGB_R2_GPIO 18
|
|
#define RGB_R3_GPIO 3
|
|
#define RGB_R4_GPIO 46
|
|
|
|
/*
|
|
* --- Panel timings (Advance) ---
|
|
*
|
|
* 18MHz is not tunable in practice. With htotal 820 and vtotal 500 it gives
|
|
* ~44 Hz, which this panel locks to; dropping to 14MHz yields ~34 Hz and the
|
|
* image rolls vertically because the panel cannot lock that slow. Bandwidth
|
|
* is dealt with by the bounce buffer instead.
|
|
*/
|
|
#define RGB_PCLK_HZ (18 * 1000 * 1000)
|
|
#define RGB_HSYNC_PULSE_WIDTH 4
|
|
#define RGB_HSYNC_BACK_PORCH 8
|
|
#define RGB_HSYNC_FRONT_PORCH 8
|
|
#define RGB_VSYNC_PULSE_WIDTH 4
|
|
#define RGB_VSYNC_BACK_PORCH 8
|
|
#define RGB_VSYNC_FRONT_PORCH 8
|
|
|
|
/*
|
|
* Scanlines staged in internal RAM per bounce-buffer chunk (two are
|
|
* allocated, so this costs 2 * 800 * LINES * 2 bytes of internal RAM).
|
|
*
|
|
* Bigger is better here: each chunk buys the refill ISR that much more time
|
|
* to copy the next one out of PSRAM before the LCD needs it. When it runs
|
|
* late -- which WiFi interrupt activity is enough to cause -- the panel
|
|
* displays whatever was left in the buffer, showing up as short stale lines
|
|
* at the left edge where each chunk begins.
|
|
*/
|
|
/*
|
|
* 20 lines: two buffers at 800px x 2 bytes = 64KB of internal RAM.
|
|
*
|
|
* This used to be 16 because 20 "fails to allocate once WiFi is up" -- true
|
|
* when display_init() ran after wifi_init_sta(). It no longer does: the
|
|
* two-phase split means display_alloc() takes these buffers from a virgin
|
|
* heap before WiFi has touched it (~200KB free at that point), so the extra
|
|
* 12.8KB is affordable now.
|
|
*
|
|
* Bigger directly attacks the artefact mechanism: each chunk buys the refill
|
|
* ISR that much more time to fetch the next one out of PSRAM before the LCD
|
|
* needs it. Going further eats the internal RAM WiFi wants at runtime --
|
|
* watch the "Free internal heap" line, and if it drops near single digits,
|
|
* come back down.
|
|
*/
|
|
#define BOUNCE_BUFFER_LINES 20
|
|
|
|
static esp_lcd_panel_handle_t panel = NULL;
|
|
static lv_display_t *disp = NULL;
|
|
|
|
/*
|
|
* Double-buffered flush.
|
|
*
|
|
* LVGL renders into whichever framebuffer the LCD is NOT currently scanning,
|
|
* and the final flush of a refresh hands that buffer to the panel, which
|
|
* switches to it. Nothing ever writes into the buffer being displayed.
|
|
*
|
|
* This matters more than it sounds: copying a partial rectangle into the live
|
|
* framebuffer races the LCD's own reads, and the corruption scales with how
|
|
* much is written -- forcing full-width flushes made it visibly worse. Those
|
|
* corrupt pixels then persist indefinitely, because LVGL has no idea they are
|
|
* wrong and never reinvalidates them.
|
|
*/
|
|
static void rgb_flush_cb(lv_display_t *display, const lv_area_t *area, uint8_t *px_map)
|
|
{
|
|
if (lv_display_flush_is_last(display)) {
|
|
esp_lcd_panel_draw_bitmap(panel, 0, 0, DISPLAY_H_RES, DISPLAY_V_RES, px_map);
|
|
}
|
|
lv_display_flush_ready(display);
|
|
}
|
|
|
|
/*
|
|
* Phase 1: allocate everything, scan nothing.
|
|
*
|
|
* Called BEFORE wifi_init_sta(), on a heap nothing else has touched. The
|
|
* bounce buffers need two contiguous 25.6KB DMA-capable internal blocks, and
|
|
* WiFi's dynamic RX buffers churn that same pool hard enough to fragment it:
|
|
* observed on hardware (2026-08-04) as "no mem for bounce buffer" with 84KB
|
|
* of total free heap -- plenty of bytes, no block big enough, and retries
|
|
* useless because the fragmentation is not transient.
|
|
*
|
|
* Allocating first is deterministic where retrying was a dice roll. The
|
|
* panel is NOT scanning after this returns -- scan-out (and with it the
|
|
* PSRAM-reading refill ISR that flash writes would fault) starts only in
|
|
* display_start().
|
|
*/
|
|
esp_err_t display_alloc(void)
|
|
{
|
|
ESP_LOGI(TAG, "Allocating 800x480 RGB panel (CrowPanel Advance)");
|
|
|
|
const esp_lcd_rgb_panel_config_t panel_config = {
|
|
.clk_src = LCD_CLK_SRC_DEFAULT,
|
|
.timings = {
|
|
.pclk_hz = RGB_PCLK_HZ,
|
|
.h_res = DISPLAY_H_RES,
|
|
.v_res = DISPLAY_V_RES,
|
|
.hsync_pulse_width = RGB_HSYNC_PULSE_WIDTH,
|
|
.hsync_back_porch = RGB_HSYNC_BACK_PORCH,
|
|
.hsync_front_porch = RGB_HSYNC_FRONT_PORCH,
|
|
.vsync_pulse_width = RGB_VSYNC_PULSE_WIDTH,
|
|
.vsync_back_porch = RGB_VSYNC_BACK_PORCH,
|
|
.vsync_front_porch = RGB_VSYNC_FRONT_PORCH,
|
|
.flags = {
|
|
/*
|
|
* The Advance runs both syncs idle-HIGH (LovyanGFX
|
|
* hsync_polarity/vsync_polarity = 1), which is esp_lcd's
|
|
* default, so the *_idle_low flags stay clear. Its
|
|
* pclk_idle_high = 1 maps onto LCD_CK_OUT_EDGE, which esp_lcd
|
|
* exposes as pclk_active_neg.
|
|
*/
|
|
.pclk_active_neg = true,
|
|
},
|
|
},
|
|
.data_width = 16,
|
|
.bits_per_pixel = 16,
|
|
/* Two framebuffers so LVGL never draws into the one being scanned. */
|
|
.num_fbs = 2,
|
|
/*
|
|
* Bounce buffer in internal RAM. Fetching the framebuffer straight
|
|
* from PSRAM at 18MHz underruns the LCD FIFO and the image walks
|
|
* sideways; this stages each chunk in internal RAM first.
|
|
*
|
|
* Its refill ISR reads PSRAM, and PSRAM shares the cache with flash
|
|
* on the S3, so a flash write while the panel is running faults it
|
|
* with "Cache disabled but cached memory region accessed". app_main
|
|
* therefore finishes all NVS/WiFi flash activity *before* calling
|
|
* display_init(), and CONFIG_ESP_WIFI_NVS_ENABLED is off so the WiFi
|
|
* stack does not write to flash later.
|
|
*/
|
|
.bounce_buffer_size_px = DISPLAY_H_RES * BOUNCE_BUFFER_LINES,
|
|
.dma_burst_size = 64,
|
|
.hsync_gpio_num = RGB_HSYNC_GPIO,
|
|
.vsync_gpio_num = RGB_VSYNC_GPIO,
|
|
.de_gpio_num = RGB_DE_GPIO,
|
|
.pclk_gpio_num = RGB_PCLK_GPIO,
|
|
.disp_gpio_num = GPIO_NUM_NC,
|
|
.data_gpio_nums = {
|
|
RGB_B0_GPIO, RGB_B1_GPIO, RGB_B2_GPIO, RGB_B3_GPIO, RGB_B4_GPIO,
|
|
RGB_G0_GPIO, RGB_G1_GPIO, RGB_G2_GPIO, RGB_G3_GPIO, RGB_G4_GPIO, RGB_G5_GPIO,
|
|
RGB_R0_GPIO, RGB_R1_GPIO, RGB_R2_GPIO, RGB_R3_GPIO, RGB_R4_GPIO,
|
|
},
|
|
.flags = {
|
|
.fb_in_psram = true,
|
|
},
|
|
};
|
|
|
|
ESP_RETURN_ON_ERROR(esp_lcd_new_rgb_panel(&panel_config, &panel), TAG,
|
|
"esp_lcd_new_rgb_panel failed");
|
|
|
|
ESP_LOGI(TAG, "Panel buffers allocated; free internal heap: %u KB",
|
|
(unsigned)(heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024));
|
|
return ESP_OK;
|
|
}
|
|
|
|
/*
|
|
* Phase 2: start scan-out and hand the framebuffers to LVGL.
|
|
*
|
|
* From esp_lcd_panel_init() onward the bounce-refill ISR is reading PSRAM
|
|
* continuously, so every flash write (NVS, WiFi PHY calibration) must already
|
|
* be done -- see the init-order comment in main.c.
|
|
*/
|
|
esp_err_t display_start(void)
|
|
{
|
|
ESP_RETURN_ON_FALSE(panel != NULL, ESP_ERR_INVALID_STATE, TAG,
|
|
"display_alloc() must run before display_start()");
|
|
|
|
/* lv_display_create() below allocates from LVGL's heap. Without lv_init()
|
|
* that heap is a null pool and the call faults with LoadProhibited. */
|
|
ESP_RETURN_ON_FALSE(lv_is_initialized(), ESP_ERR_INVALID_STATE, TAG,
|
|
"lv_init() must run before display_start()");
|
|
|
|
ESP_RETURN_ON_ERROR(esp_lcd_panel_reset(panel), TAG, "panel reset failed");
|
|
ESP_RETURN_ON_ERROR(esp_lcd_panel_init(panel), TAG, "panel init failed");
|
|
|
|
void *fb0 = NULL;
|
|
void *fb1 = NULL;
|
|
ESP_RETURN_ON_ERROR(esp_lcd_rgb_panel_get_frame_buffer(panel, 2, &fb0, &fb1), TAG,
|
|
"failed to get framebuffers");
|
|
|
|
const size_t fb_size = DISPLAY_H_RES * DISPLAY_V_RES * sizeof(uint16_t);
|
|
memset(fb0, 0, fb_size);
|
|
memset(fb1, 0, fb_size);
|
|
|
|
disp = lv_display_create(DISPLAY_H_RES, DISPLAY_V_RES);
|
|
ESP_RETURN_ON_FALSE(disp != NULL, ESP_FAIL, TAG, "lv_display_create failed");
|
|
|
|
lv_display_set_color_format(disp, LV_COLOR_FORMAT_RGB565);
|
|
lv_display_set_flush_cb(disp, rgb_flush_cb);
|
|
|
|
/*
|
|
* DIRECT mode with the two framebuffers as LVGL's own buffers. LVGL draws
|
|
* the dirty regions into the back buffer and keeps both in sync itself;
|
|
* no separate internal draw buffer is needed, which also hands ~25KB of
|
|
* internal RAM back for the bounce buffers.
|
|
*/
|
|
lv_display_set_buffers(disp, fb0, fb1, fb_size, LV_DISPLAY_RENDER_MODE_DIRECT);
|
|
|
|
ESP_LOGI(TAG, "Panel up: %dx%d @ %d MHz, double-buffered fb0=%p fb1=%p (%u KB each)",
|
|
DISPLAY_H_RES, DISPLAY_V_RES, RGB_PCLK_HZ / 1000000,
|
|
fb0, fb1, (unsigned)(fb_size / 1024));
|
|
ESP_LOGI(TAG, "Free internal heap: %u KB",
|
|
(unsigned)(heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024));
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
lv_display_t *display_get_default(void)
|
|
{
|
|
return disp;
|
|
}
|
|
|
|
esp_lcd_panel_handle_t display_get_panel(void)
|
|
{
|
|
return panel;
|
|
}
|