Skip to content

ESP32-3248S035R Board Support Package (BSP)

ESP32-3248S035R Board Support Package (BSP)

Section titled “ESP32-3248S035R Board Support Package (BSP)”

The esp32_3248s035r_bsp module is the Board Support Package for the ESP32-3248S035R — a 3.5-inch ESP32 development board featuring an ST7796 TFT LCD and an XPT2046 resistive touch controller (the “R” suffix distinguishes it from the “C” capacitive variant). It abstracts all hardware initialization behind a single board_init() entry point, wires LVGL into the display and touch pipelines, and registers the custom control-event codes used by the UI layer.

This BSP is the resistive-touch counterpart to esp32_3248s035c_bsp.md (capacitive touch). Both share the same ST7796 display driver and the same BSP contract, but differ in their touch subsystem and touch-calibration logic.


  1. Hardware Profile
  2. Module Structure
  3. Architecture Overview
  4. Component Reference
  5. Initialization Sequence
  6. Display Pipeline
  7. Touch Pipeline
  8. LVGL Integration
  9. Control Event System
  10. Key Design Decisions
  11. Configuration Constants
  12. Dependencies
  13. Related Modules

FeatureDetail
SoCESP32 (dual-core Xtensa LX6)
Display controllerST7796 (SPI, 3.5-inch)
Display resolution480 × 320 px (landscape physical; portrait-corrected via MADCTL mirror-Y)
Display busSPI2 host, shared with touch
Touch controllerXPT2046 resistive (SPI, same bus as display, own CS pin)
Touch typeResistive — requires linear ADC calibration
Backlight controlGPIO or LEDC PWM (feature-flag ESP3D_BRIGHTNESS_CONTROL_FEATURE)
Color formatRGB565 (16-bit, little-endian byte-swap optional)

Important — orientation correction: The ST7796 on this board requires a mirror-Y-only MADCTL value (0x88) for correct portrait orientation. This value is not reachable through the shared driver’s DISPLAY_ORIENTATION enum, so board_init() calls esp_lcd_panel_mirror(handle, false, true) explicitly after driver initialization. This is a board-specific workaround; the shared ST7796 driver is not modified.


boards/esp32_3248s035r/
├── components/bsp/ ← BSP component (this module)
│ ├── board_init.c ← All hardware init, LVGL wiring
│ ├── board_init.h ← Public BSP API
│ ├── board_config.h ← Pin map and tuning constants
│ ├── control_event.h ← control_event_t struct
│ ├── control_types.c ← Custom LVGL event registration
│ ├── control_types.h ← control_family_t, event code externs
│ └── touch_xpt2046_def.h ← XPT2046 SPI instance & default config
├── Factory/ ← Standalone factory test app
│ └── main/
│ ├── st7796.c / .h ← Factory-only bare-metal ST7796 driver
│ ├── touch.c / .h ← Factory-only bare-metal XPT2046 driver
│ ├── main.c ← Factory app entry point
│ └── ... ← buttons, buzzer, encoder, sdcard, gfx
└── build_scripts/ ← Python variant build automation
├── build_one.py
├── common.py
└── variants.py

The components/bsp/ directory is the only part consumed by the main firmware. The Factory/ subtree is an independent ESP-IDF project used only for production testing. See esp32_3248s035r_factory.md and esp32_3248s035r_build_scripts.md for those sub-modules.


UI_Framework_&_Screens

ESP-IDF

Hardware_Peripheral_Drivers

esp32_3248s035r_bsp (components/bsp/)

st7796_spi_configure()

touch_xpt2046_configure()

backlight_configure()

spi_bus_add_device()

shared bus, own CS

lv_display_create()

lv_display_set_flush_cb()

lv_indev_create()

esp_timer_create()

lv_tick_inc()

on_color_trans_done

notify_lvgl_flush_ready()

touch_xpt2046_spi_read_reg12()

touch_xpt2046_default_config

lv_event_register_id()

activity_process_event()

board_init.c

touch_xpt2046_def.h

control_event.h

control_types.c

disp_st7796

SPI driver

touch_xpt2046

driver

disp_backlight

driver

SPI2 host

spi_bus_*

esp_lcd_panel_io

IO handle

esp_timer

lvgl_tick

LVGL

lv_display_t

lv_indev_t

activity_manager


The central file of this module. All hardware bring-up, driver configuration, and LVGL wiring live here. It exposes a minimal public API through board_init.h.

