Skip to content

BSP Display Drivers — Intel 8080 (i80) Parallel Bus

BSP Display Drivers — Intel 8080 (i80) Parallel Bus

Section titled “BSP Display Drivers — Intel 8080 (i80) Parallel Bus”

The bsp_display_drivers_i80 module provides vendor-specific LCD panel drivers for displays connected via the Intel 8080 (i80) 8-bit or 16-bit parallel bus, also known as the 8080 parallel interface. These drivers are used on ESP32-S3 boards where display bandwidth requirements exceed what SPI can deliver, but where the more demanding RGB parallel interface is not available or needed.

Two controller families are supported:

DriverControllerBoard
disp_rm68120RM68120 (16-bit i80 command encoding)esp32s3_hmi43v3
disp_st7796_i80ST7796 (standard 8-bit i80)esp32s3_zx3d50ce02s_usrc_4832

Both drivers sit within the BSP hardware abstraction layer and wrap the ESP-IDF esp_lcd framework. The SPI-based counterparts to these drivers are documented in bsp_display_drivers_spi.md. RGB parallel panel drivers live in bsp_display_drivers_rgb.md.


Each i80 driver is split into two layers:

Panel Layer (rm68120_panel.c / st7796_i80_panel.c)

Wrapper Layer (disp_rm68120.c / disp_st7796_i80.c)

BSP Layer (board_init.c)

esp_lcd_panel_draw_bitmap

flush_ready_cb

lv_display_flush_ready

board_init

disp_xxx_configure

init_lvgl

lvgl_flush_cb

Panel Handle

esp_lcd_new_i80_bus

esp_lcd_new_panel_io_i80

esp_lcd_new_panel_xxx

on_color_trans_done ISR

i80_flush_ready_cb

LVGL

panel_init

panel_draw_bitmap

panel_reset

panel_mirror / swap_xy

panel_del

  • Wrapper layer — Creates the i80 bus, panel IO, and panel objects. Installs the ISR callback that routes the DMA transfer-complete interrupt to LVGL’s flush-ready signal. Exposes _get_panel_handle() and _get_io_handle() accessors for the BSP init_lvgl function.
  • Panel layer — Implements the standard esp_lcd_panel_t virtual-dispatch interface (init, reset, draw_bitmap, mirror, swap_xy, set_gap, invert_color, disp_on_off, del). Each function sends the appropriate controller commands over the i80 IO handle.

ST7796_I80

RM68120

disp_rm68120_config.h

disp_rm68120_config_t

disp_rm68120_orientation_t

disp_rm68120_flush_ready_cb_t

disp_rm68120.c

disp_rm68120_configure()

disp_rm68120_get_io_handle()

disp_rm68120_get_panel_handle()

rm68120_panel.c

rm68120_panel_t

esp_lcd_new_panel_rm68120()

panel_rm68120_*()

disp_st7796_i80_config.h

disp_st7796_i80_config_t

disp_st7796_i80_orientation_t

disp_st7796_i80_flush_ready_cb_t

disp_st7796_i80.c

disp_st7796_i80_configure()

disp_st7796_i80_get_io_handle()

disp_st7796_i80_get_panel_handle()

st7796_i80_panel.c

st7796_i80_panel_t

esp_lcd_new_panel_st7796_i80()

panel_st7796_i80_*()

esp32s3_hmi43v3

board_init.c

esp32s3_zx3d50ce02s_usrc_4832

board_init.c


Both drivers use configuration structures that bundle the three native ESP-IDF esp_lcd config sub-structures. Pin assignments are not stored here — they come from board-specific constants defined in board_init.c and passed in at init time.

