Skip to content

ESP32-3248S035C Board Support Package (BSP)

ESP32-3248S035C Board Support Package (BSP)

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

The esp32_3248s035c_bsp module is the hardware abstraction layer for the ESP32-3248S035C development board — a 3.5-inch, 320×480 portrait-orientation touch display board built around the original ESP32 SoC. The “C” suffix denotes capacitive touch (GT911 controller), distinguishing it from the resistive-touch sibling esp32_3248s035r_bsp.

The BSP’s single responsibility is to bring up all hardware peripherals and hand off a ready-to-use LVGL display + input device stack to the application layer (UI Framework).


  1. Hardware Overview
  2. Module Architecture
  3. File Structure
  4. Public API
  5. Initialization Flow
  6. LVGL Integration
  7. Touch System & Wake-up Logic
  8. Custom Control Events
  9. Snapshot Feature
  10. FreeRTOS Task Layout
  11. Build System
  12. Differences from Related BSPs

CategoryComponentDetails
SoCESP32Original Xtensa LX6 dual-core, 240 MHz
DisplayST77963.5″, 320×480, SPI (SPI2_HOST / HSPI), 40 MHz
TouchGT911Capacitive multi-touch, I2C (400 kHz)
SD CardMicro-SDSPI (SPI3_HOST / VSPI), 20 MHz — own bus
UARTUART0TX=GPIO1, RX=GPIO3 (shared with console), 115200 bps
BacklightGPIO27Active-HIGH, plain on/off (no PWM dimming)
Buzzer—Not populated (GPIO_NC)
Encoder—Not physical — emulated via touch
Buttons—Not physical — emulated via touch footer zone
Switch—Not physical — emulated
Potentiometer—Not physical — emulated
SignalGPIO
CSGPIO15
DCGPIO2
MOSIGPIO13
CLKGPIO14
MISOGPIO12
RSTNC (not connected)
BacklightGPIO27
SignalGPIO
SDAGPIO33
SCLGPIO32
RSTGPIO25
IRQNC (default) / GPIO21 (with HARDWARE_MOD_GT911_INT)

I2C addresses probed in order: 0x5D, 0x14.

SignalGPIO
MISOGPIO19
MOSIGPIO23
CLKGPIO18
CSGPIO5
CDNC

get_lvgl_display()

get_lvgl_lock()

get_touch_indev()

lv_display_create()

lv_indev_create()

st7796_spi_configure()

st7796_spi_get_panel_handle()

touch_gt911_configure()

touch_gt911_read()

backlight_configure()

activity_manager_init()

activity_process_event()

bus_i2c_init()

lvgl_flush_cb()

touch_read_cb()

notify_lvgl_flush_ready()

(SPI done ISR)

Application / UI Layer

(esp3d_x_ui, UIManager)

esp32_3248s035c_bsp

(board_init.c)

LVGL v9

(lv_display, lv_indev)

ST7796 SPI Driver

(disp_st7796_spi)

GT911 Touch Driver

(touch_gt911)

Backlight Driver

(disp_backlight)

Activity Manager

(activity_manager)

I2C Bus Driver

(bus_i2c)

BSP Module Files

includes

includes

includes

includes

includes

calls

board_init.c

board_init.h

board_config.h

disp_st7796_spi_def.h

touch_gt911_def.h

tasks_def.h

control_event.h

control_types.h

control_types.c

lv_conf.h


boards/esp32_3248s035c/
├── components/
│ └── bsp/
│ ├── board_init.c # All hardware initialization logic
│ ├── board_init.h # Public BSP API
│ ├── board_config.h # GPIO pins, frequencies, display dimensions
│ ├── control_event.h # control_event_t struct definition
│ ├── control_types.h # control_family_t enum + custom event declarations
│ ├── control_types.c # Custom LVGL event registration
│ ├── disp_st7796_spi_def.h # ST7796 default config instance
│ ├── touch_gt911_def.h # GT911 default config instance
│ ├── disp_backlight_def.h # Backlight config instance
│ ├── tasks_def.h # FreeRTOS task parameters
│ ├── lv_conf.h # LVGL build configuration
│ ├── sd_def.h # SD card config instance
│ └── serial_def.h # UART config instance
└── build_scripts/
├── build_one.py # Entry point: build/check a named variant
├── common.py # build_variant() / check_variant() logic
└── variants.py # make_variant_args() — cmake flag sets

