root/drivers/net/wwan/iosm/iosm_ipc_pm.c
// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (C) 2020-21 Intel Corporation.
 */

#include "iosm_ipc_protocol.h"

/* Timeout value in MS for the PM to wait for device to reach active state */
#define IPC_PM_ACTIVE_TIMEOUT_MS (500)

/* Note that here "active" has the value 1, as compared to the enums
 * ipc_mem_host_pm_state or ipc_mem_dev_pm_state, where "active" is 0
 */
#define IPC_PM_SLEEP (0)
#define CONSUME_STATE (0)
#define IPC_PM_ACTIVE (1)

void ipc_pm_signal_hpda_doorbell(struct iosm_pm *ipc_pm, u32 identifier,
                                 bool host_slp_check)
{
        if (host_slp_check && ipc_pm->host_pm_state != IPC_MEM_HOST_PM_ACTIVE &&
            ipc_pm->host_pm_state != IPC_MEM_HOST_PM_ACTIVE_WAIT) {
                ipc_pm->pending_hpda_update = true;
                dev_dbg(ipc_pm->dev,
                        "Pend HPDA update set. Host PM_State: %d identifier:%d",
                        ipc_pm->host_pm_state, identifier);
                return;
        }

        if (!ipc_pm_trigger(ipc_pm, IPC_PM_UNIT_IRQ, true)) {
                ipc_pm->pending_hpda_update = true;
                dev_dbg(ipc_pm->dev, "Pending HPDA update set. identifier:%d",
                        identifier);
                return;
        }
        ipc_pm->pending_hpda_update = false;

        /* Trigger the irq towards CP */
        ipc_cp_irq_hpda_update(ipc_pm->pcie, identifier);

        ipc_pm_trigger(ipc_pm, IPC_PM_UNIT_IRQ, false);
}

/* Wake up the device if it is in low power mode. */
static bool ipc_pm_link_activate(struct iosm_pm *ipc_pm)
{
        if (ipc_pm->cp_state == IPC_MEM_DEV_PM_ACTIVE)
                return true;

        if (ipc_pm->cp_state == IPC_MEM_DEV_PM_SLEEP) {
                if (ipc_pm->ap_state == IPC_MEM_DEV_PM_SLEEP) {
                        /* Wake up the device. */
                        ipc_cp_irq_sleep_control(ipc_pm->pcie,
                                                 IPC_MEM_DEV_PM_WAKEUP);
                        ipc_pm->ap_state = IPC_MEM_DEV_PM_ACTIVE_WAIT;

                        goto not_active;
                }

                if (ipc_pm->ap_state == IPC_MEM_DEV_PM_ACTIVE_WAIT)
                        goto not_active;

                return true;
        }

not_active:
        /* link is not ready */
        return false;
}

bool ipc_pm_wait_for_device_active(struct iosm_pm *ipc_pm)
{
        bool ret_val = false;

        if (ipc_pm->ap_state != IPC_MEM_DEV_PM_ACTIVE) {
                /* Complete all memory stores before setting bit */
                smp_mb__before_atomic();

                /* Wait for IPC_PM_ACTIVE_TIMEOUT_MS for Device sleep state
                 * machine to enter ACTIVE state.
                 */
                set_bit(0, &ipc_pm->host_sleep_pend);

                /* Complete all memory stores after setting bit */
                smp_mb__after_atomic();

                if (!wait_for_completion_interruptible_timeout
                   (&ipc_pm->host_sleep_complete,
                    msecs_to_jiffies(IPC_PM_ACTIVE_TIMEOUT_MS))) {
                        dev_err(ipc_pm->dev,
                                "PM timeout. Expected State:%d. Actual: %d",
                                IPC_MEM_DEV_PM_ACTIVE, ipc_pm->ap_state);
                        goto  active_timeout;
                }
        }

        ret_val = true;
active_timeout:
        /* Complete all memory stores before clearing bit */
        smp_mb__before_atomic();

        /* Reset the atomic variable in any case as device sleep
         * state machine change is no longer of interest.
         */
        clear_bit(0, &ipc_pm->host_sleep_pend);

        /* Complete all memory stores after clearing bit */
        smp_mb__after_atomic();

        return ret_val;
}

