root/drivers/thermal/thermal_hwmon.c
// SPDX-License-Identifier: GPL-2.0
/*
 *  thermal_hwmon.c - Generic Thermal Management hwmon support.
 *
 *  Code based on Intel thermal_core.c. Copyrights of the original code:
 *  Copyright (C) 2008 Intel Corp
 *  Copyright (C) 2008 Zhang Rui <rui.zhang@intel.com>
 *  Copyright (C) 2008 Sujith Thomas <sujith.thomas@intel.com>
 *
 *  Copyright (C) 2013 Texas Instruments
 *  Copyright (C) 2013 Eduardo Valentin <eduardo.valentin@ti.com>
 */
#include <linux/err.h>
#include <linux/export.h>
#include <linux/hwmon.h>
#include <linux/slab.h>
#include <linux/thermal.h>

#include "thermal_hwmon.h"
#include "thermal_core.h"

/* hwmon sys I/F */
/* thermal zone devices with the same type share one hwmon device */
struct thermal_hwmon_device {
        char type[THERMAL_NAME_LENGTH];
        struct device *device;
        int count;
        struct list_head tz_list;
        struct list_head node;
};

struct thermal_hwmon_attr {
        struct device_attribute attr;
        char name[16];
};

/* one temperature input for each thermal zone */
struct thermal_hwmon_temp {
        struct list_head hwmon_node;
        struct thermal_zone_device *tz;
        struct thermal_hwmon_attr temp_input;   /* hwmon sys attr */
        struct thermal_hwmon_attr temp_crit;    /* hwmon sys attr */
};

static LIST_HEAD(thermal_hwmon_list);

static DEFINE_MUTEX(thermal_hwmon_list_lock);

static ssize_t
temp_input_show(struct device *dev, struct device_attribute *attr, char *buf)
{
        int temperature;
        int ret;
        struct thermal_hwmon_attr *hwmon_attr
                        = container_of(attr, struct thermal_hwmon_attr, attr);
        struct thermal_hwmon_temp *temp
                        = container_of(hwmon_attr, struct thermal_hwmon_temp,
                                       temp_input);
        struct thermal_zone_device *tz = temp->tz;

        ret = thermal_zone_get_temp(tz, &temperature);

        if (ret)
                return ret;

        return sysfs_emit(buf, "%d\n", temperature);
}

static ssize_t
temp_crit_show(struct device *dev, struct device_attribute *attr, char *buf)
{
        struct thermal_hwmon_attr *hwmon_attr
                        = container_of(attr, struct thermal_hwmon_attr, attr);
        struct thermal_hwmon_temp *temp
                        = container_of(hwmon_attr, struct thermal_hwmon_temp,
                                       temp_crit);
        struct thermal_zone_device *tz = temp->tz;
        int temperature;
        int ret;

        guard(thermal_zone)(tz);

        ret = tz->ops.get_crit_temp(tz, &temperature);
        if (ret)
                return ret;

        return sysfs_emit(buf, "%d\n", temperature);
}


static struct thermal_hwmon_device *
thermal_hwmon_lookup_by_type(const struct thermal_zone_device *tz)
{
        struct thermal_hwmon_device *hwmon;
        char type[THERMAL_NAME_LENGTH];

        mutex_lock(&thermal_hwmon_list_lock);
        list_for_each_entry(hwmon, &thermal_hwmon_list, node) {
                strscpy(type, tz->type);
                strreplace(type, '-', '_');
                if (!strcmp(hwmon->type, type)) {
                        mutex_unlock(&thermal_hwmon_list_lock);
                        return hwmon;
                }
        }
        mutex_unlock(&thermal_hwmon_list_lock);

        return NULL;
}

/* Find the temperature input matching a given thermal zone */
static struct thermal_hwmon_temp *
thermal_hwmon_lookup_temp(const struct thermal_hwmon_device *hwmon,
                          const struct thermal_zone_device *tz)
{
        struct thermal_hwmon_temp *temp;

        mutex_lock(&thermal_hwmon_list_lock);
        list_for_each_entry(temp, &hwmon->tz_list, hwmon_node)
                if (temp->tz == tz) {
                        mutex_unlock(&thermal_hwmon_list_lock);
                        return temp;
                }
        mutex_unlock(&thermal_hwmon_list_lock);

        return NULL;
}

static bool thermal_zone_crit_temp_valid(struct thermal_zone_device *tz)
{
        int temp;
        return tz->ops.get_crit_temp && !tz->ops.get_crit_temp(tz, &temp);
}

