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