root/drivers/net/wireless/intel/iwlwifi/mld/power.c
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/*
 * Copyright (C) 2024-2026 Intel Corporation
 */
#include <net/mac80211.h>

#include "mld.h"
#include "hcmd.h"
#include "power.h"
#include "iface.h"
#include "link.h"
#include "constants.h"

int iwl_mld_update_device_power(struct iwl_mld *mld, bool d3)
{
        struct iwl_device_power_cmd cmd = {};

        if (iwlmld_mod_params.power_scheme != IWL_POWER_SCHEME_CAM)
                cmd.flags |=
                        cpu_to_le16(DEVICE_POWER_FLAGS_POWER_SAVE_ENA_MSK);

        if (d3)
                cmd.flags |=
                        cpu_to_le16(DEVICE_POWER_FLAGS_NO_SLEEP_TILL_D3_MSK);

        IWL_DEBUG_POWER(mld,
                        "Sending device power command with flags = 0x%X\n",
                        cmd.flags);

        return iwl_mld_send_cmd_pdu(mld, POWER_TABLE_CMD, &cmd);
}

int iwl_mld_enable_beacon_filter(struct iwl_mld *mld,
                                 const struct ieee80211_bss_conf *link_conf,
                                 bool d3)
{
        struct iwl_beacon_filter_cmd cmd = {
                IWL_BF_CMD_CONFIG_DEFAULTS,
                .bf_enable_beacon_filter = cpu_to_le32(1),
                .ba_enable_beacon_abort = cpu_to_le32(1),
        };

        if (ieee80211_vif_type_p2p(link_conf->vif) != NL80211_IFTYPE_STATION)
                return 0;

#ifdef CONFIG_IWLWIFI_DEBUGFS
        if (iwl_mld_vif_from_mac80211(link_conf->vif)->disable_bf)
                return 0;
#endif

        if (link_conf->cqm_rssi_thold) {
                cmd.bf_energy_delta =
                        cpu_to_le32(link_conf->cqm_rssi_hyst);
                /* fw uses an absolute value for this */
                cmd.bf_roaming_state =
                        cpu_to_le32(-link_conf->cqm_rssi_thold);
        }

        if (d3)
                cmd.ba_escape_timer = cpu_to_le32(IWL_BA_ESCAPE_TIMER_D3);

        return iwl_mld_send_cmd_pdu(mld, REPLY_BEACON_FILTERING_CMD,
                                    &cmd);
}

int iwl_mld_disable_beacon_filter(struct iwl_mld *mld,
                                  struct ieee80211_vif *vif)
{
        struct iwl_beacon_filter_cmd cmd = {};

        if (ieee80211_vif_type_p2p(vif) != NL80211_IFTYPE_STATION)
                return 0;

        return iwl_mld_send_cmd_pdu(mld, REPLY_BEACON_FILTERING_CMD,
                                    &cmd);
}

static bool iwl_mld_power_is_radar(struct iwl_mld *mld,
                                   const struct ieee80211_bss_conf *link_conf)
{
        const struct ieee80211_chanctx_conf *chanctx_conf;

        chanctx_conf = wiphy_dereference(mld->wiphy, link_conf->chanctx_conf);

        if (WARN_ON(!chanctx_conf))
                return false;

        return chanctx_conf->def.chan->flags & IEEE80211_CHAN_RADAR;
}

