Skip to content

Touch Controllers

The touch_controllers module provides a unified set of low-level hardware drivers for all touchscreen controllers used across the supported board SKUs. Each driver is self-contained, statically allocated, and exposes a consistent C API so that the board-level BSP can call it the same way regardless of the underlying chip.

Four controllers are supported:

DriverTechnologyBusBoards
FT5x06Capacitive (I²C)I²Cesp32s3_hmi43v3, esp32s3_zx3d50ce02s_usrc_4832
FT6336UCapacitive (I²C)I²Cpibot_pendant_v1_0
GT911Capacitive (I²C)I²Cesp32_3248s035c, esp32s3_4827s043c, esp32s3_8048s043c, esp32s3_8048s050c, esp32s3_8048s070c, esp32s3_bzm_tft35_gt911, esp32s3_8048_touch_lcd_7
XPT2046Resistive (SPI)SPI (HW or SW)esp32_2432s028r, esp32_3248s035r

LVGL (Core 1)

Touch Controller Drivers (this module)

Bus Drivers

Board Support Package (per-board)

bus init then configure()

bus init then configure()

bus init then configure()

read_reg12_fn callback

calls *_read()

calls *_read()

calls *_read()

calls *_read()

maps to lv_indev_data_t

init_touch_controller()

touch_read_cb() - LVGL indev callback

bus_i2c

(shared I²C)

bus_sw_spi

(bit-bang SPI)

ESP-IDF SPI

(hardware SPI)

touch_ft5x06

Capacitive I²C

touch_ft6336u

Capacitive I²C

touch_gt911

Capacitive I²C

touch_xpt2046

Resistive SPI

lv_indev_t

(pointer device)

activity_manager

(wake-up / screen timeout)

The drivers sit between the raw bus layer and LVGL. The BSP owns the bus (I²C or SPI) and the LVGL input-device registration; the driver owns the chip-level protocol, GPIO interrupt wiring, and coordinate transforms.


hardware/common/drivers/
├── touch_ft5x06/
│ ├── touch_ft5x06.c # Driver implementation
│ ├── touch_ft5x06.h # Public header
│ └── touch_ft5x06_config.h # touch_ft5x06_config_t / touch_ft5x06_data_t
├── touch_ft6336u/
│ ├── touch_ft6336u.c
│ ├── touch_ft6336u.h
│ └── touch_ft6336u_config.h
├── touch_gt911/
│ ├── touch_gt911.c
│ ├── touch_gt911.h
│ └── touch_gt911_config.h
└── touch_xpt2046/
├── touch_xpt2046.c
├── touch_xpt2046.h
└── touch_xpt2046_config.h
boards/<board>/components/bsp/
├── board_init.c # init_touch_controller(), touch_read_cb()
└── touch_xpt2046_def.h # Board-specific SPI read callback (XPT2046 boards only)

All four drivers implement the same conceptual API. Types are driver-prefixed but structurally identical.

// Configure and initialize the controller.
// Must be called once after the underlying bus is up.
// Returns ESP_OK on success, ESP_ERR_* otherwise.
esp_err_t touch_ft5x06_configure (const touch_ft5x06_config_t *config);
esp_err_t touch_ft6336u_configure(const touch_ft6336u_config_t *config);
esp_err_t touch_gt911_configure (const touch_gt911_config_t *config);
esp_err_t touch_xpt2046_configure(const touch_xpt2046_config_t *config);

Read (called every LVGL tick from touch_read_cb)

Section titled “Read (called every LVGL tick from touch_read_cb)”
touch_ft5x06_data_t touch_ft5x06_read (void);
touch_ft6336u_data_t touch_ft6336u_read(void);
touch_gt911_data_t touch_gt911_read (void);
touch_xpt2046_data_t touch_xpt2046_read(void);

All return the same logical structure:

typedef struct {
bool is_pressed; // true when a touch is detected
int16_t x; // X coordinate (after transforms), -1 when not pressed
int16_t y; // Y coordinate (after transforms), -1 when not pressed
} touch_<driver>_data_t;
void touch_ft5x06_deinit (void);
void touch_ft6336u_deinit(void);
void touch_gt911_deinit (void);
void touch_xpt2046_deinit(void);

Removes the GPIO ISR handler (where applicable) and clears the initialized state. The bus itself is not torn down — that remains the BSP’s responsibility.

uint16_t touch_<driver>_get_x_max(void);
uint16_t touch_<driver>_get_y_max(void);

