This is an automated email from the ASF dual-hosted git repository. jerpelea pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/nuttx.git
commit 8c7a5701e30a941d672e1f91f7f56af6af16b4f4 Author: Alan Carvalho de Assis <[email protected]> AuthorDate: Sun Aug 23 12:29:04 2026 -0300 esp32s3/lcd: Fix rolling issue on high resolution LCDs This patch get some fixes from ESP-IDF to fix a rolling issue that happens on big resolution LCDs such as 800x480 LCDs. Signed-off-by: Alan C. Assis <[email protected]> Assisted-by: Claude Code --- arch/xtensa/src/esp32s3/Kconfig | 42 +++++++ arch/xtensa/src/esp32s3/esp32s3_lcd.c | 222 +++++++++++++++++++++++++++++++++- 2 files changed, 263 insertions(+), 1 deletion(-) diff --git a/arch/xtensa/src/esp32s3/Kconfig b/arch/xtensa/src/esp32s3/Kconfig index 23c017e1acd..4d189c7baa5 100644 --- a/arch/xtensa/src/esp32s3/Kconfig +++ b/arch/xtensa/src/esp32s3/Kconfig @@ -2650,6 +2650,48 @@ config ESP32S3_LCD_CLOCK_MHZ int "LCD Pixel Clock Frequency in MHz" default 2 +config ESP32S3_LCD_BOUNCE_LINES + int "Bounce buffer height in lines (0 to disable)" + default 0 + ---help--- + Scan the panel out of two small buffers in internal RAM, each this + many lines tall, refilled from the framebuffer as they are consumed, + rather than having the DMA read the framebuffer directly. + + This matters when the framebuffer is in PSRAM. The DMA then has to + fetch every pixel from PSRAM in real time, and it is competing with + whatever else uses that memory; a CPU writing to the framebuffer, or + flushing it out of the cache, can stall those fetches for long + enough to empty the LCD FIFO. The panel sees the data stream + falter, and what shows up is an image that shakes and then rolls. + + With a bounce buffer the only thing on the critical path is internal + RAM, which nothing else can stall. The copy from PSRAM happens in + the interrupt that follows each buffer being consumed and has a + whole buffer's worth of time to complete. This is the arrangement + the ESP-IDF RGB panel driver calls a bounce buffer. + + The framebuffer must be a whole number of these buffers, so the + value has to divide the vertical resolution. Two buffers of this + size are taken out of internal RAM. Ten lines of an 800 pixel wide + panel at 16bpp costs 32000 bytes. + +config ESP32S3_LCD_PCLK_ACTIVE_NEG + bool "Latch data on the falling edge of the pixel clock" + default n + ---help--- + By default the pixel clock is low during the first half of a cycle, + so the panel latches the data lines, and DE and the sync signals + with them, on the rising edge. Enable this for a panel that + expects the falling edge instead. + + A panel driven on the wrong edge samples the sync signals at the + moment they change, which usually shows up as an image that is + recognisable but will not hold vertical lock. + + This is the equivalent of pclk_active_neg in the ESP-IDF RGB panel + driver. + config ESP32S3_LCD_VFRONTPORCH int "LCD Vertical Front Porch" default 40 diff --git a/arch/xtensa/src/esp32s3/esp32s3_lcd.c b/arch/xtensa/src/esp32s3/esp32s3_lcd.c index c3f12193e88..a047e6a2d31 100644 --- a/arch/xtensa/src/esp32s3/esp32s3_lcd.c +++ b/arch/xtensa/src/esp32s3/esp32s3_lcd.c @@ -26,6 +26,7 @@ #include <inttypes.h> #include <stdint.h> +#include <stdlib.h> #include <string.h> #include <assert.h> #include <errno.h> @@ -40,6 +41,7 @@ #include "esp_clk.h" #include "esp_gpio.h" #include "esp32s3_dma.h" +#include "hal/gdma_ll.h" #include "esp_irq.h" #include "xtensa.h" @@ -150,6 +152,28 @@ #define ESP32S3_LCD_DMADESC_NUM (ESP32S3_LCD_FB_SIZE / \ ESP32S3_DMA_BUFLEN_MAX + 1) +#if CONFIG_ESP32S3_LCD_BOUNCE_LINES > 0 + +/* Scan out of two buffers in internal RAM, refilled from the framebuffer as + * they are consumed, so that nothing on the panel's critical path depends on + * PSRAM. + */ + +# define ESP32S3_LCD_HAVE_BOUNCE 1 + +# define ESP32S3_LCD_BB_SIZE (CONFIG_ESP32S3_LCD_HRES * \ + CONFIG_ESP32S3_LCD_BOUNCE_LINES * \ + ESP32S3_LCD_DATA_WIDTH) + +# define ESP32S3_LCD_BB_DESCS ((ESP32S3_LCD_BB_SIZE + \ + ESP32S3_DMA_BUFLEN_MAX - 1) / \ + ESP32S3_DMA_BUFLEN_MAX) + +# if (ESP32S3_LCD_FB_SIZE % ESP32S3_LCD_BB_SIZE) != 0 +# error "CONFIG_ESP32S3_LCD_BOUNCE_LINES must divide the vertical resolution" +# endif +#endif + #define ESP32S3_LCD_LAYERS CONFIG_ESP32S3_LCD_BUFFER_LAYERS /* Get current layer pointer */ @@ -202,6 +226,18 @@ struct esp32s3_lcd_s uint32_t yoffset; /* The current pan offset */ +#ifdef ESP32S3_LCD_HAVE_BOUNCE + /* Descriptors for the two bounce buffers, linked into a ring so that the + * transfer never ends and the controller never has to be restarted. + */ + + struct esp32s3_dmadesc_s bbdesc[2][ESP32S3_LCD_BB_DESCS]; + + uint32_t bouncepos; /* Offset of the next chunk to copy */ + uint32_t bbeof; /* Buffers consumed so far */ + int bbcpuint; /* CPU interrupt for the DMA channel */ +#endif + int cpuint; /* CPU interrupt assigned to this LCD */ uint8_t cpu; /* CPU ID */ int32_t dma_channel; /* DMA channel */ @@ -331,6 +367,16 @@ static struct fb_vtable_s g_base_vtable = #endif }; +#ifdef ESP32S3_LCD_HAVE_BOUNCE +/* The bounce buffers. Ordinary static data, so they are in internal RAM, + * which is the whole point of them. + */ + +static uint8_t g_bounce[2][ESP32S3_LCD_BB_SIZE] + aligned_data(64); + +#endif + /**************************************************************************** * Private Functions ****************************************************************************/ @@ -731,6 +777,16 @@ static int IRAM_ATTR lcd_interrupt(int irq, void *context, void *arg) ESP32S3_LCD_FB_SIZE); #endif +#ifdef ESP32S3_LCD_HAVE_BOUNCE + /* Anchor the ring at the first bounce buffer again. The read + * position needs no correction: the framebuffer is a whole number of + * bounce buffers, so by the time a frame has been sent the buffers + * already hold the first two pieces of the next one. + */ + + esp32s3_dma_load(priv->bbdesc[0], priv->dma_channel, true); +#endif + /* Enable DMA TX */ esp32s3_dma_enable(priv->dma_channel, true); @@ -749,6 +805,74 @@ static int IRAM_ATTR lcd_interrupt(int irq, void *context, void *arg) return 0; } +#ifdef ESP32S3_LCD_HAVE_BOUNCE +/**************************************************************************** + * Name: esp32s3_lcd_bounce_fill + * + * Description: + * Copy the next piece of the framebuffer into a bounce buffer that has + * just been sent, and step the read position on. The position wraps at + * the end of the framebuffer, which is also the end of a frame because + * the framebuffer is a whole number of bounce buffers, so the two stay in + * step without anything having to resynchronise them. + * + * Input Parameters: + * priv - The LCD driver state. + * bb - Which of the two buffers to refill. + * + ****************************************************************************/ + +static void IRAM_ATTR esp32s3_lcd_bounce_fill(struct esp32s3_lcd_s *priv, + int bb) +{ + struct esp32s3_layer_s *layer = CURRENT_LAYER(priv); + + memcpy(g_bounce[bb], + &layer->framebuffer[priv->yoffset * ESP32S3_LCD_STRIDE + + priv->bouncepos], + ESP32S3_LCD_BB_SIZE); + + priv->bouncepos += ESP32S3_LCD_BB_SIZE; + if (priv->bouncepos >= ESP32S3_LCD_FB_SIZE) + { + priv->bouncepos = 0; + } +} + +/**************************************************************************** + * Name: esp32s3_lcd_dma_interrupt + * + * Description: + * One bounce buffer has been sent to the panel. Refill it while the + * other one is being sent. + * + ****************************************************************************/ + +static int IRAM_ATTR esp32s3_lcd_dma_interrupt(int irq, void *context, + void *arg) +{ + struct esp32s3_lcd_s *priv = &g_lcd_priv; + int status = esp32s3_dma_get_interrupt(priv->dma_channel, true); + + if (status <= 0) + { + return 0; + } + + esp32s3_dma_clear_interrupt(priv->dma_channel, true, status); + + if ((status & GDMA_LL_EVENT_TX_EOF) != 0) + { + /* The buffer that finished is the one this transfer started from */ + + esp32s3_lcd_bounce_fill(priv, priv->bbeof & 1); + priv->bbeof++; + } + + return 0; +} +#endif + /**************************************************************************** * Name: esp32s3_lcd_dmasetup * @@ -783,8 +907,20 @@ static int esp32s3_lcd_dmasetup(void) { struct esp32s3_layer_s *layer = &priv->layer[i]; + /* A framebuffer is large: at 800x480 in 16bpp one is 768000 bytes, + * twice that when double buffered, so it does not come from internal + * RAM and the allocation is one that can realistically fail. Say so + * rather than faulting on the memset that follows. + */ + layer->framebuffer = memalign(64, ESP32S3_LCD_FB_MEM_SIZE); - DEBUGASSERT(layer->framebuffer != NULL); + if (layer->framebuffer == NULL) + { + lcderr("Failed to allocate %d bytes for framebuffer %d\n", + ESP32S3_LCD_FB_MEM_SIZE, i); + goto errout; + } + memset(layer->framebuffer, 0, ESP32S3_LCD_FB_MEM_SIZE); esp32s3_dma_setup(layer->dmadesc, @@ -794,7 +930,63 @@ static int esp32s3_lcd_dmasetup(void) true, priv->dma_channel); } +#ifdef ESP32S3_LCD_HAVE_BOUNCE + { + struct esp32s3_dmadesc_s *tail; + int i; + + /* One chain per bounce buffer, each ending in the end of frame mark + * that raises the interrupt this driver refills on, and the two of + * them joined into a ring. The transfer then never finishes, so the + * controller runs continuously and the panel sees an unbroken stream. + */ + + for (i = 0; i < 2; i++) + { + esp32s3_dma_setup(priv->bbdesc[i], + ESP32S3_LCD_BB_DESCS, + g_bounce[i], + ESP32S3_LCD_BB_SIZE, + true, priv->dma_channel); + } + + for (i = 0; i < 2; i++) + { + for (tail = priv->bbdesc[i]; tail->next != NULL; tail = tail->next) + { + } + + tail->next = priv->bbdesc[i ^ 1]; + } + + /* Prime both buffers with the start of the first frame */ + + priv->bouncepos = 0; + priv->bbeof = 0; + + esp32s3_lcd_bounce_fill(priv, 0); + esp32s3_lcd_bounce_fill(priv, 1); + } +#endif + return OK; + +errout: + for (int i = 0; i < ESP32S3_LCD_LAYERS; i++) + { + struct esp32s3_layer_s *layer = &priv->layer[i]; + + if (layer->framebuffer != NULL) + { + free(layer->framebuffer); + layer->framebuffer = NULL; + } + } + + esp32s3_dma_release(priv->dma_channel); + priv->dma_channel = -1; + + return -ENOMEM; } /**************************************************************************** @@ -871,6 +1063,16 @@ static void esp32s3_lcd_enableclk(void) (ESP32S3_LCD_CLK_N << LCD_CAM_LCD_CLKM_DIV_NUM_S) | (clk_a << LCD_CAM_LCD_CLKM_DIV_A_S) | (clk_b << LCD_CAM_LCD_CLKM_DIV_B_S); + +#ifdef CONFIG_ESP32S3_LCD_PCLK_ACTIVE_NEG + /* Drive the pixel clock high for the first half of the cycle, so that the + * data lines, and DE and the sync signals with them, are stable across the + * falling edge the panel latches on. + */ + + regval |= LCD_CAM_LCD_CK_OUT_EDGE_M; +#endif + esp32s3_lcd_putreg(LCD_CAM_LCD_CLOCK_REG, regval); } @@ -1006,7 +1208,25 @@ static void esp32s3_lcd_enable(void) struct esp32s3_lcd_s *priv = &g_lcd_priv; struct esp32s3_layer_s *layer = CURRENT_LAYER(priv); +#ifdef ESP32S3_LCD_HAVE_BOUNCE + priv->bbcpuint = esp_setup_irq(ESP32S3_PERIPH_DMA_OUT_CH0 + + priv->dma_channel, + 1, ESP_IRQ_TRIGGER_LEVEL, + esp32s3_lcd_dma_interrupt, NULL); + if (priv->bbcpuint < 0) + { + lcderr("Failed to attach the DMA interrupt: %d\n", priv->bbcpuint); + return; + } + + esp32s3_dma_enable_interrupt(priv->dma_channel, true, + GDMA_LL_EVENT_TX_EOF, true); + up_enable_irq(ESP32S3_IRQ_DMA_OUT_CH0 + priv->dma_channel); + + esp32s3_dma_load(priv->bbdesc[0], priv->dma_channel, true); +#else esp32s3_dma_load(layer->dmadesc, priv->dma_channel, true); +#endif esp32s3_dma_enable(priv->dma_channel, true); /* Delay 1 microsecond to wait the DMA start */
