# Mainline kernel bring-up for the Nintendo Switch The point of this project is to boot a **modern mainline kernel** on the Switch so that current Arch Linux ARM (systemd 261, cgroup v2, the new mount API) runs natively — instead of the vendor 4.9.140 L4T BSP. ## Why this is hard The Switch's display, clocks, power domains and fixed-function blocks (NVDEC…) are only described by NVIDIA's downstream L4T device trees and only driven by the L4T tree. Upstream has **no Switch board support**: there is no `nintendo`-compatible DTS in `arch/arm64/boot/dts/nvidia/`, and the GPU is driven by `nouveau` while the display has to be described by hand. So the work is: a board device tree, the right Kconfig, and a boot script. ## Boot chain (unchanged, and reusable) ``` hekate (RCM inject) -> L4T bootstack -> U-Boot (bl33) -> boot.scr -> Linux ``` hekate loads `bl31.bin` + `bl33.bin` (a U-Boot build) from `switchroot/alarm/`, and U-Boot then runs `boot.scr` from the same directory. ### The console problem, and the trick that solves it A retail Switch has **no wired UART**, and the panel scanout is block-linear, so anything drawn into it renders as speckle. The only usable console is a **USB CDC-ACM gadget on the Tegra XUDC** (`700d0000.usb`), which gives `/dev/ttyGS0` on the device and `/dev/ttyACM0` on the PC. Two things must both be true, and each fails silently on its own: 1. **Device side** — the gadget has to be built and bound. The switchroot initramfs does this through `usb-serial-mode-start` when `usb_logging` is on the kernel command line, but the mainline boot path bypasses that initramfs entirely, so `/sbin/init` builds the gadget itself (see `patches/README.md`). 2. **Host side** — `CONFIG_USB_ACM` has to be built in. It is `=m` in the stock config, and a build with `ALARM_NO_MODULES=1` never produces it, so the gadget enumerates perfectly (`lsusb` shows `0955:7020`) and no `/dev/ttyACM0` ever appears. The kernel config therefore insists on `USB_TEGRA_XUDC`, `PHY_TEGRA_XUSB`, `USB_CONFIGFS`, `USB_CONFIGFS_ACM` **and `USB_ACM`**, all listed in `REQUIRED_OPTIONS` so that a silent regression fails the build instead of producing a mute console. ## What is done | Item | State | |---|---| | Kconfig generator | rewritten — see below | | `REQUIRED_OPTIONS` build assertion | added (`verify_required_options`) | | Device tree `device-tree/tegra210-nintendo-switch.dts` | **boots to userspace** | | Boot script `device-tree/boot-mainline.cmd` → `.scr` | **boots** | | `payload --kernel built` ships `boot.scr` + `switch.dtb` | done | | Switch panel driver (`device-tree/panel-jdi-lpm062m326a.c`) | ported from switchroot patch 0010; installed by `install_switch_panel()` | | USB serial console (`/dev/ttyACM0`) | working | | USB networking (CDC-NCM, `/sbin/init`) | working — `ssh root@192.168.7.2` | | Headless desktop (Xvfb + openbox + x11vnc) | installed and running on the device | | **Result** | **Arch Linux ARM userspace on a mainline 7.2 kernel, root shell over `/dev/ttyACM0`, SSH over USB, and a VNC desktop** | ### The Kconfig bug that was found `generate_tegra_config` asked for **seven symbols that do not exist** — most importantly `CONFIG_PCIE_TEGRA`, which is why the kernel was built **without PCIe and therefore without WiFi**. The driver is `CONFIG_PCI_TEGRA` (`pci-tegra.c` handles `nvidia,tegra210-pcie`), and the in-code comment claimed the opposite. Silent drops like that are the whole failure mode, so the build now reads back the `.config` `olddefconfig` produced and **fails** if anything in `REQUIRED_OPTIONS` is missing. It also installs `switch-fb` *before* `olddefconfig` (it used to be installed after, so `CONFIG_SWITCH_FB` was always dropped and the driver never built). ### Why the stock `boot.scr` cannot be reused The L4T script loads `nx-plat.dtimg` (a DTIMG, magic `d7b7ab1e`), selects an entry by SKU, and then sanity-checks it against `${fdt_id_text}` / `${fdt_rev}`. Those properties only exist in L4T device trees, so a mainline DTB fails the check and the script does `sleep 10; reset` — a boot loop with nothing on screen. `boot-mainline.cmd` replaces all of it with a plain load-kernel/load-initramfs/load-dtb/`bootm`. ## Device tree status Working (in the tree now): - 2 GiB at `0x80000000` and `clk32k_in` (required by `pmc@7000e400`) - **PSCI** — `/psci { compatible = "arm,psci-1.0"; method = "smc"; }` plus `enable-method = "psci"` on each CPU. `tegra210.dtsi` provides neither, and without them arm64 never sets `cpu_ops`, so SMP bring-up dereferences NULL before console init: a completely silent death - **arch timer `clock-frequency = <19200000>`** — `tegra210.dtsi` omits it and this U-Boot does not program `CNTFRQ_EL0`. Without it the kernel has no clock and `calibrate_delay()` spins forever - **microSD** `mmc@700b0000` (where the root filesystem lives) - **XUSB device controller** `usb@700d0000` + `padctl@7009f000` (the console) `regulator-fixed` stand-ins are used for the PMIC rails so stage 1 does not depend on a driver that has not been validated. ### The device-tree traps Every one of these produced a silent failure, and every fix is small. | Property | Symptom without it | Why | |---|---|---| | `cd-gpios = <&gpio TEGRA_GPIO(Z, 1) GPIO_ACTIVE_LOW>` + `disable-wp` | no `mmcblk0` at all. The SDHCI controller binds, reports "no card", and the kernel dies at `VFS: Unable to mount root fs on unknown-block(0,0)` | the controller's own card-detect bit is not wired on this board. L4T has `cd-gpios = <0x5c 0xc9 0>` + `cd-inverted`; `0xc9` is 201 = `TEGRA_GPIO(Z, 1)`, and mainline's own Jetson Nano (`tegra210-p3450-0000`) uses that exact pin for SDMMC1 | | `no-1-8-v`, and `vqmmc-supply` at 3.3 V | UHS negotiation against a rail that cannot switch | the SD pads run at 3.3 V and the rails here are fixed stand-ins, not the real MAX77620 | | `usb-role-switch` + `role-switch-default-mode = "peripheral"` on the padctl `usb2-0` port | `tegra-xusb-padctl: failed to setup XUSB ports: -22`, and all USB dead | the driver returns `-EINVAL` for any OTG/peripheral port without `usb-role-switch` (`drivers/phy/tegra/xusb.c`) | | `phy-names = "usb2-0"` on `usb@700d0000` | the gadget binds, `/dev/ttyGS0` appears, and the host sees **nothing at all** | `tegra_xudc_probe()` looks the PHY up as `"usb2-%d"` with `devm_phy_optional_get()`. Because the lookup is *optional*, a wrong name fails silently: `usbphy[0]` stays NULL, the VBUS notifier is never registered, `device_mode` never becomes true, and the device never connects | | `pcie@1003000` enabled, with both ports and lanes `pcie-0`…`pcie-4` | with the controller left `disabled`, `dmesg` says nothing about PCIe at all | the controller has to be enabled *and* both `pci@1,0`/`pci@2,0` present, with the padctl carrying every lane they reference. Enabling only `pci@2,0` on `pcie-0` trained the link but the endpoint never answered config cycles | | overrides written at the **root** for a node that lives under `host1x@50000000` | the device never probes and `dmesg` never mentions it; `/sys/class/drm/` has `card0` and `renderD128` but **no connectors** | a device tree merges by **path**. `dsi@54300000 { status = "okay"; }` at the root does not enable `host1x@50000000/dsi@54300000` — it creates a *second*, disconnected node and the real one stays `disabled`. The DTBs are addressable by label (`&dsia`) or full path (`&{/host1x@50000000/dc@54200000}`), and the DC and DSI are the only nodes here with this problem | Note that `usb@70090000` (XHCI, host mode) already used the `"usb2-0"` form, which is what made the XUDC's `"usb2"` easy to overlook. Not yet described (stage 2/3): - MAX77620 PMIC and the real regulators (SD/USB/display/WiFi power) - the DSI panel (`dsia`) and the PWM backlight - Joy-Con rails, `als`, audio ### WiFi: the state of play The BCM4356 sits on **PCIe port 1** (`pci@2,0`), one lane, on the padctl's `pcie-0` lane. The controller is now **enabled**, following switchroot's mainline device tree, which also revealed two gaps in what this project had: - they enable **both** ports, not just `pci@2,0` - their padctl carries lanes **`pcie-1`…`pcie-4`** as well as `pcie-0`; this tree had only `pcie-0` Both are now in place. An earlier attempt enabled only `pci@2,0` on `pcie-0`, trained the link and then panicked the kernel on the first config read of the endpoint — see "What still failed" below, which is worth reading before touching this again. **What works, and why the link now trains.** The chip is powered from three MAX77620 rails, and this was the actual blocker for a long time: | rail | name | role | |---|---|---| | `sd3` | vdd-1v8 | `hvddio-pex`, `vddio-pex-ctl` | | `ldo1` | vdd-pex | `dvddio-pex` | | `ldo7` | vdd-gen-pll-edp | padctl `avdd-pll-uerefe` | The decisive detail: **`ldo1` has neither `regulator-boot-on` nor `regulator-always-on` in L4T — it is off until a consumer enables it.** Our earlier tree stood in for these with `regulator-fixed`, which the kernel treats as always-on but which never touches the PMIC. So `ldo1` stayed off, the chip stayed unpowered, and PCIe reported `link 1 down` no matter what the gating pins did. With a real `max77620` node and those rails on: ```text max77620 0-003c: PMIC Version OTP:0x35 and ES:0x8 tegra-pcie 1003000.pcie: 4x1, 1x1 configuration tegra-pcie 1003000.pcie: probing port 1, using 1 lanes tegra-pcie 1003000.pcie: PCI host bridge to bus 0000:00 pci 0000:00:02.0: [10de:0faf] type 01 class 0x060400 PCIe Root Port ``` Note the absence of `link 1 down`: the driver only logs on failure, so silence here means **the link trained**. Also in place: the gating pins are muxed and driven (`pinmux-pins` shows `pin 56 (WIFI_EN PH0): 1003000.pcie ... group wifi_en_ph0`, and `/sys/kernel/debug/gpio` shows `gpio-56 |wifi-en out hi`), the pinmux state is attached to the **PCIe node** so the pinctrl core applies it deterministically at probe, and the firmware is present (`brcmfmac4356-pcie.bin` resolves through the cypress symlink). **What still fails.** The root port enumerates, then the kernel assigns bus 1 and reads the endpoint's config space — and the endpoint never answers: ```text pci 0000:00:02.0: bridge configuration invalid ([bus 00-00]), reconfiguring [ 31.552] After 10 seconds, these CPUS still haven't responded to the NMI: 1 [ 42.640] SError Interrupt on CPU1 ... pci_generic_config_read+0x4c/0xb4 tegra_pcie_config_read+0x14/0x28 tegra_pcie_probe+0x5a4/0x1178 [ 42.640] Kernel panic - not syncing: Asynchronous SError Interrupt ``` So: link up, root port fine, endpoint silent. The config read hangs ~40 s and then aborts as an asynchronous SError, which panics the whole kernel. **Next thing to try: a reset pulse.** We drive `wifi_rst` (PH1) high from the first moment, so a chip that needs PERST# held low while the rails come up, then released, never sees one. Sequencing that release after the rails are stable is the most promising remaining lead. **And separately: the SError itself is a robustness bug.** `tegra_pcie_probe` registers the error ISR in `tegra_pcie_get_resources()` *before* `pci_host_probe()` scans, and `tegra_pcie_isr` explicitly handles master aborts with the comment that they "happen a lot during enumeration". A config abort escalating to a fatal SError means it is not being reported as an AFI error at all. That is worth fixing on its own merits — it is what turned a missing peripheral into an unbootable machine. ### Debugging gotcha: don't add an I2C pinctrl state for the PMIC bus The PMIC is on `i2c@7000d000`, whose pins are `pwr_i2c_scl_py3` / `pwr_i2c_sda_py4` (function `i2cpmu`), and L4T does configure them explicitly. Adding the same state to mainline **broke the PMIC outright**: ```text max77620 0-003c: Failed to read CID: -110 max77620 0-003c: probe with driver max77620 failed with error -110 ``` `-110` is `ETIMEDOUT`. U-Boot has already left those pins in a working state, and the Tegra pinmux driver only writes pins that a DT state names — so overriding them was strictly worse. The `i2c@7000d000` node therefore carries no pinctrl state, deliberately, and says so in a comment. ### Display **The panel works on mainline.** Verified by setting the mode from userspace: ``` Encoder 68: type DSI Connector 67: DSI-1 status connected 77x137 mm #0 720x1280 60.00 720 856 928 1000 1280 1290 1292 1300 78000 modetest -M tegra -r -> full 720x1280 test pattern on the panel ``` That proves the DC, the DSI host, the panel driver and its init sequence, the `pavdd`/`navdd`/`vdd_dsi_csi` rails, and the backlight all work. The key piece came from **switchroot's mainline tree**, not from the L4T device tree: the panel is a **JDI LPM062M326A**, and the vendor kernel keeps its timings and init sequence in C (`compatible = "o,720-1280-6-0"`), so the L4T device tree was no help — it carries only two GPIOs. Their patch 0010 is that driver, ported here as `device-tree/panel-jdi-lpm062m326a.c` and installed by `install_switch_panel()`. Wiring, from their device tree and the L4T tree: ```text dsi@54300000 avdd-dsi-csi-supply = <&vdd_dsi_csi> panel@0 compatible = "jdi,lpm062m326a" reset-gpios = <&gpio TEGRA_GPIO(V, 2) GPIO_ACTIVE_HIGH> vdd1-supply = <&pavdd> (PI0, 5.4V fixed) vdd2-supply = <&navdd> (PI1, 5.4V fixed) backlight = <&backlight> backlight pwm-backlight, pwms = <&pwm 0 35000> enable-gpios = <&gpio TEGRA_GPIO(V, 1) GPIO_ACTIVE_HIGH> vdd_dsi_csi fixed 1.2V, gated by MAX77620 GPIO7 ``` The panel reset is **active high** (`PV2`) and the backlight enable is `PV1` — the same two pins the L4T tree calls the panel reset and `lcd-bl-en`. **There is no boot console on the panel, and that is an upstream limitation.** This is worth understanding before trying to fix it. `tegra_drm_probe()` calls `drm_client_setup(drm, NULL)`, but current mainline only ships fbdev backends for three GEM implementations: ```text drivers/gpu/drm/drm_fbdev_dma.c GEM-DMA drivers/gpu/drm/drm_fbdev_shmem.c GEM-shmem drivers/gpu/drm/drm_fbdev_ttm.c TTM ``` and tegra uses **none of them** — it has its own `tegra_bo` / `tegra_gem_object_funcs` in `drivers/gpu/drm/tegra/gem.c`. The generic `drm_fbdev_generic.c` that used to serve any GEM driver was removed upstream. So no fbdev client is created, there is no `/dev/fb0`, and fbcon has nothing to attach to: ```text /sys/class/drm/ card0 card0-DSI-1 renderD128 /sys/class/graphics/ fbcon <- no fb0 card0-DSI-1/status connected ``` The fix is upstream work: migrate tegra's GEM to `drm_gem_shmem` or GEM-DMA so a fbdev backend applies. Not a device-tree change. `patches/kernel-display-hotplug.patch` in this project changes `tegra_dsi_host_attach()` to call `drm_kms_helper_connector_hotplug_event()` instead of `drm_helper_hpd_irq_event()`, which skips connectors without `DRM_CONNECTOR_POLL_HPD`: ```c if (!(connector->polled & DRM_CONNECTOR_POLL_HPD)) continue; ``` That change is **correct but currently moot** — it fixes a real no-op hotplug, but with no fbdev client registered there is nothing for the hotplug to retry. It is kept because it is the right behaviour and would take effect the moment a fbdev backend applies. **What does work is a userspace graphics stack on the panel.** The mode can be set from userspace, and `modetest -M tegra -r` proves the whole chain: DSI host, panel driver and init sequence, rails, backlight. A compositor (Xorg's `modesetting` driver, or a Wayland compositor) sets the mode itself, so the desktop can render on the Switch's own screen. That is the route to a usable handheld — the missing half is **input**, which needs switchroot's drivers: touchscreen (patches 0011/0023) and Joy-Cons (0016/0019). **Porting note.** Patch 0010 is from 2019, so the port needed: `drmP.h` removed, `devm_drm_panel_alloc()` instead of a manual `drm_panel_init()`/`funcs`/`dev` assignment, `get_modes()` taking the connector as an argument, `drm_panel_attach`/`detach` dropped, `devm_of_find_backlight()` in place of `of_find_backlight_by_node()`, `vrefresh` dropped (computed from the timings now), and `MIPI_DSI_MODE_EOT_PACKET` dropped (the polarity of that flag flipped; sending EOT is the default). ### Touchscreen — unresolved, and honestly documented An STMicroelectronics touch controller at **0x49 on i2c3** (`i2c@7000c500`). The wiring comes from the vendor's own device tree, extracted from `switchroot/ubuntu-jammy/nx-plat.dtimg` (compatible `nvidia,odin`, `nintendo,icosa`): | Piece | Value | |---|---| | bus | i2c3 at 400 kHz, pins `gen3_i2c_scl_pf0` / `gen3_i2c_sda_pf1` muxed to `i2c3` | | analogue rail | MAX77620 **ldo6**, 2.9 V (vendor name `vdd-tp-2v9`) | | I/O rail | fixed regulator on **PJ7** (vendor name `stm,vio-gpio`) | | interrupt | **PX1**, level-low — deliberately *removed*, see below | | native axes | 1280x720 against a 720x1280 panel, hence `touchscreen-swapped-x-y` | | vendor compatible | `stm,ftm4_fts` | **It does not work yet.** This section records exactly how far it got, because getting there took five rebuilds and three wrong hypotheses. Established: - The controller **answers on I2C**. `STMFTS_READ_INFO` (0x80) succeeds and returns `36 70 01 00 81 06 00 00`. - It queues a `CONTROLLER_READY` (0x10) event in its FIFO. - The reset is handled by **polling that FIFO for `CONTROLLER_READY`**, which is where upstream landed too (`Input: stmfts - wait for controller ready after reset`, from the FTS5 series, applied to FTS4 and FTS5 alike). The chip's firmware boot includes flash CRC verification and can take a few hundred ms before its I2C state machine ACKs — during which reads fail and the i2c core silently retries them. **Slow reads immediately after a reset are expected.** Not established: - After the reset the controller returns an **error event**, `0f ba d0 00 c0 de` — whose payload reads as `bad0 0c0de`, i.e. *"bad code"*. - `STMFTS_READ_INFO` reports **chip id 0x0000**. A real STM controller reports a real ID (an FTM5 on a Pixel 4a reports `0x4836`). Those two together do not look like a real touch controller, and that is the open question: whether `0x49` on i2c3 is the touchscreen at all. If it is, the authoritative reference for the init sequence is the **vendor driver** for `stm,ftm4_fts` — Samsung's downstream `ftm4_ts.c` and its `fts_wait_for_ready` — because FTM4-family panels are documented as using different registers, a different init sequence, and a different data packing from the FTS driver that mainline targets. #### Bugs found and fixed on the way | Bug | Symptom | Fix | |---|---|---| | i2c3 pins never muxed | `-110` on the driver's first I2C access | `i2c3_pins` group, referenced from the node | | level-low interrupt on a floating pin | the handler stormed continuously, ate the ready event, and starved the wait loop on the mutex — probe failed after ~90 s | interrupt removed from the DT; PX1 configured anyway (`touch_pins`) | | poll thread started before `configure()` | it consumed the ready event, then `reinit_completion()` discarded the completion it had produced | thread now starts *after* `configure()` | Two hypotheses that were **wrong**, recorded so nobody retries them: - *"The 256-byte event reads are slow because of the I2C DMA path."* They are slow because the chip is booting after the reset. Reads are fast (~6 ms) before the reset and slow (0.5-1 s) after it — temporal, not size-dependent. This was inferred by comparing an 8-byte read *before* the reset with 256-byte reads *after* it, which is not a controlled comparison. - *"The `0x2c = 0x48` write from switchroot's patch 0011 causes the error event."* Removing it changed nothing. It is still gone, because it exists only to make the controller signal its status event *by interrupt*, which this driver does not use — but it was not the cause. #### Bring-up crutches, and when to drop them Both rails are `regulator-always-on`. That is deliberate and temporary: on a failed probe the driver disables its supplies, which powers the controller down and takes `0x49` off the bus, and keeping VIO up is what makes `i2ctransfer -y 0 w1@0x49 ...` usable for poking at the protocol. Revert both to switched rails once the driver probes cleanly. The driver also dumps the first eight event reads (24 bytes each) and the chip identity. That is scaffolding for this specific hunt. The rest of the port is in `patches/kernel-stmfts-switch.patch`: - the **event layout** is the Switch's, not the driver's — both coordinates are 12-bit and share the low nibble of one byte, and the size fields collapse: ```c x = (event[1] << 4) | ((event[3] & 0xf0) >> 4); y = (event[2] << 4) | (event[3] & 0x0f); ``` - `input_mt_sync_frame()` was missing, so clients saw `BTN_TOUCH` set but never reported, and treated taps as hovers; - commands complete by reading the event FIFO rather than waiting for an interrupt, and the interrupt is optional. ### Joy-Con input (rails) The Joy-Con rails are **UARTs**, and the controllers speak HID over them. That is a different path from the one mainline's `hid-nintendo` covers (USB and Bluetooth), so it needs switchroot's patch 0016, installed as a whole file by `install_switch_joycon()` from `drivers/joycon.c`. | | Right Joy-Con | Left Joy-Con | |---|---|---| | UART | `serial@70006040` (uartb) | `serial@70006200` (uartc) | | pins | `uart2_tx_pg0`, `uart2_rx_pg1`, `uart2_rts_pg2`, `uart2_cts_pg3` | `uart3_tx_pd1`, `uart3_rx_pd2`, `uart3_rts_pd3`, `uart3_cts_pd4` | | function | `uartb` | `uartc` | | charge enable | `TEGRA_GPIO(K, 3)` | `TEGRA_GPIO(CC, 3)` | Those pins are ordinary pinmux pins (`pinmux@700008d4`), **not** padctl lanes, so configuring them cannot disturb the USB/PCIe lanes the console depends on. That was the main risk in this change and it turned out not to apply. switchroot's patch **0019 is deliberately not included**. `hid-joycon.c` binds `HID_USB_DEVICE` / `HID_BLUETOOTH_DEVICE` Nintendo devices — the USB and Bluetooth paths — which mainline's `hid-nintendo.c` already covers. It does nothing for the rails, and shipping both would be two drivers for one job. #### Two traps worth remembering **1. `CONFIG_JOYSTICK_JOYCON=y` is not enough to get the driver built.** `drivers/input/Makefile` gates the whole directory: ```make obj-$(CONFIG_INPUT_JOYSTICK) += joystick/ ``` `CONFIG_INPUT_JOYSTICK` was off, so the directory was never entered and the driver's own `obj-$(CONFIG_JOYSTICK_JOYCON) += joycon.o` line was inert. kconfig never complained, because `drivers/input/joystick/Kconfig` is sourced *unconditionally* — the symbol was visible, settable, and present in both `.config` and `include/config/auto.conf` while `joycon.o` was never compiled. `CONFIG_INPUT_JOYSTICK=y` is therefore required as well. This is the failure `verify_built_in_drivers()` now catches, because `verify_required_options()` cannot: reading `.config` proves a *symbol* survived kconfig, not that any *code* was built. That check reads the built `vmlinux` for a string each installed driver defines. Only built-in (`=y`) drivers belong in it — `switch-fb` is deliberately `=m` and is correctly absent. **2. Mainline cannot invert the rails' TXD/RTS.** switchroot's DT sets `nvidia,invert-txd` and `nvidia,invert-rts` ("invert TXD and RTS via IRDA_CSR"). Mainline's `serial-tegra.c` defines `TEGRA_UART_IRDA_CSR` and **never uses it**, and reads none of those properties. The bit layout is authoritative in the vendor tree — `INVERT_RXD 0x01`, `TXD 0x02`, `CTS 0x04`, `RTS 0x08`, OR'd into `IRDA_CSR` — so it *can* be implemented, but this DT deliberately omits the properties and tests **without** inversion first. If the rails talk, the property was vestigial; if they are silent, that is the next change, and it has a real reference rather than a guess. #### Porting notes The driver is 2018/2019-era and needed three API fixes plus tidy-ups, all small: - `struct power_supply_config` no longer has `of_node` (the core takes the fwnode from the parent device, so the line is simply gone); - `serdev_device_ops.receive_buf` returns `size_t` now, not `int`; - `platform_driver.remove` returns `void` now, not `int`; - six file-local helpers were missing prototypes and are now `static` — safe because the only other consumer would have been `hid-joycon.c`, which is not shipped. **Status: untested on hardware.** The driver builds clean and is in the kernel, and the device tree carries the rails, but nothing has driven a Joy-Con yet. ### Debugging gotcha: `pinconf-pins` is useless on Tegra ```c static void tegra_pinconf_dbg_show(struct pinctrl_dev *pctldev, struct seq_file *s, unsigned offset) { } ``` That callback is **empty**, and `pinconf-pins` is built on it. Every pin prints as `pin 56 (WIFI_EN PH0): ` with nothing after the colon — configured or not, on every Tegra SoC. It looks exactly like "the pinmux did not apply". Use these instead: - `pinmux-pins` — which device owns each pin, its function and its group - `pinconf-groups` — per-group config (`tegra_pinconf_group_dbg_show` is real) Known broken: `simple-framebuffer` fails with `-22`, so there is no local console on the panel. The USB console is unaffected. Also worth recording: **UART-A (`0x70006000`) is not registered on a retail unit** (B/C/D are), so `earlycon` there is useless and may hang the bus. ## Troubleshooting ### `make` segfaults during `modules` ```text make[1]: *** [Makefile:2118: modules] Segmentation fault (core dumped) make: INTERNAL: Exiting with 22 jobserver tokens available; should be 24! ``` This is **not** a kernel or toolchain bug. GNU make expands `$(foreach)` by recursing on the C stack, `scripts/Makefile.modfinal` iterates every `=m` symbol, and the arm64 defconfig queues **1400+ modules** — enough to overflow the default 8 MiB stack. A core dump points at `/usr/bin/make` running `-f ./scripts/Makefile.modfinal`, with frames full of `$$(...)` text. `vmlinux` links fine, which is what makes it look like a module-specific bug. The build now runs every `make` through `run_make()`, which raises `RLIMIT_STACK` to unlimited first (`ulimit -s unlimited`, falling back to 64 MiB). Verified: the same tree that crashed linked **4696 `.ko` files** with `EXIT=0` once the stack was raised. If you hit it by hand, just `ulimit -s unlimited` before running make. ## Building and testing ```sh cargo run -- kernel # builds v7.2 + injects the DTS; emits switch.dtb cargo run -- payload --kernel built cargo run -- rootfs # (needs sudo) for the Arch rootfs ``` Then copy `output/bootloader/` and `output/switchroot/` onto the FAT partition of the SD card and boot **More configs → L4T ALARM**. ### Reading the result - U-Boot prints `MAINLINE: …` lines, and the boot script paints solid colours into the framebuffer as a coarse progress indicator (red → green → blue → white → purple, then the kernel takes over). - Kernel output arrives over **USB serial**: `minicom -D /dev/ttyACM0`. - If the kernel dies before userspace, the two working channels are the FAT partition (files written by the init scripts — `ALARM_USERS.txt`, `ALARM_UP.txt`) and ramoops plus a warm reboot. See `patches/README.md`, "Reading a boot that has no console". Verified end to end: ```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 ```