SymbolSignatureDescription
board_initesp_err_t board_init(void)Top-level initialization; call once from ESP3DX::begin().
board_get_nameconst char *board_get_name(void)Returns BOARD_NAME_STR from board_config.h.
board_get_versionconst char *board_get_version(void)Returns BOARD_VERSION_STR from board_config.h.
get_lvgl_displaylv_display_t *get_lvgl_display(void)Returns the LVGL display handle for use by ESP3DXUi.
get_lvgl_lock_lock_t *get_lvgl_lock(void)Returns the LVGL API mutex for thread-safe UI access.
get_touch_indevlv_indev_t *get_touch_indev(void)Returns the LVGL input device handle (touch).
SymbolDescription
init_lvglInitializes LVGL, creates the display and DMA draw buffer(s), registers the flush callback and tick timer, creates the touch input device.
init_touch_controllerAttaches XPT2046 to the shared SPI bus via spi_bus_add_device(), then calls touch_xpt2046_configure().
lvgl_flush_cbLVGL flush callback: optionally writes pixels to snapshot file, optionally byte-swaps RGB565, then calls esp_lcd_panel_draw_bitmap().
notify_lvgl_flush_readyesp_lcd_panel_io completion callback; calls lv_display_flush_ready() to release the render pipeline.
touch_calibrateLinear rescaling of a raw 12-bit XPT2046 ADC reading to screen coordinates, clamped at zero on the low end.
touch_read_cbLVGL input read callback; reads XPT2046 data, applies calibration, handles the wake-up touch-consume cycle via activity_manager.
increase_lvgl_tickESP timer callback; advances the LVGL tick by LVGL_TICK_PERIOD_MS.
static _lock_t lvgl_api_lock; // LVGL thread-safety mutex
static lv_display_t *lvgl_display; // LVGL display handle
static lv_color16_t *lvgl_buf1; // Primary DMA draw buffer
static lv_color16_t *lvgl_buf2; // Secondary DMA draw buffer (optional)
static esp_timer_handle_t lvgl_tick_timer; // Periodic LVGL tick timer
static lv_indev_t *touch_indev; // LVGL touch input device

Provides compile-time configuration and the SPI I/O function for the XPT2046 resistive touch controller.

static inline uint16_t touch_xpt2046_spi_read_reg12(uint8_t reg);

Performs a 16-bit half-duplex SPI transaction using the device handle touch_xpt2046_spi_device (populated at runtime by init_touch_controller()). Returns the 12-bit ADC result shifted down from the 16-bit frame, or 0 on error.

Protocol detail: XPT2046 uses an 8-bit command byte followed by 16 RX bits; bits [15:3] hold the 12-bit result (>> 3).

ConstantPurpose
TOUCH_SPI_FREQ_HZXPT2046 SPI clock speed
TOUCH_SPI_CS_PINGPIO for touch CS (separate from display CS)
TOUCH_IRQ_PINGPIO for touch interrupt / pen-down detection
TOUCH_THRESHOLDMinimum pressure value to qualify a touch
TOUCH_SWAP_XY_FLAGXY axis swap for physical orientation
TOUCH_MIRROR_X_FLAG / TOUCH_MIRROR_Y_FLAGAxis inversion flags
TOUCH_X_MAX / TOUCH_Y_MAXLogical screen dimensions
TOUCH_CALIBRATION_X_MIN/MAXADC raw values at screen edges (X axis)
TOUCH_CALIBRATION_Y_MIN/MAXADC raw values at screen edges (Y axis)
// Runtime device handle — NULL until spi_bus_add_device() in board_init.c
static spi_device_handle_t touch_xpt2046_spi_device = NULL;
// Passed to spi_bus_add_device() — half-duplex, 8-bit command field
const spi_device_interface_config_t touch_xpt2046_spi_device_config = { ... };
// Passed to touch_xpt2046_configure()
const touch_xpt2046_config_t touch_xpt2046_default_config = { ... };

Defines the unified control event payload carried by all physical input events dispatched to the UI layer via LVGL’s custom event system.

typedef struct {
lv_indev_t *indev; // Source LVGL input device
uint32_t btn_id; // Button / switch identifier
lv_indev_type_t type; // LVGL input type (pointer, encoder, …)
control_family_t family_id; // Device class (touch, encoder, switch, potentiometer)
int32_t steps; // Encoder step count (signed)
uint32_t press_duration; // Duration of a hold press in milliseconds
} control_event_t;

