Skip to content

BSP Board Initialization — Display Flush Pipeline

BSP Board Initialization — Display Flush Pipeline

Section titled “BSP Board Initialization — Display Flush Pipeline”

The bsp_board_initialization_display sub-module implements the LVGL-to-hardware display flush pipeline for each supported board. It is the narrow seam between LVGL’s render engine and the underlying panel controller driver, responsible for:

  • Transferring completed LVGL draw buffers to the physical display panel
  • Signalling LVGL when the hardware transfer is complete (so it can reuse the buffer)
  • Capturing raw pixel data for the optional screenshot/snapshot feature
  • Handling bus-specific synchronization (VSYNC semaphores for RGB, ISR callbacks for SPI/I80)
  • Temporarily reducing the RGB pixel clock during SD card or flash filesystem access (bsp_accessFs / bsp_releaseFs)

This module does not own LVGL initialisation, buffer allocation, or touch input — those belong to the sibling modules bsp_board_initialization_lvgl and bsp_board_initialization_touch.


Hardware_Abstraction_Layer (BSP)

bsp_board_initialization

bsp_board_initialization_display

(current module)

bsp_board_initialization_lvgl

bsp_board_initialization_touch

bsp_board_initialization_inputs

bsp_display_drivers_spi

bsp_display_drivers_i80

bsp_display_drivers_rgb

See bsp_board_initialization.md for the full board initialisation sequence that calls into this module.


Each board in scope uses a different physical bus to drive its panel controller. The bus type determines which flush-ready signalling mechanism is used.

BoardPanel ControllerBus TypeFlush-Ready Signal
esp32s3_8048s070cEK9716RGB Paralleldisp_on_vsync_event (VSYNC ISR semaphore)
esp32s3_bzm_tft35_gt911ST7796SPInotify_lvgl_flush_ready (on_color_trans_done)
esp32s3_hmi43v3RM68120Intel 8080 (I80)i80_flush_ready_cb (driver callback)
esp32s3_zx3d50ce02s_usrc_4832ST7796 i80Intel 8080 (I80)i80_flush_ready_cb (driver callback)
pibot_pendant_v1_0ILI9341SPInotify_lvgl_flush_ready (on_color_trans_done)

For hardware-level driver details (panel init commands, SPI clock, RGB timing) see:


Architecture: Three Flush Pipeline Variants

Section titled “Architecture: Three Flush Pipeline Variants”

All five boards share a single logical flush pipeline — they differ only in how the hardware signals “transfer complete” back to LVGL.

RGB path

(EK9716)

I80 path

(RM68120 / ST7796 i80)

SPI path

(ILI9341 / ST7796 SPI)

LVGL (Core 1)

draw buffer ready

SPI board

lv_display_flush_ready()

I80 board

lv_display_flush_ready()

RGB board

wait VSYNC

lv_display_flush_ready()

Render engine

lv_refr_task

lvgl_flush_cb()

esp_lcd_panel_draw_bitmap()

notify_lvgl_flush_ready()

on_color_trans_done ISR

esp_lcd_panel_draw_bitmap()

i80_flush_ready_cb()

driver flush callback

sem_gui_ready /

sem_vsync_end

esp_lcd_panel_draw_bitmap()

disp_on_vsync_event()

VSYNC ISR


Signature (all boards):

static void lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map);

This is the single function registered with LVGL via lv_display_set_flush_cb(). LVGL calls it from its render task whenever a partial draw buffer is ready to be sent to the display.

Responsibilities in order:

  1. Snapshot capture (conditional on ESP3D_SNAPSHOT_FEATURE): if a screenshot capture is in progress, pixel data from the current flush area is written to a file in 120-byte chunks before the data is sent to the panel.
  2. Color byte-swap (DISPLAY_SWAP_COLOR_FLAG): optionally swaps RGB565 byte order in-place using lv_draw_sw_rgb565_swap() when the panel controller expects a different endianness from LVGL’s internal format.
  3. Bus-type synchronization (RGB only): gives sem_gui_ready and waits on sem_vsync_end to align the bitmap transfer with the display’s VSYNC pulse (see RGB-specific section).
  4. DMA transfer: calls esp_lcd_panel_draw_bitmap() to push the pixel buffer to the panel controller over the physical bus.
  5. Flush-ready signalling: for SPI and I80 boards the lv_display_flush_ready() call is not made here — it is deferred to an ISR callback (notify_lvgl_flush_ready or i80_flush_ready_cb) that fires when the DMA transfer completes. For the RGB board, lv_display_flush_ready() is called directly at the end of lvgl_flush_cb (after the VSYNC-gated transfer).

