Skip to content

esp32s3_bzm_tft35_gt911_bsp

Board Support Package (BSP) for the ESP32-S3 BZM TFT 3.5″ board, which pairs an ST7796-driven SPI display with a GT911 capacitive touch controller. The BSP bridges the raw hardware drivers to LVGL and the ESP3D application layer, and is accompanied by a standalone factory application and a Python-based build-variant system.


  1. Hardware Overview
  2. Module Structure
  3. Architecture
  4. BSP Component Reference
  5. Initialization Sequence
  6. Data Flow
  7. Factory Application
  8. Build System
  9. Configuration Reference
  10. Integration with the Application Layer
  11. Related Modules

AttributeValue
MCUESP32-S3 (WROOM module)
Display controllerST7796
Display interfaceSPI (hardware SPI with DMA)
Display color formatRGB565 (16-bit)
Touch controllerGT911
Touch interfaceI2C
SD card interfaceSPI
Backlight controlGPIO (optional PWM via LEDC)
EncoderQuadrature via ESP32-S3 PCNT (optional)
ButtonsUp to 3, active-LOW
BuzzerGPIO-driven passive buzzer

The board exposes a 3.5″ TFT with capacitive multi-touch. Unlike the resistive-touch boards in the same family (see esp32_3248s035r), no calibration is required for the GT911 — coordinates are read directly from the controller over I2C.


boards/esp32s3_bzm_tft35_gt911/
├── components/
│ └── bsp/ ← BSP component (runtime firmware)
│ ├── board_init.c ← Master HW + LVGL initialization
│ ├── board_init.h
│ ├── board_config.h ← Pin assignments & display constants
│ ├── control_event.h ← control_event_t definition
│ ├── control_types.c ← Custom LVGL event registration
│ ├── control_types.h
│ ├── tasks_def.h ← FreeRTOS task priorities/stack sizes
│ ├── disp_st7796_spi_def.h ← ST7796 SPI config instance
│ ├── touch_gt911_def.h ← GT911 I2C config instance
│ └── disp_backlight_def.h ← Backlight config instance
├── Factory/
│ ├── bootloader_components/
│ │ └── custom_bootloader/
│ │ └── hooks.c ← Factory bootloader hooks
│ ├── custom_bootloader/
│ │ └── hooks.c ← OTA-backup bootloader hooks
│ ├── main/
│ │ ├── main.c ← Factory app entry point
│ │ ├── st7796.c / .h ← Bare-metal ST7796 driver
│ │ ├── touch.c / touch.h ← GT911 wrapper (factory context)
│ │ ├── gfx.c / .h ← Software framebuffer & primitives
│ │ ├── buttons.c / .h ← GPIO button driver
│ │ ├── buzzer.c / .h ← Passive buzzer driver
│ │ ├── encoder.c / .h ← Quadrature encoder (PCNT)
│ │ ├── sdcard.c / .h ← SPI SD card (FAT)
│ │ └── factory_log.h ← Factory logging control
│ └── tools/
│ ├── flash_all.py ← Flash complete image set
│ └── flash_factory.py ← Flash factory app only
└── build_scripts/
├── build_one.py ← Single-variant build entry
├── common.py ← Shared build helpers
└── variants.py ← Firmware variant definitions

ESP3D Services

LVGL Stack

Hardware Drivers

hardware/common/drivers

BSP Component

boards/esp32s3_bzm_tft35_gt911/components/bsp

Application Layer

board_init

get_lvgl_display

get_lvgl_lock

st7796_spi_configure

bus_i2c_init

touch_gt911_configure

backlight_configure

lv_init, lv_display_create

lv_indev_create

touch_read_cb

lvgl_flush_cb

notify_lvgl_flush_ready

control_events_init

ESP3DX / UIManager

(main firmware)

board_init.c

board_init()

control_event.h

control_event_t

control_types.c

control_events_init()

disp_st7796_spi

ST7796 SPI Panel

touch_gt911

GT911 I2C Touch

disp_backlight

Backlight Control

bus_i2c

Shared I2C Bus

LVGL Core

lv_init / lv_display

lv_indev

Touch Input Device

activity_manager

Wake-up Detection

esp3d_snapshot

Screen Capture

Core 1 - UI (LVGL)

lv_tick_inc() every LVGL_TICK_PERIOD_MS

lvgl_flush_cb