This struct is the data payload attached to custom lv_event_t objects. UI screens receive it via lv_event_get_param().

On this board, only indev, type (pointer), and the calibrated coordinates passed through the standard LVGL pointer path are active. The steps, btn_id, family_id, and press_duration fields exist for API parity with richer boards such as pibot_pendant_v1_0_bsp.md that expose encoders and physical switches.


Registers board-specific custom LVGL event codes after lv_init() has been called.

// Defined in control_types.h — extern across the firmware
lv_event_code_t LV_EVENT_SWITCH_PRESSED;
lv_event_code_t LV_EVENT_SWITCH_RELEASED;
lv_event_code_t LV_EVENT_POTENTIOMETER_CHANGED;
void control_events_init(void); // Called by board_init() after init_lvgl()

lv_event_register_id() assigns runtime numeric IDs (starting after LV_EVENT_LAST). These IDs are stable for the duration of a power cycle but are not persisted. The UI layer dispatches and listens for these codes through the standard lv_event_send() / lv_obj_add_event_cb() API.


esp_timerLVGLESP-IDF SPI bustouch_xpt2046 driverst7796_spi driverbacklight driveractivity_managerboard_init()ESP3DX::begin()esp_timerLVGLESP-IDF SPI bustouch_xpt2046 driverst7796_spi driverbacklight driveractivity_managerboard_init()ESP3DX::begin()ESP3D_DISPLAY_FEATUREESP3D_TOUCH_FEATUREESP3D_TOUCH_FEATUREboard_init()activity_manager_init()backlight_configure()backlight_set(0) [off during init]st7796_spi_configure()spi_bus_initialize(SPI2)panel_handle, io_handleesp_lcd_panel_mirror(false, true) [mirror-Y fix]spi_bus_add_device(XPT2046 CS)touch_xpt2046_configure()lv_init()lv_display_create(480×320)heap_caps_malloc(DMA) × 1 or 2lv_display_set_buffers()lv_display_set_flush_cb(lvgl_flush_cb)esp_timer_create(increase_lvgl_tick)esp_timer_start_periodic(LVGL_TICK_PERIOD_MS)esp_lcd_panel_io_register_event_callbacks(notify_lvgl_flush_ready)lv_indev_create() [type=POINTER]lv_indev_set_read_cb(touch_read_cb)control_events_init()ESP_OK

The display uses the SPI panel model from ESP-IDF’s esp_lcd component, connected through the shared disp_st7796 driver from display_spi_drivers.md.

lvgl_flush_cb()

on_color_trans_done

IRQ

LVGL render

(Core 1)

Snapshot

hook

(optional)

RGB565

byte-swap

(optional)

esp_lcd_panel_draw_bitmap()

SPI DMA transfer

ST7796

panel (SPI2)

3.5 inch TFT LCD

480x320

notify_lvgl_flush_ready()

lv_display_flush_ready()

Key rendering properties:

  • Render mode: LV_DISPLAY_RENDER_MODE_PARTIAL — LVGL renders only dirty regions.
  • Buffer size: DISPLAY_WIDTH_PX × DISPLAY_BUFFER_LINES_NB × 2 bytes, allocated from DMA-capable internal SRAM (MALLOC_CAP_DMA).
  • Double buffer: controlled by DISPLAY_USE_DOUBLE_BUFFER_FLAG. When enabled, LVGL can prepare the next frame while the current one is transferring over SPI.
  • Color format: LV_COLOR_FORMAT_RGB565.
  • Color-byte swap: DISPLAY_SWAP_COLOR_FLAG controls whether lv_draw_sw_rgb565_swap() is called before draw_bitmap().

The XPT2046 resistive controller shares the SPI bus with the ST7796 display. Both are separate logical devices on SPI2_HOST, differentiated by their CS pins.

SPI half-duplex

own CS pin

is_pressed?

screen active

first touch = wake-up

finger released

XPT2046

hardware ADC

12-bit X/Y/Z

touch_xpt2046_read()

touch_read_cb()

LVGL timer, every 10 ms

touch_calibrate()

raw to screen coords

linear rescale + clamp

activity_process_event()

wake-up detection

LVGL pointer event

LV_INDEV_STATE_PRESSED

(x, y)

Touch consumed

for wake-up

LV_INDEV_STATE_RELEASED

touch_consumed_for_wakeup = false

Resumes normal reporting

