blob: 25894940e0372612b4a841d8b042e7b3e330b553 [file] [log] [blame]
/* Copyright (c) 2010-2011, Code Aurora Forum. All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 and
* only version 2 as published by the Free Software Foundation.
*
* 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.
*
*/
/*
* Qualcomm PMIC8058 Thermal Manager driver
*
*/
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/platform_device.h>
#include <linux/err.h>
#include <linux/thermal.h>
#include <linux/interrupt.h>
#include <linux/mfd/pmic8058.h>
#include <linux/completion.h>
#include <linux/msm_adc.h>
/* PMIC8058 TEMP_ALRM registers */
#define SSBI_REG_TEMP_ALRM_CTRL 0x1B
#define SSBI_REG_TEMP_ALRM_PWM 0x9B
#define SSBI_REG_TEMP_ALRM_TEST1 0x7A
#define SSBI_REG_TEMP_ALRM_TEST2 0xAB
/* TEMP_ALRM_CTRL */
#define PM8058_TEMP_ST3_SD 0x80
#define PM8058_TEMP_ST2_SD 0x40
#define PM8058_TEMP_STATUS_MASK 0x30
#define PM8058_TEMP_STATUS_SHIFT 4
#define PM8058_TEMP_THRESH_MASK 0x0C
#define PM8058_TEMP_THRESH_SHIFT 2
#define PM8058_TEMP_OVRD_ST3 0x02
#define PM8058_TEMP_OVRD_ST2 0x01
#define PM8058_TEMP_OVRD_MASK 0x03
#define PM8058_TEMP_STAGE_STEP 20000 /* Stage step: 20 C */
#define PM8058_TEMP_STAGE_HYSTERESIS 2000
#define PM8058_TEMP_THRESH_MIN 105000 /* Threshold Min: 105 C */
#define PM8058_TEMP_THRESH_STEP 5000 /* Threshold step: 5 C */
/* TEMP_ALRM_PWM */
#define PM8058_TEMP_PWM_EN_MASK 0xC0
#define PM8058_TEMP_PWM_EN_SHIFT 6
#define PM8058_TEMP_PWM_PER_PRE_MASK 0x38
#define PM8058_TEMP_PWM_PER_PRE_SHIFT 3
#define PM8058_TEMP_PWM_PER_DIV_MASK 0x07
#define PM8058_TEMP_PWM_PER_DIV_SHIFT 0
/* Trips: from critical to less critical */
#define PM8058_TRIP_STAGE3 0
#define PM8058_TRIP_STAGE2 1
#define PM8058_TRIP_STAGE1 2
#define PM8058_TRIP_NUM 3
#define PM8058_TEMP_ADC_CH CHANNEL_ADC_DIE_TEMP
struct pm8058_tm_device {
struct pm8058_chip *pm_chip;
struct thermal_zone_device *tz_dev;
unsigned long temp;
enum thermal_device_mode mode;
unsigned int thresh;
unsigned int stage;
unsigned int irq;
void *adc_handle;
};
enum pmic_thermal_override_mode {
SOFTWARE_OVERRIDE_DISABLED = 0,
SOFTWARE_OVERRIDE_ENABLED,
};
static inline int pm8058_tm_read_ctrl(struct pm8058_chip *chip, u8 *reg)
{
int rc;
rc = pm8058_read(chip, SSBI_REG_TEMP_ALRM_CTRL, reg, 1);
if (rc)
pr_err("%s: pm8058_read FAIL: rc=%d\n", __func__, rc);
return rc;
}
static inline int pm8058_tm_write_ctrl(struct pm8058_chip *chip, u8 reg)
{
int rc;
rc = pm8058_write(chip, SSBI_REG_TEMP_ALRM_CTRL, &reg, 1);
if (rc)
pr_err("%s: pm8058_write FAIL: rc=%d\n", __func__, rc);
return rc;
}
static inline int pm8058_tm_write_pwm(struct pm8058_chip *chip, u8 reg)
{
int rc;
rc = pm8058_write(chip, SSBI_REG_TEMP_ALRM_PWM, &reg, 1);
if (rc)
pr_err("%s: pm8058_write FAIL: rc=%d\n", __func__, rc);
return rc;
}
static inline int
pm8058_tm_shutdown_override(struct pm8058_chip *chip,
enum pmic_thermal_override_mode mode)
{
int rc;
u8 reg;
rc = pm8058_tm_read_ctrl(chip, &reg);
if (rc < 0)
return rc;
reg &= ~(PM8058_TEMP_OVRD_MASK | PM8058_TEMP_STATUS_MASK);
if (mode == SOFTWARE_OVERRIDE_ENABLED)
reg |= (PM8058_TEMP_OVRD_ST3 | PM8058_TEMP_OVRD_ST2) &
PM8058_TEMP_OVRD_MASK;
rc = pm8058_tm_write_ctrl(chip, reg);
return rc;
}
static int pm8058_tz_get_temp(struct thermal_zone_device *thermal,
unsigned long *temp)
{
struct pm8058_tm_device *tm = thermal->devdata;
DECLARE_COMPLETION_ONSTACK(wait);
struct adc_chan_result adc_result = {
.physical = 0lu,
};
int rc;
if (!tm || !temp)
return -EINVAL;
*temp = tm->temp;
rc = adc_channel_request_conv(tm->adc_handle, &wait);
if (rc < 0) {
pr_err("%s: adc_channel_request_conv() failed, rc = %d\n",
__func__, rc);
return rc;
}
wait_for_completion(&wait);
rc = adc_channel_read_result(tm->adc_handle, &adc_result);
if (rc < 0) {
pr_err("%s: adc_channel_read_result() failed, rc = %d\n",
__func__, rc);
return rc;
}
*temp = adc_result.physical;
tm->temp = adc_result.physical;
return 0;
}
static int pm8058_tz_get_mode(struct thermal_zone_device *thermal,
enum thermal_device_mode *mode)
{
struct pm8058_tm_device *tm = thermal->devdata;
if (!tm || !mode)
return -EINVAL;
*mode = tm->mode;
return 0;
}
static int pm8058_tz_set_mode(struct thermal_zone_device *thermal,
enum thermal_device_mode mode)
{
struct pm8058_tm_device *tm = thermal->devdata;
if (!tm)
return -EINVAL;
if (mode != tm->mode) {
if (mode == THERMAL_DEVICE_ENABLED)
pm8058_tm_shutdown_override(tm->pm_chip,
SOFTWARE_OVERRIDE_ENABLED);
else
pm8058_tm_shutdown_override(tm->pm_chip,
SOFTWARE_OVERRIDE_DISABLED);
}
tm->mode = mode;
return 0;
}
static int pm8058_tz_get_trip_type(struct thermal_zone_device *thermal,
int trip, enum thermal_trip_type *type)
{
struct pm8058_tm_device *tm = thermal->devdata;
if (!tm || trip < 0 || !type)
return -EINVAL;
switch (trip) {
case PM8058_TRIP_STAGE3:
*type = THERMAL_TRIP_CRITICAL;
break;
case PM8058_TRIP_STAGE2:
*type = THERMAL_TRIP_HOT;
break;
case PM8058_TRIP_STAGE1:
*type = THERMAL_TRIP_HOT;
break;
default:
return -EINVAL;
}
return 0;
}
static int pm8058_tz_get_trip_temp(struct thermal_zone_device *thermal,
int trip, unsigned long *temp)
{
struct pm8058_tm_device *tm = thermal->devdata;
int thresh_temp;
if (!tm || trip < 0 || !temp)
return -EINVAL;
thresh_temp = tm->thresh * PM8058_TEMP_THRESH_STEP +
PM8058_TEMP_THRESH_MIN;
switch (trip) {
case PM8058_TRIP_STAGE3:
thresh_temp += 2 * PM8058_TEMP_STAGE_STEP;
break;
case PM8058_TRIP_STAGE2:
thresh_temp += PM8058_TEMP_STAGE_STEP;
break;
case PM8058_TRIP_STAGE1:
break;
default:
return -EINVAL;
}
*temp = thresh_temp;
return 0;
}
static int pm8058_tz_get_crit_temp(struct thermal_zone_device *thermal,
unsigned long *temp)
{
struct pm8058_tm_device *tm = thermal->devdata;
if (!tm || !temp)
return -EINVAL;
*temp = tm->thresh * PM8058_TEMP_THRESH_STEP + PM8058_TEMP_THRESH_MIN +
2 * PM8058_TEMP_STAGE_STEP;
return 0;
}
static struct thermal_zone_device_ops pm8058_thermal_zone_ops = {
.get_temp = pm8058_tz_get_temp,
.get_mode = pm8058_tz_get_mode,
.set_mode = pm8058_tz_set_mode,
.get_trip_type = pm8058_tz_get_trip_type,
.get_trip_temp = pm8058_tz_get_trip_temp,
.get_crit_temp = pm8058_tz_get_crit_temp,
};
static irqreturn_t pm8058_tm_isr(int irq, void *data)
{
struct pm8058_tm_device *tm = data;
int rc;
u8 reg;
rc = pm8058_tm_read_ctrl(tm->pm_chip, &reg);
if (rc < 0)
goto isr_handled;
tm->stage = (reg & PM8058_TEMP_STATUS_MASK) >> PM8058_TEMP_STATUS_SHIFT;
tm->thresh = (reg & PM8058_TEMP_THRESH_MASK) >>
PM8058_TEMP_THRESH_SHIFT;
if (reg & (PM8058_TEMP_ST2_SD | PM8058_TEMP_ST3_SD)) {
reg &= ~(PM8058_TEMP_ST2_SD | PM8058_TEMP_ST3_SD |
PM8058_TEMP_STATUS_MASK);
pm8058_tm_write_ctrl(tm->pm_chip, reg);
}
thermal_zone_device_update(tm->tz_dev);
/* Notify user space */
if (tm->mode == THERMAL_DEVICE_ENABLED)
kobject_uevent(&tm->tz_dev->device.kobj, KOBJ_CHANGE);
isr_handled:
return IRQ_HANDLED;
}
static int pm8058_tm_init_reg(struct pm8058_tm_device *tm)
{
int rc;
u8 reg;
rc = pm8058_tm_read_ctrl(tm->pm_chip, &reg);
if (rc < 0)
return rc;
tm->stage = (reg & PM8058_TEMP_STATUS_MASK) >> PM8058_TEMP_STATUS_SHIFT;
tm->temp = 0;
/* Use temperature threshold set 0: (105, 125, 145) */
tm->thresh = 0;
reg = (tm->thresh << PM8058_TEMP_THRESH_SHIFT) &
PM8058_TEMP_THRESH_MASK;
rc = pm8058_tm_write_ctrl(tm->pm_chip, reg);
if (rc < 0)
return rc;
/*
* Set the PMIC alarm module PWM to have a frequency of 8 Hz. This
* helps cut down on the number of unnecessary interrupts fired when
* changing between thermal stages. Also, Enable the over temperature
* PWM whenever the PMIC is enabled.
*/
reg = 1 << PM8058_TEMP_PWM_EN_SHIFT |
3 << PM8058_TEMP_PWM_PER_PRE_SHIFT |
3 << PM8058_TEMP_PWM_PER_DIV_SHIFT;
rc = pm8058_tm_write_pwm(tm->pm_chip, reg);
return rc;
}
static int __devinit pmic8058_tm_probe(struct platform_device *pdev)
{
struct pm8058_tm_device *tmdev;
struct pm8058_chip *pm_chip;
unsigned int irq;
int rc;
pm_chip = dev_get_drvdata(pdev->dev.parent);
if (pm_chip == NULL) {
pr_err("%s: no driver data passed in.\n", __func__);
return -EFAULT;
}
irq = platform_get_irq(pdev, 0);
if (!irq) {
pr_err("%s: no IRQ passed in.\n", __func__);
return -EFAULT;
}
tmdev = kzalloc(sizeof *tmdev, GFP_KERNEL);
if (tmdev == NULL) {
pr_err("%s: kzalloc() failed.\n", __func__);
return -ENOMEM;
}
rc = adc_channel_open(PM8058_TEMP_ADC_CH, &(tmdev->adc_handle));
if (rc < 0) {
pr_err("%s: adc_channel_open() failed.\n", __func__);
kfree(tmdev);
return rc;
}
tmdev->pm_chip = pm_chip;
tmdev->tz_dev = thermal_zone_device_register("pm8058_tz",
PM8058_TRIP_NUM, tmdev,
&pm8058_thermal_zone_ops,
0, 0, 0, 0);
if (tmdev->tz_dev == NULL) {
pr_err("%s: thermal_zone_device_register() failed.\n",
__func__);
adc_channel_close(tmdev->adc_handle);
kfree(tmdev);
return -ENODEV;
}
rc = pm8058_tm_init_reg(tmdev);
pm8058_tm_shutdown_override(tmdev->pm_chip, SOFTWARE_OVERRIDE_DISABLED);
if (rc < 0) {
thermal_zone_device_unregister(tmdev->tz_dev);
adc_channel_close(tmdev->adc_handle);
kfree(tmdev);
return rc;
}
/* start in HW control, switch to SW control when user changes mode */
tmdev->mode = THERMAL_DEVICE_DISABLED;
thermal_zone_device_update(tmdev->tz_dev);
platform_set_drvdata(pdev, tmdev);
rc = request_threaded_irq(irq, NULL, pm8058_tm_isr,
IRQF_TRIGGER_RISING | IRQF_DISABLED,
"pm8058-tm-irq", tmdev);
if (rc < 0) {
pr_err("%s: request_irq(%d) FAIL: %d\n", __func__, irq, rc);
thermal_zone_device_unregister(tmdev->tz_dev);
platform_set_drvdata(pdev, tmdev->pm_chip);
adc_channel_close(tmdev->adc_handle);
kfree(tmdev);
return rc;
}
tmdev->irq = irq;
pr_notice("%s: OK\n", __func__);
return 0;
}
static int __devexit pmic8058_tm_remove(struct platform_device *pdev)
{
struct pm8058_tm_device *tmdev = platform_get_drvdata(pdev);
thermal_zone_device_unregister(tmdev->tz_dev);
platform_set_drvdata(pdev, tmdev->pm_chip);
pm8058_tm_shutdown_override(tmdev->pm_chip, THERMAL_DEVICE_DISABLED);
adc_channel_close(tmdev->adc_handle);
free_irq(tmdev->irq, tmdev);
kfree(tmdev);
return 0;
}
#ifdef CONFIG_PM
static int pmic8058_tm_suspend(struct device *dev)
{
struct platform_device *pdev = to_platform_device(dev);
struct pm8058_tm_device *tm = platform_get_drvdata(pdev);
/* Clear override bits in suspend to allow hardware control */
pm8058_tm_shutdown_override(tm->pm_chip, SOFTWARE_OVERRIDE_DISABLED);
return 0;
}
static int pmic8058_tm_resume(struct device *dev)
{
struct platform_device *pdev = to_platform_device(dev);
struct pm8058_tm_device *tm = platform_get_drvdata(pdev);
/* Override hardware actions so software can control */
if (tm->mode == THERMAL_DEVICE_ENABLED)
pm8058_tm_shutdown_override(tm->pm_chip,
SOFTWARE_OVERRIDE_ENABLED);
return 0;
}
static const struct dev_pm_ops pmic8058_tm_pm_ops = {
.suspend = pmic8058_tm_suspend,
.resume = pmic8058_tm_resume,
};
#define PM8058_TM_PM_OPS (&pmic8058_tm_pm_ops)
#else
#define PM8058_TM_PM_OPS NULL
#endif
static struct platform_driver pmic8058_tm_driver = {
.probe = pmic8058_tm_probe,
.remove = __devexit_p(pmic8058_tm_remove),
.driver = {
.name = "pm8058-tm",
.owner = THIS_MODULE,
.pm = PM8058_TM_PM_OPS,
},
};
static int __init pm8058_tm_init(void)
{
return platform_driver_register(&pmic8058_tm_driver);
}
static void __exit pm8058_tm_exit(void)
{
platform_driver_unregister(&pmic8058_tm_driver);
}
module_init(pm8058_tm_init);
module_exit(pm8058_tm_exit);
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("PMIC8058 Thermal Manager driver");
MODULE_VERSION("1.0");
MODULE_ALIAS("platform:pmic8058-tm");