# 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](#status) and
> [`patches/README.md`](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`](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/`](reference/switchroot-mainline/README.md) and
> §11 of `BRINGUP.md`.
>
> Published on notdoublehub as
> [`alarm-tegra-x1`](https://notdouble.com/p/JG2HQZWB/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`](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`](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](#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:
```ini
[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/`](patches/README.md)) |
| `nx-plat.dtimg` | Platform device trees (Tegra DC/SOR/DSI panel, regulators, …) |
| `boot.scr` | U-Boot boot script that loads the above and `bootm`s |
| `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
```bash
# 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`):
```bash
# 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
```bash
# 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
```bash
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:
```sh
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:
```bash
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`](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`](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`](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`:
```sh
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:
```bash
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](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.