evidence · 31.9 KiB

hdmi-hotplug.md

Committed at docs/research/hdmi-hotplug.md. This is what grounds the requirements that cite it.

Open raw source

HDMI hot-plug: can plugging in HDMI switch the display automatically?

Researched 2026-09-28 on worktree k230-hdmi-hotplug, branch spec/hdmi-hotplug, base e843c5d6. Research only — no board access, no push/merge. Answers the six questions from the task brief, in order, each grounded per .skills/k230-spec-change/SKILL.md’s tiering (board observation

vendor source read > nothing, which gets <!-- UNVERIFIED -->). Nothing in this document is tier-1 (board-observed); every claim below is tier 2 (vendor or our own source, read and cited by path/line) unless flagged UNVERIFIED.

Headline answer: there is no mux chip. The four DSI data lanes and clock are wired in bare parallel to both the RM69A10 panel and the LT9611 bridge — confirmed from the schematic, not inferred. The panel and the LT9611 also share their reset and interrupt GPIOs as literal single nets, not merely as reused pin numbers in different device trees. Every vendor source we found (LILYGO’s own abandoned merge attempt, their shipped launcher UI, and our own nix/sd-image.nix’s documented U-Boot behavior) selects HDMI vs. panel by which device tree blob boots, and switches by rebooting. Nobody — not LILYGO, not Canaan’s reference tree, not our own kernel — has ever switched the K230’s single DSI host between a panel and a bridge without a reboot. Our kernel’s bridge-attach path is additionally missing a connector creation call, so even the reboot-based switch does not work out of the box. True hot-plug-without-reboot, which is what the user asked for, is therefore a real, unproven engineering project, not a device-tree edit. Section 5 stages the work so the reboot-based switch (which mirrors a demonstrated vendor mechanism) ships first, and the no-reboot goal is pursued after it as explicitly higher-risk, possibly-infeasible follow-on work.

Sources used

1. Hardware routing: how do the DSI lanes and clock reach both outputs?

No mux or switch chip exists on this board. The schematic shows plain parallel copper: the SoC’s four DSI data-lane pairs and clock pair are one set of nets, and both the panel connector and the LT9611 bridge are wired directly to that same set.

Evidence, from T-Display K230_V1.0_NEW.pdf (Altium export, 8 sheets, A4 and A3/A2 title blocks, drawn by LILYGO, dated 2026-06-26):

The panel connector only breaks out two of the four lanes. Sheet “Video” (page 4 in file order, titled “Video”), the 15-pin LCD/touch FPC connector lists pins DSI_CLK_N/DSI_CLK_P, DSI_D0_P/N, DSI_D1_P/N only — no D2/D3 pins on that connector. This matches our own device tree’s panel-dsi-lane = <2> (nix/dts/display-rm69a10-568x1232.dtsi:72) and confirms the RM69A10 runs 2-lane while the LT9611, wired to all four pairs, would run 4-lane. UNVERIFIED: whether D2/D3 being simultaneously driven by the host (if a future DT ran the DSI host in 4-lane mode for HDMI) while left floating/unterminated at the panel connector causes any signal-integrity concern for the panel’s own 2-lane reception; not measured, and the two lane counts are configured by two different, mutually exclusive device trees today (see §3), so they have never been electrically simultaneous in any observed boot.

GPIO23/24 are literal shared nets, not just reused GPIO numbers. Sheet “Video”, the net-label table directly above the 15-pin LCD/touch connector reads (transcribed from the rendered schematic):

I2C3_SCL  ->  HDMI_CSCL  ->  TP_SCL   (connector pin 3)
I2C3_SDA  ->  HDMI_CSDA  ->  TP_SDA   (connector pin 4)
HDMI_RSTN ->  TP_RST                  (connector pin 5)
HDMI_INT  ->  TP_INT                  (connector pin 1)