⚠️ LVGL threading rule: lvgl_flush_cb runs on Core 1 inside the LVGL task. The snapshot write path uses a FreeRTOS mutex (g_snapshot.mutex) with a non-blocking xSemaphoreTake(..., 0) to avoid stalling the LVGL task when the snapshot system is not available.


notify_lvgl_flush_ready — SPI boards only

Section titled “notify_lvgl_flush_ready — SPI boards only”

Boards: esp32s3_bzm_tft35_gt911, pibot_pendant_v1_0

Signature:

static bool notify_lvgl_flush_ready(esp_lcd_panel_io_handle_t panel_io,
esp_lcd_panel_io_event_data_t *edata,
void *user_ctx);

Registered as the on_color_trans_done callback on the esp_lcd_panel_io handle. The ESP-IDF SPI LCD driver fires this from its ISR when the DMA transfer of a bitmap is complete. user_ctx carries the lv_display_t * pointer set during init_lvgl().

lv_display_flush_ready((lv_display_t *)user_ctx);
return false; // no high-priority task woken

This call unblocks LVGL’s render scheduler so it can start the next frame or partial flush immediately.


Boards: esp32s3_hmi43v3, esp32s3_zx3d50ce02s_usrc_4832

Signature:

static void i80_flush_ready_cb(void);

Passed as a function pointer into the board-specific I80 display driver at configure time (e.g. disp_rm68120_configure(&cfg, i80_flush_ready_cb)). The I80 driver invokes it from its transfer-complete ISR. Because the I80 driver wrapper abstracts the callback type, this function takes no parameters and uses the module-static lvgl_display pointer directly:

lv_display_flush_ready(lvgl_display);

This differs from the SPI path where the display pointer is passed through user_ctx. Both mechanisms achieve the same effect: unblocking the LVGL render task after DMA completes.


Board: esp32s3_8048s070c

Signature:

static bool disp_on_vsync_event(esp_lcd_panel_handle_t panel,
const esp_lcd_rgb_panel_event_data_t *event_data,
void *user_data);

Registered as the .on_vsync event callback on the RGB panel via esp_lcd_rgb_panel_register_event_callbacks(). Unlike SPI/I80 panels, RGB panels stream pixel data continuously from a single frame buffer. This means an esp_lcd_panel_draw_bitmap() call writes into the active scanout buffer; without synchronization, the DMA scanout engine may read a partially-updated buffer, causing visible tearing.

The VSYNC callback implements a two-semaphore handshake:

StepLVGL taskVSYNC ISR
1Render done, lvgl_flush_cb called—
2xSemaphoreGive(sem_gui_ready)—
3xSemaphoreTake(sem_vsync_end, MAX_DELAY) — blocks—
4—xSemaphoreTakeFromISR(sem_gui_ready) — non-blocking
5—xSemaphoreGiveFromISR(sem_vsync_end) — unblocks LVGL
6esp_lcd_panel_draw_bitmap() — safe: scanout just started new frame—
7lv_display_flush_ready()—

The BaseType_t high_task_awoken return value allows FreeRTOS to perform an immediate context switch back to the LVGL task from ISR context if giving sem_vsync_end woke a higher-priority task.


bsp_accessFs / bsp_releaseFs — RGB board only

Section titled “bsp_accessFs / bsp_releaseFs — RGB board only”

Board: esp32s3_8048s070c Conditional: ESP3D_PATCH_FS_ACCESS_RELEASE

esp_err_t bsp_accessFs(void);
esp_err_t bsp_releaseFs(void);

On the RGB board the pixel clock (DISPLAY_PCLK_FREQ_HZ) and the SD/Flash SPI clock share GPIO routing or bus bandwidth. At full pixel clock speed, concurrent SPI filesystem access causes data corruption or DMA stalls. These two functions are called by ESP3DFlash::accessFS() and ESP3DSd::accessFS() (declared in board_init.h) to throttle the pixel clock before a filesystem operation and restore it after.

esp_err_t bsp_accessFs(void) {
// Reduce pixel clock to DISPLAY_PATCH_FS_FREQ_HZ
esp_lcd_rgb_panel_set_pclk(disp_panel, DISPLAY_PATCH_FS_FREQ_HZ);
vTaskDelay(pdMS_TO_TICKS(DISPLAY_PATCH_FS_DELAY_MS)); // let clock settle
return ESP_OK;
}
esp_err_t bsp_releaseFs(void) {
// Restore normal pixel clock
esp_lcd_rgb_panel_set_pclk(disp_panel, DISPLAY_PCLK_FREQ_HZ);
vTaskDelay(pdMS_TO_TICKS(DISPLAY_PATCH_FS_DELAY_MS));
return ESP_OK;
}

