# 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 ` — 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.