/* * 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 #include #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. */ /* 16 is the practical ceiling: two buffers at 20 lines need 64KB of internal * RAM and the allocation fails once WiFi is up. */ #define BOUNCE_BUFFER_LINES 16 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; }