Both functions guard against a NULL disp_panel handle (returning ESP_OK silently if the panel was never initialised). The vTaskDelay after the clock change allows the display PLL to stabilise before filesystem access begins or rendering resumes.


SPI Pipeline (pibot_pendant_v1_0, esp32s3_bzm_tft35_gt911)

Section titled “SPI Pipeline (pibot_pendant_v1_0, esp32s3_bzm_tft35_gt911)”
notify_lvgl_flush_ready() [ISR]esp_lcd SPI driver (DMA)Snapshot writer (optional)lvgl_flush_cb()LVGL Render Task (Core 1)notify_lvgl_flush_ready() [ISR]esp_lcd SPI driver (DMA)Snapshot writer (optional)lvgl_flush_cb()LVGL Render Task (Core 1)DMA transfer in progressnext frame / partial flush scheduledcall with draw buffer + areawrite pixels to file (if capturing)rgb565_swap (if DISPLAY_SWAP_COLOR_FLAG)esp_lcd_panel_draw_bitmap()on_color_trans_done callbacklv_display_flush_ready()

Intel 8080 Pipeline (esp32s3_hmi43v3, esp32s3_zx3d50ce02s_usrc_4832)

Section titled “Intel 8080 Pipeline (esp32s3_hmi43v3, esp32s3_zx3d50ce02s_usrc_4832)”
i80_flush_ready_cb() [ISR]esp_lcd I80 driver (DMA)Snapshot writer (optional)lvgl_flush_cb()LVGL Render Task (Core 1)i80_flush_ready_cb() [ISR]esp_lcd I80 driver (DMA)Snapshot writer (optional)lvgl_flush_cb()LVGL Render Task (Core 1)I80 parallel DMA transfercall with draw buffer + areawrite pixels to file (if capturing)rgb565_swap (if DISPLAY_SWAP_COLOR_FLAG)esp_lcd_panel_draw_bitmap()transfer complete callbacklv_display_flush_ready() disp_on_vsync_event() [ISR]RGB panel DMA scanoutSemaphore pairSnapshot writer (optional)lvgl_flush_cb()LVGL Render Task (Core 1)disp_on_vsync_event() [ISR]RGB panel DMA scanoutSemaphore pairSnapshot writer (optional)lvgl_flush_cb()LVGL Render Task (Core 1)BLOCKED - waiting for VSYNC pulsecall with draw buffer + areawrite pixels to file (if capturing)rgb565_swap (if DISPLAY_SWAP_COLOR_FLAG)xSemaphoreGive(sem_gui_ready)xSemaphoreTake(sem_vsync_end, MAX_DELAY)VSYNC ISR firesTakeFromISR(sem_gui_ready)GiveFromISR(sem_vsync_end)unblockedesp_lcd_panel_draw_bitmap()lv_display_flush_ready()

Why VSYNC synchronization matters: The RGB DMA engine scans out the frame buffer continuously. Without the semaphore gate, LVGL could begin writing new pixels into the same buffer that DMA is currently reading, producing horizontal tearing. The semaphore pair ensures the bitmap write happens immediately after a VSYNC edge — at the safest point in the frame period.


When ESP3D_SNAPSHOT_FEATURE is enabled at build time, each lvgl_flush_cb implementation includes an inline pixel capture path. LVGL renders in partial mode (area by area), so the snapshot accumulates data across multiple flush calls until captured_pixels >= expected_pixels.

No

Yes

Failed - skip

Acquired

Yes - set error + abort

No

Yes - capture done

No - more areas pending

lvgl_flush_cb() called

g_snapshot.ongoing?

xSemaphoreTake

(mutex, 0)

fwrite() in 120-byte chunks

Write error?

captured_pixels

≥ expected_pixels?

g_snapshot.ongoing = false

g_snapshot.error = true/false

Color swap + bus transfer

(normal flush path)

Key constraints:

  • The mutex is taken non-blocking (timeout = 0) to guarantee the LVGL task is never stalled waiting for snapshot state.
  • File writes use fixed 120-byte chunks to stay within ESP32 safe stack and heap allocation patterns (see esp32_memory_constraints.md).
  • On error, both g_snapshot.error and g_snapshot.ongoing are set to stop the capture without crashing.
  • Pixel size is determined at runtime from sizeof(lv_color_t) (2 bytes for RGB565, 4 bytes for ARGB8888).

