| // SPDX-License-Identifier: GPL-2.0-only | |
| /* | |
| * yoga_fan.c - Lenovo Yoga/Legion Fan Hardware Monitoring Driver | |
| * | |
| * Provides fan speed monitoring for Lenovo Yoga, Legion, and IdeaPad | |
| * laptops by interfacing with the Embedded Controller (EC) via ACPI. | |
| * | |
| * The driver implements a passive discrete-time first-order lag filter | |
| * with slew-rate limiting (RLLag). This addresses low-resolution | |
| * tachometer sampling in the EC by smoothing RPM readings based on | |
| * the time delta (dt) between userspace requests, ensuring physical | |
| * consistency without background task overhead or race conditions. | |
| * The filter implements multirate filtering with autoreset in case | |
| * of large sampling time. | |
| * | |
| * Copyright (C) 2021-2026 Sergio Melas <sergiomelas@gmail.com> | |
| */ | |
| #include <linux/acpi.h> | |
| #include <linux/dmi.h> | |
| #include <linux/err.h> | |
| #include <linux/hwmon.h> | |
| #include <linux/ktime.h> | |
| #include <linux/module.h> | |
| #include <linux/platform_device.h> | |
| #include <linux/slab.h> | |
| #include <linux/math64.h> | |
| /* Driver Configuration Constants */ | |
| #define DRVNAME "yogafan" | |
| #define MAX_FANS 8 | |
| /* Filter Configuration Constants */ | |
| #define TAU_MS 1000 /* Time constant for the first-order lag (ms) */ | |
| #define MAX_SLEW_RPM_S 1500 /* Maximum allowed change in RPM per second */ | |
| #define MAX_SAMPLING 5000 /* Maximum allowed Ts for reset (ms) */ | |
| #define MIN_SAMPLING 100 /* Minimum interval between filter updates (ms) */ | |
| /* RPM Sanitation Constants */ | |
| #define RPM_FLOOR_LIMIT 50 /* Snap filtered value to 0 if raw is 0 */ | |
| struct yogafan_config { | |
| int multiplier; | |
| int fan_count; | |
| const char *paths[2]; | |
| }; | |
| struct yoga_fan_data { | |
| acpi_handle active_handles[MAX_FANS]; | |
| long filtered_val[MAX_FANS]; | |
| ktime_t last_sample[MAX_FANS]; | |
| int multiplier; | |
| int fan_count; | |
| }; | |
| /* Specific configurations mapped via DMI */ | |
| static const struct yogafan_config yoga_8bit_fans_cfg = { | |
| .multiplier = 100, | |
| .fan_count = 1, | |
| .paths = { "\\_SB.PCI0.LPC0.EC0.FANS", NULL } | |
| }; | |
| static const struct yogafan_config ideapad_8bit_fan0_cfg = { | |
| .multiplier = 100, | |
| .fan_count = 1, | |
| .paths = { "\\_SB.PCI0.LPC0.EC0.FAN0", NULL } | |
| }; | |
| static const struct yogafan_config legion_16bit_dual_cfg = { | |
| .multiplier = 1, | |
| .fan_count = 2, | |
| .paths = { "\\_SB.PCI0.LPC0.EC0.FANS", "\\_SB.PCI0.LPC0.EC0.FA2S" } | |
| }; | |
| static const struct yogafan_config loq_15iax9_8bit_dual_cfg = { | |
| .multiplier = 100, | |
| .fan_count = 2, | |
| .paths = { "\\_SB.PC00.LPCB.EC0.FA1S", "\\_SB.PC00.LPCB.EC0.FA2S" } | |
| }; | |
| static const struct yogafan_config xiaoxin_8bit_dual_cfg = { | |
| .multiplier = 100, | |
| .fan_count = 2, | |
| .paths = { "\\_SB.PCI0.LPC0.EC0.FANS", "\\_SB.PCI0.LPC0.EC0.FA2S" } | |
| }; | |
| static const struct yogafan_config yoga_pro_7_14iah10_cfg = { | |
| .multiplier = 100, | |
| .fan_count = 1, | |
| .paths = { "\\_SB.PC00.LPCB.EC0.FANS", NULL } | |
| }; | |
| static void apply_rllag_filter(struct yoga_fan_data *data, int idx, long raw_rpm) | |
| { | |
| ktime_t now = ktime_get_boottime(); | |
| s64 dt_ms = ktime_to_ms(ktime_sub(now, data->last_sample[idx])); | |
| long delta, step, limit, alpha; | |
| s64 temp_num; | |
| if (raw_rpm < RPM_FLOOR_LIMIT) { | |
| data->filtered_val[idx] = 0; | |
| data->last_sample[idx] = now; | |
| return; | |
| } | |
| if (data->last_sample[idx] == 0 || dt_ms > MAX_SAMPLING) { | |
| data->filtered_val[idx] = raw_rpm; | |
| data->last_sample[idx] = now; | |
| return; | |
| } | |
| if (dt_ms < MIN_SAMPLING) | |
| return; | |
| delta = raw_rpm - data->filtered_val[idx]; | |
| if (delta == 0) { | |
| data->last_sample[idx] = now; | |
| return; | |
| } | |
| temp_num = dt_ms << 12; | |
| alpha = (long)div64_s64(temp_num, (s64)(TAU_MS + dt_ms)); | |
| step = (delta * alpha) >> 12; | |
| if (step == 0 && delta != 0) | |
| step = (delta > 0) ? 1 : -1; | |
| limit = (MAX_SLEW_RPM_S * (long)dt_ms) / 1000; | |
| if (limit < 1) | |
| limit = 1; | |
| if (step > limit) | |
| step = limit; | |
| else if (step < -limit) | |
| step = -limit; | |
| data->filtered_val[idx] += step; | |
| data->last_sample[idx] = now; | |
| } | |
| static int yoga_fan_read(struct device *dev, enum hwmon_sensor_types type, | |
| u32 attr, int channel, long *val) | |
| { | |
| struct yoga_fan_data *data = dev_get_drvdata(dev); | |
| unsigned long long raw_acpi; | |
| acpi_status status; | |
| if (type != hwmon_fan || attr != hwmon_fan_input) | |
| return -EOPNOTSUPP; | |
| status = acpi_evaluate_integer(data->active_handles[channel], NULL, NULL, &raw_acpi); | |
| if (ACPI_FAILURE(status)) | |
| return -EIO; | |
| apply_rllag_filter(data, channel, (long)raw_acpi * data->multiplier); | |
| *val = data->filtered_val[channel]; | |
| return 0; | |
| } | |
| static umode_t yoga_fan_is_visible(const void *data, enum hwmon_sensor_types type, | |
| u32 attr, int channel) | |
| { | |
| const struct yoga_fan_data *fan_data = data; | |
| if (type == hwmon_fan && channel < fan_data->fan_count) | |
| return 0444; | |
| return 0; | |
| } | |
| static const struct hwmon_ops yoga_fan_hwmon_ops = { | |
| .is_visible = yoga_fan_is_visible, | |
| .read = yoga_fan_read, | |
| }; | |
| static const struct hwmon_channel_info *yoga_fan_info[] = { | |
| HWMON_CHANNEL_INFO(fan, | |
| HWMON_F_INPUT, HWMON_F_INPUT, | |
| HWMON_F_INPUT, HWMON_F_INPUT, | |
| HWMON_F_INPUT, HWMON_F_INPUT, | |
| HWMON_F_INPUT, HWMON_F_INPUT), | |
| NULL | |
| }; | |
| static const struct hwmon_chip_info yoga_fan_chip_info = { | |
| .ops = &yoga_fan_hwmon_ops, | |
| .info = yoga_fan_info, | |
| }; | |
| static const struct dmi_system_id yogafan_quirks[] = { | |
| { | |
| .ident = "Lenovo LOQ 15IAX9", | |
| .matches = { | |
| DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), | |
| DMI_MATCH(DMI_PRODUCT_FAMILY, "LOQ 15IAX9"), | |
| }, | |
| .driver_data = (void *)&loq_15iax9_8bit_dual_cfg, | |
| }, | |
| { | |
| .ident = "Lenovo XiaoXin Pro 13ARE 2020", | |
| .matches = { | |
| DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), | |
| DMI_MATCH(DMI_PRODUCT_FAMILY, "XiaoXinPro-13ARE 2020"), | |
| }, | |
| .driver_data = (void *)&xiaoxin_8bit_dual_cfg, | |
| }, | |
| { | |
| .ident = "Lenovo IdeaPad 3 15ALC6", | |
| .matches = { | |
| DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), | |
| DMI_MATCH(DMI_PRODUCT_FAMILY, "IdeaPad 3 15ALC6"), | |
| }, | |
| .driver_data = (void *)&ideapad_8bit_fan0_cfg, | |
| }, | |
| { | |
| .ident = "Lenovo Legion Pro 7 16AFR10H", | |
| .matches = { | |
| DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), | |
| DMI_MATCH(DMI_PRODUCT_FAMILY, "Legion Pro 7 16AFR10H"), | |
| }, | |
| .driver_data = (void *)&xiaoxin_8bit_dual_cfg, | |
| }, | |
| { | |
| .ident = "Lenovo Yoga Pro 7 14IAH10", | |
| .matches = { | |
| DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), | |
| DMI_MATCH(DMI_PRODUCT_FAMILY, "Yoga Pro 7 14IAH10"), | |
| }, | |
| .driver_data = (void *)&yoga_pro_7_14iah10_cfg, | |
| }, | |
| { | |
| .ident = "Lenovo Yoga 7 16ARP8", | |
| .matches = { | |
| DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), | |
| DMI_MATCH(DMI_PRODUCT_FAMILY, "Yoga 7 16ARP8"), | |
| }, | |
| .driver_data = (void *)&xiaoxin_8bit_dual_cfg, | |
| }, | |
| { | |
| .ident = "Lenovo Yoga", | |
| .matches = { | |
| DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), | |
| DMI_MATCH(DMI_PRODUCT_FAMILY, "Yoga"), | |
| }, | |
| .driver_data = (void *)&yoga_8bit_fans_cfg, | |
| }, | |
| { | |
| .ident = "Lenovo Legion", | |
| .matches = { | |
| DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), | |
| DMI_MATCH(DMI_PRODUCT_FAMILY, "Legion"), | |
| }, | |
| .driver_data = (void *)&legion_16bit_dual_cfg, | |
| }, | |
| { | |
| .ident = "Lenovo IdeaPad", | |
| .matches = { | |
| DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"), | |
| DMI_MATCH(DMI_PRODUCT_FAMILY, "IdeaPad"), | |
| }, | |
| .driver_data = (void *)&ideapad_8bit_fan0_cfg, | |
| }, | |
| { } | |
| }; | |
| MODULE_DEVICE_TABLE(dmi, yogafan_quirks); | |
| static int yoga_fan_probe(struct platform_device *pdev) | |
| { | |
| const struct dmi_system_id *dmi_id; | |
| const struct yogafan_config *cfg; | |
| struct yoga_fan_data *data; | |
| struct device *hwmon_dev; | |
| int i; | |
| dmi_id = dmi_first_match(yogafan_quirks); | |
| if (!dmi_id) | |
| return -ENODEV; | |
| cfg = dmi_id->driver_data; | |
| data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); | |
| if (!data) | |
| return -ENOMEM; | |
| data->multiplier = cfg->multiplier; | |
| for (i = 0; i < cfg->fan_count; i++) { | |
| acpi_status status; | |
| status = acpi_get_handle(NULL, (char *)cfg->paths[i], | |
| &data->active_handles[data->fan_count]); | |
| if (ACPI_SUCCESS(status)) | |
| data->fan_count++; | |
| } | |
| if (data->fan_count == 0) | |
| return -ENODEV; | |
| hwmon_dev = devm_hwmon_device_register_with_info(&pdev->dev, DRVNAME, | |
| data, &yoga_fan_chip_info, NULL); | |
| return PTR_ERR_OR_ZERO(hwmon_dev); | |
| } | |
| static struct platform_driver yoga_fan_driver = { | |
| .driver = { .name = DRVNAME }, | |
| .probe = yoga_fan_probe, | |
| }; | |
| static struct platform_device *yoga_fan_device; | |
| static int __init yoga_fan_init(void) | |
| { | |
| int ret; | |
| if (!dmi_check_system(yogafan_quirks)) | |
| return -ENODEV; | |
| ret = platform_driver_register(&yoga_fan_driver); | |
| if (ret) | |
| return ret; | |
| yoga_fan_device = platform_device_register_simple(DRVNAME, -1, NULL, 0); | |
| if (IS_ERR(yoga_fan_device)) { | |
| platform_driver_unregister(&yoga_fan_driver); | |
| return PTR_ERR(yoga_fan_device); | |
| } | |
| return 0; | |
| } | |
| static void __exit yoga_fan_exit(void) | |
| { | |
| platform_device_unregister(yoga_fan_device); | |
| platform_driver_unregister(&yoga_fan_driver); | |
| } | |
| module_init(yoga_fan_init); | |
| module_exit(yoga_fan_exit); | |
| MODULE_AUTHOR("Sergio Melas <sergiomelas@gmail.com>"); | |
| MODULE_DESCRIPTION("Lenovo Yoga/Legion Fan Monitor Driver"); | |
| MODULE_LICENSE("GPL"); |