alarm-tegra-x1 / device-tree/README.md

raw file 27970 bytes
# 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 [email protected]` |
| 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
```