static void ipc_pm_on_link_sleep(struct iosm_pm *ipc_pm)
{
        /* pending sleep ack and all conditions are cleared
         * -> signal SLEEP__ACK to CP
         */
        ipc_pm->cp_state = IPC_MEM_DEV_PM_SLEEP;
        ipc_pm->ap_state = IPC_MEM_DEV_PM_SLEEP;

        ipc_cp_irq_sleep_control(ipc_pm->pcie, IPC_MEM_DEV_PM_SLEEP);
}

static void ipc_pm_on_link_wake(struct iosm_pm *ipc_pm, bool ack)
{
        ipc_pm->ap_state = IPC_MEM_DEV_PM_ACTIVE;

        if (ack) {
                ipc_pm->cp_state = IPC_MEM_DEV_PM_ACTIVE;

                ipc_cp_irq_sleep_control(ipc_pm->pcie, IPC_MEM_DEV_PM_ACTIVE);

                /* check the consume state !!! */
                if (test_bit(CONSUME_STATE, &ipc_pm->host_sleep_pend))
                        complete(&ipc_pm->host_sleep_complete);
        }

        /* Check for pending HPDA update.
         * Pending HP update could be because of sending message was
         * put on hold due to Device sleep state or due to TD update
         * which could be because of Device Sleep and Host Sleep
         * states.
         */
        if (ipc_pm->pending_hpda_update &&
            ipc_pm->host_pm_state == IPC_MEM_HOST_PM_ACTIVE)
                ipc_pm_signal_hpda_doorbell(ipc_pm, IPC_HP_PM_TRIGGER, true);
}

bool ipc_pm_trigger(struct iosm_pm *ipc_pm, enum ipc_pm_unit unit, bool active)
{
        union ipc_pm_cond old_cond;
        union ipc_pm_cond new_cond;
        bool link_active;

        /* Save the current D3 state. */
        new_cond = ipc_pm->pm_cond;
        old_cond = ipc_pm->pm_cond;

        /* Calculate the power state only in the runtime phase. */
        switch (unit) {
        case IPC_PM_UNIT_IRQ: /* CP irq */
                new_cond.irq = active;
                break;

        case IPC_PM_UNIT_LINK: /* Device link state. */
                new_cond.link = active;
                break;

        case IPC_PM_UNIT_HS: /* Host sleep trigger requires Link. */
                new_cond.hs = active;
                break;

        default:
                break;
        }

        /* Something changed ? */
        if (old_cond.raw == new_cond.raw) {
                /* Stay in the current PM state. */
                link_active = old_cond.link == IPC_PM_ACTIVE;
                goto ret;
        }

        ipc_pm->pm_cond = new_cond;

        if (new_cond.link)
                ipc_pm_on_link_wake(ipc_pm, unit == IPC_PM_UNIT_LINK);
        else if (unit == IPC_PM_UNIT_LINK)
                ipc_pm_on_link_sleep(ipc_pm);

        if (old_cond.link == IPC_PM_SLEEP && new_cond.raw) {
                link_active = ipc_pm_link_activate(ipc_pm);
                goto ret;
        }

        link_active = old_cond.link == IPC_PM_ACTIVE;

ret:
        return link_active;
}

bool ipc_pm_prepare_host_sleep(struct iosm_pm *ipc_pm)
{
        /* suspend not allowed if host_pm_state is not IPC_MEM_HOST_PM_ACTIVE */
        if (ipc_pm->host_pm_state != IPC_MEM_HOST_PM_ACTIVE) {
                dev_err(ipc_pm->dev, "host_pm_state=%d\tExpected to be: %d",
                        ipc_pm->host_pm_state, IPC_MEM_HOST_PM_ACTIVE);
                return false;
        }

        ipc_pm->host_pm_state = IPC_MEM_HOST_PM_SLEEP_WAIT_D3;

        return true;
}

bool ipc_pm_prepare_host_active(struct iosm_pm *ipc_pm)
{
        if (ipc_pm->host_pm_state != IPC_MEM_HOST_PM_SLEEP) {
                dev_err(ipc_pm->dev, "host_pm_state=%d\tExpected to be: %d",
                        ipc_pm->host_pm_state, IPC_MEM_HOST_PM_SLEEP);
                return false;
        }

        /* Sending Sleep Exit message to CP. Update the state */
        ipc_pm->host_pm_state = IPC_MEM_HOST_PM_ACTIVE_WAIT;

        return true;
}

