root/drivers/net/wireless/intel/iwlwifi/mld/thermal.c
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/*
 * Copyright (C) 2024-2025 Intel Corporation
 */
#ifdef CONFIG_THERMAL
#include <linux/sort.h>
#include <linux/thermal.h>
#endif

#include "fw/api/phy.h"

#include "thermal.h"
#include "mld.h"
#include "hcmd.h"

#define IWL_MLD_NUM_CTDP_STEPS          20
#define IWL_MLD_MIN_CTDP_BUDGET_MW      150

#define IWL_MLD_CT_KILL_DURATION (5 * HZ)

void iwl_mld_handle_ct_kill_notif(struct iwl_mld *mld,
                                  struct iwl_rx_packet *pkt)
{
        const struct ct_kill_notif *notif = (const void *)pkt->data;

        IWL_ERR(mld,
                "CT Kill notification: temp = %d, DTS = 0x%x, Scheme 0x%x - Enter CT Kill\n",
                le16_to_cpu(notif->temperature), notif->dts,
                notif->scheme);

        iwl_mld_set_ctkill(mld, true);

        wiphy_delayed_work_queue(mld->wiphy, &mld->ct_kill_exit_wk,
                                 round_jiffies_relative(IWL_MLD_CT_KILL_DURATION));
}

static void iwl_mld_exit_ctkill(struct wiphy *wiphy, struct wiphy_work *wk)
{
        struct iwl_mld *mld;

        mld = container_of(wk, struct iwl_mld, ct_kill_exit_wk.work);

        IWL_ERR(mld, "Exit CT Kill\n");
        iwl_mld_set_ctkill(mld, false);
}

void iwl_mld_handle_temp_notif(struct iwl_mld *mld, struct iwl_rx_packet *pkt)
{
        const struct iwl_dts_measurement_notif *notif =
                (const void *)pkt->data;
        int temp;
        u32 ths_crossed;

        temp = le32_to_cpu(notif->temp);

        /* shouldn't be negative, but since it's s32, make sure it isn't */
        if (IWL_FW_CHECK(mld, temp < 0, "negative temperature %d\n", temp))
                return;

        ths_crossed = le32_to_cpu(notif->threshold_idx);

        /* 0xFF in ths_crossed means the notification is not related
         * to a trip, so we can ignore it here.
         */
        if (ths_crossed == 0xFF)
                return;

        IWL_DEBUG_TEMP(mld, "Temp = %d Threshold crossed = %d\n",
                       temp, ths_crossed);

        if (IWL_FW_CHECK(mld, ths_crossed >= IWL_MAX_DTS_TRIPS,
                         "bad threshold: %d\n", ths_crossed))
                return;

#ifdef CONFIG_THERMAL
        if (mld->tzone)
                thermal_zone_device_update(mld->tzone, THERMAL_TRIP_VIOLATED);
#endif /* CONFIG_THERMAL */
}

#ifdef CONFIG_THERMAL
static int iwl_mld_get_temp(struct iwl_mld *mld, s32 *temp)
{
        struct iwl_host_cmd cmd = {
                .id = WIDE_ID(PHY_OPS_GROUP, CMD_DTS_MEASUREMENT_TRIGGER_WIDE),
                .flags = CMD_WANT_SKB,
        };
        const struct iwl_dts_measurement_resp *resp;
        int ret;

        lockdep_assert_wiphy(mld->wiphy);

        ret = iwl_mld_send_cmd(mld, &cmd);
        if (ret) {
                IWL_ERR(mld,
                        "Failed to send the temperature measurement command (err=%d)\n",
                        ret);
                return ret;
        }

        if (iwl_rx_packet_payload_len(cmd.resp_pkt) < sizeof(*resp)) {
                IWL_ERR(mld,
                        "Failed to get a valid response to DTS measurement\n");
                ret = -EIO;
                goto free_resp;
        }

        resp = (const void *)cmd.resp_pkt->data;
        *temp = le32_to_cpu(resp->temp);

        IWL_DEBUG_TEMP(mld,
                       "Got temperature measurement response: temp=%d\n",
                       *temp);

free_resp:
        iwl_free_resp(&cmd);
        return ret;
}

