[PATCH wireless-next 18/35] wifi: mm81x: add ps.c
From: Lachlan Hodges
Date: Thu Feb 26 2026 - 23:19:16 EST
(Patches split per file for review, see cover letter for more
information)
Signed-off-by: Lachlan Hodges <lachlan.hodges@xxxxxxxxxxxxxx>
---
drivers/net/wireless/morsemicro/mm81x/ps.c | 239 +++++++++++++++++++++
1 file changed, 239 insertions(+)
create mode 100644 drivers/net/wireless/morsemicro/mm81x/ps.c
diff --git a/drivers/net/wireless/morsemicro/mm81x/ps.c b/drivers/net/wireless/morsemicro/mm81x/ps.c
new file mode 100644
index 000000000000..432165697acf
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/ps.c
@@ -0,0 +1,239 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+#include <linux/types.h>
+#include <linux/atomic.h>
+#include <linux/slab.h>
+#include <linux/mutex.h>
+#include <linux/workqueue.h>
+#include <linux/completion.h>
+#include <linux/gpio/consumer.h>
+#include <linux/jiffies.h>
+#include "hif.h"
+#include "debug.h"
+#include "skbq.h"
+#include "mac.h"
+#include "bus.h"
+#include "ps.h"
+
+#define MM81X_WAKEUP_DELAY_MS 10
+
+static bool mm81x_ps_is_busy_pin_asserted(struct mm81x *mm)
+{
+ bool active_high =
+ !(mm->firmware_flags & MM81X_FW_FLAGS_BUSY_ACTIVE_LOW);
+
+ if (!mm->ps.gpios_supported)
+ return false;
+
+ return (gpiod_get_value_cansleep(mm->ps.busy_gpio) == active_high);
+}
+
+static void mm81x_ps_set_wake_gpio(struct mm81x *mm, bool raise)
+{
+ if (!mm->ps.gpios_supported)
+ return;
+
+ gpiod_set_value_cansleep(mm->ps.wake_gpio, raise);
+
+ mm81x_dbg(mm, MM81X_DBG_ANY, "%s wake up pin",
+ (raise) ? "set" : "cleared");
+}
+
+static void mm81x_ps_wait_after_wake_pin_raise(struct mm81x *mm)
+{
+ unsigned long timeout;
+
+ if (!mm->ps.gpios_supported)
+ return;
+
+ if (mm81x_ps_is_busy_pin_asserted(mm))
+ return;
+
+ timeout = msecs_to_jiffies(MM81X_WAKEUP_DELAY_MS);
+ wait_for_completion_timeout(mm->ps.awake, timeout);
+}
+
+static void mm81x_ps_wakeup(struct mm81x_ps *mps)
+{
+ DECLARE_COMPLETION_ONSTACK(awake);
+ struct mm81x *mm = container_of(mps, struct mm81x, ps);
+
+ if (!mps->enable || !mps->suspended)
+ return;
+
+ WRITE_ONCE(mps->awake, &awake);
+ mm81x_ps_set_wake_gpio(mm, true);
+ mm81x_ps_wait_after_wake_pin_raise(mm);
+ WRITE_ONCE(mps->awake, NULL);
+
+ mm81x_set_bus_enable(mm, true);
+ mps->suspended = false;
+}
+
+static void mm81x_ps_sleep(struct mm81x_ps *mps)
+{
+ struct mm81x *mm = container_of(mps, struct mm81x, ps);
+
+ if (!mps->enable || mps->suspended)
+ return;
+
+ mps->suspended = true;
+ mm81x_set_bus_enable(mm, false);
+ mm81x_ps_set_wake_gpio(mm, false);
+}
+
+static irqreturn_t mm81x_ps_irq_handle(int irq, void *arg)
+{
+ struct mm81x_ps *mps = (struct mm81x_ps *)arg;
+ struct mm81x *mm = container_of(mps, struct mm81x, ps);
+ struct completion *awake = READ_ONCE(mps->awake);
+
+ if (awake)
+ complete(awake);
+ else
+ queue_work(mm->chip_wq, &mps->async_wake_work);
+
+ return IRQ_HANDLED;
+}
+
+static void mm81x_ps_async_wake_work(struct work_struct *work)
+{
+ struct mm81x_ps *mps =
+ container_of(work, struct mm81x_ps, async_wake_work);
+ struct mm81x *mm = container_of(mps, struct mm81x, ps);
+
+ if (mm81x_ps_is_busy_pin_asserted(mm)) {
+ mutex_lock(&mps->lock);
+ mm81x_ps_wakeup(mps);
+ mutex_unlock(&mps->lock);
+ }
+}
+
+static void mm81x_ps_evaluate(struct mm81x_ps *mps)
+{
+ struct mm81x *mm = container_of(mps, struct mm81x, ps);
+ bool needs_wake = false;
+ unsigned long expire;
+ unsigned long flags_on_entry =
+ (mm->hif.event_flags &
+ ~BIT(MM81X_HIF_EVT_DATA_TRAFFIC_PAUSE_PEND));
+
+ if (!mps->enable)
+ return;
+
+ needs_wake = (mps->wakers > 0);
+ needs_wake |= (flags_on_entry > 0);
+ needs_wake |= (mm81x_hif_get_tx_buffered_count(mm) > 0);
+
+ if (needs_wake) {
+ mm81x_ps_wakeup(mps);
+ return;
+ }
+
+ if (!mm81x_ps_is_busy_pin_asserted(mm)) {
+ mm81x_ps_sleep(mps);
+ return;
+ }
+
+ expire = msecs_to_jiffies(DEFAULT_BUS_TIMEOUT_MS);
+ cancel_delayed_work(&mps->delayed_eval_work);
+ queue_delayed_work(mm->chip_wq, &mps->delayed_eval_work, expire);
+}
+
+static void mm81x_ps_evaluate_work(struct work_struct *work)
+{
+ struct mm81x_ps *mps =
+ container_of(work, struct mm81x_ps, delayed_eval_work.work);
+
+ if (mps->enable) {
+ mutex_lock(&mps->lock);
+ mm81x_ps_evaluate(mps);
+ mutex_unlock(&mps->lock);
+ }
+}
+
+static int mm81x_ps_irq_init(struct mm81x *mm)
+{
+ int irq;
+ struct mm81x_ps *mps = &mm->ps;
+
+ if (!mm->ps.gpios_supported)
+ return 0;
+
+ irq = gpiod_to_irq(mm->ps.busy_gpio);
+ if (irq < 0)
+ return irq;
+
+ return request_irq(irq, (irq_handler_t)mm81x_ps_irq_handle,
+ (mm->firmware_flags &
+ MM81X_FW_FLAGS_BUSY_ACTIVE_LOW) ?
+ IRQF_TRIGGER_FALLING :
+ IRQF_TRIGGER_RISING,
+ "mm81x_notify", mps);
+}
+
+static void mm81x_ps_irq_deinit(struct mm81x *mm)
+{
+ struct mm81x_ps *mps = &mm->ps;
+
+ if (mm->ps.gpios_supported)
+ free_irq(gpiod_to_irq(mm->ps.busy_gpio), mps);
+}
+
+void mm81x_ps_enable(struct mm81x *mm)
+{
+ struct mm81x_ps *mps = &mm->ps;
+
+ if (mps->enable) {
+ mutex_lock(&mps->lock);
+ if (mps->wakers == 0) {
+ WARN_ON_ONCE(1);
+ } else {
+ mps->wakers--;
+ mm81x_ps_evaluate(mps);
+ }
+ mutex_unlock(&mps->lock);
+ }
+}
+
+void mm81x_ps_disable(struct mm81x *mm)
+{
+ struct mm81x_ps *mps = &mm->ps;
+
+ if (mps->enable) {
+ mutex_lock(&mps->lock);
+ mps->wakers++;
+ mm81x_ps_evaluate(mps);
+ mutex_unlock(&mps->lock);
+ }
+}
+
+int mm81x_ps_init(struct mm81x *mm)
+{
+ struct mm81x_ps *mps = &mm->ps;
+
+ mps->enable = !(mm->bus_type == MM81X_BUS_TYPE_SDIO &&
+ !mm->ps.gpios_supported);
+ mps->suspended = true;
+ mps->wakers = 1; /* we default to being on */
+ mutex_init(&mps->lock);
+
+ INIT_WORK(&mps->async_wake_work, mm81x_ps_async_wake_work);
+ INIT_DELAYED_WORK(&mps->delayed_eval_work, mm81x_ps_evaluate_work);
+
+ return mm81x_ps_irq_init(mm);
+}
+
+void mm81x_ps_finish(struct mm81x *mm)
+{
+ struct mm81x_ps *mps = &mm->ps;
+
+ if (mps->enable) {
+ mps->enable = false;
+ mm81x_ps_irq_deinit(mm);
+ cancel_work_sync(&mps->async_wake_work);
+ cancel_delayed_work_sync(&mps->delayed_eval_work);
+ }
+}
--
2.43.0