Skip to content

esp32s3_zx3d50ce02s_usrc_4832_factory_app_display

esp32s3_zx3d50ce02s_usrc_4832_factory_app_display

Section titled “esp32s3_zx3d50ce02s_usrc_4832_factory_app_display”

Display subsystem for the ESP32-S3 ZX3D50CE02S USRC 4832 factory application. This module owns the complete rendering stack used during factory-test and firmware-recovery: an ST7796 controller driven over an 8-bit Intel 8080 (i80) parallel bus, fronted by a lightweight software graphics library. It is one of four sibling modules that together form the full factory application for this board — see esp32s3_zx3d50ce02s_usrc_4832_factory_app_recovery for the recovery/menu logic that calls into this layer.


  1. Module Overview
  2. Architecture
  3. Component: ST7796 I80 Driver (st7796_i80.c)
  4. Component: GFX Graphics Library (gfx.c)
  5. Data Flow
  6. Snapshot Subsystem
  7. Configuration Reference
  8. Inter-Module Dependencies
  9. Comparison with Peer Display Modules

PropertyValue
BoardESP32-S3 · ZX3D50CE02S USRC 4832
Display controllerST7796
Bus interface8-bit Intel 8080 (i80) parallel
Resolution480 × 320 px (landscape)
Color formatRGB565, BGR element order
Wire endiannessBig-endian (byte-swapped before transfer)
Source filesboards/esp32s3_zx3d50ce02s_usrc_4832/Factory/main/st7796_i80.c
boards/esp32s3_zx3d50ce02s_usrc_4832/Factory/main/gfx.c
Shared HW driverhardware/drivers_video_i80/disp_st7796_i80/

The factory application does not use LVGL. Instead it uses a minimal, self-contained graphics stack with zero dynamic allocation and a tiny RAM footprint:

  • A static line buffer (line_buf[SCREEN_WIDTH]) in gfx.c is reused for every row write.
  • A static scratch buffer (scratch[SCREEN_WIDTH]) inside st7796_i80_flush() holds the byte-swapped row before DMA hand-off.
  • A binary semaphore serialises back-to-back async transfers so neither buffer is overwritten while a DMA transaction is in flight.

2.1 Module position in the factory application

Section titled “2.1 Module position in the factory application”

Factory Application

gfx_* draw calls

reads events

flash script

esp32s3_zx3d50ce02s_usrc_4832_factory_app_recovery

(main.c - menu, OTA, update logic)

esp32s3_zx3d50ce02s_usrc_4832_factory_app_display

◀ THIS MODULE ▶

(gfx.c + st7796_i80.c)

esp32s3_zx3d50ce02s_usrc_4832_factory_app_input

(buttons.c, encoder.c, touch.c)

esp32s3_zx3d50ce02s_usrc_4832_factory_app_flash_tools

(flash_all.py, flash_factory.py)

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

disp_st7796_i80_configure()

disp_st7796_i80_get_panel_handle()

esp_lcd_new_i80_bus()

new_panel_st7796()

DMA / parallel write

transfer-done ISR

gfx.c

Software graphics library

─────────────────

gfx_clear / gfx_fill_rect

gfx_draw_char / gfx_draw_string

gfx_hline / gfx_vline / gfx_rect

gfx_snapshot_begin/end

st7796_i80.c

I80 bus driver wrapper

─────────────────

st7796_i80_init()

st7796_i80_flush()

st7796_i80_flush_ready_cb() ← ISR

disp_st7796_i80 (shared hardware component)

hardware/drivers_video_i80/disp_st7796_i80/

─────────────────

disp_st7796_i80_configure()

disp_st7796_i80_get_panel_handle()

ESP-IDF esp_lcd i80 bus

esp_lcd_panel_draw_bitmap()

ST7796 Panel (hardware)

480 × 320 · BGR · 8-bit i80


3. Component: ST7796 I80 Driver (st7796_i80.c)

Section titled “3. Component: ST7796 I80 Driver (st7796_i80.c)”
ConcernSolution
Bus initialisationWraps disp_st7796_i80_configure() with board-specific pin and timing constants from hw_config.h
Async transfer syncBinary semaphore — given at init, taken at every flush entry, given back from ISR
Byte ordergfx.c produces big-endian RGB565; this driver un-swaps to native endian before the DMA hand-off
Panel handleObtained once after disp_st7796_i80_configure() and cached in s_panel
static esp_lcd_panel_handle_t s_panel = NULL;
static SemaphoreHandle_t s_flush_done_sem = NULL;

s_panel is valid after a successful st7796_i80_init(). s_flush_done_sem is a binary semaphore initialised to the given state so the very first flush never blocks.