static int compare_temps(const void *a, const void *b)
{
        return ((s16)le16_to_cpu(*(const __le16 *)a) -
                (s16)le16_to_cpu(*(const __le16 *)b));
}

struct iwl_trip_walk_data {
        __le16 *thresholds;
        int count;
};

static int iwl_trip_temp_iter(struct thermal_trip *trip, void *arg)
{
        struct iwl_trip_walk_data *twd = arg;

        if (trip->temperature == THERMAL_TEMP_INVALID)
                return 0;

        twd->thresholds[twd->count++] =
                cpu_to_le16((s16)(trip->temperature / 1000));
        return 0;
}
#endif

int iwl_mld_config_temp_report_ths(struct iwl_mld *mld)
{
        struct temp_report_ths_cmd cmd = {0};
        int ret;
#ifdef CONFIG_THERMAL
        struct iwl_trip_walk_data twd = {
                .thresholds = cmd.thresholds,
                .count = 0
        };

        if (!mld->tzone)
                goto send;

        /* The thermal core holds an array of temperature trips that are
         * unsorted and uncompressed, the FW should get it compressed and
         * sorted.
         */

        /* compress trips to cmd array, remove uninitialized values*/
        for_each_thermal_trip(mld->tzone, iwl_trip_temp_iter, &twd);

        cmd.num_temps = cpu_to_le32(twd.count);
        if (twd.count)
                sort(cmd.thresholds, twd.count, sizeof(s16),
                     compare_temps, NULL);

send:
#endif
        lockdep_assert_wiphy(mld->wiphy);

        ret = iwl_mld_send_cmd_pdu(mld, WIDE_ID(PHY_OPS_GROUP,
                                                TEMP_REPORTING_THRESHOLDS_CMD),
                                   &cmd);
        if (ret)
                IWL_ERR(mld, "TEMP_REPORT_THS_CMD command failed (err=%d)\n",
                        ret);

        return ret;
}

#ifdef CONFIG_THERMAL
static int iwl_mld_tzone_get_temp(struct thermal_zone_device *device,
                                  int *temperature)
{
        struct iwl_mld *mld = thermal_zone_device_priv(device);
        int temp;
        int ret = 0;

        wiphy_lock(mld->wiphy);

        if (!mld->fw_status.running) {
                /* Tell the core that there is no valid temperature value to
                 * return, but it need not worry about this.
                 */
                *temperature = THERMAL_TEMP_INVALID;
                goto unlock;
        }

        ret = iwl_mld_get_temp(mld, &temp);
        if (ret)
                goto unlock;

        *temperature = temp * 1000;
unlock:
        wiphy_unlock(mld->wiphy);
        return ret;
}

static int iwl_mld_tzone_set_trip_temp(struct thermal_zone_device *device,
                                       const struct thermal_trip *trip,
                                       int temp)
{
        struct iwl_mld *mld = thermal_zone_device_priv(device);
        int ret;

        wiphy_lock(mld->wiphy);

        if (!mld->fw_status.running) {
                ret = -EIO;
                goto unlock;
        }

        if ((temp / 1000) > S16_MAX) {
                ret = -EINVAL;
                goto unlock;
        }

        ret = iwl_mld_config_temp_report_ths(mld);
unlock:
        wiphy_unlock(mld->wiphy);
        return ret;
}

static  struct thermal_zone_device_ops tzone_ops = {
        .get_temp = iwl_mld_tzone_get_temp,
        .set_trip_temp = iwl_mld_tzone_set_trip_temp,
};

