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

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.

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.

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.

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 # | Signal | Description |
|---|---|---|
| 1, 17 | 3.3V | Power supply (3.3V input) |
| 2, 4 | 5V | Power supply (5V input) |
| 6, 9, 14, 20, 25 | GND | Power ground |
| 11 | TP_IRQ | Touch panel interrupt, low when touch detected |
| 18 | LCD_RS | LCD instruction / data register selection |
| 19 | LCD_SI / TP_SI | SPI data input, shared by LCD and touch panel |
| 21 | TP_SO | Touch panel SPI data output |
| 22 | LCD_RST | LCD reset signal |
| 23 | LCD_SCK / TP_SCK | SPI clock signal, shared by LCD and touch panel |
| 24 | LCD_CS | LCD chip select, active low |
| 26 | TP_CS | Touch panel chip select, active low |
| 3, 5, 7, 8, 10, 12, 13, 15, 16 | NC | Not connected |
Warning: Pins 3, 5, 7, 8, 10, 12, 13, 15 and 16 are listed as NC (not connected) in the datasheet, however the display physically occupies all 26 pins of the GPIO header. Treat all 26 pins as reserved when this display is mounted.
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.gitchmod -R 755 LCD-showcd 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 = 90graphics fb1: fb_ili9486 frame buffer, 480x320, 300 KiB video memoryads7846 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=1dtparam=i2c_arm=ondtparam=spi=onenable_uart=1dtoverlay=mhs35:rotate=90hdmi_group=2hdmi_mode=1hdmi_mode=87hdmi_cvt 480 320 60 6 0 0 0hdmi_drive=2
Change the dtoverlay line to the following :-
dtoverlay=mhs35:rotate=270
| Value | Orientation |
| rotate=90 | Default, USB ports on the right |
| rotate=270 | USB ports on the left, power cable at top |
| rotate=0 | Portrait, GPIO header at bottom |
| rotate=180 | Portrait, GPIO header at top |

Warning: Every time you change the rotation value you must recalibrate the touchscreen. The display rotation and touch calibration are completely independent and do not automatically stay in sync.
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-showsudo 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

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.confSection "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.
Warning: Remember: if you change the rotation in config.txt at any point, you must re-run xinput_calibrator and update 99-calibration.conf with the new values. The two settings are independent of each other.
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 fbswapcoherent_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
| File | Purpose |
| /boot/firmware/config.txt | Overlay, 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.conf | Touch calibration values from xinput_calibrator |
| /usr/share/X11/xorg.conf.d/45-evdev.conf | Evdev 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:
| Component | Value |
| Display driver | fbtft / fb_ili9486 |
| Touch driver | ads7846 (evdev, XPT2046 compatible) |
| Input driver | xserver-xorg-input-evdev |
| Framebuffer device | /dev/fb1 (fb0 reserved for HDMI on Pi 4) |
| Framebuffer swap | bcm2708_fb.fbswap=1 (set by installer) |
| Display server | X11 (Wayland switched off by installer) |
| SPI bus speed | 115 MHz |
| Default rotation | 90 degrees (change in /boot/firmware/config.txt) |
| Calibration config | /etc/X11/xorg.conf.d/99-calibration.conf |
| Hold for right-click | 1000ms 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 W1sudo 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-showsudo 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 shutdownDefaultDependencies=noBefore=shutdown.target reboot.target halt.targetRequires=shutdown.target[Service]Type=oneshotExecStart=/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-shutdownsudo 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 ratioconvert 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 TFTDefaultDependencies=noBefore=shutdown.target reboot.target halt.targetRequires=shutdown.target[Service]Type=oneshotExecStart=/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-reloadsudo 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.


