Skip to content

BSP Touch Controllers

The bsp_touch_controllers module provides a unified set of hardware drivers for touchscreen input across all supported board variants. It is the lowest-level touch layer in the BSP stack, sitting between raw touch controller silicon and the BSP Board Initialization layer, which bridges touch data into the LVGL input device framework.

Four independent drivers cover the complete range of touch controllers deployed across the supported boards:

DriverTechnologyBusTypical Boards
touch_ft5x06Capacitive — FocalTech FT5x06 familyI2Cesp32_3248s035c, esp32s3_hmi43v3, esp32s3_zx3d50ce02s_usrc_4832
touch_ft6336uCapacitive — FocalTech FT6336UI2Cesp32s3_4827s043c
touch_gt911Capacitive — Goodix GT911I2Cesp32s3_bzm_tft35_gt911, esp32s3_8048* series
touch_xpt2046Resistive — XPT2046SPI (shared with display)esp32_2432s028r, esp32_3248s035r, pibot_pendant_v1_0

All four drivers expose an identical public API shape (configure / read / get_x_max / get_y_max / deinit) so board-level code can swap controllers without touching the LVGL integration layer.


The bsp_touch_controllers module depends only on ESP-IDF peripheral drivers (I2C, SPI, GPIO) and esp3d_log. It is consumed exclusively by board-specific board_init.c files, which wire touch data into LVGL’s lv_indev_t pointer device.

LVGL Input Device (Core 1)

BSP Board Initialization

ESP-IDF Peripheral Drivers

BSP Touch Controllers (this module)

touch_ft5x06

FocalTech FT5x06

I2C Capacitive

touch_ft6336u

FocalTech FT6336U

I2C Capacitive

touch_gt911

Goodix GT911

I2C Capacitive

touch_xpt2046

XPT2046

SPI Resistive

driver/i2c

driver/spi_master

driver/gpio

board_init.c (per board)

init_touch_controller()

touch_read_cb()

lv_indev_t

LV_INDEV_TYPE_POINTER

10 ms read timer

activity_manager

wake-up arbitration

Hardware Abstraction Layer

bsp

bsp_bsp_board_initialization

bsp_touch_controllers

(current module)

bsp_display_drivers_spi

bsp_display_drivers_i80

bsp_display_drivers_rgb

bsp_bus_drivers

bsp_io_expanders

bsp_physical_inputs


touch_ft5x06 — FocalTech FT5x06 Family (I2C Capacitive)

Section titled “touch_ft5x06 — FocalTech FT5x06 Family (I2C Capacitive)”

Source: hardware/common/drivers/touch_ft5x06/

The FT5x06 driver targets the FocalTech FT5206/FT5306/FT5406 and compatible ICs. All register addresses are 8-bit.

Key characteristics:

  • Probes a 0-terminated list of up to two candidate I2C addresses at initialization — boards can ship with either address depending on pull resistors.
  • Writes vendor-recommended calibration thresholds (THGROUP, THPEAK, THCAL, THWATER, THTEMP, THDIFF, PERIODACTIVE, PERIODMONITOR) to each candidate address during the probe loop; the first address that ACKs is kept.
  • Can auto-discover screen resolution from the device itself: enters test mode (FT5X06_TEST_STATE) and reads registers 0x0C–0x0F when x_max/y_max are 0 in config.
  • Interrupt pin used as a hint only — the ISR sets volatile bool _touch_active; the I2C register is the source of truth.
  • 50 ms I2C timeout on all transactions to avoid blocking the LVGL task.

Initialization sequence:

  1. Drive INT pin low (output) as a pre-reset address strap.
  2. Toggle RST pin: LOW (10 ms) → HIGH (10 ms).
  3. Reconfigure INT pin as floating input with falling-edge ISR.
  4. Walk i2c_addr[]: write calibration registers, read Vendor/Firmware IDs. First success wins.
  5. If x_max/y_max == 0: read resolution registers in test mode, then restore operation mode.
  6. Apply swap_xy to axis limits if configured.

touch_ft6336u — FocalTech FT6336U (I2C Capacitive)

Section titled “touch_ft6336u — FocalTech FT6336U (I2C Capacitive)”

Source: hardware/common/drivers/touch_ft6336u/

The FT6336U is a single fixed-address capacitive controller for smaller panels.