static void iwl_mld_thermal_zone_register(struct iwl_mld *mld)
{
        int ret;
        char name[16];
        static atomic_t counter = ATOMIC_INIT(0);
        struct thermal_trip trips[IWL_MAX_DTS_TRIPS] = {
                [0 ... IWL_MAX_DTS_TRIPS - 1] = {
                        .temperature = THERMAL_TEMP_INVALID,
                        .type = THERMAL_TRIP_PASSIVE,
                        .flags = THERMAL_TRIP_FLAG_RW_TEMP,
                },
        };

        BUILD_BUG_ON(ARRAY_SIZE(name) >= THERMAL_NAME_LENGTH);

        sprintf(name, "iwlwifi_%u", atomic_inc_return(&counter) & 0xFF);
        mld->tzone =
                thermal_zone_device_register_with_trips(name, trips,
                                                        IWL_MAX_DTS_TRIPS,
                                                        mld, &tzone_ops,
                                                        NULL, 0, 0);
        if (IS_ERR(mld->tzone)) {
                IWL_DEBUG_TEMP(mld,
                               "Failed to register to thermal zone (err = %ld)\n",
                               PTR_ERR(mld->tzone));
                mld->tzone = NULL;
                return;
        }

        ret = thermal_zone_device_enable(mld->tzone);
        if (ret) {
                IWL_DEBUG_TEMP(mld, "Failed to enable thermal zone\n");
                thermal_zone_device_unregister(mld->tzone);
        }
}

int iwl_mld_config_ctdp(struct iwl_mld *mld, u32 state,
                        enum iwl_ctdp_cmd_operation op)
{
        struct iwl_ctdp_cmd cmd = {
                .operation = cpu_to_le32(op),
                .window_size = 0,
        };
        u32 budget;
        int ret;

        lockdep_assert_wiphy(mld->wiphy);

        /* Do a linear scale from IWL_MLD_MIN_CTDP_BUDGET_MW to the configured
         * maximum in the predefined number of steps.
         */
        budget = ((mld->power_budget_mw - IWL_MLD_MIN_CTDP_BUDGET_MW) *
                  (IWL_MLD_NUM_CTDP_STEPS - 1 - state)) /
                 (IWL_MLD_NUM_CTDP_STEPS - 1) +
                 IWL_MLD_MIN_CTDP_BUDGET_MW;
        cmd.budget = cpu_to_le32(budget);

        ret = iwl_mld_send_cmd_pdu(mld, WIDE_ID(PHY_OPS_GROUP, CTDP_CONFIG_CMD),
                                   &cmd);

        if (ret) {
                IWL_ERR(mld, "cTDP command failed (err=%d)\n", ret);
                return ret;
        }

        if (op == CTDP_CMD_OPERATION_START)
                mld->cooling_dev.cur_state = state;

        return 0;
}

static int iwl_mld_tcool_get_max_state(struct thermal_cooling_device *cdev,
                                       unsigned long *state)
{
        *state = IWL_MLD_NUM_CTDP_STEPS - 1;

        return 0;
}

static int iwl_mld_tcool_get_cur_state(struct thermal_cooling_device *cdev,
                                       unsigned long *state)
{
        struct iwl_mld *mld = (struct iwl_mld *)(cdev->devdata);

        *state = mld->cooling_dev.cur_state;

        return 0;
}

static int iwl_mld_tcool_set_cur_state(struct thermal_cooling_device *cdev,
                                       unsigned long new_state)
{
        struct iwl_mld *mld = (struct iwl_mld *)(cdev->devdata);
        int ret;

        wiphy_lock(mld->wiphy);

        if (!mld->fw_status.running) {
                ret = -EIO;
                goto unlock;
        }

        if (new_state >= IWL_MLD_NUM_CTDP_STEPS) {
                ret = -EINVAL;
                goto unlock;
        }

        ret = iwl_mld_config_ctdp(mld, new_state, CTDP_CMD_OPERATION_START);

unlock:
        wiphy_unlock(mld->wiphy);
        return ret;
}

static const struct thermal_cooling_device_ops tcooling_ops = {
        .get_max_state = iwl_mld_tcool_get_max_state,
        .get_cur_state = iwl_mld_tcool_get_cur_state,
        .set_cur_state = iwl_mld_tcool_set_cur_state,
};

