Skip to content

BSP Board Initialization Module

The bsp_bsp_board_initialization module is the board-specific hardware initialization layer for the second tier of supported boards in the ESP32/ESP32-S3 pendant firmware. Each board variant in this module provides a concrete board_init.c implementation that brings up its specific combination of display controller, touch controller, physical inputs, and LVGL framework in the correct sequence.

This module is a child of the broader bsp module and operates as the lowest-level integration point between raw hardware drivers and the LVGL-based UI system. It owns the LVGL tick timer, display flush pipeline, input device registration, and the optional filesystem-access PCLK patch for RGB panel boards.


BoardSoCDisplayBusTouchExtra Inputs
esp32s3_8048s070cESP32-S3EK9716RGB parallelGT911—
esp32s3_bzm_tft35_gt911ESP32-S3ST7796SPIGT911—
esp32s3_hmi43v3ESP32-S3RM68120Intel 8080 (i80)FT5x06TCA9554 IO expander
esp32s3_zx3d50ce02s_usrc_4832ESP32-S3ST7796Intel 8080 (i80)FT5x06—
fysetc_wifi_proESP32-S3———Headless WiFi+SD
pibot_pendant_v1_0ESP32ILI9341SPIFT6336UButtons, Encoder, 4-pos Switch, Potentiometer, Buzzer

Hardware_Abstraction_Layer

bsp (parent module)

bsp_bsp_board_initialization

(this module)

bsp_bsp_control_events

bsp_display_drivers_spi

bsp_display_drivers_i80

bsp_display_drivers_rgb

bsp_touch_controllers

bsp_io_expanders

bsp_bus_drivers

bsp_physical_inputs

bsp_buzzer

main.cpp

app_main()

Core Platform

ESP3DX::begin()

board_init()

(this module)

LVGL Framework

tft_ui_task - Core 1

UI Framework

UIManager / Screens

Hardware Drivers

display / touch / inputs

board_init() is called early in the system startup sequence. It must complete before tft_ui_task begins running LVGL on Core 1. All LVGL calls after that point must be protected by the lvgl_api_lock mutex returned by get_lvgl_lock().


pibot_pendant_v1_0 - SPI + full physical inputs

DMA done

board_init()

ili9341_spi_configure()

touch_ft6336u_configure()

phy_buttons_configure()

phy_encoder_configure()

phy_switch_configure()

phy_potentiometer_configure()

buzzer_configure()

init_lvgl()

notify_lvgl_flush_ready() - DMA ISR

esp32s3_hmi43v3 - i80 panel + IO expander

transfer done

board_init()

bus_i2c_init()

io_tca9554_configure()

disp_rm68120_configure()

touch_ft5x06_configure()

init_lvgl()

lvgl_flush_cb()

i80_flush_ready_cb() - i80 ISR

esp32s3_bzm_tft35_gt911 - SPI panel

DMA done

board_init()

st7796_spi_configure()

touch_gt911_configure()

init_lvgl()

lvgl_flush_cb()

notify_lvgl_flush_ready() - DMA ISR

esp32s3_8048s070c - RGB panel

VSYNC semaphore

throttle PCLK

board_init()

disp_ek9716_configure()

bus_i2c_init()

touch_gt911_configure()

init_lvgl()

lvgl_flush_cb()

disp_on_vsync_event() - VSYNC ISR

bsp_accessFs() / bsp_releaseFs()


board_init() called

activity_manager_init()

backlight_set(0)

if ESP3D_BRIGHTNESS_CONTROL_FEATURE

init_io_expander()

hmi43v3 only

Display driver configure

board-specific

init_touch_controller()

if ESP3D_TOUCH_FEATURE

Physical inputs init

pibot only: buttons / encoder / switch / pot / buzzer

init_lvgl()

backlight_set(DEFAULT_LEVEL)

if ESP3D_BRIGHTNESS_CONTROL_FEATURE

control_events_init()

board_init() → ESP_OK

Steps with a tan background are conditional on compile-time feature flags (ESP3D_BRIGHTNESS_CONTROL_FEATURE, ESP3D_TOUCH_FEATURE, ESP3D_HARDWARE_BUTTONS_FEATURE, etc.). fysetc_wifi_pro skips all display and input steps — its board_init() logs the headless state and returns immediately.

