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