Return the effective touch-resolution limits after any swap_xy transform. Used by the BSP when scaling touch coordinates to display pixel coordinates.


FieldTypeDescription
i2c_porti2c_port_tESP-IDF I²C port number
i2c_clk_speedintI²C clock frequency in Hz
rst_pinint8_tGPIO for hardware reset, -1 if not wired
int_pinint8_tGPIO for interrupt, -1 if not wired
swap_xyboolSwap X ↔ Y before returning data
invert_xboolInvert X axis (x_max − x)
invert_yboolInvert Y axis (y_max − y)
x_maxuint16_tTouch resolution width; 0 = read from device
y_maxuint16_tTouch resolution height; 0 = read from device

touch_ft5x06_config_t and touch_gt911_config_t

Section titled “touch_ft5x06_config_t and touch_gt911_config_t”
uint8_t i2c_addr[3]; // Candidate I²C addresses, 0-terminated

Both controllers can be factory-strapped to different I²C addresses. The driver probes each candidate in order and uses the first one that responds.

uint8_t i2c_addr; // Single fixed I²C address

The XPT2046 is resistive and SPI-based, so its configuration differs significantly:

typedef struct {
int8_t irq_pin; // IRQ pin, -1 if not wired
touch_xpt2046_read_reg12_fn_t read_reg12_fn; // Board SPI read callback
uint16_t touch_threshold; // Pressure threshold (Z1+4095-Z2)
bool swap_xy;
bool invert_x;
bool invert_y;
uint16_t x_max;
uint16_t y_max;
} touch_xpt2046_config_t;

The read_reg12_fn callback decouples the driver from the SPI bus ownership: on most boards the XPT2046 shares the display’s SPI bus, so the transaction must be issued by board-level code. The callback performs a 16-bit SPI transfer and returns the 12-bit ADC result.


FT5x06 (Capacitive, I²C, 8-bit register addresses)

Section titled “FT5x06 (Capacitive, I²C, 8-bit register addresses)”
I2C_BusGPIOFT5x06BSPI2C_BusGPIOFT5x06BSPalt[_touch_active == false (INT pin wired)]loop[LVGL tick (Core 1)]touch_ft5x06_configure(&cfg)INT pin → OUTPUT, drive LOWRST pin cycle (10 ms)INT pin → INPUT, falling-edge ISRProbe candidate addresses (write calibration regs, read IDs)Read x/y max in TEST_STATE mode (if not in config)ESP_OKtouch_ft5x06_read(){is_pressed=false}Read TOUCH_POINTS register (0x02)Read TOUCH1_XH..YL (4 bytes at 0x03)Apply swap_xy / invert_x / invert_y{is_pressed, x, y}

Interrupt strategy: ISR sets _touch_active = true (IRAM-safe, volatile). read() skips the I²C transaction when the flag is clear and an INT pin is wired. The flag is cleared at the start of each read cycle.

Address probing: up to three candidate addresses are tried in order. The driver writes FT5x06 vendor threshold registers during probing; the first address that acknowledges all writes is adopted.

Auto-detection of limits: if x_max or y_max is 0 in the config, the driver switches the chip into TEST_STATE (register 0x00 = 0x40), reads the hardware limits from registers 0x0C–0x0F, then returns to OP_STATE.


FT6336U (Capacitive, I²C, 8-bit register addresses)

Section titled “FT6336U (Capacitive, I²C, 8-bit register addresses)”
I2C_BusGPIOFT6336UBSPI2C_BusGPIOFT6336UBSPalt[No interrupt AND not previously pressed]loop[LVGL tick (Core 1)]touch_ft6336u_configure(&cfg)RST pin cycle (20 ms low, 300 ms stabilization)Ping (read CHIP_ID register)Write DEVICE_MODE, THRESHHOLD, TOUCHRATE_ACTIVEINT pin → INPUT, falling-edge ISR (if wired)Write INTERRUPT_MODE (trigger vs. polling)Read VENDOR_ID, CHIP_ID (log only)ESP_OKtouch_ft6336u_read(){is_pressed=false}Burst read 5 regs: TOUCH_POINTS, XH, XL, YH, YL (0x02-0x06)Apply swap_xy / invert_x / invert_yUpdate _was_pressed state{is_pressed, x, y}

