Skip to content

RGB Parallel Display Drivers — Technical Documentation

RGB Parallel Display Drivers — Technical Documentation

Section titled “RGB Parallel Display Drivers — Technical Documentation”

Module: bsp_display_drivers_rgb Source path: hardware/drivers_video_rgb/ Components: disp_ek9716_config_t, disp_ili9485_config_t, disp_st7262_config_t Purpose: Configuration layer for the three RGB parallel (DPI) panel controllers supported by the firmware. Each driver exposes a typed configuration struct and an orientation enum that abstract ESP-IDF’s raw esp_lcd_rgb_panel_config_t + swap_xy/mirror calls behind a board-facing API.


  1. Introduction
  2. Architecture Overview
  3. Driver Inventory
  4. Configuration Structures
  5. Orientation Enumerations
  6. RGB Parallel vs. SPI/I80 Drivers
  7. Physical vs. Logical Resolution
  8. VSync Synchronisation and the LVGL Flush Path
  9. SD Card Shared-Bus Constraint
  10. Board Compatibility Matrix
  11. Integration with the BSP Layer
  12. Data Flow Diagram
  13. Component Interaction Diagram
  14. Key Constraints and Pitfalls
  15. Related Documentation

The display_drivers_rgb module provides the hardware configuration layer for displays driven over the RGB parallel (DPI) interface. Unlike SPI or Intel 8080 (i80) panel controllers — which hold their own on-chip frame-buffer RAM (GRAM) and accept pixel data over a serial or 8/16-bit data bus — RGB parallel panels have no on-chip frame buffer. The ESP32-S3’s own PSRAM is the framebuffer; a GDMA engine continuously streams it to the glass, synchronized with hardware HSYNC/VSYNC/DE/PCLK signals.

This structural difference makes the RGB driver module significantly simpler at the C-API level (it is configuration only, not a panel-command driver), while requiring more careful system integration: PSRAM allocation, VSync semaphore synchronisation inside the BSP, and PCLK frequency budgeting are all critical correctness concerns that do not exist for SPI panels.

The three drivers in this module share an identical struct layout:

esp_lcd_rgb_panel_config_t ← physical timing/wiring (fixed by glass)
orientation enum ← logical orientation for the mounted board
hor_res / ver_res ← logical (LVGL-facing) resolution

For orientation math, validation status, and the UI-layer implications of these choices, see display_drivers.md, which is the canonical cross-family reference.


Hardware

display_drivers_rgb module

ESP-IDF RGB Driver

BSP Layer - per-board board_init.c

Application Layer (Core 1)

flush request

sem_give(gui_ready)

sem_take(vsync_end)

ISR sem exchange

draw_bitmap()

pixel transform

(swap_xy/mirror)

continuous DMA stream

register on_vsync

panel_config passed to

esp_lcd_new_rgb_panel()

panel_config passed to

esp_lcd_new_rgb_panel()

panel_config passed to

esp_lcd_new_rgb_panel()

LVGL

(lv_display_t)

lvgl_flush_cb()

disp_on_vsync_event()

(ISR)

VSync Semaphores

sem_gui_ready

sem_vsync_end

init_lvgl()

init_display_panel()

esp_lcd_rgb_panel

esp_lcd_panel_draw_bitmap()

GDMA Engine

(continuous streaming)

disp_st7262_config_t

hardware/drivers_video_rgb/disp_st7262/

disp_ek9716_config_t

hardware/drivers_video_rgb/disp_ek9716/

disp_ili9485_config_t

hardware/drivers_video_rgb/disp_ili9485/

ESP32-S3 PSRAM

(framebuffer)

RGB Panel Glass

(HSYNC/VSYNC/DE/PCLK

+ 16 data lines)


DriverConfig headerOrientation enumTypical board / panel size
ST7262disp_st7262/disp_st7262_config.hdisp_st7262_orientation_tesp32s3_8048s043c, esp32s3_8048s050c (800×480)
EK9716disp_ek9716/disp_ek9716_config.hdisp_ek9716_orientation_tesp32s3_8048s070c, esp32s3_8048_touch_lcd_7 (800×480 / 1024×600)
ILI9485disp_ili9485/disp_ili9485_config.hdisp_ili9485_orientation_tesp32s3_4827s043c (480×272 native, mounted 90°)

