| /* |
| * 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"); |