Key characteristics:

  • Single I2C address (no probe loop). Uses a real register read of Chip ID (0xA3) as the device ping, because zero-length I2C writes are unreliable with the ESP-IDF legacy I2C driver and return ESP_ERR_TIMEOUT even when the device is present.
  • Hybrid interrupt + state tracking: The FT6336U does not always fire an interrupt on finger release. The driver keeps a _was_pressed flag and continues polling registers until it observes touch_points == 0, ensuring LVGL always receives a clean LV_INDEV_STATE_RELEASED transition.
  • Supports interrupt mode (FT6336U_INTERRUPT_MODE = 1) when an INT pin is wired, or falls back to pure polling mode (FT6336U_INTERRUPT_MODE = 0) when int_pin == -1.
  • Hardware reset sequence is mandatory before the ping — the device does not respond on I2C while held in reset.

Release detection logic:

ISR fires → _touch_active = true
→ read registers → is_pressed = true → _was_pressed = true
Next tick → _touch_active = false BUT _was_pressed = true → still read registers
→ touch_points = 0 → is_pressed = false → _was_pressed = false → stop polling

touch_gt911 — Goodix GT911 (I2C Capacitive)

Section titled “touch_gt911 — Goodix GT911 (I2C Capacitive)”

Source: hardware/common/drivers/touch_gt911/

The GT911 is a 5-point capacitive touch controller widely used on larger ESP32-S3 panels. All register addresses are 16-bit.

Key characteristics:

  • Address strapping via INT pin: The GT911 samples the INT pin level during hardware reset to select its I2C address. The driver holds INT low during RST toggle (selecting 0x5D by default), then reconfigures INT as an interrupt input.
  • Probes a 0-terminated list of candidate addresses by reading Product ID register (0x8140).
  • Auto-discovers resolution from GT911_XY_MAX_REG (0x8048) when config values are 0. The 4-byte register contains X-max (little-endian) at bytes 0–1, Y-max at bytes 2–3.
  • Status register 0x814E: bit 7 = buffer-ready, bits 3:0 = active touch point count. The driver must write 0 to 0x814E after each read to acknowledge the buffer; failing to do so stalls the interrupt line.
  • All I2C transactions use a 16-bit address prefix (two register-address bytes).

I2C address strap summary:

INT level during RSTI2C address
LOW (driver default)0x5D
HIGH0x14

Source: hardware/common/drivers/touch_xpt2046/

The XPT2046 is a resistive ADC touch controller. It differs fundamentally from the I2C capacitive drivers: it has no internal event buffer, requires pressure validation, and shares the display’s SPI bus.

Key characteristics:

  • SPI bus owned by board-level code. The driver receives a function pointer touch_xpt2046_read_reg12_fn_t at configure time. The board’s touch_xpt2046_def.h provides a static inline implementation using spi_device_transmit(). The XPT2046 is added as a second device on the display SPI bus via spi_bus_add_device().
  • No ISR. The IRQ pin is sampled synchronously with gpio_get_level() each read cycle.
  • Pressure validation: reads CMD_Z1_READ and CMD_Z2_READ; computes z = z1 + 4095 − z2. If z < touch_threshold, the reading is rejected.
  • Raw ADC values are 12-bit (0–4095). invert_x/invert_y mirror from 4095 (the ADC range), not from x_max (the panel resolution).
  • No coordinate auto-discovery — x_max/y_max must always be set in the config.

SPI transaction format (half-duplex):

┌──────────────┬─────────────────────────────┐
│ CMD (8-bit) │ RX 16-bit → right-shift 3 │
└──────────────┴─────────────────────────────┘

All four drivers expose the same five-function interface:

/* Initialize the controller.
* Must be called after the underlying bus (I2C or SPI) is ready.
* Safe to call only once — subsequent calls return ESP_OK immediately. */
esp_err_t touch_xxx_configure(const touch_xxx_config_t *config);
/* Read current touch state. Non-blocking.
* Called from touch_read_cb() at ~10 ms intervals on Core 1.
* Returns { is_pressed=false, x=-1, y=-1 } when idle. */
touch_xxx_data_t touch_xxx_read(void);
/* Return the configured or auto-discovered horizontal resolution. */
uint16_t touch_xxx_get_x_max(void);
/* Return the configured or auto-discovered vertical resolution. */
uint16_t touch_xxx_get_y_max(void);
/* Release GPIO ISR handler and reset internal driver state.
* Subsequent calls to touch_xxx_read() return not-pressed. */
void touch_xxx_deinit(void);

Return Data Structure (identical for all four drivers)

