Home » Projects » Electronics Projects » Arduino Touchscreen Display – Coding the XPT2046

Arduino Touchscreen Display – Coding the XPT2046

Coding the XPT2046

Here is the source code to run a TJCTM24024 module Arduino Touchscreen Display, and in particular, coding the XPT2046 resistive touch pad.

In a previous article, I described discovering how to use a “mystery module from China”. The steps were to find the datasheet and schematic; learn what driver chips were being used; and make sure that Arduino or other libraries were available.

In my project, I have used an available Arduino display library to run the ILI9341 TFT driver. This chip is supported by most Arduino libraries. The biggest challenge is that these libraries can consume tons of scarce Arduino flash memory. With my Arduino Pro Mini 3.3V I am limited by 32K. I wanted to make sure to use only 30-40% of this memory for the display and touch screen. This way I would have space left to program the Touchscreen display to actually do something, like control my signal generator. After experimenting with several Arduino touchscreen display libraries, I chose the PDQ version of the Adafruit GFX library. It seemed to have the best display speed and the smallest memory footprint.

Coding the XPT2046 driver required some hand crafting. I was not happy with the performance or clarity of the several existing libraries, and decided to roll my own.

Coding the XPT2046

The C++ class I wrote in coding the XPT2046 software driver is described below. It was very useful to be able to access the data sheets and application notes for this chip as well as its sister AD7843 chip. In coding the XPT2046 I also learned a lot about how the SPI bus works.

// XPT2046A.h
// 
/* XPT2046 / ADS7843 TouchPad Controller Driver
   Adapted from https://github.com/spapadim/XPT2046 and https://github.com/PaulStoffregen/XPT2046_Touchscreen
   See http://www.ti.com/lit/an/sbaa036/sbaa036.pdf "Touch Screen Controller Tips"
   for information on algorithms. See data sheets for information on SPI timing:
   - https://ldm-systems.ru/f/doc/catalog/HY-TFT-2,8/XPT2046.pdf
   - http://www.ti.com/lit/ds/symlink/ads7843.pdf
*/
#ifndef _XPT2046A_h
#define _XPT2046A_h

#include <arduino.h>

typedef struct Point {
	long	x, y;
};

typedef struct cal_params {
	long An, Bn, Cn, Dn, En, Fn, V;
};

class XPT2046A {
public:

	enum rotation_state : byte { ROT0, ROT90, ROT180, ROT270 };

	XPT2046A(byte pin_CS, byte pin_PenIRQ);
	void init(uint16_t tft_width, uint16_t tft_height);
	/* Touching is discovered by polling. It could be implemented as
	   a hardware interrupt if desired. */
	boolean isTouching() const { return (digitalRead(_irq_pin) == LOW); }
	void getADCData(Point &adc_data);
	void getLCDPosition(Point & pos_data);
	void setCalibration(cal_params cal) { _cal = cal; }
	void setRotation(rotation_state rot) { _rot = rot; }

private:
	static const uint16_t ADC_MAX = 0x0fff;  

	byte _cs_pin, _irq_pin;
	uint16_t _width, _height;
	rotation_state _rot;
	/* Default calibration parameters were previously determined with XPT2046A-calib */
	cal_params _cal = { -322560L ,2016L, 94892544L ,-10752L ,-418992L ,67339008L , -4789248L };

	void enableSPI(boolean state);
	uint16_t readADC(byte ctrl) const;
};
#endif

 

Here is the implementation for this class. Keep in mind, the TFT display and the resistive touchscreen are two entirely different things. When you read the touch pen position, you are reading the output from an analog-to-digital convertor hooked up to resistors in the touchscreen which lines on top of the TFT display. When coding the XPT2046, I learned that this 12 bit ADC outputs values between 0-4095 for each of the x and y axis. In turn, these need to be converted to values aligned with the TFT which has a width of 240 and a height of 320 pixels. This requires calibration and conversion. When coding the XPT2046 you also need to consider screen rotation and adjust for that as well.

// 
// XPT2046 Touch Controller
// 
#include <Arduino.h>
#include <SPI.h>
#include "XPT2046A.h"

#define REQ_Z1	0xB1
#define REQ_Z2	0xC1
#define REQ_X	0x91
#define REQ_Y	0xD1
#define REQ_Y_POWERDN 0xD0
#define MAX_READS 0xFF

inline static void swap(long &a, long &b) {
	long tmp = a;
	a = b;
	b = tmp;
}

XPT2046A::XPT2046A(byte pin_CS, byte pin_PenIRQ)
: _cs_pin(pin_CS), _irq_pin(pin_PenIRQ) {
}

void XPT2046A::init(uint16_t tft_width, uint16_t tft_height)
{
	pinMode(_cs_pin, OUTPUT);
	pinMode(_irq_pin, INPUT_PULLUP);
	_width = tft_width;
	_height = tft_height;
	
	SPI.begin();
	delay(10);
	enableSPI(true);
	delay(3);
	SPI.transfer(REQ_Y);
	delay(3);
	SPI.transfer16(0);
	delay(3);
	enableSPI(false);
}

void XPT2046A::getADCData(Point &adc_data)
{
	enableSPI(true);

	int16_t x, y;
	/* Pressure reading not implemented. */
	/* REMEMBER that the SPI reads the data from the PREVIOUS SPI Request */
	SPI.transfer(REQ_X);
	x = readADC(REQ_X);
	SPI.transfer16(REQ_Y);
	y = readADC(REQ_Y);
	SPI.transfer16(REQ_Y_POWERDN);
	SPI.transfer16(0);
	enableSPI(false);
	/* Normalization for different orientation and direction of touchpad. This may be
	   different for your module. The purpose of the normalization is to achieve the same
	   origin points (0,0) for the TFT and Touchpad. */
	adc_data.y = x;
	adc_data.x = 4096 - y;
}

void XPT2046A::getLCDPosition(Point & pos_data)
{
	if (!isTouching()) {
		pos_data.x = pos_data.y = 0xffff;
		return;
	}
	Point adc;
	getADCData(adc);

	pos_data.x = ((_cal.An * adc.x) + (_cal.Bn * adc.y) + _cal.Cn) / _cal.V;

	pos_data.y = ((_cal.Dn * adc.x) + (_cal.En * adc.y) + _cal.Fn) / _cal.V;

	// adjust for rotation
	switch (_rot)
	{
	case XPT2046A::ROT90:
		pos_data.x = _width - pos_data.x;
		swap(pos_data.x, pos_data.y);
		break;
	case XPT2046A::ROT180:
		pos_data.x = _width - pos_data.x;
		pos_data.y = _height - pos_data.y;
		break;
	case XPT2046A::ROT270:
		pos_data.y = _height - pos_data.y;
		swap(pos_data.x, pos_data.y);
		break;
	case XPT2046A::ROT0:
	default:
		break;
	}
}

void XPT2046A::enableSPI(boolean state)
{
	if (state) {
		// use SPI.beginTransaction here if compatible with TFT library
		digitalWrite(_cs_pin, LOW);
	}
	else {
		digitalWrite(_cs_pin, HIGH);
		// SPI.endTransaction here if compatible
	}
}

uint16_t XPT2046A::readADC(byte ctrl) const 
{
	uint16_t prev = 0xffff, cur = 0xffff;
	byte i = 0;
	do
	{
		prev = cur;
		/* Uses 16 clocks per transfer - see data sheets. 
		   This loop keeps repeating until data settles 
		   prev - cur, or otherwise fails.
		*/
		cur = SPI.transfer16(ctrl) >> 3;
	} while ((prev != cur) && (++i < MAX_READS));

	return cur;
}

 

In order to get the touchscreen and the TFT display to play nicely together, you need calibration. I chose the popular 3-point calibration method. In coding the XPT2046 (or any other touch-TFT combination) you find the touchscreen x-y points that align with the display x-y points at three specific (unrelated) locations spread across the display. Finally, you calculate the calibration parameters using an algorithm which is implemented below.

// XPT2046A_calib.h
/*	Three Point Calibration developed from
		A http://www.ti.com/lit/an/slyt277/slyt277.pdf
		B http://www.embedded.com/design/system-integration/4023968/How-To-Calibrate-Touch-Screens
	Relies on struct definitions in XPT2046A.h but these could be created locally if needed
	Calculations use long integers. This works without overflow with 240x320 TFT but 64 bit
	might be needed if screen is larger as noted in "B". Could also be implemented with floats.
	Algorithm is distributed using individual parameter functions to avoid register spill
	issues during calculations.
*/

#ifndef _XPT2046A_CALIB_h
#define _XPT2046A_CALIB_h

#if defined(ARDUINO) && ARDUINO >= 100
	#include "arduino.h"
#else
	#include "WProgram.h"
#endif

#include "XPT2046A.h"

bool MakeCalibrationParams(Point *pDisplayPoints, Point *pTouchPoints, cal_params &cal);
long _makeAn(Point *d, Point *t);
long _makeBn(Point *d, Point *t);
long _makeCn(Point *d, Point *t);
long _makeDn(Point *d, Point *t);
long _makeEn(Point *d, Point *t);
long _makeFn(Point *d, Point *t);

/* "A" "Calibration in touch-screen systems" TI Analog Applications Journal 3Q 2007 */
/* "B" is copyright (c) 2001 Carlos E. Vidales. Modified January 2016. */
#endif

// XPT2046A_calib.cpp
#include "XPT2046A_calib.h"

bool MakeCalibrationParams(Point * pDisplayPoints, Point * pTouchPoints, cal_params & cal)
{
	/* Touchpoints = Touchpad; ScreenPoints = TFT */

	cal.V = ((pTouchPoints[0].x - pTouchPoints[2].x) * (pTouchPoints[1].y - pTouchPoints[2].y)) -
		((pTouchPoints[1].x - pTouchPoints[2].x) * (pTouchPoints[0].y - pTouchPoints[2].y));


	if (cal.V == 0) return false;
	cal.An = _makeAn(pDisplayPoints, pTouchPoints);
	cal.Bn = _makeBn(pDisplayPoints, pTouchPoints);
	cal.Cn = _makeCn(pDisplayPoints, pTouchPoints);
	cal.Dn = _makeDn(pDisplayPoints, pTouchPoints);
	cal.En = _makeEn(pDisplayPoints, pTouchPoints);
	cal.Fn = _makeFn(pDisplayPoints, pTouchPoints);
	return true;
}

long _makeAn(Point * d, Point * t)
{
	return ((d[0].x - d[2].x) * (t[1].y - t[2].y)) -
		((d[1].x - d[2].x) * (t[0].y - t[2].y));
}

long _makeBn(Point * d, Point * t)
{
	return ((t[0].x - t[2].x) * (d[1].x - d[2].x)) -
		((d[0].x - d[2].x) * (t[1].x - t[2].x));
}

long _makeCn(Point * d, Point * t)
{
	return (t[2].x * d[1].x - t[1].x * d[2].x) * t[0].y +
		(t[0].x * d[2].x - t[2].x * d[0].x) * t[1].y +
		(t[1].x * d[0].x - t[0].x * d[1].x) * t[2].y;
}

long _makeDn(Point * d, Point * t)
{
	return ((d[0].y - d[2].y) * (t[1].y - t[2].y)) -
		((d[1].y - d[2].y) * (t[0].y - t[2].y));
}

long _makeEn(Point * d, Point * t)
{
	return ((t[0].x - t[2].x) * (d[1].y - d[2].y)) -
		((d[0].y - d[2].y) * (t[1].x - t[2].x));
}

long _makeFn(Point * d, Point * t)
{
	return (t[2].x * d[1].y - t[1].x * d[2].y) * t[0].y +
		(t[0].x * d[2].y - t[2].x * d[0].y) * t[1].y +
		(t[1].x * d[0].y - t[0].x * d[1].y) * t[2].y;
}

 

I will provide an Arduino test program which uses this XPT2046 and calibration in a later article.

Leave a Reply