static void iwl_mld_cooling_device_register(struct iwl_mld *mld)
{
        char name[] = "iwlwifi";

        BUILD_BUG_ON(ARRAY_SIZE(name) >= THERMAL_NAME_LENGTH);

        mld->cooling_dev.cdev =
                thermal_cooling_device_register(name,
                                                mld,
                                                &tcooling_ops);

        if (IS_ERR(mld->cooling_dev.cdev)) {
                IWL_DEBUG_TEMP(mld,
                               "Failed to register to cooling device (err = %ld)\n",
                               PTR_ERR(mld->cooling_dev.cdev));
                mld->cooling_dev.cdev = NULL;
                return;
        }
}

static void iwl_mld_thermal_zone_unregister(struct iwl_mld *mld)
{
        if (!mld->tzone)
                return;

        IWL_DEBUG_TEMP(mld, "Thermal zone device unregister\n");
        if (mld->tzone) {
                thermal_zone_device_unregister(mld->tzone);
                mld->tzone = NULL;
        }
}

static void iwl_mld_cooling_device_unregister(struct iwl_mld *mld)
{
        if (!mld->cooling_dev.cdev)
                return;

        IWL_DEBUG_TEMP(mld, "Cooling device unregister\n");
        if (mld->cooling_dev.cdev) {
                thermal_cooling_device_unregister(mld->cooling_dev.cdev);
                mld->cooling_dev.cdev = NULL;
        }
}
#endif /* CONFIG_THERMAL */

static u32 iwl_mld_ctdp_get_max_budget(struct iwl_mld *mld)
{
        u64 bios_power_budget = 0;
        u32 default_power_budget;

        switch (CSR_HW_RFID_TYPE(mld->trans->info.hw_rf_id)) {
        case IWL_CFG_RF_TYPE_GF:
                /* dual-radio devices have a higher budget */
                if (CSR_HW_RFID_IS_CDB(mld->trans->info.hw_rf_id))
                        default_power_budget = 5200;
                else
                        default_power_budget = 2880;
                break;
        case IWL_CFG_RF_TYPE_FM:
                default_power_budget = 3450;
                break;
        case IWL_CFG_RF_TYPE_WH:
        case IWL_CFG_RF_TYPE_PE:
        default:
                default_power_budget = 5550;
                break;
        }

        iwl_bios_get_pwr_limit(&mld->fwrt, &bios_power_budget);

        /* 32bit in UEFI, 16bit in ACPI; use BIOS value if it is in range */
        if (bios_power_budget &&
            bios_power_budget != 0xffff && bios_power_budget != 0xffffffff &&
            bios_power_budget >= IWL_MLD_MIN_CTDP_BUDGET_MW &&
            bios_power_budget <= default_power_budget)
                return (u32)bios_power_budget;

        return default_power_budget;
}

void iwl_mld_thermal_initialize(struct iwl_mld *mld)
{
        lockdep_assert_not_held(&mld->wiphy->mtx);

        wiphy_delayed_work_init(&mld->ct_kill_exit_wk, iwl_mld_exit_ctkill);

        mld->power_budget_mw = iwl_mld_ctdp_get_max_budget(mld);
        IWL_DEBUG_TEMP(mld, "cTDP power budget: %d mW\n", mld->power_budget_mw);

#ifdef CONFIG_THERMAL
        iwl_mld_cooling_device_register(mld);
        iwl_mld_thermal_zone_register(mld);
#endif
}

void iwl_mld_thermal_exit(struct iwl_mld *mld)
{
        wiphy_lock(mld->wiphy);
        wiphy_delayed_work_cancel(mld->wiphy, &mld->ct_kill_exit_wk);
        wiphy_unlock(mld->wiphy);

#ifdef CONFIG_THERMAL
        iwl_mld_cooling_device_unregister(mld);
        iwl_mld_thermal_zone_unregister(mld);
#endif
}