All three drivers delegate panel initialisation entirely to ESP-IDF’s esp_lcd_rgb_panel component (esp_lcd_new_rgb_panel()). The config struct is the sole output of this module — it feeds the BSP layer’s board_init.c which calls the ESP-IDF API.


Every RGB config struct contains the same three fields:

FieldTypeDescription
panel_configesp_lcd_rgb_panel_config_tESP-IDF RGB panel configuration: PSRAM handle, GDMA settings, PCLK frequency, HSYNC/VSYNC/DE polarity, back/front porch timings, 16 parallel data pin assignments, and physical h_res/v_res (glass pixel count — fixed by wiring, never virtual).
orientationdisp_<panel>_orientation_tLogical orientation for the mounted board. Maps directly to an esp_lcd_panel_swap_xy() + esp_lcd_panel_mirror() call sequence at init.
hor_resuint16_tLogical horizontal resolution in pixels, LVGL-facing. Equals DISPLAY_WIDTH_PX. May differ from panel_config.timings.h_res when the board is rotated 90°/270°.
ver_resuint16_tLogical vertical resolution in pixels, LVGL-facing. Equals DISPLAY_HEIGHT_PX. May differ from panel_config.timings.v_res when the board is rotated 90°/270°.

Important: panel_config.timings.h_res and v_res must always equal the physical glass pixel matrix size. They are fixed by hardware wiring and cannot be virtualised. The logical hor_res/ver_res fields carry the board’s orientation-corrected shape that LVGL operates on.


/* hardware/drivers_video_rgb/disp_st7262/disp_st7262_config.h */
typedef struct {
esp_lcd_rgb_panel_config_t panel_config; /**< RGB panel bus/timing configuration */
disp_st7262_orientation_t orientation; /**< Panel orientation */
uint16_t hor_res; /**< Horizontal resolution (logical) */
uint16_t ver_res; /**< Vertical resolution (logical) */
} disp_st7262_config_t;

The ST7262 is a native-landscape 800×480 RGB controller. Its orientation enum uses a straightforward 0°/90°/180°/270° mapping (see Orientation Enumerations below).


/* hardware/drivers_video_rgb/disp_ek9716/disp_ek9716_config.h */
typedef struct {
esp_lcd_rgb_panel_config_t panel_config; /**< RGB panel bus/timing configuration */
disp_ek9716_orientation_t orientation; /**< Panel orientation */
uint16_t hor_res; /**< Horizontal resolution (logical) */
uint16_t ver_res; /**< Vertical resolution (logical) */
} disp_ek9716_config_t;

The EK9716 is another native-landscape RGB controller. Its orientation enum follows the same 0°/90°/180°/270° mapping as the ST7262.


/* hardware/drivers_video_rgb/disp_ili9485/disp_ili9485_config.h */
typedef struct {
esp_lcd_rgb_panel_config_t panel_config; /**< RGB panel bus/timing configuration */
disp_ili9485_orientation_t orientation; /**< Panel orientation */
uint16_t hor_res; /**< Horizontal resolution (logical) */
uint16_t ver_res; /**< Vertical resolution (logical) */
} disp_ili9485_config_t;

The ILI9485 used in this codebase is wired in RGB565 parallel mode (not its SPI command-interface mode). Its native glass orientation is landscape (480×272), but the reference board (esp32s3_4827s043c) mounts it in a 90°-rotated enclosure. Consequently its orientation enum values carry different swap_xy/mirror semantics than the other two drivers — see the section below.


Both the ST7262 and EK9716 orientation enums follow the same straightforward mapping:

Enum valueDegree rotationswap_xymirror_xmirror_yDeterminant
PORTRAIT0°———+1 ✅
LANDSCAPE90°✓✓—+1 ✅
PORTRAIT_INVERTED180°—✓✓+1 ✅
LANDSCAPE_INVERTED270°✓—✓+1 ✅

Note: The PORTRAIT variant maps to 0° with these panels’ native glass orientation.


