Getting a touch screen display for Raspberry Pi working over SPI requires more than just plugging in a ribbon cable. While the display controller (like the ILI9486) handles the pixels, the touch controller (usually an XPT2046 resistive chip) operates as a completely separate SPI device sharing the same bus. To get both talking to your Python script without colliding, you must map the touch controller to SPI Chip Enable 1 (CE1), cap the SPI clock speed at 2MHz, and apply an affine transformation matrix to correct the raw 12-bit ADC coordinates.

This guide walks through the exact hardware setup, pin mapping, and Python implementation for the Waveshare 3.5-inch SPI Touch LCD, targeting the Raspberry Pi 4 Model B running Raspberry Pi OS (Bookworm, 64-bit).

Spec Sheet & Parts List

Difficulty: Intermediate | Time: 45 minutes | Cost: ~$109
ComponentExact Model / VariantEst. Price
MicrocontrollerRaspberry Pi 4 Model B (4GB RAM)$55.00
Touch DisplayWaveshare 3.5inch RPi Touch LCD (B) - ILI9486 + XPT2046$32.00
StorageSanDisk Extreme 32GB microSDHC (A2, V30)$12.00
Power SupplyOfficial Raspberry Pi 27W USB-C PD Power Supply$10.00

Note: If you are using a Raspberry Pi 5, the GPIO pinout for SPI remains identical, but the configuration file path has moved from /boot/config.txt to /boot/firmware/config.txt.

Pin Mapping & Wiring the SPI Touch Controller

The Waveshare 3.5-inch LCD (B) uses a 40-pin header that routes both the display and touch signals. The critical detail most tutorials miss is that the display uses CE0 (GPIO 8) while the XPT2046 touch controller uses CE1 (GPIO 7). They share SCLK, MOSI, and MISO.

XPT2046 Touch PinRaspberry Pi GPIO (BCM)Physical PinFunction
T_CLKGPIO 11 (SCLK)23SPI Clock (Shared)
T_DINGPIO 10 (MOSI)19Master Out Slave In (Shared)
T_DOGPIO 9 (MISO)21Master In Slave Out (Shared)
T_CSGPIO 7 (CE1)26Touch Chip Select (Dedicated)
T_IRQGPIO 1711Interrupt Request (Active Low)
Pro-Tip: Never wire the T_IRQ pin to a 5V-tolerant GPIO if you are using a level shifter. The XPT2046 operates at 3.3V logic. Connect T_IRQ directly to BCM 17.

Wiring Steps

  1. Power down the Raspberry Pi and disconnect the USB-C cable.
  2. Align the 40-pin female header on the Waveshare display with the male GPIO header on the Pi. Ensure Pin 1 (3.3V) aligns correctly.
  3. Press the display shield down evenly until it seats fully on the standoffs.
  4. Boot the Pi and open a terminal. Run sudo raspi-config, navigate to Interface Options > SPI, and enable it.
  5. Reboot the Pi. Verify the SPI devices exist by running ls -l /dev/spidev*. You should see spidev0.0 (display) and spidev0.1 (touch).

Complete Python Touch Calibration Code

This script targets Raspberry Pi OS Bookworm (64-bit). It uses spidev to read the XPT2046 ADC directly and gpiozero to monitor the interrupt pin. Install dependencies first: sudo apt install python3-spidev python3-gpiozero.

import spidev
import time
from gpiozero import Button

# --- PIN DEFINITIONS ---
TOUCH_SPI_BUS = 0
TOUCH_SPI_DEVICE = 1  # CE1 is device 1
TOUCH_IRQ_PIN = 17    # BCM 17

# XPT2046 Control Bytes (8-bit mode, 12-bit resolution)
CMD_X = 0x90  # Read X channel
CMD_Y = 0xD0  # Read Y channel

def init_spi():
    """Initialize SPI bus with strict speed limits for XPT2046."""
    spi = spidev.SpiDev()
    try:
        spi.open(TOUCH_SPI_BUS, TOUCH_SPI_DEVICE)
        # XPT2046 max stable clock is ~2MHz. Higher causes invalid args or garbage data.
        spi.max_speed_hz = 2000000 
        spi.mode = 0b00
    except FileNotFoundError as e:
        print(f'FATAL: {e}. Is SPI enabled in raspi-config?')
        exit(1)
    except OSError as e:
        print(f'FATAL: {e}. Check SPI speed (must be <= 2MHz) or user permissions.')
        exit(1)
    return spi

def read_channel(spi, channel):
    """Read 12-bit ADC value from XPT2046."""
    # Send command byte, then two dummy bytes to clock out the 12-bit response
    resp = spi.xfer2([channel, 0x00, 0x00])
    # Response is in resp[1] (lower 8 bits) and resp[2] (upper 4 bits)
    raw = ((resp[1] << 8) | resp[2]) >> 3
    return raw

