diff options
Diffstat (limited to 'drivers/net/wireless/ath/wil6210/pm.c')
-rw-r--r-- | drivers/net/wireless/ath/wil6210/pm.c | 350 |
1 files changed, 350 insertions, 0 deletions
diff --git a/drivers/net/wireless/ath/wil6210/pm.c b/drivers/net/wireless/ath/wil6210/pm.c new file mode 100644 index 0000000..8f5d1b4 --- /dev/null +++ b/drivers/net/wireless/ath/wil6210/pm.c @@ -0,0 +1,350 @@ +/* + * Copyright (c) 2014,2017 Qualcomm Atheros, Inc. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "wil6210.h" +#include <linux/jiffies.h> + +int wil_can_suspend(struct wil6210_priv *wil, bool is_runtime) +{ + int rc = 0; + struct wireless_dev *wdev = wil->wdev; + struct net_device *ndev = wil_to_ndev(wil); + + wil_dbg_pm(wil, "can_suspend: %s\n", is_runtime ? "runtime" : "system"); + + if (!(ndev->flags & IFF_UP)) { + /* can always sleep when down */ + wil_dbg_pm(wil, "Interface is down\n"); + goto out; + } + if (test_bit(wil_status_resetting, wil->status)) { + wil_dbg_pm(wil, "Delay suspend when resetting\n"); + rc = -EBUSY; + goto out; + } + if (wil->recovery_state != fw_recovery_idle) { + wil_dbg_pm(wil, "Delay suspend during recovery\n"); + rc = -EBUSY; + goto out; + } + + /* interface is running */ + switch (wdev->iftype) { + case NL80211_IFTYPE_MONITOR: + case NL80211_IFTYPE_STATION: + case NL80211_IFTYPE_P2P_CLIENT: + if (test_bit(wil_status_fwconnecting, wil->status)) { + wil_dbg_pm(wil, "Delay suspend when connecting\n"); + rc = -EBUSY; + goto out; + } + break; + /* AP-like interface - can't suspend */ + default: + wil_dbg_pm(wil, "AP-like interface\n"); + rc = -EBUSY; + break; + } + +out: + wil_dbg_pm(wil, "can_suspend: %s => %s (%d)\n", + is_runtime ? "runtime" : "system", rc ? "No" : "Yes", rc); + + if (rc) + wil->suspend_stats.rejected_by_host++; + + return rc; +} + +static int wil_resume_keep_radio_on(struct wil6210_priv *wil) +{ + int rc = 0; + + /* wil_status_resuming will be cleared when getting + * WMI_TRAFFIC_RESUME_EVENTID + */ + set_bit(wil_status_resuming, wil->status); + clear_bit(wil_status_suspended, wil->status); + wil_c(wil, RGF_USER_CLKS_CTL_0, BIT_USER_CLKS_RST_PWGD); + wil_unmask_irq(wil); + + wil6210_bus_request(wil, wil->bus_request_kbps_pre_suspend); + + /* Send WMI resume request to the device */ + rc = wmi_resume(wil); + if (rc) { + wil_err(wil, "device failed to resume (%d)\n", rc); + if (no_fw_recovery) + goto out; + rc = wil_down(wil); + if (rc) { + wil_err(wil, "wil_down failed (%d)\n", rc); + goto out; + } + rc = wil_up(wil); + if (rc) { + wil_err(wil, "wil_up failed (%d)\n", rc); + goto out; + } + } + + /* Wake all queues */ + if (test_bit(wil_status_fwconnected, wil->status)) + wil_update_net_queues_bh(wil, NULL, false); + +out: + if (rc) + set_bit(wil_status_suspended, wil->status); + return rc; +} + +static int wil_suspend_keep_radio_on(struct wil6210_priv *wil) +{ + int rc = 0; + unsigned long start, data_comp_to; + + wil_dbg_pm(wil, "suspend keep radio on\n"); + + /* Prevent handling of new tx and wmi commands */ + set_bit(wil_status_suspending, wil->status); + wil_update_net_queues_bh(wil, NULL, true); + + if (!wil_is_tx_idle(wil)) { + wil_dbg_pm(wil, "Pending TX data, reject suspend\n"); + wil->suspend_stats.rejected_by_host++; + goto reject_suspend; + } + + if (!wil_is_rx_idle(wil)) { + wil_dbg_pm(wil, "Pending RX data, reject suspend\n"); + wil->suspend_stats.rejected_by_host++; + goto reject_suspend; + } + + if (!wil_is_wmi_idle(wil)) { + wil_dbg_pm(wil, "Pending WMI events, reject suspend\n"); + wil->suspend_stats.rejected_by_host++; + goto reject_suspend; + } + + /* Send WMI suspend request to the device */ + rc = wmi_suspend(wil); + if (rc) { + wil_dbg_pm(wil, "wmi_suspend failed, reject suspend (%d)\n", + rc); + goto reject_suspend; + } + + /* Wait for completion of the pending RX packets */ + start = jiffies; + data_comp_to = jiffies + msecs_to_jiffies(WIL_DATA_COMPLETION_TO_MS); + if (test_bit(wil_status_napi_en, wil->status)) { + while (!wil_is_rx_idle(wil)) { + if (time_after(jiffies, data_comp_to)) { + if (wil_is_rx_idle(wil)) + break; + wil_err(wil, + "TO waiting for idle RX, suspend failed\n"); + wil->suspend_stats.failed_suspends++; + goto resume_after_fail; + } + wil_dbg_ratelimited(wil, "rx vring is not empty -> NAPI\n"); + napi_synchronize(&wil->napi_rx); + msleep(20); + } + } + + /* In case of pending WMI events, reject the suspend + * and resume the device. + * This can happen if the device sent the WMI events before + * approving the suspend. + */ + if (!wil_is_wmi_idle(wil)) { + wil_err(wil, "suspend failed due to pending WMI events\n"); + wil->suspend_stats.failed_suspends++; + goto resume_after_fail; + } + + wil_mask_irq(wil); + + /* Disable device reset on PERST */ + wil_s(wil, RGF_USER_CLKS_CTL_0, BIT_USER_CLKS_RST_PWGD); + + if (wil->platform_ops.suspend) { + rc = wil->platform_ops.suspend(wil->platform_handle, true); + if (rc) { + wil_err(wil, "platform device failed to suspend (%d)\n", + rc); + wil->suspend_stats.failed_suspends++; + wil_c(wil, RGF_USER_CLKS_CTL_0, BIT_USER_CLKS_RST_PWGD); + wil_unmask_irq(wil); + goto resume_after_fail; + } + } + + /* Save the current bus request to return to the same in resume */ + wil->bus_request_kbps_pre_suspend = wil->bus_request_kbps; + wil6210_bus_request(wil, 0); + + set_bit(wil_status_suspended, wil->status); + clear_bit(wil_status_suspending, wil->status); + + return rc; + +resume_after_fail: + set_bit(wil_status_resuming, wil->status); + clear_bit(wil_status_suspending, wil->status); + rc = wmi_resume(wil); + /* if resume succeeded, reject the suspend */ + if (!rc) { + rc = -EBUSY; + if (test_bit(wil_status_fwconnected, wil->status)) + wil_update_net_queues_bh(wil, NULL, false); + } + return rc; + +reject_suspend: + clear_bit(wil_status_suspending, wil->status); + if (test_bit(wil_status_fwconnected, wil->status)) + wil_update_net_queues_bh(wil, NULL, false); + return -EBUSY; +} + +static int wil_suspend_radio_off(struct wil6210_priv *wil) +{ + int rc = 0; + struct net_device *ndev = wil_to_ndev(wil); + + wil_dbg_pm(wil, "suspend radio off\n"); + + /* if netif up, hardware is alive, shut it down */ + if (ndev->flags & IFF_UP) { + rc = wil_down(wil); + if (rc) { + wil_err(wil, "wil_down : %d\n", rc); + goto out; + } + } + + /* Disable PCIe IRQ to prevent sporadic IRQs when PCIe is suspending */ + wil_dbg_pm(wil, "Disabling PCIe IRQ before suspending\n"); + wil_disable_irq(wil); + + if (wil->platform_ops.suspend) { + rc = wil->platform_ops.suspend(wil->platform_handle, false); + if (rc) { + wil_enable_irq(wil); + goto out; + } + } + + set_bit(wil_status_suspended, wil->status); + +out: + wil_dbg_pm(wil, "suspend radio off: %d\n", rc); + + return rc; +} + +static int wil_resume_radio_off(struct wil6210_priv *wil) +{ + int rc = 0; + struct net_device *ndev = wil_to_ndev(wil); + + wil_dbg_pm(wil, "Enabling PCIe IRQ\n"); + wil_enable_irq(wil); + /* if netif up, bring hardware up + * During open(), IFF_UP set after actual device method + * invocation. This prevent recursive call to wil_up() + * wil_status_suspended will be cleared in wil_reset + */ + if (ndev->flags & IFF_UP) + rc = wil_up(wil); + else + clear_bit(wil_status_suspended, wil->status); + + return rc; +} + +int wil_suspend(struct wil6210_priv *wil, bool is_runtime) +{ + int rc = 0; + struct net_device *ndev = wil_to_ndev(wil); + bool keep_radio_on = ndev->flags & IFF_UP && + wil->keep_radio_on_during_sleep; + + wil_dbg_pm(wil, "suspend: %s\n", is_runtime ? "runtime" : "system"); + + if (test_bit(wil_status_suspended, wil->status)) { + wil_dbg_pm(wil, "trying to suspend while suspended\n"); + return 0; + } + + if (!keep_radio_on) + rc = wil_suspend_radio_off(wil); + else + rc = wil_suspend_keep_radio_on(wil); + + wil_dbg_pm(wil, "suspend: %s => %d\n", + is_runtime ? "runtime" : "system", rc); + + if (!rc) + wil->suspend_stats.suspend_start_time = ktime_get(); + + return rc; +} + +int wil_resume(struct wil6210_priv *wil, bool is_runtime) +{ + int rc = 0; + struct net_device *ndev = wil_to_ndev(wil); + bool keep_radio_on = ndev->flags & IFF_UP && + wil->keep_radio_on_during_sleep; + unsigned long long suspend_time_usec = 0; + + wil_dbg_pm(wil, "resume: %s\n", is_runtime ? "runtime" : "system"); + + if (wil->platform_ops.resume) { + rc = wil->platform_ops.resume(wil->platform_handle, + keep_radio_on); + if (rc) { + wil_err(wil, "platform_ops.resume : %d\n", rc); + goto out; + } + } + + if (keep_radio_on) + rc = wil_resume_keep_radio_on(wil); + else + rc = wil_resume_radio_off(wil); + + if (rc) + goto out; + + suspend_time_usec = + ktime_to_us(ktime_sub(ktime_get(), + wil->suspend_stats.suspend_start_time)); + wil->suspend_stats.total_suspend_time += suspend_time_usec; + if (suspend_time_usec < wil->suspend_stats.min_suspend_time) + wil->suspend_stats.min_suspend_time = suspend_time_usec; + if (suspend_time_usec > wil->suspend_stats.max_suspend_time) + wil->suspend_stats.max_suspend_time = suspend_time_usec; + +out: + wil_dbg_pm(wil, "resume: %s => %d, suspend time %lld usec\n", + is_runtime ? "runtime" : "system", rc, suspend_time_usec); + return rc; +} |