Skip to content

esp32s3_bzm_tft35_gt911_factory_app_display

esp32s3_bzm_tft35_gt911_factory_app_display

Section titled “esp32s3_bzm_tft35_gt911_factory_app_display”

Display subsystem for the Factory recovery application on the ESP32-S3 BZM TFT 3.5” board (GT911 capacitive touch variant). It provides a two-layer stack — a drawing API (gfx.c) sitting on top of a minimal standalone ST7796 SPI driver (st7796.c) — that the factory application uses to render its interactive menu UI, progress indicators, and diagnostic screens. An optional snapshot subsystem (compiled in when ENABLE_SNAPSHOT is defined) tees every rendered frame to a raw file on the SD card so the snap2png.py tool can produce a PNG audit trail.


  1. Architecture Overview
  2. Component Descriptions
  3. API Reference
  4. Data Flow
  5. Snapshot File Format
  6. ST7796 Initialization Sequence
  7. Memory Design
  8. Hardware Configuration
  9. Color Encoding and Byte Order
  10. Module Relationships

The module is a self-contained display stack, isolated from the main firmware’s LVGL-based UI system. It operates entirely in the factory application’s single task context — no LVGL, no DMA callbacks, no FreeRTOS queues. All SPI transfers use synchronous polling (spi_device_polling_transmit).

Hardware

esp32s3_bzm_tft35_gt911_factory_app_display

Factory Application (esp32s3_bzm_tft35_gt911_factory_app)

gfx_* calls

gfx_flush() wrapper

snap_write()

[if capturing]

fwrite()

spi_device_polling_transmit

MOSI/CLK/CS/DC

main.c

(draw_header, draw_menu,

draw_progress, …)

gfx.c

Graphics Abstraction Layer

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

gfx_clear / gfx_draw_char

gfx_draw_string / gfx_hline

gfx_vline / gfx_rect

gfx_fill_rect

Snapshot Subsystem

(ENABLE_SNAPSHOT)

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

gfx_snapshot_begin/end

snap_write

st7796.c

ST7796 SPI Driver

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

st7796_init / st7796_flush

st7796_backlight

lcd_cmd / lcd_data

ESP-IDF SPI Master

(polling mode, SPI3_HOST)

ST7796 TFT

320×480 RGB565

(portrait mode)

SD Card

(snapshot files)

font11x21

11×21 bitmap font

LayerFileRole
Drawing APIgfx.cPixel-level primitives; byte-swap management; snapshot tee
Hardware Driverst7796.cSPI bus setup; ST7796 register sequences; chunked DMA transfer
Font datafont11x21.hEmbedded 11×21 bitmap font accessed via font11x21_get_glyph()
Pin configurationhw_config.hTFT_* GPIO/SPI macros; SCREEN_WIDTH/HEIGHT/ROTATION

gfx.c provides the complete drawing vocabulary used by the factory application’s UI renderer. Its design priorities are:

  • Zero dynamic allocation in the hot drawing path — a static uint16_t line_buf[SCREEN_WIDTH] (640 bytes in portrait mode) is reused across all line, fill, and clear operations.
  • Uniform byte-swap — every color value is byte-swapped from host-endian to SPI big-endian (the ST7796 wire format) exactly once, before being written into line_buf or char_buf.
  • Transparent snapshot tee — the single internal gfx_flush() wrapper calls both st7796_flush() and (when capturing) snap_write(), so no drawing call site needs to be aware of snapshotting.

gfx_init() is intentionally empty. The ST7796 driver must be initialized separately via st7796_init() before any drawing call.

When ENABLE_SNAPSHOT is defined at compile time, gfx.c gains a file-backed framebuffer tee:

  • gfx_snapshot_begin(filepath) opens a file, writes an 8-byte header (width + height as uint32_t LE), and pre-fills the pixel region with zeros (black). The caller is then expected to perform a full screen redraw to populate the file.
  • Every gfx_flush() call subsequently writes its pixel data to the open file via snap_write(), un-swapping bytes back from SPI big-endian to native RGB565.
  • gfx_snapshot_end() closes the file.
  • snap_write() uses fseek to position each row at the correct file offset (SNAP_HEADER_SIZE + (row * SCREEN_WIDTH + x0) * 2), allowing partial-region updates to land in the correct row without requiring a full scratch buffer.

The raw snapshot file is consumed offline by the esp32s3_bzm_tft35_gt911_factory_app_tools snap2png.py script, which reads the header dimensions and converts the RGB565 pixel stream to a standard PNG.