def main():
    spi = init_spi()
    irq_pin = Button(TOUCH_IRQ_PIN, pull_up=True, bounce_time=0.01)
    
    print('Touch screen initialized. Touch the panel...')
    
    try:
        while True:
            # XPT2046 pulls IRQ low when touched
            if not irq_pin.is_pressed:
                # Read multiple times and average to reduce noise
                x_vals = [read_channel(spi, CMD_X) for _ in range(3)]
                y_vals = [read_channel(spi, CMD_Y) for _ in range(3)]
                
                x_raw = sum(x_vals) // len(x_vals)
                y_raw = sum(y_vals) // len(y_vals)
                
                # Basic affine mapping (requires calibration for your specific screen)
                # Example: Map 0-4095 raw ADC to 480x320 pixels
                x_pixel = int((x_raw / 4095) * 480)
                y_pixel = int((y_raw / 4095) * 320)
                
                print(f'Touch detected! Raw: ({x_raw}, {y_raw}) -> Pixel: ({x_pixel}, {y_pixel})')
                
                # Debounce delay
                time.sleep(0.05)
            else:
                time.sleep(0.01)
                
    except KeyboardInterrupt:
        print('\nExiting...')
    finally:
        spi.close()

if __name__ == '__main__':
    main()

Debugging: First Three Things to Check When It Fails

When integrating an SPI touch screen display for Raspberry Pi, things rarely work on the first compile. Here are the exact error strings you will encounter and how to fix them.

1. Error: FileNotFoundError: [Errno 2] No such file or directory: '/dev/spidev0.1'

Cause: The SPI bus is disabled, or the device tree overlay for the touch controller is missing.

Fix: Run sudo raspi-config and enable SPI. If you are using a custom overlay in /boot/firmware/config.txt (like dtoverlay=ads7846), ensure it is spelled correctly. A typo in the overlay name will prevent the kernel from creating spidev0.1.

2. Error: OSError: [Errno 22] Invalid argument during spi.xfer2()

Cause: The SPI clock speed is set too high. The XPT2046 datasheet claims support for higher speeds, but in practice, the parasitic capacitance of the flexible printed circuit (FPC) and the Pi's GPIO routing causes signal degradation above 2MHz.

Fix: In your Python code, explicitly set spi.max_speed_hz = 2000000. Do not leave it at the default (which can be 10MHz+).

3. Symptom: Touch coordinates are inverted or stuck in one corner

Cause: The X and Y axes on resistive touch panels are physically arbitrary relative to the display orientation. Furthermore, the raw 12-bit ADC values (0-4095) do not map 1:1 to the screen edges due to bezel dead-zones.

Fix: You must implement a 3-point calibration matrix. Use the evtest utility (sudo apt install evtest) to read the raw kernel input events, then calculate the transformation matrix using the xinput_calibrator tool if running X11, or apply the matrix math manually in Python for headless/Pygame setups.

Extending and Simplifying the Build

How to Simplify: If you do not strictly need a 3.5-inch form factor and want to eliminate SPI debugging entirely, switch to the Official Raspberry Pi 7-inch Touchscreen Display. It uses the DSI (Display Serial Interface) port for video and a dedicated I2C bus for the FT5406 capacitive touch controller. The kernel handles both automatically; no Python SPI polling or manual calibration is required. It is strictly plug-and-play on Pi 4 and Pi 5.

How to Extend: To build a complex, multi-screen kiosk, upgrade the software stack. Replace raw spidev polling with the Kivy framework. Kivy includes built-in touch input providers that can read directly from /dev/input/eventX (if you configure the ads7846 kernel overlay to handle the SPI polling in C instead of Python). This offloads the interrupt handling to the kernel, dropping your Python CPU usage from ~15% to near zero and enabling multi-touch gestures if you swap the XPT2046 for an I2C capacitive controller like the FT6236.

Frequently Asked Questions

What is the best touch screen display for Raspberry Pi 5?

For the Raspberry Pi 5, the best overall display is the Raspberry Pi Touch Display 2 (7-inch) or the newer 11.9-inch variant. The Pi 5 features significantly higher thermal output and a redesigned board layout that makes stacking rigid SPI shields (like the Waveshare 3.5-inch) physically awkward without extended headers. DSI-based displays connect via a flat flex cable, keeping the Pi 5's SoC clear for active cooling while providing hardware-accelerated touch input.

Why is my SPI touch screen display for Raspberry Pi lagging?

SPI touch lag is almost always caused by software polling rather than hardware limits. If your Python script uses time.sleep(0.1) in a while True loop to check the touch state, you are artificially capping your response rate to 10Hz. Use hardware interrupts (via the T_IRQ pin and gpiozero.Button as shown in the code above) or offload the polling to the Linux kernel using the ads7846 device tree overlay, which pushes touch events to the /dev/input/ subsystem asynchronously.

Can I use a touch screen display for Raspberry Pi without an external monitor?

Yes, this is called running 'headless' with a local UI. However, Raspberry Pi OS (Bookworm) defaults to the Wayland window manager, which behaves differently than the legacy X11 server. If you are using Pygame or Tkinter for your touch UI, you must ensure your script runs in the correct display environment. For Pygame, initialize it with os.environ['SDL_VIDEODRIVER'] = 'kmsdrm' to bypass X11/Wayland entirely and render directly to the DRM (Direct Rendering Manager) framebuffer, which is significantly faster and more reliable for dedicated kiosk builds.