Getting an Arduino and touch screen to communicate reliably over SPI is a rite of passage for embedded makers. The most common and cost-effective combination is the Arduino Uno R3 paired with a 2.8-inch ILI9341 TFT display featuring an XPT2046 resistive touch controller. While the hardware is cheap (usually under $20 for the display), the shared SPI bus and 5V-to-3.3V logic mismatches cause 90% of the failures beginners face on the bench.
This guide provides the exact pin mapping, fully compilable calibration code targeting the Arduino Uno R3 (ATmega328P), and a debugging decision tree to resolve the most common SPI and touch controller errors.
Hardware Spec Sheet & Parts List
Before wiring, verify your specific module variant. Many cheap ILI9341 boards lack 5V tolerance on the logic pins, which will slowly degrade the XPT2046 touch controller when driven directly by an Uno's 5V GPIOs. The parts below assume a 5V-tolerant shield or module with built-in level shifters.
| Component | Exact Variant / Model | Est. Price (2026) | Critical Notes |
|---|---|---|---|
| Microcontroller | Arduino Uno R3 (ATmega328P) | $25.00 | 5V logic, 14 digital I/O, hardware SPI on pins 11-13. |
| Display + Touch | HiLetgo 2.8" ILI9341 SPI TFT (with XPT2046) | $16.50 | Must have 'SDO/MISO' broken out. Verify it has a 3.3V LDO regulator. |
| Level Shifter (If needed) | SparkFun Logic Level Converter (BOB-12009) | $3.95 | Required if your ILI9341 module lacks built-in 5V tolerance. |
| Wiring | 24 AWG Solid Core Hookup Wire | $8.00 | Keep SPI traces under 4 inches to prevent clock signal degradation. |
SPI Pin Mapping for Arduino Uno R3
The ILI9341 display and the XPT2046 touch controller share the same hardware SPI bus (MOSI, MISO, SCK). They are separated by unique Chip Select (CS) pins. Never connect the display CS and touch CS to the same Arduino pin.
| ILI9341 / XPT2046 Pin | Arduino Uno R3 Pin | Function |
|---|---|---|
| VCC | 5V | Power (Module's onboard LDO drops to 3.3V) |
| GND | GND | Common Ground |
| CS (Display) | D10 | Display Chip Select |
| CS (Touch) | D8 | Touch Controller Chip Select |
| RESET | D9 | Display Hardware Reset |
| DC / RS | D7 | Data / Command Selector |
| SDI / MOSI | D11 | Hardware SPI Master Out Slave In |
| SDO / MISO | D12 | Hardware SPI Master In Slave Out |
| SCK | D13 | Hardware SPI Clock |
| IRQ (Touch) | D3 | Touch Interrupt (Active LOW) |
Complete Calibration and Touch Code
This code targets the Arduino Uno R3. It requires the Adafruit_GFX, Adafruit_ILI9341, and XPT2046_Touchscreen libraries (install via Arduino Library Manager). The code includes a basic mapping function to translate raw touch ADC values into screen coordinates, and handles the SPI bus sharing gracefully.
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <XPT2046_Touchscreen.h>
// --- PIN DEFINITIONS (Arduino Uno R3) ---
#define TFT_CS 10
#define TFT_DC 7
#define TFT_RST 9
#define TOUCH_CS 8
#define TOUCH_IRQ 3
// --- CALIBRATION VALUES (Adjust based on your specific screen) ---
#define TS_MINX 300
#define TS_MAXX 3800
#define TS_MINY 300
#define TS_MAXY 3800
// Initialize Display and Touch objects
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
XPT2046_Touchscreen ts(TOUCH_CS, TOUCH_IRQ);
void setup() {
Serial.begin(115200);
// Initialize Display
tft.begin();
tft.setRotation(1); // Landscape mode (320x240)
tft.fillScreen(ILI9341_BLACK);
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(2);
tft.setCursor(10, 10);
tft.println("Touch Screen Ready");
// Initialize Touch Controller
ts.begin();
ts.setRotation(1);
// Error handling: Check if touch controller responds
if (!ts.touched() && digitalRead(TOUCH_IRQ) == HIGH) {
Serial.println("Warning: IRQ is HIGH. Touch may not be calibrated.");
}
}
void loop() {
// Only process if the screen is actually being touched
if (ts.touched()) {
TS_Point p = ts.getPoint();
// Map raw touch data to screen coordinates
int x = map(p.x, TS_MINX, TS_MAXX, 0, tft.width());
int y = map(p.y, TS_MINY, TS_MAXY, 0, tft.height());
// Constrain values to prevent drawing off-screen
x = constrain(x, 0, tft.width() - 1);
y = constrain(y, 0, tft.height() - 1);
// Draw a pixel at the touch location
tft.drawPixel(x, y, ILI9341_RED);
// Print coordinates to Serial Monitor for debugging
Serial.print("X: "); Serial.print(x);
Serial.print(" Y: "); Serial.print(y);
Serial.print(" Pressure: "); Serial.println(p.z);
// Small delay to prevent SPI bus flooding
delay(15);
}
}
Debugging: The First Three Things to Check When It Fails
When your Arduino and touch screen setup fails, do not immediately rewrite your code. Hardware and SPI configuration issues are almost always the culprit. Follow this ranked troubleshooting path.
1. Compilation Error: Missing Library Headers
Exact Error String: fatal error: XPT2046_Touchscreen.h: No such file or directory
Cause: You are using the generic Adafruit_TouchScreen library (meant for 4-wire analog resistive screens) instead of the SPI-based XPT2046 library.
Fix: Open the Arduino IDE Library Manager, search for XPT2046_Touchscreen by Paul Stoffregen, and install it. Remove any #include <TouchScreen.h> lines from your code. You can verify the correct repository at the PaulStoffregen XPT2046 GitHub.
2. Display Works, But Touch Reads 0,0 or 4095,4095
Symptom: The TFT draws graphics perfectly, but the Serial Monitor prints X: 0 Y: 0 or maxed-out values regardless of where you press.
Cause: SPI Chip Select (CS) conflict or fried touch controller logic.
Fix: First, verify that TFT_CS (Pin 10) and TOUCH_CS (Pin 8) are strictly separated in your code. If they are correct, use a multimeter to measure the voltage on the XPT2046 VCC pin. If it reads 5V instead of 3.3V, your module lacks an LDO, and you have likely destroyed the touch IC by feeding 5V logic into a 3.3V chip. You must replace the module and add a logic level shifter on the SPI lines.
3. Screen Flickers or Touch Freezes the Display
Symptom: The display updates, but when you touch the screen, the UI freezes, tears, or flickers violently.
Cause: SPI bus contention. The touch controller and display are trying to drive the MISO line simultaneously.
Fix: Ensure your touch CS pin is set HIGH when not reading touch data. The XPT2046_Touchscreen library handles this automatically, but if you are manually toggling SPI, you must release the bus. Additionally, lower the SPI clock speed for the touch controller if your wires are longer than 4 inches. Add SPI.setClockDivider(SPI_CLOCK_DIV4); in your setup block.
How to Extend or Simplify the Build
Depending on your project timeline, you can either strip this build down to its bare essentials or scale it up for commercial-grade UIs.
To Simplify (The 'No-Wiring' Route):
Ditch the jumper wires and buy a TFT Touch Shield (like the Adafruit 2.8" TFT Touch Shield v2). These plug directly into the Uno's female headers, route the SPI pins internally, and include onboard level shifting and microSD card sockets. It costs about $10 more but eliminates 100% of wiring errors.
To Extend (The 'Pro UI' Route):
Raw pixel drawing is fine for testing, but for real appliances, integrate LVGL (Light and Versatile Graphics Library). LVGL provides buttons, sliders, and charts. Because LVGL is resource-heavy, extending to this level requires upgrading from the Uno R3 to an ESP32-S3 with PSRAM, as the ATmega328P's 2KB SRAM cannot buffer LVGL's draw calls. Alternatively, implement Adafruit's GFX Button class to create clickable UI zones without leaving the Uno ecosystem.
Arduino and Touch Screen FAQ
Why is my Arduino and touch screen sharing SPI pins causing display flicker?
Hardware SPI (pins 11, 12, 13 on the Uno) is a shared bus. Display flicker occurs when the touch controller's Chip Select (CS) pin is left LOW while the display is trying to write data, causing data collisions on the MISO/MOSI lines. Ensure the XPT2046 library is managing the CS pin state, and keep SPI jumper wires under 4 inches to prevent clock-signal ringing, which the touch controller often misinterprets as valid data.
Can I use an Arduino Nano instead of an Uno for this touch screen setup?
Yes, the Arduino Nano (ATmega328P variant) shares the exact same pinout and architecture as the Uno R3 for this purpose. The hardware SPI pins on the Nano are D11 (MOSI), D12 (MISO), and D13 (SCK). However, the Nano's 5V pin on older clones often struggles to supply the 150mA+ required by the ILI9341 backlight and the XPT2046 simultaneously. Power the display's VCC from an external 5V buck converter rather than the Nano's onboard regulator to prevent brownouts.
How do I fix inverted X and Y axes on my Arduino touch screen?
Inverted axes happen when the physical orientation of the touch panel's resistive layers doesn't match the display's scan direction. First, try changing ts.setRotation(1); to 0, 2, or 3 in your code. If the axes are mirrored (e.g., pressing left registers as right), invert the mapping math: change map(p.x, TS_MINX, TS_MAXX, 0, tft.width()) to map(p.x, TS_MAXX, TS_MINX, 0, tft.width()). Swapping the MIN and MAX arguments in the map function flips the axis logically without requiring hardware changes.