notify_lvgl_flush_ready

(ISR callback)

touch_read_cb

(10 ms poll)

Core 0 - Network & CNC

Network / Serial Task

GCode Host Task

tft_ui_task

lv_task_handler()

esp_timer

increase_lvgl_tick()

ST7796 Display

(SPI + DMA)

GT911 Touch

(I2C)

LVGL must run entirely on Core 1. The _lock_t lvgl_api_lock mutex (returned by get_lvgl_lock()) must be held by any Core 0 code that calls LVGL APIs.


File: boards/esp32s3_bzm_tft35_gt911/components/bsp/board_init.c

Top-level hardware initialization. Called once from ESP3DX::begin() before the UI task starts.

esp_err_t board_init(void);

Sequence of operations:

  1. activity_manager_init() — initializes the inactivity / wake-up tracker
  2. (if ESP3D_BRIGHTNESS_CONTROL_FEATURE) backlight_configure() + backlight_set(0) — backlight off during init
  3. st7796_spi_configure() — configures the SPI bus and ST7796 panel
  4. init_touch_controller() — configures the I2C bus and GT911
  5. init_lvgl() — creates the LVGL display, allocates draw buffers, starts the tick timer, registers flush and indev callbacks
  6. control_events_init() — registers custom LVGL event IDs

Returns ESP_OK on success; the first failing step returns its esp_err_t immediately.

Accessor functions (used by ESP3DXUi):

FunctionReturns
get_lvgl_display()lv_display_t* — the active LVGL display handle
get_lvgl_lock()_lock_t* — mutex for LVGL API access from other cores
get_touch_indev()lv_indev_t* — the LVGL touch input device

The ST7796 panel is initialized by the shared disp_st7796_spi driver (see display_drivers_spi). Configuration is supplied by disp_st7796_spi_def.h (board-specific pin/timing instance of spi_st7796_config_t).

ParameterDescription
spi_bus.hostSPI host index (SPI2_HOST or SPI3_HOST)
spi_bus.mosi/miso/sclk/cs/dc/rstGPIO pin assignments
spi_bus.clock_speed_hzSPI clock (typically 40–80 MHz)
display.width / heightPanel resolution
display.orientationRotation mapped to MADCTL register
interface.swap_color_bytesRGB565 byte-swap for DMA alignment

Buffers are allocated from DMA-capable DRAM (MALLOC_CAP_DMA):

draw_buf_size = DISPLAY_WIDTH_PX × DISPLAY_BUFFER_LINES_NB × sizeof(lv_color16_t)

Double-buffering is controlled by DISPLAY_USE_DOUBLE_BUFFER_FLAG. When enabled, lvgl_buf2 is allocated alongside lvgl_buf1, allowing DMA transfers to overlap with the next render pass.

Called by LVGL when a dirty region must be pushed to the display:

static void lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map);
  1. (if ESP3D_SNAPSHOT_FEATURE) Captures pixel data to an open FILE* in g_snapshot in 120-byte chunks
  2. (if DISPLAY_SWAP_COLOR_FLAG) Byte-swaps the RGB565 buffer in-place via lv_draw_sw_rgb565_swap()
  3. Calls esp_lcd_panel_draw_bitmap() to push the rectangle over SPI/DMA

Registered as esp_lcd_panel_io_callbacks_t.on_color_trans_done. Fires when the SPI DMA transfer completes and calls lv_display_flush_ready() to unblock LVGL.

static bool notify_lvgl_flush_ready(esp_lcd_panel_io_handle_t panel_io,
esp_lcd_panel_io_event_data_t *edata,
void *user_ctx);

Periodic esp_timer callback that advances the LVGL timebase:

static void increase_lvgl_tick(void *arg); // calls lv_tick_inc(LVGL_TICK_PERIOD_MS)

Period: LVGL_TICK_PERIOD_MS milliseconds (defined in tasks_def.h).


Guarded by ESP3D_TOUCH_FEATURE. When disabled, all touch symbols are omitted.

The GT911 is a capacitive multi-touch controller. Only the first touch point is used by LVGL. Configuration is supplied by touch_gt911_def.h (board-specific instance of touch_gt911_config_t).

