Skip to content

esp32s3_hmi43v3_factory_app_display

Display subsystem of the factory test application for the ESP32-S3 HMI43V3 board. It provides a lightweight, framebuffer-free graphics API (gfx.c) and a board-specific thin wrapper over the shared RM68120 i80-parallel LCD driver (rm68120.c).

This module is one of five subsystems that compose esp32s3_hmi43v3_factory_app:

Sibling moduleRole
esp32s3_hmi43v3_factory_app_mainMenu logic, OTA actions, callers of every gfx_* function
esp32s3_hmi43v3_factory_app_inputButtons, encoder, touch, buzzer
esp32s3_hmi43v3_factory_app_storageSD card mount/unmount (snapshot files land here)
esp32s3_hmi43v3_factory_app_toolsHost-side tools including snap2png.py for snapshot conversion

esp32s3_hmi43v3_factory_app_display

ENABLE_SNAPSHOT

snap_write()

factory_app_main

gfx_clear / gfx_draw_string

gfx_fill_rect / gfx_rect

snapshot_take

gfx.c

Graphics Abstraction Layer

rm68120.c

Driver Wrapper

hardware/drivers_video_i80/disp_rm68120

Shared RM68120 Component

RM68120 LCD Panel

(480×272 - i80 16-bit)

SD Card

/sdcard/snapXXX.raw

rm68120.c - Display Driver Wrapper

gfx.c - Public Graphics API

gfx_init()

gfx_clear()

Primitives

gfx_hline / gfx_vline

gfx_rect / gfx_fill_rect

Text Rendering

gfx_draw_char

gfx_draw_string

font12x24 bitmap

Snapshot Subsystem

gfx_snapshot_begin

gfx_snapshot_end

gfx_snapshot_is_capturing

