Mainline Linux + Arch Linux ARM on the Samsung Galaxy S20 FE 5G Snapdragon
(codename r8q, SoC SM8250 "kona" / Snapdragon 865), booted through
Mu-Silicium UEFI — not a
downstream Android kernel, the real mainline kernel.
Everything here exists so that anyone with their own r8q can reproduce it from clean upstream sources: the device-tree change that lights the panel, the kernel config, the switch-root initramfs, the systemd services, and a small set of scripts that take you from a UEFI flash to a booting Arch install you can SSH into.
Read
PREREQUISITES.mdfirst (unlocked bootloader, host packages, and the fact that this wipes your userdata), then followINSTALLATION.md.
No proprietary firmware is shipped. Storage, USB and display run on built-in mainline drivers with no firmware at all. GPU acceleration needs the Adreno 650 firmware from linux-firmware (
a650_sqe.fw,a650_gmu.bin) plus the Samsung-signed zap shader from your own device's stock firmware (Samsung's TrustZone only accepts Samsung's signature) — seeINSTALLATION.md§9.
| # | Milestone | State |
|---|---|---|
| 1 | Mainline kernel boots — BusyBox/switch-root initramfs, output on the phone screen | ✅ |
| 2 | UFS storage — all LUNs and the full GPT show up as /dev/sd* (first try, no hacks) |
✅ |
| 3 | Display — a persistent, live mainline framebuffer console on the AMOLED panel | ✅ |
| 4 | Arch Linux ARM + systemd on userdata, root autologin on the panel (tty1) |
✅ |
| 5 | SSH over USB — ssh root@172.16.42.1, plus USB tethering (internet + pacman) |
✅ |
| 6 | GPU acceleration — Adreno 650 via freedreno/turnip (GLES 3.2 + Vulkan 1.3) | ✅ |
| 7 | GNOME 50 on the panel — gdm autologin, mutter rendering on the Adreno (sway still available as a fallback) | ✅ |
| 8 | Touchscreen — STM FTS5CU56A multitouch, working under Plasma Mobile and GNOME | ✅ |
| 9 | Faster boot — console loglevel=3 (dropped ignore_loglevel) + a printk sysctl the initramfs can't override; ~12 s to userspace |
✅ |
| 10 | Volume-Up key — remapped to pm8150l_gpios gpio3, emits KEY_VOLUMEUP |
✅ |
| 11 | Battery — MAX77705 fuel gauge (max17042) telemetry + charging (max77705 MFD + charger) |
✅ |
| 12 | Wi-Fi — QCA6390 over PCIe via ath11k_pci + MHI; wlp1s0 scans and associates |
✅ |
| 13 | KDE Plasma Mobile — sddm autologin → mobile shell on the Adreno, ~12 s cold boot (GNOME still installed as a fallback) |
✅ |
| 14 | CPU wedge root-caused — the "phone dies under load" bug is cpu7 never returning from cpu-sleep-1-0 power collapse; that idle state is now disabled on cpus 4-7 |
✅ fix holding (first 50 min session clean; longer soak still welcome) |
See the Roadmap below for what's next (Bluetooth, USB host mode, audio, a greeter/lock screen).
Mu-Silicium is
flashed to BOOT; the ESP is the cache partition reformatted vfat (R8QESP);
the rootfs is the userdata partition; the kernel is mainline Linux 7.1.2.
r8q-arch/
├── README.md ← you are here
├── PREREQUISITES.md ← read first (bootloader, host packages, data wipe)
├── INSTALLATION.md ← the step-by-step
├── dts/ ← display fix, touchscreen, battery and QCA6390 Wi-Fi nodes (sm8250-samsung-common.dtsi + r8q.dts)
├── patches/ ← kernel patches (GPU zap-shader; i2c FIFO force; FTS5CU56A touch driver)
├── config/ ← r8q_bringup.config, cmdline.txt
├── initramfs/ ← switch-root /init (+ irfs.devnodes)
├── rootfs/ ← systemd services, networkd, Plasma Mobile/sddm wiring, cpuidle workaround, gadget script, GPU + touch + battery + Wi-Fi services
└── scripts/ ← build-uefi / flash / deploy-esp / install-arch / host-tether / build_kernel
Download mode → heimdall flashes Mu-Silicium UEFI (with our DTB embedded)
to BOOT → UEFI auto-runs \EFI\BOOT\BOOTAA64.EFI (our kernel Image, EFI-stub,
with an embedded switch-root initramfs) → the initramfs mounts userdata and
switch_roots into Arch Linux ARM / systemd → NCM USB gadget comes up →
ssh root@172.16.42.1.
The DTB lives inside the firmware (Mu-Silicium exposes it as an EFI config
table via DtPlatformDxe), so DTB changes need a re-flash; the kernel Image
lives on the ESP and is swapped over mass-storage mode.
There is no mainline panel driver yet, so display and GPU are split: the panel
keeps scanning out of the firmware-lit simple-framebuffer (card0), while the
msm module is loaded post-boot with separate_gpu_kms=1 and provides only the
Adreno 650 render node (renderD128). Compositors render on the GPU and
blit into the dumb buffer.
Two r8q-specific things make the GPU actually render (both in
patches/):
- Samsung's TrustZone rejects the generic zap shader from linux-firmware, and the GPU then stays locked in secure mode — every render write is silently dropped. You need the Samsung-signed zap from your own stock firmware.
- Loading that zap into the DT carveout hard-resets the SoC (Samsung's TZ
rejects the region); the
msm.r8q_zap_dyn=1patch loads it into dynamically allocated RAM instead, exactly like stock Android'spil-tz-genericdoes.
rootfs/ ships the pieces: r8q-gpu.service (loads msm after boot — never
at coldplug, and never unload it), the modprobe options, and the desktop
wiring for whichever session you run. The drop-in ordering the display manager
after r8q-gpu.service matters either way: compositors choose their render
device once at startup, so if the display manager wins that race the whole
session silently runs on the CPU.
- KDE Plasma Mobile (default) —
sddmautologin intoplasma-mobile.desktop. KWin needs no GPU glue: it has a real split display/render-device concept, treats everyDRM_BUS_PLATFORMnode as compatible and prefers the one that isn't a software renderer, so it pairscard0(simpledrm) withrenderD128(Adreno) by itself. It also derives scale 2.7 from the panel's DPI, which works only because the connector is DSI and the DT carrieswidth-mm/height-mm. - GNOME (fallback, still installed) — needs the
mutter-device-preferred-primaryudev tag so mutter renders on the Adreno and scans out on simpledrm, plus a gdm drop-in and gdm autologin. Switch back withsystemctl disable sddm && systemctl enable gdm. - A tty1 autologin profile that starts sway on the GPU is kept as a second fallback.
Until this was found, the phone would stop dead under sustained rendering. It is
not a display or GPU fault: cpu7 enters the cpu-sleep-1-0 power-collapse state
and never comes back out, so every later kick_all_cpus_sync() — the BPF JIT's
text poking hits this constantly via systemd's cgroup BPF — blocks forever waiting
for a core that will not answer even an NMI. RCU says so directly, reporting cpu7
with an even dynticks counter (idle=…/0x4000000000000000), meaning it believes
that core is idle.
The tell is distinctive: the kernel keeps answering ICMP while userspace stops
entirely — ping stays at ~2 ms but sshd cannot emit its version banner. The
workaround disables idle state1 on cpus 4-7 only; cpus 0-3 use a different state
and keep their power collapse. Same family as the s2idle hard reset — this SoC's
low-power paths are not trustworthy under mainline.
The STM FTS5CU56A touch controller sits on i2c5 (qupv3_se5), powered by
a fixed LDO on TLMM GPIO 92 that the firmware leaves off. No mainline driver
speaks its Samsung-TSP protocol, so patches/fts5cu56a.c is a small from-scratch
driver (device-ID handshake, system reset, the touchtype/scan-on command
sequence, and the 16-byte FIFO event format). Two quirks make the bus usable:
the SE5 GPI-DMA channels are TrustZone-owned and its FIFO writes hang, so
patches/0003 adds i2c-qcom-geni.r8q_force_fifo=1 (skip GPI, route data
through the SE-DMA path). r8q-touch.service loads the stack after boot.
Touch follows display rotation (portrait and landscape both land correctly). That took making the display look like a real built-in panel — see below.
The panel is scanned out by simpledrm from the framebuffer the UEFI lit — there is no real mainline display driver yet. Two small touches make it behave like the built-in panel it is:
patches/0006makes simpledrm advertise a DSI connector instead ofUnknown. mutter decides "is this a built-in panel?" purely from the connector type (eDP/LVDS/DSI/DPI), and only a built-in panel gets an integrated touchscreen mapped to it with the output rotation transform applied — so this is what makes touch follow rotation. A DTpanelnode (width-mm/height-mm) gives it a real physical size so it stops showing up as a 27″ display.- Suspend is disabled on purpose. s2idle hard-resets this SoC, so the systemd
sleep targets are masked; and now that a battery gauge and a built-in display
exist, GNOME would otherwise idle- or critical-battery-suspend into that reset.
A locked system dconf default (
rootfs/etc/dconf/…) forces every power action tonothingand disables idle detection.
The phone's charger and fuel gauge live in a Maxim MAX77705 companion PMIC on
i2c0 (qupv3_se0), which rides the same r8q_force_fifo=1 geni path as the
touchscreen. Three i2c slaves matter: the ModelGauge fuel gauge at 0x36, the top
PMIC/MUIC at 0x66, and the charger at 0x69.
- Telemetry (read-only): the fuel gauge is register-compatible with the
mainline
max17042_batterydriver (compatible = "maxim,max77705-battery"), so the DT just adds afuel-gauge@36node — no new driver./sys/class/power_supply/max170xx_batteryreports capacity, voltage, current and temperature. - Charging: the
max77705MFD (pmic@66) +max77705-charger(charger@69) drivers, with asimple-batterynode supplying the charge parameters, drive the charger so the phone actually refills (max77705-chargershowsstatus = Charging). This unit's MAX77705 reports silicon revision PASS2, which mainline's MFD rejects (it only whitelists PASS3), sopatches/0005relaxes that check — the register layout is identical on PASS2.
r8q-battery.service loads all three modules after r8q-touch.service brings the
bus up. The fuel-gauge node is deliberately left IRQ-less so read-only telemetry
keeps working even if the charger/MFD ever fail to probe.
One caveat worth knowing: mainline has no driver for the MAX77705's MUIC, so the
port type is never detected and the charger stays at the USB-SDP default of
500 mA. A running GNOME session draws more than that, so the pack slowly
discharges while plugged in even though the charger reports Charging. The
service raises input_current_limit to 700 mA on start, which makes the net
battery current positive again. Raise it further at your own risk — it is the
host port, not the phone, that has to supply it.
Wi-Fi is a discrete Qualcomm QCA6390 on PCIe (not the on-SoC WCSS), driven
by ath11k_pci + MHI. Nothing needs to be enabled in the kernel config — ATH11K,
MHI, QRTR, PCIE_QCOM, PCI_PWRCTRL_PWRSEQ and POWER_SEQUENCING_QCOM_WCN are
all in arm64 defconfig — and the firmware ships in linux-firmware
(/lib/firmware/ath11k/QCA6390/hw2.0/). The work is all device tree: a
qca6390-pmu that power-sequences the chip and drives WLAN_EN (tlmm gpio90) /
BT_EN (gpio76), &pcie0 + &pcie0_phy, a wifi@0 endpoint under &pcieport0,
and the PMIC rails those need (vreg_s5a_1p9, vreg_s6a_0p95, and a pm8150l
regulators-1 node for vreg_s8c_1p35).
Two things will cost you a lot of time if you don't know them:
PHY_QCOM_QMP_PCIEis a module. Untilphy-qcom-qmp-pcie.kois loaded,1c00000.pciesits silently in/sys/kernel/debug/devices_deferred— the host bridge defers on the missing phy and prints nothing at all.qrtr-mhi.komust be installed and loaded. MHI creates anIPCRchannel device when the chip enters mission mode, andqrtr-mhiis what binds to it to carry QMI. Without it, ath11k stops atWait for device to enter SBL or Mission modeand never says anything else — which reads exactly like a firmware that won't boot, even though the firmware is already running. If you hit that, buildmhi.kowithCONFIG_MHI_BUS_DEBUG=yand read/sys/kernel/debug/mhi/*/regdump:BHI_EXECENV: 0x2means the device is in mission mode and the problem is above MHI, not in the firmware.
r8q-wifi.service loads the stack deliberately, in order, after the geni bus is
up: phy-qcom-qmp-pcie → pwrseq-qcom-wcn → pci-pwrctrl-pwrseq → wait for the
endpoint → qrtr-mhi → ath11k_pci. r8q-wifi-blacklist.conf stops udev from
coldplugging any of it at ~9 s, which otherwise races the touch/battery bring-up
on the same geni controller and produces SE0 i2c timeouts and MAX77705 IRQ storms.
Association is NetworkManager, so you can join a network from GNOME's own
Wi-Fi menu on the touchscreen instead of editing config over SSH. The one thing
that must be right is NetworkManager/conf.d/10-r8q.conf: it marks usb0
unmanaged, because that interface is the SSH lifeline and is configured
statically by systemd-networkd — let NM take it over and it will reconfigure the
interface out from under your session. dns=none for the same reason (the static
/etc/resolv.conf is what the USB tether relies on).
Note that the MAC address is random on every boot: this unit reports
board_id 0xff and no calibration data, so ath11k has no stored address to use.
Two things worth knowing about the log noise Wi-Fi produces:
- AER correctable errors. With PCIe ASPM enabled, the QCA6390 misses replay
deadlines coming out of L0s/L1 and the link reports a stream of
Data Link Layer ... [12] Timeoutcorrectable errors whenever Wi-Fi goes active. They are harmless — the packet is retransmitted — but they flood the log. Measured over three scans: 6 with ASPM on, 0 with it off.r8q-wifi.servicetherefore disables ASPM on that one link, and it has to do so afterphy0appears: ath11k disables ASPM for the firmware download and then callsaspm_restore, which puts it back. - They were reaching the panel because of the initramfs.
/initraises the console log level to 8 so early boot lands in the ESP logs, and nothing lowers it again — sologlevel=3on the cmdline is overridden for the whole session and every kernel message gets CPU-blitted onto the framebuffer.sysctl.d/50-r8q-printk.confputs it back to 4 once userspace is up. That is worth more than quiet: it took boot from 21 s back down to ~12 s, the same lever as the originalloglevel=3change.
The device boots to an accelerated KDE Plasma Mobile session you can touch and SSH into. What's left, roughly in the order it's worth doing:
| Goal | What it needs | Notes |
|---|---|---|
| Bluetooth | QCA6390 BT (hci_qca over UART/serdev) |
Same chip as Wi-Fi, and the qca6390-pmu in the DT already drives its BT_EN line — so the power sequencing is done and this is the cheapest remaining win. |
| USB host mode | dwc3 role switch + VBUS (pm8150b regulator or powered OTG hub) |
Currently peripheral-only (that's how SSH works). Host mode gets a real keyboard/mouse. Needs a role-switch path and VBUS supply. |
| Greeter / lock screen | Replace sddm autologin with a real login | Plasma Mobile already ships plasma-keyboard as an on-screen keyboard, so a headless login is now viable. Pure userspace/config work; no kernel changes. |
| Audio | LPASS + WCD938x codec + cs35l41 speaker amps (SoundWire) |
The hardest mainline bring-up here; lowest priority for a dev device. |