Declared in board_init.h and consumed by esp3d_x_ui (the LVGL task host).

/* Board lifecycle */
esp_err_t board_init(void); // Initialize all hardware; called once at startup
const char* board_get_name(void); // Returns "ESP32-3248S035C"
const char* board_get_version(void); // Returns "v1.0"
/* LVGL access — valid after board_init() succeeds */
lv_display_t* get_lvgl_display(void); // LVGL display handle
_lock_t* get_lvgl_lock(void); // Mutex for thread-safe LVGL API calls
lv_indev_t* get_touch_indev(void); // Touch input device handle
// (only when ESP3D_TOUCH_FEATURE=1)

Thread safety: LVGL itself is single-threaded (Core 1). The get_lvgl_lock() mutex must be held before calling any lv_* API from another task. See UI Framework for the locking pattern.


board_init() is called once by ESP3DX::begin() (see Core Platform) before the LVGL task starts.

yes

no

yes

no

board_init called

activity_manager_init()

- screen wake/sleep tracking

ESP3D_BRIGHTNESS

_CONTROL_FEATURE?

backlight_configure()

backlight_set(0)

- backlight OFF initially

st7796_spi_configure()

- SPI bus + panel init

esp_lcd_panel_mirror(false, true)

⚠ Y-mirror quirk - board-specific

correction not expressible in driver enum

ESP3D_TOUCH

_FEATURE?

init_touch_controller()

bus_i2c_init()

touch_gt911_configure()

init_lvgl()

- full LVGL stack setup

control_events_init()

- register custom LVGL events

board_init returns ESP_OK

This board requires mirror-Y only (raw MADCTL 0x88) for a correct portrait image. The shared ST7796 SPI driver expresses orientation as a 4-combination enum (swap_xy × mirror_x) and cannot represent mirror-Y-alone. Rather than patching the shared driver (which all other boards also use), board_init() applies a one-off call immediately after st7796_spi_configure():

esp_lcd_panel_mirror(st7796_spi_get_panel_handle(), false, true);
// ^mirrorX ^mirrorY

The same quirk exists on esp32_3248s035r, which is the same physical panel with a different touch controller.


ESP TimerST7796 SPI DriverLVGL v9board_init.cESP TimerST7796 SPI DriverLVGL v9board_init.c8 lines × 320px × 2 bytes = 5,120 bytesSingle buffer (DISPLAY_USE_DOUBLE_BUFFER_FLAG=0)lv_init()lv_display_create(320, 480)heap_caps_malloc(draw_buf, MALLOC_CAP_DMA)lv_display_set_buffers(..., LV_DISPLAY_RENDER_MODE_PARTIAL)lv_display_set_color_format(LV_COLOR_FORMAT_RGB565)lv_display_set_flush_cb(lvgl_flush_cb)esp_timer_create(increase_lvgl_tick, period=20ms)esp_timer_start_periodic()esp_lcd_panel_io_register_event_callbacks(notify_lvgl_flush_ready)lv_indev_create() - LV_INDEV_TYPE_POINTERlv_indev_set_read_cb(touch_read_cb)lv_timer_set_period(touch_timer, 10ms)
ParameterValue
Width320 px
Height480 px
Color formatRGB565 (16-bit)
Buffer modeSingle (no double buffering)
Buffer lines8 rows
Buffer size5,120 bytes (DMA-capable heap)
Render modeLV_DISPLAY_RENDER_MODE_PARTIAL
Color byte swapYes (DISPLAY_SWAP_COLOR_FLAG=1)

Memory note: With no PSRAM on this board, the 5,120-byte DMA buffer is allocated from internal DRAM using MALLOC_CAP_DMA. This is deliberately small to leave headroom for WiFi and other subsystems. See Memory Constraints Guide.

notify_lvgl_flush_ready (ISR)ST7796 SPI PanelSnapshot (optional)lvgl_flush_cbLVGL Render Enginenotify_lvgl_flush_ready (ISR)ST7796 SPI PanelSnapshot (optional)lvgl_flush_cbLVGL Render Enginelvgl_flush_cb(disp, area, px_map)Write pixel data to file (if g_snapshot.ongoing)lv_draw_sw_rgb565_swap() - byte-swap RGB565 wordsesp_lcd_panel_draw_bitmap(x1,y1,x2,y2, px_map)SPI transfer done (interrupt)lv_display_flush_ready(disp)
ParameterValue
Core1
Priority15
Stack size12,288 bytes
Tick period20 ms
Touch poll period10 ms

The GT911 is a capacitive multi-touch controller read over I2C. The BSP reads a single contact point (x, y, is_pressed) and feeds it to LVGL as a pointer input device.

Orientation correction applied at driver level:

ParameterValueEffect
swap_xy0No axis swap
invert_x1Mirror horizontal
invert_y1Mirror vertical

touch_read_cb() implements a first-touch-consumed pattern to prevent accidental input when waking the display from a screen-off / idle state:

initial state

touch detected

activity_process_event() returns false\n(display was sleeping)

activity_process_event() returns true\n(display was already active)

finger lifted\n(release also ignored)

finger lifted\n(release forwarded to LVGL)

finger held

finger lifted

finger held\n(all events suppressed)

Released

Pressing

WakeConsumed

ForwardedPress

ForwardedHeld

  • activity_process_event() (from Activity Manager) notifies the system of user activity and returns true if the display was already active. When it returns false, the touch event is consumed internally and LVGL receives LV_INDEV_STATE_RELEASED for the entire press-hold-release cycle.

Three custom LVGL event codes are registered at runtime (after lv_init()) in control_events_init():

EventVariablePurpose
Switch pressedLV_EVENT_SWITCH_PRESSED4-position switch depression
Switch releasedLV_EVENT_SWITCH_RELEASED4-position switch release
Potentiometer changedLV_EVENT_POTENTIOMETER_CHANGEDAnalog position change

On this board, the 4-position switch and potentiometer are not physically present. The events are registered for API compatibility with other BSPs (e.g., pibot_pendant_v1_0_bsp) that do have these hardware inputs. The UI screen layer listens to these same events regardless of board.

Passed as the LVGL event user data on LV_EVENT_SWITCH_* and LV_EVENT_POTENTIOMETER_CHANGED:

typedef struct {
lv_indev_t *indev; // LVGL input device originating the event
uint32_t btn_id; // Button / switch position identifier
lv_indev_type_t type; // LVGL input type (BUTTON, POINTER, etc.)
control_family_t family_id; // Hardware family that fired
int32_t steps; // Encoder delta steps (signed)
uint32_t press_duration; // Duration of press in milliseconds
} control_event_t;
typedef enum {
CONTROL_FAMILY_BUTTONS = 0, // Physical or emulated push buttons
CONTROL_FAMILY_SWITCH, // 4-position selector switch
CONTROL_FAMILY_TOUCH, // Touchscreen contact (future use)
CONTROL_FAMILY_ENCODER, // Rotary encoder
CONTROL_FAMILY_POTENTIOMETER // Analog potentiometer
} control_family_t;

When ESP3D_SNAPSHOT_FEATURE=1 is enabled at build time, lvgl_flush_cb() intercepts each partial frame flush and writes raw pixel data to a file, progressively capturing a full-screen screenshot.

no

yes

timeout/busy

acquired

yes

no

yes

no

lvgl_flush_cb() called

g_snapshot.ongoing?

Normal flush path

xSemaphoreTake

g_snapshot.mutex timeout=0

fwrite pixels in 120-byte chunks

write error?

g_snapshot.error = true

g_snapshot.ongoing = false