ILI9485 (native-landscape — orientation mapping differs)

Section titled “ILI9485 (native-landscape — orientation mapping differs)”

The ILI9485’s native glass layout is landscape (480×272). The esp32s3_4827s043c board mounts it 90° rotated inside the pendant enclosure, so its PORTRAIT enum value does not mean 0° — it means a 90°-rotated presentation. The mapping was corrected in 2026-08 to fix a reflection bug (see display_drivers.md §Orientation math):

Enum valueLogical orientationswap_xymirror_xmirror_yDeterminantComment
LANDSCAPE0° (native)———+1 ✅No transform
LANDSCAPE_INVERTED180°—✓✓+1 ✅Point reflection
PORTRAIT90°✓✓—+1 ✅swap + mirror X
PORTRAIT_INVERTED270°✓—✓+1 ✅swap + mirror Y

⚠️ Critical rule: swap_xy alone has a transform matrix determinant of −1 (a reflection, not a rotation — text renders mirrored/backwards on hardware). swap_xy always requires exactly one mirrored axis alongside it to produce a genuine rotation (determinant +1). This was a confirmed hardware bug in an earlier revision. See display_drivers.md §Orientation math for the full derivation.


I80 Parallel - separate module

RM68120, ST7796-i80

Own GRAM + 8/16-bit

data bus (not DPI)

notify_flush_ready()

or i80_flush_ready_cb()

RGB Parallel Panels - this module

ILI9485, ST7262

EK9716

No GRAM - ESP32-S3

PSRAM is the framebuffer

swap_xy + mirror

CPU-side pixel transform

Real cost: lower PCLK

if tearing appears

esp_lcd_rgb_panel

+ GDMA continuous stream

SPI / I80 Panel Controllers

ILI9341, ST7796

ILI9488

Own GRAM

(frame buffer in the

controller chip)

MADCTL register 0x36

hardware remap

zero CPU cost

esp_lcd_panel_io_spi

or esp_lcd_panel_io_i80

For the SPI driver details, see display_drivers_spi.md. For the I80 driver details, see display_drivers_i80.md.

CharacteristicSPI (MADCTL)RGB Parallel (this module)I80 Parallel
Frame buffer locationController GRAMESP32 PSRAMController GRAM
Orientation costFree (register write)CPU pixel transform (real cost)Free (register write)
VSync requiredNoYes (semaphore pair)No — notify_flush_ready callback
Flush callback stylenotify_lvgl_flush_ready()VSync semaphore gatei80_flush_ready_cb()
PCLK / pixel clockN/A (SPI clock)Critical — lower if tearingN/A (i80 WR strobe)
PSRAM requiredNoYes (draw buffers)No

Because RGB parallel panels cannot remap their physical scan resolution, two separate resolution pairs exist for any board with a rotated mount:

┌──────────────────────────────────────────────────────────┐
│ board_config.h (main firmware) │
│ │
│ // Logical (LVGL-facing, portrait terms) │
│ #define DISPLAY_WIDTH_PX 272 │
│ #define DISPLAY_HEIGHT_PX 480 │
│ │
│ // Physical (glass wiring, feeds panel_config.timings) │
│ #define DISPLAY_PANEL_PHYSICAL_WIDTH_PX 480 │
│ #define DISPLAY_PANEL_PHYSICAL_HEIGHT_PX 272 │
└──────────────────────────────────────────────────────────┘

Board with native orientation - e.g. esp32s3_8048s043c

identity

Physical glass

800 x 480

timings.h_res = 800

timings.v_res = 480

Logical canvas

800 x 480

hor_res = 800 ver_res = 480

no transform needed

Board with rotated mount - e.g. esp32s3_4827s043c

physical scan

logical canvas

Physical glass matrix

480 columns x 272 rows

panel_config.timings.h_res = 480

panel_config.timings.v_res = 272

swap_xy + mirror_y

DISP_ILI9485_ORIENTATION_PORTRAIT_INVERTED

bridges physical to logical

Logical LVGL canvas

272 wide x 480 tall

hor_res = 272 ver_res = 480

DISPLAY_WIDTH_PX = 272

DISPLAY_HEIGHT_PX = 480

