1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
|
// SPDX-License-Identifier: GPL-2.0+
/*
* GPD Pocket fan controller driver
*
* Copyright (C) 2017 Hans de Goede <hdegoede@redhat.com>
*/
#include <linux/acpi.h>
#include <linux/devm-helpers.h>
#include <linux/gpio/consumer.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/platform_device.h>
#include <linux/power_supply.h>
#include <linux/thermal.h>
#include <linux/workqueue.h>
#define MAX_SPEED 3
#define TEMP_LIMIT0_DEFAULT 55000
#define TEMP_LIMIT1_DEFAULT 60000
#define TEMP_LIMIT2_DEFAULT 65000
#define HYSTERESIS_DEFAULT 3000
#define SPEED_ON_AC_DEFAULT 2
static int temp_limits[3] = {
TEMP_LIMIT0_DEFAULT, TEMP_LIMIT1_DEFAULT, TEMP_LIMIT2_DEFAULT,
};
module_param_array(temp_limits, int, NULL, 0444);
MODULE_PARM_DESC(temp_limits,
"Millicelsius values above which the fan speed increases");
static int hysteresis = HYSTERESIS_DEFAULT;
module_param(hysteresis, int, 0444);
MODULE_PARM_DESC(hysteresis,
"Hysteresis in millicelsius before lowering the fan speed");
static int speed_on_ac = SPEED_ON_AC_DEFAULT;
module_param(speed_on_ac, int, 0444);
MODULE_PARM_DESC(speed_on_ac,
"minimum fan speed to allow when system is powered by AC");
struct gpd_pocket_fan_data {
struct device *dev;
struct thermal_zone_device *dts0;
struct thermal_zone_device *dts1;
struct gpio_desc *gpio0;
struct gpio_desc *gpio1;
struct delayed_work work;
int last_speed;
};
static void gpd_pocket_fan_set_speed(struct gpd_pocket_fan_data *fan, int speed)
{
if (speed == fan->last_speed)
return;
gpiod_direction_output(fan->gpio0, !!(speed & 1));
gpiod_direction_output(fan->gpio1, !!(speed & 2));
fan->last_speed = speed;
}
static int gpd_pocket_fan_min_speed(void)
{
if (power_supply_is_system_supplied())
return speed_on_ac;
else
return 0;
}
static void gpd_pocket_fan_worker(struct work_struct *work)
{
struct gpd_pocket_fan_data *fan =
container_of(work, struct gpd_pocket_fan_data, work.work);
int t0, t1, temp, speed, min_speed, i;
if (thermal_zone_get_temp(fan->dts0, &t0) ||
thermal_zone_get_temp(fan->dts1, &t1)) {
dev_warn(fan->dev, "Error getting temperature\n");
speed = MAX_SPEED;
goto set_speed;
}
temp = max(t0, t1);
speed = fan->last_speed;
min_speed = gpd_pocket_fan_min_speed();
/* Determine minimum speed */
for (i = min_speed; i < ARRAY_SIZE(temp_limits); i++) {
if (temp < temp_limits[i])
break;
}
if (speed < i)
speed = i;
/* Use hysteresis before lowering speed again */
for (i = min_speed; i < ARRAY_SIZE(temp_limits); i++) {
if (temp <= (temp_limits[i] - hysteresis))
break;
}
if (speed > i)
speed = i;
if (fan->last_speed <= 0 && speed)
speed = MAX_SPEED; /* kick start motor */
set_speed:
gpd_pocket_fan_set_speed(fan, speed);
/* When mostly idle (low temp/speed), slow down the poll interval. */
queue_delayed_work(system_wq, &fan->work,
msecs_to_jiffies(4000 / (speed + 1)));
}
static void gpd_pocket_fan_force_update(struct gpd_pocket_fan_data *fan)
{
fan->last_speed = -1;
mod_delayed_work(system_wq, &fan->work, 0);
}
static int gpd_pocket_fan_probe(struct platform_device *pdev)
{
struct gpd_pocket_fan_data *fan;
int i, ret;
for (i = 0; i < ARRAY_SIZE(temp_limits); i++) {
if (temp_limits[i] < 20000 || temp_limits[i] > 90000) {
dev_err(&pdev->dev, "Invalid temp-limit %d (must be between 20000 and 90000)\n",
temp_limits[i]);
temp_limits[0] = TEMP_LIMIT0_DEFAULT;
temp_limits[1] = TEMP_LIMIT1_DEFAULT;
temp_limits[2] = TEMP_LIMIT2_DEFAULT;
break;
}
}
if (hysteresis < 1000 || hysteresis > 10000) {
dev_err(&pdev->dev, "Invalid hysteresis %d (must be between 1000 and 10000)\n",
hysteresis);
hysteresis = HYSTERESIS_DEFAULT;
}
if (speed_on_ac < 0 || speed_on_ac > MAX_SPEED) {
dev_err(&pdev->dev, "Invalid speed_on_ac %d (must be between 0 and 3)\n",
speed_on_ac);
speed_on_ac = SPEED_ON_AC_DEFAULT;
}
fan = devm_kzalloc(&pdev->dev, sizeof(*fan), GFP_KERNEL);
if (!fan)
return -ENOMEM;
fan->dev = &pdev->dev;
ret = devm_delayed_work_autocancel(&pdev->dev, &fan->work,
gpd_pocket_fan_worker);
if (ret)
return ret;
/* Note this returns a "weak" reference which we don't need to free */
fan->dts0 = thermal_zone_get_zone_by_name("soc_dts0");
if (IS_ERR(fan->dts0))
return -EPROBE_DEFER;
fan->dts1 = thermal_zone_get_zone_by_name("soc_dts1");
if (IS_ERR(fan->dts1))
return -EPROBE_DEFER;
fan->gpio0 = devm_gpiod_get_index(fan->dev, NULL, 0, GPIOD_ASIS);
if (IS_ERR(fan->gpio0))
return PTR_ERR(fan->gpio0);
fan->gpio1 = devm_gpiod_get_index(fan->dev, NULL, 1, GPIOD_ASIS);
if (IS_ERR(fan->gpio1))
return PTR_ERR(fan->gpio1);
gpd_pocket_fan_force_update(fan);
platform_set_drvdata(pdev, fan);
return 0;
}
#ifdef CONFIG_PM_SLEEP
static int gpd_pocket_fan_suspend(struct device *dev)
{
struct gpd_pocket_fan_data *fan = dev_get_drvdata(dev);
cancel_delayed_work_sync(&fan->work);
gpd_pocket_fan_set_speed(fan, gpd_pocket_fan_min_speed());
return 0;
}
static int gpd_pocket_fan_resume(struct device *dev)
{
struct gpd_pocket_fan_data *fan = dev_get_drvdata(dev);
gpd_pocket_fan_force_update(fan);
return 0;
}
#endif
static SIMPLE_DEV_PM_OPS(gpd_pocket_fan_pm_ops,
gpd_pocket_fan_suspend,
gpd_pocket_fan_resume);
static struct acpi_device_id gpd_pocket_fan_acpi_match[] = {
{ "FAN02501" },
{},
};
MODULE_DEVICE_TABLE(acpi, gpd_pocket_fan_acpi_match);
static struct platform_driver gpd_pocket_fan_driver = {
.probe = gpd_pocket_fan_probe,
.driver = {
.name = "gpd_pocket_fan",
.acpi_match_table = gpd_pocket_fan_acpi_match,
.pm = &gpd_pocket_fan_pm_ops,
},
};
module_platform_driver(gpd_pocket_fan_driver);
MODULE_AUTHOR("Hans de Goede <hdegoede@redhat.com");
MODULE_DESCRIPTION("GPD pocket fan driver");
MODULE_LICENSE("GPL");
|