root/drivers/hwmon/adt7x10.c
// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * adt7x10.c - Part of lm_sensors, Linux kernel modules for hardware
 *       monitoring
 * This driver handles the ADT7410 and compatible digital temperature sensors.
 * Hartmut Knaack <knaack.h@gmx.de> 2012-07-22
 * based on lm75.c by Frodo Looijaard <frodol@dds.nl>
 * and adt7410.c from iio-staging by Sonic Zhang <sonic.zhang@analog.com>
 */

#include <linux/device.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/jiffies.h>
#include <linux/hwmon.h>
#include <linux/err.h>
#include <linux/delay.h>
#include <linux/interrupt.h>
#include <linux/regmap.h>

#include "adt7x10.h"

/*
 * ADT7X10 status
 */
#define ADT7X10_STAT_T_LOW              (1 << 4)
#define ADT7X10_STAT_T_HIGH             (1 << 5)
#define ADT7X10_STAT_T_CRIT             (1 << 6)
#define ADT7X10_STAT_NOT_RDY            (1 << 7)

/*
 * ADT7X10 config
 */
#define ADT7X10_FAULT_QUEUE_MASK        (1 << 0 | 1 << 1)
#define ADT7X10_CT_POLARITY             (1 << 2)
#define ADT7X10_INT_POLARITY            (1 << 3)
#define ADT7X10_EVENT_MODE              (1 << 4)
#define ADT7X10_MODE_MASK               (1 << 5 | 1 << 6)
#define ADT7X10_FULL                    (0 << 5 | 0 << 6)
#define ADT7X10_PD                      (1 << 5 | 1 << 6)
#define ADT7X10_RESOLUTION              (1 << 7)

/*
 * ADT7X10 masks
 */
#define ADT7X10_T13_VALUE_MASK          0xFFF8
#define ADT7X10_T_HYST_MASK             0xF

/* straight from the datasheet */
#define ADT7X10_TEMP_MIN (-55000)
#define ADT7X10_TEMP_MAX 150000

/* Each client has this additional data */
struct adt7x10_data {
        struct regmap           *regmap;
        u8                      config;
        u8                      oldconfig;
        bool                    valid;          /* true if temperature valid */
};

enum {
        adt7x10_temperature = 0,
        adt7x10_t_alarm_high,
        adt7x10_t_alarm_low,
        adt7x10_t_crit,
};

static const u8 ADT7X10_REG_TEMP[] = {
        [adt7x10_temperature] = ADT7X10_TEMPERATURE,            /* input */
        [adt7x10_t_alarm_high] = ADT7X10_T_ALARM_HIGH,          /* high */
        [adt7x10_t_alarm_low] = ADT7X10_T_ALARM_LOW,            /* low */
        [adt7x10_t_crit] = ADT7X10_T_CRIT,                      /* critical */
};

static irqreturn_t adt7x10_irq_handler(int irq, void *private)
{
        struct device *dev = private;
        struct adt7x10_data *d = dev_get_drvdata(dev);
        unsigned int status;
        int ret;

        ret = regmap_read(d->regmap, ADT7X10_STATUS, &status);
        if (ret < 0)
                return IRQ_HANDLED;

        if (status & ADT7X10_STAT_T_HIGH)
                hwmon_notify_event(dev, hwmon_temp, hwmon_temp_max_alarm, 0);
        if (status & ADT7X10_STAT_T_LOW)
                hwmon_notify_event(dev, hwmon_temp, hwmon_temp_min_alarm, 0);
        if (status & ADT7X10_STAT_T_CRIT)
                hwmon_notify_event(dev, hwmon_temp, hwmon_temp_crit_alarm, 0);

        return IRQ_HANDLED;
}

static int adt7x10_temp_ready(struct regmap *regmap)
{
        unsigned int status;
        int i, ret;

        for (i = 0; i < 6; i++) {
                ret = regmap_read(regmap, ADT7X10_STATUS, &status);
                if (ret < 0)
                        return ret;
                if (!(status & ADT7X10_STAT_NOT_RDY))
                        return 0;
                msleep(60);
        }
        return -ETIMEDOUT;
}

static s16 ADT7X10_TEMP_TO_REG(long temp)
{
        return DIV_ROUND_CLOSEST(clamp_val(temp, ADT7X10_TEMP_MIN,
                                           ADT7X10_TEMP_MAX) * 128, 1000);
}

static int ADT7X10_REG_TO_TEMP(struct adt7x10_data *data, s16 reg)
{
        /* in 13 bit mode, bits 0-2 are status flags - mask them out */
        if (!(data->config & ADT7X10_RESOLUTION))
                reg &= ADT7X10_T13_VALUE_MASK;
        /*
         * temperature is stored in twos complement format, in steps of
         * 1/128°C
         */
        return DIV_ROUND_CLOSEST(reg * 1000, 128);
}

