Setting up a touch screen with Raspberry Pi 5 using the DSI (Display Serial Interface) port is the most reliable method for low-latency, kiosk-grade embedded projects. Unlike SPI displays that choke the CPU or HDMI setups that require a separate USB cable for touch data, a DSI display routes both 800x480 video and I2C touch telemetry directly through the Pi’s high-speed ribbon connectors. To get this running, you need a DSI-compatible panel (like the Waveshare 5-inch DSI LCD), a 15-pin FPC cable for video, and a 4-pin jumper wire for the I2C touch overlay.
This guide targets the Raspberry Pi 5 (4GB variant) running the 64-bit Pi OS Bookworm. We will cover the physical wiring, the specific config.txt overrides required for the Pi 5’s new RP1 I/O controller, and a complete Python pygame implementation with robust error handling.
Interface Comparison: DSI vs SPI vs HDMI+USB
Before cutting wires, it is critical to understand why DSI is the superior choice for modern Pi builds. The table below breaks down the real-world performance metrics you can expect from the three primary touch screen interfaces available for the Raspberry Pi 5.
| Interface Type | Max Refresh Rate | CPU Overhead (Video) | Touch Latency | Typical Cost (5-inch) |
|---|---|---|---|---|
| DSI (Direct) | 60 Hz (Hardware) | < 2% (Offloaded to RP1) | ~15 ms (I2C/evdev) | $45 - $60 |
| SPI (GPIO) | 30 Hz (Software) | 15% - 25% (Framebuffer) | ~40 ms (SPI polling) | $25 - $35 |
| HDMI + USB | 60 Hz (Hardware) | < 2% (GPU driven) | ~25 ms (USB HID) | $55 - $80 |
| DPI (Parallel) | 60 Hz (Hardware) | ~5% (GPIO multiplexing) | ~20 ms (I2C/evdev) | $40 - $50 |
While HDMI+USB is plug-and-play, it requires two cables and leaves your USB ports occupied. SPI is cheap but unusable for smooth UI animations. DSI hits the sweet spot: native 60Hz video with minimal CPU tax, leaving your Pi 5’s processing power free for computer vision or MQTT broker tasks.
Hardware BOM and I2C Pin Mapping
To replicate this exact build, source the following specific components. Do not substitute the power supply; the Pi 5 and a 5-inch DSI backlight will trigger brownout warnings if fed by a standard 15W phone charger.
- Compute Board: Raspberry Pi 5 (4GB or 8GB RAM)
- Display: Waveshare 5" DSI LCD (800x480, IPS, Capacitive Touch)
- Power Supply: Official Raspberry Pi 27W USB-C PD Power Supply (5V/5A)
- Storage: 32GB microSD Card (A2 rating minimum for Bookworm OS swap handling)
- Cables: 15-pin FPC ribbon (included with display), 4-pin female-to-female jumper wires
Touch Controller Pin Mapping
The DSI ribbon cable handles the video signal, but the capacitive touch controller (typically a Goodix GT911 or FocalTech FT5x06) communicates via the I2C bus. You must bridge the display’s touch pins to the Pi 5’s 40-pin header as follows:
| Pi 5 Pin # | BCM GPIO | Function | Waveshare Touch Pin |
|---|---|---|---|
| 1 | 3.3V Power | VCC | VCC (3.3V) |
| 6 | Ground | GND | GND |
| 3 | GPIO 2 (SDA1) | I2C Data | SDA |
| 5 | GPIO 3 (SCL1) | I2C Clock | SCL |
Wiring and Pi OS Bookworm Configuration
Follow these numbered steps to physically assemble and configure the OS. Note that Pi OS Bookworm changed the boot partition mount point, which trips up many older tutorials.
- Seat the DSI Ribbon: Lift the black locking collar on the Pi 5’s DSI port. Insert the 15-pin FPC cable with the copper contacts facing the PCB (towards the board, not the Ethernet port). Push the collar down to lock.
- Connect I2C Touch Jumpers: Wire VCC, GND, SDA, and SCL from the display’s touch header to the Pi 5’s 40-pin header using the mapping table above.
- Flash OS and Mount Boot: Flash Pi OS Bookworm (64-bit) to your SD card. Boot the Pi, open a terminal, and navigate to the firmware directory:
cd /boot/firmware/(Do not use/boot/as in older Bullseye tutorials). - Edit config.txt: Open the configuration file with
sudo nano config.txt. Add the following lines at the very bottom to enable the DSI pipeline and I2C bus:
# Enable I2C for touch controller
dtparam=i2c_arm=on
# Force DSI video output and ignore HDMI hotplug
dtoverlay=vc4-kms-dsi-7inch
display_auto_detect=0
- Reboot and Verify: Run
sudo reboot. Upon restart, the display should show the Pi OS desktop. If the screen is white or blank, power down and reseat the FPC cable—improper seating is the cause of 90% of DSI failures.
Python Pygame Touch Implementation
Below is a complete, compilable Python script using pygame. It initializes the display, maps touch events via the Linux evdev subsystem, and draws interactive buttons. This code targets the 800x480 resolution of the Waveshare 5" DSI panel.
import pygame
import sys
import os
# --- HARDWARE & PIN DEFINITIONS ---
# Pi 5 DSI uses hardware I2C Bus 1 for touch (SDA=GPIO2, SCL=GPIO3)
# Video is routed via DSI ribbon; no GPIO pins used for pixel data
I2C_BUS = 1
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 480
FPS = 60
# UI Element Definitions
BUTTON_WIDTH = 200
BUTTON_HEIGHT = 80
BUTTON_X = (SCREEN_WIDTH - BUTTON_WIDTH) // 2
BUTTON_Y = (SCREEN_HEIGHT - BUTTON_HEIGHT) // 2
def draw_button(screen, color, text):
"""Draws a simple touch target and centers text."""
rect = pygame.Rect(BUTTON_X, BUTTON_Y, BUTTON_WIDTH, BUTTON_HEIGHT)
pygame.draw.rect(screen, color, rect, border_radius=12)
font = pygame.font.SysFont('Arial', 32, bold=True)
text_surf = font.render(text, True, (255, 255, 255))
text_rect = text_surf.get_rect(center=rect.center)
screen.blit(text_surf, text_rect)
return rect
def main():
# Force Pygame to use the framebuffer directly if running headless/kiosk
os.environ['SDL_FBDEV'] = '/dev/fb0'
try:
pygame.init()
# Initialize fullscreen mode for kiosk-style touch deployment
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.FULLSCREEN)
pygame.display.set_caption('Pi 5 DSI Touch Interface')
except pygame.error as e:
print(f'Fatal Pygame Init Error: {e}')
print('Check if /dev/fb0 exists and user has video group permissions.')
sys.exit(1)
clock = pygame.time.Clock()
button_rect = draw_button(screen, (41, 128, 185), 'TAP ME')
pygame.display.flip()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# Touch presses register as mouse events via evdev in Pi OS
elif event.type == pygame.MOUSEBUTTONDOWN:
if button_rect.collidepoint(event.pos):
draw_button(screen, (39, 174, 96), 'TOUCHED!')
pygame.display.flip()
elif event.type == pygame.MOUSEBUTTONUP:
draw_button(screen, (41, 128, 185), 'TAP ME')
pygame.display.flip()
# Hardware escape hatch: touch top-left corner to exit
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
clock.tick(FPS)
pygame.quit()
sys.exit()
if __name__ == '__main__':
main()
Save this as touch_ui.py and execute it via python3 touch_ui.py. If you are running this over SSH, ensure you have X11 forwarding enabled or run it directly on the Pi terminal.
Debugging Touch Failures and Permission Errors
When integrating touch screens with Raspberry Pi, the video usually works immediately, but the touch input fails. If your script crashes upon touching the screen, you will likely see this exact error string in your terminal:
PermissionError: [Errno 13] Permission denied: '/dev/input/event2'
This happens because Pi OS Bookworm restricts raw access to input devices for security. Here are the first three things to check and fix when this occurs, ranked from most to least likely:
- User Not in the 'input' Group (Most Common): Your current user lacks permission to read the raw evdev touch stream. Fix this by adding your user to the input group:
sudo usermod -a -G input $USER. You must reboot or log out and back in for this to take effect. - Wayland vs X11 Evdev Mapping: Bookworm defaults to the Wayland window manager, which intercepts touch events before
pygamecan read them via/dev/input/. If you need direct framebuffer access, edit/boot/firmware/cmdline.txtand appendwayland.enable=0to force the legacy X11 server, or run your script under a Wayland-compatible SDL2 backend by settingos.environ['SDL_VIDEODRIVER'] = 'wayland'in your Python code. - Missing Udev Rules for I2C Touch: If the
inputgroup fix fails, the kernel might be assigning restrictive permissions to the specific USB/I2C node on boot. Create a custom udev rule:sudo nano /etc/udev/rules.d/99-touchscreen.rulesand add the lineSUBSYSTEM=="input", KERNEL=="event*", MODE="0666". Reload rules withsudo udevadm control --reload-rules.
sudo to bypass permission errors. Running UI frameworks as root creates severe security vulnerabilities and breaks audio routing in PulseAudio/PipeWire. Fix the group permissions instead.
Scaling the Build: Kiosk Mode vs. Simplified Button
Once your touch screen with Raspberry Pi is responding reliably, you will typically move in one of two directions: hardening it for a standalone kiosk, or stripping it down for a single-purpose appliance.
Extending to Autostart Kiosk Mode
To make the Pi boot directly into your touch interface without showing the desktop environment, use the Wayland/X11 autostart configuration. For X11, create a desktop entry file:
nano ~/.config/autostart/touch_kiosk.desktop
Populate it with the following configuration to hide the mouse cursor and launch your script:
[Desktop Entry]
Type=Application
Name=TouchKiosk
Exec=sh -c 'unclutter -idle 0 & python3 /home/pi/touch_ui.py'
Hidden=false
X-GNOME-Autostart-enabled=true
Note: Install unclutter via sudo apt install unclutter to hide the mouse pointer, which otherwise hovers over the touch point and ruins the kiosk aesthetic.
Simplifying to a Single Hardware Button
If a full UI framework like pygame is overkill and you only need a single "Confirm" touch target, drop the framebuffer entirely. Instead, use the evdev library to read raw touch coordinates directly from the I2C bus. This reduces memory usage from ~60MB (Pygame) to under 5MB, which is critical if you are running heavy background tasks like a local Mosquitto MQTT broker or a YOLO vision model on the Pi 5.
import evdev
from evdev import ecodes
# Locate the touch device automatically
devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
touch_dev = next((d for d in devices if 'touch' in d.name.lower() or 'gt911' in d.name.lower()), None)
if touch_dev:
for event in touch_dev.read_loop():
if event.type == ecodes.EV_KEY and event.code == ecodes.BTN_TOUCH:
if event.value == 1:
print('Screen Tapped - Triggering Relay')
For deeper hardware specifications regarding the DSI pipeline and I2C addressing on the RP1 chip, refer to the official Raspberry Pi display documentation. For specific driver overlays and calibration matrices for Waveshare panels, consult the Waveshare 5-inch DSI LCD wiki. Finally, for advanced event handling in Python, the Pygame event module documentation provides the complete list of touch-to-mouse event mappings.