Hybrid interrupt + polling strategy: the FT6336U does not reliably fire a release interrupt. A _was_pressed flag ensures registers are re-read every cycle until touch_points == 0 is confirmed, providing a reliable release transition. When no INT pin is wired, the chip is configured in polling mode (INTERRUPT_MODE = 0) and all reads are unconditional.


GT911 (Capacitive, I²C, 16-bit register addresses)

Section titled “GT911 (Capacitive, I²C, 16-bit register addresses)”
I2C_BusGPIOGT911BSPI2C_BusGPIOGT911BSPalt[status & 0x80 == 0 OR touch_points == 0]alt[_touch_active == false (INT pin wired)]loop[LVGL tick (Core 1)]touch_gt911_configure(&cfg)INT pin → OUTPUT, drive LOW (address strap)RST pin cycle (10 ms)INT pin → INPUT, falling-edge ISRProbe candidate addresses (read PRODUCT_ID @ 0x8140)Read XY_MAX @ 0x8048 (if x_max/y_max == 0)ESP_OKtouch_gt911_read(){is_pressed=false}Read status byte @ 0x814EWrite 0 to 0x814E (clear status){is_pressed=false}Read 4 coord bytes @ 0x8150Write 0 to 0x814E (clear status)Apply swap_xy / invert_x / invert_y{is_pressed, x, y}

Address strapping: the GT911 reads the INT pin level during its reset sequence to choose between I²C addresses 0x5D and 0x14. Driving INT LOW before releasing RST straps the device to 0x5D. After reset, the pin is reconfigured as a floating interrupt input. The driver then probes both candidates to confirm which address was selected.

Status register handshake: after each read, register 0x814E must be written 0x00 to acknowledge the data and re-arm the interrupt. Skipping this causes the controller to stall at the last reported coordinates.


SPI_CallbackGPIO_IRQXPT2046BSPSPI_CallbackGPIO_IRQXPT2046BSPalt[z < touch_threshold]alt[IRQ pin HIGH (not touched)]loop[LVGL tick (Core 1)]touch_xpt2046_configure(&cfg)Configure IRQ pin as input (level-poll, no ISR)ESP_OKtouch_xpt2046_read(){is_pressed=false}read_reg12_fn(CMD_Z1_READ)read_reg12_fn(CMD_Z2_READ)z = Z1 + 4095 - Z2{is_pressed=false}read_reg12_fn(CMD_X_READ)read_reg12_fn(CMD_Y_READ)Apply swap_xy / invert_x / invert_y{is_pressed=true, x (0-4095), y (0-4095)}

SPI ownership: the XPT2046 shares the display SPI bus on all known boards. The driver cannot own the bus because the SPI host must be time-multiplexed. Instead, the board defines a touch_xpt2046_spi_read_reg12 inline function in touch_xpt2046_def.h and passes it as read_reg12_fn in the config.

Two SPI strategies across boards:

  • esp32_2432s028r: the XPT2046 is on its own bit-banged (software) SPI bus — the callback delegates to bus_sw_spi_read_reg16.
  • esp32_3248s035r: the XPT2046 shares a hardware SPI bus with the display — the callback issues a raw spi_device_transmit.

Raw ADC coordinates: unlike the I²C drivers, XPT2046 returns raw 12-bit ADC values (0–4095). The BSP’s touch_read_cb applies a linear calibration mapping (TOUCH_CALIBRATION_X_MIN/MAX) to convert these to display pixel coordinates.


All four drivers apply the same optional transform chain internally, in this order:

Yes

No

Yes

No

Yes

No

Raw (x, y)

from hardware

swap_xy?

x ↔ y

invert_x?

x = x_max − x

invert_y?

y = y_max − y

Transformed (x, y)

returned to BSP

These transforms handle physical panel mounting orientation. The BSP may then apply a second linear scaling step when the touch controller’s native resolution differs from the display’s pixel resolution (e.g. esp32s3_4827s043c scales GT911 native touch coordinates to display pixel coordinates using get_x_max() / get_y_max()).


Every board follows the same two-function pattern regardless of which driver is selected.

init_touch_controller() — called once from board_init()

Section titled “init_touch_controller() — called once from board_init()”
static esp_err_t init_touch_controller(void) {
// 1. Initialize the bus (I²C or SPI) — owned by the BSP
esp_err_t ret = bus_i2c_init(I2C_PORT, SDA_PIN, SCL_PIN, I2C_FREQ_HZ);
if (ret != ESP_OK) { return ret; }
// 2. Call the driver configure with a board-specific default config struct
ret = touch_gt911_configure(&touch_gt911_default_config);
if (ret != ESP_OK) { return ret; }
return ESP_OK;
}