This flow applies only to boards using the RGB parallel interface (esp32s3_8048s070c) when ESP3D_PATCH_FS_ACCESS_RELEASE is defined.

RGB Panel Driver (EK9716)bsp_accessFs / bsp_releaseFsESP3DFlash / ESP3DSdRGB Panel Driver (EK9716)bsp_accessFs / bsp_releaseFsESP3DFlash / ESP3DSdbsp_accessFs()esp_lcd_rgb_panel_set_pclk(DISPLAY_PATCH_FS_FREQ_HZ)vTaskDelay - clock settleESP_OKSD card / Flash read or writebsp_releaseFs()esp_lcd_rgb_panel_set_pclk(DISPLAY_PCLK_FREQ_HZ)vTaskDelay - clock settleESP_OK

Filesystem Layer

Panel Drivers

LVGL

Board BSP - board_init.c

flush callback

SPI boards

I80 boards

RGB board after VSYNC gate

on_color_trans_done ISR

transfer done callback

VSYNC ISR

releases sem_vsync_end

RGB board only

before FS op

set_pclk throttle / restore

lvgl_flush_cb()

disp_on_vsync_event()

notify_lvgl_flush_ready()

i80_flush_ready_cb()

bsp_accessFs()

bsp_releaseFs()

lv_display_t

lv_display_flush_ready()

disp_ili9341_spi

disp_st7796_spi

disp_rm68120

disp_st7796_i80

disp_ek9716

ESP3DFlash

ESP3DSd


Guard MacroEffect on this module
ESP3D_DISPLAY_FEATUREEntire flush pipeline compiled only when 1. If 0, accessor functions return NULL.
ESP3D_SNAPSHOT_FEATUREPixel capture block inside lvgl_flush_cb is conditionally compiled.
ESP3D_PATCH_FS_ACCESS_RELEASEbsp_accessFs() / bsp_releaseFs() compiled only when 1 (RGB boards).
DISPLAY_SWAP_COLOR_FLAGPer-board config constant; 0 skips the lv_draw_sw_rgb565_swap() call entirely.
DISPLAY_USE_DOUBLE_BUFFER_FLAGControls buffer count in init_lvgl() (sibling module); the flush path is identical for single and double buffer.

  1. LVGL single-thread rule: lvgl_flush_cb must never block the LVGL task for an unbounded time. The snapshot mutex uses xSemaphoreTake(..., 0) (non-blocking). On RGB boards, xSemaphoreTake(sem_vsync_end, portMAX_DELAY) is bounded by the display refresh period (typically 60 Hz → ≤ 17 ms).

  2. ISR safety: disp_on_vsync_event and notify_lvgl_flush_ready run in ISR context. They use only FromISR FreeRTOS primitives. disp_on_vsync_event returns high_task_awoken to allow immediate context switch to the unblocked LVGL task.

  3. Flush-ready must be called exactly once per flush: LVGL’s partial render mode calls lvgl_flush_cb repeatedly. Each call must result in exactly one lv_display_flush_ready() — no more, no less. Missing it deadlocks LVGL; calling it twice corrupts the render state.

  4. disp_panel null guard in bsp_accessFs: Both FS-access functions check if (!disp_panel) return ESP_OK to safely handle calls during early boot before the panel is initialised.

  5. No dynamic allocation in the flush path: Draw buffers are allocated once during init_lvgl() (see bsp_board_initialization_lvgl.md). On RGB boards, MALLOC_CAP_SPIRAM is used (external PSRAM required). On SPI/I80 boards, MALLOC_CAP_DMA is used (internal DMA-capable SRAM). The flush path itself makes zero dynamic allocations.


DocumentRelationship
bsp_board_initialization.mdParent module: full board init sequence and board_init() entry point
bsp_board_initialization_lvgl.mdSibling: LVGL init, draw buffer allocation, tick timer setup
bsp_board_initialization_touch.mdSibling: touch controller init and touch_read_cb registered to the same lv_display_t
bsp_display_drivers_spi.mdSPI panel drivers (ILI9341, ST7796 SPI) — called from board_init() before init_lvgl()
bsp_display_drivers_i80.mdIntel 8080 panel drivers (RM68120, ST7796 i80)
bsp_display_drivers_rgb.mdRGB parallel panel drivers (EK9716)
docs/architecture/display_drivers.mdArchitecture reference: SPI vs RGB driver architecture, orientation/rotation math, physical vs logical resolution
docs/guides/esp32_memory_constraints.mdMemory allocation rules relevant to buffer sizing and chunk-write patterns