FieldDescription
i2c_addr[3]Candidate I2C addresses (0x5D / 0x14), 0-terminated
i2c_portI2C port index
i2c_clk_speedI2C frequency (typically 400 kHz)
rst_pin / int_pinReset and interrupt GPIO pins
swap_xy / invert_x / invert_yCoordinate orientation corrections
x_max / y_max0 = auto-detected from device registers
static esp_err_t init_touch_controller(void);
  1. bus_i2c_init() — initializes the shared I2C bus
  2. touch_gt911_configure() — programs the GT911 and verifies I2C communication

Registered as the LVGL input device read callback. Called every 10 ms (configured via lv_timer_set_period).

static void touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data);

Wake-up logic: The first touch event after the screen has been idle is forwarded to activity_process_event(). If the activity manager indicates a wake-up transition, the touch is consumed — it resets the idle timer but does not dispatch a press event to LVGL. This prevents an accidental tap from triggering UI actions after the screen wakes.

No

Yes

Yes

No

No

Yes

Yes

Yes

No

No

No

Yes

touch_read_cb called

touch_data.is_pressed?

was previously pressed?

was consumed

for wakeup?

Clear wakeup flag

Log: release ignored

activity_process_event()

Log: released

data->state = RELEASED

first press

(not previously pressed)?

activity_process_event()

returned true

(already active)?

data->state = PRESSED

forward coords

consumed_for_wakeup = true

data->state = RELEASED

Log: consumed

consumed_for_wakeup?


Files:

  • control_event.h — control_event_t structure
  • control_types.c — control_events_init() and custom event IDs

Unified structure passed with any custom LVGL control event:

typedef struct {
lv_indev_t *indev; // Source input device
uint32_t btn_id; // Button / switch ID
lv_indev_type_t type; // POINTER, ENCODER, BUTTON, …
control_family_t family_id; // SWITCH, ENCODER, POTENTIOMETER, …
int32_t steps; // Encoder step delta
uint32_t press_duration; // Duration in ms (buttons)
} control_event_t;

Three custom LVGL event codes are registered after lv_init():

Global VariableMeaning
LV_EVENT_SWITCH_PRESSEDA physical switch was pressed
LV_EVENT_SWITCH_RELEASEDA physical switch was released
LV_EVENT_POTENTIOMETER_CHANGEDAnalog potentiometer value changed

IDs are assigned dynamically by lv_event_register_id() and stored in module-level lv_event_code_t globals. This board does not use the encoder or potentiometer paths in the main BSP (these are available for future extension), but the events are pre-registered so that the application layer can subscribe to them uniformly across all supported boards.

Note: control_events_init() must be called after lv_init(). board_init() guarantees this ordering.


esp_timerLVGLtouch_gt911bus_i2cST7796 SPI driverbacklightactivity_managerboard_init()ESP3DX::begin()esp_timerLVGLtouch_gt911bus_i2cST7796 SPI driverbacklightactivity_managerboard_init()ESP3DX::begin()board_init()activity_manager_init()ESP_OKbacklight_configure() + backlight_set(0)ESP_OKst7796_spi_configure(st7796_default_config)ESP_OK + panel_handle + io_handlebus_i2c_init(port, sda, scl, freq)ESP_OKtouch_gt911_configure(config)ESP_OKlv_init()lv_display_create(WIDTH, HEIGHT)lvgl_displayheap_caps_malloc(buf1, MALLOC_CAP_DMA)heap_caps_malloc(buf2, MALLOC_CAP_DMA) [if double buffer]lv_display_set_buffers(buf1, buf2, size, PARTIAL)lv_display_set_flush_cb(lvgl_flush_cb)lv_display_set_color_format(RGB565)esp_timer_create(increase_lvgl_tick)lvgl_tick_timeresp_timer_start_periodic(LVGL_TICK_PERIOD_MS * 1000)esp_lcd_panel_io_register_event_callbacks(notify_lvgl_flush_ready)ESP_OKlv_indev_create() + set_type(POINTER) + set_read_cb(touch_read_cb)lv_timer_set_period(indev_timer, 10ms)control_events_init()ESP_OK

Core 1

dirty region

optional

RGB565 byte-swap

optional

snapshot write

(120-byte chunks)

esp_lcd_panel_draw_bitmap()

on_color_trans_done ISR

LVGL

lv_task_handler()

lvgl_flush_cb()

FILE* on flash/SD

SPI DMA transfer

notify_lvgl_flush_ready()

lv_display_flush_ready()

ST7796 Panel

3.5 inch TFT

activity check

