Skip to content

evnchn-agentic/q506

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Fujitsu Q506 Touchscreen Fix

Fixing a tablet's broken touchscreen — from zero GPIO interrupts to a working Alpine Linux tablet, using an AI agent over SSH and raw MMIO register manipulation.

Built through agentic coding with Claude Code. The AI operated the tablet remotely via SSH while the human touched the screen on command. The conversations ran 36,000+ lines (96MB) across 3 sessions over several days, with multiple context window exhaustions requiring automated summarization.

Fujitsu Q506 tablet running Alpine Linux with a working touchscreen — drawing test in Firefox


TL;DR — For Q506 Owners

Your ELAN touchscreen doesn't work because Gpio_Light_Mode (bit 7) is cleared in GPIO pin 77's pad configuration register, putting the pad in a low-power mode that blocks interrupt delivery. The BIOS intentionally leaves this for the OS driver to set — Windows' Intel GPIO driver does; Linux's pinctrl-cherryview.c doesn't. The touchscreen worked on older kernels (~5.0–5.4) through a mechanism still under investigation. The hardware is fine.

Quick Fix (Alpine Linux)

1. (Optional) Install devmem2apk add devmem2 (Alpine) or equivalent. The script auto-detects devmem, devmem2, or falls back to dd + od if neither is available.

2. Add iomem=relaxed to your kernel command line (in /etc/default/grub, then grub-mkconfig). This is needed because the fix writes directly to MMIO registers via /dev/mem.

3. Deploy the GPIO fix script — copy fix_gpio_pad.sh to /usr/local/bin/.

4. Install the boot service — copy fix-touchscreen-gpio to /etc/init.d/ and enable it:

chmod +x /etc/init.d/fix-touchscreen-gpio
rc-update add fix-touchscreen-gpio boot

5. Reboot. Verify with:

grep 04F3 /proc/interrupts    # should show non-zero interrupt count

The fix runs a short shell script at every boot to set one bit in a single MMIO register. That's it. No DSDT override needed on Linux.

For other distros

The fix is the same concept — you just need a different init system wrapper. On Kubuntu 24.04 (kernel 6.17), CONFIG_IO_STRICT_DEVMEM is not set, so devmem2 works without iomem=relaxed. A systemd oneshot service replaces the OpenRC script:

# /etc/systemd/system/fix-touchscreen-gpio.service
[Unit]
Description=Fix touchscreen GPIO pad register (pin 77 bit 7)
After=local-fs.target

[Service]
Type=oneshot
ExecStartPre=-/bin/sh -c "echo i2c-04F3200A:00 > /sys/bus/i2c/drivers/i2c_hid_acpi/unbind"
ExecStart=/usr/bin/devmem2 0xFED9D810 w 0x50D08281
ExecStartPost=/bin/sleep 0.5
ExecStartPost=-/bin/sh -c "echo i2c-04F3200A:00 > /sys/bus/i2c/drivers/i2c_hid_acpi/bind"
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

On distros where CONFIG_IO_STRICT_DEVMEM=y, add iomem=relaxed to the kernel cmdline. The fix script auto-detects the best available memory access tool (devmem / devmem2 / dd).


What This Is

A complete fix for the Fujitsu Stylistic Q506/ME (ARROWS Tab) tablet's touchscreen, which doesn't work in any operating system despite being fully functional in the BIOS setup menu.

The Fujitsu Stylistic Q506/ME (ARROWS Tab) is a 2016-era Windows tablet with:

  • Intel Atom x5-Z8550 (Cherry Trail / Braswell SoC)
  • 10.1" 1920x1200 display (DSI panel)
  • ELAN touchscreen (I2C HID, VID 04F3, PID 2299, ACPI ID 04F3200A)
  • 4GB RAM, 128GB eMMC
  • Insyde H2O UEFI BIOS

Root cause: Gpio_Light_Mode (bit 7) of GPIO pin 77's pad register (Cherry Trail Southeast community) is cleared, putting the pad in a low-power "light mode" that degrades the input buffer and prevents the ELAN touch controller's interrupt signal from reaching the GPIO controller. Confirmed via GRUB memrw: the register reads 0x50D08201 (bit 7 = 0) before the kernel loads on a cold boot. This is by design — Intel's UEFI reference code marks Gpio_Light_Mode as NA (don't configure) for the touchscreen interrupt pin, deferring it to the OS GPIO driver. Windows' Intel GPIO driver sets this bit during initialization; Linux's pinctrl-cherryview.c has no definition for it and never sets it. The touchscreen worked on kernels ~5.0–5.4 (Ubuntu 19.04, Ubuntu 20.04); the exact mechanism is still under investigation.

The fix: fix_gpio_pad.sh — a POSIX shell script that reads the pad register, sets bit 7, and writes it back. Auto-detects devmem / devmem2 / dd as the memory access backend.

To make it permanent on Alpine Linux:

  • Copy fix_gpio_pad.sh to /usr/local/bin/
  • Copy fix-touchscreen-gpio to /etc/init.d/ and run rc-update add fix-touchscreen-gpio boot
  • Add iomem=relaxed to kernel command line in /etc/default/grub, then grub-mkconfig -o /boot/grub/grub.cfg

Note: No DSDT override is needed on Linux. The stock DSDT works fine — the kernel's i2c_hid_acpi driver binds correctly. A minimal DSDT fix was created during the investigation for Windows compatibility (see Phase 5), but it turned out to be unnecessary for Linux.

                    ┌──────────────────────────────────────┐
                    │       Fujitsu Q506/ME Tablet          │
                    │                                      │
                    │  ELAN Touchscreen ──I2C──► i2c-4     │
                    │       │                              │
                    │  GPIO pin 77 (interrupt)             │
                    │       │                              │
                    │  BIOS skips bit 7  ──► NO INTERRUPTS │
                    │  Fix sets bit 7    ──► INTERRUPTS!   │
                    │                                      │
                    │  SSH server ◄──────────── WiFi       │
                    └──────────────────┬───────────────────┘
                                       │
                              ┌────────┴────────┐
                              │  MacBook (SSH)   │
                              │  Claude Code     │
                              │  "Touch the      │
                              │   screen NOW"    │
                              └─────────────────┘

The Journey

Phase 1: Linux First Attempt — Chasing the Wrong Layer

Session 1 (554ea414) — The longest and most exploratory phase.

Alpine Linux 3.23 was already installed with kernel 6.18. The AI SSH'd in and set up LXQt desktop environment with SDDM display manager, Intel modesetting graphics, and autologin. SDDM's greeter displayed on the DSI-1 panel, but the LXQt user session failed to launch — startlxqt needed dbus-launch, provided by the dbus-x11 package. After installing it, the full desktop came up. Screenshots confirmed via scrot over SSH.

The tablet also has a Wacom digitizer (WCOM0016:00) in addition to the ELAN touchscreen — investigated briefly as a potential conflict source, but ultimately unrelated to the interrupt problem.

The Standard Cherry Trail Fix — Didn't Work

The reference guide suggested reloading i2c_hid_acpi to work around a Cherry Trail I2C PUNIT semaphore race condition. This is a well-known fix for many Cherry Trail tablets. It didn't help here. The touchscreen was detected, the driver bound to it, but evtest showed zero events.

Chasing HID Driver Issues

dmesg: "device did not ack reset within 1000 ms"
dmesg: "device returned incorrect report (9 vs 10 expected)"
dmesg: "failed to fetch feature 10"

