Skip to content

Commit

Permalink
thunderbolt: Power down controller on Macs when nothing is plugged in
Browse files Browse the repository at this point in the history
Document and implement Apple's ACPI-based (but nonstandard) PM mechanism
for Thunderbolt 1 and 2.  Briefly, an ACPI method provided by Apple is
used to cut power to the controller.  A GPE is enabled while the
controller is powered down which sideband-signals a plug event,
whereupon power is reinstated using the ACPI method.

This saves 1.5 W on machines with a Light Ridge controller and is
reported to save 4 W on Cactus Ridge 4C and Falcon Ridge 4C.  (I believe
4 W includes the bus power drawn by Apple's Gigabit Ethernet adapter.)
It fixes a power regression introduced in 3.17 by commit 7bc5a2b
("ACPI: Support _OSI("Darwin") correctly").

A Thunderbolt controller appears to the OS as a set of PCI devices:
One upstream bridge, multiple downstream bridges and one NHI (Native
Host Interface).  The upstream and downstream bridges represent a PCIe
switch (see definition of a switch in the PCIe spec).  The NHI device is
used to manage the Converged I/O switch over which PCI tunnels are set
up to hotplugged devices.  Those devices in turn appear behind the
downstream bridges:

  (Root Port) ---- Upstream Bridge --+-- Downstream Bridge 0 ---- NHI
                                     +-- Downstream Bridge 1 --
                                     +-- Downstream Bridge 2 --
                                     ...

Power is cut to the entire set of devices.  The Linux pm model is
hierarchical and assumes that a child cannot resume before its parent.
To conform to this model, power control must be governed by the
Thunderbolt controller's topmost device, which is the upstream bridge.
The NHI and downstream bridges go to D3hot independently and the
upstream bridge goes to D3cold once all its children have suspended.
Hence this commit modifies runtime PM of the upstream bridge.

Because Apple's ACPI methods are nonstandard, a struct dev_pm_domain is
used to override the PCI bus pm_ops.  The thunderbolt driver binds to
the NHI, thus the dev_pm_domain is assigned to the upstream bridge when
its grandchild ->probes and is evicted when it ->removes.

The NHI is not able to detect plug events while suspended, it relies on
the GPE handler to resume it on hotplug.  A runtime PM ref is held for
the duration of tb_handle_hotplug() to keep the NHI awake as long as a
hotplug event is processed.  Apart from that a runtime PM ref is held
for each attached Thunderbolt switch to ensure the NHI stays runtime
active as long as devices are plugged in.  This behaviour is identical
to the macOS driver.  It implies that RTD3 (runtime D3cold with devices
attached) is not available on Macs.

Handles for the ACPI methods and various other data is stored in a newly
introduced struct tb_pm which is pointed to by struct tb.  This data is
not specific to OS-controlled tunnel management:  Thunderbolt 3 Macs
(which use ICM-controlled tunnel management) use a similar PM mechanism.
If the need arises to store additional or different PM data for Macs or
non-Macs with Thunderbolt 3 efficiently then struct tb_pm can be changed
to a union.

There are no Thunderbolt specs publicly available from Intel or Apple,
so I've included documentation to the extent that I was able to reverse-
engineer things.  Documentation on the Go2Sx and Ok2Go2Sx pins is
tentative as those are missing on my Light Ridge.  Apple only uses them
on Cactus Ridge 4C.  Someone with such a controller needs to find out
through experimentation if the documentation is accurate and amend it if
necessary.

Bugzilla: https://bugzilla.kernel.org/show_bug.cgi?id=92111
Signed-off-by: Lukas Wunner <lukas@wunner.de>
  • Loading branch information
l1k committed Jan 5, 2019
1 parent 15bb943 commit 4db7f0b
Show file tree
Hide file tree
Showing 7 changed files with 379 additions and 2 deletions.
2 changes: 1 addition & 1 deletion Documentation/acpi/osi.txt
Expand Up @@ -184,4 +184,4 @@ The Linux-3.18 change in default caused power regressions on Mac
laptops, and the 3.18 implementation did not allow changing
the default via cmdline "acpi_osi=!Darwin". Linux-4.7 fixed
the ability to use acpi_osi=!Darwin as a workaround, and
we hope to see Mac Thunderbolt power management support in Linux-4.11.
Linux-4.22 added support for Mac Thunderbolt power management.
5 changes: 5 additions & 0 deletions drivers/thunderbolt/Makefile
@@ -1,3 +1,8 @@
obj-${CONFIG_THUNDERBOLT} := thunderbolt.o
thunderbolt-objs := nhi.o ctl.o tb.o switch.o cap.o path.o tunnel_pci.o eeprom.o
thunderbolt-objs += adapter_pci.o domain.o dma_port.o icm.o property.o xdomain.o
ifdef CONFIG_ACPI
ifdef CONFIG_PM
thunderbolt-objs += pm_apple.o
endif
endif
21 changes: 20 additions & 1 deletion drivers/thunderbolt/nhi.c
Expand Up @@ -12,12 +12,14 @@
#include <linux/slab.h>
#include <linux/errno.h>
#include <linux/pci.h>
#include <linux/platform_data/x86/apple.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/delay.h>

#include "nhi.h"
#include "nhi_regs.h"
#include "pm_apple.h"
#include "tb.h"

#define RING_TYPE(ring) ((ring)->is_tx ? "TX ring" : "RX ring")
Expand Down Expand Up @@ -924,9 +926,20 @@ static int nhi_runtime_resume(struct device *dev)
{
struct pci_dev *pdev = to_pci_dev(dev);
struct tb *tb = pci_get_drvdata(pdev);
int ret;

nhi_enable_int_throttling(tb->nhi);
return tb_domain_runtime_resume(tb);
ret = tb_domain_runtime_resume(tb);

/*
* If runtime resuming due to hotplug, it may take about 700 ms
* until the event is received on the control channel. Wait as
* long even if the user has set a shorter autosuspend delay.
*/
pm_schedule_suspend(dev, dev->power.autosuspend_delay > 750 ?
dev->power.autosuspend_delay : 750);

return ret;
}
#endif /* CONFIG_PM */

Expand Down Expand Up @@ -1081,6 +1094,9 @@ static int nhi_probe(struct pci_dev *pdev, const struct pci_device_id *id)
pm_runtime_use_autosuspend(&pdev->dev);
pm_runtime_put_autosuspend(&pdev->dev);

if (x86_apple_machine)
tb_pm_apple_init(tb);

return 0;
}

Expand All @@ -1089,6 +1105,9 @@ static void nhi_remove(struct pci_dev *pdev)
struct tb *tb = pci_get_drvdata(pdev);
struct tb_nhi *nhi = tb->nhi;

if (x86_apple_machine)
tb_pm_apple_fini(tb);

pm_runtime_get_sync(&pdev->dev);
pm_runtime_dont_use_autosuspend(&pdev->dev);
pm_runtime_forbid(&pdev->dev);
Expand Down
326 changes: 326 additions & 0 deletions drivers/thunderbolt/pm_apple.c
@@ -0,0 +1,326 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Power down Thunderbolt controller on Macs when nothing is plugged in
*
* Copyright (C) 2019 Lukas Wunner <lukas@wunner.de>
*
* Apple provides the following means for power control in ACPI:
*
* * On Macs with Thunderbolt 1 Gen 1 controllers (Light Ridge, Eagle Ridge):
* * XRPE method ("Power Enable"), takes argument 1 or 0, toggles a GPIO pin
* to switch the controller on or off.
* * XRIN named object (alternatively _GPE), contains number of a GPE which
* fires as long as something is plugged in (regardless of power state).
* * XRIL method ("Interrupt Low"), returns 0 as long as something is
* plugged in, 1 otherwise.
* * XRIP and XRIO methods, unused by macOS driver.
*
* * On Macs with Thunderbolt 1 Gen 2 controllers (Cactus Ridge 4C):
* * XRIN not only fires as long as something is plugged in, but also as long
* as the controller's CIO switch is powered up.
* * XRIL method changed its meaning, it returns 0 as long as the CIO switch
* is powered up, 1 otherwise.
* * Additional SXFP method ("Force Power"), accepts only argument 0,
* switches the controller off. This carries out just the raw power
* change, unlike XRPE which disables the link on the PCIe Root Port
* in an orderly fashion before switching off the controller.
* * Additional SXLV, SXIO, SXIL methods to utilize the Go2Sx and Ok2Go2Sx
* pins (see background below). Apparently SXLV toggles the value given to
* the POC via Go2Sx (0 or 1), SXIO changes the direction (0 or 1) and SXIL
* returns the value received from the POC via Ok2Go2Sx.
* * On some Macs, additional XRST method, takes argument 1 or 0, asserts or
* deasserts a GPIO pin to reset the controller.
* * On Macs introduced 2013, XRPE was renamed TRPE.
*
* * On Macs with Thunderbolt 2 controllers (Falcon Ridge 4C and 2C):
* * SXLV, SXIO, SXIL methods to utilize Go2Sx and Ok2Go2Sx are gone.
* * On the MacPro6,1 which has multiple Thunderbolt controllers, each NHI
* device has a separate XRIN GPE and separate TRPE, SXFP and XRIL methods.
*
* Background:
*
* * Gen 1 controllers (Light Ridge, Eagle Ridge) had no power management
* and no ability to distinguish whether a DP or Thunderbolt device is
* plugged in. Apple put an ARM Cortex MCU (NXP LPC1112A) on the logic board
* which snoops on the connector lines and, depending on the type of device,
* sends an HPD signal to the GPU or fires the Thunderbolt XRIN doorbell
* interrupt. The switches for the 3.3V and 1.05V power rails of the
* Thunderbolt controller are toggled by a GPIO pin on the southbridge.
*
* * On gen 2 controllers (Cactus Ridge 4C), Intel integrated the MCU into the
* controller and called it POC. This caused a change of semantics for XRIN
* and XRIL. The POC is powered by a separate 3.3V rail which is active even
* in sleep state S4. It only draws a very small current. The regular 3.3V
* and 1.05V power rails are no longer controlled by the southbridge but by
* the POC. In other words the controller powers *itself* up and down! It's
* instructed to do so with the Go2Sx pin. Another pin, Ok2Go2Sx, allows the
* controller to indicate if it is ready to power itself down. Apple wires
* Go2Sx and Ok2Go2Sx to the same GPIO pin on the southbridge, hence the pin
* is used bidirectionally. A third pin, Force Power, is intended by Intel
* for debug only but Apple abuses it for XRPE/TRPE and SXFP. They utilize
* Go2Sx and Ok2Go2Sx only on Cactus Ridge, presumably because the controller
* somehow requires that. On Falcon Ridge they forego these pins and rely
* solely on Force Power.
*
* Implementation Notes:
*
* * To conform to Linux' hierarchical power management model, power control
* is governed by the topmost PCI device of the controller, which is the
* upstream bridge. The controller is powered down once all child devices
* of the upstream bridge have suspended and its autosuspend delay has
* elapsed.
*
* * The autosuspend delay is user configurable via sysfs and should be lower
* or equal to that of the NHI since hotplug events are not acted upon if
* the NHI has suspended but the controller has not yet powered down.
* However the delay should not be zero to avoid frequent power changes
* (e.g. multiple times just for lspci -vv) since powering up takes 2 sec.
* (Powering down is almost instantaneous.)
*/

#include <linux/acpi.h>
#include <linux/delay.h>
#include <linux/pci.h>
#include <linux/pm_domain.h>
#include <linux/pm_runtime.h>
#include "tb.h"

/**
* struct tb_pm - Thunderbolt power management data
* @tb: Pointer to the Thunderbolt domain this PM data belongs to
* @pm_domain: PM domain assigned to controller's PCIe upstream bridge
* @wake_gpe: GPE used as hotplug interrupt during powerdown
* @set: ACPI method to power controller up/down
* @get: ACPI method to query power state of controller
*/
struct tb_pm {
struct tb *tb;
struct dev_pm_domain pm_domain;
unsigned long long wake_gpe;
acpi_handle set;
acpi_handle get;
};

/*
* The dev_pm_ops assigned to the upstream bridge use pr_*() instead of dev_*()
* to get a "thunderbolt" prefix on messages, rather than "pcieport".
*/
#undef pr_fmt
#define pr_fmt(fmt) KBUILD_MODNAME " %s: " fmt, dev_name(dev)

#define to_pm(dev) container_of(dev->pm_domain, struct tb_pm, pm_domain)

static int upstream_prepare(struct device *dev)
{
struct tb_pm *pm = to_pm(dev);

if (pm_runtime_active(dev))
return 0;

/* prevent interrupts during system sleep transition */
if (ACPI_FAILURE(acpi_disable_gpe(NULL, pm->wake_gpe))) {
pr_err("cannot disable wake GPE, resuming\n");
pm_request_resume(dev);
return -EAGAIN;
}

return DPM_DIRECT_COMPLETE;
}

static void upstream_complete(struct device *dev)
{
struct tb_pm *pm = to_pm(dev);

if (pm_runtime_active(dev))
return;

/*
* If the controller was powered down before system sleep, calling XRPE
* to power it up will fail on the next runtime resume. An additional
* call to XRPE is necessary to reset the power switch first.
*/
pr_debug("resetting power switch\n");
if (ACPI_FAILURE(acpi_execute_simple_method(pm->set, NULL, 0))) {
pr_err("cannot call pm->set method\n");
dev->power.runtime_error = -EIO;
}

if (ACPI_FAILURE(acpi_enable_gpe(NULL, pm->wake_gpe))) {
pr_err("cannot enable wake GPE, resuming\n");
pm_request_resume(dev);
}
}

static int upstream_runtime_suspend(struct device *dev)
{
struct pci_dev *pdev = to_pci_dev(dev);
struct tb_pm *pm = to_pm(dev);
unsigned long long powered_down;
int ret, i;

/* children are effectively in D3cold once upstream goes to D3hot */
pci_bus_set_current_state(pdev->subordinate, PCI_D3cold);

ret = dev->bus->pm->runtime_suspend(dev);
if (ret) {
pci_wakeup_bus(pdev->subordinate);
return ret;
}

pr_debug("powering down\n");
pdev->current_state = PCI_D3cold;
if (ACPI_FAILURE(acpi_execute_simple_method(pm->set, NULL, 0))) {
pr_err("cannot call pm->set method, resuming\n");
goto err_resume;
}

/*
* On Cactus Ridge the wake GPE fires as long as the CIO switch is
* powered up. Poll until it's powered down before enabling the GPE.
* macOS polls up to 300 times with a 1 ms delay, just mimic that.
*/
for (i = 0; i < 300; i++) {
if (ACPI_FAILURE(acpi_evaluate_integer(pm->get,
NULL, NULL, &powered_down))) {
pr_err("cannot call pm->get method, resuming\n");
goto err_resume;
}
if (powered_down)
break;
usleep_range(800, 1200);
}
if (!powered_down) {
pr_notice("refused to power down, resuming\n");
goto err_resume;
}

if (ACPI_FAILURE(acpi_enable_gpe(NULL, pm->wake_gpe))) {
pr_err("cannot enable wake GPE, resuming\n");
goto err_resume;
}

return 0;

err_resume:
acpi_execute_simple_method(pm->set, NULL, 1);
dev->bus->pm->runtime_resume(dev);
pci_wakeup_bus(pdev->subordinate);
return -EAGAIN;
}

static int upstream_runtime_resume(struct device *dev)
{
struct pci_dev *pdev = to_pci_dev(dev);
struct tb_pm *pm = to_pm(dev);
int ret;

if (!dev->power.is_prepared &&
ACPI_FAILURE(acpi_disable_gpe(NULL, pm->wake_gpe))) {
pr_err("cannot disable wake GPE, disabling runtime pm\n");
pm_runtime_disable(&pm->tb->nhi->pdev->dev);
}

pr_debug("powering up\n");
if (ACPI_FAILURE(acpi_execute_simple_method(pm->set, NULL, 1))) {
pr_err("cannot call pm->set method\n");
return -ENODEV;
}

ret = dev->bus->pm->runtime_resume(dev);

/* wake children to force pci_restore_state() after D3cold */
pci_wakeup_bus(pdev->subordinate);

return ret;
}

static u32 nhi_wake(acpi_handle gpe_device, u32 gpe_number, void *ctx)
{
struct device *nhi_dev = ctx;

WARN_ON(pm_request_resume(nhi_dev) < 0);
return ACPI_INTERRUPT_HANDLED;
}

void tb_pm_apple_init(struct tb *tb)
{
struct device *nhi_dev = &tb->nhi->pdev->dev;
struct acpi_handle *nhi_handle;
struct tb_pm *pm;

/* no PM support for Alpine Ridge yet */
if (tb->root_switch->generation >= 3)
goto err_rpm_get;

pm = kzalloc(sizeof(*pm), GFP_KERNEL);
if (!pm)
goto err_free;

nhi_handle = ACPI_HANDLE(nhi_dev);
if (!nhi_handle) {
dev_err(nhi_dev, "cannot find ACPI handle\n");
goto err_free;
}

/* Macs introduced 2011/2012 have XRPE, 2013+ have TRPE */
if (ACPI_FAILURE(acpi_get_handle(nhi_handle, "XRPE", &pm->set)) &&
ACPI_FAILURE(acpi_get_handle(nhi_handle, "TRPE", &pm->set))) {
dev_err(nhi_dev, "cannot find pm->set method\n");
goto err_free;
}

if (ACPI_FAILURE(acpi_get_handle(nhi_handle, "XRIL", &pm->get))) {
dev_err(nhi_dev, "cannot find pm->get method\n");
goto err_free;
}

if (ACPI_FAILURE(acpi_evaluate_integer(nhi_handle, "XRIN", NULL,
&pm->wake_gpe))) {
dev_err(nhi_dev, "cannot find wake GPE\n");
goto err_free;
}

if (ACPI_FAILURE(acpi_install_gpe_handler(NULL, pm->wake_gpe,
ACPI_GPE_LEVEL_TRIGGERED, nhi_wake, nhi_dev))) {
dev_err(nhi_dev, "cannot install GPE handler\n");
goto err_free;
}

pm->pm_domain.ops = *tb->upstream->dev.bus->pm;
pm->pm_domain.ops.prepare = upstream_prepare;
pm->pm_domain.ops.complete = upstream_complete;
pm->pm_domain.ops.runtime_suspend = upstream_runtime_suspend;
pm->pm_domain.ops.runtime_resume = upstream_runtime_resume;
pm->tb = tb;
dev_pm_domain_set(&tb->upstream->dev, &pm->pm_domain);

tb->pm = pm;
return;

err_free:
kfree(pm);
dev_err(nhi_dev, "controller will stay powered up permanently\n");
err_rpm_get:
pm_runtime_get_noresume(nhi_dev);
}

void tb_pm_apple_fini(struct tb *tb)
{
struct device *nhi_dev = &tb->nhi->pdev->dev;
struct tb_pm *pm = tb->pm;

if (!pm) {
/* tb_pm_apple_init() failed */
pm_runtime_put_noidle(nhi_dev);
return;
}

tb->pm = NULL;
dev_pm_domain_set(&tb->upstream->dev, NULL);

if (ACPI_FAILURE(acpi_remove_gpe_handler(NULL, pm->wake_gpe,
nhi_wake)))
dev_err(nhi_dev, "cannot remove GPE handler\n");

kfree(pm);
}

0 comments on commit 4db7f0b

Please sign in to comment.