esp_err_t st7796_i80_init(void);

Performs one-time setup:

  1. Creates the binary semaphore and pre-gives it.
  2. Fills a disp_st7796_i80_config_t with the board’s hardware constants:
    • 8-bit data bus — pins TFT_DATA_PIN_0 … TFT_DATA_PIN_7; upper 8 data_gpio_nums slots padded with -1.
    • Control pins — TFT_DC_PIN, TFT_WR_PIN, TFT_CS_PIN, TFT_RST_PIN.
    • Clock — TFT_PCLK_FREQ_HZ; trans_queue_depth = 10.
    • Panel — BGR element order, 16 bpp, active-low reset, landscape orientation.
    • max_transfer_bytes = SCREEN_WIDTH * 2 (one row at a time).
  3. Calls disp_st7796_i80_configure() passing st7796_i80_flush_ready_cb as the completion ISR.
  4. Retrieves the panel handle via disp_st7796_i80_get_panel_handle().

Returns ESP_OK on success; ESP_ERR_NO_MEM if semaphore creation fails; ESP_FAIL if the panel handle is null.

void st7796_i80_flush(uint16_t x0, uint16_t y0,
uint16_t x1, uint16_t y1,
const uint16_t *data, size_t len);

Transfers one rectangular region (always one row in practice) to the panel:

  1. Takes s_flush_done_sem — blocks until the previous DMA completes.
  2. Byte-swaps up to SCREEN_WIDTH pixels from data into the static scratch[] buffer: scratch[i] = (v >> 8) | (v << 8) — converts big-endian RGB565 to native endian.
  3. Calls esp_lcd_panel_draw_bitmap() with scratch as the source.

len is a pixel count, not a byte count. Clamped to SCREEN_WIDTH as a safety guard.

static void IRAM_ATTR st7796_i80_flush_ready_cb(void);

Fired by the i80 bus driver when a DMA transfer completes. Calls xSemaphoreGiveFromISR() and issues portYIELD_FROM_ISR() if a higher-priority task was woken. Placed in IRAM (IRAM_ATTR) to satisfy ESP-IDF ISR placement requirements.

Binary Semaphoreflush_ready_cb (ISR)st7796_i80_flush()st7796_i80_init()Binary Semaphoreflush_ready_cb (ISR)st7796_i80_flush()st7796_i80_init()First flush - proceeds immediatelySecond flush - waits for DMAxSemaphoreCreateBinary()xSemaphoreGive() [pre-given]xSemaphoreTake() [available → taken]byte-swap into scratch[]esp_lcd_panel_draw_bitmap()xSemaphoreGiveFromISR()xSemaphoreTake() [blocks until ISR fires]byte-swap into scratch[]esp_lcd_panel_draw_bitmap()xSemaphoreGiveFromISR()

4. Component: GFX Graphics Library (gfx.c)

Section titled “4. Component: GFX Graphics Library (gfx.c)”

gfx.c is the sole drawing surface exposed to the rest of the factory application. It owns the shared row buffer, performs all clipping, handles byte-swapping at the colour-fill level, and optionally mirrors every write into a snapshot file on the SD card.

static uint16_t line_buf[SCREEN_WIDTH]; /* reused across every draw call */

All colour values stored in line_buf and in the per-call char_buf are pre-swapped to big-endian before being passed to gfx_flush().

static void gfx_flush(int x0, int y0, int x1, int y1,
const uint16_t *data, int count);

This is the only path through which pixels reach the hardware:

  • Always calls st7796_i80_flush().
  • When ENABLE_SNAPSHOT is defined, also calls snap_write().

All colour parameters are native RGB565 values (callers do not byte-swap). The library handles endianness internally.

void gfx_init(void);

Currently a no-op. Snapshot state is managed on demand. Provided for API symmetry with peer board implementations.

void gfx_clear(uint16_t color);

Fills the entire display with color. Byte-swaps the colour once, fills line_buf with that value, then sends the same row SCREEN_HEIGHT times.

void gfx_draw_char(int x, int y, char c, uint16_t fg, uint16_t bg);

Renders a single character from the built-in 8 × 16 bitmap font (font8x16):

  1. Clamps c to the printable range [0x20, 0x7E]; substitutes '?' otherwise.
  2. Looks up the glyph via font8x16_get_glyph(c).
  3. Expands each bit into a FONT_WIDTH × FONT_HEIGHT local pixel buffer using pre-swapped fg_swap/bg_swap values.
  4. Clips x1/y1 to screen bounds and flushes the entire glyph in a single call.
