Skip to content

Touch Support

Phil Schatzmann edited this page Aug 30, 2026 · 1 revision

Overview

Touch support in TinyGPU is split into three independent layers, each usable on its own:

  1. TouchDriver - a small abstract interface (begin()/isTouched()/getPoint()) that every concrete touch controller implements. This is the only layer you need if you just want raw touch points.
  2. Calibration & coordinate mapping - built into the TouchDriver base class itself: raw controller coordinates are turned into logical display-space Points (inversion, per-axis scaling, axis swap, rotation), so application code never deals with raw ADC/register values.
  3. GestureDetector - an optional layer on top that turns a stream of raw touch samples from any TouchDriver into higher-level events (tap, double-tap, long-press, swipe, drag/pan/scroll, pinch/rotate).
#include <TinyGPU.h>
#include <TinyGPU/Input/TouchDriverArduino.h>

using namespace tinygpu;

TouchDriverFT6236 touch(Wire, /*rstPin=*/-1, /*irqPin=*/-1);

void setup() {
  Wire.begin();
  touch.begin();

  CalibrationData cal;
  cal.screenWidth = 240;
  cal.screenHeight = 320;
  touch.setCalibration(cal);
  touch.setRotation(Rotation::Deg90);
}

void loop() {
  if (touch.isTouched()) {
    Point p;
    if (touch.getPoint(p)) {
      // p.x / p.y are already in logical display space
    }
  }
}

Header layout

Header Contents Dependencies
TouchDriver.h Picks the right implementation automatically: TouchDriverArduino.h under ARDUINO, TouchDriverSDL.h when <SDL.h> is available, otherwise TouchDriverCommon.h -
TouchDriverCommon.h The actual platform-independent pieces: Point, CalibrationData, Rotation, and the TouchDriver base class (calibration storage + raw-to-logical coordinate mapping) None - no Arduino.h, no bus library. Safe to include transitively (e.g. via TinyGPU.h) without pulling in an Arduino core
TouchDriverArduino.h Concrete controller drivers: TouchDriverXPT2046, TouchDriverFT6236/TouchDriverFT6206, TouchDriverCST816S, TouchDriverGT911, TouchDriverBitBang Arduino API (pinMode/digitalWrite/SPIClass/TwoWire) via TinyGPU/Emulation.h - include this file explicitly to use any of these drivers
TouchDriverSDL.h TouchDriverSDL - maps the desktop mouse to a TouchDriver SDL2
GestureDetector.h GestureDetector - gesture recognition on top of any TouchDriver None beyond TouchDriver.h

TouchDriver.h (not TouchDriverArduino.h/TouchDriverSDL.h directly) is what TinyGPU.h itself includes, so a plain #include <TinyGPU.h> already gets you the right base types for whatever platform you're building for - include TouchDriverArduino.h explicitly only when you need one of its concrete controller classes.


The TouchDriver interface

class TouchDriver {
 public:
  virtual bool begin() = 0;
  virtual bool isTouched() = 0;
  virtual bool getPoint(Point& outPoint) = 0;
  virtual bool getSecondPoint(Point& outPoint) { return false; }

  void setRotation(Rotation rotation);
  Rotation getRotation() const;
  bool setCalibration(const CalibrationData& cal);
  const CalibrationData& getCalibration() const;
  bool hasCalibration() const;
};
  • begin() never initializes the SPI/I2C bus itself - the application is responsible for Wire.begin()/SPI.begin() and any board-specific bus pin configuration, the same division of responsibility DisplayDriver uses.
  • isTouched() is the once-per-loop-iteration entry point. For controllers without an IRQ pin it may perform an actual controller read; for TouchDriverGT911 specifically, it's where the chip's status register gets read and cleared - see that driver's notes below for why calling it more than once per loop iteration is unsafe.
  • getPoint() returns false if no valid touch point is currently available - always check its return value, don't assume a preceding isTouched()==true guarantees a point (GT911's cached-result design is the exception that makes this matter in practice).
  • getSecondPoint() defaults to false (single-touch). Only TouchDriverGT911 overrides it with a genuine second simultaneous contact - see the table below.

Point

struct Point {
  int16_t x = 0;
  int16_t y = 0;
  uint16_t pressure = 0;
};

pressure semantics vary by controller: TouchDriverXPT2046 reports an estimated Z/pressure-related value (0..4095, clamped to 12 bits) computed from Z1 + 4095 - Z2, useful for thresholding but not a calibrated physical pressure. Every capacitive controller (FT6236, CST816S, GT911) just reports 255 while touched - "touched," not a real pressure reading.

Calibration and coordinate mapping