/*-----------------------------------------------------------------------*/

static int adt7x10_temp_read(struct adt7x10_data *data, int index, long *val)
{
        unsigned int regval;
        int ret;

        if (index == adt7x10_temperature && !data->valid) {
                /* wait for valid temperature */
                ret = adt7x10_temp_ready(data->regmap);
                if (ret)
                        return ret;
                data->valid = true;
        }

        ret = regmap_read(data->regmap, ADT7X10_REG_TEMP[index], &regval);
        if (ret)
                return ret;

        *val = ADT7X10_REG_TO_TEMP(data, regval);
        return 0;
}

static int adt7x10_temp_write(struct adt7x10_data *data, int index, long temp)
{
        return regmap_write(data->regmap, ADT7X10_REG_TEMP[index],
                            ADT7X10_TEMP_TO_REG(temp));
}

static int adt7x10_hyst_read(struct adt7x10_data *data, int index, long *val)
{
        unsigned int regs[2] = {ADT7X10_T_HYST, ADT7X10_REG_TEMP[index]};
        int hyst, ret;
        u16 regdata[2];

        ret = regmap_multi_reg_read(data->regmap, regs, regdata, 2);
        if (ret)
                return ret;

        hyst = (regdata[0] & ADT7X10_T_HYST_MASK) * 1000;

        /*
         * hysteresis is stored as a 4 bit offset in the device, convert it
         * to an absolute value
         */
        /* min has positive offset, others have negative */
        if (index == adt7x10_t_alarm_low)
                hyst = -hyst;

        *val = ADT7X10_REG_TO_TEMP(data, regdata[1]) - hyst;
        return 0;
}

static int adt7x10_hyst_write(struct adt7x10_data *data, long hyst)
{
        unsigned int regval;
        int limit, ret;

        /* convert absolute hysteresis value to a 4 bit delta value */
        ret = regmap_read(data->regmap, ADT7X10_T_ALARM_HIGH, &regval);
        if (ret < 0)
                return ret;

        limit = ADT7X10_REG_TO_TEMP(data, regval);

        hyst = clamp_val(hyst, ADT7X10_TEMP_MIN, ADT7X10_TEMP_MAX);
        regval = clamp_val(DIV_ROUND_CLOSEST(limit - hyst, 1000), 0,
                           ADT7X10_T_HYST_MASK);
        return regmap_write(data->regmap, ADT7X10_T_HYST, regval);
}

static int adt7x10_alarm_read(struct adt7x10_data *data, int index, long *val)
{
        unsigned int status;
        int ret;

        ret = regmap_read(data->regmap, ADT7X10_STATUS, &status);
        if (ret < 0)
                return ret;

        *val = !!(status & index);

        return 0;
}

static umode_t adt7x10_is_visible(const void *data,
                                  enum hwmon_sensor_types type,
                                  u32 attr, int channel)
{
        switch (attr) {
        case hwmon_temp_max:
        case hwmon_temp_min:
        case hwmon_temp_crit:
        case hwmon_temp_max_hyst:
                return 0644;
        case hwmon_temp_input:
        case hwmon_temp_min_alarm:
        case hwmon_temp_max_alarm:
        case hwmon_temp_crit_alarm:
        case hwmon_temp_min_hyst:
        case hwmon_temp_crit_hyst:
                return 0444;
        default:
                break;
        }

        return 0;
}

static int adt7x10_read(struct device *dev, enum hwmon_sensor_types type,
                        u32 attr, int channel, long *val)
{
        struct adt7x10_data *data = dev_get_drvdata(dev);

        switch (attr) {
        case hwmon_temp_input:
                return adt7x10_temp_read(data, adt7x10_temperature, val);
        case hwmon_temp_max:
                return adt7x10_temp_read(data, adt7x10_t_alarm_high, val);
        case hwmon_temp_min:
                return adt7x10_temp_read(data, adt7x10_t_alarm_low, val);
        case hwmon_temp_crit:
                return adt7x10_temp_read(data, adt7x10_t_crit, val);
        case hwmon_temp_max_hyst:
                return adt7x10_hyst_read(data, adt7x10_t_alarm_high, val);
        case hwmon_temp_min_hyst:
                return adt7x10_hyst_read(data, adt7x10_t_alarm_low, val);
        case hwmon_temp_crit_hyst:
                return adt7x10_hyst_read(data, adt7x10_t_crit, val);
        case hwmon_temp_min_alarm:
                return adt7x10_alarm_read(data, ADT7X10_STAT_T_LOW, val);
        case hwmon_temp_max_alarm:
                return adt7x10_alarm_read(data, ADT7X10_STAT_T_HIGH, val);
        case hwmon_temp_crit_alarm:
                return adt7x10_alarm_read(data, ADT7X10_STAT_T_CRIT, val);
        default:
                return -EOPNOTSUPP;
        }
}