Define both macro pairs for every RGB board, even if the physical and logical resolutions are identical. This makes rotated-mount boards self-documenting and avoids silent divergence if orientation is later changed.

The Factory app uses the same convention under different macro names:

  • SCREEN_WIDTH / SCREEN_HEIGHT — logical
  • TFT_PANEL_PHYSICAL_WIDTH / TFT_PANEL_PHYSICAL_HEIGHT — physical

VSync Synchronisation and the LVGL Flush Path

Section titled “VSync Synchronisation and the LVGL Flush Path”

RGB parallel panels require explicit VSync synchronisation to prevent screen tearing. Because the GDMA engine streams the framebuffer to the glass continuously, writing pixels at an arbitrary moment will produce partial-frame artifacts. The BSP layer implements a two-semaphore protocol:

esp_lcd_panel_draw_bitmap()disp_on_vsync_event() [ISR]lvgl_flush_cb()LVGL (Core 1)esp_lcd_panel_draw_bitmap()disp_on_vsync_event() [ISR]lvgl_flush_cb()LVGL (Core 1)optional: lv_draw_sw_rgb565_swap()(if DISPLAY_SWAP_COLOR_FLAG)pixel transform (swap_xy/mirror)written into PSRAM framebufferflush request (dirty region)sem_give(sem_gui_ready)sem_give(sem_vsync_end) on next VSync pulsesem_take(sem_vsync_end, portMAX_DELAY)esp_lcd_panel_draw_bitmap(x1,y1,x2+1,y2+1,px_map)lv_display_flush_ready()

Key points:

  • sem_gui_ready and sem_vsync_end are binary FreeRTOS semaphores created in init_lvgl().
  • The VSync ISR (disp_on_vsync_event) is registered via esp_lcd_rgb_panel_register_event_callbacks().
  • The flush callback blocks on sem_vsync_end — it must never be called from an ISR context or from a high-priority task that cannot afford to block.
  • LVGL draw buffers are allocated from PSRAM (MALLOC_CAP_SPIRAM) because internal SRAM is insufficient for RGB panel buffer sizes.
  • Optional DISPLAY_SWAP_COLOR_FLAG byte-swaps the pixel buffer before the VSync wait — never in-place if the buffer is reused across multiple flush calls (see Key Constraints).

Several RGB boards share GPIO pins between the RGB parallel data bus and the SD card SPI/SDIO bus. The BSP layer on affected boards exposes:

esp_err_t bsp_accessFs(void); // Pause RGB panel DMA, reconfigure shared pins for SD
esp_err_t bsp_releaseFs(void); // Restore shared pins to RGB panel, resume DMA

These guards appear in board_init.c for: esp32s3_8048s043c, esp32s3_8048s050c, esp32s3_8048s070c, and esp32s3_8048_touch_lcd_7.

RGB Panel DMASD Card DriverBSP board_init.cApplicationRGB Panel DMASD Card DriverBSP board_init.cApplicationbsp_accessFs()Pause / suspend DMAReconfigure shared GPIO pins for SDSD read/write operationsbsp_releaseFs()Release shared GPIO pinsResume DMA, restore RGB pin config

Not all RGB boards have this constraint. esp32s3_4827s043c does not expose bsp_accessFs/bsp_releaseFs because its SD card uses dedicated pins. Check the specific board’s board_init.c before accessing the filesystem on an RGB board.

See shared_sd_mechanism_V2.0.md for the full SD-sharing protocol.


BoardDriver config structPhysical resolutionLogical resolutionDefault orientationbsp_accessFs?
esp32s3_4827s043cdisp_ili9485_config_t480×272272×480 (portrait)DISP_ILI9485_ORIENTATION_PORTRAIT_INVERTEDNo
esp32s3_8048s043cdisp_st7262_config_t800×480800×480 (landscape)DISP_ST7262_ORIENTATION_LANDSCAPEYes
esp32s3_8048s050cdisp_st7262_config_t800×480800×480 (landscape)DISP_ST7262_ORIENTATION_LANDSCAPEYes
esp32s3_8048s070cdisp_ek9716_config_t800×480800×480 (landscape)TBD — not yet portedYes
esp32s3_8048_touch_lcd_7disp_ek9716_config_t1024×6001024×600 (landscape)TBD — not yet portedYes

“Not yet ported” entries: orientation, PCLK frequency, and touch calibration must be hardware-confirmed on each board before marking as complete. Do not assume values transfer from a sibling board — see display_drivers.md §Common pitfalls.


The RGB config structs are consumed exclusively by each board’s board_init.c inside the BSP component. The typical call chain at boot:

Yes

No

board_init()

init_display_panel()

Populate disp_panel_config_t

pin assignments, timings, orientation

esp_lcd_new_rgb_panel

panel_config, disp_panel

esp_lcd_panel_reset()

esp_lcd_panel_init()

orientation != 0 degrees?

esp_lcd_panel_swap_xy()

esp_lcd_panel_mirror()

skip transform calls

init_lvgl()

Create sem_gui_ready

sem_vsync_end

esp_lcd_rgb_panel_register_event_callbacks

on_vsync = disp_on_vsync_event

lv_display_create

hor_res, ver_res

Allocate PSRAM draw buffer(s)

MALLOC_CAP_SPIRAM

lv_display_set_flush_cb

lvgl_flush_cb

Start LVGL tick timer

The orientation field in the config struct drives the swap_xy/mirror call sequence — it is the single authoritative source for physical display orientation. The hor_res/ver_res fields are passed directly to lv_display_create() as the logical LVGL canvas size.

On boards that link DEFAULT_ORIENTATION from CMake (e.g. esp32s3_4827s043c), the orientation field is derived from the CMake-level ESP3D_ORIENTATION_* macro — the physical driver orientation and the UI-layer orientation selection share the same source of truth. See display_drivers.md §UI-layer orientation for the full unification design.


Hardware Path

BSP Flush Path

LVGL Core 1

sem_give(sem_vsync_end)

write to PSRAM framebuffer

VSYNC pulse

lv_timer_handler()

Render dirty regions

into draw buffer

PSRAM

lvgl_flush_cb()

called per dirty region

Optional RGB565 byte-swap

DISPLAY_SWAP_COLOR_FLAG

sem_give(sem_gui_ready)

sem_take(sem_vsync_end)

blocks until VSync ISR fires

esp_lcd_panel_draw_bitmap()

pixel transform into

PSRAM framebuffer

lv_display_flush_ready()

VSync ISR

disp_on_vsync_event()

GDMA: stream PSRAM

framebuffer to glass

RGB Panel glass

continuous raster scan


Touch controllers - sibling module

ESP-IDF

Factory app - per-board

BSP layer - per-board board_init.c

display_drivers_rgb module

consumed by

consumed by

consumed by

consumed by

consumed by

embeds

embeds

embeds

standalone driver

no config struct used

standalone driver

no config struct used

disp_st7262_config.h

disp_st7262_config_t

disp_st7262_orientation_t

disp_ek9716_config.h

disp_ek9716_config_t

disp_ek9716_orientation_t

disp_ili9485_config.h

disp_ili9485_config_t

disp_ili9485_orientation_t

esp32s3_8048s043c

board_init.c

esp32s3_8048s050c

board_init.c

esp32s3_8048s070c

board_init.c

esp32s3_8048_touch_lcd_7

board_init.c

esp32s3_4827s043c

board_init.c

esp32s3_8048s043c

Factory/main/st7262.c

st7262_init / st7262_flush

st7262_backlight

esp32s3_8048s050c

Factory/main/st7262.c

st7262_init / st7262_flush

st7262_backlight

esp_lcd_panel_rgb.h

esp_lcd_rgb_panel_config_t

esp_lcd_new_rgb_panel()

touch_gt911 GT911

I2C - large RGB panels

touch_ft5x06 / ft6336u

I2C alternatives

