Skip to content

ESP32-2432S028R Board Support Package

The esp32_2432s028r module provides the Board Support Package (BSP) for the ESP32-2432S028R, a popular low-cost ESP32 development board — also referred to as the “CYD” (Cheap Yellow Display) — equipped with a 2.8-inch ILI9341 SPI TFT display and an XPT2046 resistive touchscreen. The module covers the complete lifecycle of this board within the pendant firmware: hardware abstraction, LVGL display and touch integration, the standalone factory recovery application, and the multi-variant build system.

Touch-only board. Physical buttons, rotary encoder, buzzer, 4-position switch, and potentiometer are all GPIO_NUM_NC. All pendant controls are implemented as on-screen virtual elements driven exclusively by touch.


FeatureDetail
MCUESP32 (Xtensa dual-core LX6, 240 MHz)
Flash4 MB (only supported configuration)
PSRAMNone
DisplayILI9341 SPI TFT — 240×320 px portrait
Display busSPI2/HSPI, hardware DMA, 20 MHz (40 MHz causes progressive line-shift corruption on this clone board)
TouchXPT2046 resistive — dedicated bit-banged SPI bus (separate GPIOs from the display)
SD CardSPI3/VSPI, on-board micro-SD slot
UARTUART0 (GPIO1 TX / GPIO3 RX) — shared with console
BuzzerNot populated (GPIO_NUM_NC)
Physical buttonsNot populated (GPIO_NUM_NC)
Rotary encoderNot populated (GPIO_NUM_NC)

ILI9341 Display — SPI2 / HSPI (hardware SPI, DMA)

SignalGPIO
CS15
DC2
MOSI13
CLK14
MISO12
RST4
Backlight21 (active-high, on/off — no PWM dimming)

XPT2046 Touch — Bit-banged SPI (separate bus)

SignalGPIONotes
CS33
CLK25
MOSI32
MISO39Input-only GPIO
IRQ36Input-only GPIO

Calibration: X raw 200–3850 → 0–240 px; Y raw 380–3950 → 0–320 px. X axis is hardware-inverted (TOUCH_MIRROR_X_FLAG=1, confirmed on hardware 2026-08-03).

SD Card — SPI3 / VSPI

SignalGPIO
MISO19
MOSI23
CLK18
CS5
  • No PSRAM — imposes the tightest heap budget of all supported boards (≈75 KB free in WiFi mode). See esp32_memory_constraints.md.
  • Dual SPI buses — display on hardware SPI2; touch on a separate bit-banged SPI. The two buses must never share GPIO lines.
  • SPI speed cap — hard-coded to 20 MHz in board_config.h and hw_config.h due to signal integrity issues at 40 MHz on this specific board.
  • Touch-only input — all pendant controls are virtual (on-screen); no physical button, encoder, buzzer, switch, or potentiometer is wired.
  • Single OTA slot — only app0 exists. No app1 partition fits in 4 MB alongside the factory partition.
  • No custom bootloader hook — recovery is entered exclusively via the software [ESP444]FACTORY command from the main firmware; there is no physical button to hold at power-up.

The module is organized into three independent layers: the main-firmware BSP component, the standalone factory recovery application, and the Python build scripts.

calls

produces

produces

Build Scripts - build_scripts

variants.py

common.py

build_one.py

Factory Recovery App - Factory/main

app_main recovery

gfx - raw draw to ILI9341

sdcard - SPI mount/unmount

buzzer - bit-bang GPIO no-op

touch - XPT2046 polling

BSP Component - components/bsp

board_init

ILI9341 SPI driver

XPT2046 bit-banged SPI driver

LVGL integration and tick timer

Control event types

Main Firmware (ESP3DX)

app_main → ESP3DX::begin

LVGL UI screens

Communication transports

The factory recovery app is completely standalone — it does not link the BSP component and drives the ILI9341 through its own ili9341.c driver and gfx.c abstraction layer rather than through esp_lcd or LVGL.


Offset Name Size Notes
──────────────────────────────────────────────────────────────────
0x00000 bootloader 44 KB Partition table at 0xC000 (not default 0x8000)
0x0D000 nvs 12 KB NVS settings (ESP3DSettings)
0x10000 otadata 8 KB OTA state tracking
0x20000 app0 (ota_0) 2880 KB Main pendant firmware (single OTA slot)
0x2F0000 ui_resources 192 KB LVGL icons, fonts, theme palette
0x320000 flashfs 576 KB LittleFS / FAT file system
0x3B0000 factory 320 KB Factory recovery application

OTA backup area: Before switching to the factory partition, the main firmware writes an otadata backup to flash offset 0xB000 — a 4 KB-aligned region below the partition table. The factory app restores this backup at startup so that a power-off during recovery returns to the correct OTA partition. CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWED=y is required in the factory sdkconfig to permit writes below the first partition boundary.


The BSP component is linked into the main firmware. It owns the complete hardware-initialization sequence and the LVGL integration layer.

FilePurpose
board_config.hAll pin, SPI, display, touch, UART, and peripheral constants
board_init.c / .hTop-level board_init() and all LVGL sub-system setup
disp_ili9341_spi_def.hILI9341 default config struct instance
touch_xpt2046_def.hXPT2046 config struct + bit-banged SPI read hook
disp_backlight_def.hBacklight configuration struct instance
sd_def.hSD card configuration struct instance
serial_def.hUART configuration struct instance
control_event.hcontrol_event_t — unified structure for LVGL input events
control_types.c / .hCustom LVGL event ID registration (control_events_init)
tasks_def.hFreeRTOS task priorities and stack sizes
lv_conf.hLVGL compile-time configuration
control_events_initinit_lvgl()init_touch_controllerili9341_spi_configurebacklight_configureactivity_manager_initboard_init()ESP3DX::begincontrol_events_initinit_lvgl()init_touch_controllerili9341_spi_configurebacklight_configureactivity_manager_initboard_init()ESP3DX::beginboard_init()activity_manager_init()OKbacklight_configure() → backlight_set(0)ili9341_spi_configure(&ili9341_default_config)panel_handle + io_handleinit_touch_controller()bus_sw_spi_init (bit-banged SPI, GPIOs 25/32/33/39)touch_xpt2046_configure()OKinit_lvgl()lv_init()lv_display_create(240, 320)heap_caps_malloc(DMA) - single buffer, 12 lineslv_display_set_flush_cb(lvgl_flush_cb)esp_timer_start_periodic - 1 ms LVGL tickesp_lcd_panel_io_register_event_callbacks → notify_lvgl_flush_readylv_indev_create(POINTER) + touch_read_cb (10 ms period)OKcontrol_events_init()LV_EVENT_SWITCH_PRESSED/RELEASED/POTENTIOMETER_CHANGED registeredESP_OK

lv_draw

if ESP3D_SNAPSHOT_FEATURE

SPI transaction-done ISR

unblocks

LVGL render task

(Core 1)

lvgl_flush_cb

fwrite pixel chunk

to snapshot file

lv_draw_sw_rgb565_swap

byte-swap RGB565

esp_lcd_panel_draw_bitmap

SPI2 DMA transfer

notify_lvgl_flush_ready

lv_display_flush_ready

Display buffer configuration:

ParameterValueSource
Buffer modeSingleDISPLAY_USE_DOUBLE_BUFFER_FLAG=0
Buffer lines12DISPLAY_BUFFER_LINES_NB=12
Buffer size5 760 bytes240 × 12 × 2 bytes, DMA-capable SRAM
Color byte swapYesDISPLAY_SWAP_COLOR_FLAG=1 — ILI9341 expects big-endian RGB565
OrientationPortrait (0)240 wide × 320 tall
Color invertNoDISPLAY_INVERT_FLAG=0

raw 12-bit ADC

pixel x,y

screen active

screen was sleeping

LVGL indev timer

(10 ms)

touch_read_cb

touch_xpt2046_read

via bus_sw_spi bit-bang

touch_calibrate()

linear rescale + clamp

activity_process_event

LV_INDEV_STATE_PRESSED

x,y → LVGL

consume touch

LV_INDEV_STATE_RELEASED

(wake-up cycle)

LVGL pointer input

propagated to screens

The calibration formula applied inside touch_read_cb:

// maps raw ADC [cal_min, cal_max] → pixel [0, range], clamped at 0
value = (raw > cal_min) ? (raw - cal_min) * range / (cal_max - cal_min) : 0;
Axiscal_mincal_maxrangeMirror
X2003 850240Yes — TOUCH_MIRROR_X_FLAG=1
Y3803 950320No

A first touch that wakes the screen from the activity-manager idle state is consumed (not forwarded to LVGL). The next press cycle is delivered normally.

touch_xpt2046_def.h — Bit-banged SPI Hook

Section titled “touch_xpt2046_def.h — Bit-banged SPI Hook”

The XPT2046 driver (hardware/common/drivers/touch_xpt2046) accepts a read_reg12_fn function pointer. This board wires it to an inline helper that calls the bit-banged SPI layer:

static inline uint16_t touch_xpt2046_spi_read_reg12(uint8_t reg) {
return bus_sw_spi_read_reg16(reg) >> 3; // 16-bit read, top 12 bits valid
}

This decouples the touch driver from knowing whether hardware or software SPI is in use. See display_spi_drivers.md and touch_drivers.md for the shared driver APIs.

control_events_init() registers three dynamic LVGL event IDs after lv_init(). They are declared extern in control_types.h and consumed by the UI screen event handlers.

ConstantPurpose
LV_EVENT_SWITCH_PRESSEDVirtual switch press (touch-emulated)
LV_EVENT_SWITCH_RELEASEDVirtual switch release
LV_EVENT_POTENTIOMETER_CHANGEDAnalog value change (software-emulated)
typedef struct {
lv_indev_t *indev;
uint32_t btn_id;
lv_indev_type_t type;
control_family_t family_id;
int32_t steps;
uint32_t press_duration; // milliseconds
} control_event_t;

When ESP3D_SNAPSHOT_FEATURE is enabled, lvgl_flush_cb intercepts every LVGL flush and streams pixel data into an open FILE * held in the global g_snapshot structure (defined in esp3d_snapshot.h). The snapshot accumulates flushed tile areas until captured_pixels >= expected_pixels, then closes automatically. This is used for automated UI screenshot capture during testing. See display_drivers.md for the full snapshot state machine.


The factory app is a self-contained ESP-IDF application flashed to the factory partition (320 KB at 0x3B0000). It is entered exclusively via the [ESP444]FACTORY software command issued from the main firmware.

Factory/
├── main/
│ ├── main.c — app_main, menu system, OTA flash actions
│ ├── gfx.c / .h — raw raster graphics: hline/vline/fill/rect/string
│ ├── ili9341.c / .h — standalone ILI9341 SPI driver (direct register writes)
│ ├── touch.c / .h — XPT2046 bit-banged polling → touch_point_t
│ ├── sdcard.c / .h — SPI mount/unmount via esp_vfs_fat
│ ├── buzzer.c / .h — bit-bang square-wave (no-op: BUZZER_PIN = GPIO_NUM_NC)
│ ├── buttons.c / .h — GPIO button polling (no-op: all pins = GPIO_NUM_NC)
│ ├── encoder.c / .h — PCNT encoder (no-op: pins = GPIO_NUM_NC)
│ ├── hw_config.h — factory-specific pin and screen constants
│ ├── font8x16.h — embedded 8×16 bitmap font (ASCII 0x20–0x7E)
│ └── factory_log.h — FACTORY_LOGD macro (silences SD stack noise)
└── cmake/
├── features.cmake
├── colors.cmake
├── targets.cmake — installer artifact copy (postbuild)
└── sanity_check.cmake

No LVGL, no BSP. The factory app drives the ILI9341 directly via its own ili9341.c SPI driver. gfx.c provides an 8×16 font renderer, line/fill/rect primitives, and the optional raw-pixel snapshot mechanism. For cross-board factory app design, see factory_app_technical_doc.md.

left third BTN1

mid third BTN2

right third BTN3

no hit

yes

no

yes

no

Power-on or ESP444 FACTORY command

restore_otadata_from_backup()

flash 0xB000 → 0x10000

ili9341_init + backlight on

buttons/encoder/touch/buzzer init

(no-ops for NC pins)

probe_sd_files()

mount SD → check files → unmount

draw_menu - full screen render

Main loop

encoder_read - no-op

button_wait_press 100 ms - no-op

touch_read XPT2046

touch_hint_hit_test

menu_move(-1) up

menu_move(+1) down

execute_selected_action

draw_button_hint_pressed

buzzer_beep_short (no-op)

Boot app0

esp_ota_set_boot_partition → restart

SD → app0 firmware

OTA flash /sdcard/esp3dfw.bin

SD → resources

erase + write /sdcard/ui_resources.bin

Success?

rename → esp3dfw.ok

esp_restart()

rename → esp3dfw.bad

draw_menu

Success?

rename → ui_resources.ok

esp_restart()

rename → ui_resources.bad

draw_menu

The recovery app renders a button-hint bar at the bottom of the 240×320 screen using raw circle and icon geometry (no LVGL). The touch hit-test divides the full screen width into three equal columns, making each button a much larger target than the drawn circle:

┌────────────────────────────────────────┐
│ Recovery v1.x.x │ ← title (COLOR_CYAN)
│ ───────────────────────────────── │
│ Active: app0 (default) │ ← COLOR_YELLOW
│ SD: FW RES │ ← SD file indicators
│ ───────────────────────────────── │
│ > Boot app0 │ ← selected (highlight box)
│ SD -> app0 │
│ SD -> resources │
│ ───────────────────────────────── │
│ Power off to cancel │ ← status / footer
│ ════════════════════════════════ │ ← separator
│ ( up ) ( down ) ( ok ) │ ← virtual button circles
└────────────────────────────────────────┘
left third mid third right third
BTN1 BTN2 BTN3

The main firmware (esp444.cpp) writes an otadata backup to 0xB000 before switching the boot partition to factory. The factory app restores it on startup:

Flash offsetContent
0xB000 + 0x00OTA entry 1 (32 bytes)
0xB000 + 0x20OTA entry 2 (32 bytes)
0xB000 + 0x40Magic 0xAA55AA55 (4 bytes) — backup validity flag

After restoring, the backup sector is erased so the restore does not repeat on the next boot. If the magic is absent or both entries are all 0xFF, the restore is skipped silently.

Warning: The factory sdkconfig must set CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWED=y because writing at 0xB000 targets an address below the first formal partition. This is intentional: the factory app is a privileged recovery tool.

FileRole
/sdcard/esp3dfw.binFirmware image to flash to app0
/sdcard/esp3dfw.okRenamed from esp3dfw.bin after successful flash
/sdcard/esp3dfw.badRenamed from esp3dfw.bin after failed flash
/sdcard/ui_resources.binUI resources partition image
/sdcard/ui_resources.okRenamed after successful flash
/sdcard/ui_resources.badRenamed after failed flash

The first 16 bytes of ui_resources.bin are inspected for a build header (ESP3 + 12-char variant string). A mismatch is logged as a warning but does not abort the flash.

Optional Snapshot Capture (ENABLE_SNAPSHOT)

Section titled “Optional Snapshot Capture (ENABLE_SNAPSHOT)”

When compiled with ENABLE_SNAPSHOT, pressing GPIO0 (the board’s physical BOOT button) captures the current screen to a raw file on the SD card:

debounce 50 ms

all gfx calls go to display + file

GPIO0 pressed

(BOOT button)

snapshot_take()

sdcard_mount()

gfx_snapshot_begin()

/sdcard/snapNNN.raw

Full screen redraw

(menu or flash-progress)

gfx_snapshot_end()

fclose()

sdcard_unmount()

if not mid-flash

File format: 4-byte width (LE) + 4-byte height (LE) + raw RGB565 pixels, native-endian, row-major. Total size: 8 + 240 × 320 × 2 = 153 608 bytes per snapshot. Sequential filenames (snap000.raw, snap001.raw, …) are determined at startup by scanning existing files.


See tools_build_scripts.md for the general build infrastructure. This section covers what is specific to this board.

Variant nameMemoryCNC FirmwareTransportServices
4mb_wifi_fluidnc4 MBFluidNCWiFi (socket client) + SerialMDNS, UPDATE
4mb_serial_fluidnc4 MBFluidNCSerial (UART0)UPDATE
4mb_serial_grbl4 MBGRBLSerial (UART0)UPDATE
4mb_wifi_grblhal4 MBgrblHALWiFi (socket client) + SerialMDNS, UPDATE
4mb_serial_marlin4 MBMarlin (a14 groundwork)Serial (UART0)SD, UPDATE, FACTORY
factory_4mb4 MB——Recovery app only

WiFi and Bluetooth are mutually exclusive on this board (no PSRAM). Attempting to enable both causes a cmake/sanity_check.cmake build error.

Socket client / WebUI note: SOCKET_CLIENT_SERVICE=ON enables WiFi-to-CNC mode. WEBUI_SERVER, SSDP_SERVICE, and WS_SERVER_SERVICE are prohibited simultaneously (enforced by cmake/sanity_check.cmake). See feature_resource_matrix.md.

FileUsed when
sdkconfig.4mb.serialWIFI_SERVICE=OFF
sdkconfig.4mb.wifiWIFI_SERVICE=ON

board_config.cmake selects the appropriate defaults file at first configure time (when no sdkconfig exists yet in the build directory).

variants.py

VARIANTS + FACTORY_VARIANTS

build_one.py

build_variant / check_variant

generate_resources.py --build-name

ui_resources.bin

idf.py build

cmake + ninja

copy_factory_artifacts

bootloader + partitions → installer/

_copy_ui_resources_bin

