Skip to content

Factory Touch Module

The factory_touch module provides minimal, polling-based touch input for the Factory Application. It exposes a single unified API (touch_init / touch_read) that returns calibrated screen-pixel coordinates, hiding all hardware differences behind a board-specific .c implementation. There are no FreeRTOS tasks, no interrupts, and no LVGL dependency — the factory app polls touch directly from its main loop via factory_input_dispatch.

This is deliberately separate from the main pendant firmware’s BSP touch stack (see bsp_touch_controllers): the factory app is a standalone ESP-IDF application flashed to its own OTA partition and must boot even if the main firmware partition is corrupt or absent.


Shared hardware drivers (BSP)

Board-specific implementations

Factory Application

(app_main)

factory_input_dispatch

dispatch_button / touch_hint_hit_test

factory_touch

touch_init / touch_read

GT911 - direct

esp32_3248s035c

esp32s3_4827s043c

GT911 - rescaled

esp32s3_8048_touch_lcd_7

esp32s3_8048s043c

esp32s3_8048s050c

XPT2046 - bit-bang SPI

esp32_2432s028r

XPT2046 - shared HW SPI

esp32_3248s035r

touch_gt911

hardware/common/drivers/touch_gt911

bus_i2c

hardware/common/drivers/bus_i2c

ESP-IDF spi_master


Module Position in the Factory Application

Section titled “Module Position in the Factory Application”

factory_touch is one of several hardware-driver sub-modules assembled by factory_app. It is consumed exclusively by factory_input_dispatch, which maps touch coordinates to on-screen button hit regions and dispatches menu events to factory_core.

Factory Application

factory_core

(menu, update actions,

visual feedback, snapshot)

factory_input_dispatch

factory_touch

factory_buttons

factory_graphics

factory_lcd_drivers

factory_sdcard

factory_buzzer

factory_encoder


Every board variant exposes an identical C interface via its touch.h / touch.c pair.

typedef struct {
bool pressed; // true when a touch is actively detected
int16_t x; // screen X coordinate in pixels, or -1 if not pressed
int16_t y; // screen Y coordinate in pixels, or -1 if not pressed
} touch_point_t;

All coordinates are already mapped to final screen-pixel space (origin top-left, increasing right and down). No further scaling is required by callers.

Initialises the hardware for the touch controller on the current board. Must be called once during app_main, typically after the LCD driver is initialised (mandatory on boards where touch shares the display SPI bus).

  • On failure (e.g. I2C bus error, SPI device registration error) the function returns silently and sets an internal s_initialized = false flag. Subsequent touch_read() calls return { .pressed = false, .x = -1, .y = -1 }.
  • On GT911 boards the function also initialises the I2C bus via bus_i2c_init().

Polls the touch controller and returns the current touch state.

Return fieldNot pressedPressed
pressedfalsetrue
x-1pixel column [0, SCREEN_WIDTH − 1]
y-1pixel row [0, SCREEN_HEIGHT − 1]

The function is non-blocking and safe to call from the main loop at any rate. It performs a synchronous hardware transaction on every call (no caching).


BoardControllerBusCoordinate Strategy
esp32_2432s028rXPT2046Bit-banged SPI (dedicated)Calibration handled inside touch.c
esp32_3248s035cGT911I2CDriver-internal (auto-detected x/y max, no rescale needed)
esp32_3248s035rXPT2046Shared hardware SPI (with ST7796)Linear calibration via TOUCH_CALIBRATION_X/Y_MIN/MAX + pressure threshold
esp32s3_4827s043cGT911I2CDriver-internal (auto-detected x/y max, no rescale needed)
esp32s3_8048_touch_lcd_7GT911I2CRescaled: raw * SCREEN_W / touch_gt911_get_x_max()
esp32s3_8048s043cGT911I2CRescaled: raw * SCREEN_W / touch_gt911_get_x_max()
esp32s3_8048s050cGT911I2CRescaled: raw * SCREEN_H / touch_gt911_get_y_max()

Additional boards (esp32s3_8048s070c, esp32s3_bzm_tft35_gt911, esp32s3_hmi43v3, esp32s3_zx3d50ce02s_usrc_4832, pibot_pendant_v1_0) follow the same pattern and are grouped under factory_hardware_drivers in the module tree.


Variant A — GT911 / I2C, Direct Pass-Through

Section titled “Variant A — GT911 / I2C, Direct Pass-Through”

Boards: esp32_3248s035c, esp32s3_4827s043c