int thermal_add_hwmon_sysfs(struct thermal_zone_device *tz)
{
        struct thermal_hwmon_device *hwmon;
        struct thermal_hwmon_temp *temp;
        int new_hwmon_device = 1;
        int result;

        hwmon = thermal_hwmon_lookup_by_type(tz);
        if (hwmon) {
                new_hwmon_device = 0;
                goto register_sys_interface;
        }

        hwmon = kzalloc_obj(*hwmon);
        if (!hwmon)
                return -ENOMEM;

        INIT_LIST_HEAD(&hwmon->tz_list);
        strscpy(hwmon->type, tz->type, THERMAL_NAME_LENGTH);
        strreplace(hwmon->type, '-', '_');
        hwmon->device = hwmon_device_register_for_thermal(&tz->device,
                                                          hwmon->type, hwmon);
        if (IS_ERR(hwmon->device)) {
                result = PTR_ERR(hwmon->device);
                goto free_mem;
        }

 register_sys_interface:
        temp = kzalloc_obj(*temp);
        if (!temp) {
                result = -ENOMEM;
                goto unregister_name;
        }

        temp->tz = tz;
        hwmon->count++;

        snprintf(temp->temp_input.name, sizeof(temp->temp_input.name),
                 "temp%d_input", hwmon->count);
        temp->temp_input.attr.attr.name = temp->temp_input.name;
        temp->temp_input.attr.attr.mode = 0444;
        temp->temp_input.attr.show = temp_input_show;
        sysfs_attr_init(&temp->temp_input.attr.attr);
        result = device_create_file(hwmon->device, &temp->temp_input.attr);
        if (result)
                goto free_temp_mem;

        if (thermal_zone_crit_temp_valid(tz)) {
                snprintf(temp->temp_crit.name,
                                sizeof(temp->temp_crit.name),
                                "temp%d_crit", hwmon->count);
                temp->temp_crit.attr.attr.name = temp->temp_crit.name;
                temp->temp_crit.attr.attr.mode = 0444;
                temp->temp_crit.attr.show = temp_crit_show;
                sysfs_attr_init(&temp->temp_crit.attr.attr);
                result = device_create_file(hwmon->device,
                                            &temp->temp_crit.attr);
                if (result)
                        goto unregister_input;
        }

        mutex_lock(&thermal_hwmon_list_lock);
        if (new_hwmon_device)
                list_add_tail(&hwmon->node, &thermal_hwmon_list);
        list_add_tail(&temp->hwmon_node, &hwmon->tz_list);
        mutex_unlock(&thermal_hwmon_list_lock);

        return 0;

 unregister_input:
        device_remove_file(hwmon->device, &temp->temp_input.attr);
 free_temp_mem:
        kfree(temp);
 unregister_name:
        if (new_hwmon_device)
                hwmon_device_unregister(hwmon->device);
 free_mem:
        kfree(hwmon);

        return result;
}
EXPORT_SYMBOL_GPL(thermal_add_hwmon_sysfs);

void thermal_remove_hwmon_sysfs(struct thermal_zone_device *tz)
{
        struct thermal_hwmon_device *hwmon;
        struct thermal_hwmon_temp *temp;

        hwmon = thermal_hwmon_lookup_by_type(tz);
        if (unlikely(!hwmon)) {
                /* Should never happen... */
                dev_dbg(&tz->device, "hwmon device lookup failed!\n");
                return;
        }

        temp = thermal_hwmon_lookup_temp(hwmon, tz);
        if (unlikely(!temp)) {
                /* Should never happen... */
                dev_dbg(&tz->device, "temperature input lookup failed!\n");
                return;
        }

        device_remove_file(hwmon->device, &temp->temp_input.attr);
        if (thermal_zone_crit_temp_valid(tz))
                device_remove_file(hwmon->device, &temp->temp_crit.attr);

        mutex_lock(&thermal_hwmon_list_lock);
        list_del(&temp->hwmon_node);
        kfree(temp);
        if (!list_empty(&hwmon->tz_list)) {
                mutex_unlock(&thermal_hwmon_list_lock);
                return;
        }
        list_del(&hwmon->node);
        mutex_unlock(&thermal_hwmon_list_lock);

        hwmon_device_unregister(hwmon->device);
        kfree(hwmon);
}
EXPORT_SYMBOL_GPL(thermal_remove_hwmon_sysfs);

static void devm_thermal_hwmon_release(struct device *dev, void *res)
{
        thermal_remove_hwmon_sysfs(*(struct thermal_zone_device **)res);
}

int devm_thermal_add_hwmon_sysfs(struct device *dev, struct thermal_zone_device *tz)
{
        struct thermal_zone_device **ptr;
        int ret;

        ptr = devres_alloc(devm_thermal_hwmon_release, sizeof(*ptr),
                           GFP_KERNEL);
        if (!ptr) {
                dev_warn(dev, "Failed to allocate device resource data\n");
                return -ENOMEM;
        }

        ret = thermal_add_hwmon_sysfs(tz);
        if (ret) {
                dev_warn(dev, "Failed to add hwmon sysfs attributes\n");
                devres_free(ptr);
                return ret;
        }

        *ptr = tz;
        devres_add(dev, ptr);

        return ret;
}
EXPORT_SYMBOL_GPL(devm_thermal_add_hwmon_sysfs);

MODULE_IMPORT_NS("HWMON_THERMAL");