// hardware/drivers_video_i80/disp_rm68120/disp_rm68120_config.h
typedef struct {
esp_lcd_i80_bus_config_t bus_config; // Data pins, clock frequency, data width
esp_lcd_panel_io_i80_config_t io_config; // CS, DC, command bits, color space
esp_lcd_panel_dev_config_t panel_config; // Reset GPIO, RGB order, bits-per-pixel
disp_rm68120_orientation_t orientation; // PORTRAIT / LANDSCAPE / *_INVERTED
uint16_t hor_res; // Horizontal resolution (pixels)
uint16_t ver_res; // Vertical resolution (pixels)
} disp_rm68120_config_t;
typedef void (*disp_rm68120_flush_ready_cb_t)(void); // ISR-safe flush-complete signal
// hardware/drivers_video_i80/disp_st7796_i80/disp_st7796_i80_config.h
typedef struct {
esp_lcd_i80_bus_config_t bus_config;
esp_lcd_panel_io_i80_config_t io_config;
esp_lcd_panel_dev_config_t panel_config;
disp_st7796_i80_orientation_t orientation;
uint16_t hor_res;
uint16_t ver_res;
} disp_st7796_i80_config_t;
typedef void (*disp_st7796_i80_flush_ready_cb_t)(void);

Both structures are identical in shape and differ only in the orientation enum type, allowing board authors to follow the same pattern when porting to a new i80 board.

Enum valueRotationMADCTL MVNotes
*_ORIENTATION_PORTRAIT0°—Native panel orientation
*_ORIENTATION_LANDSCAPE90°setWidth ↔ Height swap
*_ORIENTATION_PORTRAIT_INVERTED180°—MY + MX set
*_ORIENTATION_LANDSCAPE_INVERTED270°setMY + MX + MV

Both wrappers expose a three-function public API following the same convention.

// hardware/drivers_video_i80/disp_rm68120/disp_rm68120.c
esp_err_t disp_rm68120_configure(
const disp_rm68120_config_t *config,
disp_rm68120_flush_ready_cb_t flush_ready_cb);
esp_lcd_panel_handle_t disp_rm68120_get_panel_handle(void);
esp_lcd_panel_io_handle_t disp_rm68120_get_io_handle(void);
// hardware/drivers_video_i80/disp_st7796_i80/disp_st7796_i80.c
esp_err_t disp_st7796_i80_configure(
const disp_st7796_i80_config_t *config,
disp_st7796_i80_flush_ready_cb_t flush_ready_cb);
esp_lcd_panel_handle_t disp_st7796_i80_get_panel_handle(void);
esp_lcd_panel_io_handle_t disp_st7796_i80_get_io_handle(void);

The configure function is called once during board_init. Calling it a second time returns ESP_FAIL immediately — both wrappers hold a static _panel_handle pointer and guard against re-initialization. The two get_* functions return the statically stored handles and log an error if called before configure.


typedef struct {
esp_lcd_panel_t base; // Must be first — vtable pointer
esp_lcd_panel_io_handle_t io;
int reset_gpio_num;
bool reset_level;
uint16_t width; // Logical width after orientation mapping
uint16_t height; // Logical height after orientation mapping
uint8_t dir; // scr_dir_t — one of 8 scan directions
int x_gap;
int y_gap;
unsigned int bits_per_pixel;
uint8_t madctl_val; // MADCTL register (0x3600)
uint8_t colmod_cal; // COLMOD register (0x3A00)
} rm68120_panel_t;

The dir field tracks one of eight scr_dir_t scan directions (e.g., SCR_DIR_LRTB, SCR_DIR_TBLR) derived from the chosen orientation at construction time. This is an RM68120-specific concept: the controller’s scan direction and the MADCTL register are set together to produce correct pixel ordering for each rotation, with width and height swapped for landscape orientations.

typedef struct {
esp_lcd_panel_t base;
esp_lcd_panel_io_handle_t io;
int reset_gpio_num;
bool reset_level;
int x_gap;
int y_gap;
unsigned int bits_per_pixel;
uint8_t madctl_val; // MADCTL register (LCD_CMD_MADCTL)
uint8_t colmod_cal; // COLMOD register (LCD_CMD_COLMOD)
} st7796_i80_panel_t;