static void iwl_mld_power_configure_uapsd_v2(struct iwl_mld *mld,
                                             struct iwl_mld_link *link,
                                             struct iwl_mac_power_cmd_v2 *cmd,
                                             bool ps_poll)
{
        bool tid_found = false;

        cmd->rx_data_timeout_uapsd =
                cpu_to_le32(IWL_MLD_UAPSD_RX_DATA_TIMEOUT);
        cmd->tx_data_timeout_uapsd =
                cpu_to_le32(IWL_MLD_UAPSD_TX_DATA_TIMEOUT);

         /* set advanced pm flag with no uapsd ACs to enable ps-poll */
        if (ps_poll) {
                cmd->flags |= cpu_to_le16(POWER_FLAGS_ADVANCE_PM_ENA_MSK);
                return;
        }

        for (enum ieee80211_ac_numbers ac = IEEE80211_AC_VO;
             ac <= IEEE80211_AC_BK;
             ac++) {
                if (!link->queue_params[ac].uapsd)
                        continue;

                cmd->flags |=
                        cpu_to_le16(POWER_FLAGS_ADVANCE_PM_ENA_MSK |
                                    POWER_FLAGS_UAPSD_MISBEHAVING_ENA_MSK);

                cmd->uapsd_ac_flags |= BIT(ac);

                /* QNDP TID - the highest TID with no admission control */
                if (!tid_found && !link->queue_params[ac].acm) {
                        tid_found = true;
                        switch (ac) {
                        case IEEE80211_AC_VO:
                                cmd->qndp_tid = 6;
                                break;
                        case IEEE80211_AC_VI:
                                cmd->qndp_tid = 5;
                                break;
                        case IEEE80211_AC_BE:
                                cmd->qndp_tid = 0;
                                break;
                        case IEEE80211_AC_BK:
                                cmd->qndp_tid = 1;
                                break;
                        }
                }
        }

        if (cmd->uapsd_ac_flags == (BIT(IEEE80211_AC_VO) |
                                    BIT(IEEE80211_AC_VI) |
                                    BIT(IEEE80211_AC_BE) |
                                    BIT(IEEE80211_AC_BK))) {
                cmd->flags |= cpu_to_le16(POWER_FLAGS_SNOOZE_ENA_MSK);
                cmd->snooze_interval = cpu_to_le16(IWL_MLD_PS_SNOOZE_INTERVAL);
                cmd->snooze_window = cpu_to_le16(IWL_MLD_PS_SNOOZE_WINDOW);
        }

        cmd->uapsd_max_sp = mld->hw->uapsd_max_sp_len;
}

static void iwl_mld_power_configure_uapsd(struct iwl_mld *mld,
                                          struct iwl_mld_link *link,
                                          struct iwl_mac_power_cmd *cmd,
                                          bool ps_poll)
{
        bool tid_found = false;

        /* set advanced pm flag with no uapsd ACs to enable ps-poll */
        if (ps_poll) {
                cmd->flags |= cpu_to_le16(POWER_FLAGS_ADVANCE_PM_ENA_MSK);
                return;
        }

        for (enum ieee80211_ac_numbers ac = IEEE80211_AC_VO;
             ac <= IEEE80211_AC_BK;
             ac++) {
                if (!link->queue_params[ac].uapsd)
                        continue;

                cmd->flags |=
                        cpu_to_le16(POWER_FLAGS_ADVANCE_PM_ENA_MSK);
                cmd->uapsd_ac_flags |= BIT(ac);

                /* QNDP TID - the highest TID with no admission control */
                if (!tid_found && !link->queue_params[ac].acm) {
                        tid_found = true;
                        switch (ac) {
                        case IEEE80211_AC_VO:
                                cmd->qndp_tid = 6;
                                break;
                        case IEEE80211_AC_VI:
                                cmd->qndp_tid = 5;
                                break;
                        case IEEE80211_AC_BE:
                                cmd->qndp_tid = 0;
                                break;
                        case IEEE80211_AC_BK:
                                cmd->qndp_tid = 1;
                                break;
                        }
                }
        }
}

static void
iwl_mld_power_config_skip_dtim(struct iwl_mld *mld,
                               const struct ieee80211_bss_conf *link_conf,
                               u8 *skip_dtim_periods, __le16 *flags)
{
        unsigned int dtimper_tu;
        unsigned int dtimper;
        unsigned int skip;

        dtimper = link_conf->dtim_period ?: 1;
        dtimper_tu = dtimper * link_conf->beacon_int;

        if (dtimper >= 10 || iwl_mld_power_is_radar(mld, link_conf))
                return;

        if (WARN_ON(!dtimper_tu))
                return;

        /* configure skip over dtim up to 900 TU DTIM interval */
        skip = max_t(int, 1, 900 / dtimper_tu);

        *skip_dtim_periods = skip;
        *flags |= cpu_to_le16(POWER_FLAGS_SKIP_OVER_DTIM_MSK);
}