void ipc_pm_set_s2idle_sleep(struct iosm_pm *ipc_pm, bool sleep)
{
        if (sleep) {
                ipc_pm->ap_state = IPC_MEM_DEV_PM_SLEEP;
                ipc_pm->cp_state = IPC_MEM_DEV_PM_SLEEP;
                ipc_pm->device_sleep_notification = IPC_MEM_DEV_PM_SLEEP;
        } else {
                ipc_pm->ap_state = IPC_MEM_DEV_PM_ACTIVE;
                ipc_pm->cp_state = IPC_MEM_DEV_PM_ACTIVE;
                ipc_pm->device_sleep_notification = IPC_MEM_DEV_PM_ACTIVE;
                ipc_pm->pm_cond.link = IPC_PM_ACTIVE;
        }
}

bool ipc_pm_dev_slp_notification(struct iosm_pm *ipc_pm, u32 cp_pm_req)
{
        if (cp_pm_req == ipc_pm->device_sleep_notification)
                return false;

        ipc_pm->device_sleep_notification = cp_pm_req;

        /* Evaluate the PM request. */
        switch (ipc_pm->cp_state) {
        case IPC_MEM_DEV_PM_ACTIVE:
                switch (cp_pm_req) {
                case IPC_MEM_DEV_PM_ACTIVE:
                        break;

                case IPC_MEM_DEV_PM_SLEEP:
                        /* Inform the PM that the device link can go down. */
                        ipc_pm_trigger(ipc_pm, IPC_PM_UNIT_LINK, false);
                        return true;

                default:
                        dev_err(ipc_pm->dev,
                                "loc-pm=%d active: confused req-pm=%d",
                                ipc_pm->cp_state, cp_pm_req);
                        break;
                }
                break;

        case IPC_MEM_DEV_PM_SLEEP:
                switch (cp_pm_req) {
                case IPC_MEM_DEV_PM_ACTIVE:
                        /* Inform the PM that the device link is active. */
                        ipc_pm_trigger(ipc_pm, IPC_PM_UNIT_LINK, true);
                        break;

                case IPC_MEM_DEV_PM_SLEEP:
                        break;

                default:
                        dev_err(ipc_pm->dev,
                                "loc-pm=%d sleep: confused req-pm=%d",
                                ipc_pm->cp_state, cp_pm_req);
                        break;
                }
                break;

        default:
                dev_err(ipc_pm->dev, "confused loc-pm=%d, req-pm=%d",
                        ipc_pm->cp_state, cp_pm_req);
                break;
        }

        return false;
}

void ipc_pm_init(struct iosm_protocol *ipc_protocol)
{
        struct iosm_imem *ipc_imem = ipc_protocol->imem;
        struct iosm_pm *ipc_pm = &ipc_protocol->pm;

        ipc_pm->pcie = ipc_imem->pcie;
        ipc_pm->dev = ipc_imem->dev;

        ipc_pm->pm_cond.irq = IPC_PM_SLEEP;
        ipc_pm->pm_cond.hs = IPC_PM_SLEEP;
        ipc_pm->pm_cond.link = IPC_PM_ACTIVE;

        ipc_pm->cp_state = IPC_MEM_DEV_PM_ACTIVE;
        ipc_pm->ap_state = IPC_MEM_DEV_PM_ACTIVE;
        ipc_pm->host_pm_state = IPC_MEM_HOST_PM_ACTIVE;

        /* Create generic wait-for-completion handler for Host Sleep
         * and device sleep coordination.
         */
        init_completion(&ipc_pm->host_sleep_complete);

        /* Complete all memory stores before clearing bit */
        smp_mb__before_atomic();

        clear_bit(0, &ipc_pm->host_sleep_pend);

        /* Complete all memory stores after clearing bit */
        smp_mb__after_atomic();
}

void ipc_pm_deinit(struct iosm_protocol *proto)
{
        struct iosm_pm *ipc_pm = &proto->pm;

        complete(&ipc_pm->host_sleep_complete);
}