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
57 static const struct yogafan_config yoga_8bit_fans_cfg = {
58 .multiplier = 100,
59 .fan_count = 1,
60 .paths = { "\\_SB.PCI0.LPC0.EC0.FANS", NULL }
61 };
62
63 static const struct yogafan_config ideapad_8bit_fan0_cfg = {
64 .multiplier = 100,
65 .fan_count = 1,
66 .paths = { "\\_SB.PCI0.LPC0.EC0.FAN0", NULL }
67 };
68
69 static const struct yogafan_config legion_16bit_dual_cfg = {
70 .multiplier = 1,
71 .fan_count = 2,
72 .paths = { "\\_SB.PCI0.LPC0.EC0.FANS", "\\_SB.PCI0.LPC0.EC0.FA2S" }
73 };
74
75 static const struct yogafan_config loq_15iax9_8bit_dual_cfg = {
76 .multiplier = 100,
77 .fan_count = 2,
78 .paths = { "\\_SB.PC00.LPCB.EC0.FA1S", "\\_SB.PC00.LPCB.EC0.FA2S" }
79 };
80
81 static const struct yogafan_config xiaoxin_8bit_dual_cfg = {
82 .multiplier = 100,
83 .fan_count = 2,
84 .paths = { "\\_SB.PCI0.LPC0.EC0.FANS", "\\_SB.PCI0.LPC0.EC0.FA2S" }
85 };
86
87 static const struct yogafan_config yoga_pro_7_14iah10_cfg = {
88 .multiplier = 100,
89 .fan_count = 1,
90 .paths = { "\\_SB.PC00.LPCB.EC0.FANS", NULL }
91 };
92
apply_rllag_filter(struct yoga_fan_data * data,int idx,long raw_rpm)93 static void apply_rllag_filter(struct yoga_fan_data *data, int idx, long raw_rpm)
94 {
95 ktime_t now = ktime_get_boottime();
96 s64 dt_ms = ktime_to_ms(ktime_sub(now, data->last_sample[idx]));
97 long delta, step, limit, alpha;
98 s64 temp_num;
99
100 if (raw_rpm < RPM_FLOOR_LIMIT) {
101 data->filtered_val[idx] = 0;
102 data->last_sample[idx] = now;
103 return;
104 }
105
106 if (data->last_sample[idx] == 0 || dt_ms > MAX_SAMPLING) {
107 data->filtered_val[idx] = raw_rpm;
108 data->last_sample[idx] = now;
109 return;
110 }
111
112 if (dt_ms < MIN_SAMPLING)
113 return;
114
115 delta = raw_rpm - data->filtered_val[idx];
116 if (delta == 0) {
117 data->last_sample[idx] = now;
118 return;
119 }
120
121 temp_num = dt_ms << 12;
122 alpha = (long)div64_s64(temp_num, (s64)(TAU_MS + dt_ms));
123 step = (delta * alpha) >> 12;
124
125 if (step == 0 && delta != 0)
126 step = (delta > 0) ? 1 : -1;
127
128 limit = (MAX_SLEW_RPM_S * (long)dt_ms) / 1000;
129 if (limit < 1)
130 limit = 1;
131
132 if (step > limit)
133 step = limit;
134 else if (step < -limit)
135 step = -limit;
136
137 data->filtered_val[idx] += step;
138 data->last_sample[idx] = now;
139 }
140
yoga_fan_read(struct device * dev,enum hwmon_sensor_types type,u32 attr,int channel,long * val)141 static int yoga_fan_read(struct device *dev, enum hwmon_sensor_types type,
142 u32 attr, int channel, long *val)
143 {
144 struct yoga_fan_data *data = dev_get_drvdata(dev);
145 unsigned long long raw_acpi;
146 acpi_status status;
147
148 if (type != hwmon_fan || attr != hwmon_fan_input)
149 return -EOPNOTSUPP;
150
151 status = acpi_evaluate_integer(data->active_handles[channel], NULL, NULL, &raw_acpi);
152 if (ACPI_FAILURE(status))
153 return -EIO;
154
155 apply_rllag_filter(data, channel, (long)raw_acpi * data->multiplier);
156 *val = data->filtered_val[channel];
157
158 return 0;
159 }
160
yoga_fan_is_visible(const void * data,enum hwmon_sensor_types type,u32 attr,int channel)161 static umode_t yoga_fan_is_visible(const void *data, enum hwmon_sensor_types type,
162 u32 attr, int channel)
163 {
164 const struct yoga_fan_data *fan_data = data;
165
166 if (type == hwmon_fan && channel < fan_data->fan_count)
167 return 0444;
168
169 return 0;
170 }
171
172 static const struct hwmon_ops yoga_fan_hwmon_ops = {
173 .is_visible = yoga_fan_is_visible,
174 .read = yoga_fan_read,
175 };
176
177 static const struct hwmon_channel_info *yoga_fan_info[] = {
178 HWMON_CHANNEL_INFO(fan,
179 HWMON_F_INPUT, HWMON_F_INPUT,
180 HWMON_F_INPUT, HWMON_F_INPUT,
181 HWMON_F_INPUT, HWMON_F_INPUT,
182 HWMON_F_INPUT, HWMON_F_INPUT),
183 NULL
184 };
185
186 static const struct hwmon_chip_info yoga_fan_chip_info = {
187 .ops = &yoga_fan_hwmon_ops,
188 .info = yoga_fan_info,
189 };
190
191 static const struct dmi_system_id yogafan_quirks[] = {
192 {
193 .ident = "Lenovo LOQ 15IAX9",
194 .matches = {
195 DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
196 DMI_MATCH(DMI_PRODUCT_FAMILY, "LOQ 15IAX9"),
197 },
198 .driver_data = (void *)&loq_15iax9_8bit_dual_cfg,
199 },
200 {
201 .ident = "Lenovo XiaoXin Pro 13ARE 2020",
202 .matches = {
203 DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
204 DMI_MATCH(DMI_PRODUCT_FAMILY, "XiaoXinPro-13ARE 2020"),
205 },
206 .driver_data = (void *)&xiaoxin_8bit_dual_cfg,
207 },
208 {
209 .ident = "Lenovo IdeaPad 3 15ALC6",
210 .matches = {
211 DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
212 DMI_MATCH(DMI_PRODUCT_FAMILY, "IdeaPad 3 15ALC6"),
213 },
214 .driver_data = (void *)&ideapad_8bit_fan0_cfg,
215 },
216 {
217 .ident = "Lenovo Legion Pro 7 16AFR10H",
218 .matches = {
219 DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
220 DMI_MATCH(DMI_PRODUCT_FAMILY, "Legion Pro 7 16AFR10H"),
221 },
222 .driver_data = (void *)&xiaoxin_8bit_dual_cfg,
223 },
224 {
225 .ident = "Lenovo Yoga Pro 7 14IAH10",
226 .matches = {
227 DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
228 DMI_MATCH(DMI_PRODUCT_FAMILY, "Yoga Pro 7 14IAH10"),
229 },
230 .driver_data = (void *)&yoga_pro_7_14iah10_cfg,
231 },
232 {
233 .ident = "Lenovo Yoga 7 16ARP8",
234 .matches = {
235 DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
236 DMI_MATCH(DMI_PRODUCT_FAMILY, "Yoga 7 16ARP8"),
237 },
238 .driver_data = (void *)&xiaoxin_8bit_dual_cfg,
239 },
240 {
241 .ident = "Lenovo Yoga",
242 .matches = {
243 DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
244 DMI_MATCH(DMI_PRODUCT_FAMILY, "Yoga"),
245 },
246 .driver_data = (void *)&yoga_8bit_fans_cfg,
247 },
248 {
249 .ident = "Lenovo Legion",
250 .matches = {
251 DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
252 DMI_MATCH(DMI_PRODUCT_FAMILY, "Legion"),
253 },
254 .driver_data = (void *)&legion_16bit_dual_cfg,
255 },
256 {
257 .ident = "Lenovo IdeaPad",
258 .matches = {
259 DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
260 DMI_MATCH(DMI_PRODUCT_FAMILY, "IdeaPad"),
261 },
262 .driver_data = (void *)&ideapad_8bit_fan0_cfg,
263 },
264 { }
265 };
266 MODULE_DEVICE_TABLE(dmi, yogafan_quirks);
267
yoga_fan_probe(struct platform_device * pdev)268 static int yoga_fan_probe(struct platform_device *pdev)
269 {
270 const struct dmi_system_id *dmi_id;
271 const struct yogafan_config *cfg;
272 struct yoga_fan_data *data;
273 struct device *hwmon_dev;
274 int i;
275
276 dmi_id = dmi_first_match(yogafan_quirks);
277 if (!dmi_id)
278 return -ENODEV;
279
280 cfg = dmi_id->driver_data;
281 data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
282 if (!data)
283 return -ENOMEM;
284
285 data->multiplier = cfg->multiplier;
286
287 for (i = 0; i < cfg->fan_count; i++) {
288 acpi_status status;
289
290 status = acpi_get_handle(NULL, (char *)cfg->paths[i],
291 &data->active_handles[data->fan_count]);
292 if (ACPI_SUCCESS(status))
293 data->fan_count++;
294 }
295
296 if (data->fan_count == 0)
297 return -ENODEV;
298
299 hwmon_dev = devm_hwmon_device_register_with_info(&pdev->dev, DRVNAME,
300 data, &yoga_fan_chip_info, NULL);
301
302 return PTR_ERR_OR_ZERO(hwmon_dev);
303 }
304
305 static struct platform_driver yoga_fan_driver = {
306 .driver = { .name = DRVNAME },
307 .probe = yoga_fan_probe,
308 };
309
310 static struct platform_device *yoga_fan_device;
311
yoga_fan_init(void)312 static int __init yoga_fan_init(void)
313 {
314 int ret;
315
316 if (!dmi_check_system(yogafan_quirks))
317 return -ENODEV;
318
319 ret = platform_driver_register(&yoga_fan_driver);
320 if (ret)
321 return ret;
322
323 yoga_fan_device = platform_device_register_simple(DRVNAME, -1, NULL, 0);
324 if (IS_ERR(yoga_fan_device)) {
325 platform_driver_unregister(&yoga_fan_driver);
326 return PTR_ERR(yoga_fan_device);
327 }
328 return 0;
329 }
330
yoga_fan_exit(void)331 static void __exit yoga_fan_exit(void)
332 {
333 platform_device_unregister(yoga_fan_device);
334 platform_driver_unregister(&yoga_fan_driver);
335 }
336
337 module_init(yoga_fan_init);
338 module_exit(yoga_fan_exit);
339
340 MODULE_AUTHOR("Sergio Melas <sergiomelas@gmail.com>");
341 MODULE_DESCRIPTION("Lenovo Yoga/Legion Fan Monitor Driver");
342 MODULE_LICENSE("GPL");
343