init_lvgl() called

lv_init()

Get panel/IO handle

from display driver (board-specific getter)

lv_display_create(WIDTH, HEIGHT)

heap_caps_malloc(draw_buf_size)

SPIRAM for RGB - DMA for SPI and i80

lv_display_set_buffers(buf1, buf2, size)

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() + esp_timer_start_periodic()

increase_lvgl_tick every LVGL_TICK_PERIOD_MS

Register driver completion callback

notify_lvgl_flush_ready / i80_flush_ready_cb /

disp_on_vsync_event (board-specific)

lv_indev_create(LV_INDEV_TYPE_POINTER)

touch_read_cb - if ESP3D_TOUCH_FEATURE

Register extra indevs

button / encoder / switch / potentiometer

pibot_pendant_v1_0 only

init_lvgl() → ESP_OK


Each bus type uses a different mechanism to signal LVGL that the frame data has been consumed by the hardware.

SPI — ILI9341 (pibot) and ST7796-SPI (bzm_tft35)

Section titled “SPI — ILI9341 (pibot) and ST7796-SPI (bzm_tft35)”
notify_lvgl_flush_ready() [ISR]SPI DMAlvgl_flush_cb()LVGL (Core 1)notify_lvgl_flush_ready() [ISR]SPI DMAlvgl_flush_cb()LVGL (Core 1)DMA transfer runs in backgroundRender complete → call flush_cbSnapshot pixel capture [if ESP3D_SNAPSHOT_FEATURE]lv_draw_sw_rgb565_swap() [if DISPLAY_SWAP_COLOR_FLAG]esp_lcd_panel_draw_bitmap() - async DMAon_color_trans_done fireslv_display_flush_ready()Schedule next render

Intel 8080 / i80 — RM68120 (hmi43v3) and ST7796-i80 (zx3d50)

Section titled “Intel 8080 / i80 — RM68120 (hmi43v3) and ST7796-i80 (zx3d50)”
i80_flush_ready_cb() [ISR]i80 Buslvgl_flush_cb()LVGL (Core 1)i80_flush_ready_cb() [ISR]i80 Buslvgl_flush_cb()LVGL (Core 1)Render complete → call flush_cbSnapshot pixel capture [if ESP3D_SNAPSHOT_FEATURE]lv_draw_sw_rgb565_swap() [if DISPLAY_SWAP_COLOR_FLAG]esp_lcd_panel_draw_bitmap()Transfer complete - registered at configure timelv_display_flush_ready()Schedule next render

RGB Parallel — EK9716 (8048s070c) — Tear-free with VSYNC

Section titled “RGB Parallel — EK9716 (8048s070c) — Tear-free with VSYNC”
disp_on_vsync_event() [VSYNC ISR]RGB Panellvgl_flush_cb()LVGL (Core 1)disp_on_vsync_event() [VSYNC ISR]RGB Panellvgl_flush_cb()LVGL (Core 1)Two binary semaphores: sem_gui_ready and sem_vsync_endHardware VSYNC interrupt firesRender complete → call flush_cbSnapshot pixel capture [if ESP3D_SNAPSHOT_FEATURE]lv_draw_sw_rgb565_swap() [if DISPLAY_SWAP_COLOR_FLAG]xSemaphoreGive(sem_gui_ready)xSemaphoreTake(sem_vsync_end, portMAX_DELAY) - BLOCKSon_vsync callbackxSemaphoreTakeFromISR(sem_gui_ready)xSemaphoreGiveFromISR(sem_vsync_end)esp_lcd_panel_draw_bitmap()lv_display_flush_ready()Schedule next render

Why VSYNC synchronization? RGB panels continuously scan out from a shared frame buffer in PSRAM. Without this synchronization, writing new pixel data races the ongoing scanout and produces visible tearing. The two semaphores ensure pixel data is written only during the vertical blanking interval. See docs/architecture/display_drivers.md for the full RGB driver model.


All boards with touch implement the same wake-up consumption pattern in touch_read_cb(). The first touch event after an inactivity timeout wakes the display but is not forwarded to the UI, preventing accidental activations.