#define POWER_KEEP_ALIVE_PERIOD_SEC    25
static void iwl_mld_power_build_cmd_v2(struct iwl_mld *mld,
                                       struct ieee80211_vif *vif,
                                       struct iwl_mac_power_cmd_v2 *cmd,
                                       bool d3)
{
        int dtimper, bi;
        int keep_alive;
        struct iwl_mld_vif *mld_vif = iwl_mld_vif_from_mac80211(vif);
        struct ieee80211_bss_conf *link_conf = &vif->bss_conf;
        struct iwl_mld_link *link = &mld_vif->deflink;
        bool ps_poll = false;

        cmd->id_and_color = cpu_to_le32(mld_vif->fw_id);

        if (ieee80211_vif_is_mld(vif)) {
                int link_id;

                if (WARN_ON(!vif->active_links))
                        return;

                /* The firmware consumes one single configuration for the vif
                 * and can't differentiate between links, just pick the lowest
                 * link_id's configuration and use that.
                 */
                link_id = __ffs(vif->active_links);
                link_conf = link_conf_dereference_check(vif, link_id);
                link = iwl_mld_link_dereference_check(mld_vif, link_id);

                if (WARN_ON(!link_conf || !link))
                        return;
        }
        dtimper = link_conf->dtim_period;
        bi = link_conf->beacon_int;

        /* Regardless of power management state the driver must set
         * keep alive period. FW will use it for sending keep alive NDPs
         * immediately after association. Check that keep alive period
         * is at least 3 * DTIM
         */
        keep_alive = DIV_ROUND_UP(ieee80211_tu_to_usec(3 * dtimper * bi),
                                  USEC_PER_SEC);
        keep_alive = max(keep_alive, POWER_KEEP_ALIVE_PERIOD_SEC);
        cmd->keep_alive_seconds = cpu_to_le16(keep_alive);

        if (iwlmld_mod_params.power_scheme != IWL_POWER_SCHEME_CAM)
                cmd->flags |= cpu_to_le16(POWER_FLAGS_POWER_SAVE_ENA_MSK);

        if (!vif->cfg.ps || iwl_mld_tdls_sta_count(mld) > 0)
                return;

        cmd->flags |= cpu_to_le16(POWER_FLAGS_POWER_MANAGEMENT_ENA_MSK);
        cmd->flags |= cpu_to_le16(POWER_FLAGS_ENABLE_SMPS_MSK);

        /* firmware supports LPRX for beacons at rate 1 Mbps or 6 Mbps only */
        if (link_conf->beacon_rate &&
            (link_conf->beacon_rate->bitrate == 10 ||
             link_conf->beacon_rate->bitrate == 60)) {
                cmd->flags |= cpu_to_le16(POWER_FLAGS_LPRX_ENA_MSK);
                cmd->lprx_rssi_threshold = POWER_LPRX_RSSI_THRESHOLD;
        }

        if (d3) {
                iwl_mld_power_config_skip_dtim(mld, link_conf,
                                               &cmd->skip_dtim_periods,
                                               &cmd->flags);
                cmd->rx_data_timeout =
                        cpu_to_le32(IWL_MLD_WOWLAN_PS_RX_DATA_TIMEOUT);
                cmd->tx_data_timeout =
                        cpu_to_le32(IWL_MLD_WOWLAN_PS_TX_DATA_TIMEOUT);
        } else if (iwl_mld_vif_low_latency(mld_vif) && vif->p2p) {
                cmd->tx_data_timeout =
                        cpu_to_le32(IWL_MLD_SHORT_PS_TX_DATA_TIMEOUT);
                cmd->rx_data_timeout =
                        cpu_to_le32(IWL_MLD_SHORT_PS_RX_DATA_TIMEOUT);
        } else {
                cmd->rx_data_timeout =
                        cpu_to_le32(IWL_MLD_DEFAULT_PS_RX_DATA_TIMEOUT);
                cmd->tx_data_timeout =
                        cpu_to_le32(IWL_MLD_DEFAULT_PS_TX_DATA_TIMEOUT);
        }

        /* uAPSD is only enabled for specific certifications. For those cases,
         * mac80211 will allow uAPSD. Always call iwl_mld_power_configure_uapsd
         * which will look at what mac80211 is saying.
         */
#ifdef CONFIG_IWLWIFI_DEBUGFS
        ps_poll = mld_vif->use_ps_poll;
#endif
        iwl_mld_power_configure_uapsd_v2(mld, link, cmd, ps_poll);
}

