Skip to content

Factory LCD Drivers

The factory_lcd_drivers module provides board-specific display drivers used exclusively by the Factory Application — the minimal ESP-IDF app flashed at manufacturing time. Each driver is a thin, self-contained C file that initialises a panel controller, drives the backlight, and blits rectangular pixel regions coming from the Factory App’s factory_graphics rasterizer (gfx.c).

These drivers are intentionally isolated from the main firmware stack: they carry no LVGL dependency, no esp3d_log calls, and no esp_lcd vendor-driver layer where a raw SPI master suffices. This keeps the factory binary small and fully bootable from a fresh, uninitialised flash.

Three distinct hardware interface families are covered across all supported boards:

InterfaceControllerBoards
SPIILI9341pibot_pendant_v1_0
SPIST7796esp32_3248s035c · esp32_3248s035r · esp32s3_bzm_tft35_gt911
Intel 8080 (I80)RM68120esp32s3_hmi43v3
Intel 8080 (I80)ST7796 I80esp32s3_zx3d50ce02s_usrc_4832
RGB ParallelST7262esp32s3_8048s043c · esp32s3_8048s050c

Scope note: The factory_lcd_drivers node in the module tree explicitly lists the ST7796-SPI and ST7262 files. The RM68120, ST7796-I80, and ILI9341 drivers are siblings inside factory_hardware_drivers but serve the identical architectural role and are documented together here for a complete picture.


ESP-IDF

Shared Hardware Components

Factory LCD Drivers

Factory Application

flush(x0,y0,x1,y1,data,len)

flush(...)

flush(...)

flush(...)

flush(...)

*_init() / *_backlight()

*_init() / *_backlight()

*_init() / *_backlight()

*_init() / *_backlight()

*_init() / *_backlight()

gfx.c

(pixel rasterizer)

main.c

(app_main)

ili9341.c

SPI · pibot_pendant_v1_0

st7796.c

SPI · 3248s035c/r · bzm_tft35

rm68120.c

I80 · hmi43v3

st7796_i80.c

I80 · zx3d50ce02s

st7262.c

RGB · 8048s043c/050c

disp_st7262

hardware/drivers_video_rgb

disp_rm68120

hardware/drivers_video_i80

disp_st7796_i80

hardware/drivers_video_i80

spi_master driver

esp_lcd_rgb_panel

gpio driver


All SPI-connected panels share the same low-level pattern: a static spi_device_handle_t, a DC (data/command) GPIO, and three primitives built on spi_device_polling_transmit.

«SPI panel family»

SPIDriver

-spi_device_handle_t spi_dev

-lcd_cmd(uint8_t cmd)

-lcd_data(const uint8_t* data, int len)

-lcd_data_byte(uint8_t val)

+init() : esp_err_t

+backlight(bool on)

+flush(x0,y0,x1,y1,data*,len)

ILI9341Driver

board: pibot_pendant_v1_0

reset: software(cmd 0x01)

inversion: ON(cmd 0x21)

gamma: minimal(none)

ST7796Driver_3248s035c

board: esp32_3248s035c

gamma: extended cmd set

reset: HW or SW(RST may be NC)

pin_bit() : guard for NC pin

ST7796Driver_3248s035r

board: esp32_3248s035r

gamma: extended cmd set

reset: hardware only(RST wired)

ST7796Driver_bzm

board: esp32s3_bzm_tft35_gt911

gamma: extended cmd set

reset: hardware only(RST wired)

FunctionDC pin statePurpose
lcd_cmd(cmd)LOW (0)Send a one-byte command register address
lcd_data(data, len)HIGH (1)Send a multi-byte data payload
lcd_data_byte(val)HIGH (1)Convenience wrapper — calls lcd_data(&val, 1)

These three helpers are static within each driver file and are never exposed outside it.

The esp32_3248s035c panel variant has no RST line (TFT_RST = GPIO_NUM_NC). Using a compile-time constant GPIO_NUM_NC (which is -1) directly in a shift expression 1ULL << PIN triggers -Wshift-count-negative even inside a runtime GPIO_IS_VALID_GPIO() guard. The pin_bit() helper resolves this:

static uint64_t pin_bit(gpio_num_t pin) {
return GPIO_IS_VALID_GPIO(pin) ? (1ULL << pin) : 0ULL;
}

When RST is GPIO_NUM_NC the driver falls back to a software reset command (0x01) followed by a 150 ms delay, rather than toggling the pin. The esp32_3248s035r and esp32s3_bzm_tft35_gt911 variants have RST wired and do not need this guard.

StepILI9341ST7796
ResetSW reset (0x01) + 150 msHW RST pin (10 ms low / 120 ms high)
Display inversionON (cmd 0x21)Not needed
Gamma controlNoneExtended cmd 0xF0 + tables 0xE0 / 0xE1
Pixel formatRGB565 (0x55)RGB565 (0x55)
MADCTL rotation4-entry lookup tableSame 4-entry lookup table