yes

no

yes - transition

was asleep

was active

no - still held

not consumed

consumed

yes - release transition

was consumed

was normal

no

Read touch driver

GT911 / FT5x06 / FT6336U

is_pressed?

First press?

not last_pressed_state

activity_process_event()

returns false = system was asleep

touch_consumed_for_wakeup = true

Report: STATE_RELEASED

touch_consumed_for_wakeup = false

Report: STATE_PRESSED at x,y

Still held

and not consumed?

Report: STATE_PRESSED

Report: STATE_RELEASED

Was previously

pressed?

Clear consumed flag

Ignore release (wake-up cycle)

activity_process_event()

Report: STATE_RELEASED

Report: STATE_RELEASED

The activity_process_event() function is provided by the activity_manager component. See bsp_bsp_control_events.md for the control_event_t structure and event registration.


Physical Input Callbacks (PiBot Pendant v1.0 Only)

Section titled “Physical Input Callbacks (PiBot Pendant v1.0 Only)”

The pibot_pendant_v1_0 board is the most input-rich variant. It registers five LVGL input devices in addition to touch, all dispatching custom events to the active LVGL screen.

LVGL Input Devices - pibot_pendant_v1_0

custom events on active screen

custom events on active screen

custom events on active screen

custom events on active screen

custom events on active screen

touch_indev

LV_INDEV_TYPE_POINTER

timer: 10 ms poll

button_indev

LV_INDEV_TYPE_BUTTON

3 physical buttons

encoder_indev

LV_INDEV_TYPE_ENCODER

PCNT-based

switch_indev

LV_INDEV_TYPE_BUTTON

4-position rotary

potentiometer_indev

LV_INDEV_TYPE_POINTER

ADC 12-bit

FT6336U driver

phy_buttons driver

phy_encoder / PCNT

phy_switch driver

phy_potentiometer / ADC

lv_screen_active()

Button and switch events carry a control_event_t payload with family_id (BUTTONS / ENCODER / SWITCH / POTENTIOMETER), btn_id, and steps. Encoder events are dispatched as LV_EVENT_KEY with LV_KEY_LEFT / LV_KEY_RIGHT. Potentiometer events use LV_EVENT_POTENTIOMETER_CHANGED with steps = mapped value 0–100.

The encoder callback implements multi-tier rate limiting to prevent LVGL event flooding while remaining responsive to fast rotation:

yes

no

asleep

active

too soon

ready

phy_encoder_read(&clicks)

clicks == 0?

data->state = RELEASED - return

activity_process_event() returns false?

Consume for wake-up - return

time_since_last = now − last_output_time

time_since_last threshold

≥ SLOW_MS

min_interval = 80 ms

≥ NORMAL_MS

min_interval = 40 ms

≥ FAST_MS

min_interval = 20 ms

very fast

min_interval = 10 ms

time_since_last ≥ min_interval?

Send LV_EVENT_KEY per click

max 5 events per call

step = ±1, key = LEFT or RIGHT

The potentiometer callback uses adaptive threshold to distinguish real user movement from ADC noise, particularly after a period of inactivity:

yes

no

yes

no

yes

no

yes

no

phy_potentiometer_read(&adc)

mapped = adc × 100 / 4095

delta = mapped − last_mapped_value

time since last real

activity > INACTIVITY_THRESHOLD?

threshold = POT_WAKE_THRESHOLD_MAPPED

high - filters noise after idle

Direction

changed?

threshold = 1

ultra-sensitive on direction reversal

delta < 0?

threshold = 2

more sensitive on descent

threshold = 3

normal on ascent

abs(delta) ≥ threshold?

Update last_activity_time

activity_process_event()

Send LV_EVENT_POTENTIOMETER_CHANGED

steps = mapped_value (0-100)

data->state = RELEASED


On boards using an RGB parallel panel (esp32s3_8048s070c and similar), the display pixel clock and the SPI/SDIO bus for SD card access compete for the ESP32-S3 PSRAM bus. Concurrent access causes bandwidth contention and potential data corruption.

The bsp_accessFs() / bsp_releaseFs() pair (compiled in when ESP3D_PATCH_FS_ACCESS_RELEASE is defined) temporarily reduces the display pixel clock before any filesystem access and restores it afterward:

