Skip to content

Display Drivers — SPI (`display_drivers_spi`)

Display Drivers — SPI (display_drivers_spi)

Section titled “Display Drivers — SPI (display_drivers_spi)”

The display_drivers_spi module is the SPI panel-controller driver layer of the Hardware Abstraction Layer (HAL). It provides two custom ESP-IDF-compatible LCD panel drivers — one for the ILI9341 and one for the ST7796S controller — along with a shared backlight configuration type. Both drivers implement the standard esp_lcd_panel_t virtual-function interface, allowing all higher-level code (BSP, LVGL) to drive either display controller through the same uniform API.

What this module is responsible for:

  • Allocating, initializing, and owning the esp_lcd_panel_t handle for each supported SPI controller
  • Managing the SPI bus and DMA pipeline for pixel transfer
  • Implementing MADCTL-based orientation (0°/90°/180°/270°) at zero per-frame CPU cost
  • Providing the on_color_trans_done ISR callback hook that releases LVGL’s render buffer after a DMA transfer completes
  • Exposing a backlight_config_t type for GPIO or PWM-based backlight control

What this module is not responsible for:


calls configure / get_panel_handle

wires lvgl_flush_cb

may share SPI bus with

Hardware_Abstraction_Layer

bsp

display_drivers_spi ← current module

display_drivers_i80

display_drivers_rgb

touch_controllers

bsp_board_initialization

io_expanders

bus_drivers

physical_inputs


hardware/common/drivers/
├── disp_ili9341/
│ ├── disp_ili9341_config.h spi_ili9341_config_t, spi_ili9341_orientation_t
│ └── disp_ili9341_spi.c ILI9341 panel driver + ili9341_spi_configure() entry point
├── disp_st7796/
│ ├── disp_st7796_config.h spi_st7796_config_t, spi_st7796_orientation_t (modern API)
│ ├── st7796.h esp_spi_st7796_config_t, esp_spi_bus_st7796_config_t
│ └── st7796.c ST7796S panel driver + esp_lcd_new_panel_st7796()
└── disp_backlight/
└── disp_backlight_config.h backlight_config_t (GPIO or LEDC PWM)

display_drivers_spi

ST7796S (disp_st7796/)

spi_st7796_config_t

(disp_st7796_config.h)

esp_lcd_new_panel_st7796()

(public entry point)

esp_spi_st7796_config_t

(st7796.h)

lcd_panel_t

(internal state)

ILI9341 (disp_ili9341/)

spi_ili9341_config_t

(disp_ili9341_config.h)

ili9341_spi_configure()

(public entry point)

ili9341_init_cmd_t

(init command table)

ili9341_panel_t

(internal state)

ili9341_spi_get_panel_handle()

ili9341_spi_get_io_handle()

backlight_config_t

(disp_backlight_config.h)

Both ili9341_panel_t and lcd_panel_t (the ST7796 internal struct) are private heap allocations that extend esp_lcd_panel_t as their first member, making them safe to cast via __containerof. All panel operations are dispatched through the esp_lcd_panel_t vtable and never called directly.


ControllerTypical resolutionBoards
ILI9341240 × 320pibot_pendant_v1_0, esp32_2432s028r
ST7796S320 × 480esp32_3248s035c, esp32_3248s035r, esp32s3_bzm_tft35_gt911

Key board notes:

  • pibot_pendant_v1_0 — The primary pendant target. ILI9341 in portrait orientation. Also has a standalone ILI9341 driver in boards/pibot_pendant_v1_0/Factory/main/ili9341.c for the factory app (independent of this module).
  • esp32_2432s028r — ILI9341 shares the SPI bus with the XPT2046 touch controller (spi_bus.is_master = false).
  • esp32_3248s035r / esp32_3248s035c — ST7796S with a MADCTL quirk (0x88) that bypasses the orientation enum. Documented in display_drivers.md.
  • esp32s3_bzm_tft35_gt911 — ST7796S with GT911 touch (I²C); orientation not yet hardware-verified.

ili9341_spi_configure() owns the full lifecycle: SPI bus, panel IO, panel create, reset, init, orientation, and backlight.

yes

no

yes

no

ili9341_spi_configure(&config)

gpio_config() - backlight GPIO

initially OFF

spi_bus.is_master?

spi_bus_initialize()

esp_lcd_new_panel_io_spi() → io_handle

esp_lcd_new_panel_ili9341()

alloc ili9341_panel_t

wire vtable

esp_lcd_panel_reset()

HW GPIO pulse or SW SWRESET

esp_lcd_panel_init()

power/timing/gamma table

MADCTL → COLMOD → SLPOUT → 120 ms

esp_lcd_panel_swap_xy()

esp_lcd_panel_mirror()

per orientation enum

invert_colors?

esp_lcd_panel_invert_color()

esp_lcd_panel_disp_on_off(true)

DISPON (0x29)

gpio_set_level() - backlight ON

return ESP_OK

panel_handle stored in static

After this call the BSP retrieves the panel handle with ili9341_spi_get_panel_handle() and passes it to init_lvgl().

ST7796S — Caller-managed bus + factory create pattern

Section titled “ST7796S — Caller-managed bus + factory create pattern”

The BSP must initialize the SPI bus and create the panel IO handle before calling esp_lcd_new_panel_st7796(), which then performs reset, init, and orientation atomically.

yes

no

yes

no

BSP (board_init.c):

spi_bus_initialize()

esp_lcd_new_panel_io_spi()

esp_lcd_new_panel_st7796(io, &cfg, &panel)

calloc(lcd_panel_t)

wire vtable

esp_lcd_panel_reset()

esp_lcd_panel_init()

SLPOUT → MADCTL → COLMOD

Gamma ext cmd2 → DISPON

landscape or

landscape_invert?

esp_lcd_panel_swap_xy(true)

portrait_invert or

landscape_invert?

esp_lcd_panel_mirror(true, true)

return ESP_OK

handle via *disp_panel


Both drivers use the MADCTL register (0x36) to set orientation via three bits:

BitMaskMeaning
MVLCD_CMD_MV_BITSwap row/column (axis transpose)
MXLCD_CMD_MX_BITMirror X (column address direction)
MYLCD_CMD_MY_BITMirror Y (row address direction)

ILI9341 orientation table (applied by ili9341_spi_configure()):

Enumswap_xymirror_xmirror_y
PORTRAIT✗✓✗
LANDSCAPE✓✓✓
PORTRAIT_INVERTED✗✗✓
LANDSCAPE_INVERTED✓✗✗

ST7796 orientation table (applied inside esp_lcd_new_panel_st7796()):

Enumswap_xymirror (X, Y)
portrait✗(✗, ✗)
landscape✓(✗, ✗)
portrait_invert✗(✓, ✓)
landscape_invert✓(✓, ✓)

⚠️ swap_xy without any mirror is a reflection, not a rotation — the landscape ST7796 case may require a board-specific MADCTL override. See display_drivers.md § Orientation math for the proof and per-board workaround values.


SPI panel controllers hold their own GRAM, so the MCU only needs to push changed pixel rectangles via DMA. The panel then autonomously scan-outs its GRAM to the glass, independent of SPI activity.

notify_lvgl_flush_ready (BSP)on_color_trans_done (ISR)SPI DMApanel_*_draw_bitmap()lvgl_flush_cb (BSP)LVGL (Core 1)notify_lvgl_flush_ready (BSP)on_color_trans_done (ISR)SPI DMApanel_*_draw_bitmap()lvgl_flush_cb (BSP)LVGL (Core 1)Async DMA transfer in progressRender buffer released - next frame may startflush_cb(drv, area, color_map)esp_lcd_panel_draw_bitmap(panel, x0,y0,x1,y1, data)CASET (0x2A) + RASET (0x2B) + RAMWR (0x2C) + pixel dataTransaction complete interruptnotify_lvgl_flush_ready()lv_disp_flush_ready(drv)

Flush signaling models across SPI boards:

ModelBoardsMechanism
Async (preferred)All SPI boards in this repoon_color_trans_done callback in panel IO config → notify_lvgl_flush_ready() → lv_disp_flush_ready()
SynchronousLegacy / simple boardslv_disp_flush_ready() called directly at end of lvgl_flush_cb(), blocking until SPI transfer completes

The async model lets LVGL prepare the next render buffer while DMA drains the current one — critical for maintaining smooth 30+ FPS on a constrained SPI clock.

⚠️ LVGL thread constraint: lv_disp_flush_ready() must only be called from the LVGL task (Core 1) or via an ISR-safe wrapper. Never call it directly from a SPI ISR without the notify_lvgl_flush_ready() indirection.


backlight_config_t (from disp_backlight_config.h) is the shared type for backlight control used across all SPI boards. It supports both simple GPIO and PWM dimming via LEDC.

Fieldpwm_control = falsepwm_control = true
gpio_num✓ used✓ used
output_invert✓ used✓ used
timer_idx, channel_idxignored✓ used
duty, freq_hz, resolution_bitsignored✓ used

The ILI9341 driver manages its own backlight inline (simple gpio_set_level()) using the backlight sub-struct of spi_ili9341_config_t. backlight_config_t is available for BSPs that manage backlight independently of the panel driver.


Both drivers implement the identical esp_lcd_panel_t vtable:

OperationILI9341 functionST7796 functionSPI command(s)
Deletepanel_ili9341_dellcd_panel_del— (free only)
Resetpanel_ili9341_resetlcd_panel_reset0x01 (SW)
Initpanel_ili9341_initlcd_panel_init0x11,0x36,0x3A,0xE0,0xE1,0x29
Draw bitmappanel_ili9341_draw_bitmaplcd_panel_draw_bitmap0x2A, 0x2B, 0x2C
Mirrorpanel_ili9341_mirrorlcd_panel_mirror0x36
Swap XYpanel_ili9341_swap_xylcd_panel_swap_xy0x36
Invert colorpanel_ili9341_invert_colorlcd_panel_invert_color0x20/0x21
Set gappanel_ili9341_set_gaplcd_panel_set_gap— (stored, used in draw)
Display on/offpanel_ili9341_disp_on_offlcd_panel_disp_on_off0x28/0x29

For per-function documentation, configuration field descriptions, init command table breakdown, and public API signatures, see:

➡ bsp_display_drivers_spi.md — full component reference for all three drivers in this module


DocumentContent
display_drivers.mdSPI vs. RGB panel family comparison; orientation/rotation math; PCLK cost of swap_xy; gfx.c pixel-count convention; ESP3D_DYNAMIC_ROTATION_FEATURE; full board reference table
bsp_display_drivers_spi.mdPer-driver component reference: config structs, panel structs, operation tables, init flow diagrams, public API
bsp_display_drivers_i80.mdI80 parallel bus drivers (RM68120, ST7796 i80)
bsp_display_drivers_rgb.mdRGB parallel drivers (EK9716, ILI9485, ST7262)
bsp_board_initialization.mdPer-board board_init.c — calls these drivers and wires lvgl_flush_cb
bsp_board_initialization_display.mdDisplay-specific BSP init: flush callbacks, vsync events, flush-ready signaling
bsp_touch_controllers.mdTouch controllers — orientation must be calibrated independently from display
guides/board_build_guidelines.mdPorting steps for new SPI boards