The ST7796 structure is simpler — orientation is applied by the wrapper layer through esp_lcd_panel_swap_xy after panel_init, rather than being encoded inside the panel struct at construction time.


The flush cycle is the critical real-time path. It must complete promptly to keep LVGL’s tick timer from stalling the UI task on Core 1.

i80_flush_ready_cbon_color_trans_done (ISR)I80 DMA Enginepanel_draw_bitmaplvgl_flush_cb (BSP)LVGL (Core 1)i80_flush_ready_cbon_color_trans_done (ISR)I80 DMA Enginepanel_draw_bitmaplvgl_flush_cb (BSP)LVGL (Core 1)Render complete, dirty lv_area_tOptional RGB565 byte-swap (DISPLAY_SWAP_COLOR_FLAG)esp_lcd_panel_draw_bitmap(x1, y1, x2+1, y2+1, px_map)Send CASET / RASET window registersesp_lcd_panel_io_tx_color(RAMWR, buf, len)DMA transfer complete interruptflush_ready_cb() via user_ctxlv_display_flush_ready(lvgl_display)Mark display ready, next frame may begin

Key points:

  • on_color_trans_done fires from ISR context (i80 bus DMA interrupt). The stored flush_ready_cb must be ISR-safe. lv_display_flush_ready is ISR-safe in LVGL v9.
  • esp_lcd_panel_draw_bitmap blocks while sending the CASET/RASET command parameters, then starts the DMA pixel transfer asynchronously and returns.
  • LVGL must not modify px_map until lv_display_flush_ready is received.
  • When the snapshot feature is enabled, lvgl_flush_cb intercepts px_map before the draw call to capture raw pixel data to a file on the filesystem.

The two controllers differ fundamentally in how commands are addressed over the i80 bus:

The RM68120 uses a 16-bit command word where the high byte is the register address and the low byte is the sub-index for multi-byte registers. Commands are constructed with the << 8 shift pattern:

// CASET split into 4 separate single-byte writes (MSB first)
esp_lcd_panel_io_tx_param(io, (LCD_CMD_CASET << 8) + 0, (uint16_t[]){(x_start >> 8) & 0xFF}, 2);
esp_lcd_panel_io_tx_param(io, (LCD_CMD_CASET << 8) + 1, (uint16_t[]){x_start & 0xFF}, 2);
esp_lcd_panel_io_tx_param(io, (LCD_CMD_CASET << 8) + 2, (uint16_t[]){((x_end-1) >> 8) & 0xFF}, 2);
esp_lcd_panel_io_tx_param(io, (LCD_CMD_CASET << 8) + 3, (uint16_t[]){(x_end-1) & 0xFF}, 2);
// Pixel data
esp_lcd_panel_io_tx_color(io, LCD_CMD_RAMWR << 8, color_data, len);

This 16-bit encoding requires bus_config to be set up for 16-bit data width and the io_config.lcd_cmd_bits to match the controller’s addressing model.

The ST7796 uses standard single-byte LCD commands, consistent with the SPI variant of the same controller. Column and row addresses are packed into a single 4-byte parameter transaction:

// CASET as a single 4-byte parameter block
esp_lcd_panel_io_tx_param(io, LCD_CMD_CASET,
(uint8_t[]){
(x_start >> 8) & 0xFF, x_start & 0xFF,
((x_end - 1) >> 8) & 0xFF, (x_end - 1) & 0xFF,
}, 4);
esp_lcd_panel_io_tx_color(io, LCD_CMD_RAMWR, color_data, len);

The RM68120 requires an extensive vendor-specific register sequence in rm68120_reg_config() before the panel can be used:

esp_lcd_new_panel_rm68120

Allocate rm68120_panel_t

Configure reset GPIO

Map orientation to dir + madctl_val + width/height swap

Set colmod_cal from bits_per_pixel

Wire vtable function pointers

Return panel handle

panel_rm68120_reset

GPIO toggle 20ms each edge

or SW SWRESET + 20ms

panel_rm68120_init

rm68120_reg_config

page enable + gamma tables

+ power + VCOM + timing

SLPOUT + 100ms delay

MADCTL - scan direction

COLMOD - pixel format

DISPON

rm68120_reg_config sends approximately 200 register writes across multiple vendor pages:

Register rangePurpose
F000–F004Page enable (unlock vendor registers)
D1xx–D6xx6-channel gamma correction curves (52 points each, R/G/B positive and negative)
B0xx–B2xxAVDD voltage setting
B1xxAVEE voltage setting
B6xx–B9xxAVDD / AVEE / VCL boosting
BAxxVGLX voltage
BCxx–BDxxGamma reference voltages (VGMP / VGMN)
BE01VCOM level
B700, C80xGate EQ control and display timing
3500TE (tearing effect) signal enable
3A00COLMOD (pixel format)
3600Memory Data Access Control (MADCTL)

The ST7796 uses a minimal sequence, identical in structure to its SPI counterpart:

esp_lcd_new_panel_st7796_i80

Allocate st7796_i80_panel_t

Configure reset GPIO

Map RGB order to madctl_val

Set colmod_cal from bits_per_pixel

Wire vtable function pointers

Return panel handle

panel_st7796_i80_reset

GPIO toggle 10ms each edge

or SW SWRESET + 20ms

panel_st7796_i80_init

SLPOUT + 100ms

MADCTL

COLMOD

DISPON

Wrapper: invert_color true

Wrapper: swap_xy if landscape


All operations follow the esp_lcd_panel_t vtable contract. Both drivers implement the full set:

OperationRM68120 functionST7796 i80 functionNotes
Deletepanel_rm68120_delpanel_st7796_i80_delReleases reset GPIO, frees struct
Resetpanel_rm68120_resetpanel_st7796_i80_resetHW (GPIO) or SW (SWRESET)
Initpanel_rm68120_initpanel_st7796_i80_initSends vendor init sequence
Draw bitmappanel_rm68120_draw_bitmappanel_st7796_i80_draw_bitmapCASET → RASET → RAMWR
Invert colorpanel_rm68120_invert_colorpanel_st7796_i80_invert_colorINVON / INVOFF
Mirrorpanel_rm68120_mirrorpanel_st7796_i80_mirrorMX / MY bits in MADCTL
Swap XYpanel_rm68120_swap_xypanel_st7796_i80_swap_xyMV bit in MADCTL
Set gappanel_rm68120_set_gappanel_st7796_i80_set_gapCoordinate offset (x_gap, y_gap)
Display on/offpanel_rm68120_disp_offpanel_st7796_i80_disp_offDISPON / DISPOFF

Draw Bitmap — Window Address Calculation

Section titled “Draw Bitmap — Window Address Calculation”

Both drivers apply the x_gap/y_gap offset before sending the window address:

effective_x_start = x_start + x_gap
effective_y_start = y_start + y_gap
effective_x_end = x_end + x_gap (stored as x_end - 1 in CASET register)
effective_y_end = y_end + y_gap (stored as y_end - 1 in RASET register)
transfer_bytes = (x_end - x_start) × (y_end - y_start) × bits_per_pixel / 8

The gap mechanism handles physical vs. logical resolution offsets when a panel’s active area does not start at controller address 0,0. See docs/architecture/display_drivers.md for the broader orientation and gap design context.


board_init()
├── activity_manager_init()
├── init_io_expander() ← TCA9554/CH422G: reset/power-enable lines
├── disp_rm68120_configure( ← create i80 bus + RM68120 panel
│ &disp_rm68120_default_config,
│ i80_flush_ready_cb)
├── init_touch_controller() ← see bsp_touch_controllers.md
└── init_lvgl()
├── lv_init()
├── disp_rm68120_get_panel_handle() ← retrieve handle for LVGL user_data
├── lv_display_create(WIDTH, HEIGHT)
├── heap_caps_malloc(DMA) × 1 or 2 ← DMA-capable SRAM draw buffers
├── lv_display_set_flush_cb(lvgl_flush_cb)
└── esp_timer periodic (LVGL tick)

The IO expander (init_io_expander) on this board controls power-enable and reset lines for the RM68120. The i80 data bus pins are wired directly to the ESP32-S3 GPIO matrix. See bsp_io_expanders.md for the IO expander driver.

esp32s3_zx3d50ce02s_usrc_4832 — ST7796 i80

Section titled “esp32s3_zx3d50ce02s_usrc_4832 — ST7796 i80”
board_init()
├── activity_manager_init()
├── backlight_configure() ← PWM backlight (optional)
│ backlight_set(0) ← start dark, avoid flash on boot
├── disp_st7796_i80_configure( ← create i80 bus + ST7796 panel
│ &disp_st7796_i80_default_config,
│ i80_flush_ready_cb)
├── init_touch_controller()
└── init_lvgl()
├── lv_init()
├── disp_st7796_i80_get_panel_handle()
└── ... (identical LVGL setup pattern)

calls configure

esp_lcd_new_i80_bus

esp_lcd_new_panel_io_i80

lv_display_flush_ready

esp3d_log_e / esp3d_log

gpio_config / gpio_set_level

vTaskDelay

IO lines / reset

backlight PWM

touch read

bsp_bsp_board_initialization

bsp_display_drivers_i80

ESP-IDF esp_lcd

LVGL v9

esp3d_log

ESP-IDF GPIO

FreeRTOS

bsp_io_expanders

disp_backlight

bsp_touch_controllers


LVGL draw buffers must be allocated with MALLOC_CAP_DMA because the i80 DMA engine reads directly from them:

lvgl_buf1 = heap_caps_malloc(draw_buf_size, MALLOC_CAP_DMA);

On ESP32-S3, DMA-capable memory is internal SRAM. A partial render mode (LV_DISPLAY_RENDER_MODE_PARTIAL) is used on both boards to keep buffer size manageable given ESP32 SRAM constraints. Double-buffering (DISPLAY_USE_DOUBLE_BUFFER_FLAG) can be enabled to allow LVGL to render into one buffer while the DMA engine transfers the other.

i80_flush_ready_cb executes in interrupt context:

static void i80_flush_ready_cb(void) {
lv_display_flush_ready(lvgl_display); // ISR-safe in LVGL v9
}

The internal disp_*_notify_flush_ready shim, registered as on_color_trans_done in esp_lcd_panel_io_i80_config_t.user_ctx, casts the context pointer back to disp_*_flush_ready_cb_t and calls it. This function must not invoke any blocking FreeRTOS API (no xQueueSend, vTaskDelay, xSemaphoreTake, etc.).

Both boards conditionally apply a byte-swap on the RGB565 pixel data before calling esp_lcd_panel_draw_bitmap:

if (DISPLAY_SWAP_COLOR_FLAG) {
lv_draw_sw_rgb565_swap(px_map, pixel_count);
}

This compensates for endianness differences between LVGL’s internal color representation and what the i80 controller expects. Whether the swap is needed depends on the board’s wiring and controller configuration.


AspectRM68120ST7796 i80
Command word width16-bit (cmd << 8)8-bit (standard)
Init sequence length~200 register writes (gamma tables + power)~5 commands
Orientation handlingInternal (dir field, 8 scan directions, MADCTL per orientation)swap_xy / mirror applied by wrapper after init
Color inversionExplicit INVON / INVOFFWrapper calls invert_color(true) after init
Bits-per-pixel16 / 18 / 2416 / 18 / 24
RGB element orderRGB or BGR via madctl_valRGB or BGR via madctl_val
Reset pulse width20 ms each edge10 ms each edge
Used on boardesp32s3_hmi43v3esp32s3_zx3d50ce02s_usrc_4832