RGB Panel Driverboard_init.cESP3DFlash / ESP3DSdRGB Panel Driverboard_init.cESP3DFlash / ESP3DSdSD or Flash read/writebsp_accessFs()esp_lcd_rgb_panel_set_pclk(DISPLAY_PATCH_FS_FREQ_HZ)vTaskDelay(DISPLAY_PATCH_FS_DELAY_MS)bsp_releaseFs()esp_lcd_rgb_panel_set_pclk(DISPLAY_PCLK_FREQ_HZ)vTaskDelay(DISPLAY_PATCH_FS_DELAY_MS)

The delay after each PCLK change allows the RGB panel to stabilize. Both functions guard against a NULL disp_panel handle and return ESP_OK immediately if the display was not initialized.

See docs/guides/esp32_memory_constraints.md (§ Serial CNC + WiFi remote — fragmentation playbook) for a full analysis of PSRAM bus contention.


All display-capable boards conditionally include pixel-capture logic inside lvgl_flush_cb() when ESP3D_SNAPSHOT_FEATURE is enabled. During a capture the flush callback:

  1. Checks g_snapshot.ongoing (set by the snapshot trigger).
  2. Takes g_snapshot.mutex in a non-blocking attempt (xSemaphoreTake(..., 0)).
  3. Writes pixel data from px_map to g_snapshot.file in 120-byte chunks — sized to fit within the ESP32’s constrained stack and fragmented heap.
  4. Increments g_snapshot.captured_pixels; clears g_snapshot.ongoing when captured_pixels >= expected_pixels.
  5. Releases the mutex, then proceeds with the normal display flush path.

This runs within the LVGL flush callback on Core 1 and must not block on the mutex. If the mutex is held by another task (e.g., the snapshot close sequence), the flush simply skips the write for that frame. See esp3d_snapshot.h in the display_core module for the g_snapshot structure.


Each board’s board_init.c exports the following symbols via board_init.h:

FunctionReturnDescription
board_init()esp_err_tFull hardware + LVGL initialization. Must complete before tft_ui_task starts.
board_get_name()const char *Human-readable board name from BOARD_NAME_STR.
board_get_version()const char *Board version string from BOARD_VERSION_STR.
get_lvgl_display()lv_display_t *LVGL display handle. Returns NULL when display feature is disabled.
get_lvgl_lock()_lock_t *LVGL API mutex. Must be held by any non-Core-1 caller of LVGL APIs.
get_touch_indev()lv_indev_t *Touch input device (ESP3D_TOUCH_FEATURE).
get_button_indev()lv_indev_t *Button input device — pibot only (ESP3D_HARDWARE_BUTTONS_FEATURE).
get_encoder_indev()lv_indev_t *Encoder input device — pibot only (ESP3D_HARDWARE_ENCODER_FEATURE).
get_switch_indev()lv_indev_t *4-position switch device — pibot only (ESP3D_HARDWARE_SWITCH_FEATURE).
get_potentiometer_indev()lv_indev_t *Potentiometer device — pibot only (ESP3D_HARDWARE_POTENTIOMETER_FEATURE).
bsp_accessFs()esp_err_tThrottle display PCLK before FS access — RGB boards (ESP3D_PATCH_FS_ACCESS_RELEASE).
bsp_releaseFs()esp_err_tRestore display PCLK after FS access — RGB boards (ESP3D_PATCH_FS_ACCESS_RELEASE).

