root/drivers/ufs/core/ufs-hwmon.c
// SPDX-License-Identifier: GPL-2.0
/*
 * UFS hardware monitoring support
 * Copyright (c) 2021, Western Digital Corporation
 */

#include <linux/hwmon.h>
#include <linux/units.h>

#include <ufs/ufshcd.h>
#include "ufshcd-priv.h"

struct ufs_hwmon_data {
        struct ufs_hba *hba;
        u8 mask;
};

static int ufs_read_temp_enable(struct ufs_hba *hba, u8 mask, long *val)
{
        u32 ee_mask;
        int err;

        err = ufshcd_query_attr(hba, UPIU_QUERY_OPCODE_READ_ATTR, QUERY_ATTR_IDN_EE_CONTROL, 0, 0,
                                &ee_mask);
        if (err)
                return err;

        *val = (mask & ee_mask & MASK_EE_TOO_HIGH_TEMP) || (mask & ee_mask & MASK_EE_TOO_LOW_TEMP);

        return 0;
}

static int ufs_get_temp(struct ufs_hba *hba, enum attr_idn idn, long *val)
{
        u32 value;
        int err;

        err = ufshcd_query_attr(hba, UPIU_QUERY_OPCODE_READ_ATTR, idn, 0, 0, &value);
        if (err)
                return err;

        if (value == 0)
                return -ENODATA;

        *val = ((long)value - 80) * MILLIDEGREE_PER_DEGREE;

        return 0;
}

static int ufs_hwmon_read(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel,
                          long *val)
{
        struct ufs_hwmon_data *data = dev_get_drvdata(dev);
        struct ufs_hba *hba = data->hba;
        int err;

        down(&hba->host_sem);

        if (!ufshcd_is_user_access_allowed(hba)) {
                up(&hba->host_sem);
                return -EBUSY;
        }

        ufshcd_rpm_get_sync(hba);

        switch (attr) {
        case hwmon_temp_enable:
                err = ufs_read_temp_enable(hba, data->mask, val);

                break;
        case hwmon_temp_crit:
                err = ufs_get_temp(hba, QUERY_ATTR_IDN_HIGH_TEMP_BOUND, val);

                break;
        case hwmon_temp_lcrit:
                err = ufs_get_temp(hba, QUERY_ATTR_IDN_LOW_TEMP_BOUND, val);

                break;
        case hwmon_temp_input:
                err = ufs_get_temp(hba, QUERY_ATTR_IDN_CASE_ROUGH_TEMP, val);

                break;
        default:
                err = -EOPNOTSUPP;

                break;
        }

        ufshcd_rpm_put_sync(hba);

        up(&hba->host_sem);

        return err;
}

static int ufs_hwmon_write(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel,
                           long val)
{
        struct ufs_hwmon_data *data = dev_get_drvdata(dev);
        struct ufs_hba *hba = data->hba;
        int err;

        if (attr != hwmon_temp_enable)
                return -EINVAL;

        if (val != 0 && val != 1)
                return -EINVAL;

        down(&hba->host_sem);

        if (!ufshcd_is_user_access_allowed(hba)) {
                up(&hba->host_sem);
                return -EBUSY;
        }

        ufshcd_rpm_get_sync(hba);

        if (val == 1)
                err = ufshcd_update_ee_usr_mask(hba, MASK_EE_URGENT_TEMP, 0);
        else
                err = ufshcd_update_ee_usr_mask(hba, 0, MASK_EE_URGENT_TEMP);

        ufshcd_rpm_put_sync(hba);

        up(&hba->host_sem);

        return err;
}

static umode_t ufs_hwmon_is_visible(const void *data,
                                    enum hwmon_sensor_types type, u32 attr,
                                    int channel)
{
        if (type != hwmon_temp)
                return 0;

        switch (attr) {
        case hwmon_temp_enable:
                return 0644;
        case hwmon_temp_crit:
        case hwmon_temp_lcrit:
        case hwmon_temp_input:
                return 0444;
        default:
                break;
        }
        return 0;
}

static const struct hwmon_channel_info *const ufs_hwmon_info[] = {
        HWMON_CHANNEL_INFO(temp, HWMON_T_ENABLE | HWMON_T_INPUT | HWMON_T_CRIT | HWMON_T_LCRIT),
        NULL
};

static const struct hwmon_ops ufs_hwmon_ops = {
        .is_visible     = ufs_hwmon_is_visible,
        .read           = ufs_hwmon_read,
        .write          = ufs_hwmon_write,
};

static const struct hwmon_chip_info ufs_hwmon_hba_info = {
        .ops    = &ufs_hwmon_ops,
        .info   = ufs_hwmon_info,
};

void ufs_hwmon_probe(struct ufs_hba *hba, u8 mask)
{
        struct device *dev = hba->dev;
        struct ufs_hwmon_data *data;
        struct device *hwmon;

        data = kzalloc_obj(*data);
        if (!data)
                return;

        data->hba = hba;
        data->mask = mask;

        hwmon = hwmon_device_register_with_info(dev, "ufs", data, &ufs_hwmon_hba_info, NULL);
        if (IS_ERR(hwmon)) {
                dev_warn(dev, "Failed to instantiate hwmon device\n");
                kfree(data);
                return;
        }

        hba->hwmon_device = hwmon;
}

void ufs_hwmon_remove(struct ufs_hba *hba)
{
        struct ufs_hwmon_data *data;

        if (!hba->hwmon_device)
                return;

        data = dev_get_drvdata(hba->hwmon_device);
        hwmon_device_unregister(hba->hwmon_device);
        hba->hwmon_device = NULL;
        kfree(data);
}

void ufs_hwmon_notify_event(struct ufs_hba *hba, u8 ee_mask)
{
        if (!hba->hwmon_device)
                return;

        if (ee_mask & MASK_EE_TOO_HIGH_TEMP)
                hwmon_notify_event(hba->hwmon_device, hwmon_temp, hwmon_temp_max_alarm, 0);

        if (ee_mask & MASK_EE_TOO_LOW_TEMP)
                hwmon_notify_event(hba->hwmon_device, hwmon_temp, hwmon_temp_min_alarm, 0);
}