wake-up: consume

already active: pass

Finger on glass

GT911 (I2C)

touch_gt911_read()

touch_read_cb()

every 10 ms

activity_manager

activity_process_event()

Suppress press event

lv_indev_data_t

state=PRESSED, x, y

LVGL gesture

& routing


Location: boards/esp32s3_bzm_tft35_gt911/Factory/

The factory application is a standalone ESP-IDF project compiled separately from the main firmware. It runs directly on the hardware without LVGL, using bare-metal SPI writes for the display and a software framebuffer (gfx.c).

  • Hardware acceptance testing after PCB assembly
  • SD-card-based firmware update workflow
  • OTA partition management (backup/restore otadata)
  • Screen snapshot capture for visual verification
ModuleFile(s)Description
Displayst7796.cBare-metal SPI init + flush (no LVGL)
Touchtouch.c / touch.hGT911 wrapper returning touch_point_t
Graphicsgfx.cSoftware framebuffer, primitives, snapshot
Buttonsbuttons.cGPIO active-LOW, button_is_pressed() / button_wait_press()
Encoderencoder.cPCNT quadrature decoder, GPIO_NUM_NC guard
Buzzerbuzzer.cGPIO passive buzzer, buzzer_beep_short()
SD Cardsdcard.cSPI FAT mount/unmount
Mainmain.cMenu system, OTA, SD update, snapshots

st7796_init() performs a full hardware sequence:

  1. Configure DC / RST / backlight GPIO outputs
  2. Initialize SPI2 bus + attach device at TFT_SPI_FREQ_HZ
  3. Hardware reset via RST pin (10 ms low → 120 ms recovery)
  4. SLPOUT (0x11) + 120 ms
  5. MADCTL (0x36) — orientation from rotation_map[]
  6. COLMOD (0x3A) — pixel format 0x55 (RGB565)
  7. Positive/negative gamma curves (0xE0 / 0xE1)
  8. DISPON (0x29)

st7796_flush() transfers pixel data in 4 096-byte chunks via polling SPI to respect DMA size limits.

Two bootloader override layers exist:

PathHookPurpose
Factory/bootloader_components/custom_bootloader/hooks.cbootloader_before_init / bootloader_after_initLightweight hooks framework
Factory/custom_bootloader/hooks.cbackup_and_erase_otadata + is_button_pressedDetects button hold at boot; backs up otadata and erases it to force factory app on next reset
ScriptDescription
tools/flash_all.pyFlash bootloader + partition table + factory app + full firmware
tools/flash_factory.pyFlash factory app partition only

Location: boards/esp32s3_bzm_tft35_gt911/build_scripts/

The build system follows the same pattern as all other boards in the project.

ScriptCLI UsageDescription
build_one.pypython build_one.py [--clean]Build a single variant
build_one.pypython build_one.py --checkCMake dry-run (no compile)

Yes

No

variants.py

make_variant_args()

Build config dict

{name, cwd, build_dir, cmake}

common.py

build_variant()

--clean flag?

clean_build_dir()

clean installer dir

generate_resources()

ui_resources partition

run_cmake_build()

_show_size_report()

copy_factory_artifacts()

_package_user_resources_kit()

generate_flash_map()

make_variant_args() starts from DEFAULT_OFF_CMAKE_ARGS (all optional features OFF) and selectively enables flags:

def make_variant_args(*on_flags):
args = DEFAULT_OFF_CMAKE_ARGS.copy()
for flag in on_flags:
args.extend(["-D", flag])
return args

The check_variant() function runs a CMake configuration pass (run_cmake_check) without compiling — used in CI to verify sdkconfig and CMake logic without a full build.


