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