root/drivers/thermal/da9062-thermal.c
// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Thermal device driver for DA9062 and DA9061
 * Copyright (C) 2017  Dialog Semiconductor
 */

/* When over-temperature is reached, an interrupt from the device will be
 * triggered. Following this event the interrupt will be disabled and
 * periodic transmission of uevents (HOT trip point) should define the
 * first level of temperature supervision. It is expected that any final
 * implementation of the thermal driver will include a .notify() function
 * to implement these uevents to userspace.
 *
 * These uevents are intended to indicate non-invasive temperature control
 * of the system, where the necessary measures for cooling are the
 * responsibility of the host software. Once the temperature falls again,
 * the IRQ is re-enabled so the start of a new over-temperature event can
 * be detected without constant software monitoring.
 */

#include <linux/errno.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/regmap.h>
#include <linux/thermal.h>
#include <linux/workqueue.h>

#include <linux/mfd/da9062/core.h>
#include <linux/mfd/da9062/registers.h>

/* Minimum, maximum and default polling millisecond periods are provided
 * here as an example. It is expected that any final implementation to also
 * include a modification of these settings to match the required
 * application.
 */
#define DA9062_DEFAULT_POLLING_MS_PERIOD        3000
#define DA9062_MAX_POLLING_MS_PERIOD            10000
#define DA9062_MIN_POLLING_MS_PERIOD            1000

#define DA9062_MILLI_CELSIUS(t)                 ((t) * 1000)

static unsigned int pp_tmp = DA9062_DEFAULT_POLLING_MS_PERIOD;

struct da9062_thermal_config {
        const char *name;
};

struct da9062_thermal {
        struct da9062 *hw;
        struct delayed_work work;
        struct thermal_zone_device *zone;
        struct mutex lock; /* protection for da9062_thermal temperature */
        int temperature;
        int irq;
        const struct da9062_thermal_config *config;
        struct device *dev;
};

static void da9062_thermal_poll_on(struct work_struct *work)
{
        struct da9062_thermal *thermal = container_of(work,
                                                struct da9062_thermal,
                                                work.work);
        unsigned long delay;
        unsigned int val;
        int ret;

        /* clear E_TEMP */
        ret = regmap_write(thermal->hw->regmap,
                           DA9062AA_EVENT_B,
                           DA9062AA_E_TEMP_MASK);
        if (ret < 0) {
                dev_err(thermal->dev,
                        "Cannot clear the TJUNC temperature status\n");
                goto err_enable_irq;
        }

        /* Now read E_TEMP again: it is acting like a status bit.
         * If over-temperature, then this status will be true.
         * If not over-temperature, this status will be false.
         */
        ret = regmap_read(thermal->hw->regmap,
                          DA9062AA_EVENT_B,
                          &val);
        if (ret < 0) {
                dev_err(thermal->dev,
                        "Cannot check the TJUNC temperature status\n");
                goto err_enable_irq;
        }

        if (val & DA9062AA_E_TEMP_MASK) {
                mutex_lock(&thermal->lock);
                thermal->temperature = DA9062_MILLI_CELSIUS(125);
                mutex_unlock(&thermal->lock);
                thermal_zone_device_update(thermal->zone,
                                           THERMAL_EVENT_UNSPECIFIED);

                /*
                 * pp_tmp is between 1s and 10s, so we can round the jiffies
                 */
                delay = round_jiffies(msecs_to_jiffies(pp_tmp));
                queue_delayed_work(system_freezable_wq, &thermal->work, delay);
                return;
        }

        mutex_lock(&thermal->lock);
        thermal->temperature = DA9062_MILLI_CELSIUS(0);
        mutex_unlock(&thermal->lock);
        thermal_zone_device_update(thermal->zone,
                                   THERMAL_EVENT_UNSPECIFIED);

err_enable_irq:
        enable_irq(thermal->irq);
}

static irqreturn_t da9062_thermal_irq_handler(int irq, void *data)
{
        struct da9062_thermal *thermal = data;

        disable_irq_nosync(thermal->irq);
        queue_delayed_work(system_freezable_wq, &thermal->work, 0);

        return IRQ_HANDLED;
}

static int da9062_thermal_get_temp(struct thermal_zone_device *z,
                                   int *temp)
{
        struct da9062_thermal *thermal = thermal_zone_device_priv(z);

        mutex_lock(&thermal->lock);
        *temp = thermal->temperature;
        mutex_unlock(&thermal->lock);

        return 0;
}