static void iwl_mld_power_build_cmd(struct iwl_mld *mld,
                                    struct ieee80211_vif *vif,
                                    struct iwl_mac_power_cmd *cmd,
                                    bool d3)
{
        int dtimper, bi;
        int keep_alive;
        struct iwl_mld_vif *mld_vif = iwl_mld_vif_from_mac80211(vif);
        struct ieee80211_bss_conf *link_conf = &vif->bss_conf;
        struct iwl_mld_link *link = &mld_vif->deflink;
        bool ps_poll = false;
        __le32 fw_id = cpu_to_le32(mld_vif->fw_id);

        if (ieee80211_vif_is_mld(vif)) {
                int link_id;

                if (WARN_ON(!vif->active_links))
                        return;

                /* The firmware consumes one single configuration for the vif
                 * and can't differentiate between links, just pick the lowest
                 * link_id's configuration and use that.
                 */
                link_id = __ffs(vif->active_links);
                link_conf = link_conf_dereference_check(vif, link_id);
                link = iwl_mld_link_dereference_check(mld_vif, link_id);

                if (WARN_ON(!link_conf || !link))
                        return;
        }
        dtimper = link_conf->dtim_period;
        bi = link_conf->beacon_int;

        /* Regardless of power management state the driver must set
         * keep alive period. FW will use it for sending keep alive NDPs
         * immediately after association. Check that keep alive period
         * is at least 3 * DTIM
         */
        keep_alive = DIV_ROUND_UP(ieee80211_tu_to_usec(3 * dtimper * bi),
                                  USEC_PER_SEC);
        keep_alive = max(keep_alive, POWER_KEEP_ALIVE_PERIOD_SEC);

        cmd->id_and_color = fw_id;
        cmd->keep_alive_seconds = cpu_to_le16(keep_alive);

        if (iwlmld_mod_params.power_scheme != IWL_POWER_SCHEME_CAM)
                cmd->flags |= cpu_to_le16(POWER_FLAGS_POWER_SAVE_ENA_MSK);

        if (vif->cfg.ps && iwl_mld_tdls_sta_count(mld) == 0) {
                cmd->flags |= cpu_to_le16(POWER_FLAGS_POWER_MANAGEMENT_ENA_MSK);
                cmd->flags |= cpu_to_le16(POWER_FLAGS_ENABLE_SMPS_MSK);

                /* firmware supports LPRX for beacons at rate 1 Mbps or
                 * 6 Mbps only
                 */
                if (link_conf->beacon_rate &&
                    (link_conf->beacon_rate->bitrate == 10 ||
                     link_conf->beacon_rate->bitrate == 60)) {
                        cmd->flags |= cpu_to_le16(POWER_FLAGS_LPRX_ENA_MSK);
                        cmd->lprx_rssi_threshold = POWER_LPRX_RSSI_THRESHOLD;
                }
        }

        if (d3) {
                iwl_mld_power_config_skip_dtim(mld, link_conf,
                                               &cmd->skip_dtim_periods,
                                               &cmd->flags);
                cmd->rx_data_timeout =
                        cpu_to_le32(IWL_MLD_WOWLAN_PS_RX_DATA_TIMEOUT);
                cmd->tx_data_timeout =
                        cpu_to_le32(IWL_MLD_WOWLAN_PS_TX_DATA_TIMEOUT);
        } else if (iwl_mld_vif_low_latency(mld_vif) && vif->p2p) {
                cmd->tx_data_timeout =
                        cpu_to_le32(IWL_MLD_SHORT_PS_TX_DATA_TIMEOUT);
                cmd->rx_data_timeout =
                        cpu_to_le32(IWL_MLD_SHORT_PS_RX_DATA_TIMEOUT);
        } else {
                cmd->rx_data_timeout =
                        cpu_to_le32(IWL_MLD_DEFAULT_PS_RX_DATA_TIMEOUT);
                cmd->tx_data_timeout =
                        cpu_to_le32(IWL_MLD_DEFAULT_PS_TX_DATA_TIMEOUT);
        }

#ifdef CONFIG_IWLWIFI_DEBUGFS
        ps_poll = mld_vif->use_ps_poll;
#endif
        iwl_mld_power_configure_uapsd(mld, link, cmd, ps_poll);
}

int iwl_mld_update_mac_power(struct iwl_mld *mld, struct ieee80211_vif *vif,
                             bool d3)
{
        int cmd_ver = iwl_fw_lookup_cmd_ver(mld->fw, MAC_PM_POWER_TABLE, 0);

        if (cmd_ver >= 3) {
                struct iwl_mac_power_cmd cmd = {};

                iwl_mld_power_build_cmd(mld, vif, &cmd, d3);
                return iwl_mld_send_cmd_with_flags_pdu(mld,
                                                       MAC_PM_POWER_TABLE, 0,
                                                       &cmd, sizeof(cmd));
        } else {
                struct iwl_mac_power_cmd_v2 cmd = {};

                iwl_mld_power_build_cmd_v2(mld, vif, &cmd, d3);
                return iwl_mld_send_cmd_with_flags_pdu(mld,
                                                       MAC_PM_POWER_TABLE, 0,
                                                       &cmd, sizeof(cmd));
        }
}