TouchDriver::mapCoordinates() (called internally by every concrete driver's getPoint()) turns raw controller coordinates into logical display-space coordinates, in this fixed order:

raw coordinates -> inversion -> independent X/Y calibration -> swap XY -> display rotation

That order matters - in particular, swapping raw X/Y before calibration would incorrectly apply the X calibration range to Y and vice versa, which is why swap happens after.

struct CalibrationData {
  int16_t rawXMin = 300, rawXMax = 3800;   // raw controller coordinate ranges
  int16_t rawYMin = 300, rawYMax = 3800;
  uint16_t screenWidth = 240, screenHeight = 320;  // unrotated display dimensions
  bool invertX = false, invertY = false;
  bool swapXY = false;   // applied AFTER raw X/Y calibration
};

Call setCalibration() with your panel's real raw ranges and screen size - the defaults above are just placeholders and setCalibration() returns false (rejecting the call) if rawXMin >= rawXMax, rawYMin >= rawYMax, or either screen dimension is 0. Until setCalibration() has been called successfully, mapCoordinates() passes raw coordinates straight through (still applying rotation) rather than silently guessing a calibration.

setRotation(Rotation) (Deg0/Deg90/Deg180/Deg270) rotates the already-calibrated point to match the display's current orientation - keep this in sync with whatever rotation you set on the matching DisplayDriver (see DisplayDrivers.md), they are two independent settings that both need to agree for touch coordinates to line up with what's on screen.


Controller drivers (TouchDriverArduino.h)

Class Controller Bus Touch type Notes
TouchDriverXPT2046 XPT2046 SPI Resistive, single-touch Z1/Z2-based pressure threshold (setZThreshold()/getZThreshold(), default 400) gates whether a read counts as "touched"; discards the first ADC conversion per axis for stability
TouchDriverFT6236 FT6236/FT6336 I2C (addr 0x38) Capacitive, single-touch TouchDriverFT6206 is a drop-in alias (register-compatible part)
TouchDriverCST816S CST816S I2C (addr 0x15) Capacitive, single-touch
TouchDriverGT911 GT911 I2C (addr 0x5D, alt 0x14) Capacitive, up to 5 touches (2 cached) The only built-in driver where getSecondPoint() returns a real second contact - see below
TouchDriverBitBang Any 4-wire resistive panel (X-/X+/Y-/Y+ wired straight to GPIO) Bit-banged GPIO (pinMode/digitalWrite/digitalRead/analogRead) Resistive, single-touch No controller chip at all - for panels whose resistive layer pins go straight to MCU pins. Requires analogRead(); see platform note below
#include <TinyGPU/Input/TouchDriverArduino.h>
using namespace tinygpu;

TouchDriverXPT2046 touch(SPI, /*csPin=*/17, /*irqPin=*/16);
TouchDriverFT6236 touch(Wire, /*rstPin=*/-1, /*irqPin=*/-1);
TouchDriverCST816S touch(Wire, /*rstPin=*/-1, /*irqPin=*/13);
TouchDriverGT911 touch(Wire, /*rstPin=*/-1, /*irqPin=*/-1);
TouchDriverBitBang touch(/*xPlusPin=*/32, /*xMinusPin=*/33, /*yPlusPin=*/25, /*yMinusPin=*/26);

GT911's status-register quirk

GT911's status register (bit 7 = buffer ready, bits [3:0] = touch point count) must be cleared unconditionally on every isTouched() call, not only when a touch was actually found - clearing it only after a detected touch is a deadlock, since a stale, unacknowledged flag prevents the chip from ever reporting a new one. Because of that, isTouched() does the chip's real read/clear I/O and caches whatever point data was present; getPoint()/getSecondPoint() just return the cached result rather than re-querying the chip. This is the same "call isTouched() exactly once per loop iteration" contract every other driver in this file already expects, but on TouchDriverGT911 violating it (calling isTouched() more than once between getPoint() calls) will actively consume/clear a pending touch before getPoint() ever sees it, where on the other drivers it's merely redundant work.

TouchDriverBitBang, in more detail

For 4-wire resistive panels wired directly to GPIO instead of through a controller chip:

  • Touch presence (isTouched()) is a cheap digital check: Y+ configured INPUT_PULLUP, X- driven LOW. Untouched, Y+'s internal pull-up holds it high; a touch shorts the two resistive layers together, pulling Y+ low.
  • Position (getPoint()) drives one axis's plates to create a voltage gradient, floats the other axis, and analogRead()s the resulting position - the classic resistive-touchscreen measurement technique (the same one used by, e.g., Adafruit's TouchScreen library), discarding the first ADC conversion per axis for stability, matching TouchDriverXPT2046's own approach.
  • Pressure is always reported as 255 while touched (no calibrated Z measurement, unlike XPT2046) - this driver only needs to know whether the panel is touched at all.
  • Platform note: requires a real Arduino core's analogRead(). The ESP-IDF-native TinyGPU/Emulation fallback (EmulationIDF.h, used when building against plain ESP-IDF without the arduino-esp32 component) does not emulate analogRead()/ADC, so this driver won't compile against that fallback - it will against a real Arduino core (arduino-esp32, AVR, ...) on any board with the four GPIOs to spare.

Desktop / SDL (TouchDriverSDL.h)

TouchDriverSDL maps the desktop mouse to a TouchDriver for exercising touch-driven UI (buttons, sliders, gestures) on the SDL desktop backend without any physical touch hardware - pairs naturally with DisplayDriverSDL for a fully hardware-free UI test loop.

  • The left mouse button stands in for a finger: button down = touched, cursor position = touch point.
  • Like the other drivers, isTouched() doesn't implicitly loop - it's where the SDL event queue gets drained (SDL_PollEvent), so call it exactly once per loop() iteration. SDL_QUIT (closing the window) exits the process, since there's no hardware equivalent to hand back to the sketch.
#include <TinyGPU/Input/TouchDriverSDL.h>
using namespace tinygpu;

TouchDriverSDL touch;

TouchDriver.h picks TouchDriverSDL.h automatically (over TouchDriverArduino.h) whenever <SDL.h> is available and ARDUINO isn't defined - a desktop CMake build of this library's own examples gets this for free without needing to name the header explicitly.


Gesture recognition (GestureDetector.h)

GestureDetector sits on top of any TouchDriver (it doesn't replace it - feed it the same driver you'd otherwise poll directly) and turns raw touch samples into a single event stream:

#include <TinyGPU/Input/GestureDetector.h>
using namespace tinygpu;

GestureDetector gestures;

void handleGesture(GestureEvent& ev) {
  switch (ev.type) {
    case GestureType::kTap: /* ... */ break;
    case GestureType::kSwipeLeft: /* ... */ break;
    default: break;
  }
}

void setup() {
  gestures.onGesture = handleGesture;
  gestures.isDraggable = [](int16_t x, int16_t y) { return isOverDraggableWidget(x, y); };  // optional
}

void loop() {
  gestures.update(touch);  // same TouchDriver instance you'd call isTouched()/getPoint() on
}

Recognized gestures

GestureType Kind Trigger
kTap Discrete Touch released within tapMaxDurationMs (default 300ms) and tapMaxMovePx (default 12px) of where it started
kDoubleTap Discrete A kTap following a previous one within doubleTapMaxGapMs (default 350ms)
kLongPress Discrete Held past longPressMinDurationMs (default 600ms) without enough movement to start a drag
kSwipeLeft/kSwipeRight/kSwipeUp/kSwipeDown Discrete A drag/pan/scroll released within swipeMaxDurationMs (default 500ms) having covered at least swipeMinDistancePx (default 40px); direction is whichever axis moved further. Reported in addition to the drag/pan/scroll's own kEnded event, so swipe-only callers don't need to track distance/duration themselves
kDrag / kPan / kScroll Continuous (kBegan/kChanged/kEnded) Movement past dragStartThresholdPx (default 8px). Reported as kDrag if isDraggable is set and returns true for the touch's starting position; otherwise kScroll if vertical movement dominates, else kPan
kPinchIn/kPinchOut Continuous (kChanged only) Two-finger distance changing by more than 0.5px since the last sample - requires a genuine second touch point
kRotate Continuous (kChanged only) Two-finger angle changing by more than 1 degree since the last sample - requires a genuine second touch point

All tuning thresholds above are public fields on GestureDetector (tapMaxDurationMs, tapMaxMovePx, doubleTapMaxGapMs, longPressMinDurationMs, swipeMinDistancePx, swipeMaxDurationMs, dragStartThresholdPx) - adjust them to taste before your first update() call.

Pinch/rotate need a real second touch point (TouchDriver::getSecondPoint() returning true). Of the built-in controller drivers, only TouchDriverGT911 can supply one - kPinchIn/kPinchOut/kRotate will simply never fire against TouchDriverXPT2046, TouchDriverFT6236/TouchDriverFT6206, TouchDriverCST816S, TouchDriverBitBang, or TouchDriverSDL (all inherently single-touch), no matter how the panel is actually touched.

GestureEvent carries the current point, the point where the touch/gesture began, cumulative and step deltas, pinch scale/rotation, and elapsed duration - see the struct in GestureDetector.h for the exact fields.


Matching touch to a display driver

A touch panel's calibration and rotation are set independently from the paired DisplayDriver's own rotation (see DisplayDrivers.md) - nothing keeps them in sync automatically. When you call a display driver's setRotation(), call the matching TouchDriver::setRotation() with the same logical rotation too, or touch coordinates will stop lining up with what's rendered on screen. LCDBoard setups in TinyGPU/Boards/ pair a specific touch controller with a specific display driver/panel per board (e.g. TouchDriverFT6236 with the FT6336G on one board, TouchDriverGT911 with two others) as a reference for wiring both up together correctly.

Verification status

TouchDriverBitBang (the newest addition here) was build-verified with a real compile against this project's Arduino-Emulator toolchain. The pre-existing I2C/SPI controller drivers (TouchDriverXPT2046/FT6236/CST816S/GT911) and GestureDetector were reviewed against their source but not independently re-verified as part of writing this document - see each driver's own doc comments in TouchDriverArduino.h for hardware-specific caveats already captured there.