Introduction
March 22, 2026 ยท View on GitHub
This doc serves two purposes:
- Documenting specific touch drivers.
- Providing an explanation of the touch calibration process and general design information.
For setup instructions please see setup. Main README
Touch screens vary considerably in quality. Manufacturers such as Adafruit make good quality displays: a sustained touch at a fixed location produces readings with a high level of consistency. Further, they produce consistent results over the entire surface with no dead zones.Chinese units can be cheap but produce noisy outputs. Two units were tested. One was unusable: the display was fine but one axis of touch only showed variation over about a third of its length. The other unit could be calibrated but produced inaccurate readings close to one edge.
This Adafruit screen was used in development with this touch controller with good results.
Calibration
To understand calibration, note the coordinate naming convention. Values
returned by the hardware driver are referred to as x and y, while pixel
values are row and col. The xy values have nominal 12-bit resolution but
owing to hardware tolerances typically have a range less than the nominal. The
range of row/col values is precisely that of the display size in pixels.
Calibration has the following objectives:
- If a display has NxP pixels, ensuring that the larger number is associated with the long axis of the touch panel.
- Allowing for the fact that the physical dimensions of the touch overlay may exceed those of the pixel array. In this case, as points on the pixel array are touched, the range of values from the touch controller is smaller than its theoretical 0-4095 bits. Calibration enables the touch driver base class to correct for this.
- Mapping
(x,y)touch coordinates onto(row,col)screen coordinates. This must allow for landscape/portrait or upside down orientation. For example, if a display has 240x320 pixels and is mounted in portrait mode, a touch near the top left corner will issue something close torow=0, col=0. A touch near the bottom right will issue something close torow=319, col=239.
The output of the calibration process is a line of code defining values for the
touchpad driver's init method. This is documented below.
Determining the long axis is possible because the touch constructor takes as
an arg the initialised display driver instance: the code can deduce the long
axis from ssd.height and ssd.width (the pixel dimensions). Orientation can
be deduced because touch.setup prompts the user to touch points in a specific
order.
TSC2007
Constructor. This takes the following positional args:
i2cAn initialised I2C bus. Baudrate should be 100_000.ssdInitialised display driver instance.addr=0x48I2C address of device.
Optional keyword-only arg:
alen:int=10This determines the post-processing done on touch samples. When a touch occurs a set of N samples is acquired and the mean is taken as the touch location.alendetermines the size of the set.
Method: init. This is a method of the base class and its args are described
below in "the init method". In practice the user runs the calibration script
touch.setup.py which outputs a line of code such as
tpad.init(240, 320, 241, 292, 3866, 3887, True, True, False)
This is pasted into touch_setup.py.
XPT2046
Constructor. This takes the following mandatory positional args:
spiAn initialised SPI bus. Baudrate 2.5MHz max.cspinAn initialisedPininstance connected to the device chip select.ssdInitialised display driver instance.
Optional keyword-only arg:
alen:int=10This determines the post-processing done on touch samples. When a touch occurs a set of N samples is acquired and the mean is taken as the touch location.alendetermines the size of the set.
Method: init. This is a method of the base class and its args are described
below in "the init method". In practice the user runs the calibration script
touch.setup.py which outputs a line of code such as
tpad.init(240, 320, 241, 292, 3866, 3887, True, True, False)
This is pasted into touch_setup.py.
FT6206 Capacitive controller
FT6206 constructor mandatory positional args:
i2cAn initialised I2C bus. Baudrate should be 400_000 max.ssdInitialised display driver instance.
Optional args:
addr=0x38I2C address of device.thresh=128Touch detection threshold.
The example tested was Adafruit 2.8" touch shield.
The FT6206 produced pre-calibrated row and column values which did not need
calibration or pre-processing. Calibration should still be performed to enable
screen orientation to be detected. The .init method ignores the last four
numeric args as scaling is not required.
See setup_examples/ili9341_ft6206_pico.py.
CST328 Capacitive controller
CST328 class.
Constructor mandatory positional args:
i2cAn initialised I2C bus. Baudrate should be 400_000 max.rstAPininstance initialised withPin.OUT, value=1.pintAPininstance initialised withPin.IN.ssdInitialised display driver instance.
Optional arg:
addr=0x1AI2C address of device.
See setup_examples/st7789_cst328_ws_esp32_2_8.py for a touch_setup.py
example.
Tested with Waveshare ESP32-S3 2.8 inch touch LCD
CST816S Capacitive controller
CST816S class.
Constructor mandatory positional args:
i2cAn initialised I2C bus. Baudrate should be 400_000 max.rstAPininstance initialised withPin.OUT, value=1.pintAPininstance initialised withPin.IN.ssdInitialised display driver instance.
Optional arg:
addr=0x15I2C address of device.
Bound variable:
versionReturns the chip version information. See technical note below and code comments.
See setup_examples/gc9a01_ws_rp2040_touch.py for a touch_setup.py
example. Note that touch.setup.py is unusable with circular
displays because the crosses lie outside the visible area. However the
controller is pre-calibrated and the following initialisation should be used:
tpad.init(240, 240, 0, 0, 240, 240, False, True, True)
The three boolean args are described below and may be changed to match the
orientation of the screen (to validate run touch.check).
Tested with Waveshare 1.28 inch touch LCD and Waveshare RP2040 touch LCD 1.28.
Technical note
The chip has the curious property of being undetectable by I2C.scan. It only
becomes present on the I2C bus for a "brief period" after it issues an
interrupt. It is also poorly documented, with the above "brief period" being
unknown. This ha the weird consequence that it is impossible to check the chip's
presence and version details until after it has detected a touch and raised an
interrupt. If anyone can shed any light on this, please raise an issue.
CST820 Capacitive controller
This is used in the capacitive version of the 2.4" CYD (Cheap Yellow Display).
CST820 class.
Constructor mandatory positional args:
i2cAn initialised I2C bus. Baudrate should be 400_000 max.rstAPininstance initialised withPin.OUT, value=1.ssdInitialised display driver instance.
Optional arg:
addr=0x15I2C address of device.
Method:
versionReturns the chip version number. Should be 183 (0xB7).
See setup_examples/CYD_ESP32_2432S024C.py for a touch_setup.py example.
Under the hood
The following provides details for those wishing to adapt the code or to contribute new touch drivers.
Design
Touchscreen hardware comes in various forms requiring different drivers. All
drivers are subclassed from ABCTouch defined in touch.touch.py. This
abstract base class performs coordinate scaling to handle calibration values,
also reflection and rotation for landscape/portrait or USD configuration. It
also does averaging to reduce the noise present in touch measurements. This
enables hardware specific subclasses to be extremely minimal, simplifying the
development of further drivers. Currently three drivers are provided:
- TSC2007 e.g. Adafruit
- XPT2046 Used on many Chinese resistive touchscreens.
- FT6206 e.g. Adafruit.
Mapping
The x and y values have 12 bit resolution but due to hardware tolerances
typically span less than their nominal range. In general minimum values are
greater than 0 and the maximum is less than 4095. The ABC compensates for these
limitations and performs mapping from (x, y) to (row, col) in accordance
with the display orientation (e.g. landscape/portrait, usd, etc.).
API: The init method
This base class method takes the following mandatory positional arguments:
xpix: intNumber of pixels associated withxcoordinate.ypix: intNumber of pixels associated withycoordinate.xmin: intMinimum value ofx. These four values are for scaling.ymin: intMinimum value ofy.xmax: intMaximum value ofx.ymax: intMaximum value ofy.trans:boolTranspose axes.rr:boolReflect rows.rc:boolReflect columns.
The calibration procedure provides all these args.
poll
This base class method is periodically called by the GUI. It takes no args and
returns True if the screen is touched. In this case the touch coordinates in
pixels may be retrieved from .row and .col bound variables.
Signals from touch overlays are noisy. A PreProcess object aims to reduce
noise. The poll method calls the preprocessor's get method. If this returns
True, smoothed touch coordinates may be accessed in the ._x and ._y bound
variables. If the get method returns False, poll should return False and
the GUI will ignore the touch.
Internally the pre-processor calls the acquire method of the subclass. It may
do this multiple times before returning a result. The get and acquire
methods may raise an OSError: this is trapped by the GUI and the touch is
rejected.
If get returns True the poll method converts the raw xy coordinates to
pixel values, updating .row and .col. In this case poll returns True and
the GUI accepts the touch.
acquire
This method of the superclass is the hardware interface. It returns a bool
indicating if a touch was in progress when called. If so, the base class
._x and ._y bound variables are updated with the raw values from the
hardware. The method can also throw an OSError if the hardware produces an
invalid response. This is trapped by the GUI and the touch is ignored.
The preprocessor
The use of a separate preprocessor object allows for the possibility of using a different algorithm with an individual hardware driver. The preprocessor is instantiated in the hardware driver's constructor, and takes args provided to the driver's constructor.
The currently implemented PreProcess class (in touch.py) works as follows.
Constructor args:
tpadTheTouchinstance.alen:intArray length: number of samples to acquire.
When the get method is called, arrays .ax and .ay are populated by
repeated calls to the superclass acquire. The mean values of x and y are
calculated.
If the touch ends before a full sample set is acquired, get returns False
and no touch is recorded. Otherwise get returns True and the ABC bound
variables ._x and ._y are updated to contain the mean values of the raw
touch coordinates.
There is also a NoPreProcess class for pre-calibrated displays. This simply
passes get calls to acquire.