Each arrow is the same wire carrying two net-label annotations, not two separate nets tied by a jumper — this is one continuous, unbroken run connecting the LT9611’s RSTN pin (33) and CSCL/CSDA pins (34/35) on the HDMI sheet to the touch/LCD FPC connector’s TP_SCL/TP_SDA/TP_RST pins, and the LT9611’s INT_ATST_GPIO3 pin (12) to that connector’s TP_INT pin. This resolves the “not established” flag in docs/dts-evidence.md:157-159 and docs/findings.md:207-208: the schematic has now been read for this exact question, and the answer is yes, LT9611 reset/interrupt/I2C and touch reset/interrupt/I2C are the same physical nets. The panel’s own reset (LCD_RST, connector pin 11, GPIO22) is a separate, standalone net not tied to HDMI_RSTN — the LT9611 does not touch the panel’s own reset or enable lines, only touch’s.

This single-net sharing has a consequence beyond “pick one in the device tree”: vendor and our own device trees disagree about the electrical convention on that shared line. The vendor’s k230-canmv-v3.dts LT9611 node requests reset-gpios = <&gpio0_ports 24 GPIO_ACTIVE_HIGH> (k230-canmv-v3.dts:64, in our pinned kernel tree), while our board’s touch node requests reset-gpios = <&gpio0_ports 24 GPIO_ACTIVE_LOW> (nix/dts/k230-tdisplay.dts:200) on the same physical pin. Similarly the vendor LT9611 node asks for interrupts = <23 IRQ_TYPE_EDGE_FALLING> (k230-canmv-v3.dts:66) while our touch node asks for interrupts = <23 IRQ_TYPE_LEVEL_LOW> (nix/dts/k230-tdisplay.dts:224, deliberately changed from edge to level per that file’s own comment, because edge-triggered lost interrupts on this exact silicon). Two drivers wanting different trigger types and different active-sense on one wire is a real conflict, not a naming coincidence — reinforcing why no vendor source ever wires both nodes into one device tree at once (§3).

2. What GPIO23/24 do, and what else is nearby

3. How LILYGO’s own firmware enables HDMI

It is a boot-time device-tree choice with a full reboot, not a runtime switch — and LILYGO’s own engineering history shows they tried a merged device tree first and abandoned it.

Evidence, Xinyuan-LilyGO/T-Display-K230, k230_bsp/overlay/buildroot-overlay/linux/:

Our own vendored U-Boot already has an equivalent selection mechanism, independently confirmed from our own committed, hardware-tested source comments in nix/sd-image.nix:142-156: U-Boot’s bootcmd runs a k230_set_dtb command that reads a text file naming a DTB — trying force_dtb first (short-circuits selection), then falling back to hdmi_dtb, then lcd_dtb twice, observed on hardware to fail all three and drop to a U-Boot prompt if none of the three files exist. Our SD image writes all three names pointing at the same panel DTB today, to keep first boot simple. UNVERIFIED: whether U-Boot’s hdmi_dtb/lcd_dtb fallback path performs its own live display-presence probe (i.e., a stage-1 auto-detect independent of LILYGO’s userspace toggle) or is purely an operator-set preference file read at boot with no hardware probing; the U-Boot C source for k230_set_dtb was not read for this document (it ships as a prebuilt blob per nix/uboot-k230.nix, not as buildable source we have checked out). This is worth resolving before design work in §5, since if U-Boot itself already probes for a live cable, that is a much cheaper place to do boot-time selection than anything in Linux.

4. Our kernel

5. Designing a hot-swap

