alarm-tegra-x1 0.1.0
Arch Linux ARM on the Nintendo Switch, on a modern mainline kernel
what it is
A build system and device tree for running Arch Linux ARM on the Nintendo Switch (HAC-001, Tegra X1) on a current mainline kernel instead of the vendor's frozen L4T 4.9.140. It boots to userspace with a USB serial console, USB networking (CDC-NCM, so ssh works with no WiFi), a working 720x1280 DSI panel, and a headless desktop over VNC. BRINGUP.md documents the observation channels and every silent failure encountered, because on this hardware almost nothing fails loudly: the display scanout is block-linear, there is no UART on a retail unit, and the kernel dies before printk reaches anything persistent.
readme
ALARM — Arch Linux ARM for the NVIDIA Tegra X1 (Nintendo Switch)
⚠️ AI-GENERATED PROJECT / README
This README — and the majority of the code and changes in this repository — were written by a Large Language Model (LLM) AI assistant, not by a human, with human direction and review. Treat everything here as unverified: check commands before running them, especially anything involving
sudo, loop devices, or writing to a block device. Do not assume the code is correct, safe, or complete just because it is written confidently. Review it.The mainline kernel now boots to userspace. A 7.2 kernel built by this project boots on the Switch, mounts the Arch Linux ARM root filesystem, and gives a root shell over USB serial (
/dev/ttyACM0). That is the project's goal: current userspace on a current kernel. The stock L4T 4.9.140 entry is still there as a known-good fallback, and it also reaches a root shell, with WiFi and SSH working.systemd does not run on either path yet — a small shell script takes its place as PID 1. See Status and
patches/README.md.
A build system that produces an SD-card image for running Arch Linux ARM on the Nintendo Switch (HAC-001), targeting the Tegra X1 (ODNX2-A2) SoC and booting through hekate's L4T loader.
A full record of the mainline bring-up — the observation channels, every silent failure, and the reasoning behind each fix — is in
BRINGUP.md. Start there if you are debugging a boot, or read §4 of it first if you are about to change anything.Prior art: switchroot have already done mainline Switch bring-up, including the panel driver and the PCIe lane configuration this project was missing. See
reference/switchroot-mainline/and §11 ofBRINGUP.md.Published on notdoublehub as
alarm-tegra-x1. The manifest isnotdouble.toml;.ndignoreexcludes the user photos and videos, thel4t-kernel-4.9clone (~930 MB), the switchroot rootfs parts (~4 GB each) and thebuild//output/trees, so an upload is ~1.3 MB of source.
Status
What works
- The mainline kernel boots to userspace. A 7.2 kernel built by this project
(clang 23,
--kernel built) boots on the Switch, mounts the Arch Linux ARM ext4 root filesystem, and provides a root shell over USB serial (/dev/ttyACM0on the host,/dev/ttyGS0on the device). alarm-buildCLI (Rust) builds: hekate boot tree, device tree, rootfs, kernel, and a bootable GPT SD-card image.- hekate's L4T loader accepts the generated entry and boots.
- U-Boot (
bl33) loads the kernel and device tree and runs ourboot.scr. - On the mainline kernel: microSD (SDHCI plus card detect), the Tegra XUDC USB serial gadget, the arch timer, PSCI and SMP all work.
sshdand a console shell start at boot. On the L4T entry, WiFi, DHCP, DNS andpacmanwork as well.
What does not work
- The display works. The panel is a JDI LPM062M326A, its driver is ported from
switchroot's patch 0010, and the whole chain — DSI, panel, init sequence, rails,
backlight — is proven by
modetest -M tegra -rdrawing a full 720×1280 image on the Switch's own screen. There is no boot console on it, and that is an upstream limitation, not a device-tree one: mainline only ships fbdev backends for GEM-DMA/shmem/TTM, and tegra has its own GEM. A userspace compositor works (that is the route to a desktop on the panel); input drivers are the other half. Seedevice-tree/README.md, "Display". - WiFi on the mainline kernel. The PCIe controller is deliberately left
disabled. Enabling it now trains the link and enumerates the root port — the
MAX77620
ldo1rail was the missing piece — but the endpoint never answers config cycles, and the resulting asynchronous SError panics the kernel and costs userspace entirely. Seedevice-tree/README.md, "WiFi: the state of play". - systemd does not run on either path. Current Arch Linux ARM ships
systemd 261, so a shell script takes its place as PID 1. The cost is no
systemd units, no
journalctl, noNetworkManagerand no user session management. - Kernel modules are not built. The kernel is built with
ALARM_NO_MODULES=1, so anything configured=mis simply absent. That silently broke the host-side USB console driver (CONFIG_USB_ACM, now built in) and is the reasonREQUIRED_OPTIONSexists.
Both the kernel and the userspace need matching /lib/modules + /lib/firmware;
the build imports them from the switchroot image (switchroot/install/l4t.00 +
l4t.01).
systemd is now unblocked in principle. systemd 261's baseline is kernel
4.15 and this project boots 7.2, so the cgroup v2 and new-mount-API
requirements are met — the shell PID 1 is now a choice rather than a necessity.
The path to a real systemd boot on the mainline kernel is to point /sbin/init
back at /usr/lib/systemd/systemd and work through what the first boot
complains about. On the L4T 4.9.140 entry systemd remains impossible, which is
why that entry keeps the shell init.
See Troubleshooting for details.
Hardware specifications
| Component | Specification | |-----------|---------------| | SoC | NVIDIA Tegra X1 (ODNX2-A2) | | Process | TSMC 20nm | | CPU (Big) | 4× Cortex-A57 @ 2.2 GHz (ARMv8-A) | | CPU (Little) | 4× Cortex-A53 @ 1.3 GHz (ARMv8-A) | | GPU | GM20B (Maxwell) 256 cores @ 1000 MHz | | Memory | 8 GB LPDDR4 (64-bit, 25.6 GB/s) | | Bootloader | hekate v6.x |
How booting works
hekate does not boot a raw kernel Image + .dtb pair. It boots Linux
through its L4T loader, which reads an entry in
bootloader/ini/*.ini that points at a boot-prefix directory:
[L4T ALARM]
l4t=1
boot_prefixes=/switchroot/alarm/
id=ALARM
rootdev=mmcblk0p2
rootlabel_retries=1
r2p_action=self
fbconsole=0
bootargs_extra=usb_logging auto_rootdev_disable
From that prefix, bl33 (a U-Boot stage, run by hekate) executes boot.scr,
which loads:
| File | Purpose |
|------|---------|
| uImage | Kernel (U-Boot image, load/entry 0x80200000) |
| initramfs | Finds the root partition, mounts it, and starts PID 1 (patched — see patches/) |
| nx-plat.dtimg | Platform device trees (Tegra DC/SOR/DSI panel, regulators, …) |
| boot.scr | U-Boot boot script that loads the above and bootms |
| bl31.bin | ARM Trusted Firmware secure monitor (loaded by hekate) |
| bl33.bin | L4T bootloader stage (loaded by hekate) |
Because the Switch panel is a DSI panel that must be brought up by a display
driver, the default kernel is the known-good switchroot L4T kernel shipped
in switchroot/ubuntu-jammy/. A vanilla kernel built from linux.git does not
carry the panel/regulator patch set and will not light up the display; it can
still be packaged with --kernel built for experimentation.
hekate runtime files
hekate is usually injected over RCM, but it still loads support files from the
SD card. The build fetches the official hekate release bundle and copies its
whole bootloader/ folder, which provides:
| Path | Purpose |
|------|---------|
| bootloader/sys/libsys_lp0.bso | LP0 sleep support |
| bootloader/sys/libsys_minerva.bso | RAM training / Minerva |
| bootloader/sys/nyx.bin | Nyx GUI |
| bootloader/sys/l4t/bpmpfw*.bin, mtc_tbl*.bin | BPMP firmware + RAM tables for L4T |
| bootloader/sys/res.pak, thk.bin, emummc.kipm | Resources / emuMMC |
Without these hekate refuses to boot L4T with
"missing lp0 (sleep) lib / missing minerva lib / update bootloader folder!".
Set HEKATE_BOOTLOADER_DIR to point at an existing bootloader/ folder
instead of downloading one.
Prerequisites
# Arch Linux
sudo pacman -S git make gcc flex bison dtc bc rsync parted dosfstools \
e2fsprogs mtools uboot-tools base-devel
# Ubuntu/Debian
sudo apt install git make gcc flex bison device-tree-compiler bc rsync parted \
dosfstools e2fsprogs mtools u-boot-tools build-essential
# Fedora
sudo dnf install git make gcc flex bison dtc bc rsync parted dosfstools \
e2fsprogs mtools uboot-tools
Cross compiler (only needed for --kernel built):
# Arch: sudo pacman -S aarch64-linux-gnu-gcc
# Ubuntu: sudo apt install gcc-aarch64-linux-gnu
Rootfs and image creation require root. Do not prefix the commands with
sudo — the build tool re-executes its own binary through sudo when it needs
to (running sudo cargo fails because rustup toolchains are per-user). Set
ALARM_NO_ELEVATE=1 to disable this and handle privileges yourself.
Building
# Initialise the build directory and download sources
cargo run -- init
# Assemble the hekate L4T boot tree under ./output (uses the switchroot kernel)
cargo run -- payload
# Bootstrap the ALARM root filesystem (prompts for your sudo password)
cargo run -- rootfs
# Create the bootable image
cargo run -- image --size 8
# Flash it to an SD card (auto-sudo; confirms the target is a block device)
cargo run -- flash /dev/sdX
cargo run -- image also runs payload and rootfs, so a plain
cargo run -- image is enough once init has completed. Use --kernel built
to package the locally built kernel instead of the switchroot one, and
--desktop for an XFCE rootfs.
Step by step
cargo run -- init # create build dirs + config
cargo run -- kernel # build the Linux kernel (optional; needs patches)
cargo run -- device-tree # compile the fallback device tree
cargo run -- payload # assemble bootloader/ini + switchroot/alarm
cargo run -- rootfs # bootstrap the ALARM rootfs (auto-sudo)
cargo run -- image # create alarm-tegra-x1.img (auto-sudo)
cargo run -- flash /dev/sdX # flash (auto-sudo)
Image layout
alarm-tegra-x1.img is a GPT disk with two partitions:
| Partition | Type | Label | Contents |
|-----------|------|-------|----------|
| 1 | FAT32 (512 MiB) | BOOT | bootloader/, switchroot/ (read by hekate) |
| 2 | ext4 | ALARM | Arch Linux ARM root filesystem |
The ext4 label (ALARM) must match id= in the hekate L4T entry. The ext4 is
created with orphan_file and metadata_csum_seed disabled, because the
4.9 L4T kernel cannot mount them.
Booting on the Switch
- Insert the flashed SD card.
- Enter RCM (hold VOL UP + POWER) and inject hekate.
- In hekate open More configs… and select L4T ALARM.
fbconsole=0 keeps the built-in display out of the way. The kernel console is
the USB serial gadget, enabled by usb_logging in the hekate entry: plug
the Switch into your PC and open the port with minicom -D /dev/ttyACM0. A
root shell is started there automatically.
First boot
The system does not show a login prompt — a root shell runs as PID 1 and
another on ttyGS0. Set a root password and the WiFi credentials, then reboot
or just run the init script's steps by hand:
passwd root # if PAM refuses, use the /etc/shadow method below
wpa_passphrase "YourSSID" "YourPassphrase" > /etc/wpa_supplicant/wpa_supplicant.conf
After a reboot, alarm-init.sh brings up WiFi, gets a DHCP lease and starts
sshd, so you can ssh root@<address> instead of using the serial cable.
Configuration
build/config/build.toml holds the hardware description, image layout
([image]) and toolchain settings. The kernel config is generated from the
arm64 defconfig with Tegra X1 options applied (src/kernel.rs); to customise
it, pass your own file:
cargo run -- kernel --custom-config build/config/kernel.config
Relevant environment variables:
| Variable | Purpose |
|----------|---------|
| HEKATE_BOOTLOADER_DIR | Use a local hekate bootloader/ folder instead of downloading |
| ALARM_NO_ELEVATE | Disable automatic sudo re-execution |
Project structure
alarm/
├── src/
│ ├── main.rs # CLI entry point
│ ├── config.rs # build + image configuration
│ ├── kernel.rs # kernel/device-tree build
│ ├── payload.rs # hekate L4T boot tree assembly
│ ├── rootfs.rs # ALARM rootfs + disk image
│ ├── packages.rs # U-Boot download/build
│ └── utils.rs # toolchain, flashing, privilege handling
├── device-tree/ # fallback DTS + switch-fb driver
├── switchroot/ # reference L4T boot files (kernel/ATF/dtimg/initramfs)
├── patches/ # non-systemd boot fix + card snapshot (see its README)
├── build/ # build directory (created by `init`)
└── output/ # generated boot tree + image
Troubleshooting
These are the real failures encountered while bringing this up; most were in the boot chain.
-
missing lp0 (sleep) lib / missing minerva lib / update bootloader folder!hekate's ownbootloader/sys/files are missing from the SD card. The build now copies the full hekate release bundle, so regenerate withpayload(or setHEKATE_BOOTLOADER_DIR). -
Selecting the entry bounces straight back to hekate.
boot.scrrunsreset(which returns to hekate) when it cannot load the kernel/initramfs/dtimg, and hekate's L4T loader returns to the menu onPath missing/Minerva missing/BL31 missing/BL33 missing/loading BPMP-FWerrors. The entry must stay comment-free: hekate treats a#line as a comment, but a;line inside a section is parsed as a key/value and forwarded into U-Boot's environment. -
failed to mount rootfs/filesystem has errors. The ext4 was created with features the 4.9 kernel cannot mount. The build disablesorphan_fileandmetadata_csum_seed; to fix an existing card:sudo tune2fs -O ^orphan_file,^metadata_csum_seed /dev/sdX2. -
<dev> not a switchroot installation.... The switchroot L4T initramfs refuses toswitch_rootunless/etc/switchroot_version.confexists in the rootfs. The builder writes it automatically; for an already-flashed card:sudo sh -c 'echo switchroot_version=1.0 > /run/media/$USER/ALARM/etc/switchroot_version.conf'. -
Failed to mount early API filesystems/ no login (systemd). systemd 261 cannot run on the 4.9 kernel: its baseline is 4.15, it needs cgroup v2, and there is no boot-argument escape hatch. This is why the initramfs now hands PID 1 to/etc/alarm-init.shinstead of/usr/lib/systemd/systemd. If you later switch to a newer Switch-capable kernel (e.g. L4S), re-runpatches/patch-initramfs.shwithout the/sbin/initreplacement to get systemd back. -
systemd will not start on the 7.2 kernel either — the three usual walls. On 7.2 the kernel side is no longer the problem (systemd's baseline is 4.15 and it wants cgroup v2; 7.2 has both). When systemd is eventually given PID 1 back, these are the three failures that account for most "systemd won't start" cases on a kernel booted this way, and they are worth checking in order:
- cgroup v2 mounted at
/sys/fs/cgroup. With a shell as PID 1 there is nothing to mount it. ConfirmCONFIG_CGROUPSand the unified hierarchy, and that/sys/fs/cgroupexists beforesystemdstarts. (/etc/alarm-init.shdoes not mount it today.) - A
/etc/machine-idto regenerate. The ALARM tarball ships an empty one and journald needs it;alarm-build rootfswrites a random value (src/rootfs.rs), and the builder patches an existing card too. - devtmpfs + a writable
/etcand/var. With a shell as PID 1 there is no udev, so/devmust come from devtmpfs.CONFIG_DEVTMPFS_MOUNTis not enough here — it only mounts devtmpfs fromprepare_namespace(), which never runs when an initramfs supplies/init. Seepatches/README.md(line 105) andBRINGUP.md§4.
Also note
bootargs_extrainsdcard-snapshot/L4T-alarm.inistill carriessystemd.unified_cgroup_hierarchy=0, which is a leftover from the 4.9 bring-up and fights the second item above. - cgroup v2 mounted at
-
A panic does not reboot the console — it just hangs. Two things were wrong. PSCI's
SYSTEM_RESETis non-functional on this hardware, and the Tegra PMC restart handler was registered atSYS_OFF_PRIO_LOW, which puts it behind PSCI's priority 129 on the same notifier chain — so PSCI was always tried first and never returned. The PMC handler is now registered atSYS_OFF_PRIO_FIRMWARE(128), ahead of PSCI, so a panic reboots warm through the PMC. This matters because a warm reboot is what lets hekate's preserved ramoops region survive, and that is the only way to read a log from a kernel that died before userspace. -
The kernel dies with no output anywhere. On a retail Switch there is no UART and the panel scanout is block-linear, so a silent death is the default. The two working channels are the FAT partition (files written by
/sbin/init) and ramoops plus a warm reboot — seepatches/README.md, "Reading a boot that has no console". Several device-tree mistakes produce exactly this symptom; the table indevice-tree/README.mdlists them. -
The USB gadget enumerates but there is no
/dev/ttyACM0.CONFIG_USB_ACMis=min the stock config, and a build withALARM_NO_MODULES=1never produces it, so the host has no driver to claim the interface. It is now=yand listed inREQUIRED_OPTIONSso it cannot regress silently. -
The boot stops at
Checking dev node for errorsand nothing more happens. The switchroot initramfs's userspacemounthangs mounting the ext4 root — the kernel logs nothing at all, no ext4 message and no error — while the kernel mounts the same filesystem without trouble. The mainline boot script therefore passesrdinit=/nonexistent, the kernel mounts the root itself viaprepare_namespace(), and/sbin/initfrom the root filesystem takes over. That also sidesteps every other initramfs trap; seepatches/README.md. -
passwddoes nothing / SSH still saysPermission denied.passwdandchpasswdroute through PAM, and Arch's PAM stack expects systemd modules that are absent here, so they can fail silently. Write the hash straight into/etc/shadow:HASH=$(openssl passwd -6 'yourpassword') cp /etc/shadow /etc/shadow.bak grep -v '^root:' /etc/shadow > /tmp/shadow.new echo "root:${HASH}:0:0:99999:7:::" >> /tmp/shadow.new cat /tmp/shadow.new > /etc/shadow -
No WiFi/BT/audio. The rootfs needs the modules and firmware that match the booted 4.9 kernel. The build imports them automatically from the switchroot image parts (
switchroot/install/l4t.00+l4t.01, which concatenate to one ext4 image with the volume labelSWR-JAM). -
Read the full boot log. systemd journals to RAM by default. Make it persistent, then read it from the card on your PC:
sudo install -d -m 755 /run/media/$USER/ALARM/var/log/journal printf '0123456789abcdef0123456789abcdef\n' | sudo tee /run/media/$USER/ALARM/etc/machine-id >/dev/null sudo install -d -m 755 /run/media/$USER/ALARM/var/log/journal/0123456789abcdef0123456789abcdef # ... boot, then re-insert the card ... sudo journalctl -D /run/media/$USER/ALARM/var/log/journal --no-pager -b | tail -250 -
Build errors. Run
cargo run -- configto review settings, andcargo run -- clean --allfor a clean rebuild.
License
MIT License — see LICENSE for details.
Acknowledgments
- NVIDIA for Tegra X1 support
- Arch Linux ARM for the base system
- hekate and switchroot teams for the L4T boot chain
Authorship / disclaimer
This repository's code, documentation (including this README), and the troubleshooting steps above were generated by a Large Language Model (LLM) AI assistant under human supervision. It may contain mistakes, hallucinated details, or unsafe instructions. Verify before running, and prefer primary sources (hekate, switchroot, Arch Linux ARM) for anything critical.
files
62 files — expand to browse
bootloader/
ini/
device-tools/
device-tree/
drivers/
patches/
reference/
switchroot-mainline/
extracted/
0004-xusb-gadget.patch0010-v0-drm-panel-add-driver-for-jdi-lpm062m326a.patch0011-v0-stmfts-switch-support.patch0012-v0-platform-integration-for-switch-panel.patch0016-v0-input-working-Joy-Con-rails-driver.patch0019-v4-HID-switchcon-add-nintendo-switch-controller-driver.patch0023-input-touschscreen-stmfts-Sync-mt-events-on-input.patch0026-v0-usb-udc-tegra_xudc-use-dma_alloc_coherent.patchREADME.mdtegra210-nintendo-switch.dtsrootfs/
switchroot/
ubuntu-jammy/
try it
$ cargo build --release
$ ALARM_NO_MODULES=1 cargo run -- kernel && cargo run -- payload --kernel built
vibe log
model: DeepSeek V4.1 Flash
Boot current Arch Linux ARM on a Nintendo Switch with a current mainline kernel rather than the vendor L4T 4.9 BSP, and document every silent failure found on the way. Prior art from switchroot's own mainline kernel tree (panel driver, PCIe lanes) was located late and is vendored under reference/ for porting.
nd download JG2HQZWB/alarm-tegra-x1
nd run JG2HQZWB/alarm-tegra-x1
nd fork JG2HQZWB/alarm-tegra-x1
/api/projects/alarm-tegra-x1/context
/api/projects/alarm-tegra-x1/context