st7796.c is a minimal, factory-only SPI driver for the ST7796 TFT controller. It does not use the shared hardware driver from display_spi_drivers (hardware/common/drivers/disp_st7796/); instead it provides a lean, self-contained implementation suited to the factory environment (no LVGL, no esp_lcd panel API abstraction, no ESP3D-X log dependency).

Key design choices:

  • Uses spi_device_polling_transmit — synchronous, no DMA callback dependencies, predictable latency in a non-RTOS-sensitive single-task context.
  • Issues spi_bus_free() before spi_bus_initialize() to handle the case where the SPI3 peripheral retains state after the main firmware’s software reset into the factory partition.
  • Pixel data is sent in 4096-byte chunks to stay within SPI DMA transfer size limits.
  • Backlight is kept off (gpio_set_level(TFT_LED, 0)) during st7796_init() to prevent the user from seeing partial initialization frames; the caller enables it afterwards with st7796_backlight(true).
  • The gamma tuning bytes (commands 0xE0/0xE1) mirror the values used by the production ST7796 driver in hardware/common/drivers/disp_st7796/st7796.c, ensuring visual consistency between the factory app and the main firmware.
  • The DC (Data/Command) GPIO is driven directly by lcd_cmd() (DC=0) and lcd_data() (DC=1) before each SPI transaction.

All coordinates are in pixels relative to the top-left corner (0, 0). All functions silently clip to screen bounds — no call results in an out-of-bounds SPI transfer. Colors are expressed in native host-endian RGB565; the internal gfx_flush() wrapper handles the SPI byte-swap transparently.

Color helpers:

/* Compose an RGB565 value from 8-bit R, G, B components */
#define GFX_RGB565(r, g, b) ((uint16_t)(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)))
/* Predefined constants */
#define COLOR_BLACK 0x0000
#define COLOR_WHITE 0xFFFF
#define COLOR_RED GFX_RGB565(255, 0, 0)
#define COLOR_GREEN GFX_RGB565( 0, 255, 0)
#define COLOR_BLUE GFX_RGB565( 0, 0, 255)
#define COLOR_YELLOW GFX_RGB565(255, 255, 0)
#define COLOR_CYAN GFX_RGB565( 0, 255, 255)
#define COLOR_GRAY GFX_RGB565(128, 128, 128)
#define COLOR_DARKGRAY GFX_RGB565( 64, 64, 64)

Function reference:

FunctionSignatureDescription
gfx_initvoid gfx_init(void)Placeholder; currently a no-op. Call st7796_init() separately before drawing.
gfx_clearvoid gfx_clear(uint16_t color)Fill the entire screen with a solid RGB565 color. Sends one row-wide flush per row (480 calls in portrait mode).
gfx_draw_charvoid gfx_draw_char(int x, int y, char c, uint16_t fg, uint16_t bg)Render a single 11×21 character at (x, y) with foreground and background colors. Non-printable characters are substituted with ?.
gfx_draw_stringvoid gfx_draw_string(int x, int y, const char *str, uint16_t fg, uint16_t bg)Render a null-terminated string left-to-right. Stops when the next character would exceed SCREEN_WIDTH.
gfx_hlinevoid gfx_hline(int x, int y, int w, uint16_t color)Draw a horizontal line of width w starting at (x, y). Single gfx_flush() call.
gfx_vlinevoid gfx_vline(int x, int y, int h, uint16_t color)Draw a vertical line of height h starting at (x, y). Sends one pixel per flush call — avoid very tall lines in performance-critical paths.
gfx_rectvoid gfx_rect(int x, int y, int w, int h, uint16_t color)Draw a hollow rectangle outline using 2 hlines + 2 vlines.
gfx_fill_rectvoid gfx_fill_rect(int x, int y, int w, int h, uint16_t color)Draw a solid filled rectangle, one row-wide flush per row.
gfx_snapshot_beginbool gfx_snapshot_begin(const char *filepath)(Requires ENABLE_SNAPSHOT) Open a raw capture file. Returns false if already capturing or the file cannot be created.
gfx_snapshot_endbool gfx_snapshot_end(void)(Requires ENABLE_SNAPSHOT) Close the capture file. Returns false if not currently capturing.
gfx_snapshot_is_capturingbool gfx_snapshot_is_capturing(void)(Requires ENABLE_SNAPSHOT) Returns true if a snapshot file is currently open.
FunctionSignatureDescription
st7796_initesp_err_t st7796_init(void)Configure GPIO pins, initialize SPI bus and device on SPI3_HOST, run ST7796 startup sequence. Returns ESP_OK on success.
st7796_backlightvoid st7796_backlight(bool on)Enable (true) or disable (false) the TFT backlight GPIO (GPIO 48).
st7796_flushvoid st7796_flush(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, const uint16_t *data, size_t len)Set CASET/RASET address window and transmit len RGB565 pixels in 4096-byte DMA chunks. Data must already be in SPI big-endian byte order.

