Every Guide for This Raspberry Pi Touchscreen Is Out of Date. Here's What Works on moOde 10

I have a Raspberry Pi 3A+ running moOde as a little music player, with an IQaudio DAC+ on top. Adding the DAC took about two minutes. Adding a cheap 3.5" touchscreen to it took an entire evening, and not because the job is hard — because every single guide I found was written for software that doesn't exist anymore.

So here's the version that actually works, on moOde 10, in 2026. If you're staring at a black screen right now and wondering what you broke, you probably didn't break anything. Skip to the checkpoint in Step 8.

What I'm working with

  • Raspberry Pi 3 Model A+ (512 MB — this matters later)
  • IQaudio DAC+ HAT
  • 3.5" RPi display, ILI9486 panel with an XPT2046 touch controller — the "goodtft/LCD-show" type you find everywhere for about ten quid
  • moOde 10.0.0, which is RaspiOS 13.2 Trixie 64-bit, kernel 6.12.47

Get your audio working before you touch the display. Seat the DAC, pick IQaudIO Pi-DAC+ under Configure → Audio, play something, confirm it works. You want one variable at a time.

The one rule: never run LCD35-show. The vendor's install script rewrites your config.txt from scratch. On moOde that wipes your DAC overlay and can leave the thing unbootable. Clone the repo — there's exactly one file in it you need — but don't run the script.

Why the old guides fail

Worth knowing, because it'll save you from "fixing" things that aren't broken:

  • fbcp is dead. Almost every tutorial has you compile rpi-fbcp to mirror fb0 to fb1. It grabs the screen through DispmanX, which doesn't exist under the KMS driver. It's been unusable since Bullseye. Don't build it.
  • "Switch to fkms" is not the fix either. vc4-fkms-v3d is deprecated on Bookworm and later. You comment out the KMS line entirely instead.
  • fbturbo is gone on Trixie. Bookworm-era guides install xserver-xorg-video-fbturbo. Use plain fbdev now.
  • Don't install LightDM. moOde starts the display itself. With KMS off, LightDM just fails with "no screens found."

That last one caused me the most confusion, so here's the thing to check first.

moOde starts X by itself — verify before you fight it

Every Trixie guide tells you to switch from Wayland to X11. Run it on moOde and you get this:

$ sudo raspi-config nonint do_wayland W1
sed: can't read /etc/lightdm/lightdm.conf: No such file or directory
sed: can't read /etc/lightdm/lightdm.conf: No such file or directory
...

Looks bad. It's harmless. That command works by editing LightDM's config file, and moOde doesn't ship a display manager at all, so there was nothing to edit and nothing got changed.

It also means the step is pointless here. Check what moOde actually does:

$ systemctl cat localdisplay | grep -i exec
ExecStart=/usr/bin/xinit -- -nocursor

xinit. That's X11, launched directly, no Wayland and no display manager anywhere. The whole Wayland-versus-X11 question only exists on systems where a display manager picks the session. (And -nocursor is a nice touch — no mouse pointer floating around on a touchscreen.)

Step 1 — Hardware

Stack order is Pi → DAC+ → display.

The display's 26-pin socket goes onto GPIO pins 1–26, at the end nearest the SD card slot. Pin 1 is the corner pin closest to the SD card. When it's seated right, 14 pins stay exposed at the other end. Get it wrong in the other direction and you get no display and possibly a short, so look twice before you power on.

Two things nobody mentions:

  • Clearance. The display has to physically clear the DAC's RCA jacks. If your DAC doesn't have tall pass-through pins, you need a GPIO stacking header extender. Order one before you start or you'll be waiting on the post like I was.
  • No pin conflict. The display uses SPI (GPIO 7–11, 24, 25), the DAC uses I2S (GPIO 18–21). They don't collide. This stack is fine.

Use a proper 5V/2.5A supply. The Pi is feeding both boards now.

Step 2 — Back up, then install packages

sudo cp /boot/firmware/config.txt /boot/firmware/config.txt.bak

sudo apt update
sudo apt install xserver-xorg-video-fbdev xserver-xorg-legacy \
                 xserver-xorg-input-evdev evtest git xinput -y

Notice what's not in there: no fbturbo, no lightdm, no cmake. Nothing gets compiled — that was fbcp's world and we're not going there.

xinput isn't on the minimal moOde image and you'll want it for touch tuning later. Install it now.

Step 3 — X server permissions

We're about to disable KMS, which means X loses access to the DRM device and needs elevated rights. Without this you get a black screen and Cannot open /dev/tty0 (Permission denied) buried in a log you haven't thought to check yet.

sudo tee /etc/X11/Xwrapper.config > /dev/null << 'EOF'
allowed_users=anybody
needs_root_rights=yes
EOF

About those << 'EOF' blocks. Each one is a single command, but it has to keep its line breaks. If your SSH client flattens multi-line pastes into one line, it'll silently write a broken file and you'll spend an hour chasing a symptom that looks nothing like "bad config file." Ask me how I know.

If pasting is unreliable, use sudo nano <filename> instead and paste just the file contents. Also: no leading spaces on those two lines above.

Step 4 — Clear out conflicting X configs

sudo mv /usr/share/X11/xorg.conf.d/20-noglamor.conf \
        /usr/share/X11/xorg.conf.d/20-noglamor.conf.bak 2>/dev/null
sudo rm -f /usr/share/X11/xorg.conf.d/99-fbturbo.conf

Step 5 — Grab the overlay

cd ~
git clone https://github.com/goodtft/LCD-show
sudo cp LCD-show/usr/tft35a-overlay.dtb /boot/firmware/overlays/tft35a.dtbo
sudo mkdir -p /etc/X11/xorg.conf.d

Clone the repo, take the one file, walk away. Do not run anything in there.

Step 6 — Point X at the SPI screen

sudo tee /etc/X11/xorg.conf.d/99-fbdev.conf > /dev/null << 'EOF'
Section "ServerLayout"
    Identifier "SPI Layout"
    Screen "SPI Screen"
EndSection

Section "Device"
    Identifier "SPI Display"
    Driver "fbdev"
    Option "fbdev" "/dev/fb1"
    Option "ShadowFB" "on"
EndSection

Section "Monitor"
    Identifier "SPI Monitor"
EndSection

Section "Screen"
    Identifier "SPI Screen"
    Monitor "SPI Monitor"
    Device "SPI Display"
    DefaultDepth 16
    SubSection "Display"
        Depth 16
        Modes "480x320"
        Virtual 480 320
    EndSubSection
EndSection
EOF

The ServerLayout section is what forces X to use only the little screen. On a Pi 3 the SPI panel comes up as fb1 because HDMI already claimed fb0, so you have to name it explicitly — X won't work it out on its own.

Step 7 — Edit config.txt

Good news: moOde's config.txt is tidy and already includes several lines other guides tell you to add — max_framebuffers=2, disable_fw_kms_setup=1, hdmi_drive=2, hdmi_force_hotplug=1. Look before you duplicate.

Open it with sudo nano /boot/firmware/config.txt.

Two changes in the existing [all] block near the top:

#dtoverlay=vc4-kms-v3d
display_auto_detect=0

Leave dtoverlay=iqaudio-dacplus alone. It sits in a block marked "Do not alter this section" and there's no reason to go near it.

Then add this at the very bottom of the file:

[all]
# ===== 3.5" SPI display =====
gpu_mem=32
dtparam=spi=on
dtoverlay=tft35a:rotate=90
hdmi_group=2
hdmi_mode=87
hdmi_cvt 480 320 60 6 0 0 0

