summaryrefslogtreecommitdiffstats
path: root/drivers/hwmon/yogafan.c
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/hwmon/yogafan.c')
-rw-r--r--drivers/hwmon/yogafan.c342
1 files changed, 342 insertions, 0 deletions
diff --git a/drivers/hwmon/yogafan.c b/drivers/hwmon/yogafan.c
new file mode 100644
index 000000000..48fa5148d
--- /dev/null
+++ b/drivers/hwmon/yogafan.c
@@ -0,0 +1,342 @@
+// 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");