static void
iwl_mld_tpe_sta_cmd_data(struct iwl_txpower_constraints_cmd *cmd,
                         const struct ieee80211_bss_conf *link)
{
        u8 i;

        /* NOTE: the 0 here is IEEE80211_TPE_CAT_6GHZ_DEFAULT,
         * we fully ignore IEEE80211_TPE_CAT_6GHZ_SUBORDINATE
         */

        BUILD_BUG_ON(ARRAY_SIZE(cmd->psd_pwr) !=
                     ARRAY_SIZE(link->tpe.psd_local[0].power));

        /* if not valid, mac80211 puts default (max value) */
        for (i = 0; i < ARRAY_SIZE(cmd->psd_pwr); i++)
                cmd->psd_pwr[i] = min(link->tpe.psd_local[0].power[i],
                                      link->tpe.psd_reg_client[0].power[i]);

        BUILD_BUG_ON(ARRAY_SIZE(cmd->eirp_pwr) !=
                     ARRAY_SIZE(link->tpe.max_local[0].power));

        for (i = 0; i < ARRAY_SIZE(cmd->eirp_pwr); i++)
                cmd->eirp_pwr[i] = min(link->tpe.max_local[0].power[i],
                                       link->tpe.max_reg_client[0].power[i]);
}

static int
iwl_mld_set_ap_power_type(struct iwl_txpower_constraints_cmd *cmd,
                          struct ieee80211_vif *vif,
                          struct ieee80211_bss_conf *link)
{
        if (vif->type == NL80211_IFTYPE_AP) {
                cmd->ap_type = cpu_to_le16(IWL_6GHZ_AP_TYPE_VLP);
                return 0;
        }

        switch (link->power_type) {
        case IEEE80211_REG_LPI_AP:
                cmd->ap_type = cpu_to_le16(IWL_6GHZ_AP_TYPE_LPI);
                break;
        case IEEE80211_REG_SP_AP:
                cmd->ap_type = cpu_to_le16(IWL_6GHZ_AP_TYPE_SP);
                break;
        case IEEE80211_REG_VLP_AP:
                cmd->ap_type = cpu_to_le16(IWL_6GHZ_AP_TYPE_VLP);
                break;
        default:
                return -EINVAL;
        }

        return 0;
}

void
iwl_mld_send_ap_tx_power_constraint_cmd(struct iwl_mld *mld,
                                        struct ieee80211_vif *vif,
                                        struct ieee80211_bss_conf *link)
{
        struct iwl_txpower_constraints_cmd cmd = {};
        struct iwl_mld_link *mld_link = iwl_mld_link_from_mac80211(link);
        int ret;

        lockdep_assert_wiphy(mld->wiphy);

        if (!mld_link->active && vif->type != NL80211_IFTYPE_AP)
                return;

        if (link->chanreq.oper.chan->band != NL80211_BAND_6GHZ)
                return;

        cmd.link_id = cpu_to_le16(mld_link->fw_id);
        memset(cmd.psd_pwr, DEFAULT_TPE_TX_POWER, sizeof(cmd.psd_pwr));
        memset(cmd.eirp_pwr, DEFAULT_TPE_TX_POWER, sizeof(cmd.eirp_pwr));

        if (iwl_mld_set_ap_power_type(&cmd, vif, link))
                return;

        if (vif->type != NL80211_IFTYPE_AP)
                iwl_mld_tpe_sta_cmd_data(&cmd, link);

        IWL_DEBUG_POWER(mld, "AP power type: %d\n", le16_to_cpu(cmd.ap_type));
        ret = iwl_mld_send_cmd_pdu(mld,
                                   WIDE_ID(PHY_OPS_GROUP,
                                           AP_TX_POWER_CONSTRAINTS_CMD),
                                   &cmd);
        if (ret)
                IWL_ERR(mld,
                        "failed to send AP_TX_POWER_CONSTRAINTS_CMD (%d)\n",
                        ret);
}

int iwl_mld_set_tx_power(struct iwl_mld *mld,
                         struct ieee80211_bss_conf *link_conf,
                         s16 tx_power)
{
        u32 cmd_id = REDUCE_TX_POWER_CMD;
        struct iwl_mld_link *mld_link = iwl_mld_link_from_mac80211(link_conf);
        u16 u_tx_power = tx_power == IWL_DEFAULT_MAX_TX_POWER ?
                IWL_DEV_MAX_TX_POWER : 8 * tx_power;
        struct iwl_dev_tx_power_cmd cmd = {
                .common.set_mode = cpu_to_le32(IWL_TX_POWER_MODE_SET_LINK),
                .common.pwr_restriction = cpu_to_le16(u_tx_power),
        };
        int len = sizeof(cmd.common) + sizeof(cmd.v11);

        if (iwl_fw_lookup_cmd_ver(mld->fw, cmd_id, 10) == 10)
                len = sizeof(cmd.common) + sizeof(cmd.v10);

        if (WARN_ON(!mld_link))
                return -ENODEV;

        cmd.common.link_id = cpu_to_le32(mld_link->fw_id);

        return iwl_mld_send_cmd_pdu(mld, cmd_id, &cmd, len);
}