The XPT2046 returns raw 12-bit ADC values that do not directly map to screen pixels. The touch_calibrate() function performs a linear rescaling per axis:

screen_coord = (raw - cal_min) * range / (cal_max - cal_min)

Where cal_min and cal_max are the measured ADC values at the physical screen edges (stored in board_config.h as TOUCH_CALIBRATION_X_MIN/MAX and TOUCH_CALIBRATION_Y_MIN/MAX), and range is the screen dimension (TOUCH_X_MAX or TOUCH_Y_MAX). Values below cal_min are clamped to zero.

The Factory app (boards/esp32_3248s035r/Factory/main/touch.c) uses an identical algorithm under the name calibrate(), ensuring consistent touch mapping between the factory test and the main firmware.

The touch callback integrates with the activity_manager to handle the display timeout/wake-up cycle:

  1. First touch after screen dim/sleep: activity_process_event() returns false → the touch is flagged as a wake-up activator and not forwarded to LVGL. The display wakes up without triggering an unintended UI action.
  2. Touch hold or subsequent touches: Reported normally to LVGL as LV_INDEV_STATE_PRESSED with calibrated coordinates.
  3. Touch release after wake-up: The consumed flag is cleared silently; no spurious release event is generated.

The BSP wires LVGL to the hardware without any involvement from the UI layer itself. All glue code lives in board_init.c.

UI Layer Responsibility (ESP3DXUi)

BSP Responsibility

board_init()

lv_display_create()

lv_display_set_buffers()

lv_display_set_flush_cb()

esp_timer → increase_lvgl_tick()

lv_tick_inc() every LVGL_TICK_PERIOD_MS

lv_indev_create()

lv_indev_set_read_cb(touch_read_cb)

read timer = 10 ms

_lock_t lvgl_api_lock

exposed via get_lvgl_lock()

get_lvgl_display()

get_lvgl_lock()

get_touch_indev()

lv_timer_handler()

in tft_ui_task (Core 1)

Thread-safety rule: lv_timer_handler() runs exclusively on Core 1 inside tft_ui_task. The lvgl_api_lock must be held by any other task or ISR that calls LVGL APIs. The BSP exposes this lock via get_lvgl_lock() so the UI layer can manage it. See ui_core.md for the full task architecture.

Snapshot feature (ESP3D_SNAPSHOT_FEATURE): When enabled, lvgl_flush_cb() intercepts each flush call and writes pixel data in 120-byte chunks to a file handle stored in g_snapshot. The snapshot completes when captured_pixels >= expected_pixels. The mutex g_snapshot.mutex is taken with a zero timeout so flush latency is unaffected when the snapshot system is idle.


lv_event_register_id

lv_event_register_id

lv_event_register_id

lv_event_send(obj, code, &control_event_t)

lv_event_send(obj, code, &control_event_t)

lv_event_send(obj, code, &control_event_t)

control_events_init()

called by board_init()

LV_EVENT_SWITCH_PRESSED

LV_EVENT_SWITCH_RELEASED

LV_EVENT_POTENTIOMETER_CHANGED

UI Screens

(lv_obj_add_event_cb)

The three custom event codes are defined for API parity with boards that have physical switches and potentiometers. On the ESP32-3248S035R, only the resistive touch path is active in normal use. The switch and potentiometer events exist because the same UI screens are shared across all supported boards.

The control_event_t payload is always stack-allocated by the caller and passed by pointer as the LVGL event user data. UI screens retrieve it with lv_event_get_param().


Both the ST7796 display and the XPT2046 touch controller share SPI2_HOST. The display driver calls spi_bus_initialize() first; init_touch_controller() then calls spi_bus_add_device() to register XPT2046 as a second device on the already-initialized bus. The ESP-IDF SPI master driver serializes access per transaction, so no explicit application-level locking is needed.

The physical panel requires MADCTL 0x88 (portrait with mirror-Y). The shared st7796 driver exposes only a 4-entry DISPLAY_ORIENTATION enum that cannot express this exact combination. Rather than modifying the shared driver (which would affect other boards), board_init() calls esp_lcd_panel_mirror(handle, false, true) immediately after st7796_spi_configure(). This is explicitly documented in the source with a comment referencing the Factory app’s raw MADCTL value for cross-verification.

heap_caps_malloc(size, MALLOC_CAP_DMA) is used for LVGL draw buffers instead of plain malloc. The SPI DMA engine on ESP32 requires the source buffer to be in internal SRAM reachable by DMA (not external PSRAM or stack). Failure to allocate is treated as a fatal error returned from board_init().

