# Mainline kernel bring-up on the Nintendo Switch — the full record
This is the working log of getting a **modern mainline kernel** to boot Arch Linux
ARM on a Nintendo Switch (HAC-001, Erista, Tegra X1 ODNX02-A2). It exists because
almost every failure in this bring-up was **silent**, and the ones that were not
silent were misleading. If you are picking this up, read §4 first — knowing how to
observe a failure is worth more than any single fix below.
The other READMEs hold the reference material:
- [`README.md`](README.md) — what the project is and its current status
- [`patches/README.md`](patches/README.md) — the initramfs, the mainline boot path, the PID 1 scripts
- [`device-tree/README.md`](device-tree/README.md) — the device tree and its traps
---
## 1. Goal and result
**Goal:** current userspace (Arch Linux ARM, systemd 261) on a current kernel,
instead of the vendor's frozen **L4T 4.9.140** BSP.
**Result:**
```text
[root@switch /]# uname -a
Linux switch 7.2.0-dirty #26 SMP PREEMPT ... aarch64 GNU/Linux
[root@switch /]# lsblk
mmcblk0 179:0 0 119.3G 0 disk
|-mmcblk0p1 179:1 0 31.1G 0 part /mnt/boot
|-mmcblk0p2 179:2 0 59G 0 part /
`-mmcblk0p3 179:3 0 29.2G 0 part
```
A 7.2 kernel, built by this project, boots on the console, mounts the Arch root
filesystem, and gives a root shell over USB serial. Working on that kernel:
microSD with card detect, the Tegra XUDC USB console, the arch timer, PSCI and
SMP, and warm reboots.
---
## 2. The boot chain
```text
hekate (RCM inject) -> L4T bootstack -> U-Boot (bl33) -> boot.scr -> Linux
```
- hekate loads `bl31.bin` (ATF) and `bl33.bin` (a U-Boot build) from
`switchroot/alarm/`, and U-Boot then runs `boot.scr` from the same directory.
- `boot.scr` is generated from `device-tree/boot-mainline.cmd` with `mkimage`.
The stock L4T script cannot be reused: it loads a Device Tree *image*
(`nx-plat.dtimg`), picks an entry by SKU, and validates it against
`${fdt_id_text}`/`${fdt_rev}` — properties only L4T device trees carry. A
mainline DTB fails that check and the script does `sleep 10; reset`, i.e. a
boot loop with nothing on screen.
---
## 3. Hardware facts worth knowing
- **No wired UART on a retail unit.** UART-A (`0x70006000`) is not registered
(B/C/D are), so `earlycon` there is useless and can hang the bus.
- **The panel scanout is block-linear.** Anything written linearly (U-Boot text,
`fbcon`, a DRM log client, `mw` fills) renders as speckle. *Solid* fills do
work, because a uniform colour is uniform in any layout — which is why the boot
script's progress colours are readable.
- **hekate preserves a 2 MiB region at `0xB0000000`** across a warm reboot
(`PSTORE_ADDR`/`PSTORE_SZ` in `bdk/memory_map.h`). This is the single most
valuable fact in the whole document — see §4.2.
- **The Switch's reset path is the PMC, not PSCI.** ATF's R2P ("reset to
payload") returns a warm reboot to hekate. See §6.
- **SD layout** as used here: `p1` FAT (`SWITCH SD`), `p2` ext4 (Arch root),
`p3` scratch/swap. `mmcblk0p1` is the microSD on `sdhci@700b0000`.
---
## 4. Observing a failure — the hard part
Every early attempt failed *silently*: no UART, an unreadable display, and a
kernel that died before `printk` reached anything persistent. Channels that do
**not** work, and why:
| Channel | Why it fails |
|---|---|
| Display / `fbcon` / DRM log client | block-linear scanout renders text as speckle |
| UART-A `earlycon` | the port is not registered on retail units |
| `pstore_blk` on a partition | needs a working block device before the SD driver is up |
| hekate's `L4T_panic.bin` | only fires when `h_err & ERR_L4T_KERNEL`, i.e. an *L4T* kernel panic |
| U-Boot `echo` / `fdt print` | goes to the unreadable display |
### 4.1 FAT-partition files (userspace, no reboot needed)
Once userspace runs, it can just write to the FAT boot partition, which a PC
reads directly. `/sbin/init` and `/etc/alarm-init.sh` publish mounts, UDC state,
`ttyGS` nodes, the process list and `dmesg` to `ALARM_USERS.txt` / `ALARM_UP.txt`.
Two traps here, both of which cost time:
- **`CONFIG_DEVTMPFS_MOUNT` does not help an initramfs.** That option only mounts
devtmpfs from `prepare_namespace()`, which never runs when the initramfs
supplies `/init`. So `/dev` has *no device nodes* until the initramfs mounts
devtmpfs itself — and an early diagnostic block placed before that point
silently does nothing.
- **The SD probe is asynchronous.** `/init` can start before the card has been
scanned and its partitions created, so `/dev/mmcblk0p1` may not exist yet.
Wait for it.
### 4.2 ramoops plus a warm reboot (kernel, no userspace needed)
The kernel logs continuously into the ramoops region at `0xB0000000` (see the
device tree). A **panic reboots warm via the PMC**, hekate preserves that region,
and the next boot dumps it to the SD card for reading on a PC.
The boot script copies 2 MiB with raw `mmc write` into scratch space:
```sh
sudo dd if=/dev/sda bs=512 skip=188944384 count=4096 of=pstore-ram.bin status=none
```
Then extract the log text. The region holds a `dmesg` record (written on panic)
and a continuous console zone, so **a hang is recorded too** — but only if the
machine warm-reboots, which is why §6 mattered so much.
`mmc write` gotchas, both of which make it fail *silently*:
- syntax is `mmc write <addr> <blk#> <cnt>` — no device argument; select with
`mmc dev` first
- `blk#` and `cnt` are parsed with `hextoul()`, i.e. **hexadecimal**. A decimal
block number is read as a huge hex value, lands past the end of the card, and
writes nothing.
### 4.3 The trap that wasted the most time: a *cold* reboot
The markers were written, the region was dumped, and it was always empty. The
cause was not the kernel at all: **the console was being power-cycled between
boots.** A power-off drops DRAM. Only a warm reboot (a SoC reset, which ATF's R2P
redirects back to hekate) preserves the region. Once the panic path worked, the
kernel rebooted *itself*, warm, and the log finally survived.
---
## 5. Kernel configuration traps
- **Seven Kconfig symbols in the generator did not exist.** Most importantly
`CONFIG_PCIE_TEGRA` — the real symbol is `CONFIG_PCI_TEGRA` (`pci-tegra.c`
handles `nvidia,tegra210-pcie`) — which is why the kernel was built with no
PCIe and therefore no WiFi. Also `ARM_TEGRA_MC`→`TEGRA_MC`, and dropped
`MMC_TEGRA`, `COMMON_CLK_TEGRA`, `ARM_CPU_TOPOLOGY`, `CAKE`, `USB_DWC3_TEGRA`.
- **`REQUIRED_OPTIONS` / `FORBIDDEN_OPTIONS`** now read back the `.config` that
`olddefconfig` produced and **fail the build** if anything silently dropped.
Silent drops are the entire failure mode here.
- **`CONFIG_USB_ACM=m`** and a build with `ALARM_NO_MODULES=1` meant the *host*
had no `cdc_acm` driver: the gadget enumerated perfectly and no
`/dev/ttyACM0` ever appeared. Now `=y`, and in `REQUIRED_OPTIONS`.
- **`make` segfaults** building modules. Not a toolchain bug: GNU make recurses
on the C stack for `$(foreach)`, `Makefile.modfinal` iterates every `=m`
symbol, and the arm64 defconfig queues 1400+ of them — overflowing the default
8 MiB stack. Every `make` now runs through `run_make()`, which raises
`RLIMIT_STACK` first.
---
## 6. The kernel fix that unlocked everything: panic must reboot
A panic simply **hung**. Two things were wrong:
- PSCI's `SYSTEM_RESET` is non-functional on this hardware.
- The Tegra PMC restart handler was registered at `SYS_OFF_PRIO_LOW` (224), which
puts it **behind** PSCI's priority 129 on the *same* notifier chain — so PSCI
was always tried first and never returned.
Both `register_restart_handler` and `devm_register_sys_off_handler(SYS_OFF_MODE_RESTART)`
land on `restart_handler_list`, ordered by priority, lowest first. The PMC
handler is now registered at `SYS_OFF_PRIO_FIRMWARE` (128), ahead of PSCI, and
`tegra_pmc_restart()` sets `PMC_CNTRL_MAIN_RST` — a warm reset that ATF's R2P
catches and returns to hekate with DRAM intact.
**This is what made every other diagnosis possible.** Without it, a dead kernel
left no readable trace at all.
A related dead end worth recording: a *deliberate* panic placed too early
(`setup_arch`) also hangs, because the PMC driver has not probed yet and PSCI is
all there is. A control panic has to go after `do_basic_setup()`.
---
## 7. Device-tree traps
Full table in [`device-tree/README.md`](device-tree/README.md). The short version,
all silent:
| Property | Symptom | Cause |
|---|---|---|
| `/psci` + `enable-method` | completely silent death before console init | `tegra210.dtsi` provides neither, so arm64 never sets `cpu_ops` and SMP bring-up dereferences NULL |
| `timer { clock-frequency = <19200000> }` | `calibrate_delay()` spins forever | the dtsi omits it and this U-Boot does not program `CNTFRQ_EL0` |
| `cd-gpios = <&gpio TEGRA_GPIO(Z, 1) GPIO_ACTIVE_LOW>` + `disable-wp` | no `mmcblk0` at all, then `VFS: Unable to mount root fs on unknown-block(0,0)` | the controller's own card-detect bit is not wired. L4T has `cd-gpios = <0x5c 0xc9 0>`; `0xc9` is 201 = `TEGRA_GPIO(Z, 1)`, and mainline's own Jetson Nano uses that exact pin for SDMMC1 |
| `no-1-8-v`, `vqmmc-supply` at 3.3 V | UHS negotiation against a rail that cannot switch | the pads run at 3.3 V and the rails are fixed stand-ins |
| `usb-role-switch` + `role-switch-default-mode` | `tegra-xusb-padctl: failed to setup XUSB ports: -22`, all USB dead | the driver returns `-EINVAL` for any OTG/peripheral port without `usb-role-switch` |
| `phy-names = "usb2-0"` on `usb@700d0000` | gadget binds, `/dev/ttyGS0` exists, host sees **nothing** | `tegra_xudc_probe()` looks the PHY up as `"usb2-%d"` via `devm_phy_optional_get()`. Being *optional*, a wrong name fails silently: `usbphy[0]` stays NULL, the VBUS notifier is never registered, `device_mode` never becomes true |
| `pcie@1003000` enabled + `pci@2,0` | `dmesg` says nothing about PCIe at all | the controller was simply left disabled |
Two more debugging notes:
- **`pinconf-pins` is useless on Tegra.** `tegra_pinconf_dbg_show()` is an empty
function, so every pin prints as `pin 56 (WIFI_EN PH0): ` with nothing after
the colon, configured or not. Use `pinmux-pins` (owner/function/group) and
`pinconf-groups` instead.
- **`*/` inside a DT comment closes it.** Writing
`/sys/kernel/debug/pinctrl/*/pinconf-pins` in a `/* ... */` comment ends the
comment early and the lexer fails with a bewildering
`Unexpected 'the'` error.
---
## 8. Initramfs traps
The switchroot L4T initramfs is **bypassed** on the mainline path
(`rdinit=/nonexistent`), but it is still shipped, so the traps are recorded in
[`patches/README.md`](patches/README.md). Each was silent in a different way:
| Trap | What you see |
|---|---|
| `tegra_gpio` format mismatch | drops into `exec /bin/bash` and hangs forever — mainline's `gpio-tegra.c` creates a file of the *same name* with a different format, so the parse yields `0` = "both volume buttons held" |
| `ply-image` can block | boot stops with no output; it is called unconditionally, including right after the gadget setup |
| userspace `mount` hangs | stops right after `Checking dev node for errors`; `mount -t ext4 ... /mnt` hangs *inside the userspace binary* while the kernel mounts the same filesystem fine |
| `fsck.ext4` is a dangling symlink | silently does nothing (points at a nonexistent `fsck.ext3`) |
| `cut` is missing | `cut: command not found` |
| `boot_m`/`boot_p` cmdline args | `mount_boot_disk` finds nothing |
**This is why the mainline path skips the initramfs.** The kernel mounts the root
itself via `prepare_namespace()`, and `/sbin/init` from the root filesystem takes
over. That sidesteps the entire class of problems at once.
---
## 9. The final architecture
```text
boot.scr
bootargs ... rdinit=/nonexistent rootwait=30 panic=10
bootm ${kernload} ${initaddr} ${fdtraddr}
kernel
mounts /dev/mmcblk0p2 itself (prepare_namespace)
runs /sbin/init
/sbin/init (rootfs/sbin-init, installed by alarm-build rootfs)
mounts proc/sys/dev/devpts, sets the hostname
builds the USB CDC-ACM gadget on 700d0000.usb and binds it
verifies/forces the padctl port role to "device"
publishes ALARM_USERS.txt to the FAT partition
execs /etc/alarm-init.sh
/etc/alarm-init.sh (rootfs/alarm-init.sh)
stty -F /dev/ttyGS0 sane
setsid /bin/bash --rcfile /etc/alarm-console.bashrc -i \
< /dev/ttyGS0 > /dev/ttyGS0 2>&1 &
WiFi + sshd, then keeps a shell alive as PID 1
```
Three deliberate details in the console shell:
- **All three descriptors bound explicitly.** `agetty -n -l /bin/bash ttyGS0`
produced a prompt but no command output: input reached the shell, output never
came back down the port.
- **`stty sane` first**, so a CR-only terminal submits lines.
- **`--rcfile` disabling bracketed paste**, because bash's `ESC[?2004h` gets
partially echoed by minicom and the fragment is executed as a bogus command.
`rootwait=30` matters on this path: it converts an unfindable root from an
eternal silent sleep into a panic, which ramoops records and the PMC reboots.
---
## 10. Bring-up workflow
```sh
# kernel (config + Image + dtbs), no modules
ALARM_NO_MODULES=1 cargo run -- kernel
# device tree only
make -C build/kernel/src ARCH=arm64 LLVM=1 LLVM_IAS=1 dtbs
# boot script
mkimage -A arm64 -O linux -T script -C none -n "ALARM mainline boot" \
-d device-tree/boot-mainline.cmd device-tree/boot-mainline.scr
# assemble the boot tree
cargo run -- payload --kernel built
```
Getting files onto the card: **UMS from hekate** exposes the SD to the PC, and
`udisksctl mount -b /dev/sdX1` works without root for the FAT partition. The
ext4 root partition is root-owned, so writing `/sbin/init` or `/etc/alarm-init.sh`
into it needs `sudo` — which is why `alarm-build rootfs` installs them at build
time and a running card is patched with an explicit `sudo cp`.
---
## 11. Prior art: switchroot's mainline kernel
Everything in this project was written from scratch, and late in the work it
turned out that **switchroot have already done mainline bring-up for the
Switch**:
- `https://gitlab.com/switchroot/kernel/mainline-kernel` (branch `switch-dev`)
- a 2425-line `tegra210-nintendo-switch.dts`
- a `switch-patches/` series of 26 patches that is effectively a map of the job
Several things still open here are solved there. The reference material is
vendored in [`reference/switchroot-mainline/`](reference/switchroot-mainline/README.md),
which documents each patch and what their device tree says about the display, the
PCIe lanes and the USB path.
The most immediately valuable pieces:
- **The panel is identified**: a **JDI LPM062M326A**, reset `PV2` active high,
`pavdd`/`navdd` supplies. Patch 0010 is the driver, 0012 the integration. This
was the piece that could not be derived from the L4T device tree, because the
vendor kernel keeps the timings and init sequence in C.
- **PCIe enables both ports**, with `pci@1,0` on lanes `pcie-1` through `pcie-4`.
This project enabled only `pci@2,0` on lane `pcie-0`, which is a plausible
reason the endpoint never answered config cycles.
- **Patch 0026** is a 25-line DMA fix in the XUDC driver, and a very likely cause
of the USB stalls that make bulk transfers over the gadget unreliable.
Their tree is an **older mainline** than 7.2, so bindings have moved
(`xudc@700d0000` → `usb@700d0000`, `phy-names = "usb2"` → `"usb2-0"`). Treat it
as a reference to port from, not to drop in.
## 12. Remaining work
1. **The display** — the panel is identified (JDI LPM062M326A), its driver is
ported from switchroot's patch 0010, and the DC/DSI/backlight/rails wiring is
in the tree. What remains is verifying it on hardware: mainline's T210 DSI
path is thin (only `tegra210-smaug` uses it), so the panel driver binding
cleanly is not guaranteed.
2. **WiFi.** The link trains and the root port enumerates — the MAX77620 `ldo1`
rail was the missing piece. switchroot's device tree showed two further gaps
now filled here: **both** PCIe ports enabled, and the padctl carrying lanes
`pcie-1`…`pcie-4` rather than only `pcie-0`. Whether that makes the endpoint
answer config cycles is the open question; if it does not, the next lead is a
proper reset pulse on `wifi_rst` after the rails are stable.
3. **USB link reliability.** The NCM gadget works but stalls under sustained
transfer (~550 KiB/s, erratic latency, UDP often lost). Patch 0026 was
investigated and is a **no-op** for 7.2 — it is a 4.9-era
`dma_zalloc_coherent` → `dma_alloc_coherent` rename that upstream already
made — so the cause is still unknown. Worth revisiting, since it affects any
large download on the device.
4. **systemd.** Its baseline is kernel 4.15 and this project boots 7.2, so it is
viable: point `/sbin/init` at `/usr/lib/systemd/systemd` and work through the
first boot.
5. **Kernel modules.** Drop `ALARM_NO_MODULES=1` and the ~1450 `=m` drivers come
back, removing a whole class of "silently absent" problems.
6. **Clock persistence.** The Tegra RTC reads 1970, which makes every package
signature look invalid; `hwclock -f /dev/rtc1 -w` failed and NTP does not
survive this link (UDP is unreliable). `/sbin/init` now reads the PMIC RTC at
boot, but the write side is still unresolved.
7. **The rest of switchroot's patch set** — Joy-Cons (0016/0019), touchscreen
(0011/0023), fan (0015), USB-PD (0020) — all identified and available to port.