The static functions lcd_cmd(), lcd_data(), and lcd_data_byte() are internal to st7796.c and are not part of the public interface.


ST7796 TFTESP-IDF SPIst7796.cgfx.cmain.cST7796 TFTESP-IDF SPIst7796.cgfx.cmain.cloop[chunks of 4096 bytes]loop[for each row]gfx_fill_rect(x, y, w, h, color)byte-swap color into line_buf[w]gfx_flush(x, y+row, x+w-1, y+row, line_buf, w)st7796_flush(x0, y0, x1, y1, data, len)lcd_cmd(0x2A) - CASET (column address)lcd_data(col_data, 4)lcd_cmd(0x2B) - RASET (row address)lcd_data(row_data, 4)lcd_cmd(0x2C) - RAMWR (memory write)spi_device_polling_transmit(chunk)MOSI/CLK/CS/DC SD Card Filest7796.cgfx.cmain.cSD Card Filest7796.cgfx.cmain.cFull screen redraw beginsloop[every draw call]Raw file ready for snap2png.pygfx_snapshot_begin("/sdcard/snap000.raw")fopen() → write 8-byte header (width=320, height=480 LE) → pre-fill 307,200 zero bytestruegfx_*(…)gfx_flush(x0, y0, x1, y1, data, n)st7796_flush(…) - display updatedsnap_write(x0, y0, x1, y1, data_swapped, n)fseek to row offset → un-swap bytes → fwrite pixelsgfx_snapshot_end()fclose()true

The raw snapshot file produced by gfx_snapshot_begin / gfx_snapshot_end has the following binary layout:

Offset Size Type Description
────── ──── ──── ───────────
0 4 uint32 LE Screen width (320 in default portrait mode)
4 4 uint32 LE Screen height (480 in default portrait mode)
8 w×h×2 uint16[] RGB565 pixels, native endian, row-major
Row 0 (top) → Row h-1 (bottom)
Each row: pixel[0] (left) → pixel[w-1] (right)

Total file size (320×480 portrait): 8 + 320 × 480 × 2 = 307,208 bytes

The snap_write() function reverses the SPI big-endian byte swap applied by gfx.c drawing functions before writing to the file, so stored values are native-endian RGB565 as consumed by standard image tools:

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

The esp32s3_bzm_tft35_gt911_factory_app_tools snap2png.py script reads this format and produces a standard PNG file.


st7796_init() configures GPIO and SPI before running the ST7796 startup register sequence:

GPIO config

DC(40) + LED(48) + RST(45) as OUTPUT

Backlight OFF during init

spi_bus_free(SPI3_HOST)

Clear stale SPI state from prior firmware

spi_bus_initialize(SPI3_HOST, DMA_AUTO)

max_transfer_sz = SCREEN_WIDTH × SCREEN_HEIGHT × 2

spi_bus_add_device()

speed = 20 MHz, CS = GPIO 41

RST low 10 ms → RST high

⏱ delay 120 ms

CMD 0x11 - Sleep Out

⏱ delay 120 ms

CMD 0x36 + rotation_map[SCREEN_ROTATION]

MADCTL - sets orientation (default: 0x48 portrait)

CMD 0x3A + 0x55

Pixel Format = 16-bit RGB565

CMD 0xF0 + 0xC3

Unlock extension command set 2

CMD 0xE0 - Positive Gamma (14 bytes)

CMD 0xE1 - Negative Gamma (14 bytes)

CMD 0xF0 + 0x3C

Lock extension command set 2

CMD 0x29 - Display ON

⏱ delay 50 ms

Init complete

Caller invokes st7796_backlight(true)

Rotation map (SCREEN_ROTATION → MADCTL byte):

SCREEN_ROTATIONMADCTLLogical SizeOrientation
00x28480 × 320Landscape
1 (default)0x48320 × 480Portrait
20xE8480 × 320Landscape inverted
30x88320 × 480Portrait inverted

⚠️ The portrait default (SCREEN_ROTATION = 1) matches the res_480_320 UI resource set orientation used by the main firmware. It was derived from hardware-confirmed values for the same GT911+ST7796 combination on esp32_3248s035c and has not yet been independently confirmed on this specific board’s hardware.


This module is designed to operate within the constrained DRAM budget of the factory application (no WiFi stack, no Bluetooth).

Stack - allocated per call, freed on return

char_buf[FONT_WIDTH × FONT_HEIGHT]

11 × 21 × 2 = 462 bytes

gfx_draw_char only

spi_transaction_t ~32 bytes

inner loop of st7796_flush

Static / BSS - zero heap cost

line_buf[SCREEN_WIDTH]

320 × 2 = 640 bytes

Reused across ALL drawing calls

s_snap_file FILE*

ENABLE_SNAPSHOT only

spi_dev spi_device_handle_t

Key constraints respected:

  • No malloc() / new in any drawing path.
  • char_buf (462 bytes) is the largest per-call stack allocation; it falls within the 512-byte safe limit defined in the project memory constraints guide.
  • line_buf is a module-level static — the same buffer is reused for every row of every fill or clear operation, eliminating both allocation overhead and fragmentation risk.
  • snap_write() uses fseek/fwrite in a tight per-pixel loop with no intermediate heap buffer; the SD card’s internal buffering handles I/O efficiency.

All pin assignments and display geometry are resolved at compile time from hw_config.h. The factory application does not support runtime reconfiguration.

MacroGPIOPurpose
TFT_HOSTSPI3_HOSTSPI peripheral
TFT_CS41Chip select (active low)
TFT_DC40Data/Command select (low = command, high = data)
TFT_MOSI38SPI data out to display
TFT_CLK39SPI clock
TFT_MISONCNot connected — panel is write-only
TFT_RST45Hardware reset (also shared with GT911 touch RST)
TFT_LED48Backlight enable (active high)
TFT_SPI_FREQ_HZ—20 MHz (conservative; 40 MHz is the main firmware target)
MacroPortrait (default)Landscape
SCREEN_WIDTH320480
SCREEN_HEIGHT480320
SCREEN_ROTATION10 or 2

Touch Controller (GT911, I2C) — for context

Section titled “Touch Controller (GT911, I2C) — for context”

The GT911 touch controller shares the TFT_RST line (GPIO 45). Since st7796_init() already toggles RST low→high to reset the ST7796 panel, no separate GT911 hardware reset is required. Touch is handled by esp32s3_bzm_tft35_gt911_factory_app_input and is not part of this display module.

Buttons (BUTTON_1/2/3_PIN), buzzer (BUZZER_PIN), and encoder (ENCODER_A/B_PIN) are all GPIO_NUM_NC — this board is touch-only.


Understanding the byte-swap pipeline is important when reading or modifying rendering code:

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

applied once per color, before line_buf fill

st7796_flush():

spi_device_polling_transmit

snap_write():

native = (sw >> 8) | (sw << 8)

fwrite(&native, …)

Application color

RGB565 host endian

e.g. COLOR_RED = 0xF800

line_buf[]

Big-endian RGB565

(ST7796 wire format)

ST7796 TFT

receives big-endian RGB565

from SPI MOSI

SD Card file

native-endian RGB565

(directly readable by image tools)

The byte-swap in gfx.c is applied once before filling line_buf or char_buf. Since line_buf is reused across multiple row calls (e.g. gfx_fill_rect calls gfx_flush H times with the same buffer), the swap happens only during buffer initialization, not per-flush — this avoids the double-swap bug that would arise if the driver re-swapped in place. Unlike some RGB-parallel board drivers (e.g. esp32s3_8048s043c), st7796_flush() transmits the pixel data directly to SPI without any additional byte manipulation.


gfx_* API

sdcard_mount() before

snap file path is valid

snap2png.py consumes

.raw files from SD

Same physical panel,

different driver stack

Production runtime

uses esp_lcd + LVGL

esp32s3_bzm_tft35_gt911_factory_app_main

(main.c - menu, actions, snapshot trigger)

esp32s3_bzm_tft35_gt911_factory_app_display

gfx.c + st7796.c

esp32s3_bzm_tft35_gt911_factory_app_storage

sdcard.c

esp32s3_bzm_tft35_gt911_factory_app_input

touch.c · buttons.c · encoder.c · buzzer.c