static const struct thermal_zone_device_ops da9062_thermal_ops = {
        .get_temp       = da9062_thermal_get_temp,
};

static struct thermal_trip trips[] = {
        { .temperature = DA9062_MILLI_CELSIUS(125), .type = THERMAL_TRIP_HOT },
};

static const struct da9062_thermal_config da9062_config = {
        .name = "da9062-thermal",
};

static const struct of_device_id da9062_compatible_reg_id_table[] = {
        { .compatible = "dlg,da9062-thermal", .data = &da9062_config },
        { },
};

MODULE_DEVICE_TABLE(of, da9062_compatible_reg_id_table);

static int da9062_thermal_probe(struct platform_device *pdev)
{
        struct da9062 *chip = dev_get_drvdata(pdev->dev.parent);
        struct da9062_thermal *thermal;
        const struct of_device_id *match;
        int ret = 0;

        match = of_match_node(da9062_compatible_reg_id_table,
                              pdev->dev.of_node);
        if (!match)
                return -ENXIO;

        if (pdev->dev.of_node) {
                if (!of_property_read_u32(pdev->dev.of_node,
                                          "polling-delay-passive",
                                          &pp_tmp)) {
                        if (pp_tmp < DA9062_MIN_POLLING_MS_PERIOD ||
                            pp_tmp > DA9062_MAX_POLLING_MS_PERIOD) {
                                dev_warn(&pdev->dev,
                                         "Out-of-range polling period %d ms\n",
                                         pp_tmp);
                                pp_tmp = DA9062_DEFAULT_POLLING_MS_PERIOD;
                        }
                }
        }

        thermal = devm_kzalloc(&pdev->dev, sizeof(struct da9062_thermal),
                               GFP_KERNEL);
        if (!thermal) {
                ret = -ENOMEM;
                goto err;
        }

        thermal->config = match->data;
        thermal->hw = chip;
        thermal->dev = &pdev->dev;

        INIT_DELAYED_WORK(&thermal->work, da9062_thermal_poll_on);
        mutex_init(&thermal->lock);

        thermal->zone = thermal_zone_device_register_with_trips(thermal->config->name,
                                                                trips, ARRAY_SIZE(trips), thermal,
                                                                &da9062_thermal_ops, NULL, pp_tmp,
                                                                0);
        if (IS_ERR(thermal->zone)) {
                dev_err(&pdev->dev, "Cannot register thermal zone device\n");
                ret = PTR_ERR(thermal->zone);
                goto err;
        }
        ret = thermal_zone_device_enable(thermal->zone);
        if (ret) {
                dev_err(&pdev->dev, "Cannot enable thermal zone device\n");
                goto err_zone;
        }

        dev_dbg(&pdev->dev,
                "TJUNC temperature polling period set at %d ms\n", pp_tmp);

        ret = platform_get_irq_byname(pdev, "THERMAL");
        if (ret < 0)
                goto err_zone;

        thermal->irq = ret;

        ret = request_threaded_irq(thermal->irq, NULL,
                                   da9062_thermal_irq_handler,
                                   IRQF_TRIGGER_LOW | IRQF_ONESHOT,
                                   "THERMAL", thermal);
        if (ret) {
                dev_err(&pdev->dev,
                        "Failed to request thermal device IRQ.\n");
                goto err_zone;
        }

        platform_set_drvdata(pdev, thermal);
        return 0;

err_zone:
        thermal_zone_device_unregister(thermal->zone);
err:
        return ret;
}

static void da9062_thermal_remove(struct platform_device *pdev)
{
        struct  da9062_thermal *thermal = platform_get_drvdata(pdev);

        free_irq(thermal->irq, thermal);
        cancel_delayed_work_sync(&thermal->work);
        thermal_zone_device_unregister(thermal->zone);
}

static struct platform_driver da9062_thermal_driver = {
        .probe  = da9062_thermal_probe,
        .remove = da9062_thermal_remove,
        .driver = {
                .name           = "da9062-thermal",
                .of_match_table = da9062_compatible_reg_id_table,
        },
};

module_platform_driver(da9062_thermal_driver);

MODULE_AUTHOR("Steve Twiss");
MODULE_DESCRIPTION("Thermal TJUNC device driver for Dialog DA9062 and DA9061");
MODULE_LICENSE("GPL");
MODULE_ALIAS("platform:da9062-thermal");