230 lines
8.4 KiB
C
230 lines
8.4 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.
|
|
*/
|
|
/* 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;
|
|
|
|
/*
|
|
* Partial-mode flush: blit the dirty rectangle from a small internal-RAM
|
|
* draw buffer into the single PSRAM framebuffer.
|
|
*
|
|
* Page-flipping two framebuffers instead would be cheaper, but the swap
|
|
* lands mid-scanout on this panel and tears visibly. Copying into one
|
|
* framebuffer that the LCD scans continuously is what the vendor driver does.
|
|
*/
|
|
/*
|
|
* 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);
|
|
}
|
|
|
|
esp_err_t display_init(void)
|
|
{
|
|
ESP_LOGI(TAG, "Initializing 800x480 RGB panel (CrowPanel Advance)");
|
|
|
|
/* 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_init()");
|
|
|
|
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_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;
|
|
}
|