(#ifdef ENABLE_SNAPSHOT)

static gfx_flush()

Proxy to RM68120 + snapshot file

rm68120_init()

Configures i80 bus, semaphore

rm68120_flush()

Byte-swap + draw_bitmap

rm68120_flush_ready_cb()

ISR - gives semaphore

Binary Semaphore

s_flush_done_sem


Hardware Interface — i80 (Intel 8080) Parallel Bus

Section titled “Hardware Interface — i80 (Intel 8080) Parallel Bus”

The HMI43V3 board drives its LCD through an Intel 8080 (i80) 16-bit parallel bus, which is architecturally distinct from the SPI displays used on other boards (e.g. ILI9341 on the pibot pendant) and from the RGB-parallel panels used on the 8048s/8048_touch family.

PropertyValue
ControllerRaydium RM68120
Bus typeIntel 8080 (i80) — 16-bit wide
Transfer modeAsynchronous / DMA-queued
Pixel formatRGB565 (16 bpp)
OrientationLandscape (DISP_RM68120_ORIENTATION_LANDSCAPE)
ResolutionSCREEN_WIDTH × SCREEN_HEIGHT (from hw_config.h)
ClockTFT_PCLK_FREQ_HZ (from hw_config.h)
CS pinTFT_CS_PIN
DC pinTFT_DC_PIN
WR pinTFT_WR_PIN
RST pinTFT_RST_PIN
Data pinsTFT_DATA_PIN_0 … TFT_DATA_PIN_15 (16 lines)

Why Asynchronous Transfers Require a Semaphore

Section titled “Why Asynchronous Transfers Require a Semaphore”

Unlike SPI (which blocks until the transfer completes), i80 DMA transfers are fire-and-forget — the CPU returns immediately and a completion interrupt fires later. If a second flush is issued before the first completes, the DMA may still be reading the scratch buffer when it gets overwritten.

The driver solves this with a binary semaphore (s_flush_done_sem):

i80 ISResp_lcd (DMA)rm68120.cgfx.cfactory_app_maini80 ISResp_lcd (DMA)rm68120.cgfx.cfactory_app_mainInit: semaphore pre-given (available)gfx_fill_rect(...)rm68120_flush(row N)xSemaphoreTake() - first call passes immediatelyesp_lcd_panel_draw_bitmap()returns (DMA queued)gfx_fill_rect(...)rm68120_flush(row N+1)xSemaphoreTake() - BLOCKS until DMA donerm68120_flush_ready_cb()xSemaphoreGiveFromISR()Unblocked, writes scratch bufferesp_lcd_panel_draw_bitmap()

All boards share the same gfx.c structure, and by convention gfx.c always writes big-endian RGB565 (network/SPI order). The RM68120 i80 bus expects native (little-endian) RGB565. rm68120_flush() performs the per-pixel byte swap into a local scratch buffer before calling esp_lcd_panel_draw_bitmap():

// Inside rm68120_flush():
uint16_t v = data[i];
scratch[i] = (uint16_t)((v >> 8) | (v << 8)); // big-endian → native

This keeps gfx.c portable across board families while isolating the hardware-specific correction to the driver wrapper.


gfx.c owns the static line buffer (line_buf[SCREEN_WIDTH]) and provides the entire public drawing surface. It has no heap allocations; all rendering is line-by-line.

FunctionSignatureDescription
gfx_initvoid gfx_init(void)No-op placeholder. Snapshot is on-demand; display init is done by rm68120_init().
gfx_clearvoid gfx_clear(uint16_t color)Fills the entire screen with color. Sends one row at a time via line_buf.
gfx_draw_charvoid gfx_draw_char(int x, int y, char c, uint16_t fg, uint16_t bg)Renders a single character using the embedded 12×24-pixel font12x24 bitmap. Characters outside 0x20–0x7E are replaced with '?'.
gfx_draw_stringvoid gfx_draw_string(int x, int y, const char *str, uint16_t fg, uint16_t bg)Renders a null-terminated string left-to-right. Stops at the screen right edge.
gfx_hlinevoid gfx_hline(int x, int y, int w, uint16_t color)Horizontal line. Clips to screen bounds.
gfx_vlinevoid gfx_vline(int x, int y, int h, uint16_t color)Vertical line. Clips to screen bounds. Sends one pixel per flush call.
gfx_rectvoid gfx_rect(int x, int y, int w, int h, uint16_t color)Hollow rectangle (four lines).
gfx_fill_rectvoid gfx_fill_rect(int x, int y, int w, int h, uint16_t color)Solid filled rectangle. Clips to screen bounds. Sends one row at a time via line_buf.
gfx_snapshot_beginbool gfx_snapshot_begin(const char *filepath)Opens a raw file and writes the 8-byte header. Returns false if a capture is already active or the file cannot be created. Only compiled when ENABLE_SNAPSHOT is defined.
gfx_snapshot_endbool gfx_snapshot_end(void)Closes the snapshot file. Returns false if no capture was active.
gfx_snapshot_is_capturingbool gfx_snapshot_is_capturing(void)Returns true while a snapshot file is open.
FunctionDescription
static gfx_flush()Thin proxy: calls rm68120_flush(), then conditionally calls snap_write() when ENABLE_SNAPSHOT is defined and a capture is active.
static snap_write()Seeks to the correct offset in the snapshot file for the bounding rectangle and writes pixels. Performs the reverse byte swap (big-endian → native) for the file.

All public API functions accept colors as native RGB565 (host endian). gfx.c applies the big-endian swap before writing to line_buf or the character bitmap buffer, immediately before calling gfx_flush().

Native RGB565 (caller)
→ (color >> 8) | (color << 8) in gfx.c
→ big-endian in line_buf / char_buf
→ passed to rm68120_flush()
→ (v >> 8) | (v << 8) in rm68120.c
→ native RGB565 sent to panel DMA

A thin wrapper over the shared hardware/drivers_video_i80/disp_rm68120 component (see display_i80_drivers). It does not re-implement the panel protocol; it only provides the initialization glue, the semaphore, and the scratch-buffer byte swap.

FunctionSignatureDescription
rm68120_initesp_err_t rm68120_init(void)Creates the flush semaphore (pre-given), configures the i80 bus with disp_rm68120_configure(), and stores the panel handle.
rm68120_flushvoid rm68120_flush(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, const uint16_t *data, size_t len)Takes the semaphore (blocks if previous DMA in flight), swaps bytes into a static scratch[SCREEN_WIDTH] buffer, calls esp_lcd_panel_draw_bitmap(). len is a pixel count, not byte count.
static rm68120_flush_ready_cbvoid IRAM_ATTR rm68120_flush_ready_cb(void)ISR-context callback invoked by the i80 bus driver when a transfer completes. Gives the semaphore and yields if a higher-priority task was unblocked.
disp_rm68120_config_t {
.bus_config.bus_width = 16, // 16-bit data bus
.bus_config.max_transfer_bytes = SCREEN_WIDTH*2, // one row at a time
.io_config.pclk_hz = TFT_PCLK_FREQ_HZ,
.io_config.trans_queue_depth = 10,
.panel_config.bits_per_pixel = 16,
.orientation = DISP_RM68120_ORIENTATION_LANDSCAPE,
}

The callback rm68120_flush_ready_cb is passed as user_ctx to disp_rm68120_configure(), which registers it on the i80 panel IO on_color_trans_done event via the shared driver’s disp_rm68120_notify_flush_ready().


The snapshot feature captures a full-screen raw image to SD card during a factory test. It is opt-in at compile time via ENABLE_SNAPSHOT.

GPIO_NUM_0 pressed

snapshot_check() detects edge

sdcard_mount()

snap_find_next_number()

finds first unused snapXXX.raw

gfx_snapshot_begin(filepath)

Open file, write 8-byte header

Pre-fill with black (512-byte chunks)

Full screen redraw

draw_menu() or draw_flashing_screen()

All gfx_* calls now dual-write

gfx_snapshot_end()

fclose(s_snap_file)

[Host] snap2png.py snapXXX.raw

Converts to PNG for visual verification

Offset Size Content
------ ------ ------------------------------------------
0 4 uint32_t width (little-endian)
4 4 uint32_t height (little-endian)
8 W×H×2 Raw RGB565 pixels, native endian, row-major

Total file size: 8 + SCREEN_WIDTH × SCREEN_HEIGHT × 2 bytes.

For each flush call intercepted during capture, snap_write() computes the target offset in the file for each row of the bounding rectangle:

offset = SNAP_HEADER_SIZE + (row * SCREEN_WIDTH + x0) * 2

Since gfx.c stores pixels big-endian in the draw buffers, snap_write() performs the swap back to native before fwrite():

uint16_t native = (sw >> 8) | (sw << 8);
fwrite(&native, sizeof(uint16_t), 1, s_snap_file);

The main firmware BSP (board_init.c) implements its own snapshot mechanism inside lvgl_flush_cb(). The two systems are independent:

PropertyFactory App SnapshotBSP / LVGL Snapshot
Locationgfx.c — ENABLE_SNAPSHOTboard_init.c — ESP3D_SNAPSHOT_FEATURE
Hook pointStatic gfx_flush() proxylvgl_flush_cb() before draw_bitmap
TriggerGPIO_NUM_0 pressUI layer / esp3d_snapshot_* API
SyncNone needed (single-task)Mutex (g_snapshot.mutex)
File formatwidth(4) + height(4) + raw RGB565Raw pixel stream (no header)

rm68120.c

gfx.c

factory_app_main

if ENABLE_SNAPSHOT

and capturing

DMA done interrupt

gfx_clear(BLACK)

gfx_draw_string(x,y,label)

gfx_fill_rect(x,y,w,h,color)

static line_buf

SCREEN_WIDTH x uint16_t

byte-swap

native to big-endian

gfx_flush()

Proxy function

snap_write()

(if capturing)

xSemaphoreTake()

(block if DMA busy)

static scratch

SCREEN_WIDTH x uint16_t

byte-swap

big-endian to native

esp_lcd_panel_draw_bitmap()

rm68120_flush_ready_cb()

xSemaphoreGiveFromISR()

SD Card

/sdcard/snapXXX.raw


DependencyPathPurpose
gfx.hFactory/main/gfx.hPublic GFX API header
rm68120.hFactory/main/rm68120.hRM68120 wrapper API header
font12x24.hFactory/main/font12x24.hEmbedded 12×24-pixel bitmap font
hw_config.hFactory/main/hw_config.hPin definitions, SCREEN_WIDTH, SCREEN_HEIGHT, TFT_PCLK_FREQ_HZ
disp_rm68120.hhardware/drivers_video_i80/disp_rm68120/Shared i80 driver component (see display_i80_drivers)
factory_log.hFactory/main/factory_log.hFACTORY_LOGD macro (silences SD stack noise)
FreeRTOSESP-IDFSemaphoreHandle_t, xSemaphoreTake, xSemaphoreGiveFromISR
esp_lcdESP-IDFesp_lcd_panel_handle_t, esp_lcd_panel_draw_bitmap

ENABLE_SNAPSHOT

gfx.c

rm68120.c

disp_rm68120

(shared hardware component)

FreeRTOS

Binary Semaphore

esp_lcd

panel draw bitmap

SD Card

(optional, snapshot only)


The BSP (esp32s3_hmi43v3_bsp) and the factory app display share the same underlying disp_rm68120 hardware component. They are never active at the same time — the factory app runs independently of the main firmware. Their i80_flush_ready_cb / rm68120_flush_ready_cb patterns follow the same semaphore discipline for the same underlying reason (async DMA ordering).

Board BSP (main firmware)

Factory App (this module)

signals LVGL flush done

gfx.c

Simple pixel API

rm68120.c

Wrapper + semaphore

lvgl_flush_cb()

LVGL partial render

i80_flush_ready_cb()

lv_display_flush_ready()

hardware/drivers_video_i80/disp_rm68120

Shared component

RM68120 LCD Panel


Comparison with Other Board Display Modules

Section titled “Comparison with Other Board Display Modules”

The factory app display subsystem exists in a parallel structure on every supported board. This board is one of two in the family that use an i80 parallel bus:

BoardModuleControllerBus typeDriver file
esp32s3_hmi43v3esp32s3_hmi43v3_factory_app_displayRM68120i80 16-bitrm68120.c
esp32s3_zx3d50ce02s_usrc_4832esp32s3_zx3d50ce02s_usrc_4832_factory_app (display)ST7796i80st7796_i80.c
pibot_pendant_v1_0pibot_pendant_v1_0_factory_app_displayILI9341SPIili9341.c
esp32s3_bzm_tft35_gt911esp32s3_bzm_tft35_gt911_factory_app_displayST7796SPIst7796.c
esp32s3_8048s043c / 8048s050cesp32s3_8048s043c_factory_app (display)ST7262RGB parallelst7262.c
esp32s3_8048s070cesp32s3_8048s070c_factory_app_displayST7262RGB parallel—

All boards share the same gfx.c API surface and the same snap_write() / snapshot file format. Only the display driver file and the bus interface differ.


  1. No framebuffer: Rendering is immediate and line-by-line via line_buf[SCREEN_WIDTH]. This minimises RAM use on an ESP32-S3 where DRAM is constrained, at the cost of no retained scene state.

  2. Big-endian convention in gfx.c: By always writing big-endian in gfx.c, the file is portable across SPI and i80 boards. Each driver wrapper is responsible for the byte swap appropriate to its bus.

  3. Semaphore pre-given at init: The first rm68120_flush() call is guaranteed to proceed without blocking. Subsequent calls wait for the in-flight DMA rather than adding a second frame of latency.

  4. IRAM_ATTR on the ISR callback: rm68120_flush_ready_cb() runs from the i80 bus ISR and must reside in IRAM to avoid cache-miss stalls during flash reads.

  5. snap_write() seeks rather than appending: The snapshot file is pre-filled with black at gfx_snapshot_begin() so partial captures (e.g. if the app crashes mid-draw) produce a valid file with black regions rather than a truncated one.