FlagEffect on board_init
ESP3D_DISPLAY_FEATUREGates the entire display + LVGL init path.
ESP3D_BRIGHTNESS_CONTROL_FEATUREEnables backlight configure and initial blackout/restore calls.
ESP3D_TOUCH_FEATUREEnables touch driver init and touch_indev LVGL registration.
ESP3D_HARDWARE_BUTTONS_FEATUREEnables 3-button GPIO driver and button_indev.
ESP3D_HARDWARE_ENCODER_FEATUREEnables PCNT-based rotary encoder and encoder_indev.
ESP3D_HARDWARE_SWITCH_FEATUREEnables 4-position switch driver and switch_indev.
ESP3D_HARDWARE_POTENTIOMETER_FEATUREEnables ADC potentiometer driver and potentiometer_indev.
ESP3D_BUZZER_FEATUREEnables buzzer PWM driver initialization.
ESP3D_SNAPSHOT_FEATURECompiles in the pixel-capture path inside lvgl_flush_cb().
ESP3D_PATCH_FS_ACCESS_RELEASECompiles in bsp_accessFs() / bsp_releaseFs() PCLK throttling.
DISPLAY_USE_DOUBLE_BUFFER_FLAGAllocates a second draw buffer (lvgl_buf2) alongside lvgl_buf1.
DISPLAY_SWAP_COLOR_FLAGCalls lv_draw_sw_rgb565_swap() in the flush callback before sending pixels.

Draw buffer allocations follow ESP32 memory constraints strictly. The capability flag must match what the display DMA engine requires:

Bus TypeBoardsAllocation CapRationale
RGB parallel8048s070cMALLOC_CAP_SPIRAMBuffer too large for internal RAM; RGB engine reads from PSRAM directly
SPIbzm_tft35, pibotMALLOC_CAP_DMASPI DMA engine requires DMA-capable internal SRAM
Intel 8080 (i80)hmi43v3, zx3d50MALLOC_CAP_DMAi80 DMA engine requires DMA-capable internal SRAM

On allocation failure, board_init() returns ESP_ERR_NO_MEM immediately. There is no fallback — display buffer allocation failure is considered unrecoverable. If double-buffer mode is enabled and the second allocation fails, the first buffer is freed before returning.

See docs/guides/esp32_memory_constraints.md for heap fragmentation analysis and buffer sizing guidance.


⚠️ Critical: LVGL is single-threaded and runs exclusively on Core 1 inside tft_ui_task. See docs/architecture/screens_architecture.md.

The following rules apply to all callbacks registered by this module:

  • touch_read_cb, button_read_cb, encoder_read_cb, switch_read_cb, potentiometer_read_cb — called by the LVGL task on Core 1. They must not block. Heavy computation or I2C/SPI transactions must be offloaded.
  • increase_lvgl_tick — called from a high-priority ESP timer task. It only calls lv_tick_inc(), which is documented as ISR-safe.
  • notify_lvgl_flush_ready (SPI DMA ISR), i80_flush_ready_cb (i80 ISR), disp_on_vsync_event (VSYNC ISR) — called from ISR context. They may only call ISR-safe functions: lv_display_flush_ready() or FreeRTOS FromISR variants. Never call standard LVGL APIs from these callbacks.
  • Any task other than Core 1 that needs to call LVGL APIs (e.g., triggering a screen refresh) must acquire the mutex returned by get_lvgl_lock() before doing so and release it immediately after.

ReferenceDescription
bsp.mdParent BSP module — first-tier boards (esp32_2432s028r, esp32_3248s035*, esp32s3_4827s043c, esp32s3_8048s043/050/touch_lcd_7)
bsp_bsp_control_events.mdcontrol_event_t definition and control_events_init() — custom LVGL event types
bsp_display_drivers_spi.mdILI9341 and ST7796 SPI panel drivers
bsp_display_drivers_i80.mdRM68120 and ST7796-i80 parallel bus drivers
bsp_display_drivers_rgb.mdEK9716 RGB parallel panel driver
bsp_touch_controllers.mdGT911, FT5x06, FT6336U touch drivers
bsp_physical_inputs.mdphy_buttons, phy_encoder, phy_switch, phy_potentiometer drivers
bsp_bus_drivers.mdI2C and software-SPI bus initialization
bsp_io_expanders.mdTCA9554 and CH422G IO expander drivers
bsp_buzzer.mdBuzzer PWM driver
display_core.mdesp3d_snapshot.h, LVGL UI task (tft_ui_task), UIManager
docs/architecture/display_drivers.mdSPI vs RGB driver architecture, rotation math, PCLK semantics
docs/architecture/screens_architecture.mdLVGL Core 1 threading model, screen lifecycle
docs/guides/esp32_memory_constraints.mdHeap fragmentation, PSRAM bus contention, buffer sizing playbook