Given §3 and §4, split the goal into what is demonstrated to work (reboot-based switching, once our kernel gets the missing connector patch) and what is not demonstrated anywhere (no-reboot switching while Linux keeps running). The OpenSpec proposal (openspec/changes/plugging-in-hdmi-moves-the-display/) stages exactly this split; summarized here:

  1. Read-only board probe (board-gated, narrow, no writes): confirm the 0x3b I2C identity actually answers on &i2c3 with touch present and read the LT9611’s HPD status register through it, and read GPIO23/24’s current direction/level with touch running, without changing anything. This is the answer to “what is the first board probe command” below.
  2. Driver/DT build (host-build-proof only, no board changes): add the missing drm_bridge_connector_init() call to canaan_dsi.c, and build (not boot) a second, HDMI-only DTB derived from k230-tdisplay.dts’s own &dsi graph (our own board’s I2C/GPIO facts, not the vendor’s generic tree) so the resulting HDMI-A-1 connector is genuinely this board’s LT9611, not a copy of the unrelated Canaan reference tree. Verified by nix build, not by booting.
  3. Manual switch, reboot-based (board-gated): the credible near-term deliverable. Mirrors LILYGO’s own proven, self-reverting one-shot mechanism (§3) using our own nix/sd-image.nix/nix/device-tree.nix machinery: write an alternate /boot/k230-tdisplay-hdmi.dtb, flip U-Boot’s DTB-selector text file, reboot; a one-shot marker restores the panel DTB on the next boot regardless of why it happened, so a bad probe or an unrelated crash always self-heals back to the panel. This satisfies the AGENTS.md/task risk requirement directly: recovery is a plain reboot, and it defaults to the panel.
  4. Hotplug automation, no reboot (board-gated, explicitly high risk): attempt the design the user actually asked for. The concrete blockers, all found above, that make this uncertain rather than merely large:
    • The LT9611 can only be found by the K230 (§2) if it is already a live, un-reset, I2C-probed device — which means it must be present in the running boot’s device tree even while the panel is the active display, contradicting §4’s one-endpoint-per-port finding. A DT where the LT9611 exists as a plain I2C client (for HPD/status polling) without being the &dsi port@1 endpoint might let it probe and raise its shared-GPIO23 interrupt without contending for the DSI graph — untested, and depends on the open-drain question in §4.
    • GPIO24 (reset) is a single net with two disagreeing polarity conventions (§1). A no-reboot design cannot reset one chip without also resetting the other; the only safe pattern is to never re-toggle GPIO24 after both chips’ initial power-on reset, and confirm — UNVERIFIED, not measured — that neither driver retoggles it during normal atomic_enable/mode-change operation.
    • Actually repainting the DSI host from panel to bridge live requires new canaan_dsi.c logic (tear down the panel connector/encoder state, bring up a bridge connector/encoder, without the one-shot drm_of_find_panel_or_bridge() this driver currently relies on) — real, unbounded-until-attempted kernel work, not present in any source surveyed.
    • Sway/card-shell must additionally handle the resulting output-topology change (DSI-1 disappearing, HDMI-A-1 appearing at a different resolution and orientation) — see the landscape audit below, and note it is Large on its own. Given this, task the OpenSpec proposal to attempt automation but mark it explicitly speculative, separate from the reboot-based switch, so the reboot-based switch is not held hostage to an unproven no-reboot design.
  5. Manual override in Settings: a Settings row that triggers the reboot-based switch (stage 3) directly, independent of whether stage 4 ever lands — cheap, and gives the user a working “go to HDMI” control immediately even if true hot-plug automation stalls.

What in the shell and card-shell assumes a fixed 568×1232 portrait panel

Grep for 568/1232 and output names across nix/shell.nix and nix/rust-shell-client/src/*.rs (the “shell” capability) and nix/card-shell/, nix/card-shell-policy/ (the “card-shell” capability):

This is a Large item on its own (476 raw literal-constant hits, 380 of them outside test code), separate from and larger than the DSI/DT work above. It needs, at minimum: deriving the design-space transform from the actual output’s reported geometry instead of the 568.0/1232.0 literals in lib.rs/render.rs/wifi_ui.rs; a landscape layout for the grid/navigation/service-UI screens (navigation.rs, home_grid.rs, service_ui.rs) rather than a naive stretch of the portrait design; and a Sway output/input stanza for HDMI-A-1 alongside the existing DSI-1 one in nix/shell.nix. None of this can be host-verified beyond compiling and unit-testing the now-parameterized math; the actual on-monitor layout needs a board with an HDMI display attached.

6. Risk and recovery

A wrong mux/modeset gives a black panel or a black HDMI output. Because there is no mux (§1), “wrong mux” in practice means “wrong DTB” or “wrong GPIO polarity on the shared reset net” — both are boot-time, not runtime-silent, failure modes once stage 3 exists. Recovery is a reboot, and it must default to the panel:

Answers to the report questions