Two things there are deliberate:

  • That [all] header is insurance. config.txt uses conditional filters like [pi4]. If the file happens to end inside one of those, your appended lines quietly apply to the wrong board and nothing works. [all] resets the filter.
  • gpu_mem=32, not 64. On a 512 MB board every megabyte counts and there's no GPU work happening anyway.

The hdmi_group=2 here overrides moOde's own hdmi_group=0 further up. Last value wins, so you don't need to edit the earlier line.

Save, then sudo reboot.

Step 8 — The checkpoint that actually matters

A black screen here is normal. Old guides promise boot text on the little display. That comes from mapping the console to the panel with fbcon=map:10, which this setup doesn't do. Console text goes to HDMI, and the panel stays dark even when the driver has loaded perfectly.

The real test is over SSH:

$ ls /dev/fb*
/dev/fb0  /dev/fb1

$ dmesg | grep -i fb1
[   10.503105] graphics fb1: fb_ili9486 frame buffer, 480x320, 300 KiB video memory,
               32 KiB buffer memory, fps=31, spi0.0 at 16 MHz

That second line is the finish line for the hard part. Driver bound, right resolution, SPI at 16 MHz, about 31 fps.

Want to prove the panel itself is alive? Throw noise at it:

sudo dd if=/dev/urandom of=/dev/fb1 bs=307200 count=1

Static on the screen means everything below X is working. Clear it with if=/dev/zero.

If fb1 is missing, it's the overlay. Check the .dtbo is actually where you copied it, check your config lines for typos, and re-seat the display on pins 1–26.

Step 9 — Switch it on in moOde 10

The menu got renamed. It used to live under Configure → System → Local Display. In moOde 10 it's:

Configure → Peripherals → moOde WebUI → WebUI toggle ON

Leave Target URL as http://localhost/. While you're on that page:

  • Disable GPU → ON. This matters here specifically. We turned KMS off, so there's no DRM device, and without this flag Chromium wastes time hunting for hardware acceleration that isn't there. It saves RAM too, which you don't have.
  • Wake on play → ON works nicely with the default 10-minute screen blank.
  • RESTART Local Display is your fast retry button. Most changes don't need a full reboot.

Reboot, and then wait. On 512 MB, the first Chromium launch can be two or three minutes of apparent nothing while it swaps itself awake. I nearly declared failure twice before it appeared. Give it five minutes before you start debugging.

Step 10 — Touch calibration, the part that took longest

The display was up in about an hour. Touch took the rest of the evening. Here's every trap, so you can skip them all.

Trap 1: the stock calibration file does nothing at all

goodtft ships per-rotation files like 99-calibration.conf-35-0. Copy one in and nothing happens, because it looks like this:

Section "InputClass"
    Identifier      "calibration"
    MatchProduct    "ADS7846 Touchscreen"
    Option  "Calibration"   "268 3880 227 3936"
    Option  "SwapAxes"      "0"
EndSection

No Driver line. Without one, the device stays on X's default driver, libinput — and libinput ignores Calibration and SwapAxes completely, because those are evdev options. The file is inert. It just sits there looking like it's doing something.

Trap 2: xinput_calibrator gives you garbage

Run it on a device that already has calibration applied and it stacks the two together. Mine spat out:

Option "Calibration" "2820 2891 2291 2413"

Those numbers claim the entire width of my screen spans 71 raw units out of 4095. Every tap collapsed onto a single point, which feels exactly like completely dead touch. Very confusing when all you were trying to fix was a small offset.

It also kept refusing to finish, complaining "mis-click" on taps I'd placed dead centre on the crosshair. That's its geometric sanity check tripping over a noisy resistive panel with swapped axes. --misclick 0 turns the check off, but the compounding problem is still there, so the numbers stay useless.

What actually works: measure it yourself with evtest

sudo evtest

Pick the ADS7846 device, tap each of the four corners, and write down the ABS_X and ABS_Y values. Use a stylus or a fingernail — a soft fingertip is vague on these panels. Mine, measured in portrait:

CornerABS_XABS_Y
top-left330445
top-right3253650
bottom-left3808326
bottom-right37603090

Look at what moves. Going left to right barely changes ABS_X but sweeps ABS_Y across its whole range. Top to bottom does the opposite. The axes on this panel are physically swapped compared to what the stock file assumes.

That one fact explained everything. The stock SwapAxes 0 file could never have worked. And all those "mis-click" complaints were the calibrator correctly noticing my taps were moving along the wrong axis.

The data is reusable too — rotating the display just permutes these numbers, so you measure once and can recompute for any orientation.

Trap 3: evdev can only describe a rectangle

With SwapAxes 1 and calibration built from those corners, touch mostly worked. Except the bottom-right buttons stayed dead.

The corner numbers explain it. The bottom edge reads about 3090 on the left and about 3650 on the right. The touch surface isn't a clean rectangle — it's tilted. An evdev Calibration string is four edge values, which is a rectangle by definition, so any single number for "the bottom" is an average. Set it to 3650 and the right corner works while the left breaks. There's no value that fixes both.

The fix: libinput's calibration matrix

libinput's CalibrationMatrix is a full 3×3 affine transform, so it can express a tilt. Hand the device back to libinput and give it a matrix:

sudo rm -f /usr/share/X11/xorg.conf.d/45-evdev.conf

sudo tee /etc/X11/xorg.conf.d/99-calibration.conf > /dev/null << 'EOF'
Section "InputClass"
    Identifier "calibration"
    MatchProduct "ADS7846 Touchscreen"
    Driver "libinput"
    Option "CalibrationMatrix" "-1.186 -0.010 1.100 0.134 1.366 -0.240 0 0 1"
EndSection
EOF

sudo systemctl restart localdisplay

There's a nice irony here. libinput looked like the villain early on, silently swallowing every evdev option I threw at it. It was the right tool the whole time — I just wasn't speaking its language.

Those exact numbers are for my panel. Work yours out from your own corner measurements and then tune.

Tuning without restarting anything

This is the trick I wish I'd found three hours earlier. The matrix can be changed on a running X server, which turns a five-minute edit-restart-test cycle into a five-second one:

sudo DISPLAY=:0 xinput set-prop "ADS7846 Touchscreen" \
     "libinput Calibration Matrix" -1.186 -0.010 1.100 0.134 1.366 -0.240 0 0 1

Only three numbers in the middle ever need touching:

NumberSymptomWhat to do
6thEvery button needs you to tap slightly above itMake it more negative (roughly 3 px per 0.01)
5thBottom edge is off but the top is fineLower it in steps of 0.03
4thLeft half sits lower than the right halfDecrease it

Live changes disappear on reboot, so once you've found values you like, write them into the file and restart localdisplay once to make them stick.

And set your expectations properly: the middle of the screen gets accurate and every button becomes hittable, but the extreme corners may stay a few pixels out. A 3×3 matrix can't fully flatten a warped resistive film, and your corner taps have their own noise in them. Good enough really is good enough here.

Portrait or landscape?

I ran both for a while. Landscape won, but it was closer than I expected.

Portrait (320×480) is genuinely good. moOde's UI is responsive and mobile-first, so it stacks like a phone — cover art, track info, controls, all comfortably sized. If you only care about hitting buttons reliably, portrait is the better choice.

Landscape (480×320) compresses the layout and shrinks the touch targets, but it suited how the unit sits on my shelf and it reads better as a now-playing display from across the room. It's also the only sensible option if you ever want Peppy VU meters, which are built for wide screens.

Switching isn't free. You change rotate= in config.txt, Modes and Virtual in 99-fbdev.conf, and then you redo the touch calibration from scratch, because rotation remaps the axes. Rotation is a boot-time overlay parameter as well, so a service restart won't do it — you need a full reboot.

One handy shortcut while you're sorting orientation out: if the picture looks right but tapping bottom-left opens the top-right menu, that's a clean diagonal mirror. Both axes are inverted, 180° out. Reverse both pairs of calibration numbers and you're done — no reboot needed.

Living with it

Honest notes after using it for a while on a 3A+ with 512 MB:

  • Redraw is about 10–15 fps. SPI is slow. For a now-playing screen that's completely fine. It is not slick.
  • Chromium swaps. First launch is genuinely slow. Be patient rather than assuming something's broken.
  • The volume knob is a drag-rotate gesture, which is about the hardest thing you can ask of a cheap resistive panel. Taps land far more reliably than circular drags. moOde's CoverView mode (toggle on that same Peripherals page) gives you big tap-friendly controls designed for small local displays — worth trying if the full UI feels cramped.
  • Keep a copy of your config changes. moOde updates can rewrite config.txt. If the screen dies after an update, re-append the Step 7 block. Recovery from a bad edit is easy too — the boot partition is readable on any PC, so you can restore config.txt.bak straight from the SD card.

And the big one: if you're buying another display, get HDMI or DSI. SPI panels need this whole driver dance, on a software stack that keeps shifting underneath them. The screen costs less; the evening costs more.

The short version

If you remember five things from all of this:

  1. Never run LCD35-show. Take tft35a-overlay.dtb from the repo and ignore everything else in it.
  2. fbcp and vc4-fkms-v3d are both dead ends now. Comment out vc4-kms-v3d and point X straight at /dev/fb1.
  3. moOde runs xinit itself. No LightDM, no Wayland switch, and those lightdm.conf sed errors are noise.
  4. The checkpoint is fb1 existing — not text appearing on the screen.
  5. For touch: skip xinput_calibrator, measure four corners with evtest, and use a libinput calibration matrix if your panel turns out to be tilted.

It's a fiddly little project, but the end result is a self-contained music player with a proper now-playing screen, and that's a genuinely nice thing to have on a shelf.

Sources and further reading

These are the pages that actually helped, plus the ones that back up the claims I've made about what's dead and why.

  • goodtft/LCD-show issue #396 — the closest thing to a working moOde recipe that existed before this post. Written for Bookworm on a Pi 4B, so the package list and a few paths have drifted, but it's where the "point X at fb1 instead of mirroring" idea came from.
  • goodtft/LCD-show — the repo you clone for tft35a-overlay.dtb. Worth a look inside the scripts to see exactly how much of your config.txt they'd overwrite.
  • "FBCP is never going to be compatible with Raspberry Pi OS beyond Bullseye" — a Raspberry Pi engineer saying it plainly on the official forums. If you only read one link to understand why the old guides fail, make it this one.
  • juj/fbcp-ili9341 — the faster fork of fbcp. Its README now opens by declaring the project over, explains that DispmanX is obsolete from Pi 5 onward, and notes it never worked on 64-bit systems anyway. Straight from the author.
  • Raspberry Pi forums: 3.5" 480x320 XPT2046 on Bookworm — worth reading for the alternative approach. A Pi engineer suggests skipping the goodtft driver entirely and using the in-kernel piscreen DRM overlay instead. I didn't go that route, but if you'd rather have a proper DRM device than the fbdev setup above, start there.
  • moOde releases on GitHub — release notes for every version, including which RaspiOS base and kernel each one ships with. Useful for checking whether an update is about to move your ground.
  • moodeaudio.org — the project itself, plus the forum where moOde-specific display questions actually get answered.

Comments

lokesh said…
Becoming a full stack developer requires more than learning a single programming language. Understanding frontend development, backend programming, databases, APIs, version control, and project workflows can help learners develop a broader technical skill set. Those researching a full stack python developer course in electronic city bangalore may want to compare the practical components included in each program.

Popular posts from this blog

Configure PostgreSQL and phpPgAdmin in WAMP

Angular - 4 year road map

Angular 4 - Modules, components and routing