static int adt7x10_write(struct device *dev, enum hwmon_sensor_types type,
                         u32 attr, int channel, long val)
{
        struct adt7x10_data *data = dev_get_drvdata(dev);

        switch (attr) {
        case hwmon_temp_max:
                return adt7x10_temp_write(data, adt7x10_t_alarm_high, val);
        case hwmon_temp_min:
                return adt7x10_temp_write(data, adt7x10_t_alarm_low, val);
        case hwmon_temp_crit:
                return adt7x10_temp_write(data, adt7x10_t_crit, val);
        case hwmon_temp_max_hyst:
                return adt7x10_hyst_write(data, val);
        default:
                return -EOPNOTSUPP;
        }
}

static const struct hwmon_channel_info * const adt7x10_info[] = {
        HWMON_CHANNEL_INFO(temp, HWMON_T_INPUT | HWMON_T_MAX | HWMON_T_MIN |
                           HWMON_T_CRIT | HWMON_T_MAX_HYST | HWMON_T_MIN_HYST |
                           HWMON_T_CRIT_HYST | HWMON_T_MIN_ALARM |
                           HWMON_T_MAX_ALARM | HWMON_T_CRIT_ALARM),
        NULL,
};

static const struct hwmon_ops adt7x10_hwmon_ops = {
        .is_visible = adt7x10_is_visible,
        .read = adt7x10_read,
        .write = adt7x10_write,
};

static const struct hwmon_chip_info adt7x10_chip_info = {
        .ops = &adt7x10_hwmon_ops,
        .info = adt7x10_info,
};

static void adt7x10_restore_config(void *private)
{
        struct adt7x10_data *data = private;

        regmap_write(data->regmap, ADT7X10_CONFIG, data->oldconfig);
}

int adt7x10_probe(struct device *dev, const char *name, int irq,
                  struct regmap *regmap)
{
        struct adt7x10_data *data;
        unsigned int config;
        struct device *hdev;
        int ret;

        data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
        if (!data)
                return -ENOMEM;

        data->regmap = regmap;

        dev_set_drvdata(dev, data);

        /* configure as specified */
        ret = regmap_read(regmap, ADT7X10_CONFIG, &config);
        if (ret < 0) {
                dev_dbg(dev, "Can't read config? %d\n", ret);
                return ret;
        }
        data->oldconfig = config;

        /*
         * Set to 16 bit resolution, continous conversion and comparator mode.
         */
        data->config = data->oldconfig;
        data->config &= ~(ADT7X10_MODE_MASK | ADT7X10_CT_POLARITY |
                        ADT7X10_INT_POLARITY);
        data->config |= ADT7X10_FULL | ADT7X10_RESOLUTION | ADT7X10_EVENT_MODE;

        if (data->config != data->oldconfig) {
                ret = regmap_write(regmap, ADT7X10_CONFIG, data->config);
                if (ret)
                        return ret;
                ret = devm_add_action_or_reset(dev, adt7x10_restore_config, data);
                if (ret)
                        return ret;
        }
        dev_dbg(dev, "Config %02x\n", data->config);

        hdev = devm_hwmon_device_register_with_info(dev, name, data,
                                                    &adt7x10_chip_info, NULL);
        if (IS_ERR(hdev))
                return PTR_ERR(hdev);

        if (irq > 0) {
                ret = devm_request_threaded_irq(dev, irq, NULL,
                                                adt7x10_irq_handler,
                                                IRQF_TRIGGER_FALLING |
                                                IRQF_ONESHOT,
                                                dev_name(dev), hdev);
                if (ret)
                        return ret;
        }

        return 0;
}
EXPORT_SYMBOL_GPL(adt7x10_probe);

static int adt7x10_suspend(struct device *dev)
{
        struct adt7x10_data *data = dev_get_drvdata(dev);

        return regmap_write(data->regmap, ADT7X10_CONFIG,
                            data->config | ADT7X10_PD);
}

static int adt7x10_resume(struct device *dev)
{
        struct adt7x10_data *data = dev_get_drvdata(dev);

        return regmap_write(data->regmap, ADT7X10_CONFIG, data->config);
}

EXPORT_SIMPLE_DEV_PM_OPS(adt7x10_dev_pm_ops, adt7x10_suspend, adt7x10_resume);

MODULE_AUTHOR("Hartmut Knaack");
MODULE_DESCRIPTION("ADT7410/ADT7420, ADT7310/ADT7320 common code");
MODULE_LICENSE("GPL");