Skip to content

MHS35 Touchscreen LCD on Raspberry Pi – 64 Bit OS

MHS35 Touchscreen LCD on Raspberry Pi 4

Complete Setup Guide for Raspberry Pi OS Trixie (64-bit)

Raspberry Pi 4B  •  Debian Trixie aarch64  •  ILI9486 / XPT2046

Raspberry Pi booting to Raspberry Pi Desktop on a small HDMI touchscreen display placed on a wooden desk.
A Raspberry Pi connected to a small HDMI touchscreen during boot, showing the Raspberry Pi Desktop welcome screen while system services initialise.

TL;DR

The MHS35 3.5″ TFT touchscreen works on Raspberry Pi OS Trixie (64-bit), but not using the instructions supplied with the device. Here’s the short version:

  • Clone the goodtft repository, not the Lcdwiki one supplied in the box:
git clone https://github.com/goodtft/LCD-show.git
  • Run the installer:
cd LCD-show && sudo ./MHS35-show
  • Set your rotation and touch calibration in one command:
sudo ./rotate.sh 270

That’s it. The display appears on /dev/fb1 on Pi 4 (not fb0), the installer handles the Wayland to X11 switch automatically, and rotate.sh handles both the display rotation and touch recalibration together. Full details, explanation of why things work the way they do, and optional shutdown image instructions are in the guide below.

Introduction

Somewhere between lockdown one and lockdown three, I purchased a Metal Case with 3.5″ TFT Touch Screen for the Raspberry Pi 4, available from The Pi Hut at:

https://thepihut.com/products/raspberry-pi-4-metal-case-with-3-5-tft-touchscreen-480×320

I have absolutely no memory of why.

Working my way through my cupboard of doom at the weekend, I found it still in its original packaging. The instructions pointed to the following GitHub repository, which hadn’t been touched in around five years… a strong hint that the installer script predates 64-bit Raspberry Pi OS entirely:

https://github.com/Lcdwiki/LCD-show.git

It’s worth noting that The Pi Hut have since updated their product page to reference a different repository:

https://github.com/goodtft/LCD-show.git

The item remains on sale, so this is clearly still a supported product.

The original Lcdwiki repository no longer works on 64-bit Trixie due to compilation failures and incompatible 32-bit ARM flags. However, as we’ll see later in this guide, the goodtft repository does prove useful, though it provides no instructions for touchscreen calibration, which we’ll resolve later in the guide.

For this guide I’m not using the included metal case. Instead I’m running the display on a development Pi 4 housed in an Aluminium Armour Heatsink Case, also available from The Pi Hut at:

https://thepihut.com/products/aluminium-armour-heatsink-case-for-raspberry-pi-4

This case is essentially a giant heatsink, which is great for silent running of the Pi. If you’re doing the same, cover the two JST fan pins on the back of the display with Kapton/Electrical tape before mounting it… those pins will short against the aluminium case and cause all manner of mystery problems if left exposed.

Raspberry Pi with aluminium armour heatsink placed beside an MHS 3.5-inch SPI display board with Kapton tape covering the fan connector pins.

In this setup guide, I’ll walk through how I configured and calibrated this touchscreen display on a Raspberry Pi 4 using a fresh installation of Trixie, the latest Pi OS at the time of writing. This guide is the result of working through every dead end so you don’t have to.

The good news… it works beautifully once you know the correct approach. If you have better tips, tricks or a different approach, I’d love to hear your thoughts in the comments below.

What This Guide Covers

  • Getting the MHS35 3.5″ TFT display working on Raspberry Pi OS Trixie (64-bit)
  • Understanding why the original Lcdwiki installer script fails on 64-bit Trixie and what the goodtft repository does differently
  • Configuring the ILI9486 display driver and SPI framebuffer
  • Enabling and calibrating the XPT2046 resistive touchscreen using evdev and xinput_calibrator
  • Setting display rotation and recalibrating touch to match
  • Optional: displaying a blank screen or custom image on shutdown

Prerequisites

  • Raspberry Pi 4B with Raspberry Pi OS Trixie (64-bit) freshly installed
  • MHS35 display mounted on the GPIO header
  • SSH access or keyboard/HDMI monitor for initial setup
  • Internet connection on the Pi

For this article, I’m starting with a fresh install of Trixie using the Raspberry Pi Imager https://www.raspberrypi.com/software/

If you are setting this up headless (no keyboard/Display), you’ll need to ensure your network name is configured correctly.

Screenshot of Raspberry Pi Imager showing Raspberry Pi OS 64-bit selected in the operating system list.
Screenshot of Raspberry Pi Imager showing Raspberry Pi OS 64-bit selected in the operating system list.
Using Raspberry Pi Imager v2.0.6 to select Raspberry Pi OS (64-bit) before writing the operating system image to a microSD card.

Start with a full system update before proceeding

On first boot the display will be a blank screen, and will remain this way until we’ve installed and configured the driver.

Small 3.5-inch Raspberry Pi SPI touchscreen powered on showing a blank blue screen on a wooden desk.
The 3.5-inch SPI touchscreen connected to the Raspberry Pi powering on but displaying a blank blue screen before the correct display drivers are configured.
sudo apt update && sudo apt full-upgrade -y

When the Pi has completed downloading any patches, updates etc, reboot the device.

sudo reboot now

Hardware Reference

The full product user manual and technical specification is available at:

https://cdn.shopify.com/s/files/1/0176/3274/files/MHS-3.5inch_Display_User_Manual_EN.pdf

Key hardware specifications from the datasheet:

  • Display driver IC: ILI9486
  • Touch controller IC: XPT2046 (compatible with ADS7846, which is why the ads7846 Linux driver works correctly with this display)
  • Interface: SPI, supports up to 125MHz signal input
  • Resolution: 320×480 pixels
  • Touch type: Resistive
  • Operating voltage: 3.3V / 5V

Note: The datasheet lists the touch chip as XPT2046. Although the touchscreen controller is XPT2046, it is register-compatible with the ADS7846, which is why the standard Linux ads7846 driver works without modification. You will see ‘ADS7846 Touchscreen’ in dmesg and xinput output, which is correct and expected.

GPIO Pin Usage

The display occupies the following GPIO pins. Any project sharing this Pi should avoid using these pins for other purposes:

Pin #SignalDescription
1, 173.3VPower supply (3.3V input)
2, 45VPower supply (5V input)
6, 9, 14, 20, 25GNDPower ground
11TP_IRQTouch panel interrupt, low when touch detected
18LCD_RSLCD instruction / data register selection
19LCD_SI / TP_SISPI data input, shared by LCD and touch panel
21TP_SOTouch panel SPI data output
22LCD_RSTLCD reset signal
23LCD_SCK / TP_SCKSPI clock signal, shared by LCD and touch panel
24LCD_CSLCD chip select, active low
26TP_CSTouch panel chip select, active low
3, 5, 7, 8, 10, 12, 13, 15, 16NCNot connected

Step 1: Clone the LCD-show Repository

The MHS35 connects to the Pi via the SPI interface. Because it is not a standard display, the Linux kernel needs to be told how to communicate with it at boot time. This is done via a device tree overlay, a small binary file loaded during boot that describes the hardware and configures the SPI bus, framebuffer driver and touch controller. Without the correct overlay, the kernel has no knowledge the display exists.

The goodtft LCD-show repository contains the correct overlay, evdev driver, calibrator and installer script. Clone it to your Pi:

cd ~
git clone https://github.com/goodtft/LCD-show.git
chmod -R 755 LCD-show
cd LCD-show

Note: The repository includes pre-built arm64 deb packages for both the evdev input driver and xinput_calibrator, which are not available in the standard Trixie apt repositories. This is one of the key reasons the goodtft repository works on Trixie where the original Lcdwiki repository does not.

Step 2: Run the Installer Script

Run the MHS35 installer script. This handles the overlay, SPI configuration, evdev driver installation and switches the system from Wayland to X11 automatically:

sudo ./MHS35-show

The script will reboot the Pi on completion. After reboot the display should show the boot splash screen and desktop.

Note: The script creates a symlink from /boot/config.txt to /boot/firmware/config.txt so it correctly targets the Trixie boot configuration path. It also calls raspi-config to switch from Wayland to X11, which is required for the fbdev display driver to work.

Tip: After reboot, verify the drivers loaded correctly:

dmesg | grep -iE "ili|ads7846|fb"

You should see:

fb_ili9486 spi0.0: fbtft_property_value: rotate = 90
graphics fb1: fb_ili9486 frame buffer, 480x320, 300 KiB video memory
ads7846 spi0.1: touchscreen, irq 59

Step 3: Set Your Display Rotation

The installer script defaults to 90 degree rotation. You may need a different orientation depending on how your display and Pi are physically mounted.

The Easy Way

The simplest way to change the orientation of the screen and calibration of the touch display is to use the provided script :-

cd ~/LCD-show
./rotate.sh 270

The command takes one parameter and that’s the angle (0, 90, 180, 270). There are additional angles for HDMI monitors which I won’t cover here. The script makes the necessary changes to calibration and restarts the device.

However, if you wish to have manual control…

Edit the boot configuration to set your preferred rotation:

sudo nano /boot/firmware/config.txt

Find the dtoverlay line added by the installer and change the rotate value:

[all]
hdmi_force_hotplug=1
dtparam=i2c_arm=on
dtparam=spi=on
enable_uart=1
dtoverlay=mhs35:rotate=90
hdmi_group=2
hdmi_mode=1
hdmi_mode=87
hdmi_cvt 480 320 60 6 0 0 0
hdmi_drive=2

Change the dtoverlay line to the following :-

dtoverlay=mhs35:rotate=270
ValueOrientation
rotate=90Default, USB ports on the right
rotate=270USB ports on the left, power cable at top
rotate=0Portrait, GPIO header at bottom
rotate=180Portrait, GPIO header at top
Raspberry Pi Desktop interface displayed on a small 3.5-inch SPI touchscreen connected to a Raspberry Pi.

Save the file and reboot:

sudo reboot

Step 4: Install xinput_calibrator

The goodtft repository includes a pre-built arm64 deb package for xinput_calibrator. Install it from the LCD-show directory:

cd ~/LCD-show
sudo dpkg -i xinput-calibrator_0.7.5+git20140201-1+b2_arm64.deb

Step 5: Calibrate the Touchscreen

Run the calibrator from an SSH session or terminal. It will display four crosshairs on the LCD screen in turn. Tap each one as accurately as possible with the stylus:

DISPLAY=:0 xinput_calibrator
Raspberry Pi 3.5-inch SPI touchscreen showing the touchscreen calibration screen with crosshair target.

On completion it will output a calibration block similar to this:

ection "InputClass"
Identifier "calibration"
MatchProduct "ADS7846 Touchscreen"
Option "Calibration" "293 3909 3788 236"
Option "SwapAxes" "1"
EndSection

Copy the output and write it to the calibration config file:

sudo nano /etc/X11/xorg.conf.d/99-calibration.conf

Paste the output from xinput_calibrator, for reference as I needed my display upside down

cat /etc/X11/xorg.conf.d/99-calibration.conf
Section "InputClass"
Identifier "calibration"
MatchProduct "ADS7846 Touchscreen"
Option "Calibration" "293 3909 3788 236"
Option "SwapAxes" "1"
Option "EmulateThirdButton" "1"
Option "EmulateThirdButtonTimeout" "1000"
Option "EmulateThirdButtonMoveThreshold" "300"
EndSection

The EmulateThirdButtonTimeout is a useful parameter, it allows you to set a time and drift limit to emulate a right click on the screen by holding the point down for 1 second (1000 ms) as long as the pointer doesn’t move 300 points during that time.

Now save the file, and test by applying the change without rebooting:

sudo systemctl restart lightdm

Tip: Test all four corners of the screen after calibration to confirm the cursor reaches each edge accurately. If touch feels off, simply re-run xinput_calibrator and repeat the process.

Verifying Everything Works

Check the display driver loaded correctly:

dmesg | grep -iE "ili|ads7846|fb"

You should see output similar to the following:

dmesg | grep -iE "ili|ads7846|fb"
[ 0.000000] NUMA: Faking a node at [mem 0x0000000000000000-0x00000000fbffffff]
[ 0.000000] Faking node 1 at [mem 0x0000000080000000-0x00000000fbffffff] (1984MB)
[ 0.000000] NODE_DATA(1) allocated [mem 0xfb7f9300-0xfb7fbfff]
[ 0.000000] DMA32 [mem 0x0000000040000000-0x00000000fbffffff]
[ 0.000000] node 1: [mem 0x0000000080000000-0x00000000fbffffff]
[ 0.000000] Initmem setup node 1 [mem 0x0000000080000000-0x00000000fbffffff]
[ 0.000000] Kernel command line: coherent_pool=1M 8250.nr_uarts=1 snd_bcm2835.enable_headphones=0 cgroup_disable=memory numa_policy=interleave nvme.max_host_mem_size_mb=0 snd_bcm2835.enable_headphones=1 snd_bcm2835.enable_hdmi=1 bcm2708_fb.fbwidth=480 bcm2708_fb.fbheight=320 bcm2708_fb.fbswap=1 numa=fake=2 system_heap.max_order=0 smsc95xx.macaddr=DC:A6:32:44:71:4D vc_mem.mem_base=0x3ec00000 vc_mem.mem_size=0x40000000 console=ttyS0,115200 console=tty1 root=PARTUUID=aa5e1c16-02 rootfstype=ext4 fsck.repair=yes rootwait quiet splash plymouth.ignore-serial-consoles cfg80211.ieee80211_regdom=GB
[ 0.000000] Built 2 zonelists, mobility grouping on. Total pages: 1012736
[ 0.000328] LSM: initializing lsm=capability
[ 0.032069] raspberrypi-firmware soc:firmware: Firmware hash is cd866525580337c0aee4b25880e1f5f9f674fb24
[ 0.962322] bcm2708_fb soc:fb: FB found 1 display(s)
[ 0.965728] bcm2708_fb soc:fb: Registered framebuffer for display 0, size 480x320
[ 4.194830] systemd[1]: Listening on systemd-initctl.socket - initctl Compatibility Named Pipe.
[ 6.472055] fbtft: module is from the staging directory, the quality is unknown, you have been warned.
[ 6.475523] ads7846 spi0.1: supply vcc not found, using dummy regulator
[ 6.477473] ads7846 spi0.1: touchscreen, irq 45
[ 6.478427] input: ADS7846 Touchscreen as /devices/platform/soc/fe204000.spi/spi_master/spi0/spi0.1/input/input0
[ 6.639062] fb_ili9486: module is from the staging directory, the quality is unknown, you have been warned.
[ 6.639546] SPI driver fb_ili9486 has no spi_device_id for ilitek,ili9486
[ 6.639650] fb_ili9486 spi0.0: fbtft_property_value: regwidth = 16
[ 6.639659] fb_ili9486 spi0.0: fbtft_property_value: buswidth = 8
[ 6.639665] fb_ili9486 spi0.0: fbtft_property_value: debug = 0
[ 6.639671] fb_ili9486 spi0.0: fbtft_property_value: rotate = 90
[ 6.639677] fb_ili9486 spi0.0: fbtft_property_value: fps = 30
[ 6.639682] fb_ili9486 spi0.0: fbtft_property_value: txbuflen = 32768
[ 7.068245] graphics fb1: fb_ili9486 frame buffer, 480x320, 300 KiB video memory, 32 KiB buffer memory, fps=31, spi0.0 at 115 MHz

Note: On Raspberry Pi 4, the LCD will appear as fb1 rather than fb0. The Pi 4 always reserves fb0 for the HDMI virtual framebuffer regardless of whether an HDMI cable is connected. The goodtft installer script adds bcm2708_fb.fbswap=1 to the kernel command line, which instructs the firmware to present the LCD as the primary display to Xorg. Xorg then auto-detects and drives the desktop to it correctly without any manual fbdev configuration required.

Confirm fbswap is active:

cat /proc/cmdline | grep fbswap

You should see bcm2708_fb.fbswap=1 in the output.

cat /proc/cmdline | grep fbswap
coherent_pool=1M 8250.nr_uarts=1 snd_bcm2835.enable_headphones=0 cgroup_disable=memory numa_policy=interleave nvme.max_host_mem_size_mb=0 snd_bcm2835.enable_headphones=1 snd_bcm2835.enable_hdmi=1 bcm2708_fb.fbwidth=480 bcm2708_fb.fbheight=320 bcm2708_fb.fbswap=1 numa=fake=2 system_heap.max_order=0 smsc95xx.macaddr=DC:A6:32:44:71:4D vc_mem.mem_base=0x3ec00000 vc_mem.mem_size=0x40000000 console=ttyS0,115200 console=tty1 root=PARTUUID=aa5e1c16-02 rootfstype=ext4 fsck.repair=yes rootwait quiet splash plymouth.ignore-serial-consoles cfg80211.ieee80211_regdom=GB

Check the touch device is registered:

DISPLAY=:0 xinput list

You should see ADS7846 Touchscreen listed as a slave pointer device.

Check the calibration is active:

DISPLAY=:0 xinput list-props "ADS7846 Touchscreen" | grep -iE "calib|swap"

Summary of Files Changed

FilePurpose
/boot/firmware/config.txtOverlay, SPI, rotation and HDMI settings (via symlink)
/proc/cmdline (kernel)bcm2708_fb.fbswap=1 added by installer, presents LCD as primary display to Xorg
/etc/X11/xorg.conf.d/99-calibration.confTouch calibration values from xinput_calibrator
/usr/share/X11/xorg.conf.d/45-evdev.confEvdev input driver configuration (installed by script)

Final Working Configuration

Once all steps are complete, your system should match the following state. Use this as a reference to confirm everything is correctly configured:

ComponentValue
Display driverfbtft / fb_ili9486
Touch driverads7846 (evdev, XPT2046 compatible)
Input driverxserver-xorg-input-evdev
Framebuffer device/dev/fb1 (fb0 reserved for HDMI on Pi 4)
Framebuffer swapbcm2708_fb.fbswap=1 (set by installer)
Display serverX11 (Wayland switched off by installer)
SPI bus speed115 MHz
Default rotation90 degrees (change in /boot/firmware/config.txt)
Calibration config/etc/X11/xorg.conf.d/99-calibration.conf
Hold for right-click1000ms touch hold (EmulateThirdButton)

Troubleshooting

Screen stays blank after running the installer

dmesg | grep -iE "ili|spi|fb"

The LCD will appear as fb1 on Pi 4, not fb0. If no fb_ili9486 entries appear at all, the overlay did not load. Check /boot/firmware/config.txt contains the dtoverlay=mhs35 line and that the script completed without errors.

Desktop does not appear after reboot

The installer switches from Wayland to X11 automatically, but confirm it took effect:

sudo raspi-config nonint get_wayland

A return value of W1 confirms X11 is active. If not, run:

sudo raspi-config nonint do_wayland W1
sudo reboot

Touch works but cursor moves in wrong direction after rotation change

Re-run the calibrator and update the config file:

DISPLAY=:0 xinput_calibrator

Paste the output into /etc/X11/xorg.conf.d/99-calibration.conf and restart lightdm.

xinput_calibrator not found

Install from the goodtft repository deb package:

cd ~/LCD-show
sudo dpkg -i xinput-calibrator_0.7.5+git20140201-1+b2_arm64.deb

Optional: Blank Screen on Shutdown

By default when the Pi shuts down, the SPI bus goes idle and the display freezes on the last frame. The MHS35 backlight is hardwired and cannot be controlled via software, however a clean black screen can be displayed before the system halts.

Note: Confirm whether your display has software backlight control. If the following returns nothing, the backlight is hardwired:

ls /sys/class/backlight/

Create the Shutdown Service

sudo nano /etc/systemd/system/tft-shutdown.service
[Unit]
Description=Blank TFT display on shutdown
DefaultDependencies=no
Before=shutdown.target reboot.target halt.target
Requires=shutdown.target
[Service]
Type=oneshot
ExecStart=/bin/bash -c "dd if=/dev/zero of=/dev/fb1 2>/dev/null; true"
TimeoutStartSec=2
[Install]
WantedBy=shutdown.target halt.target reboot.target

Once edited, enable and start the service…

sudo systemctl enable tft-shutdown
sudo systemctl start tft-shutdown

Tip: The backlight remains physically powered but the screen displays solid black, which in most environments is indistinguishable from off and far cleaner than a frozen desktop image.

Optional: Custom Shutdown Image

Instead of a black screen, a custom image can be displayed on shutdown. The framebuffer expects raw RGB565 pixel data, so the image must be pre-converted at setup time.

Install ImageMagick

sudo apt install imagemagick

Prepare Your Image

# Stretch to fit (ignores aspect ratio)
convert your-image.jpg -resize 480x320! -depth 8 RGB:- | sudo tee /opt/tft-shutdown.raw > /dev/null
# Letterbox to preserve aspect ratio
convert your-image.jpg -resize 480x320 -background black -gravity center -extent 480x320 -depth 8 RGB:- | sudo tee /opt/tft-shutdown.raw > /dev/null

Update the Shutdown Service

sudo nano /etc/systemd/system/tft-shutdown.service
[Unit]
Description=Show shutdown image on TFT
DefaultDependencies=no
Before=shutdown.target reboot.target halt.target
Requires=shutdown.target
[Service]
Type=oneshot
ExecStart=/bin/bash -c "cat /opt/tft-shutdown.raw > /dev/fb1 2>/dev/null; sleep 2; true"
TimeoutStartSec=5
[Install]
WantedBy=shutdown.target halt.target reboot.target

Once edited, enable and start the service…

sudo systemctl daemon-reload
sudo systemctl enable tft-shutdown

Test Without Shutting Down

sudo cat /opt/tft-shutdown.raw > /dev/fb1

If the image looks wrong, re-run the convert command with adjusted parameters and test again. No reboot required.

Final Thoughts

In fairness to The Pi Hut, their product page has been updated since the device was originally sold. The instructions supplied in the box pointed to the old Lcdwiki repository, but the product page now correctly references the goodtft repository and the steps do get the display working. If I had checked the product page first rather than relying on the printed instructions in the packaging, I’d have saved myself a few dead ends.

Where this guide picks up is rotation and recalibration. The updated instructions assume landscape orientation and don’t cover what happens when you need the display mounted differently. In my case, with the power cable coming out the top and the Pi standing on the bottom edge of the Aluminium Armour heatsink case, 270 degrees was the only orientation that made physical sense. And every time you change that rotation value, the touchscreen needs recalibrating to match, which isn’t documented anywhere in the current instructions.

What started as a weekend curiosity from the cupboard of doom turned into a deeper dive than expected. The fbswap kernel parameter, the evdev versus libinput distinction, the goodtft installer being considerably more capable than its own documentation suggests… there was more going on under the hood than the simple “run this script” instructions implied.

The cupboard of doom still has more to give. I’m planning to put this display to work on a few projects including a live train departure board, a TfL Underground status display and possibly a Batocera gaming setup. If any of those sound interesting, keep an eye on the blog.

If this saved you an afternoon of head scratching, that’s exactly what it was written for. If you’ve found a better approach, spotted something I’ve missed, or your setup differs from mine, drop a comment below. I read them all.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.