641 lines
14 KiB
C
641 lines
14 KiB
C
/*
|
|
* intel_byt_ec_thermal.c - Intel Baytrail(M) Platform Thermal Driver
|
|
*
|
|
* Copyright (C) 2013 Intel Corporation
|
|
*
|
|
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
*
|
|
* 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.
|
|
*
|
|
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
* Author: Durgadoss R <durgadoss.r@intel.com>
|
|
*
|
|
* This driver talks to the Embedded Controller(EC) on the platform,
|
|
* to retrieve temperature from the Thermal sensors (if available).
|
|
* EC Firmware support is required for this driver to work.
|
|
*/
|
|
|
|
#define pr_fmt(fmt) "intel_byt_ec_thermal: " fmt
|
|
|
|
#include <linux/pm.h>
|
|
#include <linux/init.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/kfifo.h>
|
|
#include <linux/module.h>
|
|
#include <linux/thermal.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/platform_device.h>
|
|
|
|
#include <asm/intel_byt_ec.h>
|
|
#include <asm/intel_mid_thermal.h>
|
|
|
|
#define DEVICE_NAME "byt_ec_thermal"
|
|
|
|
/* Number of Thermal sensors on the platform */
|
|
#define NUM_THERMAL_SENSORS 7
|
|
|
|
/* Registers that govern Thermal Monitoring */
|
|
#define TEMP_SENSOR_SELECT 0x52
|
|
#define TEMP_THRESH_HIGH 0x53
|
|
#define TEMP_THRESH_LOW 0x54
|
|
#define TEMP_THRESH_STS 0x55
|
|
|
|
/* EC commands */
|
|
#define SET_TEMP_THRESHOLD 0x4A
|
|
#define SET_FAN_SPEED 0x1A
|
|
|
|
/* Fan control registers */
|
|
#define PWM_PORT 0x41
|
|
#define PWM_VALUE 0x44
|
|
|
|
/* Fan states are mapped from 0 to 3 */
|
|
#define MAX_FAN_STATES 4
|
|
|
|
/*
|
|
* Hysteresis value is 4 bits wide. A value of 0x01
|
|
* corresponds to 1C and 0x0F corresponds to 15C.
|
|
* Can be changed at run-time through Sysfs.
|
|
*/
|
|
#define DEFAULT_HYST_mC 2000
|
|
|
|
#define NUM_ALERT_LEVELS 2
|
|
#define ALERT_RW_MASK 0x03
|
|
|
|
static DEFINE_MUTEX(thrm_update_lock);
|
|
|
|
/*
|
|
* The EC can have many thermal sensors connected to it. For this
|
|
* platform, this array holds the register address to read the
|
|
* temperature reported by these sensors.
|
|
*/
|
|
static const int ec_sensors[NUM_THERMAL_SENSORS] = {
|
|
0x01, 0x02, 0x50, 0xBA, 0xBB, 0x7E, 0xB9};
|
|
|
|
struct thermal_device_info {
|
|
struct intel_mid_thermal_sensor *sensor;
|
|
int sensor_index;
|
|
u8 trips[NUM_ALERT_LEVELS];
|
|
};
|
|
|
|
struct thermal_data {
|
|
struct platform_device *pdev;
|
|
struct notifier_block nb;
|
|
struct thermal_zone_device **tzd;
|
|
struct thermal_cooling_device *fan_cdev;
|
|
int cur_fan_state;
|
|
|
|
/* Values obtained from platform data */
|
|
int num_sensors;
|
|
struct intel_mid_thermal_sensor *sensors;
|
|
};
|
|
static struct thermal_data *tdata;
|
|
|
|
static int set_fan_speed(unsigned long rpm)
|
|
{
|
|
u8 val;
|
|
int ret;
|
|
|
|
/* Select the PWM port */
|
|
ret = byt_ec_read_byte(PWM_PORT, &val);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
ret = byt_ec_write_byte(PWM_PORT, val | (1 << 0));
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
/* Configure the Fan RPM registers with new value */
|
|
ret = byt_ec_write_byte(PWM_VALUE, rpm);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
/* Send EC command to update Fan RPM */
|
|
ret = byt_ec_send_cmd(0x1A);
|
|
if (ret)
|
|
pr_err("EC command to set Fan speed failed:%d\n", ret);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int set_hyst_mC(long hyst_mC)
|
|
{
|
|
int ret;
|
|
u8 val, hystC;
|
|
|
|
hystC = hyst_mC / 1000;
|
|
|
|
/* Hysteresis is 4 bits wide */
|
|
if (hystC > 15)
|
|
return -EINVAL;
|
|
|
|
mutex_lock(&thrm_update_lock);
|
|
|
|
/* Bits[4:7] of TEMP_SENSOR_SELECT hold the hysteresis in C */
|
|
ret = byt_ec_read_byte(TEMP_SENSOR_SELECT, &val);
|
|
if (ret < 0)
|
|
goto exit;
|
|
|
|
/* Set Bits[4:7] of 'val' to Hysteresis value */
|
|
val = (val & 0x0F) | (hystC << 4);
|
|
|
|
ret = byt_ec_write_byte(TEMP_SENSOR_SELECT, val);
|
|
|
|
exit:
|
|
mutex_unlock(&thrm_update_lock);
|
|
return ret;
|
|
}
|
|
|
|
static int get_hyst_mC(long *hyst)
|
|
{
|
|
int ret;
|
|
u8 val;
|
|
|
|
mutex_lock(&thrm_update_lock);
|
|
|
|
/* Bits[4:7] of TEMP_SENSOR_SELECT hold the hysteresis in C */
|
|
ret = byt_ec_read_byte(TEMP_SENSOR_SELECT, &val);
|
|
if (ret < 0)
|
|
goto exit;
|
|
|
|
/* Return the hysteresis in mC */
|
|
*hyst = (val >> 4) * 1000;
|
|
exit:
|
|
mutex_unlock(&thrm_update_lock);
|
|
return ret;
|
|
}
|
|
|
|
static int set_trip_temp(struct thermal_device_info *td_info,
|
|
int flag, int temp)
|
|
{
|
|
u8 val;
|
|
u8 new_val, old_val;
|
|
u8 new_reg, old_reg;
|
|
int ret;
|
|
|
|
/*
|
|
* EC requires us to update both the thresholds simultaneously.
|
|
* 'new_val' is the threshold that needs to programmed 'now'.
|
|
* 'old_val' is what is stored previously. Same logic applies
|
|
* to the 'address' of these registers as well. Convert the
|
|
* values from mC to C before writing into the registers.
|
|
*/
|
|
new_val = temp;
|
|
old_val = td_info->trips[flag % 1];
|
|
|
|
if (flag) {
|
|
new_reg = TEMP_THRESH_HIGH;
|
|
old_reg = TEMP_THRESH_LOW;
|
|
} else {
|
|
new_reg = TEMP_THRESH_LOW;
|
|
old_reg = TEMP_THRESH_HIGH;
|
|
}
|
|
|
|
ret = byt_ec_read_byte(TEMP_SENSOR_SELECT, &val);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
/* Bits[0:3] of TEMP_SENSOR_SELECT select the required sensor */
|
|
val = (val & 0xF0) | td_info->sensor_index;
|
|
|
|
ret = byt_ec_write_byte(TEMP_SENSOR_SELECT, val);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
ret = byt_ec_write_byte(new_reg, new_val);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
ret = byt_ec_write_byte(old_reg, old_val);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
/* Send SET_TEMP_THRESHOLD command */
|
|
ret = byt_ec_send_cmd(SET_TEMP_THRESHOLD);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
/*
|
|
* There is no way to read the trip points back from
|
|
* the EC. So, store the value in our local structure.
|
|
*/
|
|
td_info->trips[flag] = new_val;
|
|
return ret;
|
|
}
|
|
|
|
static int update_temp(struct thermal_zone_device *tzd, long *temp)
|
|
{
|
|
int ret;
|
|
u8 val;
|
|
struct thermal_device_info *td_info = tzd->devdata;
|
|
int ec_reg = ec_sensors[td_info->sensor_index];
|
|
|
|
ret = byt_ec_read_byte(ec_reg, &val);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
/* Value read from EC is in C; convert to mC */
|
|
*temp = val * 1000;
|
|
return 0;
|
|
}
|
|
|
|
static ssize_t show_temp(struct thermal_zone_device *tzd, long *temp)
|
|
{
|
|
int ret;
|
|
|
|
mutex_lock(&thrm_update_lock);
|
|
|
|
ret = update_temp(tzd, temp);
|
|
|
|
mutex_unlock(&thrm_update_lock);
|
|
return ret;
|
|
}
|
|
|
|
static ssize_t store_trip_temp(struct thermal_zone_device *tzd,
|
|
int trip, long trip_temp)
|
|
{
|
|
int ret;
|
|
struct thermal_device_info *td_info = tzd->devdata;
|
|
|
|
if (trip_temp != 0 && trip_temp < 1000) {
|
|
dev_err(&tzd->device, "Temperature should be in mC\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
mutex_lock(&thrm_update_lock);
|
|
|
|
ret = set_trip_temp(td_info, trip == 1, trip_temp / 1000);
|
|
if (ret)
|
|
dev_err(&tzd->device, "Setting trip point failed:%d\n", ret);
|
|
|
|
mutex_unlock(&thrm_update_lock);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static ssize_t show_trip_temp(struct thermal_zone_device *tzd,
|
|
int trip, long *trip_temp)
|
|
{
|
|
struct thermal_device_info *td_info = tzd->devdata;
|
|
|
|
mutex_lock(&thrm_update_lock);
|
|
|
|
/* Convert to mC */
|
|
*trip_temp = td_info->trips[trip] * 1000;
|
|
|
|
mutex_unlock(&thrm_update_lock);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static ssize_t store_trip_hyst(struct thermal_zone_device *tzd,
|
|
int trip, long hyst)
|
|
{
|
|
/* We expect the Hysteresis in mC */
|
|
if (hyst != 0 && hyst < 1000) {
|
|
dev_err(&tzd->device, "Temperature should be in mC\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
/*
|
|
* All thermal sensors share the same hysteresis for
|
|
* all trip points.
|
|
*/
|
|
return set_hyst_mC(hyst);
|
|
}
|
|
|
|
static ssize_t show_trip_hyst(struct thermal_zone_device *tzd,
|
|
int trip, long *hyst)
|
|
{
|
|
return get_hyst_mC(hyst);
|
|
}
|
|
|
|
static ssize_t show_trip_type(struct thermal_zone_device *tzd,
|
|
int trip, enum thermal_trip_type *trip_type)
|
|
{
|
|
/* All are passive trip points */
|
|
*trip_type = THERMAL_TRIP_PASSIVE;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int fan_get_max_state(struct thermal_cooling_device *cdev,
|
|
unsigned long *state)
|
|
{
|
|
*state = MAX_FAN_STATES;
|
|
return 0;
|
|
}
|
|
|
|
static int fan_get_cur_state(struct thermal_cooling_device *cdev,
|
|
unsigned long *state)
|
|
{
|
|
mutex_lock(&thrm_update_lock);
|
|
|
|
*state = tdata->cur_fan_state;
|
|
|
|
mutex_unlock(&thrm_update_lock);
|
|
return 0;
|
|
}
|
|
|
|
static int fan_set_cur_state(struct thermal_cooling_device *cdev,
|
|
unsigned long state)
|
|
{
|
|
int ret;
|
|
unsigned int rpm;
|
|
|
|
if (state >= MAX_FAN_STATES || state < 0) {
|
|
pr_err("Invalid Fan state:%ld\n", state);
|
|
return -EINVAL;
|
|
}
|
|
|
|
mutex_lock(&thrm_update_lock);
|
|
|
|
switch (state) {
|
|
case 0:
|
|
rpm = 0;
|
|
break;
|
|
case 1:
|
|
rpm = 50;
|
|
break;
|
|
case 2:
|
|
rpm = 100;
|
|
break;
|
|
case 3:
|
|
rpm = 100;
|
|
break;
|
|
default:
|
|
rpm = 0;
|
|
}
|
|
|
|
ret = set_fan_speed(rpm);
|
|
if (ret < 0)
|
|
goto exit;
|
|
|
|
tdata->cur_fan_state = state;
|
|
exit:
|
|
mutex_unlock(&thrm_update_lock);
|
|
return ret;
|
|
}
|
|
|
|
static struct thermal_device_info *initialize_sensor(int index,
|
|
struct intel_mid_thermal_sensor *sensor)
|
|
{
|
|
struct thermal_device_info *td_info =
|
|
kzalloc(sizeof(struct thermal_device_info), GFP_KERNEL);
|
|
|
|
if (!td_info)
|
|
return NULL;
|
|
|
|
td_info->sensor = sensor;
|
|
td_info->sensor_index = index;
|
|
|
|
return td_info;
|
|
}
|
|
|
|
static void notify_thermal_event(int indx)
|
|
{
|
|
int ret;
|
|
long cur_temp;
|
|
char *thermal_event[3];
|
|
|
|
struct thermal_zone_device *tzd = tdata->tzd[indx];
|
|
|
|
mutex_lock(&thrm_update_lock);
|
|
|
|
/*
|
|
* Read the current Temperature and send it to user land;
|
|
* so that the user space can avoid a sysfs read.
|
|
*/
|
|
ret = update_temp(tzd, &cur_temp);
|
|
if (ret) {
|
|
dev_err(&tzd->device, "Cannot update temperature\n");
|
|
goto exit;
|
|
}
|
|
|
|
pr_info("Thermal Event: sensor: %s, cur_temp: %ld\n",
|
|
tzd->type, cur_temp);
|
|
thermal_event[0] = kasprintf(GFP_KERNEL, "NAME=%s", tzd->type);
|
|
thermal_event[1] = kasprintf(GFP_KERNEL, "TEMP=%ld", cur_temp);
|
|
thermal_event[2] = NULL;
|
|
|
|
kobject_uevent_env(&tzd->device.kobj, KOBJ_CHANGE, thermal_event);
|
|
|
|
kfree(thermal_event[1]);
|
|
kfree(thermal_event[0]);
|
|
|
|
exit:
|
|
mutex_unlock(&thrm_update_lock);
|
|
return;
|
|
}
|
|
|
|
static void handle_therm_trip(void)
|
|
{
|
|
u8 sts;
|
|
int i, ret;
|
|
|
|
ret = byt_ec_read_byte(TEMP_THRESH_STS, &sts);
|
|
if (ret < 0) {
|
|
pr_err("Failed to read status register:%d\n", ret);
|
|
return;
|
|
}
|
|
|
|
for (i = 0; i < NUM_THERMAL_SENSORS; i++) {
|
|
if (!(sts & (1 << i)))
|
|
continue;
|
|
|
|
notify_thermal_event(i);
|
|
|
|
/* Clear the interrupt by writing 0 into it */
|
|
sts = sts & ~(1 << i);
|
|
ret = byt_ec_write_byte(TEMP_THRESH_STS, sts);
|
|
if (ret < 0) {
|
|
pr_err("Failed to clear status register:%d\n", ret);
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
static int ec_thermal_evt_callback(struct notifier_block *nb,
|
|
unsigned long event, void *data)
|
|
{
|
|
switch (event) {
|
|
case BYT_EC_SCI_THERMAL:
|
|
pr_info("SCI THERMAL EVENT\n");
|
|
break;
|
|
case BYT_EC_SCI_THERMTRIP:
|
|
handle_therm_trip();
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static struct thermal_zone_device_ops tzd_ops = {
|
|
.get_temp = show_temp,
|
|
.get_trip_type = show_trip_type,
|
|
.get_trip_temp = show_trip_temp,
|
|
.set_trip_temp = store_trip_temp,
|
|
.get_trip_hyst = show_trip_hyst,
|
|
.set_trip_hyst = store_trip_hyst,
|
|
};
|
|
|
|
static struct thermal_cooling_device_ops fan_cooling_ops = {
|
|
.get_max_state = fan_get_max_state,
|
|
.get_cur_state = fan_get_cur_state,
|
|
.set_cur_state = fan_set_cur_state,
|
|
};
|
|
|
|
static int byt_ec_thermal_probe(struct platform_device *pdev)
|
|
{
|
|
int i, size, ret;
|
|
struct intel_mid_thermal_platform_data *pdata;
|
|
|
|
pdata = pdev->dev.platform_data;
|
|
if (!pdata) {
|
|
dev_err(&pdev->dev, "Unable to fetch platform data\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
tdata = devm_kzalloc(&pdev->dev,
|
|
sizeof(struct thermal_data), GFP_KERNEL);
|
|
if (!tdata) {
|
|
dev_err(&pdev->dev, "kzalloc failed\n");
|
|
return -ENOMEM;
|
|
}
|
|
|
|
tdata->pdev = pdev;
|
|
tdata->num_sensors = pdata->num_sensors;
|
|
tdata->sensors = pdata->sensors;
|
|
platform_set_drvdata(pdev, tdata);
|
|
|
|
if (tdata->num_sensors != NUM_THERMAL_SENSORS)
|
|
dev_warn(&pdev->dev, "Number of sensors do not match\n");
|
|
|
|
size = sizeof(struct thermal_zone_device *) * tdata->num_sensors;
|
|
tdata->tzd = kzalloc(size, GFP_KERNEL);
|
|
if (!tdata->tzd) {
|
|
dev_err(&pdev->dev, "kzalloc failed\n");
|
|
return -ENOMEM;
|
|
}
|
|
|
|
/* Register each sensor with the generic thermal framework */
|
|
for (i = 0; i < tdata->num_sensors; i++) {
|
|
tdata->tzd[i] = thermal_zone_device_register(
|
|
tdata->sensors[i].name,
|
|
NUM_ALERT_LEVELS, ALERT_RW_MASK,
|
|
initialize_sensor(i, &tdata->sensors[i]),
|
|
&tzd_ops,
|
|
NULL, 0, 0);
|
|
if (IS_ERR(tdata->tzd[i])) {
|
|
ret = PTR_ERR(tdata->tzd[i]);
|
|
dev_err(&pdev->dev,
|
|
"registering thermal sensor %s failed: %d\n",
|
|
tdata->sensors[i].name, ret);
|
|
goto exit_reg;
|
|
}
|
|
}
|
|
|
|
/* Register Fan as a cooling device */
|
|
tdata->fan_cdev = thermal_cooling_device_register("Fan_EC", NULL,
|
|
&fan_cooling_ops);
|
|
if (IS_ERR(tdata->fan_cdev)) {
|
|
ret = PTR_ERR(tdata->fan_cdev);
|
|
tdata->fan_cdev = NULL;
|
|
goto exit_reg;
|
|
}
|
|
|
|
/* Set default Hysteresis */
|
|
ret = set_hyst_mC(DEFAULT_HYST_mC);
|
|
if (ret) {
|
|
dev_err(&pdev->dev,
|
|
"Setting default hysteresis failed:%d\n", ret);
|
|
goto exit_cdev;
|
|
}
|
|
|
|
/* Register for EC SCI events */
|
|
tdata->nb.notifier_call = &ec_thermal_evt_callback;
|
|
byt_ec_evt_register_notify(&tdata->nb);
|
|
|
|
return 0;
|
|
|
|
exit_cdev:
|
|
thermal_cooling_device_unregister(tdata->fan_cdev);
|
|
exit_reg:
|
|
while (--i >= 0)
|
|
thermal_zone_device_unregister(tdata->tzd[i]);
|
|
kfree(tdata->tzd);
|
|
return ret;
|
|
}
|
|
|
|
static int byt_ec_thermal_resume(struct device *dev)
|
|
{
|
|
dev_info(dev, "resume called.\n");
|
|
return 0;
|
|
}
|
|
|
|
static int byt_ec_thermal_suspend(struct device *dev)
|
|
{
|
|
dev_info(dev, "suspend called.\n");
|
|
return 0;
|
|
}
|
|
|
|
static int byt_ec_thermal_remove(struct platform_device *pdev)
|
|
{
|
|
int i;
|
|
struct thermal_data *tdata = platform_get_drvdata(pdev);
|
|
|
|
if (!tdata)
|
|
return 0;
|
|
|
|
for (i = 0; i < tdata->num_sensors; i++)
|
|
thermal_zone_device_unregister(tdata->tzd[i]);
|
|
|
|
kfree(tdata->tzd);
|
|
thermal_cooling_device_unregister(tdata->fan_cdev);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*********************************************************************
|
|
* Driver initialization and finalization
|
|
*********************************************************************/
|
|
|
|
static const struct dev_pm_ops thermal_pm_ops = {
|
|
.suspend = byt_ec_thermal_suspend,
|
|
.resume = byt_ec_thermal_resume,
|
|
};
|
|
|
|
static struct platform_driver byt_ec_thermal_driver = {
|
|
.driver = {
|
|
.name = DEVICE_NAME,
|
|
.owner = THIS_MODULE,
|
|
.pm = &thermal_pm_ops,
|
|
},
|
|
.probe = byt_ec_thermal_probe,
|
|
.remove = byt_ec_thermal_remove,
|
|
};
|
|
|
|
static int byt_ec_thermal_module_init(void)
|
|
{
|
|
return platform_driver_register(&byt_ec_thermal_driver);
|
|
}
|
|
|
|
static void byt_ec_thermal_module_exit(void)
|
|
{
|
|
platform_driver_unregister(&byt_ec_thermal_driver);
|
|
}
|
|
|
|
late_initcall(byt_ec_thermal_module_init);
|
|
module_exit(byt_ec_thermal_module_exit);
|
|
|
|
MODULE_AUTHOR("Durgadoss R <durgadoss.r@intel.com>");
|
|
MODULE_DESCRIPTION("Intel Baytrail-M Platform Thermal Driver");
|
|
MODULE_LICENSE("GPL");
|