alarm-tegra-x1 0.1.0

Arch Linux ARM on the Nintendo Switch, on a modern mainline kernel

nintendo-switchtegra-x1arch-linux-armmainline-kernelkernel-bringupdevice-treeembeddedhandheld rustdevice-treeu-bootclangkconfigdrmdsiusb-gadgetncmramoops RustShellC

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 of BRINGUP.md.

Published on notdoublehub as alarm-tegra-x1. The manifest is notdouble.toml; .ndignore excludes the user photos and videos, the l4t-kernel-4.9 clone (~930 MB), the switchroot rootfs parts (~4 GB each) and the build//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/ttyACM0 on the host, /dev/ttyGS0 on the device).
  • alarm-build CLI (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 our boot.scr.
  • On the mainline kernel: microSD (SDHCI plus card detect), the Tegra XUDC USB serial gadget, the arch timer, PSCI and SMP all work.
  • sshd and a console shell start at boot. On the L4T entry, WiFi, DHCP, DNS and pacman work 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 -r drawing 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. See device-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 ldo1 rail was the missing piece — but the endpoint never answers config cycles, and the resulting asynchronous SError panics the kernel and costs userspace entirely. See device-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, no NetworkManager and no user session management.
  • Kernel modules are not built. The kernel is built with ALARM_NO_MODULES=1, so anything configured =m is simply absent. That silently broke the host-side USB console driver (CONFIG_USB_ACM, now built in) and is the reason REQUIRED_OPTIONS exists.

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

  1. Insert the flashed SD card.
  2. Enter RCM (hold VOL UP + POWER) and inject hekate.
  3. 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 own bootloader/sys/ files are missing from the SD card. The build now copies the full hekate release bundle, so regenerate with payload (or set HEKATE_BOOTLOADER_DIR).

  • Selecting the entry bounces straight back to hekate. boot.scr runs reset (which returns to hekate) when it cannot load the kernel/initramfs/dtimg, and hekate's L4T loader returns to the menu on Path missing / Minerva missing / BL31 missing / BL33 missing / loading BPMP-FW errors. 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 disables orphan_file and metadata_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 to switch_root unless /etc/switchroot_version.conf exists 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.sh instead of /usr/lib/systemd/systemd. If you later switch to a newer Switch-capable kernel (e.g. L4S), re-run patches/patch-initramfs.sh without the /sbin/init replacement 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:

    1. cgroup v2 mounted at /sys/fs/cgroup. With a shell as PID 1 there is nothing to mount it. Confirm CONFIG_CGROUPS and the unified hierarchy, and that /sys/fs/cgroup exists before systemd starts. (/etc/alarm-init.sh does not mount it today.)
    2. A /etc/machine-id to regenerate. The ALARM tarball ships an empty one and journald needs it; alarm-build rootfs writes a random value (src/rootfs.rs), and the builder patches an existing card too.
    3. devtmpfs + a writable /etc and /var. With a shell as PID 1 there is no udev, so /dev must come from devtmpfs. CONFIG_DEVTMPFS_MOUNT is not enough here — it only mounts devtmpfs from prepare_namespace(), which never runs when an initramfs supplies /init. See patches/README.md (line 105) and BRINGUP.md §4.

    Also note bootargs_extra in sdcard-snapshot/L4T-alarm.ini still carries systemd.unified_cgroup_hierarchy=0, which is a leftover from the 4.9 bring-up and fights the second item above.

  • A panic does not reboot the console — it just hangs. Two things were wrong. PSCI's SYSTEM_RESET is non-functional on this hardware, and the Tegra PMC restart handler was registered at SYS_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 at SYS_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 — see patches/README.md, "Reading a boot that has no console". Several device-tree mistakes produce exactly this symptom; the table in device-tree/README.md lists them.

  • The USB gadget enumerates but there is no /dev/ttyACM0. CONFIG_USB_ACM is =m in the stock config, and a build with ALARM_NO_MODULES=1 never produces it, so the host has no driver to claim the interface. It is now =y and listed in REQUIRED_OPTIONS so it cannot regress silently.

  • The boot stops at Checking dev node for errors and nothing more happens. The switchroot initramfs's userspace mount hangs 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 passes rdinit=/nonexistent, the kernel mounts the root itself via prepare_namespace(), and /sbin/init from the root filesystem takes over. That also sidesteps every other initramfs trap; see patches/README.md.

  • passwd does nothing / SSH still says Permission denied. passwd and chpasswd route 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 label SWR-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 -- config to review settings, and cargo run -- clean --all for 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

click a file to view it · full files tab ↗

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.

⑂ fork download bundle (.tar.gz) 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 license readme recent small · by kenton · maintainer JG2HQZWB… · 62 files · 1502018 bytes · 20 downloads · 53 views · updated 2026-10-19

doubles — similar projects

all doubles →