ui_resources_*.bin → installer/

package_user_resources_kit

manifest + sources → installer/kit/

generate_flash_map

flash_mgr.py → installer/.json

a14 build changes:

  • The installer directory name comes from CMake (cmake/build_config_name.cmake, which appends the log level _LOG_<LEVEL>): installer_config_name(cmake_args, build_dir) reads <build_dir>/build_config_name.txt, and falls back to build_config_name() before any configure. copy_factory_artifacts(cmake_args, build_dir) uses it too.
  • build_config_name() (no log suffix) is passed to generate_resources.py as --build-name, so a resources binary fits the variant with or without logs (build trailer, see tools_build_scripts_ui_resources.md).
  • Factory/cmake/postbuild.cmake adds add_dependencies(app bootloader): the POST_BUILD step copies bootloader.bin, and on a clean build ninja could run the copy before the bootloader sub-build.
  • components/bsp/lv_conf.h is regenerated for LVGL 9.6.

All variant cmake flag lists are generated by make_variant_args(*on_flags). The function first sets every recognized root CMake option to OFF, then re-enables only the provided flags. This guarantees no stale CMake cache values survive from a previous build targeting a different board.

# Example: 4mb_wifi_fluidnc
make_variant_args(
"ESP32_2432S028R=ON",
"MEMORY_4_MB=ON",
"TARGET_FW_FLUIDNC=ON",
"SERIAL_SERVICE=ON",
"WIFI_SERVICE=ON",
"TFT_UI_SERVICE=ON",
"TFT_TOUCH_SERVICE=ON",
"SD_CARD_SERVICE=ON",
"MDNS_SERVICE=ON",
"SOCKET_CLIENT_SERVICE=ON",
"UPDATE_SERVICE=ON",
"FACTORY_SERVICE=ON",
)

cmake Capabilities Set by board_config.cmake

Section titled “cmake Capabilities Set by board_config.cmake”
CMake variableValueEffect
IDF_TARGETesp32Enforced via environment during build
RESOLUTION_SCREENres_320_240Shared UI resource set with pibot_pendant_v1_0
DEFAULT_ORIENTATIONPORTRAIT240 wide × 320 tall; fixed at compile time
DYNAMIC_ROTATION_FEATUREOFFRotation menu entry hidden (no runtime transform cost)
FIXED_UIOFFVirtual control positions computed relative to canvas shape
TFT_BRIGHTNESS_CONTROLONBacklight on/off control available
HARDWARE_BUTTONSOFFPhysical buttons absent
HARDWARE_ENCODEROFFEncoder absent
HARDWARE_SWITCHOFFSwitch absent
HARDWARE_POTENTIOMETEROFFPotentiometer absent
BUZZER_SERVICEOFF (forced)Buzzer not populated on this board

Featureesp32_2432s028rpibot_pendant_v1_0esp32_3248s035r
MCUESP32ESP32ESP32
DisplayILI9341 240×320 SPIILI9341 240×320 SPIST7796 480×320 SPI
Touch controllerXPT2046XPT2046XPT2046
Touch busSeparate bit-banged SPIShared hardware SPISeparate bit-banged SPI
Physical buttonsNone33
Rotary encoderNoneYesYes
BuzzerNoneYesYes
PSRAMNoneNoneNone
OTA slots1 (app0 only)2 (app0 + app1)2 (app0 + app1)
Custom bootloaderNoYes (physical button trigger)Yes (physical button trigger)
Factory entrySoftware [ESP444] onlySW + hold button at bootSW + hold button at boot
Max SPI speed20 MHz (HW limit)40 MHz40 MHz
UI resource setres_320_240res_320_240Different resolution

DocumentRelationship
display_drivers.mdSPI panel driver architecture, orientation/rotation math, snapshot state machine
esp32_memory_constraints.mdHeap budget per transport, fragmentation guidance for no-PSRAM builds
feature_resource_matrix.mdFeature/transport compatibility matrix and sanity_check.cmake rules
board_build_guidelines.mdGeneral board build conventions and variant naming
ui_resources_guide.mdHow to update icons, fonts, and theme palette via SD card
factory_app_technical_doc.mdCross-board factory app technical reference
pibot_pendant_v1_0_bsp.mdReference board — same ILI9341 + XPT2046 + res_320_240, with physical controls
esp32_3248s035r.mdSibling board — also uses XPT2046 on a separate bit-banged SPI bus
screens_architecture.mdScreen lifecycle and LVGL screen transition patterns
connection_management.mdTransport initialization and connection status system