2 · Intel 8080 (I80) Family (RM68120 / ST7796-I80)

Section titled “2 · Intel 8080 (I80) Family (RM68120 / ST7796-I80)”

Boards esp32s3_hmi43v3 and esp32s3_zx3d50ce02s_usrc_4832 use I80 parallel-bus controllers. The factory driver for these boards does not reimplement the panel protocol; instead it delegates to the shared BSP components from hardware/drivers_video_i80 (see bsp_display_drivers_i80) and contributes only one file: an ISR-safe flush-completion callback.

IRAM_ATTR callbackdisp_rm68120 / disp_st7796_i80Factory I80 Drivergfx.cIRAM_ATTR callbackdisp_rm68120 / disp_st7796_i80Factory I80 Drivergfx.cI80 DMA transfer startsflush(x0, y0, x1, y1, data, len)esp_lcd_panel_draw_bitmap()transfer-complete interrupt firesxSemaphoreGiveFromISR()semaphore acquired, flush returns

Both I80 callbacks follow an identical ISR-safe pattern:

static void IRAM_ATTR rm68120_flush_ready_cb(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(s_flush_done_sem, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

IRAM_ATTR is mandatory — these callbacks execute in an interrupt context. Placing them in flash (the default) would cause an instruction-cache fault if a cache miss occurs during the ISR.


Boards esp32s3_8048s043c and esp32s3_8048s050c use a 16-bit RGB parallel interface driven by esp_lcd_rgb_panel. The factory driver is a thin wrapper over the shared disp_st7262 component from hardware/drivers_video_rgb (see bsp_display_drivers_rgb) — the identical component used by the main firmware for these boards.

Key differences from the SPI family:

PropertySPI / I80RGB Parallel
FramebufferNone (pixel data streamed per command)Persistent in PSRAM
Transfer mechanismCommand + data sequence per regionesp_lcd_panel_draw_bitmap() copies into PSRAM
DC pinRequiredNot applicable
Byte order expected by panelBig-endian (SPI convention)Native little-endian
Panel dependencyStandalone (no shared component)Reuses disp_st7262 component

gfx.c always produces big-endian RGB565 pixels — the convention shared by all SPI panels. The ST7262 RGB wiring expects native little-endian order. The driver corrects this inside st7262_flush() using a static scratch buffer:

/* Swap into scratch — NOT in place.
* gfx_hline()/gfx_fill_rect() fill their line buffer once and call
* flush() once per row with the SAME buffer pointer. Swapping in place
* would flip the bytes back and forth on every call, producing
* alternating wrong-colour rows. */
static uint16_t scratch[SCREEN_WIDTH];
for (size_t i = 0; i < count; i++) {
uint16_t v = data[i];
scratch[i] = (uint16_t)((v >> 8) | (v << 8));
}
esp_lcd_panel_draw_bitmap(s_panel, x0, y0, x1 + 1, y1 + 1, scratch);

len passed to flush() is a pixel count, not a byte count. This is consistent across every board’s gfx.c implementation.

Both esp32s3_8048s043c and esp32s3_8048s050c are mounted in native landscape orientation. DISP_ST7262_ORIENTATION_LANDSCAPE is a pure passthrough — no swap_xy or mirror transformation is applied and the physical and logical resolutions are identical. This contrasts with esp32s3_4827s043c (documented in the main firmware BSP) which is rotated 90° and has differing physical/logical dimensions. See display_drivers.md for the full orientation and rotation math.


RGB Parallel

Intel 8080 Interface

SPI Interface

pibot_pendant_v1_0

320x240 ILI9341

esp32_3248s035c

480x320 ST7796

RST = NC

esp32_3248s035r

480x320 ST7796

RST wired

esp32s3_bzm_tft35_gt911

480x320 ST7796

RST wired

esp32s3_hmi43v3

800x480 RM68120

esp32s3_zx3d50ce02s_usrc_4832

480x320 ST7796-I80

esp32s3_8048s043c

800x480 ST7262

esp32s3_8048s050c

800x480 ST7262

ili9341.c

st7796.c

rm68120.c

st7796_i80.c

st7262.c


All SPI and RGB drivers share the same lifecycle, driven by app_main:

Yes

No

app_main

driver_init()

Configure GPIO

DC · LED · RST

Backlight OFF

gpio_set_level(TFT_LED, 0)

spi_bus_free()

clears stale SPI state from

previous firmware SW_RESET

spi_bus_initialize()

spi_bus_add_device()

RST wired?

HW reset

RST LOW 10ms then HIGH 120ms

SW reset

lcd_cmd(0x01) + 150ms

lcd_cmd(0x11) Sleep Out

+ 120ms delay

MADCTL (0x36)

rotation from hw_config.h

COLMOD (0x3A) = 0x55

16-bit RGB565

Gamma tables

ST7796 only: 0xF0 / 0xE0 / 0xE1

lcd_cmd(0x29) Display ON

+ 50ms

Return esp_err_t to app_main

driver_backlight(true)

gfx_init() proceeds

SPI bus pre-free: After an ESP-IDF software reset, the SPI2 peripheral may retain its previous configuration. Calling spi_bus_free() before spi_bus_initialize() is a defensive no-op when the bus was never initialised in this boot, and a necessary cleanup when it was.


Yes

No

gfx.c calls flush(x0, y0, x1, y1, data, len)

lcd_cmd(0x2A)

Column Address Set

lcd_data(col_bytes, 4)

lcd_cmd(0x2B)

Row Address Set

lcd_data(row_bytes, 4)

lcd_cmd(0x2C)

Memory Write

gpio_set_level(DC, 1)

remaining bytes > 0?

spi_device_polling_transmit

chunk up to 4096 bytes

Return

The 4096-byte chunk limit is imposed by the ESP-IDF SPI DMA size constraints. A full 480×320 RGB565 frame is 307 200 bytes, requiring at least 75 chunks.

No

Yes

gfx.c calls flush(x0, y0, x1, y1, data, len)

s_panel != NULL?

Return early - panel not ready

Byte-swap pixels into scratch[SCREEN_WIDTH]

big-endian to native little-endian

esp_lcd_panel_draw_bitmap

(panel, x0, y0, x1+1, y1+1, scratch)

RGB panel DMA copies scratch

into PSRAM framebuffer

Return


All SPI and I80 controllers support four orientations through the MADCTL register (0x36). The factory drivers use the same MADCTL bit values as the main firmware:

SCREEN_ROTATIONMADCTL valueDescription
00x28Landscape (default)
10x48Portrait
20xE8Landscape inverted
30x88Portrait inverted

SCREEN_ROTATION is defined per board in hw_config.h. The & 0x03 mask applied at lookup time prevents out-of-bounds access for any unexpected value.


The factory drivers deliberately mirror — but are decoupled from — the main firmware display stack:

Factory Application

Main Firmware (BSP / LVGL path)

init sequence mirrored

init sequence mirrored

reused as-is

disp_st7796/st7796.c

esp_lcd vendor driver

disp_st7262 component

RGB panel

disp_ili9341/disp_ili9341_spi.c

esp_lcd vendor driver

Factory st7796.c

plain spi_master, no esp_lcd

Factory st7262.c

wrapper, reuses disp_st7262

Factory ili9341.c

plain spi_master, no esp_lcd

  • SPI panels (ILI9341, ST7796): The factory reimplements the init sequence directly over the raw spi_master driver. No esp_lcd vendor abstraction or esp3d_log is pulled in. The init sequence and MADCTL/gamma values are matched to the main firmware driver to produce identical visual output from both apps.

  • RGB parallel (ST7262): The factory reuses hardware/drivers_video_rgb/disp_st7262 unchanged. This component has no LVGL dependency, making direct reuse safe.

  • I80 panels (RM68120, ST7796-I80): The factory reuses the shared I80 components from hardware/drivers_video_i80 and contributes only the flush-ready ISR callback.


The factory drivers are consumed exclusively through the three-function interface expected by gfx.c:

/* Called once at startup by app_main */
esp_err_t <controller>_init(void);
/* Called after gfx_init() succeeds — turns on the backlight */
void <controller>_backlight(bool on);
/* Called by gfx_flush() — one call per scan line or filled rectangle */
void <controller>_flush(uint16_t x0, uint16_t y0,
uint16_t x1, uint16_t y1,
const uint16_t *data, size_t len);

gfx.c is the only caller of flush(). It always passes len as a pixel count (not bytes), consistent across every board. The data pointer may refer to a buffer that is reused on the next call — drivers must not modify it in place. (The ST7262 static scratch buffer exists for exactly this reason.)


ConstraintRationale
No LVGL dependencyFactory app has no LVGL stack; drivers must be self-contained
No esp3d_logKeeps factory binary independent of the main firmware logging subsystem
No dynamic allocationAll driver state is static; no heap is used in the driver layer
4096-byte SPI chunksESP-IDF SPI DMA per-transfer size limit; chunking is mandatory for full-frame blits
IRAM_ATTR on I80 callbacksCalled from ISR context; flash placement causes instruction-cache faults
Static scratch buffer in ST7262 flushPrevents byte-swap from corrupting shared gfx.c line buffers across repeated calls
spi_bus_free() before initClears SPI peripheral register state left by the previous firmware after SW_RESET
& 0x03 on rotation indexGuards against out-of-bounds access for unexpected SCREEN_ROTATION values