GT911 IC (I2C)touch_gt911 (shared)touch.capp_mainGT911 IC (I2C)touch_gt911 (shared)touch.capp_mainloop[main loop polling]touch_init()bus_i2c_init(port, sda, scl, freq)touch_gt911_configure(&config)I2C probe + config registers writeACKESP_OKtouch_read()touch_gt911_read()I2C read status + point registersraw x, y, pressedtouch_gt911_data_ttouch_point_t (x, y already in screen pixels)

The GT911 driver applies swap_xy, invert_x, and invert_y flags internally (sourced from hw_config.h). Reported coordinates match the panel’s native resolution, so no post-processing is needed.

The touch_gt911_config_t used at init time:

static const touch_gt911_config_t config = {
.i2c_addr = TOUCH_I2C_ADDR_LIST,
.i2c_port = TOUCH_I2C_PORT_IDX,
.i2c_clk_speed = TOUCH_I2C_FREQ_HZ,
.rst_pin = TOUCH_RST_PIN,
.int_pin = TOUCH_IRQ_PIN,
.swap_xy = TOUCH_SWAP_XY_FLAG,
.invert_x = TOUCH_MIRROR_X_FLAG,
.invert_y = TOUCH_MIRROR_Y_FLAG,
.x_max = 0, // auto-detected from device registers
.y_max = 0, // auto-detected from device registers
};

Variant B — GT911 / I2C, Rescaled Coordinates

Section titled “Variant B — GT911 / I2C, Rescaled Coordinates”

Boards: esp32s3_8048_touch_lcd_7, esp32s3_8048s043c, esp32s3_8048s050c

Identical initialisation to Variant A. The difference is in touch_read():

// GT911 config registers report ~468x253 for an 800x480 physical panel.
// Rescale to actual SCREEN_WIDTH / SCREEN_HEIGHT at read time.
pt.x = (int)data.x * SCREEN_WIDTH / touch_gt911_get_x_max();
pt.y = (int)data.y * SCREEN_HEIGHT / touch_gt911_get_y_max();

No

Yes

No

Yes

touch_read() called

s_initialized?

return

{pressed=false, x=-1, y=-1}

touch_gt911_read()

is_pressed?

pt.x = data.x * SCREEN_WIDTH / get_x_max()

pt.y = data.y * SCREEN_HEIGHT / get_y_max()

return {pressed=true, x, y}

Why rescaling is needed: The GT911 stores its own maximum resolution in on-chip config registers. On these panels the programmed value (~468 / ~253) does not match the physical RGB panel resolution (800 × 480). touch_gt911_get_x_max() / get_y_max() read back what the IC actually reports, so the rescale is always accurate regardless of the config value.


Variant C — XPT2046 / Shared Hardware SPI

Section titled “Variant C — XPT2046 / Shared Hardware SPI”

Board: esp32_3248s035r

The XPT2046 is added as a second SPI device on the same TFT_HOST bus used by the ST7796 LCD. touch_init() must therefore be called after st7796_init() has already configured the bus.

XPT2046 ICESP-IDF spi_mastertouch.cst7796_init() [called first]app_mainXPT2046 ICESP-IDF spi_mastertouch.cst7796_init() [called first]app_mainalt[z < TOUCH_PRESSURE_THRESHOLD][touched]loop[main loop polling]st7796_init() - configures TFT_HOST bustouch_init()spi_bus_add_device(TFT_HOST, &dev_cfg, &s_spi_dev)s_spi_dev handletouch_read()read_reg12(CMD_Z1_READ)read_reg12(CMD_Z2_READ)z = z1 + 4095 - z2{pressed=false, x=-1, y=-1}read_reg12(CMD_X_READ)read_reg12(CMD_Y_READ)apply swap_xy / mirror_x / mirror_y flagscalibrate(raw, cal_min, cal_max, screen_range){pressed=true, x, y}

Each SPI transaction is a 3-byte full-duplex exchange. The 12-bit ADC result occupies the upper bits of the 16-bit response:

TX: [ CMD ][ 0x00 ][ 0x00 ]
RX: [ --- ][ MSB ][ LSB ]
result = (MSB << 8 | LSB) >> 3 // 12-bit ADC value [0..4095]

XPT2046 command bytes used:

ConstantValueMeasures
CMD_X_READ0xD0X position
CMD_Y_READ0x90Y position
CMD_Z1_READ0xB0Pressure Z1
CMD_Z2_READ0xC0Pressure Z2
int16_t z = z1 + 4095 - z2;
if (z < TOUCH_PRESSURE_THRESHOLD) { return; } // TOUCH_PRESSURE_THRESHOLD = 300

This is not the XPT2046 datasheet’s impedance formula but is reliable in practice for discriminating a real press from panel noise.

