| /* |
| * drivers/misc/therm_fan_est.c |
| * |
| * Copyright (c) 2013-2014, NVIDIA CORPORATION. All rights reserved. |
| * |
| * This software is licensed under the terms of the GNU General Public |
| * License version 2, as published by the Free Software Foundation, and |
| * may be copied, distributed, and modified under those terms. |
| * |
| * 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. |
| * |
| */ |
| |
| #include <linux/platform_device.h> |
| #include <linux/kernel.h> |
| #include <linux/cpufreq.h> |
| #include <linux/delay.h> |
| #include <linux/mutex.h> |
| #include <linux/init.h> |
| #include <linux/err.h> |
| #include <linux/clk.h> |
| #include <linux/debugfs.h> |
| #include <linux/seq_file.h> |
| #include <linux/uaccess.h> |
| #include <linux/slab.h> |
| #include <linux/syscalls.h> |
| #include <linux/therm_est.h> |
| #include <linux/thermal.h> |
| #include <linux/module.h> |
| #include <linux/hwmon-sysfs.h> |
| |
| #define DEFERRED_RESUME_TIME 3000 |
| #define DEBUG 0 |
| struct therm_fan_estimator { |
| long cur_temp; |
| #if DEBUG |
| long cur_temp_debug; |
| #endif |
| long polling_period; |
| struct workqueue_struct *workqueue; |
| struct delayed_work therm_fan_est_work; |
| long toffset; |
| int ntemp; |
| int ndevs; |
| struct therm_fan_est_subdevice *devs; |
| struct thermal_zone_device *thz; |
| int current_trip_index; |
| char *cdev_type; |
| int active_trip_temps[MAX_ACTIVE_STATES]; |
| int active_hysteresis[MAX_ACTIVE_STATES]; |
| int active_trip_temps_hyst[(MAX_ACTIVE_STATES << 1) + 1]; |
| }; |
| |
| |
| static void fan_set_trip_temp_hyst(struct therm_fan_estimator *est, int trip, |
| unsigned long hyst_temp, |
| unsigned long trip_temp) |
| { |
| est->active_hysteresis[trip] = hyst_temp; |
| est->active_trip_temps[trip] = trip_temp; |
| est->active_trip_temps_hyst[(trip << 1)] = trip_temp; |
| est->active_trip_temps_hyst[((trip - 1) << 1) + 1] = |
| trip_temp - hyst_temp; |
| } |
| |
| static void therm_fan_est_work_func(struct work_struct *work) |
| { |
| int i, j, index, trip_index, sum = 0; |
| long temp = 0; |
| struct delayed_work *dwork = container_of(work, |
| struct delayed_work, work); |
| struct therm_fan_estimator *est = container_of( |
| dwork, |
| struct therm_fan_estimator, |
| therm_fan_est_work); |
| |
| for (i = 0; i < est->ndevs; i++) { |
| if (est->devs[i].get_temp(est->devs[i].dev_data, &temp)) |
| continue; |
| est->devs[i].hist[(est->ntemp % HIST_LEN)] = temp; |
| } |
| |
| for (i = 0; i < est->ndevs; i++) { |
| for (j = 0; j < HIST_LEN; j++) { |
| index = (est->ntemp - j + HIST_LEN) % HIST_LEN; |
| sum += est->devs[i].hist[index] * |
| est->devs[i].coeffs[j]; |
| } |
| } |
| #if !DEBUG |
| est->cur_temp = sum / 100 + est->toffset; |
| #else |
| est->cur_temp = est->cur_temp_debug; |
| #endif |
| for (trip_index = 0; |
| trip_index < ((MAX_ACTIVE_STATES << 1) + 1); trip_index++) { |
| if (est->cur_temp < est->active_trip_temps_hyst[trip_index]) |
| break; |
| } |
| if (est->current_trip_index != (trip_index - 1)) { |
| if (!((trip_index - 1) % 2) || (!est->current_trip_index) || |
| ((trip_index - est->current_trip_index) >= 2) || |
| ((trip_index - est->current_trip_index) <= -2)) { |
| pr_info("%s, cur_temp:%ld, cur_trip_index:%d", |
| __func__, est->cur_temp, est->current_trip_index); |
| thermal_zone_device_update(est->thz); |
| } |
| est->current_trip_index = trip_index - 1; |
| } |
| |
| est->ntemp++; |
| queue_delayed_work(est->workqueue, &est->therm_fan_est_work, |
| msecs_to_jiffies(est->polling_period)); |
| } |
| |
| static int therm_fan_est_bind(struct thermal_zone_device *thz, |
| struct thermal_cooling_device *cdev) |
| { |
| int i; |
| struct therm_fan_estimator *est = thz->devdata; |
| if (!strcmp(cdev->type, est->cdev_type)) { |
| for (i = 0; i < MAX_ACTIVE_STATES; i++) |
| thermal_zone_bind_cooling_device(thz, i, cdev, i, i); |
| } |
| |
| return 0; |
| } |
| |
| static int therm_fan_est_unbind(struct thermal_zone_device *thz, |
| struct thermal_cooling_device *cdev) |
| { |
| int i; |
| struct therm_fan_estimator *est = thz->devdata; |
| if (!strcmp(cdev->type, est->cdev_type)) { |
| for (i = 0; i < MAX_ACTIVE_STATES; i++) |
| thermal_zone_unbind_cooling_device(thz, i, cdev); |
| } |
| |
| return 0; |
| } |
| |
| static int therm_fan_est_get_trip_type(struct thermal_zone_device *thz, |
| int trip, |
| enum thermal_trip_type *type) |
| { |
| *type = THERMAL_TRIP_ACTIVE; |
| return 0; |
| } |
| |
| static int therm_fan_est_get_trip_temp(struct thermal_zone_device *thz, |
| int trip, long *temp) |
| { |
| struct therm_fan_estimator *est = thz->devdata; |
| |
| if (est->current_trip_index == 0) |
| *temp = 0; |
| |
| if (trip * 2 <= est->current_trip_index) /* tripped then lower */ |
| *temp = est->active_trip_temps_hyst[trip * 2 - 1]; |
| else /* not tripped, then upper */ |
| *temp = est->active_trip_temps_hyst[trip * 2]; |
| |
| return 0; |
| } |
| |
| static int therm_fan_est_set_trip_temp(struct thermal_zone_device *thz, |
| int trip, long temp) |
| { |
| struct therm_fan_estimator *est = thz->devdata; |
| |
| /*Need trip 0 to remain as it is*/ |
| if (((temp - est->active_hysteresis[trip]) < 0) || (trip <= 0)) |
| return -EINVAL; |
| |
| fan_set_trip_temp_hyst(est, trip, est->active_hysteresis[trip], temp); |
| return 0; |
| } |
| |
| static int therm_fan_est_get_temp(struct thermal_zone_device *thz, long *temp) |
| { |
| struct therm_fan_estimator *est = thz->devdata; |
| |
| *temp = est->cur_temp; |
| return 0; |
| } |
| |
| static int therm_fan_est_set_trip_hyst(struct thermal_zone_device *thz, |
| int trip, long hyst_temp) |
| { |
| struct therm_fan_estimator *est = thz->devdata; |
| |
| /*Need trip 0 to remain as it is*/ |
| if ((est->active_trip_temps[trip] - hyst_temp) < 0 || trip <= 0) |
| return -EINVAL; |
| |
| fan_set_trip_temp_hyst(est, trip, |
| hyst_temp, est->active_trip_temps[trip]); |
| return 0; |
| } |
| |
| static int therm_fan_est_get_trip_hyst(struct thermal_zone_device *thz, |
| int trip, long *temp) |
| { |
| struct therm_fan_estimator *est = thz->devdata; |
| |
| *temp = est->active_hysteresis[trip]; |
| return 0; |
| } |
| |
| static struct thermal_zone_device_ops therm_fan_est_ops = { |
| .bind = therm_fan_est_bind, |
| .unbind = therm_fan_est_unbind, |
| .get_trip_type = therm_fan_est_get_trip_type, |
| .get_trip_temp = therm_fan_est_get_trip_temp, |
| .get_temp = therm_fan_est_get_temp, |
| .set_trip_temp = therm_fan_est_set_trip_temp, |
| .get_trip_hyst = therm_fan_est_get_trip_hyst, |
| .set_trip_hyst = therm_fan_est_set_trip_hyst, |
| }; |
| |
| static ssize_t show_coeff(struct device *dev, |
| struct device_attribute *da, |
| char *buf) |
| { |
| struct therm_fan_estimator *est = dev_get_drvdata(dev); |
| ssize_t len, total_len = 0; |
| int i, j; |
| |
| for (i = 0; i < est->ndevs; i++) { |
| len = snprintf(buf + total_len, PAGE_SIZE, "[%d]", i); |
| total_len += len; |
| for (j = 0; j < HIST_LEN; j++) { |
| len = snprintf(buf + total_len, PAGE_SIZE, " %ld", |
| est->devs[i].coeffs[j]); |
| total_len += len; |
| } |
| len = snprintf(buf + total_len, PAGE_SIZE, "\n"); |
| total_len += len; |
| } |
| return strlen(buf); |
| } |
| |
| static ssize_t set_coeff(struct device *dev, |
| struct device_attribute *da, |
| const char *buf, size_t count) |
| { |
| struct therm_fan_estimator *est = dev_get_drvdata(dev); |
| int devid, scount; |
| long coeff[20]; |
| |
| if (HIST_LEN > 20) |
| return -EINVAL; |
| |
| scount = sscanf(buf, "[%d] %ld %ld %ld %ld %ld %ld %ld %ld %ld %ld " \ |
| "%ld %ld %ld %ld %ld %ld %ld %ld %ld %ld", |
| &devid, &coeff[0], &coeff[1], &coeff[2], &coeff[3], |
| &coeff[4], &coeff[5], &coeff[6], &coeff[7], &coeff[8], |
| &coeff[9], &coeff[10], &coeff[11], &coeff[12], |
| &coeff[13], &coeff[14], &coeff[15], &coeff[16], |
| &coeff[17], &coeff[18], &coeff[19]); |
| |
| if (scount != HIST_LEN + 1) |
| return -1; |
| |
| if (devid < 0 || devid >= est->ndevs) |
| return -EINVAL; |
| |
| /* This has obvious locking issues but don't worry about it */ |
| memcpy(est->devs[devid].coeffs, coeff, sizeof(long) * HIST_LEN); |
| |
| return count; |
| } |
| |
| static ssize_t show_offset(struct device *dev, |
| struct device_attribute *da, |
| char *buf) |
| { |
| struct therm_fan_estimator *est = dev_get_drvdata(dev); |
| |
| snprintf(buf, PAGE_SIZE, "%ld\n", est->toffset); |
| return strlen(buf); |
| } |
| |
| static ssize_t set_offset(struct device *dev, |
| struct device_attribute *da, |
| const char *buf, size_t count) |
| { |
| struct therm_fan_estimator *est = dev_get_drvdata(dev); |
| int offset; |
| |
| if (kstrtoint(buf, 0, &offset)) |
| return -EINVAL; |
| |
| est->toffset = offset; |
| |
| return count; |
| } |
| |
| static ssize_t show_temps(struct device *dev, |
| struct device_attribute *da, |
| char *buf) |
| { |
| struct therm_fan_estimator *est = dev_get_drvdata(dev); |
| ssize_t total_len = 0; |
| int i, j; |
| int index; |
| |
| /* This has obvious locking issues but don't worry about it */ |
| for (i = 0; i < est->ndevs; i++) { |
| total_len += snprintf(buf + total_len, PAGE_SIZE, "[%d]", i); |
| for (j = 0; j < HIST_LEN; j++) { |
| index = (est->ntemp - j + HIST_LEN) % HIST_LEN; |
| total_len += snprintf(buf + total_len, |
| PAGE_SIZE, |
| " %ld", |
| est->devs[i].hist[index]); |
| } |
| total_len += snprintf(buf + total_len, PAGE_SIZE, "\n"); |
| } |
| return strlen(buf); |
| } |
| |
| #if DEBUG |
| static ssize_t set_temps(struct device *dev, |
| struct device_attribute *da, |
| const char *buf, size_t count) |
| { |
| struct therm_fan_estimator *est = dev_get_drvdata(dev); |
| int temp; |
| |
| if (kstrtoint(buf, 0, &temp)) |
| return -EINVAL; |
| |
| est->cur_temp_debug = temp; |
| |
| return count; |
| } |
| #endif |
| |
| static struct sensor_device_attribute therm_fan_est_nodes[] = { |
| SENSOR_ATTR(coeff, S_IRUGO | S_IWUSR, show_coeff, set_coeff, 0), |
| SENSOR_ATTR(offset, S_IRUGO | S_IWUSR, show_offset, set_offset, 0), |
| #if DEBUG |
| SENSOR_ATTR(temps, S_IRUGO | S_IWUSR, show_temps, set_temps, 0), |
| #else |
| SENSOR_ATTR(temps, S_IRUGO, show_temps, 0, 0), |
| #endif |
| }; |
| |
| static int therm_fan_est_probe(struct platform_device *pdev) |
| { |
| int i, j; |
| long temp; |
| struct therm_fan_estimator *est; |
| struct therm_fan_est_subdevice *dev; |
| struct therm_fan_est_data *data; |
| |
| est = devm_kzalloc(&pdev->dev, |
| sizeof(struct therm_fan_estimator), GFP_KERNEL); |
| if (IS_ERR_OR_NULL(est)) |
| return -ENOMEM; |
| |
| platform_set_drvdata(pdev, est); |
| |
| data = pdev->dev.platform_data; |
| |
| est->devs = data->devs; |
| est->ndevs = data->ndevs; |
| est->toffset = data->toffset; |
| est->polling_period = data->polling_period; |
| |
| for (i = 0; i < MAX_ACTIVE_STATES; i++) { |
| est->active_trip_temps[i] = data->active_trip_temps[i]; |
| est->active_hysteresis[i] = data->active_hysteresis[i]; |
| } |
| |
| est->active_trip_temps_hyst[0] = data->active_trip_temps[0]; |
| |
| for (i = 1; i < MAX_ACTIVE_STATES; i++) |
| fan_set_trip_temp_hyst(est, i, |
| data->active_hysteresis[i], est->active_trip_temps[i]); |
| |
| /* initialize history */ |
| for (i = 0; i < data->ndevs; i++) { |
| dev = &est->devs[i]; |
| |
| if (dev->get_temp(dev->dev_data, &temp)) |
| return -EINVAL; |
| |
| for (j = 0; j < HIST_LEN; j++) |
| dev->hist[j] = temp; |
| } |
| |
| est->workqueue = alloc_workqueue(dev_name(&pdev->dev), |
| WQ_HIGHPRI | WQ_UNBOUND, 1); |
| if (!est->workqueue) |
| return -ENOMEM; |
| |
| est->current_trip_index = 0; |
| |
| INIT_DELAYED_WORK(&est->therm_fan_est_work, therm_fan_est_work_func); |
| |
| queue_delayed_work(est->workqueue, |
| &est->therm_fan_est_work, |
| msecs_to_jiffies(est->polling_period)); |
| est->cdev_type = data->cdev_type; |
| est->thz = thermal_zone_device_register((char *) dev_name(&pdev->dev), |
| 10, 0x3FF, est, |
| &therm_fan_est_ops, data->tzp, 0, 0); |
| if (IS_ERR_OR_NULL(est->thz)) |
| return -EINVAL; |
| for (i = 0; i < ARRAY_SIZE(therm_fan_est_nodes); i++) |
| device_create_file(&pdev->dev, |
| &therm_fan_est_nodes[i].dev_attr); |
| |
| return 0; |
| } |
| |
| static int therm_fan_est_remove(struct platform_device *pdev) |
| { |
| struct therm_fan_estimator *est = platform_get_drvdata(pdev); |
| |
| if (!est) |
| return -EINVAL; |
| |
| cancel_delayed_work(&est->therm_fan_est_work); |
| thermal_zone_device_unregister(est->thz); |
| |
| return 0; |
| } |
| |
| #if CONFIG_PM |
| static int therm_fan_est_suspend(struct platform_device *pdev, |
| pm_message_t state) |
| { |
| struct therm_fan_estimator *est = platform_get_drvdata(pdev); |
| |
| if (!est) |
| return -EINVAL; |
| |
| pr_info("therm-fan-est: %s, cur_temp:%ld", __func__, est->cur_temp); |
| cancel_delayed_work(&est->therm_fan_est_work); |
| est->current_trip_index = 0; |
| |
| return 0; |
| } |
| |
| static int therm_fan_est_resume(struct platform_device *pdev) |
| { |
| struct therm_fan_estimator *est = platform_get_drvdata(pdev); |
| |
| if (!est) |
| return -EINVAL; |
| pr_info("therm-fan-est: %s, cur_temp:%ld", __func__, est->cur_temp); |
| |
| queue_delayed_work(est->workqueue, |
| &est->therm_fan_est_work, |
| msecs_to_jiffies(DEFERRED_RESUME_TIME)); |
| return 0; |
| } |
| #endif |
| |
| static struct platform_driver therm_fan_est_driver = { |
| .driver = { |
| .owner = THIS_MODULE, |
| .name = "therm-fan-est", |
| }, |
| .probe = therm_fan_est_probe, |
| .remove = therm_fan_est_remove, |
| #if CONFIG_PM |
| .suspend = therm_fan_est_suspend, |
| .resume = therm_fan_est_resume, |
| #endif |
| }; |
| |
| module_platform_driver(therm_fan_est_driver); |
| |
| MODULE_DESCRIPTION("fan thermal estimator"); |
| MODULE_AUTHOR("Anshul Jain <anshulj@nvidia.com>"); |
| MODULE_LICENSE("GPL v2"); |