void gfx_draw_string(int x, int y, const char *str, uint16_t fg, uint16_t bg);

Calls gfx_draw_char() for each character, advancing x by FONT_WIDTH. Stops when x + FONT_WIDTH > SCREEN_WIDTH.

void gfx_hline(int x, int y, int w, uint16_t color);

Draws a horizontal line. Clips to screen bounds, fills line_buf[0..w-1] with the swapped colour, flushes as a single row span.

void gfx_vline(int x, int y, int h, uint16_t color);

Draws a vertical line. Each pixel is a separate single-pixel gfx_flush() call (one row per iteration).

void gfx_rect(int x, int y, int w, int h, uint16_t color);

Draws a hollow rectangle using four line calls:

gfx_hline(x, y, w, color); // top
gfx_hline(x, y+h-1, w, color); // bottom
gfx_vline(x, y, h, color); // left
gfx_vline(x+w-1, y, h, color); // right
void gfx_fill_rect(int x, int y, int w, int h, uint16_t color);

Fills a solid rectangle. After clipping, fills line_buf[0..w-1] once and flushes it h times (one call per row).

st7796_i80.c

gfx.c

st7796_i80_flush()

#ifdef ENABLE_SNAPSHOT

transfer done

Recovery / main.c

(draw_header, draw_menu, draw_progress …)

Primitive functions

gfx_clear / gfx_fill_rect

gfx_hline / gfx_vline / gfx_rect

gfx_draw_char / gfx_draw_string

Byte-swap color

swapped = (color >> 8) | (color << 8)

Fill line_buf or char_buf

gfx_flush(x0,y0,x1,y1,data,count)

xSemaphoreTake(s_flush_done_sem)

Byte-swap pixels into scratch[]

esp_lcd_panel_draw_bitmap()

flush_ready_cb() ISR

xSemaphoreGiveFromISR()

snap_write()

(only if ENABLE_SNAPSHOT)


Each layer adds or removes a byte-swap. The net effect is that the ST7796 always receives native-endian RGB565 with BGR element order.

(c>>8)|(c<<8)

passed as data[]

(v>>8)|(v<<8)

Caller

RGB565 native

e.g. 0xF800 = red

gfx.c

Byte-swap → big-endian

0x00F8

line_buf / char_buf

big-endian RGB565

in SRAM

st7796_i80_flush()

Byte-swap → native

0xF800 in scratch[]

esp_lcd_panel_draw_bitmap()

DMA → 8-bit i80 bus

native endian

ST7796 panel

BGR element order

Displayed correctly

Why the double swap? gfx.c follows the big-endian convention shared by every other board’s gfx.c in this project (originally written for SPI panels). The i80 driver reverses the swap before the DMA hand-off. This keeps the graphics layer uniform across all boards while adapting at the transport layer.

SRAM: line_buf[SCREEN_WIDTH] 480 × uint16_t = 960 bytes (static in gfx.c)
SRAM: scratch[SCREEN_WIDTH] 480 × uint16_t = 960 bytes (static local in st7796_i80_flush)
Total static RAM for render buffers: 1 920 bytes
No dynamic allocation anywhere in the render path.

When compiled with -DENABLE_SNAPSHOT, every pixel flushed to the display is also written to a raw binary file. This is used during factory testing to capture screen state without a physical camera.

Offset Size Type Description
────── ──── ────────── ──────────────────────────────────────────
0 4 bytes uint32_t LE Width in pixels (= SCREEN_WIDTH)
4 4 bytes uint32_t LE Height in pixels (= SCREEN_HEIGHT)
8 W×H×2 uint16_t[] Raw RGB565, native endian, row-major,
top-left origin

Total file size: 8 + 480 × 320 × 2 = 307 208 bytes (≈ 300 KB).

bool gfx_snapshot_begin(const char *filepath); // open file, write header, zero-fill pixel area
bool gfx_snapshot_end(void); // flush and close file
bool gfx_snapshot_is_capturing(void); // true while file is open

gfx_snapshot_begin() pre-fills the entire pixel area with zeros so any un-drawn region appears as black rather than uninitialised data.

static void snap_write(int x0, int y0, int x1, int y1,
const uint16_t *data_swapped, int count);

Called from gfx_flush() for each row when a capture is active. Seeks to the correct file offset per row and un-swaps the incoming big-endian pixels back to native before writing:

file_offset = SNAP_HEADER_SIZE + (row × SCREEN_WIDTH + x0) × sizeof(uint16_t)
ST7796 displaySnapshot file (SD card)gfx.cmain.c (recovery module)ST7796 displaySnapshot file (SD card)gfx.cmain.c (recovery module)s_snap_file != NULLloop[Full screen redraw]s_snap_file = NULLgfx_snapshot_begin("/sdcard/snap_001.raw")fopen(), write 8-byte headerzero-fill 307 200 bytesgfx_clear() / gfx_fill_rect() / draw_header() …st7796_i80_flush(row pixels)snap_write(row pixels, un-swapped to native)gfx_snapshot_end()fclose()

All hardware-pin constants and display geometry are defined in hw_config.h (board-specific, not part of this module). The symbols consumed by this module are:

SymbolUsed inMeaning
SCREEN_WIDTHgfx.c, st7796_i80.cHorizontal resolution (480)
SCREEN_HEIGHTgfx.cVertical resolution (320)
TFT_DC_PINst7796_i80_init()Data/Command select GPIO
TFT_WR_PINst7796_i80_init()Write-strobe GPIO
TFT_CS_PINst7796_i80_init()Chip-select GPIO
TFT_RST_PINst7796_i80_init()Reset GPIO
TFT_DATA_PIN_0..7st7796_i80_init()8-bit parallel data bus GPIOs
TFT_PCLK_FREQ_HZst7796_i80_init()i80 pixel-clock frequency
FONT_WIDTHgfx.cGlyph width in pixels (8)
FONT_HEIGHTgfx.cGlyph height in pixels (16)
FONT_BYTES_PER_ROWgfx.cBytes per glyph row (1 for an 8-wide font)
ENABLE_SNAPSHOTgfx.c (optional)Compile-time flag to enable SD snapshot capture

factory logging

esp32s3_zx3d50ce02s_usrc_4832_factory_app_recovery

board configuration

font assets

display_drivers_i80 (shared hardware component)

esp32s3_zx3d50ce02s_usrc_4832_factory_app_display

calls gfx_* API

calls st7796_i80_flush()

disp_st7796_i80_configure()

ESP-IDF esp_lcd i80 bus

gfx.c

st7796_i80.c

disp_st7796_i80/

disp_st7796_i80_configure()

disp_st7796_i80_get_panel_handle()

font8x16.h

font8x16_get_glyph()

hw_config.h

pin assignments, resolution

main.c

draw_header(), draw_menu()

draw_progress(), …

factory_log.h

FACTORY_LOGD()

ESP-IDF esp_lcd

CallerFunctionPurpose
main.c (recovery module)gfx_init()One-time setup at boot
main.cgfx_clear()Blank screen between states
main.cgfx_fill_rect()Background regions, progress bars
main.cgfx_draw_string()Labels, status text
main.cgfx_rect()Menu item borders
main.cgfx_hline() / gfx_vline()Dividers, indicators
main.cgfx_snapshot_begin() / gfx_snapshot_end() / gfx_snapshot_is_capturing()Factory snapshot capture
gfx.cst7796_i80_init()Called once during board init
gfx.cst7796_i80_flush()Per-row pixel transfer

The factory-application display layer follows the same two-file pattern (gfx.c + a panel driver wrapper) across all boards in this project. This board’s implementation is distinguished by its 8-bit i80 parallel bus and async semaphore design, which it shares in pattern with the HMI43v3 board (16-bit i80 RM68120 panel).

ModuleControllerBusByte-swap in driverAsync semaphoreNotes
This module (zx3d50ce02s)ST77968-bit i80✅✅ binary semaphore8 data pins + 8 padding -1s; CS pin used; BGR order
esp32s3_hmi43v3_factory_appRM6812016-bit i80✅✅ (same ISR pattern)16-bit bus; no CS pin; RGB order
esp32s3_bzm_tft35_gt911_factory_app_displayST7796SPIN/AN/ASPI transfers are synchronous
esp32_3248s035r / esp32_3248s035cST7796SPIN/AN/ASame controller, SPI path
esp32s3_8048s043c / esp32s3_8048s050cST7262RGB parallelN/A✅ vsync eventRGB panels use vsync callback, not transfer-done ISR
esp32s3_4827s043cILI9485RGB parallelN/A✅ vsync eventSame vsync pattern as ST7262 boards

RGB-parallel boards

(async vsync)

write to framebuffer

gfx.c

vsync DMA

(event callback)

LCD

i80 boards - this module

(async DMA)

queued DMA write

next flush unblocks

gfx.c

i80 ISR

(semaphore give)

LCD

SPI boards

(synchronous)

blocking SPI write

(returns when done)

gfx.c

LCD

For the shared hardware driver that backs this module, see the display_drivers_i80 module documentation.


This module is part of the esp32s3_zx3d50ce02s_usrc_4832_factory_app group. For related subsystems, see: