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 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 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 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 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 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 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