mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Mika Westerberg <mika.westerberg@linux.intel.com>
To: "Francisco Beltrán Millalén" <fbeltranmillalen@gmail.com>
Cc: andreas.noever@gmail.com, westeri@kernel.org,
	YehezkelShB@gmail.com, linux-usb@vger.kernel.org,
	linux-kernel@vger.kernel.org, helgaas@kernel.org,
	bhelgaas@google.com, linux-pci@vger.kernel.org, lukas@wunner.de,
	d@rrell.co
Subject: Re: [PATCH v3] thunderbolt: Call SXFP() on Apple systems with Alpine Ridge when suspending
Date: Mon, 2 Nov 2026 15:08:07 +0100	[thread overview]
Message-ID: <20261102140807.GW158991@black.igk.intel.com> (raw)
In-Reply-To: <20261021141444.10632-1-fbeltranmillalen@gmail.com>

Hi,

On Wed, Oct 21, 2026 at 11:14:44AM -0300, Francisco Beltrán Millalén wrote:
> On Apple systems with Alpine Ridge, suspending to RAM with a device
> connected to a Type-C port leaves the PCIe link to the upstream port of
> the controller down on resume, and the ports are lost until reboot.
> Calling the SXFP(0) ACPI method under the NHI before entering sleep
> avoids that.
> 
> The firmware can set bit 26 of plug events CS_20 when it resumes the
> NHI. While it is set SXFP(0) turns the router off and it does not come
> back on resume, so clear the bit first and skip SXFP(0) if that fails.
> 
> Clearing the bit needs the control channel, which is already stopped
> when the suspend_noirq hook of the NHI runs. So add a new NHI hook,
> pre_suspend_noirq, that runs just before the domain is suspended, and
> use it for Alpine Ridge.
> 
> Signed-off-by: Francisco Beltrán Millalén <fbeltranmillalen@gmail.com>
> ---
> Thanks for the review, Mika, and for the idea of moving this to pci.c.
> 
> I did not use the suspend_noirq hook in the end, because it runs too
> late.  By the time it is called, the domain has been suspended and the
> control channel is stopped, so bit 26 can no longer be cleared.  I also
> tried clearing the bit earlier and calling SXFP(0) from suspend_noirq,
> but then the side with the disk did not come back on resume.  I only
> ran that once, so I did not go further with it.
> 
> Instead, this adds a new hook that runs just before the domain is
> suspended, while the control channel still works.  That is about the
> same point where v2 called SXFP(0).  ar_nhi_ops provides it with the
> code you suggested.  If you would prefer another way, please let me
> know.
> 
> v3:
> - Renamed the function to tb_acpi_force_power(), and moved the Apple
>   and Alpine Ridge checks to ar_nhi_ops in pci.c, as you suggested.
>   tb.c is no longer changed.
> - Use the new pre_suspend_noirq hook instead of suspend_noirq (see
>   above).
> - Return early when the ICM firmware is in use, as v2 only ran with the
>   software connection manager.
> - ar_nhi_ops keeps the other default NHI operations.
> - Moved the declaration and added the empty line you asked for.
> 
> v2: https://lore.kernel.org/all/20261019202900.39062-1-fbeltranmillalen@gmail.com/
> v1: https://lore.kernel.org/all/20261019134851.7496-1-fbeltranmillalen@gmail.com/
> 
> The note on v2 about SXFP(0) and the xHCI on the side with nothing
> connected still applies, as SXFP(0) is called at about the same point.
> 
> Testing:
> I tested it on a MacBookPro14,3 (two Alpine Ridge controllers, software
> connection manager) with 6.18.49 and a backport of this patch.  The kernel also carried the PCI series I sent
> alongside v2:
> https://lore.kernel.org/all/20261019202445.38259-1-fbeltranmillalen@gmail.com/
> and a local change equivalent to the ACPICA fix mentioned in v1,
> without which the firmware does not set bit 26 here.  The old PCI
> quirk was switched off.  All cycles were 30 second suspends to RAM.
> 
> - Three cycles with a USB 3 disk on one side or the other, after a
>   first one that I don't count.  Both domains came back each time,
>   with both upstream port links at 8 GT/s x4, and the disk could be
>   read.  In each cycle SXFP(0) was called on both NHIs before their
>   control channel was stopped.
> - One cycle with nothing connected.  Both domains came back, and the
>   disk worked at USB 3 speed when plugged into either port on one side
>   afterwards.  In the lower port it first came up at USB 2 through the
>   PCH xHCI, twice; with the connector flipped it came up at USB 3 on
>   the controller's xHCI.  I think that is the cable.
> 
> There was no kernel WARNING during the whole boot.  The version for
> your tree was only built (with W=1), not booted, so the default NHI
> operations copied into ar_nhi_ops were not run here.
> 
> Not tested:
> - Real Thunderbolt devices, only the USB 3 disk behind the
>   controller's xHCI.
> - Other Alpine Ridge variants.
> - A suspend aborted after SXFP(0).
> - An NHI that is runtime suspended when the system suspends.  Here
>   both NHIs were always active.  If one is runtime suspended, the PM
>   core may skip its suspend_noirq (direct_complete), and then SXFP(0)
>   is not called.
> - Suspend-to-idle and hibernation; the checks in
>   tb_acpi_force_power() keep the patch out of both.
> 
> As noted in v1, a device plugged in while the system is suspended does
> not wake it.  I did not repeat that with this version.
> 
>  drivers/thunderbolt/acpi.c    | 47 +++++++++++++++++++++++++++++++++++++++++++
>  drivers/thunderbolt/domain.c  |  2 ++
>  drivers/thunderbolt/nhi.h     |  3 +++
>  drivers/thunderbolt/pci.c     | 45 +++++++++++++++++++++++++++++++++--------
>  drivers/thunderbolt/switch.c  | 32 +++++++++++++++++++++++++++++
>  drivers/thunderbolt/tb.h      |  4 ++++
>  drivers/thunderbolt/tb_regs.h |  3 +++
>  7 files changed, 128 insertions(+), 8 deletions(-)
> 
> diff --git a/drivers/thunderbolt/acpi.c b/drivers/thunderbolt/acpi.c
> index 53546bc477a5..824938a459d5 100644
> --- a/drivers/thunderbolt/acpi.c
> +++ b/drivers/thunderbolt/acpi.c
> @@ -8,6 +8,7 @@
>  
>  #include <linux/acpi.h>
>  #include <linux/pm_runtime.h>
> +#include <linux/suspend.h>
>  
>  #include "tb.h"
>  
> @@ -279,6 +280,52 @@ int tb_acpi_power_off_retimers(struct tb_port *port)
>  	return tb_acpi_retimer_set_power(port, false);
>  }
>  
> +/**
> + * tb_acpi_force_power() - Call SXFP(0) when entering system sleep
> + * @tb: Domain that is being suspended
> + *
> + * On Apple systems with Alpine Ridge the PCIe link to the upstream port
> + * of the controller does not come up on resume from system sleep if a
> + * device is connected, unless SXFP(0) ("Sx Force Power") under the NHI
> + * is called before entering it. Bit 26 of plug events CS_20 must be
> + * clear when SXFP(0) is called, otherwise the router is turned off and
> + * does not come back on resume.
> + *
> + * Must be called with @tb->lock held, before the domain is suspended:
> + * clearing the bit needs the control channel, and SXFP(0) does not help
> + * once the router has been put to sleep.
> + */
> +void tb_acpi_force_power(struct tb *tb)
> +{
> +	struct tb_switch *sw = tb->root_switch;
> +	struct acpi_device *adev;
> +	acpi_status status;
> +
> +	if (tb_switch_is_icm(sw))
> +		return;
> +	/*
> +	 * Only when firmware handles system suspend, as in
> +	 * quirk_apple_poweroff_thunderbolt(): the OS does not know how to
> +	 * power the controller back on. pm_suspend_in_progress() excludes
> +	 * hibernation.
> +	 */
> +	if (!pm_suspend_in_progress() || !pm_suspend_via_firmware())
> +		return;
> +
> +	adev = ACPI_COMPANION(tb->nhi->dev);
> +	if (!adev || !acpi_has_method(adev->handle, "SXFP"))
> +		return;
> +
> +	if (tb_switch_set_bit26(sw, false))
> +		return;
> +
> +	tb_sw_dbg(sw, "ACPI: calling SXFP\n");
> +	status = acpi_execute_simple_method(adev->handle, "SXFP", 0);
> +	if (ACPI_FAILURE(status))
> +		tb_sw_warn(sw, "ACPI: SXFP failed: %s\n",
> +			   acpi_format_exception(status));
> +}
> +
>  static bool tb_acpi_bus_match(struct device *dev)
>  {
>  	return tb_is_switch(dev) || tb_is_usb4_port_device(dev);
> diff --git a/drivers/thunderbolt/domain.c b/drivers/thunderbolt/domain.c
> index 12c88509a54f..a9a6d63bf414 100644
> --- a/drivers/thunderbolt/domain.c
> +++ b/drivers/thunderbolt/domain.c
> @@ -533,6 +533,8 @@ int tb_domain_suspend_noirq(struct tb *tb)
>  	 * we actually have stopped the domain and the control channel.
>  	 */
>  	mutex_lock(&tb->lock);
> +	if (tb->nhi->ops->pre_suspend_noirq)
> +		tb->nhi->ops->pre_suspend_noirq(tb->nhi);

We should not call NHI things in TB side of code (that's domain and the
routers, adapters etc). So this should be moved elsewhere.

>  	if (tb->cm_ops->suspend_noirq)
>  		ret = tb->cm_ops->suspend_noirq(tb);
>  	if (!ret)
> diff --git a/drivers/thunderbolt/nhi.h b/drivers/thunderbolt/nhi.h
> index b2e2e2c413b2..6f66a2608d06 100644
> --- a/drivers/thunderbolt/nhi.h
> +++ b/drivers/thunderbolt/nhi.h
> @@ -44,6 +44,8 @@ extern const struct dev_pm_ops nhi_pm_ops;
>   * struct tb_nhi_ops - NHI specific optional operations
>   * @init: NHI specific initialization
>   * @add_links: Setup device links for tunneling native protocols
> + * @pre_suspend_noirq: hook to run in suspend_noirq before the domain is
> + *		       suspended, while the control channel is still running
>   * @suspend_noirq: NHI specific suspend_noirq hook
>   * @resume_noirq: NHI specific resume_noirq hook
>   * @runtime_suspend: NHI specific runtime_suspend hook
> @@ -60,6 +62,7 @@ extern const struct dev_pm_ops nhi_pm_ops;
>  struct tb_nhi_ops {
>  	int (*init)(struct tb_nhi *nhi);
>  	bool (*add_links)(struct tb_nhi *nhi);
> +	void (*pre_suspend_noirq)(struct tb_nhi *nhi);
>  	int (*suspend_noirq)(struct tb_nhi *nhi, bool wakeup);
>  	int (*resume_noirq)(struct tb_nhi *nhi);
>  	int (*runtime_suspend)(struct tb_nhi *nhi);
> diff --git a/drivers/thunderbolt/pci.c b/drivers/thunderbolt/pci.c
> index e40d4d6af071..c25f33221df1 100644
> --- a/drivers/thunderbolt/pci.c
> +++ b/drivers/thunderbolt/pci.c
> @@ -360,6 +360,27 @@ static const struct tb_nhi_ops pci_nhi_default_ops = {
>  	.reset_interface = nhi_reset_interface,
>  };
>  
> +/* Alpine Ridge specific NHI operations */
> +
> +static void ar_nhi_pre_suspend_noirq(struct tb_nhi *nhi)
> +{
> +	if (x86_apple_machine)
> +		tb_acpi_force_power(dev_get_drvdata(nhi->dev));
> +}
> +
> +static const struct tb_nhi_ops ar_nhi_ops = {
> +	.add_links = nhi_pci_add_links,
> +	.pre_suspend_noirq = ar_nhi_pre_suspend_noirq,
> +	.pre_nvm_auth = nhi_pci_start_dma_port,
> +	.post_nvm_auth = nhi_pci_complete_dma_port,
> +	.request_ring_irq = nhi_pci_ring_request_msix,
> +	.release_ring_irq = nhi_pci_ring_release_msix,
> +	.shutdown = nhi_pci_release_irq,
> +	.is_present = nhi_pci_is_present,
> +	.init_interrupts = nhi_pci_init_msi,
> +	.reset_interface = nhi_reset_interface,

This hook is now so please check that the patch applies to the latest next
branch.

> +};
> +
>  /* Ice Lake specific NHI operations */
>  
>  #define ICL_LC_MAILBOX_TIMEOUT	500 /* ms */
> @@ -677,14 +698,22 @@ static struct pci_device_id nhi_ids[] = {
>  	},
>  
>  	/* Thunderbolt 3 */
> -	{ PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_2C_NHI) },
> -	{ PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_4C_NHI) },
> -	{ PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_USBONLY_NHI) },
> -	{ PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_LP_NHI) },
> -	{ PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_LP_USBONLY_NHI) },
> -	{ PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_C_2C_NHI) },
> -	{ PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_C_4C_NHI) },
> -	{ PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_C_USBONLY_NHI) },
> +	{ PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_2C_NHI),
> +	  .driver_data = (kernel_ulong_t)&ar_nhi_ops },
> +	{ PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_4C_NHI),
> +	  .driver_data = (kernel_ulong_t)&ar_nhi_ops },
> +	{ PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_USBONLY_NHI),
> +	  .driver_data = (kernel_ulong_t)&ar_nhi_ops },
> +	{ PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_LP_NHI),
> +	  .driver_data = (kernel_ulong_t)&ar_nhi_ops },
> +	{ PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_LP_USBONLY_NHI),
> +	  .driver_data = (kernel_ulong_t)&ar_nhi_ops },
> +	{ PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_C_2C_NHI),
> +	  .driver_data = (kernel_ulong_t)&ar_nhi_ops },
> +	{ PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_C_4C_NHI),
> +	  .driver_data = (kernel_ulong_t)&ar_nhi_ops },
> +	{ PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ALPINE_RIDGE_C_USBONLY_NHI),
> +	  .driver_data = (kernel_ulong_t)&ar_nhi_ops },
>  	{ PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TITAN_RIDGE_2C_NHI) },
>  	{ PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_TITAN_RIDGE_4C_NHI) },
>  	{ PCI_VDEVICE(INTEL, PCI_DEVICE_ID_INTEL_ICL_NHI0),
> diff --git a/drivers/thunderbolt/switch.c b/drivers/thunderbolt/switch.c
> index 4b8bcba7bd2f..44141b19c1f2 100644
> --- a/drivers/thunderbolt/switch.c
> +++ b/drivers/thunderbolt/switch.c
> @@ -4027,6 +4027,38 @@ int tb_switch_xhci_connect(struct tb_switch *sw)
>  	return 0;
>  }
>  
> +/**
> + * tb_switch_set_bit26() - Set or clear bit 26 of plug events CS_20
> + * @sw: Alpine Ridge router
> + * @value: %true to set the bit, %false to clear it
> + *
> + * On Apple systems the firmware can set this bit when it resumes the
> + * NHI. While it is set, SXFP(0) turns the router off and it does not
> + * come back on resume.
> + *
> + * Return: %0 on success, negative errno otherwise.
> + */
> +int tb_switch_set_bit26(struct tb_switch *sw, bool value)
> +{
> +	unsigned int offset = sw->cap_plug_events + TB_PLUG_EVENTS_CS_20;
> +	u32 val;
> +	int ret;
> +
> +	ret = tb_sw_read(sw, &val, TB_CFG_SWITCH, offset, 1);
> +	if (ret)
> +		return ret;
> +
> +	if (!!(val & TB_PLUG_EVENTS_CS_20_BIT26) == value)
> +		return 0;
> +
> +	if (value)
> +		val |= TB_PLUG_EVENTS_CS_20_BIT26;
> +	else
> +		val &= ~TB_PLUG_EVENTS_CS_20_BIT26;
> +
> +	return tb_sw_write(sw, &val, TB_CFG_SWITCH, offset, 1);
> +}
> +
>  /**
>   * tb_switch_xhci_disconnect() - Disconnect internal xHCI
>   * @sw: Router whose xHCI to disconnect
> diff --git a/drivers/thunderbolt/tb.h b/drivers/thunderbolt/tb.h
> index e86845762e22..9900d0018174 100644
> --- a/drivers/thunderbolt/tb.h
> +++ b/drivers/thunderbolt/tb.h
> @@ -1099,6 +1099,8 @@ int tb_switch_pcie_l1_enable(struct tb_switch *sw);
>  int tb_switch_xhci_connect(struct tb_switch *sw);
>  void tb_switch_xhci_disconnect(struct tb_switch *sw);
>  
> +int tb_switch_set_bit26(struct tb_switch *sw, bool value);
> +
>  int tb_port_state(struct tb_port *port);
>  int tb_wait_for_port(struct tb_port *port, bool wait_if_unplugged);
>  int tb_port_add_nfc_credits(struct tb_port *port, int credits);
> @@ -1524,6 +1526,7 @@ int tb_acpi_init(void);
>  void tb_acpi_exit(void);
>  int tb_acpi_power_on_retimers(struct tb_port *port);
>  int tb_acpi_power_off_retimers(struct tb_port *port);
> +void tb_acpi_force_power(struct tb *tb);
>  #else
>  static inline bool tb_acpi_add_links(struct tb_nhi *nhi) { return false; }
>  
> @@ -1537,6 +1540,7 @@ static inline int tb_acpi_init(void) { return 0; }
>  static inline void tb_acpi_exit(void) { }
>  static inline int tb_acpi_power_on_retimers(struct tb_port *port) { return 0; }
>  static inline int tb_acpi_power_off_retimers(struct tb_port *port) { return 0; }
> +static inline void tb_acpi_force_power(struct tb *tb) { }
>  #endif
>  
>  #ifdef CONFIG_DEBUG_FS
> diff --git a/drivers/thunderbolt/tb_regs.h b/drivers/thunderbolt/tb_regs.h
> index 92d893634d2b..0940e8b12d89 100644
> --- a/drivers/thunderbolt/tb_regs.h
> +++ b/drivers/thunderbolt/tb_regs.h
> @@ -563,6 +563,9 @@ struct tb_regs_hop {
>  #define TB_PLUG_EVENTS_CS_1_LOW_DPIN_DISABLE	BIT(5)
>  #define TB_PLUG_EVENTS_CS_1_HIGH_DPIN_DISABLE	BIT(6)
>  
> +#define TB_PLUG_EVENTS_CS_20			0x14
> +#define TB_PLUG_EVENTS_CS_20_BIT26		BIT(26)
> +
>  #define TB_PLUG_EVENTS_PCIE_WR_DATA		0x1b
>  #define TB_PLUG_EVENTS_PCIE_CMD			0x1c
>  #define TB_PLUG_EVENTS_PCIE_CMD_DW_OFFSET_MASK	GENMASK(9, 0)

      reply	other threads:[~2026-11-02 14:08 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-21 14:14 Francisco Beltrán Millalén
2026-11-02 14:08 ` Mika Westerberg [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261102140807.GW158991@black.igk.intel.com \
    --to=mika.westerberg@linux.intel.com \
    --cc=YehezkelShB@gmail.com \
    --cc=andreas.noever@gmail.com \
    --cc=bhelgaas@google.com \
    --cc=d@rrell.co \
    --cc=fbeltranmillalen@gmail.com \
    --cc=helgaas@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-pci@vger.kernel.org \
    --cc=linux-usb@vger.kernel.org \
    --cc=lukas@wunner.de \
    --cc=westeri@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®