static int16_t calibrate(int16_t raw, int16_t cal_min, int16_t cal_max, int16_t range)
{
int32_t clamped = (raw > cal_min) ? (raw - cal_min) : 0;
return (int16_t)(clamped * range / (cal_max - cal_min));
}

TOUCH_CALIBRATION_X_MIN, TOUCH_CALIBRATION_X_MAX, TOUCH_CALIBRATION_Y_MIN, TOUCH_CALIBRATION_Y_MAX are defined in each board’s hw_config.h.

Applied after raw reads, before calibration:

1. TOUCH_SWAP_XY_FLAG → swap raw_x ↔ raw_y
2. TOUCH_MIRROR_X_FLAG → raw_x = 4095 - raw_x
3. TOUCH_MIRROR_Y_FLAG → raw_y = 4095 - raw_y
4. calibrate() to pixel coordinates

Board: esp32_2432s028r

Uses a dedicated GPIO bit-banged SPI bus (independent from the display bus). Pin assignments and calibration constants are in hw_config.h. The structure is equivalent to Variant C without the spi_bus_add_device step. See the shared driver reference at bsp_touch_controllers → touch_xpt2046.


I2C or SPI poll

pressed + x,y

hit button index

MENU_UP / MENU_DOWN

/ SELECT

Touch IC

(GT911 / XPT2046)

touch_read()

touch_point_t

touch_hint_hit_test()

factory_input_dispatch

dispatch_button()

factory_input_dispatch

menu_move() / menu_select()

factory_menu_system

execute_selected_action()

factory_update_actions

touch_hint_hit_test() compares the (x, y) coordinate against the on-screen button hint rectangles rendered by factory_graphics (draw_button_hint_at). When a hit is found, dispatch_button() fires the same code path as a physical button press from factory_buttons.


All implementations use the same guard idiom to make touch_read() safe before touch_init() succeeds:

static bool s_initialized = false;
void touch_init(void) {
// ... hardware setup ...
if (error) { return; } // s_initialized stays false
s_initialized = true;
}
touch_point_t touch_read(void) {
touch_point_t pt = { .pressed = false, .x = -1, .y = -1 };
if (!s_initialized) {
return pt;
}
// ... hardware poll ...
}

This means the factory menu continues to operate via physical buttons even if the touch controller fails to initialise.


DependencyTypeUsed by variants
touch_gt911 (hardware/common/drivers/touch_gt911)Shared BSP driverA, B
bus_i2c (hardware/common/drivers/bus_i2c)Shared BSP driverA, B
ESP-IDF spi_masterESP-IDF componentC, D
hw_config.hBoard-local header (pin/cal constants)All
factory_log.hFactory logging macroAll

For the full shared driver API, see bsp_touch_controllers and bsp_bus_drivers.


boards/
├── esp32_2432s028r/Factory/main/
│ └── touch.h # touch_point_t; XPT2046 bit-banged SPI
├── esp32_3248s035c/Factory/main/
│ ├── touch.h # touch_point_t; GT911 I2C
│ └── touch.c # Variant A — GT911 direct pass-through
├── esp32_3248s035r/Factory/main/
│ ├── touch.h # touch_point_t; XPT2046 shared HW SPI
│ └── touch.c # Variant C — XPT2046 shared SPI + calibration
├── esp32s3_4827s043c/Factory/main/
│ ├── touch.h # touch_point_t; GT911 I2C
│ └── touch.c # Variant A — GT911 direct pass-through
├── esp32s3_8048_touch_lcd_7/Factory/main/
│ ├── touch.h # touch_point_t; GT911 I2C
│ └── touch.c # Variant B — GT911 rescaled
├── esp32s3_8048s043c/Factory/main/
│ ├── touch.h # touch_point_t; GT911 I2C
│ └── touch.c # Variant B — GT911 rescaled
└── esp32s3_8048s050c/Factory/main/
├── touch.h # touch_point_t; GT911 I2C
└── touch.c # Variant B — GT911 rescaled

ModuleRelationship
factory_appParent — assembles all factory hardware sub-modules
factory_input_dispatchConsumer — calls touch_read() and maps coordinates to menu events
factory_buttonsSibling input source — physical GPIO buttons, same dispatch path
factory_encoderSibling input source — rotary encoder for menu navigation
factory_lcd_driversMust be initialised first on shared-SPI boards (esp32_3248s035r)
factory_graphicsRenders button hint zones whose coordinates touch_hint_hit_test() checks
bsp_touch_controllersProvides the shared touch_gt911 and touch_xpt2046 hardware drivers
bsp_bus_driversProvides bus_i2c_init() used by GT911 variants