MacroDescription
BOARD_NAME_STRHuman-readable board name
BOARD_VERSION_STRBoard version string
DISPLAY_WIDTH_PXHorizontal resolution
DISPLAY_HEIGHT_PXVertical resolution
DISPLAY_BUFFER_LINES_NBNumber of scanlines per LVGL draw buffer
DISPLAY_USE_DOUBLE_BUFFER_FLAG1 = allocate two buffers for DMA overlap
DISPLAY_SWAP_COLOR_FLAG1 = byte-swap RGB565 before SPI transfer
LVGL_TICK_PERIOD_MSLVGL tick interval in milliseconds
MacroDescription
TOUCH_I2C_PORT_IDXI2C peripheral index
TOUCH_I2C_SDA_PINSDA GPIO
TOUCH_I2C_SCL_PINSCL GPIO
TOUCH_I2C_FREQ_HZI2C clock (e.g. 400000)
TOUCH_RST_PINGT911 reset GPIO
TOUCH_IRQ_PINGT911 interrupt GPIO
TOUCH_SWAP_XY_FLAGMirror X/Y axes in GT911 config
TOUCH_MIRROR_X_FLAGInvert X coordinate
TOUCH_MIRROR_Y_FLAGInvert Y coordinate
Feature DefineEffect when enabled
ESP3D_DISPLAY_FEATURECompiles display + LVGL init path
ESP3D_TOUCH_FEATURECompiles GT911 init + touch_read_cb
ESP3D_BRIGHTNESS_CONTROL_FEATURECompiles backlight configure call
ESP3D_SNAPSHOT_FEATURECompiles pixel-capture block in lvgl_flush_cb

See touch_drivers for full field documentation.

The BSP supplies a board-specific default config instance in touch_gt911_def.h, referenced as touch_gt911_default_config.

9.5 ST7796 SPI Driver (spi_st7796_config_t)

Section titled “9.5 ST7796 SPI Driver (spi_st7796_config_t)”

See display_drivers_spi for full field documentation.

The BSP supplies a board-specific default config instance in disp_st7796_spi_def.h, referenced as st7796_default_config.

See display_drivers_spi for full field documentation.

The BSP supplies a board-specific default config instance in disp_backlight_def.h, referenced as backlight_cfg.


10. Integration with the Application Layer

Section titled “10. Integration with the Application Layer”

main.cpp

app_main()

ESP3DX::begin()

board_init()

Hardware ready

esp3d_x_ui.cpp

tft_ui_task() on Core 1

_lock_acquire(get_lvgl_lock())

lv_task_handler()

_lock_release()

get_lvgl_display()

theme init, screen creation

The UI task acquires get_lvgl_lock() around every lv_task_handler() call. Any code on Core 0 that must call LVGL APIs (e.g., value observers) must also acquire the same lock. This single mutex guards the entire LVGL call graph.

Refer to:

  • lvgl_system — esp3d_lvgl.cpp, lv_timer_pause_all / lv_timer_resume_all
  • ui_core — UIManager, screen management, theme system
  • activity_manager — inactivity timeout, wake-up policy

ModuleDocumentationRelationship
ST7796 / ILI9341 SPI driversdisplay_drivers_spiHardware display driver used by this BSP
GT911 / FT5x06 / XPT2046 touch driverstouch_driversHardware touch driver used by this BSP
Backlight driverdisplay_drivers_spiPWM/GPIO backlight control
I2C bus drivercommunication_bus_driversShared I2C bus for GT911
Activity manageractivity_managerWake-up / inactivity detection
LVGL corelvgl_systemGraphics engine integrated by this BSP
LVGL displaylvgl_displaylv_display_t lifecycle
LVGL input deviceinput_devicelv_indev_t and read callback
LVGL event systemlvgl_event_systemlv_event_register_id() used by control_events_init()
UI core / UIManagerui_coreConsumes get_lvgl_display() and get_lvgl_lock()
Pibot Pendant v1.0 BSPpibot_pendant_v1_0_bspReference SPI-display BSP with encoder + buttons
ESP32 3248S035R BSPesp32_3248s035r_bspSimilar SPI + ST7796 board with resistive touch (XPT2046)
ESP32S3 4827S043C BSPesp32s3_4827s043c_bspSimilar ESP32-S3 board with RGB parallel display and FT5x06 touch
Factory documentationdocs/FactoryFactory app architecture and OTA bootloader
Display drivers architecturedocs/architecture/display_drivers.mdSPI vs RGB panel driver patterns

components/bsp/lv_conf.h was regenerated from the LVGL 9.6.0 template (Configuration file for v9.6.0), and the sdkconfig files set CONFIG_LV_CONF_SKIP=n so this file is actually used (see Board_Support_Packages.md). The project values are kept (LV_MEM_SIZE 48 KB, monitors positions), LV_USE_COLOR_FILTER is 0 (color filters are not used: pressed feedback is a darker local style, see ui_components.md). LV_COLOR_DEPTH is no longer set in the file: LVGL takes CONFIG_LV_COLOR_DEPTH, or 16 by default.