forked from Minki/linux
2d2474a194
Pull thermal managament updates from Zhang Rui: - Enhance thermal "userspace" governor to export the reason when a thermal event is triggered and delivered to user space. From Srinivas Pandruvada - Introduce a single TSENS thermal driver for the different versions of the TSENS IP that exist, on different qcom msm/apq SoCs'. Support for msm8916, msm8960, msm8974 and msm8996 families is also added. From Rajendra Nayak - Introduce hardware-tracked trip points support to the device tree thermal sensor framework. The framework supports an arbitrary number of trip points. Whenever the current temperature is changed, the trip points immediately below and above the current temperature are found, driver callback is invoked to program the hardware to get notified when either of the two trip points are triggered. Hardware-tracked trip points support for rockchip thermal driver is also added at the same time. From Sascha Hauer, Caesar Wang - Introduce a new thermal driver, which enables TMU (Thermal Monitor Unit) on QorIQ platform. From Jia Hongtao - Introduce a new thermal driver for Maxim MAX77620. From Laxman Dewangan - Introduce a new thermal driver for Intel platforms using WhiskeyCove PMIC. From Bin Gao - Add mt2701 chip support to MTK thermal driver. From Dawei Chien - Enhance Tegra thermal driver to enable soctherm node and set "critical", "hot" trips, for Tegra124, Tegra132, Tegra210. From Wei Ni - Add resume support for tango thermal driver. From Marc Gonzalez - several small fixes and improvements for rockchip, qcom, imx, rcar, mtk thermal drivers and thermal core code. From Caesar Wang, Keerthy, Rocky Hao, Wei Yongjun, Peter Robinson, Bui Duc Phuc, Axel Lin, Hugh Kang * 'next' of git://git.kernel.org/pub/scm/linux/kernel/git/rzhang/linux: (48 commits) thermal: int3403: Process trip change notification thermal: int340x: New Interface to read trip and notify thermal: user_space gov: Add additional information in uevent thermal: Enhance thermal_zone_device_update for events arm64: tegra: set hot trips for Tegra210 arm64: tegra: set critical trips for Tegra210 arm64: tegra: add soctherm node for Tegra210 arm64: tegra: set hot trips for Tegra132 arm64: tegra: set critical trips for Tegra132 arm64: tegra: use tegra132-soctherm for Tegra132 arm: tegra: set hot trips for Tegra124 arm: tegra: set critical trips for Tegra124 thermal: tegra: add hw-throttle for Tegra132 thermal: tegra: add hw-throttle function of: Add bindings of hw throttle for Tegra soctherm thermal: mtk_thermal: Check return value of devm_thermal_zone_of_sensor_register thermal: Add Mediatek thermal driver for mt2701. dt-bindings: thermal: Add binding document for Mediatek thermal controller thermal: max77620: Add thermal driver for reporting junction temp thermal: max77620: Add DT binding doc for thermal driver ...
572 lines
13 KiB
C
572 lines
13 KiB
C
/*
|
|
* R-Car THS/TSC thermal sensor driver
|
|
*
|
|
* Copyright (C) 2012 Renesas Solutions Corp.
|
|
* Kuninori Morimoto <kuninori.morimoto.gx@renesas.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; version 2 of the License.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
|
|
*/
|
|
#include <linux/delay.h>
|
|
#include <linux/err.h>
|
|
#include <linux/irq.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/io.h>
|
|
#include <linux/module.h>
|
|
#include <linux/of_device.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/pm_runtime.h>
|
|
#include <linux/reboot.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/spinlock.h>
|
|
#include <linux/thermal.h>
|
|
|
|
#include "thermal_hwmon.h"
|
|
|
|
#define IDLE_INTERVAL 5000
|
|
|
|
#define COMMON_STR 0x00
|
|
#define COMMON_ENR 0x04
|
|
#define COMMON_INTMSK 0x0c
|
|
|
|
#define REG_POSNEG 0x20
|
|
#define REG_FILONOFF 0x28
|
|
#define REG_THSCR 0x2c
|
|
#define REG_THSSR 0x30
|
|
#define REG_INTCTRL 0x34
|
|
|
|
/* THSCR */
|
|
#define CPCTL (1 << 12)
|
|
|
|
/* THSSR */
|
|
#define CTEMP 0x3f
|
|
|
|
struct rcar_thermal_common {
|
|
void __iomem *base;
|
|
struct device *dev;
|
|
struct list_head head;
|
|
spinlock_t lock;
|
|
};
|
|
|
|
struct rcar_thermal_priv {
|
|
void __iomem *base;
|
|
struct rcar_thermal_common *common;
|
|
struct thermal_zone_device *zone;
|
|
struct delayed_work work;
|
|
struct mutex lock;
|
|
struct list_head list;
|
|
int id;
|
|
u32 ctemp;
|
|
};
|
|
|
|
#define rcar_thermal_for_each_priv(pos, common) \
|
|
list_for_each_entry(pos, &common->head, list)
|
|
|
|
#define MCELSIUS(temp) ((temp) * 1000)
|
|
#define rcar_zone_to_priv(zone) ((zone)->devdata)
|
|
#define rcar_priv_to_dev(priv) ((priv)->common->dev)
|
|
#define rcar_has_irq_support(priv) ((priv)->common->base)
|
|
#define rcar_id_to_shift(priv) ((priv)->id * 8)
|
|
#define rcar_of_data(dev) ((unsigned long)of_device_get_match_data(dev))
|
|
#define rcar_use_of_thermal(dev) (rcar_of_data(dev) == USE_OF_THERMAL)
|
|
|
|
#define USE_OF_THERMAL 1
|
|
static const struct of_device_id rcar_thermal_dt_ids[] = {
|
|
{ .compatible = "renesas,rcar-thermal", },
|
|
{ .compatible = "renesas,rcar-gen2-thermal", .data = (void *)USE_OF_THERMAL },
|
|
{},
|
|
};
|
|
MODULE_DEVICE_TABLE(of, rcar_thermal_dt_ids);
|
|
|
|
/*
|
|
* basic functions
|
|
*/
|
|
#define rcar_thermal_common_read(c, r) \
|
|
_rcar_thermal_common_read(c, COMMON_ ##r)
|
|
static u32 _rcar_thermal_common_read(struct rcar_thermal_common *common,
|
|
u32 reg)
|
|
{
|
|
return ioread32(common->base + reg);
|
|
}
|
|
|
|
#define rcar_thermal_common_write(c, r, d) \
|
|
_rcar_thermal_common_write(c, COMMON_ ##r, d)
|
|
static void _rcar_thermal_common_write(struct rcar_thermal_common *common,
|
|
u32 reg, u32 data)
|
|
{
|
|
iowrite32(data, common->base + reg);
|
|
}
|
|
|
|
#define rcar_thermal_common_bset(c, r, m, d) \
|
|
_rcar_thermal_common_bset(c, COMMON_ ##r, m, d)
|
|
static void _rcar_thermal_common_bset(struct rcar_thermal_common *common,
|
|
u32 reg, u32 mask, u32 data)
|
|
{
|
|
u32 val;
|
|
|
|
val = ioread32(common->base + reg);
|
|
val &= ~mask;
|
|
val |= (data & mask);
|
|
iowrite32(val, common->base + reg);
|
|
}
|
|
|
|
#define rcar_thermal_read(p, r) _rcar_thermal_read(p, REG_ ##r)
|
|
static u32 _rcar_thermal_read(struct rcar_thermal_priv *priv, u32 reg)
|
|
{
|
|
return ioread32(priv->base + reg);
|
|
}
|
|
|
|
#define rcar_thermal_write(p, r, d) _rcar_thermal_write(p, REG_ ##r, d)
|
|
static void _rcar_thermal_write(struct rcar_thermal_priv *priv,
|
|
u32 reg, u32 data)
|
|
{
|
|
iowrite32(data, priv->base + reg);
|
|
}
|
|
|
|
#define rcar_thermal_bset(p, r, m, d) _rcar_thermal_bset(p, REG_ ##r, m, d)
|
|
static void _rcar_thermal_bset(struct rcar_thermal_priv *priv, u32 reg,
|
|
u32 mask, u32 data)
|
|
{
|
|
u32 val;
|
|
|
|
val = ioread32(priv->base + reg);
|
|
val &= ~mask;
|
|
val |= (data & mask);
|
|
iowrite32(val, priv->base + reg);
|
|
}
|
|
|
|
/*
|
|
* zone device functions
|
|
*/
|
|
static int rcar_thermal_update_temp(struct rcar_thermal_priv *priv)
|
|
{
|
|
struct device *dev = rcar_priv_to_dev(priv);
|
|
int i;
|
|
u32 ctemp, old, new;
|
|
int ret = -EINVAL;
|
|
|
|
mutex_lock(&priv->lock);
|
|
|
|
/*
|
|
* TSC decides a value of CPTAP automatically,
|
|
* and this is the conditions which validate interrupt.
|
|
*/
|
|
rcar_thermal_bset(priv, THSCR, CPCTL, CPCTL);
|
|
|
|
ctemp = 0;
|
|
old = ~0;
|
|
for (i = 0; i < 128; i++) {
|
|
/*
|
|
* we need to wait 300us after changing comparator offset
|
|
* to get stable temperature.
|
|
* see "Usage Notes" on datasheet
|
|
*/
|
|
udelay(300);
|
|
|
|
new = rcar_thermal_read(priv, THSSR) & CTEMP;
|
|
if (new == old) {
|
|
ctemp = new;
|
|
break;
|
|
}
|
|
old = new;
|
|
}
|
|
|
|
if (!ctemp) {
|
|
dev_err(dev, "thermal sensor was broken\n");
|
|
goto err_out_unlock;
|
|
}
|
|
|
|
/*
|
|
* enable IRQ
|
|
*/
|
|
if (rcar_has_irq_support(priv)) {
|
|
rcar_thermal_write(priv, FILONOFF, 0);
|
|
|
|
/* enable Rising/Falling edge interrupt */
|
|
rcar_thermal_write(priv, POSNEG, 0x1);
|
|
rcar_thermal_write(priv, INTCTRL, (((ctemp - 0) << 8) |
|
|
((ctemp - 1) << 0)));
|
|
}
|
|
|
|
dev_dbg(dev, "thermal%d %d -> %d\n", priv->id, priv->ctemp, ctemp);
|
|
|
|
priv->ctemp = ctemp;
|
|
ret = 0;
|
|
err_out_unlock:
|
|
mutex_unlock(&priv->lock);
|
|
return ret;
|
|
}
|
|
|
|
static int rcar_thermal_get_current_temp(struct rcar_thermal_priv *priv,
|
|
int *temp)
|
|
{
|
|
int tmp;
|
|
int ret;
|
|
|
|
ret = rcar_thermal_update_temp(priv);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
mutex_lock(&priv->lock);
|
|
tmp = MCELSIUS((priv->ctemp * 5) - 65);
|
|
mutex_unlock(&priv->lock);
|
|
|
|
if ((tmp < MCELSIUS(-45)) || (tmp > MCELSIUS(125))) {
|
|
struct device *dev = rcar_priv_to_dev(priv);
|
|
|
|
dev_err(dev, "it couldn't measure temperature correctly\n");
|
|
return -EIO;
|
|
}
|
|
|
|
*temp = tmp;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int rcar_thermal_of_get_temp(void *data, int *temp)
|
|
{
|
|
struct rcar_thermal_priv *priv = data;
|
|
|
|
return rcar_thermal_get_current_temp(priv, temp);
|
|
}
|
|
|
|
static int rcar_thermal_get_temp(struct thermal_zone_device *zone, int *temp)
|
|
{
|
|
struct rcar_thermal_priv *priv = rcar_zone_to_priv(zone);
|
|
|
|
return rcar_thermal_get_current_temp(priv, temp);
|
|
}
|
|
|
|
static int rcar_thermal_get_trip_type(struct thermal_zone_device *zone,
|
|
int trip, enum thermal_trip_type *type)
|
|
{
|
|
struct rcar_thermal_priv *priv = rcar_zone_to_priv(zone);
|
|
struct device *dev = rcar_priv_to_dev(priv);
|
|
|
|
/* see rcar_thermal_get_temp() */
|
|
switch (trip) {
|
|
case 0: /* +90 <= temp */
|
|
*type = THERMAL_TRIP_CRITICAL;
|
|
break;
|
|
default:
|
|
dev_err(dev, "rcar driver trip error\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int rcar_thermal_get_trip_temp(struct thermal_zone_device *zone,
|
|
int trip, int *temp)
|
|
{
|
|
struct rcar_thermal_priv *priv = rcar_zone_to_priv(zone);
|
|
struct device *dev = rcar_priv_to_dev(priv);
|
|
|
|
/* see rcar_thermal_get_temp() */
|
|
switch (trip) {
|
|
case 0: /* +90 <= temp */
|
|
*temp = MCELSIUS(90);
|
|
break;
|
|
default:
|
|
dev_err(dev, "rcar driver trip error\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int rcar_thermal_notify(struct thermal_zone_device *zone,
|
|
int trip, enum thermal_trip_type type)
|
|
{
|
|
struct rcar_thermal_priv *priv = rcar_zone_to_priv(zone);
|
|
struct device *dev = rcar_priv_to_dev(priv);
|
|
|
|
switch (type) {
|
|
case THERMAL_TRIP_CRITICAL:
|
|
/* FIXME */
|
|
dev_warn(dev, "Thermal reached to critical temperature\n");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct thermal_zone_of_device_ops rcar_thermal_zone_of_ops = {
|
|
.get_temp = rcar_thermal_of_get_temp,
|
|
};
|
|
|
|
static struct thermal_zone_device_ops rcar_thermal_zone_ops = {
|
|
.get_temp = rcar_thermal_get_temp,
|
|
.get_trip_type = rcar_thermal_get_trip_type,
|
|
.get_trip_temp = rcar_thermal_get_trip_temp,
|
|
.notify = rcar_thermal_notify,
|
|
};
|
|
|
|
/*
|
|
* interrupt
|
|
*/
|
|
#define rcar_thermal_irq_enable(p) _rcar_thermal_irq_ctrl(p, 1)
|
|
#define rcar_thermal_irq_disable(p) _rcar_thermal_irq_ctrl(p, 0)
|
|
static void _rcar_thermal_irq_ctrl(struct rcar_thermal_priv *priv, int enable)
|
|
{
|
|
struct rcar_thermal_common *common = priv->common;
|
|
unsigned long flags;
|
|
u32 mask = 0x3 << rcar_id_to_shift(priv); /* enable Rising/Falling */
|
|
|
|
if (!rcar_has_irq_support(priv))
|
|
return;
|
|
|
|
spin_lock_irqsave(&common->lock, flags);
|
|
|
|
rcar_thermal_common_bset(common, INTMSK, mask, enable ? 0 : mask);
|
|
|
|
spin_unlock_irqrestore(&common->lock, flags);
|
|
}
|
|
|
|
static void rcar_thermal_work(struct work_struct *work)
|
|
{
|
|
struct rcar_thermal_priv *priv;
|
|
int cctemp, nctemp;
|
|
int ret;
|
|
|
|
priv = container_of(work, struct rcar_thermal_priv, work.work);
|
|
|
|
ret = rcar_thermal_get_current_temp(priv, &cctemp);
|
|
if (ret < 0)
|
|
return;
|
|
|
|
ret = rcar_thermal_update_temp(priv);
|
|
if (ret < 0)
|
|
return;
|
|
|
|
rcar_thermal_irq_enable(priv);
|
|
|
|
ret = rcar_thermal_get_current_temp(priv, &nctemp);
|
|
if (ret < 0)
|
|
return;
|
|
|
|
if (nctemp != cctemp)
|
|
thermal_zone_device_update(priv->zone,
|
|
THERMAL_EVENT_UNSPECIFIED);
|
|
}
|
|
|
|
static u32 rcar_thermal_had_changed(struct rcar_thermal_priv *priv, u32 status)
|
|
{
|
|
struct device *dev = rcar_priv_to_dev(priv);
|
|
|
|
status = (status >> rcar_id_to_shift(priv)) & 0x3;
|
|
|
|
if (status) {
|
|
dev_dbg(dev, "thermal%d %s%s\n",
|
|
priv->id,
|
|
(status & 0x2) ? "Rising " : "",
|
|
(status & 0x1) ? "Falling" : "");
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
static irqreturn_t rcar_thermal_irq(int irq, void *data)
|
|
{
|
|
struct rcar_thermal_common *common = data;
|
|
struct rcar_thermal_priv *priv;
|
|
unsigned long flags;
|
|
u32 status, mask;
|
|
|
|
spin_lock_irqsave(&common->lock, flags);
|
|
|
|
mask = rcar_thermal_common_read(common, INTMSK);
|
|
status = rcar_thermal_common_read(common, STR);
|
|
rcar_thermal_common_write(common, STR, 0x000F0F0F & mask);
|
|
|
|
spin_unlock_irqrestore(&common->lock, flags);
|
|
|
|
status = status & ~mask;
|
|
|
|
/*
|
|
* check the status
|
|
*/
|
|
rcar_thermal_for_each_priv(priv, common) {
|
|
if (rcar_thermal_had_changed(priv, status)) {
|
|
rcar_thermal_irq_disable(priv);
|
|
schedule_delayed_work(&priv->work,
|
|
msecs_to_jiffies(300));
|
|
}
|
|
}
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
/*
|
|
* platform functions
|
|
*/
|
|
static int rcar_thermal_remove(struct platform_device *pdev)
|
|
{
|
|
struct rcar_thermal_common *common = platform_get_drvdata(pdev);
|
|
struct device *dev = &pdev->dev;
|
|
struct rcar_thermal_priv *priv;
|
|
|
|
rcar_thermal_for_each_priv(priv, common) {
|
|
rcar_thermal_irq_disable(priv);
|
|
if (rcar_use_of_thermal(dev))
|
|
thermal_remove_hwmon_sysfs(priv->zone);
|
|
else
|
|
thermal_zone_device_unregister(priv->zone);
|
|
}
|
|
|
|
pm_runtime_put(dev);
|
|
pm_runtime_disable(dev);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int rcar_thermal_probe(struct platform_device *pdev)
|
|
{
|
|
struct rcar_thermal_common *common;
|
|
struct rcar_thermal_priv *priv;
|
|
struct device *dev = &pdev->dev;
|
|
struct resource *res, *irq;
|
|
int mres = 0;
|
|
int i;
|
|
int ret = -ENODEV;
|
|
int idle = IDLE_INTERVAL;
|
|
u32 enr_bits = 0;
|
|
|
|
common = devm_kzalloc(dev, sizeof(*common), GFP_KERNEL);
|
|
if (!common)
|
|
return -ENOMEM;
|
|
|
|
platform_set_drvdata(pdev, common);
|
|
|
|
INIT_LIST_HEAD(&common->head);
|
|
spin_lock_init(&common->lock);
|
|
common->dev = dev;
|
|
|
|
pm_runtime_enable(dev);
|
|
pm_runtime_get_sync(dev);
|
|
|
|
irq = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
|
|
if (irq) {
|
|
/*
|
|
* platform has IRQ support.
|
|
* Then, driver uses common registers
|
|
* rcar_has_irq_support() will be enabled
|
|
*/
|
|
res = platform_get_resource(pdev, IORESOURCE_MEM, mres++);
|
|
common->base = devm_ioremap_resource(dev, res);
|
|
if (IS_ERR(common->base))
|
|
return PTR_ERR(common->base);
|
|
|
|
idle = 0; /* polling delay is not needed */
|
|
}
|
|
|
|
for (i = 0;; i++) {
|
|
res = platform_get_resource(pdev, IORESOURCE_MEM, mres++);
|
|
if (!res)
|
|
break;
|
|
|
|
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
|
|
if (!priv) {
|
|
ret = -ENOMEM;
|
|
goto error_unregister;
|
|
}
|
|
|
|
priv->base = devm_ioremap_resource(dev, res);
|
|
if (IS_ERR(priv->base)) {
|
|
ret = PTR_ERR(priv->base);
|
|
goto error_unregister;
|
|
}
|
|
|
|
priv->common = common;
|
|
priv->id = i;
|
|
mutex_init(&priv->lock);
|
|
INIT_LIST_HEAD(&priv->list);
|
|
INIT_DELAYED_WORK(&priv->work, rcar_thermal_work);
|
|
ret = rcar_thermal_update_temp(priv);
|
|
if (ret < 0)
|
|
goto error_unregister;
|
|
|
|
if (rcar_use_of_thermal(dev))
|
|
priv->zone = devm_thermal_zone_of_sensor_register(
|
|
dev, i, priv,
|
|
&rcar_thermal_zone_of_ops);
|
|
else
|
|
priv->zone = thermal_zone_device_register(
|
|
"rcar_thermal",
|
|
1, 0, priv,
|
|
&rcar_thermal_zone_ops, NULL, 0,
|
|
idle);
|
|
if (IS_ERR(priv->zone)) {
|
|
dev_err(dev, "can't register thermal zone\n");
|
|
ret = PTR_ERR(priv->zone);
|
|
priv->zone = NULL;
|
|
goto error_unregister;
|
|
}
|
|
|
|
if (rcar_use_of_thermal(dev)) {
|
|
/*
|
|
* thermal_zone doesn't enable hwmon as default,
|
|
* but, enable it here to keep compatible
|
|
*/
|
|
priv->zone->tzp->no_hwmon = false;
|
|
ret = thermal_add_hwmon_sysfs(priv->zone);
|
|
if (ret)
|
|
goto error_unregister;
|
|
}
|
|
|
|
rcar_thermal_irq_enable(priv);
|
|
|
|
list_move_tail(&priv->list, &common->head);
|
|
|
|
/* update ENR bits */
|
|
enr_bits |= 3 << (i * 8);
|
|
}
|
|
|
|
/* enable temperature comparation */
|
|
if (irq) {
|
|
ret = devm_request_irq(dev, irq->start, rcar_thermal_irq, 0,
|
|
dev_name(dev), common);
|
|
if (ret) {
|
|
dev_err(dev, "irq request failed\n ");
|
|
goto error_unregister;
|
|
}
|
|
|
|
rcar_thermal_common_write(common, ENR, enr_bits);
|
|
}
|
|
|
|
dev_info(dev, "%d sensor probed\n", i);
|
|
|
|
return 0;
|
|
|
|
error_unregister:
|
|
rcar_thermal_remove(pdev);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static struct platform_driver rcar_thermal_driver = {
|
|
.driver = {
|
|
.name = "rcar_thermal",
|
|
.of_match_table = rcar_thermal_dt_ids,
|
|
},
|
|
.probe = rcar_thermal_probe,
|
|
.remove = rcar_thermal_remove,
|
|
};
|
|
module_platform_driver(rcar_thermal_driver);
|
|
|
|
MODULE_LICENSE("GPL");
|
|
MODULE_DESCRIPTION("R-Car THS/TSC thermal sensor driver");
|
|
MODULE_AUTHOR("Kuninori Morimoto <kuninori.morimoto.gx@renesas.com>");
|