These errors consumed significant debugging effort:

  • Tried HID_QUIRK_NOGET — no change
  • Blacklisted hid_multitouch with install hid_multitouch /bin/false, forced hid-generic — still no events
  • Conclusion: Wrong layer. The problem wasn't in HID parsing.

The Smoking Gun: Zero Interrupts

$ grep 04F3 /proc/interrupts
 126:    0    0    0    0  chv-gpio  77  04F3200A:00

Zero interrupts across all CPUs. The kernel never received a single touch event notification. The GPIO interrupt for the touchscreen had never fired — not once.

Going Deeper: GPIO Investigation

  • Exported GPIO 787 (GPO3 base 710 + pin 77), set edge to falling — driver rebind overwrites this
  • Full GPIO pin snapshot before/after touch — no pin changed state on any controller
  • Tried DSDT GpioInt modification: Level → Edge, Shared → Exclusive — loaded via initramfs CPIO override (CONFIG_ACPI_TABLE_UPGRADE), still 0 interrupts
  • x86_android_tablets kernel module — no DMI match for Fujitsu Q506

Proof the Hardware Works: I2C Polling

Direct I2C communication proved the touch controller was alive:

I2C bus: i2c-4, address: 0x10
HID descriptor: VID=04F3, PID=2299, max input=67 bytes

After a specific init sequence (hardware reset via GPIO + SET_POWER ON, without HID RESET), polling the input register captured actual touch data:

t=116: 0x3f 0x00 0x01 0x0a 0x03 0x02 0x02 0xdb 0x02 0xdb

The hardware was functional all along. It just couldn't deliver interrupts via GPIO.

Attempted Workarounds

  1. Userspace polling daemon — A C program (elan-touch-poll) that polled I2C and forwarded data via UHID. Compiled and ran, but never fully debugged.
  2. Patched kernel module — Added polling support to i2c-hid-core.c. Built successfully but couldn't load due to kernel module CRC/signing enforcement.
  3. Standalone i2c_hid module (coiby/standalone_i2c_hid) — Too old for kernel 6.18, build failed.

Session 1 ended unresolved — the user interrupted the polling daemon debugging mid-stream. The session also suffered two context window exhaustions requiring automated summarization, and a WiFi/SSH connectivity crisis when the tablet's IP changed after a DSDT-override reboot (requiring double-hop SSH routing through a gateway machine).


Phase 2: BIOS Update — When SMM Says No

Session 2 (99be5cb5) — The user wanted to update the BIOS first, hoping a firmware bug was the root cause.

Finding the BIOS

The tablet was running BIOS v2.16 (from 2016). Found v2.34 on a third-party driver site, then located the official download URL on support.fujitsu-pc-asia.com.

The BIOS update was packaged as BIOSWindows_Q506_ST_2.34.exe — an Insyde H2O capsule containing the new 4MB BIOS region.

flashrom: Blocked by SMM

$ flashrom -p internal --ifd -i bios -w new_bios_region.bin
Warning: BIOS region SMM protection is enabled!
Warning: Setting BIOS Control at 0x0 from 0x22 to 0x01 failed.

Intel Braswell's System Management Mode (SMM) hardware write protection blocked all direct SPI flash writes. flashrom could read the chip but not write to it.

Failed Flash Attempts

Attempt Method Result
1 flashrom -p internal --ifd -i bios -w SMM Write Protection
2 UEFI Capsule-on-disk (EFI system partition) Firmware ignored it
3 Wrapped capsule with EFI_CAPSULE_HEADER Still ignored
4 fwupd with ESP detection Could not flash — no UEFI UpdateCapsule support
5 WinPE boot to run the EXE 0xc000000f — boot configuration data missing

What Worked: Framework Laptop's H2OFFT

The breakthrough came from the user's suggestion to try the EFI shell approach. The Insyde H2OFFT (Hardware-to-Firmware Flash Tool) has an EFI shell version (H2OFFT-Sx64.efi). We found a copy inside a Framework Laptop BIOS update package on GitHub — Insyde's tool is cross-compatible across all Insyde H2O BIOSes.

Steps:
1. Downloaded H2OFFT-Sx64.efi and Shell.efi from Framework's BIOS package
2. Placed both + the BIOS capsule on a FAT32 partition
3. Created startup.nsh: H2OFFT-Sx64.efi isflash.bin
4. Added GRUB chainloader entry for the EFI shell
5. Booted into EFI shell → H2OFFT flashed successfully

BIOS updated to v2.34 — confirmed via dmidecode. The touchscreen still didn't work (the BIOS update wasn't the root cause), but having a current BIOS was good hygiene.


Phase 3: The Windows Detour

Why Windows?

The touchscreen worked in the BIOS menu. The user reasoned that Windows with Fujitsu's official drivers might have special sauce that Linux was missing. This turned out to be a long detour, but it produced important DSDT insights.

Partition Surgery

The tablet had a 128GB eMMC with Alpine on a ~111GB partition. Shrinking it for a Windows partition was surprisingly hard:

  • pivot_root to RAM: SSH session hung — the SSH daemon was running on the old root
  • Custom initramfs with resize tools: Keyboard didn't work on the tablet in the initramfs shell (no USB HID drivers in initramfs)
  • Solution: Booted Alpine from USB stick, resized from the live environment

Final layout: 512MB ESP + 4GB swap/WinPE + 16GB Alpine + ~96GB Windows

Windows Installation Attempts

Attempt Method Result
1 wimapply from Linux Missing NTFS security descriptors, wouldn't boot
2 BCD hand-crafted via hivex 0xc000000f / 0xc0000225 errors (wrong device element format)
3 Multiple BCD format iterations (hivexsh, ctypes, locate) Still failed
4 Normal USB installer via GRUB chainload Worked

The key insight for attempt 4: reformatting the USB as GPT + FAT32 (via Rufus) allowed GRUB to chainload the Windows installer's boot manager. The user then ran bcdboot from the installer's command prompt, and Windows installed normally.

GRUB was configured to inject the fixed DSDT before chainloading Windows Boot Manager, preventing the ACPI_BIOS_ERROR blue screen.


Phase 4: Windows Touchscreen — Everything Says OK, Nothing Works

Windows 10 LTSC 2021 booted successfully with the fixed DSDT. Device Manager showed:

  • ACPI\04F3200A (ELAN I2C HID Device) — Started, no errors
  • HID\04F3200A&Col01 (HID-compliant touch screen) — Started
  • All I2C host controllers — Started

All Fujitsu drivers were downloaded from the Asia CDN and installed:

  • Intel chipset, ISS (sensor framework), TXE
  • ELAN pointing device (turned out to be for USB touchpad, not I2C touchscreen)
  • WiFi, Bluetooth, audio, BIOS driver, FUJ02E3, SysExt

GetSystemMetrics(SM_DIGITIZER) returned 0 — Windows didn't even think a digitizer was present. Despite all devices showing "OK", touch input never worked.


Phase 5: The Minimal DSDT — A Lesson in Not Over-Fixing

The Over-Fix Problem

The "fixed" DSDT that was deployed had been through an iasl decompile/recompile cycle. During this process, vendor-specific ACPI method names were changed to their standard equivalents:

Original (Fujitsu) After roundtrip Purpose
CDEP _DEP Device dependencies
CPR0 _PR0 Power resources for D0
SAEI _AEI GPIO event interrupts (GPO1)

These renames happened during the decompile/recompile roundtrip. The SAEI → _AEI rename was particularly damaging: it changed how Windows enumerated GPIO event handlers on GPO1 (the same controller that has the touchscreen's reset pin).

The Minimal DSDT

Created a new DSDT with only the actual compilation errors fixed. All vendor-specific names (CDEP, CPR0, SAEI) were preserved exactly as-is. The four fixes (raw diff):

DSDT diff (4 fixes)
--- dsdt_original.dsl
+++ dsdt_minimal.dsl

 # 1. Added _HID method to SDHB (SD host controller B)
 # 2. Added _HID method to SDHC (SD host controller C)
 #    (identical change in both devices)
@@ Device (SDHB) / Device (SDHC) @@
+            Method (_HID, 0, NotSerialized)
+            {
+                If ((WHRV == One))
+                {
+                    Return (WHID)
+                }
+                Return (AHID)
+            }

 # 3. Renamed YHID to _HID in GPS0 (genuine error, not vendor-specific)
@@ Device (GPS0) @@
-                Name (YHID, "BCM47521")
+                Name (_HID, "BCM47521")  // _HID: Hardware ID

 # 4. Changed device reference to method call
-                    Local1 = WIDR /* \_SB_.PCI0.I2C7.WIDR */
+                    Local1 = PSRC ()

The Result

With the minimal DSDT on Windows:

  • GetSystemMetrics(SM_DIGITIZER) jumped from 0 to 193 (bits: integrated + ready + connected, max 10 touches)
  • Windows now knew a touchscreen existed!
  • But touch still didn't work — the GPIO pad mode problem remained

This was a critical clue: the over-aggressive DSDT renaming had been hiding the touchscreen from Windows entirely. But even with proper enumeration, the hardware-level interrupt problem persisted.

In hindsight, the DSDT fix was only needed for Windows. On Linux, the stock DSDT works fine — the kernel's i2c_hid_acpi driver binds correctly regardless. The entire DSDT investigation was a valuable detour that deepened understanding of the problem, but the final Linux fix requires no DSDT override at all.


Phase 6: Back to Linux — Finding the Root Cause

The user decided to focus on Linux: "it's slow to boot and annoying to use, ngl" (about Windows).

The GPIO Pad Config Registers

With iomem=relaxed in the kernel command line, Python could read Cherry Trail GPIO pad configuration registers via /dev/mem:

# Southeast community base: 0xFED98000
# Pin 77 pad config at offset 0x5810
padcfg0 = 0x50d08201

Decoding the register:

GPIOTXSTATE  = 1
GPIORXSTATE  = 0 (input value: LOW — no touch)
GPIOTXDIS    = 0 (output enabled — wrong for an input-only interrupt pin)
GPIORXDIS    = 1 (input DISABLED — !)
PMODE        = 1 (NATIVE FUNCTION MODE — not GPIO!)
INTSEL       = 13 (APIC routing)
RXINV        = 0
RXEVCFG      = 2 (disabled)

PMODE=1: The pin was in native function mode. It was NOT configured as a GPIO pin. The kernel's GPIO subsystem requested it for interrupt use, and the pinctrl driver happily registered it, but the hardware pad was ignoring GPIO-level signals entirely.

Finding the Correct MMIO Offset

This was its own adventure. The initial offset calculation was wrong:

Attempt Offset Source Result
1 0x45f8 Family 5 base 0x45e8 + 2×8 (stride 8) 0xFFFFFFFF (unmapped)
2 0x4608 Family 5 base 0x45e8 + 2×16 (stride 16) 0xFFFFFFFF (unmapped)
3 0x5810 Empirical scan of entire MMIO space Found: 0x50d08201

The family base offsets in the actual hardware are at 0x400 boundaries (0x4400, 0x4800, 0x4C00, 0x5000, 0x5400, 0x5800), not the values listed in the kernel source comments. Pin 77 = family 5 base 0x5800 + index 2 × 8 = 0x5810.

The scan script (scan_pads.py) that found the correct offset read all non-zero/non-0xFFFFFFFF values in the 0x4400–0x5000 range, then did a full 0x0000–0x8000 scan looking for the known value 0x50d08201.


Phase 7: The GPIO Pad Fix — Victory

# fix_gpio_final.py
SE_BASE = 0xFED98000
PAD_OFFSET = 0x5810  # Pin 77

padcfg0 = read_register(SE_BASE + PAD_OFFSET)
# Before: 0x50d08201 (PMODE=1, native mode)

new = padcfg0 & ~(0x7 << 9)   # Clear PMODE bits [11:9] → GPIO mode
new |= (1 << 7)                # GPIOTXDIS=1 (we want input only)
new &= ~(1 << 8)               # GPIORXDIS=0 (enable input)

write_register(SE_BASE + PAD_OFFSET, new)
# After: 0x50d08080 (PMODE=0, GPIO mode, input enabled)

The results were immediate:

# BEFORE fix:
 126:          0     0     0     0  chv-gpio   77  04F3200A:00

# AFTER fix:
 126:       2254     0     0     0  chv-gpio   77  04F3200A:00

2,254 interrupts. After days of zero. The user touched the screen and confirmed: "things works now."


Phase 8: Making It Permanent

Boot Service (Alpine Linux)

The GPIO pad fix must run at every boot because the BIOS resets PMODE=1 during POST.

/usr/local/bin/fix_gpio_pad.sh — The fix script (sets bit 7 of pin 77's pad register)

/etc/init.d/fix-touchscreen-gpio — OpenRC init script:

#!/sbin/openrc-run
description="Fix touchscreen GPIO pad register"
depend() { after localmount; }
start() {
    ebegin "Fixing touchscreen GPIO pad (pin 77 bit 7)"
    echo i2c-04F3200A:00 > /sys/bus/i2c/drivers/i2c_hid_acpi/unbind 2>/dev/null
    /usr/local/bin/fix_gpio_pad.sh
    sleep 0.5
    echo i2c-04F3200A:00 > /sys/bus/i2c/drivers/i2c_hid_acpi/bind 2>/dev/null
    eend $?
}

Added to boot runlevel: rc-update add fix-touchscreen-gpio boot

Kernel Parameters

In GRUB's 40_custom Alpine entry:

linux /boot/vmlinuz-lts ... iomem=relaxed

Display Scaling

The 1920x1200 display on a 10" tablet needs HiDPI scaling. In ~/.profile:

export QT_SCALE_FACTOR=2
export GDK_SCALE=2

Important: Do NOT also set Xft.dpi: 192 — it stacks with QT_SCALE_FACTOR, resulting in 4x scaling. Use one or the other.

Verified After Reboot

$ grep 04F3 /proc/interrupts
 126:     15851     0     0     0  chv-gpio   77  04F3200A:00

15,851 interrupts already accumulated after boot. Touchscreen working, persistent across reboots.


Phase 9: Revisiting Assumptions — What's Actually Needed?

Weeks later, the tablet was accidentally booted from the stock GRUB entry instead of the custom "fixed ACPI" entry. The stock entry has no DSDT override — no acpi_override.cpio in the initrd. The touchscreen still worked. This prompted a systematic re-examination of what the fix actually requires.

The DSDT Override Is Not Needed on Linux

The minimal DSDT was created to fix 4 compilation errors in the stock DSDT. On Windows, this was critical — GetSystemMetrics(SM_DIGITIZER) went from 0 to 193 with the fix. But on Linux, the kernel's i2c_hid_acpi driver binds to the touchscreen correctly with the stock DSDT. The 4 errors don't affect Linux's ACPI interpreter.

The DSDT override was moved to archive/. It remains useful documentation of the ACPI investigation, and necessary if anyone wants to run Windows on this tablet.

iomem=relaxed Is Required

The fix script writes to MMIO address 0xFED98000+0x5810 via /dev/mem. Alpine's kernel has both CONFIG_STRICT_DEVMEM=y and CONFIG_IO_STRICT_DEVMEM=y, which block userspace access to MMIO regions claimed by kernel drivers. The pinctrl-cherryview driver claims the entire Southeast community region (0xFED98000-0xFED9FFFF), so without iomem=relaxed, the script's mmap() fails with EPERM.

Alternatives investigated:

  • Pinctrl debugfs (pinmux-select): Only supports pre-defined pin groups (pwm, sdmmc, spi). Pin 77 (GPIO_ALERT) is not in any group, and there's no "gpio" function exposed. Cannot change PMODE.
  • Sysfs GPIO export: Exporting GPIO 787 sets the GPIOEN bit but does not clear the PMODE field. The pad stays in native function mode.
  • Kernel module with ioremap(): Would work without iomem=relaxed, but adds compilation and module signing complexity for no practical benefit.

Security Note

iomem=relaxed disables the kernel's MMIO access control for /dev/mem, allowing any root process to read/write arbitrary hardware registers. On a personal tablet, this is a negligible concern — an attacker with root access can already load kernel modules, write to /dev/kmem, or kexec a new kernel. The STRICT_DEVMEM protection is defense-in-depth, not a hard security boundary.

That said, this tablet should not be considered suitable for high-security workloads where defense-in-depth matters. For such use cases, the kernel module approach (using ioremap() instead of /dev/mem) would eliminate the need for iomem=relaxed entirely.

The Minimal Fix

After this re-examination, the actual fix for the Fujitsu Q506 touchscreen on Linux is just:

  1. iomem=relaxed on the kernel command line
  2. A boot script that writes one GPIO pad register

No DSDT override. No kernel patches. No custom modules. One kernel parameter and one boot service.

Phase 10: Eliminating Dependencies — Python to Shell

The fix script was originally written in Python (fix_gpio_final.py), using mmap and struct to access /dev/mem. It worked, but raised a question: what about cross-distribution compatibility? A Q506 owner might run a minimal distro without Python, or an older Linux with only Python 2, or no Python at all.

The operation is fundamentally simple: read 4 bytes from a physical address, flip 3 bits, write them back. This shouldn't require a runtime.

Attempt 1 — C rewrite: A 55-line C program (fix_gpio_pad.c) compiled to a static binary. Zero runtime dependencies, works everywhere. But requires a C compiler on the target to build from source.

Attempt 2 — Pure POSIX shell: The realization that dd can seek to arbitrary offsets in /dev/mem (via lseek(), not sequential read), and od can decode the bytes. The entire fix became a 30-line shell script using only dd, od, printf, and tr — all busybox builtins or POSIX utilities.

The first shell attempt had a subtle bug: printf '\x%02x' puts \x (a hex escape) in the format string where %02x (a format specifier) also lives. The result was writing ASCII text into the MMIO register instead of binary. The fix: use POSIX octal escapes via a nested printf — inner printf generates \200\200\320\120, outer printf interprets them into raw bytes.

Another subtlety: using bs=4 for aligned 32-bit MMIO access. MMIO registers require naturally-aligned reads/writes — bs=1 with four separate 1-byte operations could produce incorrect values on hardware.

The final fix_gpio_pad.sh has zero dependencies beyond /bin/sh and /dev/mem. It works on any Linux from the last 20+ years — busybox, coreutils, dash, bash, ash, any shell.

Phase 11: The Right Tool for the Job

The pure-shell dd approach worked, but a code review raised concerns:

The dd seek problem. The script uses bs=4 skip=1068922372 to reach address 0xFED9D810. Internally, dd calls lseek() — an O(1) pointer update, not a sequential read through ~4GB. This was verified against both GNU coreutils and busybox source code. But the multiplication skip * bs produces ~4.27 billion, which overflows a 32-bit integer. Modern 32-bit Linux uses Large File Support (64-bit off_t), so it works in practice, but it's an unnecessary edge case.

The printf format collision. The first shell attempt used printf '\x%02x' — mixing a \x hex escape with a %02x format specifier in the same format string. The result: printf wrote ASCII text ("08x") into the MMIO register instead of binary bytes. Fixed with POSIX octal escapes via nested printf, but the whole approach was getting fragile.

Enter devmem2 — Linux's POKE command. The Commodore 64 had POKE 53280,0. Linux has devmem2 0xFED9D810 w 0x50D08080. Same concept: write a value to a physical address. devmem2 uses mmap() internally — no seek arithmetic, no byte-order encoding, no octal escapes. The entire fix becomes three lines of shell.

But is devmem2 universal? Research across minimal Linux distros:

Distro devmem (busybox) devmem2 (package)
BusyBox upstream Default enabled
TinyCore Linux Disabled, no extension Not available
Alpine Linux Disabled Community repo (apk add devmem2)
OpenWrt Disabled Not in packages
Buildroot Enabled by default

TinyCore — the archetype of minimal Linux — has neither. So devmem2 can't be assumed. The solution: a single unified script that auto-detects the best available backend — busybox devmem first, then devmem2, falling back to dd + od when neither is available. One script, works everywhere.

Phase 12: We Were Wrong About Why It Works

A code review questioned whether our bit positions matched the documented Cherry Trail register layout. The Linux kernel's pinctrl-cherryview.c defines:

  • PMODE at bits [19:16] (not [11:9] as we assumed)
  • GPIOCFG at bits [10:8] (3-bit direction field, not individual TXDIS/RXDIS)
  • GPIOEN at bit 15

Decoding the BIOS default 0x50D08201 using the CHV layout: PMODE[19:16] = 0 (already GPIO mode!), GPIOEN = 1 (already enabled!), GPIOCFG = 2 (GPI/input mode — correct!). By the kernel's own register definitions, pin 77 was already correctly configured. But the touchscreen didn't work.

To resolve this, we ran a systematic experiment on the live hardware, testing register values with different bit combinations:

Register Value Bit 7 GPIOCFG[10:8] Touchscreen
0x50D0F05C 0 0 (bidir) Broken — 0 new IRQs
0x50D0F0DC 1 0 (bidir) Working — ~2000 IRQ/5s
0x50D0F25C 0 2 (input) Broken — 0 new IRQs
0x50D0F2DC 1 2 (input) Working — ~2000 IRQ/5s

Bit 7 is the sole determining factor. GPIOCFG doesn't matter. PMODE was never wrong.

What we thought we were fixing (PMODE[11:9]) was actually GPIOCFG[10:8] — and it was irrelevant. Our fix worked because it also set bit 7 as a side effect (we set "GPIOTXDIS[7]" thinking it was a TX disable, but it was actually the critical bit).

Bit 7 is not defined in the Linux kernel's pinctrl-cherryview.c. But it IS documented elsewhere — in Intel's own firmware reference code, it's called Gpio_Light_Mode:

Source Bit 7 Definition
Intel Braswell UEFI reference code Gpio_Light_Mode : 1; // 7 GPIO Light Mode
Coreboot /* config0[7] - Gpio Light Mode Bar */
U-Boot gpio_light_mode << 7 in GPIO_PAD_CONF macro
Linux kernel Not defined

Coreboot's "_Bar" suffix (active-low convention) tells us: bit 7 = 0 means light mode enabled (low-power pad), bit 7 = 1 means light mode disabled (full operation). The BIOS leaves pin 77 in light mode, which degrades the input buffer enough that interrupt signals can't get through. Three firmware projects know about this bit. The Linux kernel doesn't.

The fix is now reduced to its true essence: read the pad register, set Gpio_Light_Mode (bit 7), write it back. One bit.

Phase 13: Understanding Why — Root Cause Analysis

With the fix proven and the correct bit identified, the remaining question was: why is bit 7 cleared, and is this a regression? Phase 13 investigated this from every angle — BIOS, ACPI, kernel source history, and Intel's design intent.

GRUB register test. Added a custom GRUB menu entry using the memrw module to read pin 77's register before Linux loads. Results across boot types:

Boot type PADCTRL0 value Bit 7
Cold boot (power off → on) 0x50D08201 0
Cold boot (second test) 0x50D08201 0
Warm reboot (after Linux ran) 0x50D0F25D 0

The BIOS genuinely never sets bit 7. This is not a kernel regression at the register level — the hardware starts in light mode every boot.

Intel UEFI reference code. The Braswell firmware reference code explains why the BIOS doesn't set it. The CHV_GPIO_PAD_CONF macro uses a selective-update mechanism:

// Value: only write if not NA
(((Gpio_Light_Mode) != NA) ? (Gpio_Light_Mode << 7) : 0)
// Mask: only touch if not NA
(((Gpio_Light_Mode) != NA) ? (ONE_BIT << 7) : 0)

For pin SE77 (GPIO_ALERT / TCH_PAD_INT_N), the board GPIO table sets Gpio_Light_Mode = NA — meaning the BIOS intentionally skips this bit, leaving it at the hardware default (0 = light mode). The design intent: the OS GPIO driver sets it when ready.

ACPI tables. Decompiled the DSDT and all 7 SSDTs from the tablet. Searched for any code writing to pin 77's register (offset 0x5810, address range 0xFED9D...). None found. The DSDT's GPO3 _REG method only sets availability flags. The TCS0 (touchscreen) device definition is standard I2C HID with a GpioInt reference — no register manipulation.

Kernel source bisection. Analyzed all 96 commits to pinctrl-cherryview.c between v5.4 and v6.18. No version has ever defined or set bit 7. The combined mask of all bits the driver touches in PADCTRL0 is 0xF0FF8703 — bit 7 falls in the gap between GPIORXSTATE[1] and GPIOCFG[10:8]. All writes are read-modify-write, so the driver preserves whatever the BIOS left.

INTSEL conflict check. Pin 77 is the only Southeast community pin with INTSEL=5. No interrupt line conflict exists.

The reframing. This is not a BIOS bug. Intel's architecture explicitly defers Gpio_Light_Mode to the OS driver. Windows' Intel Serial IO GPIO driver (INT33FF) sets it during initialization. Linux's pinctrl-cherryview.c never implemented this step. The proper fix is a kernel patch adding Gpio_Light_Mode support to the Linux driver.

Open question: older kernels. User reports confirm the touchscreen worked on Ubuntu 18.04/19.04/20.04 (kernels ~4.15–5.4) without any special configuration. Since no version of pinctrl-cherryview.c has ever set bit 7, and no ACPI code sets it either, the mechanism by which older kernels delivered touchscreen interrupts remains unresolved. Three unverified hypotheses remain:

  1. Changes in gpiolib-acpi.c (ACPI GPIO event handling) between v5.4 and v5.17
  2. Hans de Goede's interrupt line conflict resolution changes (v5.17) altering probe behavior
  3. The IRQF_NO_AUTOEN change in i2c-hid (v6.6) affecting IRQ enable timing

Until one of these is confirmed, the "why did it break" half of the story remains open. The fix itself — set bit 7 — is proven and understood.

Phase 14: Kubuntu — A Proper Desktop

Alpine Linux with LXQt was functional but spartan. The user installed Kubuntu 24.04 (kernel 6.17) for a more polished tablet experience. The touchscreen fix carried over with minor adaptation.

Touchscreen fix on Kubuntu. devmem2 worked without iomem=relaxed — Ubuntu's kernel has CONFIG_IO_STRICT_DEVMEM unset. Created a systemd oneshot service instead of the Alpine OpenRC script. Verified the fix is not persistent across reboots (register resets to 0x50D08201 on cold boot), confirming the boot service is necessary.

KDE performance tuning. The Atom x5 struggles with KDE's default effects. Applied lightweight settings:

  • Compositor disabled by default (toggle with Shift+Alt+F12)
  • AnimationDurationFactor=0 — all UI animations instant
  • Window shadows disabled (prevents black border artifacts with compositor off)
  • Widget style: Windows (flat, cheapest to render)
  • Window decoration: Plastik (simple QML)
  • Cursor: KDE_Classic
  • HiDPI: scale factor 2

GTK CSD shadow fix. With the compositor off, Firefox and other GTK apps render thick black borders — the shadow region becomes opaque without alpha compositing. Fixed by adding box-shadow: none; margin: 0 to ~/.config/gtk-3.0/gtk.css.

Virtual keyboard. Attempted maliit-keyboard (KDE's Virtual Keyboard settings page is Wayland-only, invisible on X11), onboard (segfault), and xvkbd (too small at HiDPI, steals focus). No usable on-screen keyboard on X11 — would need Wayland.

Known limitations:

  • Touch registers as pointer input (INPUT_PROP_POINTER), not direct touch (INPUT_PROP_DIRECT) in X11 — same on Alpine. Firefox touch test pages reject it. Works as mouse input.
  • Touch polling rate ~24 Hz with irregular gaps up to 116ms — likely Cherry Trail I2C PUNIT semaphore contention. Hardware limitation.
  • Battery gauge (MAX17047 fuel gauge) fails to probe: no platform data provided. No battery indicator. ACPI tables don't include required parameters.

Technical Deep Dive

Cherry Trail GPIO Pad Configuration

Intel Cherry Trail (Atom x5/x7) organizes GPIO pins into communities:

Community ACPI Device MMIO Base Purpose
Southwest INT33FF:00 0xFED80000 General I/O
North INT33FF:01 0xFED88000 Display, camera
East INT33FF:02 0xFED90000 USB, PCIe
Southeast INT33FF:03 0xFED98000 SD, I2C, GPIO_ALERT
Virtual INT33FF:04 0xFEDA0000 Virtual GPIO

Each community contains families of pins. Within the Southeast community:

Family Pins MMIO Offset
0 0–7 0x4400
1 15–26 0x4800
2 30–35 0x4C00
3 45–52 0x5000
4 60–69 0x5400
5 75–85 0x5800

Each pin has 8 bytes of configuration: padcfg0 (4 bytes) + padcfg1 (4 bytes).

Pin 77 = Family 5, index 2 → offset 0x5800 + 2×8 = 0x5810

CHV_PADCTRL0 Bit Fields

Bits [0]      GPIOTXSTATE          Current output value
Bits [1]      GPIORXSTATE          Current input value (read-only)
Bits [7]      Gpio_Light_Mode_Bar  0=light mode (low-power), 1=normal operation
Bits [10:8]   GPIOCFG              GPIO direction (0=bidir, 1=GPO, 2=GPI, 3=HiZ)
Bits [15]     GPIOEN               1=GPIO mode enabled
Bits [19:16]  PMODE                0=GPIO, 1-7=native function modes
Bits [23:20]  Term                 Pull-up/down termination
Bits [27:26]  GFCfg                Glitch filter configuration
Bits [31:28]  INTSEL               APIC interrupt routing

The critical field is bit 7 (Gpio_Light_Mode_Bar). When cleared, the pad operates in a low-power "light mode" that degrades the input buffer, preventing interrupt signals from reaching the GPIO controller. Intel's UEFI reference code marks this bit as NA (leave for OS driver) for the touchscreen interrupt pin — the BIOS intentionally does not set it. Confirmed by reading the register from GRUB before Linux loads (0x50D08201, bit 7 = 0). Windows' Intel GPIO driver sets this bit during initialization; Linux's pinctrl-cherryview.c has no definition for it and never sets it. This bit is documented in Intel's Braswell UEFI reference code, coreboot, and u-boot, but missing from the Linux kernel.

Note: During the initial investigation (Phases 6-7), bit 7 was misidentified as GPIOTXDIS and the fix was believed to be a PMODE change. Phase 12 established the correct register layout and identified Gpio_Light_Mode as the true root cause. The Phase 6-7 narrative above preserves the original (incorrect) understanding as it happened.

DSDT Touchscreen Definition (TCS0)

Device (TCS0) {
    Name (_HID, "04F3200A")
    Name (_CID, "PNP0C50")           // HID over I2C

    // I2C connection: bus I2C6, address 0x10, 400kHz
    I2cSerialBusV2 (0x0010, ControllerInitiated, 400000, ...)

    // Reset/enable pin: GPO1 (INT33FF:01) pin 20
    GpioIo (Exclusive, PullDefault, ..., "\\_SB.GPO1") { 0x0014 }

    // Interrupt pin: GPO3 (INT33FF:03) pin 77, active-low, level-triggered
    GpioInt (Level, ActiveLow, Shared, ..., "\\_SB.GPO3") { 0x004D }

    // HID descriptor at register 1
    Method (_DSM, ...) { Return (One) }
}

Why doesn't the BIOS set this bit?

It's by design. Intel's UEFI reference code uses a selective-update mechanism for GPIO pad configuration. Each pin's Gpio_Light_Mode field can be set to HIGH, LOW, or NA (don't touch). In the board GPIO tables for Braswell reference boards, the touchscreen interrupt pin (SE77 / GPIO_ALERT / TCH_PAD_INT_N) has Gpio_Light_Mode = NA — the firmware intentionally leaves it at the hardware default (0 = light mode), expecting the OS GPIO driver to set it when ready.

Only pins where signal integrity matters during early boot (SDMMC data/clock, some camera pins) have Gpio_Light_Mode explicitly set to HIGH in the BIOS. Everything else is deferred to the OS.

From Intel's BraswellPlatformPkg.dec: "BIT7 - GPIO Light Mode. 0 = Disable. 1 = Enable."

This was confirmed by reading pin 77's register from GRUB (via insmod memrw; read_dword) before Linux loads:

Boot type PADCTRL0 value Bit 7
Cold boot (GRUB, before Linux) 0x50D08201 0 (BIOS default — light mode, as designed)
Warm reboot (GRUB, after Linux ran) 0x50D0F25D 0 (Linux modifies other bits but not bit 7)
Running Linux 6.18 0x50D08201 0 (driver never sets this bit)
After fix script 0x50D082xx with bit 7 set 1 (working)

So why doesn't Linux set it?

Linux's pinctrl-cherryview.c has no definition for bit 7. The driver's PADCTRL0 constants cover INTSEL, TERM, PMODE, GPIOEN, GPIOCFG, TXSTATE, and RXSTATE — but skip bit 7 entirely. All PADCTRL0 writes are read-modify-write operations that preserve undefined bits, so if the BIOS doesn't set it, Linux won't either.

Windows' Intel Serial IO GPIO driver (for ACPI device INT33FF) presumably sets Gpio_Light_Mode during initialization — this is the intended design. The touchscreen works on Windows because Windows implements the full GPIO pad configuration that Intel's architecture expects.

What about older kernels?

The touchscreen worked on older kernels without any special configuration:

Source Kernel Touchscreen
url4u.jp blog ~5.0 (Ubuntu 19.04) Working — no special setup
Qiita article ~5.4 (Ubuntu 20.04) Working — no special setup
wacom-hid-descriptors #326 Later kernel Broken — touch not detected
This repo 6.18 (Alpine 3.23) Broken — 0 GPIO interrupts

Source code analysis of pinctrl-cherryview.c across all 96 commits between v5.4 and v6.18 confirms that no version of the driver has ever explicitly set bit 7. How the touchscreen worked on older kernels remains under investigation — the change may be in gpiolib-acpi.c, the ACPI subsystem, or the interrupt delivery path rather than in the pinctrl driver itself.


Dead Ends and Abandoned Approaches

Things That Didn't Work (Chronological)

# Approach Why It Failed
1 Module reload (i2c_hid_acpi) Standard Cherry Trail fix — wrong root cause for Q506
2 HID quirk (NOGET) Problem was below HID layer
3 hid-generic vs hid-multitouch Same — wrong layer
4 x86_android_tablets module No DMI match for Fujitsu Q506
5 GPIO sysfs edge override Driver rebind resets trigger type from ACPI
6 DSDT GpioInt trigger change (Level→Edge) Loaded, but 0 interrupts — pad mode was the issue
7 Standalone i2c_hid polling module Too old for kernel 6.18
8 Custom patched kernel module CRC/signing enforcement blocked loading
9 Userspace UHID polling daemon Worked but fragile; never fully deployed
10 flashrom direct SPI write SMM Write Protection
11 UEFI Capsule-on-disk update Firmware ignored all capsule formats
12 fwupd flash attempt No UEFI UpdateCapsule support
13 WinPE boot 0xc000000f — boot configuration data missing
14 wimapply from Linux Missing NTFS security descriptors
15 BCD hand-crafted via hivex (5 iterations) Binary device element format is underdocumented
16 pivot_root for partition resize SSH session hung (daemon on old root)
17 Initramfs resize shell Keyboard non-functional (no USB HID in initramfs)
18 ELAN touchpad driver on Windows Was for USB touchpad (PID 074A), not I2C touchscreen
19 Wrong MMIO offsets (0x45f8, 0x4608) Family base offsets differ from kernel source comments
20 lxqt-config-brightness Crashes on DSI panels (uses xrandr backend)
21 LXQt backlight panel plugin Crashes entire panel on DSI panels
22 xrandr --scale 0.5x0.5 for scaling DSI panels don't support xrandr scaling — blank screen
23 SOF / snd-intel-dspcfg for audio Kernel missing CONFIG_DW_DMAC — no DMA controller driver

Files

Root — The Fix

Path Description
fix_gpio_pad.sh The fix — auto-detects devmem / devmem2 / dd fallback
fix-touchscreen-gpio OpenRC boot service to run the fix at startup

archive/ — Dead Ends and Reference

Path Description
archive/fix_gpio_final.py Original Python fix script (superseded by shell version)
archive/fix_gpio_pad.c C rewrite (intermediate step, superseded by shell version)
archive/decode_padcfg.py Pad register decoder (uses incorrect Phase 6 bit-field layout)
archive/fix_and_test.sh Unbind → fix → rebind script (references old Python fix)
archive/scan_pads.py MMIO scanner that found the correct offset empirically
archive/40_custom GRUB custom menu entry (from DSDT override era, now just need iomem=relaxed)
archive/dsdt_minimal.diff Minimal DSDT fix diff (needed for Windows, not needed for Linux) — explained in Phase 5
archive/touch_monitor.ps1 Windows PowerShell cursor position monitor (used during Windows phase)
archive/cleanup.sh Post-fix cleanup: remove Windows, expand Alpine, set up swap
archive/cleanup2.sh Cleanup variant using sgdisk

What Makes This Portfolio-Worthy

This project is not "I fixed a touchscreen" — it's a case study in agentic hardware debugging, and what happens when an AI agent operates a physical device remotely via SSH.

The AI Couldn't Touch the Screen

The fundamental constraint: Claude Code could SSH into the tablet, run any command, read any register, load any kernel module — but it could not physically touch the screen. Every time a diagnostic required a touch event, the AI had to ask the human: "Touch the screen NOW — you have 30 seconds." This created a unique human-AI collaboration pattern where the human was essentially a biological peripheral: providing physical input on command while the AI orchestrated the investigation.

23 Dead Ends Before the Fix

The table above lists 23 distinct approaches that failed. Each was a reasonable hypothesis given the available information. The AI worked through driver layers (HID → I2C → GPIO → ACPI → MMIO registers), peeling back abstractions until it reached the hardware truth. The journey from "the HID driver is buggy" to "a single register bit is cleared" required crossing the entire software stack.

The Windows Detour Was Not Wasted

The Windows installation consumed a full day and produced no working touchscreen. It yielded the DSDT insight — iasl decompile/recompile roundtrips change vendor-specific ACPI method names — but this turned out to matter only for Windows. The final Linux fix needs no DSDT override at all. A full day's work that ultimately wasn't needed for the solution, but deepened understanding of the system and ruled out an entire class of causes.

Kernel Source Comments ≠ Hardware Reality

The GPIO family offsets documented in cherryview-pinctrl.c comments didn't match the actual MMIO layout. When the first two calculated offsets returned 0xFFFFFFFF, the AI wrote a brute-force scanner to empirically locate the correct register. When documentation fails, scan the hardware.

"Device OK" ≠ "Device Working"

On both Linux and Windows, every diagnostic tool said the touchscreen was properly configured:

  • Linux: Driver bound, input devices created, GPIO claimed
  • Windows: Device Manager green checkmarks, no error codes

But the hardware pad configuration was wrong at a level these tools don't inspect. OS-level diagnostics operate on abstractions. Hardware bugs require hardware-level investigation.


Timeline

Day 1 (Session 1):
  ├─ Alpine Linux setup, LXQt desktop working
  ├─ Touchscreen diagnosed: 0 GPIO interrupts
  ├─ HID layer debugging (dead end)
  ├─ DSDT GpioInt modifications (dead end)
  ├─ I2C polling proves hardware works
  ├─ Userspace daemon + kernel module attempts (dead ends)
  ├─ WiFi/SSH connectivity crisis (IP change, double-hop routing)
  └─ Session interrupted mid-debugging

Day 2-3 (Session 2):
  ├─ BIOS update journey (flashrom → capsule → EFI shell → success)
  ├─ Windows installation (wimapply → BCD → USB installer → success)
  ├─ Windows touchscreen investigation (drivers installed, still broken)
  ├─ Minimal DSDT discovery (SM_DIGITIZER 0→193)
  ├─ Back to Linux, GPIO pad investigation
  ├─ Root cause found: pad misconfiguration at MMIO 0xFED98000+0x5810 (initially misidentified as PMODE, later corrected to Gpio_Light_Mode in Phase 12)
  ├─ Wrong MMIO offset debugging (0x45f8 → 0x4608 → scan → 0x5810)
  ├─ GPIO pad fix applied (bit 7 set as side effect of PMODE change — true cause identified in Phase 12)
  ├─ TOUCHSCREEN WORKS! (2,254 IRQs immediately)
  ├─ Boot service created for persistence
  ├─ Verified working after reboot
  ├─ HiDPI scaling configured (200%)
  ├─ Windows removed, swap restored, Alpine expanded
  ├─ LXQt polish: Adwaita icons, Firefox ESR, HiDPI Openbox theme
  ├─ USB PNP investigation (Cherry Trail xHCI limitation on USB 3.0)
  ├─ Brightness control via zenity + taskbar quicklaunch
  └─ Audio investigation (kernel missing CONFIG_DW_DMAC — deferred)

Later (Phase 9 — revisiting assumptions):
  ├─ Accidentally booted stock GRUB entry — touchscreen still works
  ├─ DSDT override confirmed unnecessary for Linux
  ├─ iomem=relaxed confirmed required (CONFIG_STRICT_DEVMEM=y)
  └─ Fix reduced to: one kernel parameter + one boot script

Later (Phase 10 — eliminating dependencies):
  ├─ Python fix rewritten in C (works but requires compiler)
  ├─ C fix rewritten in pure POSIX shell (dd + od + printf)
  ├─ printf '\x%02x' bug: wrote ASCII text into MMIO register
  ├─ Fixed with POSIX octal escapes, verified on hardware
  └─ Final fix: zero dependencies, any Linux, any shell

Later (Phase 11 — the right tool for the job):
  ├─ dd seek verified as O(1) lseek(), but 32-bit overflow edge case
  ├─ devmem2 identified as Linux's POKE command (mmap, no seek math)
  ├─ Surveyed TinyCore/Alpine/OpenWrt/Buildroot for devmem availability
  ├─ TinyCore has neither devmem nor devmem2 — can't assume universal
  └─ Unified script: auto-detects devmem → devmem2 → dd fallback

Later (Phase 12 — we were wrong about why it works):
  ├─ pinctrl-cherryview.c register layout doesn't match our bit positions
  ├─ PMODE[19:16] was already 0 — the pin was already in GPIO mode
  ├─ Systematic 4-value experiment on live hardware
  ├─ Bit 7 is the sole determining factor
  ├─ Identified as Gpio_Light_Mode via Intel/coreboot/u-boot firmware sources
  ├─ Documented in 3 firmware projects, missing from Linux pinctrl-cherryview.c
  └─ Fix reduced to: read register, set Gpio_Light_Mode, write back

Later (Phase 13 — understanding why):
  ├─ Pre-commit code review: 8 fixes (framing, comments, file moves)
  ├─ GRUB memrw test: cold boot = 0x50D08201 (bit 7=0), BIOS never sets it
  ├─ Intel UEFI reference code: Gpio_Light_Mode = NA for SE77 (by design)
  ├─ DSDT + 7 SSDTs searched: no ACPI code writes to pin 77
  ├─ 96 commits of pinctrl-cherryview.c bisected: no version ever sets bit 7
  ├─ INTSEL conflict check: pin 77 unique (INTSEL=5, no conflict)
  ├─ Reframed: not a BIOS bug, but missing OS driver initialization
  └─ Open: how older kernels (~5.0–5.4) worked without bit 7 — 3 hypotheses remain

Later (Phase 14 — Kubuntu):
  ├─ Installed Kubuntu 24.04 (kernel 6.17) for a proper desktop
  ├─ devmem2 works without iomem=relaxed (CONFIG_IO_STRICT_DEVMEM unset)
  ├─ Created systemd oneshot service for touchscreen fix
  ├─ KDE performance: compositor off, animations instant, Windows widget style
  ├─ GTK CSD shadow fix (black borders with compositor off)
  ├─ Virtual keyboard: maliit/onboard/xvkbd all failed on X11
  ├─ Touch polling rate ~24 Hz (Cherry Trail I2C PUNIT contention)
  └─ Battery gauge (MAX17047) won't probe — no platform data in ACPI

Final Cleanup

With the touchscreen working and Linux as the permanent OS, Windows was removed:

  • Deleted partitions p4 (16MB Microsoft reserved) and p5 (95.9GB Windows NTFS)
  • Reformatted p2 (4GB, formerly WinPE) as Linux swap
  • Expanded Alpine's root partition (p3) from 16GB to 112GB via online resize2fs
  • Removed Windows GRUB entries and EFI/Microsoft from the ESP

Final partition layout:

Number  Start         End           Size       Type
   1    2048          1050623       512M       EFI System
   2    1050624       9439231       4.0G       Linux swap
   3    9439232       244277214     112.0G     Linux filesystem

Post-Fix Usability Polish

With the touchscreen working and Alpine as the sole OS, focus shifted to making the tablet comfortable to use as a touch-first device:

  • Missing icons: LXQt showed blank squares everywhere. Fixed by installing adwaita-icon-theme.
  • Browser: Installed firefox-esr — lightweight enough for the Atom x5 but feature-complete.
  • HiDPI title bar buttons: Created a custom HiDPI Openbox theme with 16x16 XBM button icons and fat touch targets (padding.width=8).
  • USB 3.0 quirk: Cherry Trail xHCI gives error -71 with low-speed USB devices on USB 3.0 ports — a known hardware limitation.
  • Brightness control: Built-in LXQt brightness tools crash on DSI panels. Created zenity --scale wrapper writing directly to /sys/class/backlight/intel_backlight/brightness.
  • Audio (deferred): RT5670 codec needs CONFIG_DW_DMAC — would require a custom kernel build.

Future Work: Upstreaming to the Linux Kernel

The userspace /dev/mem fix works, but the proper long-term solution is for pinctrl-cherryview.c to handle Gpio_Light_Mode natively. Intel's UEFI reference code expects the OS GPIO driver to set this bit — Windows does, Linux doesn't. This section documents the research into upstreaming a fix.

Why upstream?

The current fix requires iomem=relaxed (weakening kernel memory protections) and races with the kernel's own pinctrl driver. A kernel-level fix would:

  • Implement the GPIO initialization that Intel's architecture expects — the BIOS intentionally defers Gpio_Light_Mode to the OS driver
  • Eliminate the need for iomem=relaxed
  • Eliminate the race between our boot script and driver probe
  • Automatically benefit all affected Cherry Trail devices, not just the Q506

Who maintains the driver?

drivers/pinctrl/intel/pinctrl-cherryview.c is maintained by:

  • Andy Shevchenko (andriy.shevchenko@linux.intel.com)
  • Mika Westerberg (mika.westerberg@linux.intel.com) — original author

Patches go through linux-gpio@vger.kernel.org, merged via Linus Walleij's linux-gpio tree. Hans de Goede (hdegoede@redhat.com) is the go-to reviewer for Cherry Trail tablet quirks.

Existing precedent

No one has attempted to upstream Gpio_Light_Mode support before. However, a closely analogous bug has been fixed in this driver:

Precedent Pattern Relevance
a0bf06dc51dbCHV_PADCTRL1_INVRXTX_TXDATA preservation Driver cleared PADCTRL1 bit 7, a BIOS-set flag, breaking Goodix touchscreens. Fixed with read-modify-write. Closest analogue — same register offset (bit 7), same bug class (driver not preserving a bit it doesn't define), same symptom (broken touchscreen).
Interrupt masking on probe Driver blanket-masked all interrupts, breaking ACPI events. Fixed with selective masking. Shows the driver has a history of being too aggressive with register writes.
Duplicate interrupt line fix BIOS assigned same IRQ to two pins. Driver now detects and reassigns. DMI-quirk pattern for tablet-specific workarounds.

Possible approaches

1. Generic driver fix (preferred): Add #define CHV_PADCTRL0_GPIOLIGHTMODE BIT(7) and set this bit in chv_gpio_request_enable() when a pad is configured for GPIO mode. This implements the initialization step that Intel's UEFI reference code defers to the OS driver. Safe for all devices — setting bit 7 on a pad already in normal mode is a no-op.

2. DMI quirk: Match Fujitsu Stylistic Q506 specifically and set bit 7 only on that hardware. Reviewers may push for this if approach 1 is deemed too broad. The x86-android-tablets platform driver (drivers/platform/x86/x86-android-tablets/) has many examples of DMI-matched Cherry Trail fixes.

3. Out-of-tree kernel module: Use ioremap to manipulate the register from a loadable module. Avoids modifying the upstream driver but has no path to becoming a supported solution. Not recommended.

Challenges

  • Understanding why older kernels worked. Source code analysis confirms pinctrl-cherryview.c has never set bit 7 in any version. The mechanism by which kernels ~5.0–5.4 delivered touchscreen interrupts without bit 7 is still under investigation — the change may be in gpiolib-acpi.c, the ACPI subsystem, or the interrupt delivery path.
  • No public Intel documentation. The Cherry Trail EDS Volume 2 (document 543698) likely contains the per-pad register definitions, but it is marked Intel Confidential. The patch can reference Intel's open-source UEFI reference code, coreboot's Braswell GPIO header, and observable hardware behavior.
  • Cherry Trail is legacy hardware (2015). Fixes are still accepted (the INVRXTX_TXDATA fix merged in 2020), but reviewers may have less urgency.
  • Framing. This is best framed as "implement GPIO Light Mode initialization that Intel's architecture expects from the OS driver" rather than a BIOS bug workaround. The UEFI reference code explicitly defers this to the OS — the Linux driver simply never implemented it.

What a patch would look like

A draft commit message:

pinctrl: cherryview: Set Gpio_Light_Mode when enabling GPIO mode

Intel's Braswell/Cherry Trail UEFI reference code defines bit 7 of CHV_PADCTRL0 as "Gpio_Light_Mode" and marks it as NA (leave for OS) in the board GPIO tables for most pins, including touchscreen interrupts. The Windows Intel GPIO driver sets this bit during initialization; the Linux pinctrl-cherryview driver does not define or set it.

When bit 7 is cleared, the pad operates in a low-power "light mode" that degrades the input buffer, preventing interrupt delivery. On the Fujitsu Stylistic Q506, this results in a non-functional ELAN touchscreen (zero GPIO interrupts on pin 77). Confirmed by reading the register from GRUB before Linux loads: the BIOS value is 0x50D08201 (bit 7 = 0).

Set bit 7 in chv_gpio_request_enable() when configuring a pad for GPIO mode, implementing the initialization that Intel's architecture expects from the OS driver.

Reference: coreboot src/soc/intel/braswell/include/soc/gpio.h "config0[7] - Gpio Light Mode Bar" Reference: Intel UEFI ref code ChvRefCodePkg GpioLib.h "Gpio_Light_Mode : 1; // 7 GPIO Light Mode"

BugLink: linuxwacom/wacom-hid-descriptors#326 Cc: stable@vger.kernel.org


License

MIT

About

Fixing a Fujitsu Q506 tablet's broken touchscreen — from zero GPIO interrupts to a working Alpine Linux tablet, using an AI agent over SSH and raw MMIO register manipulation.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors