diff mbox series

[v2,12/12] xhci: tegra: enable ELPG for runtime/system PM

Message ID 20200831044043.1561074-13-jckuo@nvidia.com
State Changes Requested
Headers show
Series Tegra XHCI controller ELPG support | expand

Commit Message

JC Kuo Aug. 31, 2020, 4:40 a.m. UTC
This commit implements the complete programming sequence for ELPG
entry and exit.

 1. At ELPG entry, invokes tegra_xusb_padctl_enable_phy_sleepwalk()
    and tegra_xusb_padctl_enable_phy_wake() to configure XUSB PADCTL
    sleepwalk and wake detection circuits to maintain USB lines level
    and respond to wake events (wake-on-connect, wake-on-disconnect,
    device-initiated-wake).

 2. At ELPG exit, invokes tegra_xusb_padctl_disable_phy_sleepwalk()
    and tegra_xusb_padctl_disable_phy_wake() to disarm sleepwalk and
    wake detection circuits.

At runtime suspend, XUSB host controller can enter ELPG to reduce
power consumption. When XUSB PADCTL wake detection circuit detects
a wake event, an interrupt will be raised. xhci-tegra driver then
will invoke pm_runtime_resume() for xhci-tegra.

Runtime resume could also be triggered by protocol drivers, this is
the host-initiated-wake event. At runtime resume, xhci-tegra driver
brings XUSB host controller out of ELPG to handle the wake events.

The same ELPG enter/exit procedure will be performed for system
suspend/resume path so USB devices can remain connected across SC7.

Signed-off-by: JC Kuo <jckuo@nvidia.com>
---
 drivers/usb/host/xhci-tegra.c | 391 +++++++++++++++++++++++++++++++---
 1 file changed, 361 insertions(+), 30 deletions(-)

Comments

Thierry Reding Aug. 31, 2020, 12:50 p.m. UTC | #1
On Mon, Aug 31, 2020 at 12:40:43PM +0800, JC Kuo wrote:
> This commit implements the complete programming sequence for ELPG
> entry and exit.
> 
>  1. At ELPG entry, invokes tegra_xusb_padctl_enable_phy_sleepwalk()
>     and tegra_xusb_padctl_enable_phy_wake() to configure XUSB PADCTL
>     sleepwalk and wake detection circuits to maintain USB lines level
>     and respond to wake events (wake-on-connect, wake-on-disconnect,
>     device-initiated-wake).
> 
>  2. At ELPG exit, invokes tegra_xusb_padctl_disable_phy_sleepwalk()
>     and tegra_xusb_padctl_disable_phy_wake() to disarm sleepwalk and
>     wake detection circuits.
> 
> At runtime suspend, XUSB host controller can enter ELPG to reduce
> power consumption. When XUSB PADCTL wake detection circuit detects
> a wake event, an interrupt will be raised. xhci-tegra driver then
> will invoke pm_runtime_resume() for xhci-tegra.
> 
> Runtime resume could also be triggered by protocol drivers, this is
> the host-initiated-wake event. At runtime resume, xhci-tegra driver
> brings XUSB host controller out of ELPG to handle the wake events.
> 
> The same ELPG enter/exit procedure will be performed for system
> suspend/resume path so USB devices can remain connected across SC7.
> 
> Signed-off-by: JC Kuo <jckuo@nvidia.com>
> ---
>  drivers/usb/host/xhci-tegra.c | 391 +++++++++++++++++++++++++++++++---
>  1 file changed, 361 insertions(+), 30 deletions(-)
> 
> diff --git a/drivers/usb/host/xhci-tegra.c b/drivers/usb/host/xhci-tegra.c
> index ce6526c2caf6..9530cfc83f45 100644
> --- a/drivers/usb/host/xhci-tegra.c
> +++ b/drivers/usb/host/xhci-tegra.c
> @@ -15,9 +15,11 @@
>  #include <linux/kernel.h>
>  #include <linux/module.h>
>  #include <linux/of_device.h>
> +#include <linux/of_irq.h>
>  #include <linux/phy/phy.h>
>  #include <linux/phy/tegra/xusb.h>
>  #include <linux/platform_device.h>
> +#include <linux/usb/ch9.h>
>  #include <linux/pm.h>
>  #include <linux/pm_domain.h>
>  #include <linux/pm_runtime.h>
> @@ -224,6 +226,7 @@ struct tegra_xusb {
>  
>  	int xhci_irq;
>  	int mbox_irq;
> +	int padctl_irq;
>  
>  	void __iomem *ipfs_base;
>  	void __iomem *fpci_base;
> @@ -268,10 +271,13 @@ struct tegra_xusb {
>  		dma_addr_t phys;
>  	} fw;
>  
> +	bool suspended;
>  	struct tegra_xusb_context context;
>  };
>  
>  static struct hc_driver __read_mostly tegra_xhci_hc_driver;
> +static int tegra_xusb_exit_elpg(struct tegra_xusb *tegra, bool runtime);
> +static int tegra_xusb_enter_elpg(struct tegra_xusb *tegra, bool runtime);
>  
>  static inline u32 fpci_readl(struct tegra_xusb *tegra, unsigned int offset)
>  {
> @@ -657,6 +663,9 @@ static irqreturn_t tegra_xusb_mbox_thread(int irq, void *data)
>  
>  	mutex_lock(&tegra->lock);
>  
> +	if (pm_runtime_suspended(tegra->dev) || tegra->suspended)
> +		goto out;
> +
>  	value = fpci_readl(tegra, tegra->soc->mbox.data_out);
>  	tegra_xusb_mbox_unpack(&msg, value);
>  
> @@ -670,6 +679,7 @@ static irqreturn_t tegra_xusb_mbox_thread(int irq, void *data)
>  
>  	tegra_xusb_mbox_handle(tegra, &msg);
>  
> +out:
>  	mutex_unlock(&tegra->lock);
>  	return IRQ_HANDLED;
>  }
> @@ -812,12 +822,27 @@ static void tegra_xusb_phy_disable(struct tegra_xusb *tegra)
>  
>  static int tegra_xusb_runtime_suspend(struct device *dev)
>  {
> -	return 0;
> +	struct tegra_xusb *tegra = dev_get_drvdata(dev);
> +	int ret;
> +
> +	synchronize_irq(tegra->mbox_irq);
> +	mutex_lock(&tegra->lock);
> +	ret = tegra_xusb_enter_elpg(tegra, true);
> +	mutex_unlock(&tegra->lock);
> +
> +	return ret;
>  }
>  
>  static int tegra_xusb_runtime_resume(struct device *dev)
>  {
> -	return 0;
> +	struct tegra_xusb *tegra = dev_get_drvdata(dev);
> +	int err;
> +
> +	mutex_lock(&tegra->lock);
> +	err = tegra_xusb_exit_elpg(tegra, true);
> +	mutex_unlock(&tegra->lock);
> +
> +	return err;
>  }
>  
>  #ifdef CONFIG_PM_SLEEP
> @@ -1121,6 +1146,22 @@ static int __tegra_xusb_enable_firmware_messages(struct tegra_xusb *tegra)
>  	return err;
>  }
>  
> +static irqreturn_t tegra_xusb_padctl_irq(int irq, void *data)
> +{
> +	struct tegra_xusb *tegra = data;
> +
> +	mutex_lock(&tegra->lock);
> +	if (tegra->suspended) {
> +		mutex_unlock(&tegra->lock);
> +		return IRQ_HANDLED;
> +	}
> +	mutex_unlock(&tegra->lock);

Blank lines before and after a block can help make this less cluttered.

> +
> +	pm_runtime_resume(tegra->dev);
> +
> +	return IRQ_HANDLED;
> +}
> +
>  static int tegra_xusb_enable_firmware_messages(struct tegra_xusb *tegra)
>  {
>  	int err;
> @@ -1244,6 +1285,51 @@ static void tegra_xhci_id_work(struct work_struct *work)
>  	}
>  }
>  
> +static bool is_usb2_otg_phy(struct tegra_xusb *tegra, int index)

unsigned int index?

> +{
> +	return (tegra->usbphy[index] != NULL);
> +}
> +
> +static bool is_usb3_otg_phy(struct tegra_xusb *tegra, int index)

Here too.

> +{
> +	struct tegra_xusb_padctl *padctl = tegra->padctl;
> +	int i, port;

These can also be unsigned.

> +
> +	for (i = 0; i < tegra->num_usb_phys; i++) {
> +		if (is_usb2_otg_phy(tegra, i)) {
> +			port = tegra_xusb_padctl_get_usb3_companion(padctl,i);

Space after ",".

> +			if (index == port)
> +				return true;
> +		}
> +	}
> +
> +	return false;
> +}
> +
> +static bool is_host_mode_phy(struct tegra_xusb *tegra, int phy_type, int index)
> +{
> +	if (strcmp(tegra->soc->phy_types[phy_type].name, "hsic") == 0)
> +		return true;
> +
> +	if (strcmp(tegra->soc->phy_types[phy_type].name, "usb2") == 0) {
> +		if (is_usb2_otg_phy(tegra, index)) {
> +			return ((index == tegra->otg_usb2_port) &&
> +				 tegra->host_mode);
> +		} else
> +			return true;
> +	}
> +
> +	if (strcmp(tegra->soc->phy_types[phy_type].name, "usb3") == 0) {
> +		if (is_usb3_otg_phy(tegra, index)) {
> +			return ((index == tegra->otg_usb3_port) &&
> +				 tegra->host_mode);
> +		} else
> +			return true;
> +	}
> +
> +	return false;
> +}
> +
>  static int tegra_xusb_get_usb2_port(struct tegra_xusb *tegra,
>  					      struct usb_phy *usbphy)
>  {
> @@ -1336,6 +1422,7 @@ static void tegra_xusb_deinit_usb_phy(struct tegra_xusb *tegra)
>  static int tegra_xusb_probe(struct platform_device *pdev)
>  {
>  	struct tegra_xusb *tegra;
> +	struct device_node *np;
>  	struct resource *regs;
>  	struct xhci_hcd *xhci;
>  	unsigned int i, j, k;
> @@ -1383,6 +1470,14 @@ static int tegra_xusb_probe(struct platform_device *pdev)
>  	if (IS_ERR(tegra->padctl))
>  		return PTR_ERR(tegra->padctl);
>  
> +	np = of_parse_phandle(pdev->dev.of_node, "nvidia,xusb-padctl", 0);
> +	if (!np)
> +		return -ENODEV;
> +
> +	tegra->padctl_irq = of_irq_get(np, 0);
> +	if (tegra->padctl_irq < 0)
> +		return tegra->padctl_irq;
> +
>  	tegra->host_clk = devm_clk_get(&pdev->dev, "xusb_host");
>  	if (IS_ERR(tegra->host_clk)) {
>  		err = PTR_ERR(tegra->host_clk);
> @@ -1527,6 +1622,7 @@ static int tegra_xusb_probe(struct platform_device *pdev)
>  		goto put_powerdomains;
>  	}
>  
> +	tegra->hcd->skip_phy_initialization = 1;
>  	tegra->hcd->regs = tegra->regs;
>  	tegra->hcd->rsrc_start = regs->start;
>  	tegra->hcd->rsrc_len = resource_size(regs);
> @@ -1609,12 +1705,6 @@ static int tegra_xusb_probe(struct platform_device *pdev)
>  		goto put_usb3;
>  	}
>  
> -	err = tegra_xusb_enable_firmware_messages(tegra);
> -	if (err < 0) {
> -		dev_err(&pdev->dev, "failed to enable messages: %d\n", err);
> -		goto remove_usb3;
> -	}
> -
>  	err = devm_request_threaded_irq(&pdev->dev, tegra->mbox_irq,
>  					tegra_xusb_mbox_irq,
>  					tegra_xusb_mbox_thread, 0,
> @@ -1624,12 +1714,40 @@ static int tegra_xusb_probe(struct platform_device *pdev)
>  		goto remove_usb3;
>  	}
>  
> +	err = devm_request_threaded_irq(&pdev->dev, tegra->padctl_irq,
> +		NULL,
> +		tegra_xusb_padctl_irq,
> +		IRQF_ONESHOT |
> +		IRQF_TRIGGER_HIGH,
> +		dev_name(&pdev->dev), tegra);

The alignment here is off. Also, try to make good use of those 100
columns.

> +	if (err < 0) {
> +		dev_err(&pdev->dev, "failed to request padctl IRQ: %d\n", err);
> +		goto remove_usb3;
> +	}
> +
> +	err = tegra_xusb_enable_firmware_messages(tegra);
> +	if (err < 0) {
> +		dev_err(&pdev->dev, "failed to enable messages: %d\n", err);
> +		goto remove_usb3;
> +	}
> +
>  	err = tegra_xusb_init_usb_phy(tegra);
>  	if (err < 0) {
>  		dev_err(&pdev->dev, "failed to init USB PHY: %d\n", err);
>  		goto remove_usb3;
>  	}
>  
> +	/* Enable wake for both USB 2.0 and USB 3.0 roothubs */
> +	device_init_wakeup(&tegra->hcd->self.root_hub->dev, true);
> +	device_init_wakeup(&xhci->shared_hcd->self.root_hub->dev, true);
> +	device_init_wakeup(tegra->dev, true);
> +
> +	pm_runtime_use_autosuspend(tegra->dev);
> +	pm_runtime_set_autosuspend_delay(tegra->dev, 2000);
> +	pm_runtime_mark_last_busy(tegra->dev);
> +	pm_runtime_set_active(tegra->dev);
> +	pm_runtime_enable(tegra->dev);
> +
>  	return 0;
>  
>  remove_usb3:
> @@ -1665,6 +1783,7 @@ static int tegra_xusb_remove(struct platform_device *pdev)
>  
>  	tegra_xusb_deinit_usb_phy(tegra);
>  
> +	pm_runtime_get_sync(&pdev->dev);
>  	usb_remove_hcd(xhci->shared_hcd);
>  	usb_put_hcd(xhci->shared_hcd);
>  	xhci->shared_hcd = NULL;
> @@ -1674,8 +1793,8 @@ static int tegra_xusb_remove(struct platform_device *pdev)
>  	dma_free_coherent(&pdev->dev, tegra->fw.size, tegra->fw.virt,
>  			  tegra->fw.phys);
>  
> -	pm_runtime_put_sync(&pdev->dev);
>  	pm_runtime_disable(&pdev->dev);
> +	pm_runtime_put(&pdev->dev);
>  
>  	tegra_xusb_powergate_partitions(tegra);
>  
> @@ -1717,9 +1836,17 @@ static bool xhci_hub_ports_suspended(struct xhci_hub *hub)
>  static int tegra_xusb_check_ports(struct tegra_xusb *tegra)
>  {
>  	struct xhci_hcd *xhci = hcd_to_xhci(tegra->hcd);
> +	struct xhci_hub *rhub =  xhci_get_rhub(xhci->main_hcd);
> +	struct xhci_bus_state *bus_state = &rhub->bus_state;
>  	unsigned long flags;
>  	int err = 0;
>  
> +	if (bus_state->bus_suspended) {
> +		/* xusb_hub_suspend() has just directed one or more USB2 port(s)
> +		 * to U3 state, it takes 3ms to enter U3. */
> +		usleep_range(3000, 4000);
> +	}
> +
>  	spin_lock_irqsave(&xhci->lock, flags);
>  
>  	if (!xhci_hub_ports_suspended(&xhci->usb2_rhub) ||
> @@ -1765,45 +1892,184 @@ static void tegra_xusb_restore_context(struct tegra_xusb *tegra)
>  	}
>  }
>  
> -static int tegra_xusb_enter_elpg(struct tegra_xusb *tegra, bool wakeup)
> +static enum usb_device_speed
> +tegra_xhci_portsc_to_speed(struct tegra_xusb *tegra, u32 portsc)
> +{
> +	if (DEV_LOWSPEED(portsc))
> +		return USB_SPEED_LOW;
> +	else if (DEV_HIGHSPEED(portsc))
> +		return USB_SPEED_HIGH;
> +	else if (DEV_FULLSPEED(portsc))
> +		return USB_SPEED_FULL;
> +	else if (DEV_SUPERSPEED_ANY(portsc))
> +		return USB_SPEED_SUPER;
> +	else
> +		return USB_SPEED_UNKNOWN;
> +}

As in a prior patch you can make this simpler by dropping the elses.

> +
> +static void tegra_xhci_enable_phy_sleepwalk_wake(struct tegra_xusb *tegra)
> +{
> +	struct tegra_xusb_padctl *padctl = tegra->padctl;
> +	struct xhci_hcd *xhci = hcd_to_xhci(tegra->hcd);
> +	enum usb_device_speed speed;
> +	struct phy *phy;
> +	int index, offset;
> +	int i, j, k;

It looks like these can all be unsigned int.

> +	struct xhci_hub *rhub;
> +	u32 portsc;
> +
> +	for (i = 0, k = 0; i < tegra->soc->num_types; i++) {
> +		if (strcmp(tegra->soc->phy_types[i].name, "usb3") == 0)
> +			rhub = &xhci->usb3_rhub;
> +		else
> +			rhub = &xhci->usb2_rhub;
> +
> +		if (strcmp(tegra->soc->phy_types[i].name, "hsic") == 0)
> +			offset = tegra->soc->ports.usb2.count;
> +		else
> +			offset = 0;
> +
> +		for (j = 0; j < tegra->soc->phy_types[i].num; j++) {
> +			phy = tegra->phys[k++];
> +
> +			if (!phy)
> +				continue;
> +
> +			index = j + offset;
> +
> +			if (index >= rhub->num_ports)
> +				continue;
> +
> +			if (!is_host_mode_phy(tegra, i, j))
> +				continue;
> +
> +			portsc = readl(rhub->ports[index]->addr);
> +			speed = tegra_xhci_portsc_to_speed(tegra, portsc);
> +			tegra_xusb_padctl_enable_phy_sleepwalk(padctl, phy,
> +							       speed);
> +			tegra_xusb_padctl_enable_phy_wake(padctl, phy);
> +		}
> +	}
> +}
> +
> +static void tegra_xhci_disable_phy_wake(struct tegra_xusb *tegra)
> +{
> +	struct tegra_xusb_padctl *padctl = tegra->padctl;
> +	int i;

Same here.

> +
> +	for (i = 0; i < tegra->num_phys; i++) {
> +		if (!tegra->phys[i])
> +			continue;
> +
> +		tegra_xusb_padctl_disable_phy_wake(padctl, tegra->phys[i]);
> +	}
> +}
> +
> +static void tegra_xhci_disable_phy_sleepwalk(struct tegra_xusb *tegra)
> +{
> +	struct tegra_xusb_padctl *padctl = tegra->padctl;
> +	int i;

And here.

> +
> +	for (i = 0; i < tegra->num_phys; i++) {
> +		if (!tegra->phys[i])
> +			continue;
> +
> +		tegra_xusb_padctl_disable_phy_sleepwalk(padctl, tegra->phys[i]);
> +	}
> +}
> +
> +static int tegra_xusb_enter_elpg(struct tegra_xusb *tegra, bool runtime)
>  {
>  	struct xhci_hcd *xhci = hcd_to_xhci(tegra->hcd);
> +	struct device *dev = tegra->dev;
> +	bool wakeup = runtime ? true : device_may_wakeup(dev);
> +	unsigned int i;
>  	int err;
> +	u32 usbcmd;
> +
> +	dev_dbg(dev, "entering ELPG\n");
> +
> +	usbcmd = readl(&xhci->op_regs->command);
> +	usbcmd &= ~CMD_EIE;
> +	writel(usbcmd, &xhci->op_regs->command);
>  
>  	err = tegra_xusb_check_ports(tegra);
>  	if (err < 0) {
>  		dev_err(tegra->dev, "not all ports suspended: %d\n", err);
> -		return err;
> +		goto out;
>  	}
>  
>  	err = xhci_suspend(xhci, wakeup);
>  	if (err < 0) {
>  		dev_err(tegra->dev, "failed to suspend XHCI: %d\n", err);
> -		return err;
> +		goto out;
>  	}
>  
>  	tegra_xusb_save_context(tegra);
> -	tegra_xusb_phy_disable(tegra);
> +
> +	if (wakeup)
> +		tegra_xhci_enable_phy_sleepwalk_wake(tegra);
> +
> +	tegra_xusb_powergate_partitions(tegra);
> +
> +	for (i = 0; i < tegra->num_phys; i++) {
> +		if (!tegra->phys[i])
> +			continue;
> +
> +		phy_power_off(tegra->phys[i]);
> +		if (!wakeup)
> +			phy_exit(tegra->phys[i]);
> +	}
>  	tegra_xusb_clk_disable(tegra);

Use blank lines to separate blocks of code.

>  
> -	return 0;
> +out:
> +	if (!err)
> +		dev_dbg(tegra->dev, "entering ELPG done\n");
> +	else {
> +		usbcmd = readl(&xhci->op_regs->command);
> +		usbcmd |= CMD_EIE;
> +		writel(usbcmd, &xhci->op_regs->command);
> +
> +		dev_dbg(tegra->dev, "entering ELPG failed\n");
> +		pm_runtime_mark_last_busy(tegra->dev);
> +	}
> +
> +	return err;
>  }
>  
> -static int tegra_xusb_exit_elpg(struct tegra_xusb *tegra, bool wakeup)
> +static int tegra_xusb_exit_elpg(struct tegra_xusb *tegra, bool runtime)
>  {
>  	struct xhci_hcd *xhci = hcd_to_xhci(tegra->hcd);
> +	struct device *dev = tegra->dev;
> +	bool wakeup = runtime ? true : device_may_wakeup(dev);
> +	unsigned int i;
> +	u32 usbcmd;
>  	int err;
>  
> +	dev_dbg(dev, "exiting ELPG\n");
> +	pm_runtime_mark_last_busy(tegra->dev);
> +
>  	err = tegra_xusb_clk_enable(tegra);
>  	if (err < 0) {
>  		dev_err(tegra->dev, "failed to enable clocks: %d\n", err);
> -		return err;
> +		goto out;
>  	}
>  
> -	err = tegra_xusb_phy_enable(tegra);
> -	if (err < 0) {
> -		dev_err(tegra->dev, "failed to enable PHYs: %d\n", err);
> -		goto disable_clk;
> +	err = tegra_xusb_unpowergate_partitions(tegra);
> +	if (err)
> +		goto disable_clks;
> +
> +	if (wakeup)
> +		tegra_xhci_disable_phy_wake(tegra);
> +
> +	for (i = 0; i < tegra->num_phys; i++) {
> +		if (!tegra->phys[i])
> +			continue;
> +
> +		if (!wakeup)
> +			phy_init(tegra->phys[i]);
> +
> +		phy_power_on(tegra->phys[i]);
>  	}
>  
>  	tegra_xusb_config(tegra);
> @@ -1821,31 +2087,78 @@ static int tegra_xusb_exit_elpg(struct tegra_xusb *tegra, bool wakeup)
>  		goto disable_phy;
>  	}
>  
> -	err = xhci_resume(xhci, true);
> +	if (wakeup)
> +		tegra_xhci_disable_phy_sleepwalk(tegra);
> +
> +	err = xhci_resume(xhci, 0);
>  	if (err < 0) {
>  		dev_err(tegra->dev, "failed to resume XHCI: %d\n", err);
>  		goto disable_phy;
>  	}
>  
> -	return 0;
> +	usbcmd = readl(&xhci->op_regs->command);
> +	usbcmd |= CMD_EIE;
> +	writel(usbcmd, &xhci->op_regs->command);
> +
> +	goto out;
>  
>  disable_phy:
> -	tegra_xusb_phy_disable(tegra);
> -disable_clk:
> +	for (i = 0; i < tegra->num_phys; i++) {
> +		if (!tegra->phys[i])
> +			continue;
> +
> +		phy_power_off(tegra->phys[i]);
> +		if (!wakeup)
> +			phy_exit(tegra->phys[i]);
> +	}
> +	tegra_xusb_powergate_partitions(tegra);
> +disable_clks:
>  	tegra_xusb_clk_disable(tegra);
> +out:
> +	if (!err)
> +		dev_dbg(dev, "exiting ELPG done\n");
> +	else
> +		dev_dbg(dev, "exiting ELPG failed\n");
> +
>  	return err;
>  }
>  
>  static int tegra_xusb_suspend(struct device *dev)
>  {
>  	struct tegra_xusb *tegra = dev_get_drvdata(dev);
> -	bool wakeup = device_may_wakeup(dev);
>  	int err;
>  
>  	synchronize_irq(tegra->mbox_irq);
> -

I think that blank line actually helped with readability.

Thierry

>  	mutex_lock(&tegra->lock);
> -	err = tegra_xusb_enter_elpg(tegra, wakeup);
> +
> +	if (pm_runtime_suspended(dev)) {
> +		err = tegra_xusb_exit_elpg(tegra, true);
> +		if (err < 0)
> +			goto out;
> +	}
> +
> +	err = tegra_xusb_enter_elpg(tegra, false);
> +	if (err < 0) {
> +		if (pm_runtime_suspended(dev)) {
> +			pm_runtime_disable(dev);
> +			pm_runtime_set_active(dev);
> +			pm_runtime_enable(dev);
> +		}
> +
> +		goto out;
> +	}
> +
> +out:
> +	if (!err) {
> +		tegra->suspended = true;
> +		pm_runtime_disable(dev);
> +
> +		if (device_may_wakeup(dev)) {
> +			if (enable_irq_wake(tegra->padctl_irq))
> +				dev_err(dev, "failed to enable padctl wakes\n");
> +		}
> +	}
> +
>  	mutex_unlock(&tegra->lock);
>  
>  	return err;
> @@ -1854,14 +2167,32 @@ static int tegra_xusb_suspend(struct device *dev)
>  static int tegra_xusb_resume(struct device *dev)
>  {
>  	struct tegra_xusb *tegra = dev_get_drvdata(dev);
> -	bool wakeup = device_may_wakeup(dev);
>  	int err;
>  
>  	mutex_lock(&tegra->lock);
> -	err = tegra_xusb_exit_elpg(tegra, wakeup);
> +
> +	if (!tegra->suspended) {
> +		mutex_unlock(&tegra->lock);
> +		return 0;
> +	}
> +
> +	err = tegra_xusb_exit_elpg(tegra, false);
> +	if (err < 0) {
> +		mutex_unlock(&tegra->lock);
> +		return err;
> +	}
> +
> +	if (device_may_wakeup(dev)) {
> +		if (disable_irq_wake(tegra->padctl_irq))
> +			dev_err(dev, "failed to disable padctl wakes\n");
> +	}
> +	tegra->suspended = false;
>  	mutex_unlock(&tegra->lock);
>  
> -	return err;
> +	pm_runtime_set_active(dev);
> +	pm_runtime_enable(dev);
> +
> +	return 0;
>  }
>  #endif
>  
> -- 
> 2.25.1
>
Dmitry Osipenko Sept. 1, 2020, 8:33 p.m. UTC | #2
31.08.2020 07:40, JC Kuo пишет:
> +	err = devm_request_threaded_irq(&pdev->dev, tegra->padctl_irq,
> +		NULL,
> +		tegra_xusb_padctl_irq,
> +		IRQF_ONESHOT |

> +		IRQF_TRIGGER_HIGH,

Specifying trigger levels is meaningless for interrupts coming from a
device-tree because DT levels always take precedence.
JC Kuo Sept. 8, 2020, 2:29 a.m. UTC | #3
Hi Thierry,
Thanks for review. I will amend accordingly and submit a new revision.

JC

On 8/31/20 8:50 PM, Thierry Reding wrote:
> On Mon, Aug 31, 2020 at 12:40:43PM +0800, JC Kuo wrote:
>> This commit implements the complete programming sequence for ELPG
>> entry and exit.
>>
>>  1. At ELPG entry, invokes tegra_xusb_padctl_enable_phy_sleepwalk()
>>     and tegra_xusb_padctl_enable_phy_wake() to configure XUSB PADCTL
>>     sleepwalk and wake detection circuits to maintain USB lines level
>>     and respond to wake events (wake-on-connect, wake-on-disconnect,
>>     device-initiated-wake).
>>
>>  2. At ELPG exit, invokes tegra_xusb_padctl_disable_phy_sleepwalk()
>>     and tegra_xusb_padctl_disable_phy_wake() to disarm sleepwalk and
>>     wake detection circuits.
>>
>> At runtime suspend, XUSB host controller can enter ELPG to reduce
>> power consumption. When XUSB PADCTL wake detection circuit detects
>> a wake event, an interrupt will be raised. xhci-tegra driver then
>> will invoke pm_runtime_resume() for xhci-tegra.
>>
>> Runtime resume could also be triggered by protocol drivers, this is
>> the host-initiated-wake event. At runtime resume, xhci-tegra driver
>> brings XUSB host controller out of ELPG to handle the wake events.
>>
>> The same ELPG enter/exit procedure will be performed for system
>> suspend/resume path so USB devices can remain connected across SC7.
>>
>> Signed-off-by: JC Kuo <jckuo@nvidia.com>
>> ---
>>  drivers/usb/host/xhci-tegra.c | 391 +++++++++++++++++++++++++++++++---
>>  1 file changed, 361 insertions(+), 30 deletions(-)
>>
>> diff --git a/drivers/usb/host/xhci-tegra.c b/drivers/usb/host/xhci-tegra.c
>> index ce6526c2caf6..9530cfc83f45 100644
>> --- a/drivers/usb/host/xhci-tegra.c
>> +++ b/drivers/usb/host/xhci-tegra.c
>> @@ -15,9 +15,11 @@
>>  #include <linux/kernel.h>
>>  #include <linux/module.h>
>>  #include <linux/of_device.h>
>> +#include <linux/of_irq.h>
>>  #include <linux/phy/phy.h>
>>  #include <linux/phy/tegra/xusb.h>
>>  #include <linux/platform_device.h>
>> +#include <linux/usb/ch9.h>
>>  #include <linux/pm.h>
>>  #include <linux/pm_domain.h>
>>  #include <linux/pm_runtime.h>
>> @@ -224,6 +226,7 @@ struct tegra_xusb {
>>  
>>  	int xhci_irq;
>>  	int mbox_irq;
>> +	int padctl_irq;
>>  
>>  	void __iomem *ipfs_base;
>>  	void __iomem *fpci_base;
>> @@ -268,10 +271,13 @@ struct tegra_xusb {
>>  		dma_addr_t phys;
>>  	} fw;
>>  
>> +	bool suspended;
>>  	struct tegra_xusb_context context;
>>  };
>>  
>>  static struct hc_driver __read_mostly tegra_xhci_hc_driver;
>> +static int tegra_xusb_exit_elpg(struct tegra_xusb *tegra, bool runtime);
>> +static int tegra_xusb_enter_elpg(struct tegra_xusb *tegra, bool runtime);
>>  
>>  static inline u32 fpci_readl(struct tegra_xusb *tegra, unsigned int offset)
>>  {
>> @@ -657,6 +663,9 @@ static irqreturn_t tegra_xusb_mbox_thread(int irq, void *data)
>>  
>>  	mutex_lock(&tegra->lock);
>>  
>> +	if (pm_runtime_suspended(tegra->dev) || tegra->suspended)
>> +		goto out;
>> +
>>  	value = fpci_readl(tegra, tegra->soc->mbox.data_out);
>>  	tegra_xusb_mbox_unpack(&msg, value);
>>  
>> @@ -670,6 +679,7 @@ static irqreturn_t tegra_xusb_mbox_thread(int irq, void *data)
>>  
>>  	tegra_xusb_mbox_handle(tegra, &msg);
>>  
>> +out:
>>  	mutex_unlock(&tegra->lock);
>>  	return IRQ_HANDLED;
>>  }
>> @@ -812,12 +822,27 @@ static void tegra_xusb_phy_disable(struct tegra_xusb *tegra)
>>  
>>  static int tegra_xusb_runtime_suspend(struct device *dev)
>>  {
>> -	return 0;
>> +	struct tegra_xusb *tegra = dev_get_drvdata(dev);
>> +	int ret;
>> +
>> +	synchronize_irq(tegra->mbox_irq);
>> +	mutex_lock(&tegra->lock);
>> +	ret = tegra_xusb_enter_elpg(tegra, true);
>> +	mutex_unlock(&tegra->lock);
>> +
>> +	return ret;
>>  }
>>  
>>  static int tegra_xusb_runtime_resume(struct device *dev)
>>  {
>> -	return 0;
>> +	struct tegra_xusb *tegra = dev_get_drvdata(dev);
>> +	int err;
>> +
>> +	mutex_lock(&tegra->lock);
>> +	err = tegra_xusb_exit_elpg(tegra, true);
>> +	mutex_unlock(&tegra->lock);
>> +
>> +	return err;
>>  }
>>  
>>  #ifdef CONFIG_PM_SLEEP
>> @@ -1121,6 +1146,22 @@ static int __tegra_xusb_enable_firmware_messages(struct tegra_xusb *tegra)
>>  	return err;
>>  }
>>  
>> +static irqreturn_t tegra_xusb_padctl_irq(int irq, void *data)
>> +{
>> +	struct tegra_xusb *tegra = data;
>> +
>> +	mutex_lock(&tegra->lock);
>> +	if (tegra->suspended) {
>> +		mutex_unlock(&tegra->lock);
>> +		return IRQ_HANDLED;
>> +	}
>> +	mutex_unlock(&tegra->lock);
> 
> Blank lines before and after a block can help make this less cluttered.
> 
>> +
>> +	pm_runtime_resume(tegra->dev);
>> +
>> +	return IRQ_HANDLED;
>> +}
>> +
>>  static int tegra_xusb_enable_firmware_messages(struct tegra_xusb *tegra)
>>  {
>>  	int err;
>> @@ -1244,6 +1285,51 @@ static void tegra_xhci_id_work(struct work_struct *work)
>>  	}
>>  }
>>  
>> +static bool is_usb2_otg_phy(struct tegra_xusb *tegra, int index)
> 
> unsigned int index?
> 
>> +{
>> +	return (tegra->usbphy[index] != NULL);
>> +}
>> +
>> +static bool is_usb3_otg_phy(struct tegra_xusb *tegra, int index)
> 
> Here too.
> 
>> +{
>> +	struct tegra_xusb_padctl *padctl = tegra->padctl;
>> +	int i, port;
> 
> These can also be unsigned.
> 
>> +
>> +	for (i = 0; i < tegra->num_usb_phys; i++) {
>> +		if (is_usb2_otg_phy(tegra, i)) {
>> +			port = tegra_xusb_padctl_get_usb3_companion(padctl,i);
> 
> Space after ",".
> 
>> +			if (index == port)
>> +				return true;
>> +		}
>> +	}
>> +
>> +	return false;
>> +}
>> +
>> +static bool is_host_mode_phy(struct tegra_xusb *tegra, int phy_type, int index)
>> +{
>> +	if (strcmp(tegra->soc->phy_types[phy_type].name, "hsic") == 0)
>> +		return true;
>> +
>> +	if (strcmp(tegra->soc->phy_types[phy_type].name, "usb2") == 0) {
>> +		if (is_usb2_otg_phy(tegra, index)) {
>> +			return ((index == tegra->otg_usb2_port) &&
>> +				 tegra->host_mode);
>> +		} else
>> +			return true;
>> +	}
>> +
>> +	if (strcmp(tegra->soc->phy_types[phy_type].name, "usb3") == 0) {
>> +		if (is_usb3_otg_phy(tegra, index)) {
>> +			return ((index == tegra->otg_usb3_port) &&
>> +				 tegra->host_mode);
>> +		} else
>> +			return true;
>> +	}
>> +
>> +	return false;
>> +}
>> +
>>  static int tegra_xusb_get_usb2_port(struct tegra_xusb *tegra,
>>  					      struct usb_phy *usbphy)
>>  {
>> @@ -1336,6 +1422,7 @@ static void tegra_xusb_deinit_usb_phy(struct tegra_xusb *tegra)
>>  static int tegra_xusb_probe(struct platform_device *pdev)
>>  {
>>  	struct tegra_xusb *tegra;
>> +	struct device_node *np;
>>  	struct resource *regs;
>>  	struct xhci_hcd *xhci;
>>  	unsigned int i, j, k;
>> @@ -1383,6 +1470,14 @@ static int tegra_xusb_probe(struct platform_device *pdev)
>>  	if (IS_ERR(tegra->padctl))
>>  		return PTR_ERR(tegra->padctl);
>>  
>> +	np = of_parse_phandle(pdev->dev.of_node, "nvidia,xusb-padctl", 0);
>> +	if (!np)
>> +		return -ENODEV;
>> +
>> +	tegra->padctl_irq = of_irq_get(np, 0);
>> +	if (tegra->padctl_irq < 0)
>> +		return tegra->padctl_irq;
>> +
>>  	tegra->host_clk = devm_clk_get(&pdev->dev, "xusb_host");
>>  	if (IS_ERR(tegra->host_clk)) {
>>  		err = PTR_ERR(tegra->host_clk);
>> @@ -1527,6 +1622,7 @@ static int tegra_xusb_probe(struct platform_device *pdev)
>>  		goto put_powerdomains;
>>  	}
>>  
>> +	tegra->hcd->skip_phy_initialization = 1;
>>  	tegra->hcd->regs = tegra->regs;
>>  	tegra->hcd->rsrc_start = regs->start;
>>  	tegra->hcd->rsrc_len = resource_size(regs);
>> @@ -1609,12 +1705,6 @@ static int tegra_xusb_probe(struct platform_device *pdev)
>>  		goto put_usb3;
>>  	}
>>  
>> -	err = tegra_xusb_enable_firmware_messages(tegra);
>> -	if (err < 0) {
>> -		dev_err(&pdev->dev, "failed to enable messages: %d\n", err);
>> -		goto remove_usb3;
>> -	}
>> -
>>  	err = devm_request_threaded_irq(&pdev->dev, tegra->mbox_irq,
>>  					tegra_xusb_mbox_irq,
>>  					tegra_xusb_mbox_thread, 0,
>> @@ -1624,12 +1714,40 @@ static int tegra_xusb_probe(struct platform_device *pdev)
>>  		goto remove_usb3;
>>  	}
>>  
>> +	err = devm_request_threaded_irq(&pdev->dev, tegra->padctl_irq,
>> +		NULL,
>> +		tegra_xusb_padctl_irq,
>> +		IRQF_ONESHOT |
>> +		IRQF_TRIGGER_HIGH,
>> +		dev_name(&pdev->dev), tegra);
> 
> The alignment here is off. Also, try to make good use of those 100
> columns.
> 
>> +	if (err < 0) {
>> +		dev_err(&pdev->dev, "failed to request padctl IRQ: %d\n", err);
>> +		goto remove_usb3;
>> +	}
>> +
>> +	err = tegra_xusb_enable_firmware_messages(tegra);
>> +	if (err < 0) {
>> +		dev_err(&pdev->dev, "failed to enable messages: %d\n", err);
>> +		goto remove_usb3;
>> +	}
>> +
>>  	err = tegra_xusb_init_usb_phy(tegra);
>>  	if (err < 0) {
>>  		dev_err(&pdev->dev, "failed to init USB PHY: %d\n", err);
>>  		goto remove_usb3;
>>  	}
>>  
>> +	/* Enable wake for both USB 2.0 and USB 3.0 roothubs */
>> +	device_init_wakeup(&tegra->hcd->self.root_hub->dev, true);
>> +	device_init_wakeup(&xhci->shared_hcd->self.root_hub->dev, true);
>> +	device_init_wakeup(tegra->dev, true);
>> +
>> +	pm_runtime_use_autosuspend(tegra->dev);
>> +	pm_runtime_set_autosuspend_delay(tegra->dev, 2000);
>> +	pm_runtime_mark_last_busy(tegra->dev);
>> +	pm_runtime_set_active(tegra->dev);
>> +	pm_runtime_enable(tegra->dev);
>> +
>>  	return 0;
>>  
>>  remove_usb3:
>> @@ -1665,6 +1783,7 @@ static int tegra_xusb_remove(struct platform_device *pdev)
>>  
>>  	tegra_xusb_deinit_usb_phy(tegra);
>>  
>> +	pm_runtime_get_sync(&pdev->dev);
>>  	usb_remove_hcd(xhci->shared_hcd);
>>  	usb_put_hcd(xhci->shared_hcd);
>>  	xhci->shared_hcd = NULL;
>> @@ -1674,8 +1793,8 @@ static int tegra_xusb_remove(struct platform_device *pdev)
>>  	dma_free_coherent(&pdev->dev, tegra->fw.size, tegra->fw.virt,
>>  			  tegra->fw.phys);
>>  
>> -	pm_runtime_put_sync(&pdev->dev);
>>  	pm_runtime_disable(&pdev->dev);
>> +	pm_runtime_put(&pdev->dev);
>>  
>>  	tegra_xusb_powergate_partitions(tegra);
>>  
>> @@ -1717,9 +1836,17 @@ static bool xhci_hub_ports_suspended(struct xhci_hub *hub)
>>  static int tegra_xusb_check_ports(struct tegra_xusb *tegra)
>>  {
>>  	struct xhci_hcd *xhci = hcd_to_xhci(tegra->hcd);
>> +	struct xhci_hub *rhub =  xhci_get_rhub(xhci->main_hcd);
>> +	struct xhci_bus_state *bus_state = &rhub->bus_state;
>>  	unsigned long flags;
>>  	int err = 0;
>>  
>> +	if (bus_state->bus_suspended) {
>> +		/* xusb_hub_suspend() has just directed one or more USB2 port(s)
>> +		 * to U3 state, it takes 3ms to enter U3. */
>> +		usleep_range(3000, 4000);
>> +	}
>> +
>>  	spin_lock_irqsave(&xhci->lock, flags);
>>  
>>  	if (!xhci_hub_ports_suspended(&xhci->usb2_rhub) ||
>> @@ -1765,45 +1892,184 @@ static void tegra_xusb_restore_context(struct tegra_xusb *tegra)
>>  	}
>>  }
>>  
>> -static int tegra_xusb_enter_elpg(struct tegra_xusb *tegra, bool wakeup)
>> +static enum usb_device_speed
>> +tegra_xhci_portsc_to_speed(struct tegra_xusb *tegra, u32 portsc)
>> +{
>> +	if (DEV_LOWSPEED(portsc))
>> +		return USB_SPEED_LOW;
>> +	else if (DEV_HIGHSPEED(portsc))
>> +		return USB_SPEED_HIGH;
>> +	else if (DEV_FULLSPEED(portsc))
>> +		return USB_SPEED_FULL;
>> +	else if (DEV_SUPERSPEED_ANY(portsc))
>> +		return USB_SPEED_SUPER;
>> +	else
>> +		return USB_SPEED_UNKNOWN;
>> +}
> 
> As in a prior patch you can make this simpler by dropping the elses.
> 
>> +
>> +static void tegra_xhci_enable_phy_sleepwalk_wake(struct tegra_xusb *tegra)
>> +{
>> +	struct tegra_xusb_padctl *padctl = tegra->padctl;
>> +	struct xhci_hcd *xhci = hcd_to_xhci(tegra->hcd);
>> +	enum usb_device_speed speed;
>> +	struct phy *phy;
>> +	int index, offset;
>> +	int i, j, k;
> 
> It looks like these can all be unsigned int.
> 
>> +	struct xhci_hub *rhub;
>> +	u32 portsc;
>> +
>> +	for (i = 0, k = 0; i < tegra->soc->num_types; i++) {
>> +		if (strcmp(tegra->soc->phy_types[i].name, "usb3") == 0)
>> +			rhub = &xhci->usb3_rhub;
>> +		else
>> +			rhub = &xhci->usb2_rhub;
>> +
>> +		if (strcmp(tegra->soc->phy_types[i].name, "hsic") == 0)
>> +			offset = tegra->soc->ports.usb2.count;
>> +		else
>> +			offset = 0;
>> +
>> +		for (j = 0; j < tegra->soc->phy_types[i].num; j++) {
>> +			phy = tegra->phys[k++];
>> +
>> +			if (!phy)
>> +				continue;
>> +
>> +			index = j + offset;
>> +
>> +			if (index >= rhub->num_ports)
>> +				continue;
>> +
>> +			if (!is_host_mode_phy(tegra, i, j))
>> +				continue;
>> +
>> +			portsc = readl(rhub->ports[index]->addr);
>> +			speed = tegra_xhci_portsc_to_speed(tegra, portsc);
>> +			tegra_xusb_padctl_enable_phy_sleepwalk(padctl, phy,
>> +							       speed);
>> +			tegra_xusb_padctl_enable_phy_wake(padctl, phy);
>> +		}
>> +	}
>> +}
>> +
>> +static void tegra_xhci_disable_phy_wake(struct tegra_xusb *tegra)
>> +{
>> +	struct tegra_xusb_padctl *padctl = tegra->padctl;
>> +	int i;
> 
> Same here.
> 
>> +
>> +	for (i = 0; i < tegra->num_phys; i++) {
>> +		if (!tegra->phys[i])
>> +			continue;
>> +
>> +		tegra_xusb_padctl_disable_phy_wake(padctl, tegra->phys[i]);
>> +	}
>> +}
>> +
>> +static void tegra_xhci_disable_phy_sleepwalk(struct tegra_xusb *tegra)
>> +{
>> +	struct tegra_xusb_padctl *padctl = tegra->padctl;
>> +	int i;
> 
> And here.
> 
>> +
>> +	for (i = 0; i < tegra->num_phys; i++) {
>> +		if (!tegra->phys[i])
>> +			continue;
>> +
>> +		tegra_xusb_padctl_disable_phy_sleepwalk(padctl, tegra->phys[i]);
>> +	}
>> +}
>> +
>> +static int tegra_xusb_enter_elpg(struct tegra_xusb *tegra, bool runtime)
>>  {
>>  	struct xhci_hcd *xhci = hcd_to_xhci(tegra->hcd);
>> +	struct device *dev = tegra->dev;
>> +	bool wakeup = runtime ? true : device_may_wakeup(dev);
>> +	unsigned int i;
>>  	int err;
>> +	u32 usbcmd;
>> +
>> +	dev_dbg(dev, "entering ELPG\n");
>> +
>> +	usbcmd = readl(&xhci->op_regs->command);
>> +	usbcmd &= ~CMD_EIE;
>> +	writel(usbcmd, &xhci->op_regs->command);
>>  
>>  	err = tegra_xusb_check_ports(tegra);
>>  	if (err < 0) {
>>  		dev_err(tegra->dev, "not all ports suspended: %d\n", err);
>> -		return err;
>> +		goto out;
>>  	}
>>  
>>  	err = xhci_suspend(xhci, wakeup);
>>  	if (err < 0) {
>>  		dev_err(tegra->dev, "failed to suspend XHCI: %d\n", err);
>> -		return err;
>> +		goto out;
>>  	}
>>  
>>  	tegra_xusb_save_context(tegra);
>> -	tegra_xusb_phy_disable(tegra);
>> +
>> +	if (wakeup)
>> +		tegra_xhci_enable_phy_sleepwalk_wake(tegra);
>> +
>> +	tegra_xusb_powergate_partitions(tegra);
>> +
>> +	for (i = 0; i < tegra->num_phys; i++) {
>> +		if (!tegra->phys[i])
>> +			continue;
>> +
>> +		phy_power_off(tegra->phys[i]);
>> +		if (!wakeup)
>> +			phy_exit(tegra->phys[i]);
>> +	}
>>  	tegra_xusb_clk_disable(tegra);
> 
> Use blank lines to separate blocks of code.
> 
>>  
>> -	return 0;
>> +out:
>> +	if (!err)
>> +		dev_dbg(tegra->dev, "entering ELPG done\n");
>> +	else {
>> +		usbcmd = readl(&xhci->op_regs->command);
>> +		usbcmd |= CMD_EIE;
>> +		writel(usbcmd, &xhci->op_regs->command);
>> +
>> +		dev_dbg(tegra->dev, "entering ELPG failed\n");
>> +		pm_runtime_mark_last_busy(tegra->dev);
>> +	}
>> +
>> +	return err;
>>  }
>>  
>> -static int tegra_xusb_exit_elpg(struct tegra_xusb *tegra, bool wakeup)
>> +static int tegra_xusb_exit_elpg(struct tegra_xusb *tegra, bool runtime)
>>  {
>>  	struct xhci_hcd *xhci = hcd_to_xhci(tegra->hcd);
>> +	struct device *dev = tegra->dev;
>> +	bool wakeup = runtime ? true : device_may_wakeup(dev);
>> +	unsigned int i;
>> +	u32 usbcmd;
>>  	int err;
>>  
>> +	dev_dbg(dev, "exiting ELPG\n");
>> +	pm_runtime_mark_last_busy(tegra->dev);
>> +
>>  	err = tegra_xusb_clk_enable(tegra);
>>  	if (err < 0) {
>>  		dev_err(tegra->dev, "failed to enable clocks: %d\n", err);
>> -		return err;
>> +		goto out;
>>  	}
>>  
>> -	err = tegra_xusb_phy_enable(tegra);
>> -	if (err < 0) {
>> -		dev_err(tegra->dev, "failed to enable PHYs: %d\n", err);
>> -		goto disable_clk;
>> +	err = tegra_xusb_unpowergate_partitions(tegra);
>> +	if (err)
>> +		goto disable_clks;
>> +
>> +	if (wakeup)
>> +		tegra_xhci_disable_phy_wake(tegra);
>> +
>> +	for (i = 0; i < tegra->num_phys; i++) {
>> +		if (!tegra->phys[i])
>> +			continue;
>> +
>> +		if (!wakeup)
>> +			phy_init(tegra->phys[i]);
>> +
>> +		phy_power_on(tegra->phys[i]);
>>  	}
>>  
>>  	tegra_xusb_config(tegra);
>> @@ -1821,31 +2087,78 @@ static int tegra_xusb_exit_elpg(struct tegra_xusb *tegra, bool wakeup)
>>  		goto disable_phy;
>>  	}
>>  
>> -	err = xhci_resume(xhci, true);
>> +	if (wakeup)
>> +		tegra_xhci_disable_phy_sleepwalk(tegra);
>> +
>> +	err = xhci_resume(xhci, 0);
>>  	if (err < 0) {
>>  		dev_err(tegra->dev, "failed to resume XHCI: %d\n", err);
>>  		goto disable_phy;
>>  	}
>>  
>> -	return 0;
>> +	usbcmd = readl(&xhci->op_regs->command);
>> +	usbcmd |= CMD_EIE;
>> +	writel(usbcmd, &xhci->op_regs->command);
>> +
>> +	goto out;
>>  
>>  disable_phy:
>> -	tegra_xusb_phy_disable(tegra);
>> -disable_clk:
>> +	for (i = 0; i < tegra->num_phys; i++) {
>> +		if (!tegra->phys[i])
>> +			continue;
>> +
>> +		phy_power_off(tegra->phys[i]);
>> +		if (!wakeup)
>> +			phy_exit(tegra->phys[i]);
>> +	}
>> +	tegra_xusb_powergate_partitions(tegra);
>> +disable_clks:
>>  	tegra_xusb_clk_disable(tegra);
>> +out:
>> +	if (!err)
>> +		dev_dbg(dev, "exiting ELPG done\n");
>> +	else
>> +		dev_dbg(dev, "exiting ELPG failed\n");
>> +
>>  	return err;
>>  }
>>  
>>  static int tegra_xusb_suspend(struct device *dev)
>>  {
>>  	struct tegra_xusb *tegra = dev_get_drvdata(dev);
>> -	bool wakeup = device_may_wakeup(dev);
>>  	int err;
>>  
>>  	synchronize_irq(tegra->mbox_irq);
>> -
> 
> I think that blank line actually helped with readability.
> 
> Thierry
> 
>>  	mutex_lock(&tegra->lock);
>> -	err = tegra_xusb_enter_elpg(tegra, wakeup);
>> +
>> +	if (pm_runtime_suspended(dev)) {
>> +		err = tegra_xusb_exit_elpg(tegra, true);
>> +		if (err < 0)
>> +			goto out;
>> +	}
>> +
>> +	err = tegra_xusb_enter_elpg(tegra, false);
>> +	if (err < 0) {
>> +		if (pm_runtime_suspended(dev)) {
>> +			pm_runtime_disable(dev);
>> +			pm_runtime_set_active(dev);
>> +			pm_runtime_enable(dev);
>> +		}
>> +
>> +		goto out;
>> +	}
>> +
>> +out:
>> +	if (!err) {
>> +		tegra->suspended = true;
>> +		pm_runtime_disable(dev);
>> +
>> +		if (device_may_wakeup(dev)) {
>> +			if (enable_irq_wake(tegra->padctl_irq))
>> +				dev_err(dev, "failed to enable padctl wakes\n");
>> +		}
>> +	}
>> +
>>  	mutex_unlock(&tegra->lock);
>>  
>>  	return err;
>> @@ -1854,14 +2167,32 @@ static int tegra_xusb_suspend(struct device *dev)
>>  static int tegra_xusb_resume(struct device *dev)
>>  {
>>  	struct tegra_xusb *tegra = dev_get_drvdata(dev);
>> -	bool wakeup = device_may_wakeup(dev);
>>  	int err;
>>  
>>  	mutex_lock(&tegra->lock);
>> -	err = tegra_xusb_exit_elpg(tegra, wakeup);
>> +
>> +	if (!tegra->suspended) {
>> +		mutex_unlock(&tegra->lock);
>> +		return 0;
>> +	}
>> +
>> +	err = tegra_xusb_exit_elpg(tegra, false);
>> +	if (err < 0) {
>> +		mutex_unlock(&tegra->lock);
>> +		return err;
>> +	}
>> +
>> +	if (device_may_wakeup(dev)) {
>> +		if (disable_irq_wake(tegra->padctl_irq))
>> +			dev_err(dev, "failed to disable padctl wakes\n");
>> +	}
>> +	tegra->suspended = false;
>>  	mutex_unlock(&tegra->lock);
>>  
>> -	return err;
>> +	pm_runtime_set_active(dev);
>> +	pm_runtime_enable(dev);
>> +
>> +	return 0;
>>  }
>>  #endif
>>  
>> -- 
>> 2.25.1
>>
JC Kuo Sept. 8, 2020, 2:42 a.m. UTC | #4
Thanks Dmitry. I will remove this.

On 9/2/20 4:33 AM, Dmitry Osipenko wrote:
> 31.08.2020 07:40, JC Kuo пишет:
>> +	err = devm_request_threaded_irq(&pdev->dev, tegra->padctl_irq,
>> +		NULL,
>> +		tegra_xusb_padctl_irq,
>> +		IRQF_ONESHOT |
> 
>> +		IRQF_TRIGGER_HIGH,
> 
> Specifying trigger levels is meaningless for interrupts coming from a
> device-tree because DT levels always take precedence.
>
diff mbox series

Patch

diff --git a/drivers/usb/host/xhci-tegra.c b/drivers/usb/host/xhci-tegra.c
index ce6526c2caf6..9530cfc83f45 100644
--- a/drivers/usb/host/xhci-tegra.c
+++ b/drivers/usb/host/xhci-tegra.c
@@ -15,9 +15,11 @@ 
 #include <linux/kernel.h>
 #include <linux/module.h>
 #include <linux/of_device.h>
+#include <linux/of_irq.h>
 #include <linux/phy/phy.h>
 #include <linux/phy/tegra/xusb.h>
 #include <linux/platform_device.h>
+#include <linux/usb/ch9.h>
 #include <linux/pm.h>
 #include <linux/pm_domain.h>
 #include <linux/pm_runtime.h>
@@ -224,6 +226,7 @@  struct tegra_xusb {
 
 	int xhci_irq;
 	int mbox_irq;
+	int padctl_irq;
 
 	void __iomem *ipfs_base;
 	void __iomem *fpci_base;
@@ -268,10 +271,13 @@  struct tegra_xusb {
 		dma_addr_t phys;
 	} fw;
 
+	bool suspended;
 	struct tegra_xusb_context context;
 };
 
 static struct hc_driver __read_mostly tegra_xhci_hc_driver;
+static int tegra_xusb_exit_elpg(struct tegra_xusb *tegra, bool runtime);
+static int tegra_xusb_enter_elpg(struct tegra_xusb *tegra, bool runtime);
 
 static inline u32 fpci_readl(struct tegra_xusb *tegra, unsigned int offset)
 {
@@ -657,6 +663,9 @@  static irqreturn_t tegra_xusb_mbox_thread(int irq, void *data)
 
 	mutex_lock(&tegra->lock);
 
+	if (pm_runtime_suspended(tegra->dev) || tegra->suspended)
+		goto out;
+
 	value = fpci_readl(tegra, tegra->soc->mbox.data_out);
 	tegra_xusb_mbox_unpack(&msg, value);
 
@@ -670,6 +679,7 @@  static irqreturn_t tegra_xusb_mbox_thread(int irq, void *data)
 
 	tegra_xusb_mbox_handle(tegra, &msg);
 
+out:
 	mutex_unlock(&tegra->lock);
 	return IRQ_HANDLED;
 }
@@ -812,12 +822,27 @@  static void tegra_xusb_phy_disable(struct tegra_xusb *tegra)
 
 static int tegra_xusb_runtime_suspend(struct device *dev)
 {
-	return 0;
+	struct tegra_xusb *tegra = dev_get_drvdata(dev);
+	int ret;
+
+	synchronize_irq(tegra->mbox_irq);
+	mutex_lock(&tegra->lock);
+	ret = tegra_xusb_enter_elpg(tegra, true);
+	mutex_unlock(&tegra->lock);
+
+	return ret;
 }
 
 static int tegra_xusb_runtime_resume(struct device *dev)
 {
-	return 0;
+	struct tegra_xusb *tegra = dev_get_drvdata(dev);
+	int err;
+
+	mutex_lock(&tegra->lock);
+	err = tegra_xusb_exit_elpg(tegra, true);
+	mutex_unlock(&tegra->lock);
+
+	return err;
 }
 
 #ifdef CONFIG_PM_SLEEP
@@ -1121,6 +1146,22 @@  static int __tegra_xusb_enable_firmware_messages(struct tegra_xusb *tegra)
 	return err;
 }
 
+static irqreturn_t tegra_xusb_padctl_irq(int irq, void *data)
+{
+	struct tegra_xusb *tegra = data;
+
+	mutex_lock(&tegra->lock);
+	if (tegra->suspended) {
+		mutex_unlock(&tegra->lock);
+		return IRQ_HANDLED;
+	}
+	mutex_unlock(&tegra->lock);
+
+	pm_runtime_resume(tegra->dev);
+
+	return IRQ_HANDLED;
+}
+
 static int tegra_xusb_enable_firmware_messages(struct tegra_xusb *tegra)
 {
 	int err;
@@ -1244,6 +1285,51 @@  static void tegra_xhci_id_work(struct work_struct *work)
 	}
 }
 
+static bool is_usb2_otg_phy(struct tegra_xusb *tegra, int index)
+{
+	return (tegra->usbphy[index] != NULL);
+}
+
+static bool is_usb3_otg_phy(struct tegra_xusb *tegra, int index)
+{
+	struct tegra_xusb_padctl *padctl = tegra->padctl;
+	int i, port;
+
+	for (i = 0; i < tegra->num_usb_phys; i++) {
+		if (is_usb2_otg_phy(tegra, i)) {
+			port = tegra_xusb_padctl_get_usb3_companion(padctl,i);
+			if (index == port)
+				return true;
+		}
+	}
+
+	return false;
+}
+
+static bool is_host_mode_phy(struct tegra_xusb *tegra, int phy_type, int index)
+{
+	if (strcmp(tegra->soc->phy_types[phy_type].name, "hsic") == 0)
+		return true;
+
+	if (strcmp(tegra->soc->phy_types[phy_type].name, "usb2") == 0) {
+		if (is_usb2_otg_phy(tegra, index)) {
+			return ((index == tegra->otg_usb2_port) &&
+				 tegra->host_mode);
+		} else
+			return true;
+	}
+
+	if (strcmp(tegra->soc->phy_types[phy_type].name, "usb3") == 0) {
+		if (is_usb3_otg_phy(tegra, index)) {
+			return ((index == tegra->otg_usb3_port) &&
+				 tegra->host_mode);
+		} else
+			return true;
+	}
+
+	return false;
+}
+
 static int tegra_xusb_get_usb2_port(struct tegra_xusb *tegra,
 					      struct usb_phy *usbphy)
 {
@@ -1336,6 +1422,7 @@  static void tegra_xusb_deinit_usb_phy(struct tegra_xusb *tegra)
 static int tegra_xusb_probe(struct platform_device *pdev)
 {
 	struct tegra_xusb *tegra;
+	struct device_node *np;
 	struct resource *regs;
 	struct xhci_hcd *xhci;
 	unsigned int i, j, k;
@@ -1383,6 +1470,14 @@  static int tegra_xusb_probe(struct platform_device *pdev)
 	if (IS_ERR(tegra->padctl))
 		return PTR_ERR(tegra->padctl);
 
+	np = of_parse_phandle(pdev->dev.of_node, "nvidia,xusb-padctl", 0);
+	if (!np)
+		return -ENODEV;
+
+	tegra->padctl_irq = of_irq_get(np, 0);
+	if (tegra->padctl_irq < 0)
+		return tegra->padctl_irq;
+
 	tegra->host_clk = devm_clk_get(&pdev->dev, "xusb_host");
 	if (IS_ERR(tegra->host_clk)) {
 		err = PTR_ERR(tegra->host_clk);
@@ -1527,6 +1622,7 @@  static int tegra_xusb_probe(struct platform_device *pdev)
 		goto put_powerdomains;
 	}
 
+	tegra->hcd->skip_phy_initialization = 1;
 	tegra->hcd->regs = tegra->regs;
 	tegra->hcd->rsrc_start = regs->start;
 	tegra->hcd->rsrc_len = resource_size(regs);
@@ -1609,12 +1705,6 @@  static int tegra_xusb_probe(struct platform_device *pdev)
 		goto put_usb3;
 	}
 
-	err = tegra_xusb_enable_firmware_messages(tegra);
-	if (err < 0) {
-		dev_err(&pdev->dev, "failed to enable messages: %d\n", err);
-		goto remove_usb3;
-	}
-
 	err = devm_request_threaded_irq(&pdev->dev, tegra->mbox_irq,
 					tegra_xusb_mbox_irq,
 					tegra_xusb_mbox_thread, 0,
@@ -1624,12 +1714,40 @@  static int tegra_xusb_probe(struct platform_device *pdev)
 		goto remove_usb3;
 	}
 
+	err = devm_request_threaded_irq(&pdev->dev, tegra->padctl_irq,
+		NULL,
+		tegra_xusb_padctl_irq,
+		IRQF_ONESHOT |
+		IRQF_TRIGGER_HIGH,
+		dev_name(&pdev->dev), tegra);
+	if (err < 0) {
+		dev_err(&pdev->dev, "failed to request padctl IRQ: %d\n", err);
+		goto remove_usb3;
+	}
+
+	err = tegra_xusb_enable_firmware_messages(tegra);
+	if (err < 0) {
+		dev_err(&pdev->dev, "failed to enable messages: %d\n", err);
+		goto remove_usb3;
+	}
+
 	err = tegra_xusb_init_usb_phy(tegra);
 	if (err < 0) {
 		dev_err(&pdev->dev, "failed to init USB PHY: %d\n", err);
 		goto remove_usb3;
 	}
 
+	/* Enable wake for both USB 2.0 and USB 3.0 roothubs */
+	device_init_wakeup(&tegra->hcd->self.root_hub->dev, true);
+	device_init_wakeup(&xhci->shared_hcd->self.root_hub->dev, true);
+	device_init_wakeup(tegra->dev, true);
+
+	pm_runtime_use_autosuspend(tegra->dev);
+	pm_runtime_set_autosuspend_delay(tegra->dev, 2000);
+	pm_runtime_mark_last_busy(tegra->dev);
+	pm_runtime_set_active(tegra->dev);
+	pm_runtime_enable(tegra->dev);
+
 	return 0;
 
 remove_usb3:
@@ -1665,6 +1783,7 @@  static int tegra_xusb_remove(struct platform_device *pdev)
 
 	tegra_xusb_deinit_usb_phy(tegra);
 
+	pm_runtime_get_sync(&pdev->dev);
 	usb_remove_hcd(xhci->shared_hcd);
 	usb_put_hcd(xhci->shared_hcd);
 	xhci->shared_hcd = NULL;
@@ -1674,8 +1793,8 @@  static int tegra_xusb_remove(struct platform_device *pdev)
 	dma_free_coherent(&pdev->dev, tegra->fw.size, tegra->fw.virt,
 			  tegra->fw.phys);
 
-	pm_runtime_put_sync(&pdev->dev);
 	pm_runtime_disable(&pdev->dev);
+	pm_runtime_put(&pdev->dev);
 
 	tegra_xusb_powergate_partitions(tegra);
 
@@ -1717,9 +1836,17 @@  static bool xhci_hub_ports_suspended(struct xhci_hub *hub)
 static int tegra_xusb_check_ports(struct tegra_xusb *tegra)
 {
 	struct xhci_hcd *xhci = hcd_to_xhci(tegra->hcd);
+	struct xhci_hub *rhub =  xhci_get_rhub(xhci->main_hcd);
+	struct xhci_bus_state *bus_state = &rhub->bus_state;
 	unsigned long flags;
 	int err = 0;
 
+	if (bus_state->bus_suspended) {
+		/* xusb_hub_suspend() has just directed one or more USB2 port(s)
+		 * to U3 state, it takes 3ms to enter U3. */
+		usleep_range(3000, 4000);
+	}
+
 	spin_lock_irqsave(&xhci->lock, flags);
 
 	if (!xhci_hub_ports_suspended(&xhci->usb2_rhub) ||
@@ -1765,45 +1892,184 @@  static void tegra_xusb_restore_context(struct tegra_xusb *tegra)
 	}
 }
 
-static int tegra_xusb_enter_elpg(struct tegra_xusb *tegra, bool wakeup)
+static enum usb_device_speed
+tegra_xhci_portsc_to_speed(struct tegra_xusb *tegra, u32 portsc)
+{
+	if (DEV_LOWSPEED(portsc))
+		return USB_SPEED_LOW;
+	else if (DEV_HIGHSPEED(portsc))
+		return USB_SPEED_HIGH;
+	else if (DEV_FULLSPEED(portsc))
+		return USB_SPEED_FULL;
+	else if (DEV_SUPERSPEED_ANY(portsc))
+		return USB_SPEED_SUPER;
+	else
+		return USB_SPEED_UNKNOWN;
+}
+
+static void tegra_xhci_enable_phy_sleepwalk_wake(struct tegra_xusb *tegra)
+{
+	struct tegra_xusb_padctl *padctl = tegra->padctl;
+	struct xhci_hcd *xhci = hcd_to_xhci(tegra->hcd);
+	enum usb_device_speed speed;
+	struct phy *phy;
+	int index, offset;
+	int i, j, k;
+	struct xhci_hub *rhub;
+	u32 portsc;
+
+	for (i = 0, k = 0; i < tegra->soc->num_types; i++) {
+		if (strcmp(tegra->soc->phy_types[i].name, "usb3") == 0)
+			rhub = &xhci->usb3_rhub;
+		else
+			rhub = &xhci->usb2_rhub;
+
+		if (strcmp(tegra->soc->phy_types[i].name, "hsic") == 0)
+			offset = tegra->soc->ports.usb2.count;
+		else
+			offset = 0;
+
+		for (j = 0; j < tegra->soc->phy_types[i].num; j++) {
+			phy = tegra->phys[k++];
+
+			if (!phy)
+				continue;
+
+			index = j + offset;
+
+			if (index >= rhub->num_ports)
+				continue;
+
+			if (!is_host_mode_phy(tegra, i, j))
+				continue;
+
+			portsc = readl(rhub->ports[index]->addr);
+			speed = tegra_xhci_portsc_to_speed(tegra, portsc);
+			tegra_xusb_padctl_enable_phy_sleepwalk(padctl, phy,
+							       speed);
+			tegra_xusb_padctl_enable_phy_wake(padctl, phy);
+		}
+	}
+}
+
+static void tegra_xhci_disable_phy_wake(struct tegra_xusb *tegra)
+{
+	struct tegra_xusb_padctl *padctl = tegra->padctl;
+	int i;
+
+	for (i = 0; i < tegra->num_phys; i++) {
+		if (!tegra->phys[i])
+			continue;
+
+		tegra_xusb_padctl_disable_phy_wake(padctl, tegra->phys[i]);
+	}
+}
+
+static void tegra_xhci_disable_phy_sleepwalk(struct tegra_xusb *tegra)
+{
+	struct tegra_xusb_padctl *padctl = tegra->padctl;
+	int i;
+
+	for (i = 0; i < tegra->num_phys; i++) {
+		if (!tegra->phys[i])
+			continue;
+
+		tegra_xusb_padctl_disable_phy_sleepwalk(padctl, tegra->phys[i]);
+	}
+}
+
+static int tegra_xusb_enter_elpg(struct tegra_xusb *tegra, bool runtime)
 {
 	struct xhci_hcd *xhci = hcd_to_xhci(tegra->hcd);
+	struct device *dev = tegra->dev;
+	bool wakeup = runtime ? true : device_may_wakeup(dev);
+	unsigned int i;
 	int err;
+	u32 usbcmd;
+
+	dev_dbg(dev, "entering ELPG\n");
+
+	usbcmd = readl(&xhci->op_regs->command);
+	usbcmd &= ~CMD_EIE;
+	writel(usbcmd, &xhci->op_regs->command);
 
 	err = tegra_xusb_check_ports(tegra);
 	if (err < 0) {
 		dev_err(tegra->dev, "not all ports suspended: %d\n", err);
-		return err;
+		goto out;
 	}
 
 	err = xhci_suspend(xhci, wakeup);
 	if (err < 0) {
 		dev_err(tegra->dev, "failed to suspend XHCI: %d\n", err);
-		return err;
+		goto out;
 	}
 
 	tegra_xusb_save_context(tegra);
-	tegra_xusb_phy_disable(tegra);
+
+	if (wakeup)
+		tegra_xhci_enable_phy_sleepwalk_wake(tegra);
+
+	tegra_xusb_powergate_partitions(tegra);
+
+	for (i = 0; i < tegra->num_phys; i++) {
+		if (!tegra->phys[i])
+			continue;
+
+		phy_power_off(tegra->phys[i]);
+		if (!wakeup)
+			phy_exit(tegra->phys[i]);
+	}
 	tegra_xusb_clk_disable(tegra);
 
-	return 0;
+out:
+	if (!err)
+		dev_dbg(tegra->dev, "entering ELPG done\n");
+	else {
+		usbcmd = readl(&xhci->op_regs->command);
+		usbcmd |= CMD_EIE;
+		writel(usbcmd, &xhci->op_regs->command);
+
+		dev_dbg(tegra->dev, "entering ELPG failed\n");
+		pm_runtime_mark_last_busy(tegra->dev);
+	}
+
+	return err;
 }
 
-static int tegra_xusb_exit_elpg(struct tegra_xusb *tegra, bool wakeup)
+static int tegra_xusb_exit_elpg(struct tegra_xusb *tegra, bool runtime)
 {
 	struct xhci_hcd *xhci = hcd_to_xhci(tegra->hcd);
+	struct device *dev = tegra->dev;
+	bool wakeup = runtime ? true : device_may_wakeup(dev);
+	unsigned int i;
+	u32 usbcmd;
 	int err;
 
+	dev_dbg(dev, "exiting ELPG\n");
+	pm_runtime_mark_last_busy(tegra->dev);
+
 	err = tegra_xusb_clk_enable(tegra);
 	if (err < 0) {
 		dev_err(tegra->dev, "failed to enable clocks: %d\n", err);
-		return err;
+		goto out;
 	}
 
-	err = tegra_xusb_phy_enable(tegra);
-	if (err < 0) {
-		dev_err(tegra->dev, "failed to enable PHYs: %d\n", err);
-		goto disable_clk;
+	err = tegra_xusb_unpowergate_partitions(tegra);
+	if (err)
+		goto disable_clks;
+
+	if (wakeup)
+		tegra_xhci_disable_phy_wake(tegra);
+
+	for (i = 0; i < tegra->num_phys; i++) {
+		if (!tegra->phys[i])
+			continue;
+
+		if (!wakeup)
+			phy_init(tegra->phys[i]);
+
+		phy_power_on(tegra->phys[i]);
 	}
 
 	tegra_xusb_config(tegra);
@@ -1821,31 +2087,78 @@  static int tegra_xusb_exit_elpg(struct tegra_xusb *tegra, bool wakeup)
 		goto disable_phy;
 	}
 
-	err = xhci_resume(xhci, true);
+	if (wakeup)
+		tegra_xhci_disable_phy_sleepwalk(tegra);
+
+	err = xhci_resume(xhci, 0);
 	if (err < 0) {
 		dev_err(tegra->dev, "failed to resume XHCI: %d\n", err);
 		goto disable_phy;
 	}
 
-	return 0;
+	usbcmd = readl(&xhci->op_regs->command);
+	usbcmd |= CMD_EIE;
+	writel(usbcmd, &xhci->op_regs->command);
+
+	goto out;
 
 disable_phy:
-	tegra_xusb_phy_disable(tegra);
-disable_clk:
+	for (i = 0; i < tegra->num_phys; i++) {
+		if (!tegra->phys[i])
+			continue;
+
+		phy_power_off(tegra->phys[i]);
+		if (!wakeup)
+			phy_exit(tegra->phys[i]);
+	}
+	tegra_xusb_powergate_partitions(tegra);
+disable_clks:
 	tegra_xusb_clk_disable(tegra);
+out:
+	if (!err)
+		dev_dbg(dev, "exiting ELPG done\n");
+	else
+		dev_dbg(dev, "exiting ELPG failed\n");
+
 	return err;
 }
 
 static int tegra_xusb_suspend(struct device *dev)
 {
 	struct tegra_xusb *tegra = dev_get_drvdata(dev);
-	bool wakeup = device_may_wakeup(dev);
 	int err;
 
 	synchronize_irq(tegra->mbox_irq);
-
 	mutex_lock(&tegra->lock);
-	err = tegra_xusb_enter_elpg(tegra, wakeup);
+
+	if (pm_runtime_suspended(dev)) {
+		err = tegra_xusb_exit_elpg(tegra, true);
+		if (err < 0)
+			goto out;
+	}
+
+	err = tegra_xusb_enter_elpg(tegra, false);
+	if (err < 0) {
+		if (pm_runtime_suspended(dev)) {
+			pm_runtime_disable(dev);
+			pm_runtime_set_active(dev);
+			pm_runtime_enable(dev);
+		}
+
+		goto out;
+	}
+
+out:
+	if (!err) {
+		tegra->suspended = true;
+		pm_runtime_disable(dev);
+
+		if (device_may_wakeup(dev)) {
+			if (enable_irq_wake(tegra->padctl_irq))
+				dev_err(dev, "failed to enable padctl wakes\n");
+		}
+	}
+
 	mutex_unlock(&tegra->lock);
 
 	return err;
@@ -1854,14 +2167,32 @@  static int tegra_xusb_suspend(struct device *dev)
 static int tegra_xusb_resume(struct device *dev)
 {
 	struct tegra_xusb *tegra = dev_get_drvdata(dev);
-	bool wakeup = device_may_wakeup(dev);
 	int err;
 
 	mutex_lock(&tegra->lock);
-	err = tegra_xusb_exit_elpg(tegra, wakeup);
+
+	if (!tegra->suspended) {
+		mutex_unlock(&tegra->lock);
+		return 0;
+	}
+
+	err = tegra_xusb_exit_elpg(tegra, false);
+	if (err < 0) {
+		mutex_unlock(&tegra->lock);
+		return err;
+	}
+
+	if (device_may_wakeup(dev)) {
+		if (disable_irq_wake(tegra->padctl_irq))
+			dev_err(dev, "failed to disable padctl wakes\n");
+	}
+	tegra->suspended = false;
 	mutex_unlock(&tegra->lock);
 
-	return err;
+	pm_runtime_set_active(dev);
+	pm_runtime_enable(dev);
+
+	return 0;
 }
 #endif