Note: esp32s3_hmi43v3 is an exception — its FT5x06 shares an I²C bus that is initialized elsewhere in board_init, so init_touch_controller() calls touch_ft5x06_configure() directly without calling bus_i2c_init.

touch_read_cb() — registered as the LVGL pointer input device callback

Section titled “touch_read_cb() — registered as the LVGL pointer input device callback”
static void touch_read_cb(lv_indev_t *indev, lv_indev_data_t *data) {
static bool last_pressed_state = false;
static bool touch_consumed_for_wakeup = false;
touch_<driver>_data_t touch_data = touch_<driver>_read();
if (touch_data.is_pressed) {
if (!last_pressed_state) {
// Transition: released → pressed
if (activity_process_event()) { // Wake-up gate
data->state = LV_INDEV_STATE_PRESSED;
data->point.x = touch_data.x; // (scaled if needed)
data->point.y = touch_data.y;
touch_consumed_for_wakeup = false;
} else {
touch_consumed_for_wakeup = true; // Suppress wake-up press
data->state = LV_INDEV_STATE_RELEASED;
}
last_pressed_state = true;
} else {
// Sustain press
data->state = touch_consumed_for_wakeup
? LV_INDEV_STATE_RELEASED
: LV_INDEV_STATE_PRESSED;
data->point.x = touch_data.x;
data->point.y = touch_data.y;
}
} else {
if (last_pressed_state) {
// Transition: pressed → released
if (!touch_consumed_for_wakeup) {
activity_process_event(); // Extend screen-on timer
}
last_pressed_state = false;
touch_consumed_for_wakeup = false;
}
data->state = LV_INDEV_STATE_RELEASED;
}
}

Wake-up consumption: the first touch press after the screen has timed out is intercepted by activity_process_event(), which wakes the display and returns false. The BSP masks that press from LVGL so a tap that only wakes the screen does not also activate a UI element underneath the finger.

This callback runs on LVGL’s task (Core 1). The driver’s read() functions are therefore always called from Core 1 and must not block for more than the I²C timeout (50 ms maximum). All ISR handlers are placed in IRAM (IRAM_ATTR) and only set a volatile flag — they never touch I²C or SPI directly.


ISR context (IRAM, any core)

LVGL task - Core 1

skip

read

touch_*_read()

INT pin wired?

+ flag set?

or _was_pressed?

(FT6336U only)

return {is_pressed=false}

I²C / SPI bus read

return {is_pressed, x, y}

_touch_active = true

(volatile bool)

GPIO falling edge (INT/IRQ pin)

The XPT2046 has no ISR: the IRQ pin is polled synchronously with gpio_get_level() inside read() because resistive controllers do not latch a state register — the raw ADC must always be read while the finger is still pressing.


All driver state is statically allocated — no malloc or new is used anywhere in this module. Each driver holds:

  • One copy of its config_t struct
  • A handful of scalar state variables (_is_initialized, _i2c_addr, _x_max, _y_max, _touch_active, and for FT6336U _was_pressed)

Only one driver is compiled into any given firmware image (the BSP for each board includes exactly one touch driver component), so RAM consumption is that of a single driver — typically under 64 bytes of .bss.


BoardDriverBusI²C Address(es)
esp32_2432s028rXPT2046Software SPI—
esp32_3248s035cGT911I²C0x5D, 0x14 (probed)
esp32_3248s035rXPT2046Hardware SPI—
esp32s3_4827s043cGT911I²C0x5D, 0x14 (probed)
esp32s3_8048_touch_lcd_7GT911I²C0x5D, 0x14 (probed)
esp32s3_8048s043cGT911I²C0x5D, 0x14 (probed)
esp32s3_8048s050cGT911I²C0x5D, 0x14 (probed)
esp32s3_8048s070cGT911I²C0x5D, 0x14 (probed)
esp32s3_bzm_tft35_gt911GT911I²C0x5D, 0x14 (probed)
esp32s3_hmi43v3FT5x06I²C0x38, 0x3B (probed)
esp32s3_zx3d50ce02s_usrc_4832FT5x06I²C0x38, 0x3B (probed)
pibot_pendant_v1_0FT6336UI²C0x38 (fixed)