esp32s3_bzm_tft35_gt911_factory_app_tools

snap2png.py · flash_all.py · …

esp32s3_bzm_tft35_gt911_bsp

board_init.c (LVGL + esp_lcd runtime)

display_spi_drivers

hardware/common/drivers/disp_st7796/

ModuleRelationship
esp32s3_bzm_tft35_gt911_factory_app_mainConsumer — calls all gfx_* functions to render the factory UI; orchestrates the snapshot lifecycle (gfx_snapshot_begin → full redraw → gfx_snapshot_end)
esp32s3_bzm_tft35_gt911_factory_app_storagePeer — mounts the SD card (/sdcard) so that snapshot file paths (e.g. /sdcard/snap000.raw) are valid before gfx_snapshot_begin() is called
esp32s3_bzm_tft35_gt911_factory_app_inputPeer — handles GT911 capacitive touch, buttons, encoder, and buzzer; shares the TFT_RST GPIO (GPIO 45) with the ST7796 display reset
esp32s3_bzm_tft35_gt911_factory_app_toolsTooling — snap2png.py converts the .raw snapshots written by this module to PNG files
esp32s3_bzm_tft35_gt911_bspParallel — the BSP drives the same physical ST7796 panel in the main pendant firmware using the esp_lcd framework + LVGL; this factory module provides an entirely independent, simpler driver
display_spi_driversProduction counterpart — hardware/common/drivers/disp_st7796/st7796.c is the production ST7796 driver used by the BSP; st7796.c in this module reimplements the same init sequence without the esp_lcd/ESP3D-X dependency chain

Comparison with Other Factory Display Modules

Section titled “Comparison with Other Factory Display Modules”

The gfx.c API is identical (same function names, same GFX_RGB565 macro, same COLOR_* constants, same snapshot protocol) across all SPI-based factory display modules in this repository. Only the underlying hardware driver file differs:

Board moduleDisplay ICInterfaceDriver file
esp32s3_bzm_tft35_gt911 (this)ST7796SPI3, 20 MHzst7796.c
pibot_pendant_v1_0ILI9341SPIili9341.c
esp32_3248s035rST7796SPIst7796.c
esp32_3248s035cST7796SPIst7796.c (write-only)
esp32s3_8048s043cST7262RGB parallelst7262.c
esp32s3_8048s050cST7262RGB parallelst7262.c
esp32s3_8048s070cEK9716RGB parallelek9716.c

The font glyph size does vary by board: this board uses an 11×21 font (wider than the 8×16 used by pibot_pendant_v1_0 and the 12×24 used by esp32s3_8048s070c), tuned to the 320-pixel portrait width.


The factory app must stay minimal and free of the main firmware’s component tree. st7796.c uses only the plain ESP-IDF SPI master driver (driver/spi_master.h) and standard GPIO API — no esp_lcd, no esp3d_log, no LVGL.

Static line buffer avoids heap fragmentation

Section titled “Static line buffer avoids heap fragmentation”

line_buf[SCREEN_WIDTH] is allocated statically at module scope. All fill operations (gfx_clear, gfx_fill_rect, gfx_hline) pre-fill the buffer once and reuse it across multiple row-flush calls. This prevents heap fragmentation in a constrained environment and avoids repeated color/swap computation.

Polling transmit over interrupt-driven SPI

Section titled “Polling transmit over interrupt-driven SPI”

spi_device_polling_transmit() is used throughout. The factory app is single-threaded and does not benefit from queued/async SPI — polling avoids FreeRTOS task overhead for short transfers and makes the transfer sequence fully predictable.

spi_bus_free(SPI3_HOST) is called unconditionally before spi_bus_initialize(). This handles the common case where SPI3 retains state from the main firmware’s disp_st7796 driver after a software reset into the factory partition.

st7796_flush() breaks large pixel payloads into 4096-byte chunks. This stays within the SPI DMA hardware transfer size limits regardless of the pixel area being written.

The snapshot file is pre-filled with 307,200 zero bytes (black) at gfx_snapshot_begin() time. This ensures that any screen region not explicitly redrawn during the capture window appears as black rather than containing stale or uninitialized data — critical because there is no RAM framebuffer to copy from.

GPIO 45 is the hardware reset line for both the ST7796 panel and the GT911 touch controller. The st7796_init() toggle (RST low → high) resets both ICs simultaneously. No separate touch reset is issued, which simplifies initialization and matches the pattern used in the esp32_3248s035c/esp32_3248s035r boards.