xref: /linux/drivers/hwmon/yogafan.c (revision 0fc8f6200d2313278fbf4539bbab74677c685531)
1 // SPDX-License-Identifier: GPL-2.0-only
2 /**
3  * yoga_fan.c - Lenovo Yoga/Legion Fan Hardware Monitoring Driver
4  *
5  * Provides fan speed monitoring for Lenovo Yoga, Legion, and IdeaPad
6  * laptops by interfacing with the Embedded Controller (EC) via ACPI.
7  *
8  * The driver implements a passive discrete-time first-order lag filter
9  * with slew-rate limiting (RLLag). This addresses low-resolution
10  * tachometer sampling in the EC by smoothing RPM readings based on
11  * the time delta (dt) between userspace requests, ensuring physical
12  * consistency without background task overhead or race conditions.
13  * The filter implements multirate filtering with autoreset in case
14  * of large sampling time.
15  *
16  * Copyright (C) 2021-2026 Sergio Melas <sergiomelas@gmail.com>
17  */
18 #include <linux/acpi.h>
19 #include <linux/dmi.h>
20 #include <linux/err.h>
21 #include <linux/hwmon.h>
22 #include <linux/ktime.h>
23 #include <linux/module.h>
24 #include <linux/platform_device.h>
25 #include <linux/slab.h>
26 #include <linux/math64.h>
27 
28 /* Driver Configuration Constants */
29 #define DRVNAME			"yogafan"
30 #define MAX_FANS		8
31 
32 /* Filter Configuration Constants */
33 #define TAU_MS			1000	/* Time constant for the first-order lag (ms) */
34 #define MAX_SLEW_RPM_S		1500	/* Maximum allowed change in RPM per second */
35 #define MAX_SAMPLING		5000	/* Maximum allowed Ts for reset (ms) */
36 #define MIN_SAMPLING		100	/* Minimum interval between filter updates (ms) */
37 
38 /* RPM Sanitation Constants */
39 #define RPM_FLOOR_LIMIT		50	/* Snap filtered value to 0 if raw is 0 */
40 
41 struct yogafan_config {
42 	int multiplier;
43 	int fan_count;
44 	const char *paths[2];
45 };
46 
47 struct yoga_fan_data {
48 	acpi_handle active_handles[MAX_FANS];
49 	long filtered_val[MAX_FANS];
50 	ktime_t last_sample[MAX_FANS];
51 	int multiplier;
52 	int fan_count;
53 };
54 
55 /* Specific configurations mapped via DMI */
56 static const struct yogafan_config yoga_8bit_fans_cfg = {
57 	.multiplier = 100,
58 	.fan_count = 1,
59 	.paths = { "\\_SB.PCI0.LPC0.EC0.FANS", NULL }
60 };
61 
62 static const struct yogafan_config ideapad_8bit_fan0_cfg = {
63 	.multiplier = 100,
64 	.fan_count = 1,
65 	.paths = { "\\_SB.PCI0.LPC0.EC0.FAN0", NULL }
66 };
67 
68 static const struct yogafan_config legion_16bit_dual_cfg = {
69 	.multiplier = 1,
70 	.fan_count = 2,
71 	.paths = { "\\_SB.PCI0.LPC0.EC0.FANS", "\\_SB.PCI0.LPC0.EC0.FA2S" }
72 };
73 
74 static void apply_rllag_filter(struct yoga_fan_data *data, int idx, long raw_rpm)
75 {
76 	ktime_t now = ktime_get_boottime();
77 	s64 dt_ms = ktime_to_ms(ktime_sub(now, data->last_sample[idx]));
78 	long delta, step, limit, alpha;
79 	s64 temp_num;
80 
81 	if (raw_rpm < RPM_FLOOR_LIMIT) {
82 		data->filtered_val[idx] = 0;
83 		data->last_sample[idx] = now;
84 		return;
85 	}
86 
87 	if (data->last_sample[idx] == 0 || dt_ms > MAX_SAMPLING) {
88 		data->filtered_val[idx] = raw_rpm;
89 		data->last_sample[idx] = now;
90 		return;
91 	}
92 
93 	if (dt_ms < MIN_SAMPLING)
94 		return;
95 
96 	delta = raw_rpm - data->filtered_val[idx];
97 	if (delta == 0) {
98 		data->last_sample[idx] = now;
99 		return;
100 	}
101 
102 	temp_num = dt_ms << 12;
103 	alpha = (long)div64_s64(temp_num, (s64)(TAU_MS + dt_ms));
104 	step = (delta * alpha) >> 12;
105 
106 	if (step == 0 && delta != 0)
107 		step = (delta > 0) ? 1 : -1;
108 
109 	limit = (MAX_SLEW_RPM_S * (long)dt_ms) / 1000;
110 	if (limit < 1)
111 		limit = 1;
112 
113 	if (step > limit)
114 		step = limit;
115 	else if (step < -limit)
116 		step = -limit;
117 
118 	data->filtered_val[idx] += step;
119 	data->last_sample[idx] = now;
120 }
121 
122 static int yoga_fan_read(struct device *dev, enum hwmon_sensor_types type,
123 			 u32 attr, int channel, long *val)
124 {
125 	struct yoga_fan_data *data = dev_get_drvdata(dev);
126 	unsigned long long raw_acpi;
127 	acpi_status status;
128 
129 	if (type != hwmon_fan || attr != hwmon_fan_input)
130 		return -EOPNOTSUPP;
131 
132 	status = acpi_evaluate_integer(data->active_handles[channel], NULL, NULL, &raw_acpi);
133 	if (ACPI_FAILURE(status))
134 		return -EIO;
135 
136 	apply_rllag_filter(data, channel, (long)raw_acpi * data->multiplier);
137 	*val = data->filtered_val[channel];
138 
139 	return 0;
140 }
141 
142 static umode_t yoga_fan_is_visible(const void *data, enum hwmon_sensor_types type,
143 				   u32 attr, int channel)
144 {
145 	const struct yoga_fan_data *fan_data = data;
146 
147 	if (type == hwmon_fan && channel < fan_data->fan_count)
148 		return 0444;
149 
150 	return 0;
151 }
152 
153 static const struct hwmon_ops yoga_fan_hwmon_ops = {
154 	.is_visible = yoga_fan_is_visible,
155 	.read = yoga_fan_read,
156 };
157 
158 static const struct hwmon_channel_info *yoga_fan_info[] = {
159 	HWMON_CHANNEL_INFO(fan,
160 			   HWMON_F_INPUT, HWMON_F_INPUT,
161 			   HWMON_F_INPUT, HWMON_F_INPUT,
162 			   HWMON_F_INPUT, HWMON_F_INPUT,
163 			   HWMON_F_INPUT, HWMON_F_INPUT),
164 	NULL
165 };
166 
167 static const struct hwmon_chip_info yoga_fan_chip_info = {
168 	.ops = &yoga_fan_hwmon_ops,
169 	.info = yoga_fan_info,
170 };
171 
172 static const struct dmi_system_id yogafan_quirks[] = {
173 	{
174 		.ident = "Lenovo Yoga",
175 		.matches = {
176 			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
177 			DMI_MATCH(DMI_PRODUCT_FAMILY, "Yoga"),
178 		},
179 		.driver_data = (void *)&yoga_8bit_fans_cfg,
180 	},
181 	{
182 		.ident = "Lenovo Legion",
183 		.matches = {
184 			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
185 			DMI_MATCH(DMI_PRODUCT_FAMILY, "Legion"),
186 		},
187 		.driver_data = (void *)&legion_16bit_dual_cfg,
188 	},
189 	{
190 		.ident = "Lenovo IdeaPad",
191 		.matches = {
192 			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
193 			DMI_MATCH(DMI_PRODUCT_FAMILY, "IdeaPad"),
194 		},
195 		.driver_data = (void *)&ideapad_8bit_fan0_cfg,
196 	},
197 	{ }
198 };
199 MODULE_DEVICE_TABLE(dmi, yogafan_quirks);
200 
201 static int yoga_fan_probe(struct platform_device *pdev)
202 {
203 	const struct dmi_system_id *dmi_id;
204 	const struct yogafan_config *cfg;
205 	struct yoga_fan_data *data;
206 	struct device *hwmon_dev;
207 	int i;
208 
209 	dmi_id = dmi_first_match(yogafan_quirks);
210 	if (!dmi_id)
211 		return -ENODEV;
212 
213 	cfg = dmi_id->driver_data;
214 	data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
215 	if (!data)
216 		return -ENOMEM;
217 
218 	data->multiplier = cfg->multiplier;
219 
220 	for (i = 0; i < cfg->fan_count; i++) {
221 		acpi_status status;
222 
223 		status = acpi_get_handle(NULL, (char *)cfg->paths[i],
224 					 &data->active_handles[data->fan_count]);
225 		if (ACPI_SUCCESS(status))
226 			data->fan_count++;
227 	}
228 
229 	if (data->fan_count == 0)
230 		return -ENODEV;
231 
232 	hwmon_dev = devm_hwmon_device_register_with_info(&pdev->dev, DRVNAME,
233 							 data, &yoga_fan_chip_info, NULL);
234 
235 	return PTR_ERR_OR_ZERO(hwmon_dev);
236 }
237 
238 static struct platform_driver yoga_fan_driver = {
239 	.driver = { .name = DRVNAME },
240 	.probe = yoga_fan_probe,
241 };
242 
243 static struct platform_device *yoga_fan_device;
244 
245 static int __init yoga_fan_init(void)
246 {
247 	int ret;
248 
249 	if (!dmi_check_system(yogafan_quirks))
250 		return -ENODEV;
251 
252 	ret = platform_driver_register(&yoga_fan_driver);
253 	if (ret)
254 		return ret;
255 
256 	yoga_fan_device = platform_device_register_simple(DRVNAME, -1, NULL, 0);
257 	if (IS_ERR(yoga_fan_device)) {
258 		platform_driver_unregister(&yoga_fan_driver);
259 		return PTR_ERR(yoga_fan_device);
260 	}
261 	return 0;
262 }
263 
264 static void __exit yoga_fan_exit(void)
265 {
266 	platform_device_unregister(yoga_fan_device);
267 	platform_driver_unregister(&yoga_fan_driver);
268 }
269 
270 module_init(yoga_fan_init);
271 module_exit(yoga_fan_exit);
272 
273 MODULE_AUTHOR("Sergio Melas <sergiomelas@gmail.com>");
274 MODULE_DESCRIPTION("Lenovo Yoga/Legion Fan Monitor Driver");
275 MODULE_LICENSE("GPL");
276