Section titled “Return Data Structure (identical for all four drivers)”
typedef struct {
bool is_pressed; /* true while the panel is being touched */
int16_t x; /* logical X coordinate; -1 when not pressed */
int16_t y; /* logical Y coordinate; -1 when not pressed */
} touch_xxx_data_t;

FT5x06 and GT911 — I2C, multi-address probe

Section titled “FT5x06 and GT911 — I2C, multi-address probe”
typedef struct {
uint8_t i2c_addr[3]; /* 0-terminated list of candidate I2C addresses */
i2c_port_t i2c_port; /* ESP-IDF I2C port number */
int i2c_clk_speed; /* I2C bus clock in Hz */
int8_t rst_pin; /* RST GPIO; -1 = not wired */
int8_t int_pin; /* INT GPIO; -1 = pure polling mode */
bool swap_xy; /* swap logical X and Y axes */
bool invert_x; /* mirror on X axis */
bool invert_y; /* mirror on Y axis */
uint16_t x_max; /* horizontal resolution; 0 = auto-discover */
uint16_t y_max; /* vertical resolution; 0 = auto-discover */
} touch_ft5x06_config_t;
/* touch_gt911_config_t has the same field layout */
typedef struct {
uint8_t i2c_addr; /* fixed I2C address (not a probe list) */
i2c_port_t i2c_port;
int i2c_clk_speed;
int8_t rst_pin;
int8_t int_pin;
bool swap_xy;
bool invert_x;
bool invert_y;
uint16_t x_max;
uint16_t y_max;
} touch_ft6336u_config_t;
/* Board-supplied SPI read callback: sends one 8-bit command, returns 12-bit result */
typedef uint16_t (*touch_xpt2046_read_reg12_fn_t)(uint8_t reg);
typedef struct {
int8_t irq_pin; /* IRQ GPIO; -1 = not wired */
touch_xpt2046_read_reg12_fn_t read_reg12_fn; /* board-supplied SPI read callback */
uint16_t touch_threshold; /* minimum pressure z to qualify touch */
bool swap_xy;
bool invert_x;
bool invert_y;
uint16_t x_max; /* panel resolution (not ADC range) */
uint16_t y_max;
} touch_xpt2046_config_t;

BoardTouch DriverBusNotes
esp32_2432s028rXPT2046SPI shared with ILI9341Resistive
esp32_3248s035cFT5x06I2CCapacitive
esp32_3248s035rXPT2046SPI shared with ST7796Resistive; board has touch_calibrate()
esp32s3_4827s043cFT6336UI2CCapacitive
esp32s3_8048_touch_lcd_7GT911I2CCapacitive; 5-point
esp32s3_8048s043cGT911I2CCapacitive; 5-point
esp32s3_8048s050cGT911I2CCapacitive; 5-point
esp32s3_8048s070cGT911I2CCapacitive; 5-point
esp32s3_bzm_tft35_gt911GT911I2CCapacitive; board name encodes chip model
esp32s3_hmi43v3FT5x06I2CCapacitive
esp32s3_zx3d50ce02s_usrc_4832FT5x06I2CCapacitive
pibot_pendant_v1_0XPT2046SPI shared with ILI9341Resistive; also has encoder / buttons / potentiometer
dlc32_max_lcd(none)—Display only; no init_touch_controller present
fysetc_wifi_pro(none)—No display or touch

The sequence below applies to every board compiled with ESP3D_TOUCH_FEATURE=1. It runs once from board_init() before LVGL is started.

LVGLtouch_xxx_configureBus Driverboard_initLVGLtouch_xxx_configureBus Driverboard_initI2C drivers onlybus_i2c_init or spi_bus_add_deviceESP_OKtouch_xxx_configure with config structValidate config and pinsConfigure RST and INT GPIO pinsHardware reset RST LOW 10ms then HIGH 10msInstall ISR service and add INT pin handlerProbe I2C addresses, write calibration regs, read Chip IDAuto-discover x_max and y_max if config values are 0Apply swap_xy to stored axis limitsESP_OKlv_indev_createlv_indev_set_type POINTERlv_indev_set_read_cb touch_read_cblv_timer_set_period 10ms

LVGL calls touch_read_cb() every 10 ms on Core 1. The callback polls the driver and translates the result into LVGL input device state, with a wake-up arbitration step:

No pin

flag clear AND

not _was_pressed

flag set OR

_was_pressed

not pressed

is_pressed

Yes

display was sleeping

display was awake

No: continuing press

Yes

No

LVGL read timer

10 ms - Core 1

touch_read_cb(indev, data)

touch_xxx_read()

INT / IRQ pin

wired?

Read bus registers (always)

Return not-pressed

No bus transaction

Parse raw coordinates

Apply swap_xy, invert_x, invert_y

touch_xxx_data_t

{ is_pressed, x, y }

data.state = RELEASED

On leading-edge release:

activity_process_event()

First press?

last_pressed was false

activity_process_event()

data.state = RELEASED

touch_consumed_for_wakeup = true

data.state = PRESSED

data.point = x, y

touch_consumed

_for_wakeup?

data.state = RELEASED


Each driver optimises bus usage differently based on hardware guarantees:

XPT2046 - Synchronous IRQ + pressure gate

No

Yes

No

Yes

gpio_get_level(irq_pin)

Level == 0?

Skip SPI read

Read Z1, Z2

z = Z1 + 4095 - Z2

z >= threshold?

Read X, Y ADC

12-bit, 0-4095

FT6336U - Hybrid hint + release tracking

No

Yes

Yes

No

ISR: _touch_active = true

_touch_active

OR _was_pressed?

Skip bus read

Clear flag

Read registers

Update _was_pressed

_was_pressed?

FT5x06 / GT911 - Interrupt as strict hint

No

Yes

ISR: _touch_active = true

_touch_active?

Skip bus read (idle)

Clear flag

Read registers

Source of truth


All drivers apply transforms in the same fixed order to guarantee consistent logical-axis semantics regardless of physical display orientation:

Yes

No

Yes (I2C)

Yes (SPI)

No

Yes

No

Raw x, y

from hardware

swap_xy?

x, y = y, x

invert_x?

x = x_max - x

x = 4095 - x

invert_y?

y = y_max - y

or 4095 - y for XPT2046

Logical x, y

reported to LVGL

Order is critical: swap_xy must precede invert_x/invert_y so that the inversion always operates on the logical axis after any rotation, not the physical one.


All I2C drivers use pdMS_TO_TICKS(50) for every transaction. The touch read callback runs inside the LVGL task (Core 1). A hung I2C transaction would stall the entire UI renderer. The 50 ms ceiling ensures LVGL degrades gracefully under bus errors rather than locking the display.

Capacitive controllers pulse INT low on each new touch event. However, the FT6336U does not reliably fire an interrupt on finger release. Using the interrupt only as a wake-up hint — with register content as the definitive state — avoids missed release events. When no INT pin is wired, pure polling is used with no functional difference to the BSP layer above.

The XPT2046 has no dedicated SPI bus on any supported board — it always shares the display controller’s bus (added via spi_bus_add_device()) with its own CS pin. Injecting the read_reg12_fn callback keeps bus-management logic in board-level code (where the SPI device handle lives) and leaves the driver fully agnostic of the physical bus.

touch_read_cb() calls activity_process_event() on the leading edge of each press. When the activity manager reports the display was sleeping, the touch is flagged touch_consumed_for_wakeup = true and suppressed from LVGL’s event queue for that entire press cycle. This prevents an accidental UI action when a user touches the screen only to wake it from the screen-timeout state.

All touch-related code in board_init.c is wrapped in #if (ESP3D_TOUCH_FEATURE). Boards without a touchscreen (dlc32_max_lcd, fysetc_wifi_pro) compile cleanly without pulling in any touch driver headers or GPIO ISR registrations.


DependencyRole
driver/i2c (ESP-IDF)I2C transactions for FT5x06, FT6336U, GT911
driver/spi_master (ESP-IDF)SPI bus device management for XPT2046 (board-level)
driver/gpio (ESP-IDF)RST, INT, IRQ pin configuration and ISR installation
freertos/task.hvTaskDelay() for reset timing in I2C drivers
esp3d_logesp3d_log() / esp3d_log_e() diagnostic macros
bsp_bus_driversbus_i2c_init() initialises the shared I2C bus before any I2C touch driver is configured
bsp_bsp_board_initializationConsumes drivers via init_touch_controller() and touch_read_cb()

  • BSP Board Initialization — how board_init() calls init_touch_controller() and wires touch_read_cb() into LVGL
  • BSP Bus Drivers — bus_i2c_init() must be called before any I2C touch driver
  • BSP Display Drivers SPI — XPT2046 shares the SPI bus initialised by the display driver; spi_bus_add_device() attaches it as a second device
  • BSP Control Events — per-board control event types, registered in board_init() alongside touch setup
  • Display Drivers Architecture — physical vs. logical resolution, rotation math, and SPI/RGB panel architecture