Factory app note: The Factory app (boards/*/Factory/main/st7262.c) implements its own minimal st7262_init() / st7262_flush() without using the disp_st7262_config_t struct. It calls esp_lcd_new_rgb_panel() directly with hardcoded config values. The two code paths are independent — changes to the BSP driver config struct do not propagate to the Factory app automatically.


ESP-IDF’s esp_lcd_rgb_panel implements swap_xy as a per-pixel index computation (rgb_lcd_rotation_sw.h) instead of the plain memcpy used for unrotated panels. This measurably increases CPU load on every draw_bitmap() call.

If a rotated RGB board shows screen tearing or drift that a non-rotated board of the same family does not: reduce DISPLAY_PCLK_FREQ_HZ first (e.g. 14 MHz → 8 MHz was needed for esp32s3_4827s043c). Do not assume it is the same flash/PSRAM bandwidth issue documented in esp32_memory_constraints.md — they present similarly but have different root causes.

Touch controllers (GT911, FT5x06, FT6336U) have their own swap_xy/invert_x/invert_y settings that are physically unrelated to the display driver’s orientation. These must be re-derived from scratch on real hardware every time the display orientation changes, including between PORTRAIT and PORTRAIT_INVERTED variants. There is no formula that maps display swap_xy/mirror bits to touch config bits. See display_drivers.md §Touch axis tuning.

If DISPLAY_SWAP_COLOR_FLAG is set, the byte-swap operation must target a separate scratch buffer, not the caller’s pixel map. The BSP calls lv_draw_sw_rgb565_swap() on the px_map pointer before the VSync wait; if LVGL reuses the same buffer across multiple lvgl_flush_cb() calls (e.g. for multi-row fills), an in-place swap will corrupt every other call. See display_drivers.md §gfx.c pixel-count convention for the related gfx.c byte-order caveat.

Orientation Isolation: CMake → Driver → UI Must Stay in Sync

Section titled “Orientation Isolation: CMake → Driver → UI Must Stay in Sync”

On boards that wire DEFAULT_ORIENTATION through CMake (e.g. esp32s3_4827s043c), the orientation field of the config struct is derived from a CMake macro chain. If the CMake-level value and the hardcoded C value are ever allowed to diverge, the panel will scan in the wrong direction while LVGL renders in the other — a failure mode that compiles silently. Always drive orientation from a single CMake source of truth. See display_drivers.md §UI-layer orientation.

Physical Resolution in panel_config.timings Must Match the Glass

Section titled “Physical Resolution in panel_config.timings Must Match the Glass”

panel_config.timings.h_res and v_res are not software-configurable window sizes — they are the hardware pixel matrix counts of the physical glass. Setting them to the logical (rotated) resolution will produce incorrect GDMA timing and a corrupted image. For a 90°-rotated mount, timings.h_res = physical width (columns), timings.v_res = physical height (rows) — these are swapped relative to the logical hor_res/ver_res.

PSRAM Allocation Can Fail Under Fragmentation

Section titled “PSRAM Allocation Can Fail Under Fragmentation”

Draw buffers for RGB panels are large (an 800×480 RGB565 buffer is ~768 KB). These must be allocated from PSRAM (MALLOC_CAP_SPIRAM). The allocation is performed in init_lvgl() with a logged failure path — always check the log and the largest free PSRAM block before concluding that “there is enough memory.” See esp32_memory_constraints.md.


DocumentRelevance
display_drivers.mdPrimary cross-family reference: SPI vs RGB vs I80 comparison, orientation math (determinant proof), physical vs logical resolution, PCLK cost, touch tuning, gfx.c pixel-count convention, UI-layer orientation (ESP3D_DYNAMIC_ROTATION_FEATURE), board reference table
display_drivers_spi.mdSPI panel driver details (ILI9341, ST7796) — MADCTL, GRAM, zero-cost orientation
display_drivers_i80.mdIntel 8080 parallel driver details (RM68120, ST7796-i80) — notify_flush_ready, i80_flush_ready_cb
shared_sd_mechanism_V2.0.mdSD card shared-bus protocol for RGB boards with pin-muxed SD (bsp_accessFs/bsp_releaseFs)
screens_architecture.mdLVGL screen system that consumes the display initialised by this module
esp32_memory_constraints.mdPSRAM fragmentation, heap constraints, draw-buffer allocation strategy
board_build_guidelines.mdHow to port a new board, including RGB panel orientation bring-up checklist