From b1bce2249f618ef4b32ef645b2d327bd02f7d788 Mon Sep 17 00:00:00 2001 From: Wes Grimes Date: Fri, 2 Oct 2026 19:25:42 -0400 Subject: [PATCH 1/8] PCI: apple: Cold-init T600x/T602x PCIe-C ports without an m1n1 handoff T600x and T602x PCIe-C (the Thunderbolt PCIe tunnel) is only brought up after an m1n1 handoff ("apple,pciec-preinit-status" = 1), and no m1n1 performs one. The tunnel is always refused, so PCIe devices behind Thunderbolt 3 docks and displays (their xHCI, audio, cameras, network) never appear. Add pcie_apple.tunnel_kernel_init (default off). When set, ports on T6000/T6001/T6020/T6021 (M1 and M2 Pro/Max) without a handoff are cold-initialized by the kernel through the existing t8103 paths: apple_pcie_tunnel_prepare() clocks the port before its DART probes, and apple_pcie_setup_port() cold-initializes a port that is not running. It is a parameter rather than a DT property because m1n1 hands every installed kernel the same DT. Ports whose DART has no "apple,tunable" are refused: m1n1 does not provide them, and the DART cannot probe without them. apple_pcie_tunnel_cold_init() gains one ordering difference for these ports: they must report RUN before the root complex config-space tunables are written, which hard-resets a J416c otherwise. T600x/T602x have no oe-fabric region and pulse Intr2AXI instead, and they keep the 0x208 reset table value. The t8103 power and resume paths, selected by pcie->kernel_init, are not used. Tested on a MacBook Pro 16" M2 Pro (j416s) with an Apple Studio Display on each of its three USB-C ports, plugged in after boot and connected at boot. T600x is untested. Signed-off-by: Wes Grimes --- drivers/pci/controller/pcie-apple.c | 130 +++++++++++++++++++++++----- include/linux/pci-apple.h | 1 + 2 files changed, 108 insertions(+), 23 deletions(-) diff --git a/drivers/pci/controller/pcie-apple.c b/drivers/pci/controller/pcie-apple.c index fd903e86e5d372..b5b1f4d2fb2c17 100644 --- a/drivers/pci/controller/pcie-apple.c +++ b/drivers/pci/controller/pcie-apple.c @@ -65,6 +65,54 @@ static int link_up_timeout = 500; module_param(link_up_timeout, int, 0644); MODULE_PARM_DESC(link_up_timeout, "PCIe link training timeout in milliseconds"); +static bool tunnel_kernel_init; +module_param(tunnel_kernel_init, bool, 0444); +MODULE_PARM_DESC(tunnel_kernel_init, + "Cold-initialize T600x/T602x PCIe-C ports without an m1n1 handoff (experimental)"); + +static const char *const apple_pcie_tunnel_cold_init_machines[] = { + "apple,t6000", "apple,t6001", "apple,t6020", "apple,t6021", NULL +}; + +/** + * apple_pcie_tunnel_needs_cold_init() - whether the kernel brings up a port + * @np: PCIe-C host node + * + * No m1n1 performs the PCIe-C preinit handoff on T600x/T602x. With the + * opt-in parameter the kernel cold-initializes these ports instead. It is a + * parameter rather than a DT property because m1n1 hands every installed + * kernel the same DT. The tunnel's DART cannot probe without its tunables, + * which m1n1 does not provide, so ports whose DART lacks them are refused. + * + * Unlike "apple,pciec-kernel-init" (t8103), this does not select the t8103 + * power and resume sequences. + */ +bool apple_pcie_tunnel_needs_cold_init(struct device_node *np) +{ + struct device_node *dart = NULL; + bool tunables; + u32 status; + + if (!tunnel_kernel_init || + !of_device_is_compatible(np, "apple,t6000-pciec") || + !of_machine_compatible_match(apple_pcie_tunnel_cold_init_machines)) + return false; + + if (!of_property_read_u32(np, "apple,pciec-preinit-status", &status) && + status == 1) + return false; + + if (of_map_id(np, 0, "iommu-map", "iommu-map-mask", &dart, NULL) || !dart) + return false; + tunables = of_property_present(dart, "apple,tunable"); + if (!tunables) + pr_warn_once("%pOF: no PCIe-C DART tunables, not cold-initializing\n", + dart); + of_node_put(dart); + return tunables; +} +EXPORT_SYMBOL_GPL(apple_pcie_tunnel_needs_cold_init); + /* T8103 (original M1) and related SoCs */ #define CORE_RC_PHYIF_CTL 0x00024 #define CORE_RC_PHYIF_CTL_RUN BIT(0) @@ -263,6 +311,7 @@ struct apple_pcie { struct apple_tunable *oe_fabric_tunable; bool power_retained; bool kernel_init; + bool tunnel_cold_init; bool bus_stopped; bool reset_on_resume; bool link_kept; @@ -785,7 +834,10 @@ static void apple_pcie_tunnel_reset_hardware(struct apple_pcie_port *port) for (i = 0; i < ARRAY_SIZE(apple_pcie_tunnel_reset_regs); i++) { u32 value = apple_pcie_tunnel_reset_regs[i].value; - /* t8103 writes 0. 0x208 is the value used when firmware left the port up. */ + /* + * t8103 writes 0. 0x208 is the value used when firmware left the + * port up; T600x/T602x cold init without a handoff uses it too. + */ if (port->pcie->kernel_init && apple_pcie_tunnel_reset_regs[i].offset == 0x130) value = 0; @@ -860,10 +912,11 @@ static int apple_pcie_tunnel_reinitialize(struct apple_pcie_port *port) } /* - * t8103 has no firmware handoff for this port. Bring it up from reset: - * tunables and the reset table, then PERST and APPCLK, and only then the - * root-port config-space tunables. Stop before releasing the tunnel reset - * or starting link training. + * Bring up a port that no firmware handoff left running (t8103, or + * T600x/T602x with pcie_apple.tunnel_kernel_init): tunables and the reset + * table, then PERST and APPCLK, and only then the root-port config-space + * tunables. Stop before releasing the tunnel reset or starting link + * training. */ static int apple_pcie_tunnel_cold_init(struct apple_pcie_port *port) { @@ -887,15 +940,33 @@ static int apple_pcie_tunnel_cold_init(struct apple_pcie_port *port) port->np); apple_pcie_port_rmw_set(port, PORT_PERST_OFF, pcie->hw->port_perst); apple_pcie_port_rmw_set(port, PORT_APPCLK_EN, PORT_APPCLK); - apple_pcie_port_rmw_clear(port, PORT_APPCLK_CGDIS, PORT_APPCLK); + /* + * Without an m1n1 handoff, T600x/T602x ports must report RUN before + * the root complex config space is written: writing the RC tunables + * first hard-resets a J416c. + */ + if (!pcie->kernel_init) { + ret = read_poll_timeout_atomic(apple_pcie_port_readl, stat, + stat & PORT_STATUS_READY, + 10, 250000, false, port, + PORT_STATUS); + if (ret) + return dev_err_probe(pcie->dev, ret, + "port %pOF cold init: RUN not set (status %#x)\n", + port->np, stat); + } else { + apple_pcie_port_rmw_clear(port, PORT_APPCLK_CGDIS, PORT_APPCLK); + } - dev_info(pcie->dev, "port %pOF cold init: oe-fabric and root tunables\n", - port->np); + dev_info(pcie->dev, "port %pOF cold init: %sroot tunables\n", + port->np, pcie->oe_fabric_base ? "oe-fabric and " : ""); if (pcie->oe_fabric_base) apple_pcie_tunnel_apply_tunable(pcie->oe_fabric_base, pcie->oe_fabric_tunable); apple_pcie_tunnel_apply_tunable(pcie->cfg ? pcie->cfg->win : pcie->early_cfg, pcie->rc_tunable); + if (!pcie->kernel_init) + apple_pcie_port_rmw_clear(port, PORT_APPCLK_CGDIS, PORT_APPCLK); apple_pcie_port_writel(port, PORT_COUNTER_ENABLE, PORT_COUNTER_CTRL); apple_pcie_port_writel(port, ~0, PORT_INTSTAT); @@ -1479,13 +1550,14 @@ static int apple_pcie_setup_port(struct apple_pcie *pcie, * m1n1 owns PCIe-C cold initialization on T6020. Replaying the * port reset against that live handoff raises an asynchronous * SError. t8103 boots with ATC_PCIE off and no handoff, so - * apple,pciec-kernel-init selects the in-kernel sequence. + * apple,pciec-kernel-init selects the in-kernel sequence, as + * pcie_apple.tunnel_kernel_init does on T600x/T602x. */ ret = of_property_read_u32(pcie->dev->of_node, "apple,pciec-preinit-status", &preinit_status); preinit_ok = !ret && preinit_status == 1; - if (pcie->kernel_init && !preinit_ok) { + if ((pcie->kernel_init || pcie->tunnel_cold_init) && !preinit_ok) { stat = apple_pcie_port_readl(port, PORT_STATUS); if (stat & PORT_STATUS_READY) { dev_info(pcie->dev, @@ -1498,7 +1570,7 @@ static int apple_pcie_setup_port(struct apple_pcie *pcie, } } else if (!preinit_ok) { dev_err(pcie->dev, - "PCIe-C requires a successful m1n1 preinit handoff\n"); + "PCIe-C requires an m1n1 preinit handoff or pcie_apple.tunnel_kernel_init=1\n"); ret = -ENODEV; goto err_teardown; } else { @@ -2350,6 +2422,8 @@ static int apple_pcie_probe(struct platform_device *pdev) if (pcie->hw->tunneled) { pcie->kernel_init = of_property_read_bool(dev->of_node, "apple,pciec-kernel-init"); + pcie->tunnel_cold_init = + apple_pcie_tunnel_needs_cold_init(dev->of_node); ret = apple_pcie_tunnel_init_resources(pdev, pcie); if (ret) return ret; @@ -2698,14 +2772,16 @@ int apple_pcie_tunnel_prepare(struct device *dev, struct device_node *tunnel) struct apple_pcie *pcie; struct apple_pcie_port *port; struct apple_pcie_map config = {}, debug = {}, fabric = {}, portmap = {}; - struct apple_pcie_map oe = {}; + struct apple_pcie_map oe = {}, intr2axi = {}; + bool kernel_init = false; u32 stat; int ret; np = NULL; for_each_available_child_of_node(tunnel, np) { - if (of_device_is_compatible(np, "apple,t8103-pciec") && - of_property_read_bool(np, "apple,pciec-kernel-init")) + kernel_init = of_device_is_compatible(np, "apple,t8103-pciec") && + of_property_read_bool(np, "apple,pciec-kernel-init"); + if (kernel_init || apple_pcie_tunnel_needs_cold_init(np)) break; } if (!np) @@ -2738,7 +2814,11 @@ int apple_pcie_tunnel_prepare(struct device *dev, struct device_node *tunnel) ret = apple_pcie_map_named(np, "fabric", &fabric); if (ret) goto out_np; - ret = apple_pcie_map_named(np, "oe-fabric", &oe); + /* t8103 has an oe-fabric region; T600x/T602x pulse Intr2AXI instead. */ + if (kernel_init) + ret = apple_pcie_map_named(np, "oe-fabric", &oe); + else + ret = apple_pcie_map_named(np, "intr2axi", &intr2axi); if (ret) goto out_np; @@ -2757,11 +2837,12 @@ int apple_pcie_tunnel_prepare(struct device *dev, struct device_node *tunnel) pcie->dev = dev; pcie->hw = match->data; - pcie->kernel_init = true; + pcie->kernel_init = kernel_init; pcie->early_cfg = config.base; pcie->debug_base = debug.base; pcie->fabric_base = fabric.base; pcie->oe_fabric_base = oe.base; + pcie->intr2axi_base = intr2axi.base; pcie->debug_tunable = apple_pcie_tunable_once(np, "apple,tunable-debug", &debug.res); if (IS_ERR(pcie->debug_tunable)) { @@ -2783,13 +2864,15 @@ int apple_pcie_tunnel_prepare(struct device *dev, struct device_node *tunnel) pcie->rc_tunable = NULL; goto out_free; } - pcie->oe_fabric_tunable = apple_pcie_tunable_once(np, - "apple,tunable-oe-fabric", - &oe.res); - if (IS_ERR(pcie->oe_fabric_tunable)) { - ret = PTR_ERR(pcie->oe_fabric_tunable); - pcie->oe_fabric_tunable = NULL; - goto out_free; + if (oe.base) { + pcie->oe_fabric_tunable = + apple_pcie_tunable_once(np, "apple,tunable-oe-fabric", + &oe.res); + if (IS_ERR(pcie->oe_fabric_tunable)) { + ret = PTR_ERR(pcie->oe_fabric_tunable); + pcie->oe_fabric_tunable = NULL; + goto out_free; + } } port->pcie = pcie; @@ -2828,6 +2911,7 @@ int apple_pcie_tunnel_prepare(struct device *dev, struct device_node *tunnel) apple_pcie_unmap(&fabric); apple_pcie_unmap(&portmap); apple_pcie_unmap(&oe); + apple_pcie_unmap(&intr2axi); of_node_put(np); return ret; } diff --git a/include/linux/pci-apple.h b/include/linux/pci-apple.h index d7c2d244d209b4..b446c5e2229028 100644 --- a/include/linux/pci-apple.h +++ b/include/linux/pci-apple.h @@ -12,6 +12,7 @@ struct notifier_block; int apple_pcie_tunnel_register_notifier(struct notifier_block *nb); void apple_pcie_tunnel_unregister_notifier(struct notifier_block *nb); +bool apple_pcie_tunnel_needs_cold_init(struct device_node *np); int apple_pcie_tunnel_prepare(struct device *dev, struct device_node *tunnel); int apple_pcie_tunnel_quiesce(struct device *dev); int apple_pcie_tunnel_restore(struct device *dev); From 5501591c1de2107860b72b3a49ac644258e42af2 Mon Sep 17 00:00:00 2001 From: Wes Grimes Date: Fri, 2 Oct 2026 19:25:42 -0400 Subject: [PATCH 2/8] thunderbolt: apple: Accept PCIe-C kernel cold init on T600x/T602x ACIO only populates the tunneled PCIe-C host after an m1n1 handoff or for "apple,pciec-kernel-init" (t8103) ports. Also accept ports that the PCIe driver will cold-initialize itself, which it does only when booted with pcie_apple.tunnel_kernel_init=1, and word the disabled-tunnel messages so that they no longer name the m1n1 handoff as the only way. Signed-off-by: Wes Grimes --- drivers/thunderbolt/apple.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/drivers/thunderbolt/apple.c b/drivers/thunderbolt/apple.c index 905415690a855e..59219b9c032dbe 100644 --- a/drivers/thunderbolt/apple.c +++ b/drivers/thunderbolt/apple.c @@ -511,7 +511,10 @@ apple_cio_pcie_tunnel_is_preinitialized(struct device_node *parent) &status) && status == 1) return true; /* t8103 brings the port up itself. */ - return of_property_read_bool(child, "apple,pciec-kernel-init"); + if (of_property_read_bool(child, "apple,pciec-kernel-init")) + return true; + /* T600x/T602x without a handoff, when opted in. */ + return apple_pcie_tunnel_needs_cold_init(child); } return false; @@ -935,7 +938,7 @@ static int apple_cio_activate_pcie_tunnel_locked(struct apple_cio *acio) } if (!acio->pcie_tunnel_preinitialized) return dev_err_probe(acio->dev, -ENODEV, - "PCIe-C requires a successful m1n1 preinit handoff\n"); + "PCIe-C is not initialized by m1n1 or the kernel\n"); if (acio->pd_list->num_pds < 4 || !acio->pd_list->pd_links[3]) return dev_err_probe(acio->dev, -ENODEV, @@ -2254,7 +2257,7 @@ static int apple_cio_probe(struct platform_device *pdev) acio->pcie_tunnel_np); if (!acio->pcie_tunnel_preinitialized) { dev_warn(dev, - "PCIe-C tunnel disabled: m1n1 handoff is not initialized\n"); + "PCIe-C tunnel disabled: not initialized by m1n1 or the kernel\n"); } else { ret = apple_cio_map_pcie_intr2axi(acio); if (ret) From b3fa3e311fb73caac8e55d0d37ffada6523ec06d Mon Sep 17 00:00:00 2001 From: Wes Grimes Date: Fri, 2 Oct 2026 19:25:42 -0400 Subject: [PATCH 3/8] PCI: apple: Keep cold-initialized PCIe-C functions runtime-active A function behind a cold-initialized T600x/T602x tunnel stays in D0 (NO_D3) when it runtime-suspends, but its wakeup never arrives. When a Studio Display is power-cycled, its Titan Ridge xHCI runtime-suspends before the display's hub reconnects and never sees the hub, so the display's USB devices do not come back. Hold a runtime PM reference on each function for as long as it exists, from a PCI bus notifier registered before the host is scanned, so that neither the driver nor userspace power policy (power/control = auto) can suspend it. A bus notifier rather than a PCI fixup, because fixups in a module are never run. Tested on j416s with an Apple Studio Display: after a display power cycle its hub, camera and audio come back by themselves. Signed-off-by: Wes Grimes --- drivers/pci/controller/pcie-apple.c | 49 +++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/drivers/pci/controller/pcie-apple.c b/drivers/pci/controller/pcie-apple.c index b5b1f4d2fb2c17..28e7aae15c5a6c 100644 --- a/drivers/pci/controller/pcie-apple.c +++ b/drivers/pci/controller/pcie-apple.c @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -312,6 +313,7 @@ struct apple_pcie { bool power_retained; bool kernel_init; bool tunnel_cold_init; + struct notifier_block tunnel_pci_nb; bool bus_stopped; bool reset_on_resume; bool link_kept; @@ -2145,6 +2147,41 @@ static int apple_pcie_tunnel_add_link(struct apple_pcie *pcie, return ret; } +/* + * A function behind a cold-initialized T600x/T602x tunnel stays in D0 + * (NO_D3) when it runtime-suspends, but its wakeup never arrives: an xHCI + * that suspends before a power-cycled display's hub reconnects never sees + * the hub. Hold a runtime PM reference for as long as each function exists, + * so that neither its driver nor power/control can suspend it. + */ +static int apple_pcie_tunnel_pci_notify(struct notifier_block *nb, + unsigned long action, void *data) +{ + struct apple_pcie *pcie = container_of(nb, struct apple_pcie, + tunnel_pci_nb); + struct pci_dev *pdev = to_pci_dev(data); + + if (pci_host_bridge_priv(pci_find_host_bridge(pdev->bus)) != pcie) + return NOTIFY_DONE; + + switch (action) { + case BUS_NOTIFY_ADD_DEVICE: + pm_runtime_get_noresume(&pdev->dev); + break; + case BUS_NOTIFY_DEL_DEVICE: + pm_runtime_put_noidle(&pdev->dev); + break; + } + return NOTIFY_OK; +} + +static void apple_pcie_tunnel_pci_unregister(void *data) +{ + struct apple_pcie *pcie = data; + + bus_unregister_notifier(&pci_bus_type, &pcie->tunnel_pci_nb); +} + static int apple_pcie_tunnel_keep_d0(struct pci_dev *pdev, void *data) { /* @@ -2452,6 +2489,18 @@ static int apple_pcie_probe(struct platform_device *pdev) return ret; } + /* Every function is added after this and removed before devres runs. */ + if (pcie->tunnel_cold_init) { + pcie->tunnel_pci_nb.notifier_call = apple_pcie_tunnel_pci_notify; + ret = bus_register_notifier(&pci_bus_type, &pcie->tunnel_pci_nb); + if (ret) + return ret; + ret = devm_add_action_or_reset(dev, apple_pcie_tunnel_pci_unregister, + pcie); + if (ret) + return ret; + } + ret = apple_msi_init(pcie); if (ret) return ret; From 024975fced2cf2c729817f338aa1749424f97812 Mon Sep 17 00:00:00 2001 From: Wes Grimes Date: Fri, 2 Oct 2026 19:25:42 -0400 Subject: [PATCH 4/8] PCI: apple: Keep cold-initialized PCIe-C links up through suspend-to-idle In suspend-to-idle ACIO and the router links stay powered, and t8103 already leaves a healthy tunnel running instead of stopping the port. Cold-initialized T600x/T602x ports took the stop path. On resume the router rebuilt its tunnels, the in-place restart brought the link up only for it to drop again, the xHCI behind it was lost, and that tunnel instance could not train a link again until reboot. Let these ports keep their link through suspend-to-idle too, so the NHI keeps its tunnels and nothing is retrained. If the link was lost while asleep anyway, fail the host like a surprise unplug rather than restarting it in place; ACIO then revalidates the connection and replaces it. Tested on j416s with an Apple Studio Display: after 20 s and 3 min suspends its hub, camera and audio keep working, and unplugging the display while asleep is handled as an unplug. Signed-off-by: Wes Grimes --- drivers/pci/controller/pcie-apple.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/drivers/pci/controller/pcie-apple.c b/drivers/pci/controller/pcie-apple.c index 28e7aae15c5a6c..df5dce64f686f4 100644 --- a/drivers/pci/controller/pcie-apple.c +++ b/drivers/pci/controller/pcie-apple.c @@ -3050,7 +3050,8 @@ static bool apple_pcie_keep_link(struct apple_pcie *pcie) { if (!READ_ONCE(s2idle_keep_link) || !pm_suspend_no_platform()) return false; - if (!pcie->kernel_init || !pcie->power_retained) + if (!(pcie->kernel_init || pcie->tunnel_cold_init) || + !pcie->power_retained) return false; if (pcie->bus_stopped || pcie->resume_failed) return false; @@ -3160,8 +3161,18 @@ static int apple_pcie_resume_noirq(struct device *dev) pcie->link_kept = false; if (apple_pcie_tunnel_link_healthy(pcie)) return 0; - /* Lost while asleep: take the same path as a stopped tunnel. */ dev_warn(dev, "PCIe-C link lost during suspend-to-idle\n"); + /* + * A cold-initialized T600x/T602x port does not train again after + * an in-place restart. Fail like a surprise unplug instead: ACIO + * revalidates the connection after resume and replaces this host + * with a freshly cold-initialized one. + */ + if (pcie->tunnel_cold_init) { + ret = -ENOLINK; + goto failed; + } + /* Lost while asleep: take the same path as a stopped tunnel. */ apple_pcie_stop_for_sleep(dev); } if (pcie->reset_on_resume) { From c439df001663321ca0e553c436ebd9dc6e41c50d Mon Sep 17 00:00:00 2001 From: Wes Grimes Date: Fri, 2 Oct 2026 20:28:06 -0400 Subject: [PATCH 5/8] PCI: apple: Don't restart a quiesced PCIe-C host on resume When ACIO deactivates the PCIe tunnel while the cable stays connected, apple_pcie_tunnel_quiesce() removes the hierarchy and stops the ports, leaving the host bus_stopped until apple_pcie_tunnel_restore() restarts them on reactivation. resume_noirq() nevertheless restarts every port after a system suspend. Without a tunnel that fails, sets resume_failed, and the host then refuses restore() until it is replaced. A quiesced host has nothing to resume, so leave its ports stopped. Signed-off-by: Wes Grimes --- drivers/pci/controller/pcie-apple.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/drivers/pci/controller/pcie-apple.c b/drivers/pci/controller/pcie-apple.c index df5dce64f686f4..3133ec1fd47a6a 100644 --- a/drivers/pci/controller/pcie-apple.c +++ b/drivers/pci/controller/pcie-apple.c @@ -3157,6 +3157,14 @@ static int apple_pcie_resume_noirq(struct device *dev) return 0; if (pcie->resume_failed) return -EIO; + /* + * A host quiesced by a tunnel deactivation has no hierarchy and its + * ports are stopped. Leave them stopped: restarting them without a + * tunnel fails and marks the host dead, while + * apple_pcie_tunnel_restore() restarts them on reactivation. + */ + if (pcie->bus_stopped) + return 0; if (pcie->link_kept) { pcie->link_kept = false; if (apple_pcie_tunnel_link_healthy(pcie)) From 4f48cb1d768c53e39d428a96fbf5ae848fa837c1 Mon Sep 17 00:00:00 2001 From: Wes Grimes Date: Fri, 2 Oct 2026 19:25:42 -0400 Subject: [PATCH 6/8] arm64: dts: apple: t602x: Add PCIe-C DART tunables The tunneled PCIe-C DART requires "apple,tunable". iBoot adds "dart-tunables-instance-0" to the ADT at boot, but m1n1 does not copy it for the dart-apciec* nodes. Without it the DART fails to probe and the xHCI behind the tunnel dies after its command ring times out. Add the values to the apciec0..2 DARTs of every T602x die. They were read from the ADTs of a J416c (T6021) and a J414s (T6020), and are identical on both machines and for all three ports; on j416s they bring up a Studio Display's xHCI. apciec3, present on the desktop machines, has no values yet, so pcie_apple.tunnel_kernel_init refuses that port. Signed-off-by: Wes Grimes --- arch/arm64/boot/dts/apple/t602x-dieX.dtsi | 3 +++ .../boot/dts/apple/t602x-pciec-dart-tunables.dtsi | 15 +++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 arch/arm64/boot/dts/apple/t602x-pciec-dart-tunables.dtsi diff --git a/arch/arm64/boot/dts/apple/t602x-dieX.dtsi b/arch/arm64/boot/dts/apple/t602x-dieX.dtsi index d4efc517d0b71c..79eb4f0916542d 100644 --- a/arch/arm64/boot/dts/apple/t602x-dieX.dtsi +++ b/arch/arm64/boot/dts/apple/t602x-dieX.dtsi @@ -584,6 +584,7 @@ #iommu-cells = <1>; apple,dma-range = <0x0 0x80000000 0x0 0x80000000>; apple,dma-offset = <0x100 0x0>; +#include "t602x-pciec-dart-tunables.dtsi" }; DIE_NODE(apciec0): pcie@730000000 { @@ -812,6 +813,7 @@ #iommu-cells = <1>; apple,dma-range = <0x0 0x80000000 0x0 0x80000000>; apple,dma-offset = <0x100 0x0>; +#include "t602x-pciec-dart-tunables.dtsi" }; DIE_NODE(apciec1): pcie@b30000000 { @@ -1040,6 +1042,7 @@ #iommu-cells = <1>; apple,dma-range = <0x0 0x80000000 0x0 0x80000000>; apple,dma-offset = <0x100 0x0>; +#include "t602x-pciec-dart-tunables.dtsi" }; DIE_NODE(apciec2): pcie@f30000000 { diff --git a/arch/arm64/boot/dts/apple/t602x-pciec-dart-tunables.dtsi b/arch/arm64/boot/dts/apple/t602x-pciec-dart-tunables.dtsi new file mode 100644 index 00000000000000..8572cf187c17ae --- /dev/null +++ b/arch/arm64/boot/dts/apple/t602x-pciec-dart-tunables.dtsi @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * T602x Thunderbolt PCIe (PCIe-C) DART tunables, included in each + * apciec[0-2] DART node. iBoot adds "dart-tunables-instance-0" to the ADT + * at boot, but m1n1 does not copy it for these DARTs, and the DART cannot + * probe without it. Values read from J416c and J414s ADTs, identical on + * both machines and for all three ports. Offsets are relative to the + * DART's reg. + */ +apple,tunable = <0x20c 0xff0000b7 0xe40000b7>, + <0x220 0x000f0f0f 0x000f0f0f>, + <0x224 0x00ffffff 0x00080808>, + <0x300 0x00001f31 0x00000001>, + <0x308 0x3ffffffc 0x10000000>, + <0x310 0x3ffffffc 0x3ffffffc>; From 9e3a5bfd295eb0579b573774432147bdaede00f7 Mon Sep 17 00:00:00 2001 From: Wes Grimes Date: Fri, 2 Oct 2026 19:49:28 -0400 Subject: [PATCH 7/8] arm64: dts: apple: t600x: Add PCIe-C DART tunables As on T602x, the tunneled PCIe-C DART requires "apple,tunable", which iBoot adds to the ADT at boot as "dart-tunables-instance-0" and m1n1 does not copy for the dart-apciec* nodes. Add the values to the apciec0..2 DARTs of T6000/T6001. They were read from a J314s ADT and are identical for all three ports. They differ from the T602x values. apciec3 has no values yet, so pcie_apple.tunnel_kernel_init refuses that port. Signed-off-by: Wes Grimes --- .../boot/dts/apple/t600x-pciec-dart-tunables.dtsi | 11 +++++++++++ arch/arm64/boot/dts/apple/t600x-pciec.dtsi | 3 +++ 2 files changed, 14 insertions(+) create mode 100644 arch/arm64/boot/dts/apple/t600x-pciec-dart-tunables.dtsi diff --git a/arch/arm64/boot/dts/apple/t600x-pciec-dart-tunables.dtsi b/arch/arm64/boot/dts/apple/t600x-pciec-dart-tunables.dtsi new file mode 100644 index 00000000000000..e72d1595f4de55 --- /dev/null +++ b/arch/arm64/boot/dts/apple/t600x-pciec-dart-tunables.dtsi @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * T600x Thunderbolt PCIe (PCIe-C) DART tunables, included in each + * apciec[0-2] DART node. iBoot adds "dart-tunables-instance-0" to the ADT + * at boot, but m1n1 does not copy it for these DARTs, and the DART cannot + * probe without it. Values read from a J314s ADT, identical for all three + * ports. Offsets are relative to the DART's reg. + */ +apple,tunable = <0x20c 0xff000007 0xec000007>, + <0x220 0x000f0f0f 0x000f0f0f>, + <0x224 0x00ffffff 0x00080808>; diff --git a/arch/arm64/boot/dts/apple/t600x-pciec.dtsi b/arch/arm64/boot/dts/apple/t600x-pciec.dtsi index 86cc1ac550e6ba..46fdce8e904f32 100644 --- a/arch/arm64/boot/dts/apple/t600x-pciec.dtsi +++ b/arch/arm64/boot/dts/apple/t600x-pciec.dtsi @@ -16,6 +16,7 @@ interrupt-parent = <&aic>; interrupts = ; #iommu-cells = <1>; +#include "t600x-pciec-dart-tunables.dtsi" }; apciec0: pcie@730000000 { @@ -84,6 +85,7 @@ interrupt-parent = <&aic>; interrupts = ; #iommu-cells = <1>; +#include "t600x-pciec-dart-tunables.dtsi" }; apciec1: pcie@b30000000 { @@ -152,6 +154,7 @@ interrupt-parent = <&aic>; interrupts = ; #iommu-cells = <1>; +#include "t600x-pciec-dart-tunables.dtsi" }; apciec2: pcie@f30000000 { From c4726efeaf5a3c672124ada3abe253abf1bb78b4 Mon Sep 17 00:00:00 2001 From: Wes Grimes Date: Fri, 2 Oct 2026 19:27:18 -0400 Subject: [PATCH 8/8] tools: aurora-tb: Add a Thunderbolt PCIe test report A read-only script for testers of pcie_apple.tunnel_kernel_init on T600x/T602x: it checks the machine and parameter, lists the DART tunables per tunnel and the PCI and USB devices behind the tunnels, collects the relevant kernel log, and writes a markdown report. When a tunnel has no DART tunables, it prints the macOS ioreg command that reads them. Signed-off-by: Wes Grimes --- tools/aurora-tb/README.md | 24 ++++++ tools/aurora-tb/tb-pcie-report | 142 +++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 tools/aurora-tb/README.md create mode 100755 tools/aurora-tb/tb-pcie-report diff --git a/tools/aurora-tb/README.md b/tools/aurora-tb/README.md new file mode 100644 index 00000000000000..6ca8f3fae8dbb0 --- /dev/null +++ b/tools/aurora-tb/README.md @@ -0,0 +1,24 @@ +# Thunderbolt PCIe (PCIe-C) test report + +On M1 Pro/Max (T600x) and M2 Pro/Max (T602x) Macs, PCIe over +Thunderbolt is off unless the kernel is booted with +`pcie_apple.tunnel_kernel_init=1`. With it, the PCIe devices inside +Thunderbolt 3 docks and displays come up: for an Apple Studio Display, its +USB-C ports, camera, speakers and microphone. + +To test it: + +1. Add `pcie_apple.tunnel_kernel_init=1` to the kernel command line and + reboot. In GRUB, press `e` on the boot entry to add it for one boot. +2. Plug in a Thunderbolt 3 dock or display. USB-C hubs and DisplayPort-only + monitors do not use the PCIe tunnel and do not test it. +3. Run `tools/aurora-tb/tb-pcie-report` (with `sudo` if your user cannot + read the kernel log) and post the report it saves. + +Worth adding to the report: whether the device still works after +unplugging and replugging it, after power-cycling it, and after +`systemctl suspend`. + +A tunnel whose DART has no tunables in the device tree stays off. The +report then prints a read-only `ioreg` command to run in macOS on the same +Mac; its output lets those values be added. diff --git a/tools/aurora-tb/tb-pcie-report b/tools/aurora-tb/tb-pcie-report new file mode 100755 index 00000000000000..8b6d659d572990 --- /dev/null +++ b/tools/aurora-tb/tb-pcie-report @@ -0,0 +1,142 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# Report whether PCIe over Thunderbolt (PCIe-C) works on this Mac. +# +# Plug a Thunderbolt 3 dock or display (for example an Apple Studio Display) +# into the Mac, then run this. It only reads system state. Run it with sudo +# if your user cannot read the kernel log. +# +# tb-pcie-report # wait for you to plug the device in +# tb-pcie-report --no-wait # report the current state now +set -u + +dt=/proc/device-tree +param=/sys/module/pcie_apple/parameters/tunnel_kernel_init + +say() { printf '%s\n' "$*"; } + +compat=$(tr '\0' ' ' <"$dt/compatible" 2>/dev/null) +model=$(tr -d '\0' <"$dt/model" 2>/dev/null) +case "$compat" in +*apple,t6000*|*apple,t6001*) chip=T600x ;; +*apple,t6020*|*apple,t6021*) chip=T602x ;; +*apple,t8103*) chip=t8103 ;; +*) chip=other ;; +esac + +if [ "$chip" = other ]; then + say "This Mac ($compat) is not covered: only M1, M1 Pro/Max and" + say "M2 Pro/Max have PCIe-C support in this kernel." + exit 1 +fi + +enabled=no +[ "$(cat "$param" 2>/dev/null)" = Y ] && enabled=yes +# The parameter file only exists once pcie_apple is loaded. +grep -qE '(^| )pcie_apple\.tunnel_kernel_init=(1|y|Y)( |$)' /proc/cmdline && + enabled=yes +if [ "$chip" != t8103 ] && [ "$enabled" = no ]; then + say "pcie_apple.tunnel_kernel_init is not enabled, so the tunnel stays off." + say "Add pcie_apple.tunnel_kernel_init=1 to the kernel command line and" + say "reboot: press 'e' on the boot entry in GRUB for a single boot, or add" + say "it to GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub and run" + say "'sudo grub-mkconfig -o /boot/grub/grub.cfg'." + exit 1 +fi + +if [ "${1:-}" != --no-wait ]; then + say "Plug the Thunderbolt dock or display in now, wait about 20 seconds," + printf '%s' "then press Enter. " + read -r _ +fi + +if klog=$(journalctl -k -b -o short-monotonic --no-pager 2>/dev/null) && + [ -n "$klog" ]; then + : +elif klog=$(dmesg 2>/dev/null) && [ -n "$klog" ]; then + : +else + say "Cannot read the kernel log; run again with sudo." + exit 1 +fi + +out=$(mktemp "${TMPDIR:-/tmp}/tb-pcie-report.XXXXXX") || exit 1 + +{ + say "# PCIe-C report: $model" + say + say "- Machine: \`$compat\` ($chip)" + say "- Kernel: \`$(uname -r)\`" + say "- tunnel_kernel_init: $enabled" + say "- Date: $(date -u +%Y-%m-%dT%H:%MZ)" + say + say "## DART tunables" + say + missing=no + for t in "$dt"/soc/usb4-pcie-tunnel-*; do + # m1n1 only fills in ports that this Mac has. + [ -e "$t"/pcie@*/apple,tunable-rc ] || continue + for d in "$t"/iommu@*; do + if [ -e "$d/apple,tunable" ]; then + say "- $(basename "$t"): present" + else + say "- $(basename "$t"): **missing**" + missing=yes + fi + done + done + say + say "## Thunderbolt PCIe devices" + say + say '```' + found=no + for p in /sys/bus/pci/devices/*; do + addr=$(basename "$p") + case "$addr" in 0000:*) continue ;; esac + found=yes + driver=$(readlink "$p/driver" 2>/dev/null) && driver=${driver##*/} + printf '%s %s:%s class %s driver %s runtime %s\n' "$addr" \ + "$(cut -c3- "$p/vendor")" "$(cut -c3- "$p/device")" \ + "$(cut -c3- "$p/class")" "${driver:-none}" \ + "$(cat "$p/power/runtime_status" 2>/dev/null)" + done + [ "$found" = yes ] || say "(none)" + say '```' + say + say "## USB devices behind the tunnel" + say + say '```' + found=no + for u in /sys/bus/usb/devices/*; do + [ -e "$u/idVendor" ] || continue + case "$(readlink -f "$u")" in + */pci000[1-9]:*|*/pci00[1-9]?:*) ;; + *) continue ;; + esac + found=yes + printf '%s %s:%s %s %s\n' "$(basename "$u")" "$(cat "$u/idVendor")" \ + "$(cat "$u/idProduct")" "$(cat "$u/manufacturer" 2>/dev/null)" \ + "$(cat "$u/product" 2>/dev/null)" + done + [ "$found" = yes ] || say "(none)" + say '```' + say + say "## Kernel log" + say + say '```' + printf '%s\n' "$klog" | grep -E \ + 'PCIe-C|usb4-pcie-tunnel|apple-dart [0-9a-f]+21008000|xhci_hcd 000[1-9]|tunnel reset' | + grep -v -e 'RTKit: syslog' -e 'dependency cycle' | tail -n 80 + say '```' +} >"$out" + +cat "$out" +say +say "Report saved to $out" +if [ "$missing" = yes ]; then + say + say "Some tunnels have no DART tunables, so they stay off. If you can boot" + say "macOS on this Mac, please run this there and attach the output too:" + say + say " { sysctl -n hw.model; sw_vers -productVersion; ioreg -p IODeviceTree -l -w0 | grep -E 'dart-apciec|dart-tunables-instance'; } > ~/Desktop/apciec-dart.txt" +fi