captured_pixels += pixel_count

captured >= expected?

g_snapshot.ongoing = false

xSemaphoreGive

Key implementation details:

  • Pixels are written in 120-byte chunks to avoid deep stack usage inside the flush callback.
  • The mutex is taken with timeout=0 (non-blocking); a flush tile that cannot acquire the mutex skips snapshot writing without blocking LVGL.
  • g_snapshot.expected_pixels equals display_width × display_height; capture completes when captured_pixels reaches this value.
  • This feature is shared across multiple boards; see also esp32_3248s035r_bsp and pibot_pendant_v1_0_bsp.

All task parameters for this board are defined in tasks_def.h.

Core 1 - UI and Processing

tftUI LVGL

stack=12288, prio=15

tftStream

stack=8192, prio=0

esp3d_rendering_rx_task

stack=4096, prio=5

esp3d_gcode_host_task

stack=4096, prio=14

Core 0 - Network and I/O

tftNetwork

stack=4096, prio=0

esp3d_serial_rx_task

stack=4096, prio=10

esp3d_socket_rx_task

stack=4096, prio=5

esp3d_socket_client_rx

stack=8192, prio=5

Critical constraint: LVGL runs exclusively on Core 1 at priority 15. No other Core 1 task should block the LVGL task or call lv_* APIs without holding get_lvgl_lock(). See UI Framework Architecture.


The build scripts in boards/esp32_3248s035c/build_scripts/ automate multi-variant firmware builds using the ESP-IDF CMake toolchain.

variant config dict

yes

no

cmake -D flags

build_one.py variant_name --clean or --check

common.py

--clean?

clean_build_dir()

clean installer dir

clean parent CMake artifacts

generate_resources()

ui_resources partition

run_cmake_build()

_show_size_report()

copy_factory_artifacts()

_copy_ui_resources_bin()

_package_user_resources_kit()

generate_flash_map()

variants.py

make_variant_args(*on_flags)

  • make_variant_args(*on_flags) starts from DEFAULT_OFF_CMAKE_ARGS (all features off) and enables only the specified flags — ensuring clean, reproducible builds with no accidental feature leakage between variants.
  • build_variant() also handles the Factory sub-project (whose cwd differs from REPO_ROOT) and cleans stale CMake artifacts from parent build directories on --clean.
  • check_variant() runs run_cmake_check() only (no binary output), used for CI configuration validation.

Featureesp32_3248s035c (this)esp32_3248s035rpibot_pendant_v1_0
Touch typeCapacitive GT911 (I2C)Resistive XPT2046 (SPI)Capacitive (ILI9341 panel)
Touch calibrationNot needed (auto-detect)touch_calibrate() requiredNot needed
Display driverST7796 SPIST7796 SPIILI9341 SPI
Physical buttonsNone — emulatedNone — emulatedYes — button_read_cb()
Physical encoderNone — emulatedNone — emulatedYes — encoder_read_cb()
Physical switchNone — emulatedNone — emulatedYes — switch_read_cb()
PotentiometerNone — emulatedNone — emulatedYes — potentiometer_read_cb()
Double bufferNoNoNo
Flush notifySPI IO event callbackSPI IO event callbackSPI IO event callback
Y-mirror quirkYes — manual panel_mirrorYes — same quirk, same fixNo
LVGL task stack12,288 bytes12,288 bytes12,288 bytes

Both boards share the same ST7796 panel and the same Y-mirror orientation quirk. The key implementation difference is the touch subsystem: the resistive XPT2046 requires SPI-based register reads and a software calibration step (touch_calibrate()), while the capacitive GT911 auto-detects its resolution over I2C and needs no calibration.

The PiBot Pendant v1.0 BSP has a richer board_init() that registers real hardware callbacks for five distinct input families (button_read_cb, encoder_read_cb, potentiometer_read_cb, switch_read_cb, touch_read_cb). The ESP32-3248S035C BSP only registers touch_read_cb; the other families are available through the custom LVGL event system but have no physical backing on this board.


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.