The first touch after a screen timeout must not trigger a UI action — it should only wake the display. The touch_consumed_for_wakeup static flag in touch_read_cb() implements this pattern cleanly:

  • activity_process_event() returns false when the display is sleeping → sets the flag.
  • The flag suppresses forwarding of the press and the corresponding release to LVGL.
  • The flag is cleared on finger-up without generating a spurious release event.

This same pattern appears across all touch-enabled BSPs in the project.


All board-specific constants are defined in board_config.h (part of the BSP component but not in this module’s exported API). The BSP reads them at compile time. The most critical ones are:

CategoryConstantUsage in BSP
DisplayDISPLAY_WIDTH_PX, DISPLAY_HEIGHT_PXlv_display_create()
DisplayDISPLAY_BUFFER_LINES_NBDraw buffer height in lines
DisplayDISPLAY_USE_DOUBLE_BUFFER_FLAGEnables second DMA buffer
DisplayDISPLAY_SWAP_COLOR_FLAGRGB565 byte-swap before DMA
DisplayDISPLAY_SPI_HOST_IDXSPI host shared by display and touch
TouchTOUCH_CALIBRATION_X_MIN/MAXXPT2046 ADC range for X axis
TouchTOUCH_CALIBRATION_Y_MIN/MAXXPT2046 ADC range for Y axis
TouchTOUCH_X_MAX, TOUCH_Y_MAXScreen pixel dimensions for calibration output
TouchTOUCH_SPI_FREQ_HZ, TOUCH_SPI_CS_PINXPT2046 SPI parameters
TouchTOUCH_IRQ_PIN, TOUCH_THRESHOLDPen-down detection
TouchTOUCH_SWAP_XY_FLAG, TOUCH_MIRROR_X/Y_FLAGCoordinate transform
LVGLLVGL_TICK_PERIOD_MSesp_timer period for lv_tick_inc()

Core_Platform

LVGL

ESP-IDF components

Hardware_Peripheral_Drivers

ESP3D_SNAPSHOT_FEATURE

esp32_3248s035r_bsp

disp_st7796

SPI driver

touch_xpt2046

SPI touch

disp_backlight

GPIO/LEDC

driver/spi_master

driver/gpio

esp_timer

esp_lcd

esp_heap_caps

lvgl

activity_manager

esp3d_log

esp3d_snapshot

(optional)

DependencyWhere used
disp_st7796Display driver — st7796_spi_configure(), st7796_spi_get_panel_handle(), st7796_spi_get_io_handle()
touch_xpt2046Touch driver — touch_xpt2046_configure(), touch_xpt2046_read()
disp_backlightBacklight — backlight_configure(), backlight_set() (conditional on ESP3D_BRIGHTNESS_CONTROL_FEATURE)
activity_managerWake-up event gate in touch_read_cb() and board_init()
esp3d_logAll diagnostic output via esp3d_log() / esp3d_log_e()
esp3d_snapshotScreen capture hook inside lvgl_flush_cb() (conditional on ESP3D_SNAPSHOT_FEATURE)
ESP-IDF spi_masterBus initialization, device attach, transaction execution
ESP-IDF esp_lcdPanel I/O handle, esp_lcd_panel_draw_bitmap(), IO callbacks
ESP-IDF esp_timerPeriodic LVGL tick timer
LVGLDisplay, input device, and event system

ModuleRelationship
esp32_3248s035r_factory.mdFactory test app for the same board; uses its own bare-metal ST7796 and XPT2046 drivers, fully independent of this BSP.
esp32_3248s035r_build_scripts.mdPython build automation for all firmware variants of this board.
esp32_3248s035c_bsp.mdCapacitive-touch variant BSP (same ST7796 display, no resistive touch_calibrate() logic).
display_spi_drivers.mdShared disp_st7796 and disp_backlight hardware drivers consumed by this BSP.
touch_controllers.mdShared touch_xpt2046 driver consumed by this BSP.
ui_core.mdESP3DXUi / UIManager — consumes get_lvgl_display(), get_lvgl_lock(), get_touch_indev() provided by this BSP.
esp3d_activity_manager.mdActivity/wake-up manager called from touch_read_cb() and board_init().
pibot_pendant_v1_0_bsp.mdMore complex BSP with full physical input suite; shows how control_event_t fields (encoder steps, switch ID, potentiometer) are fully populated.

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.