From: Sergio Melas <sergiomelas@gmail.com>
To: Guenter Roeck <linux@roeck-us.net>
Cc: linux-hwmon@vger.kernel.org, Sergio Melas <sergiomelas@gmail.com>
Subject: [PATCH] hwmon: (yogafan) Add support for Lenovo Yoga/Legion fan monitoring
Date: Sun, 22 Mar 2026 21:38:09 +0100 [thread overview]
Message-ID: <20260322203809.44048-1-sergiomelas@gmail.com> (raw)
This driver provides fan speed monitoring for modern Lenovo Yoga,
Legion, and IdeaPad laptops. It interfaces with the Embedded
Controller (EC) via ACPI to retrieve tachometer data.
To address low-resolution sampling in the Lenovo EC firmware, the
driver implements a Rate-Limited Lag (RLLag) filter using a passive
discrete-time first-order model. This ensures physical consistency
of the RPM signal regardless of userspace polling rates.
Signed-off-by: Sergio Melas <sergiomelas@gmail.com>
---
v4:
- Rebased on groeck/hwmon-next branch for clean application.
- Removed unnecessary blank lines and cleaned code formatting.
- Corrected alphabetical sorting in Kconfig and Makefile.
- Technical Validation & FOPTD Verification:
- Implemented FOPTD (First Order Plus Time Delay) modeling.
- Used 10-bit fixed-point math for alpha calculation to avoid
floating point overhead in the kernel.
- Added 5000ms filter reset for resume/long-polling sanitation.
- Hardware Discovery:
- Confirmed support for paths: FANS, FA2S, FAN0.
- Restricted to LENOVO hardware via DMI matching.
---
MAINTAINERS | 6 +
drivers/hwmon/Kconfig | 11 ++
drivers/hwmon/Makefile | 1 +
drivers/hwmon/yogafan.c | 247 ++++++++++++++++++++++++++++++++++++++++
4 files changed, 265 insertions(+)
create mode 100644 drivers/hwmon/yogafan.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 830c6f076b00..9167f3d4f243 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14873,6 +14873,12 @@ W: https://linuxtv.org
Q: http://patchwork.linuxtv.org/project/linux-media/list/
F: drivers/media/usb/dvb-usb-v2/lmedm04*
+LNVYOGAFAN HARDWARE MONITORING DRIVER
+M: Sergio Melas <sergiomelas@gmail.com>
+L: linux-hwmon@vger.kernel.org
+S: Maintained
+F: drivers/hwmon/yogafan.c
+
LOADPIN SECURITY MODULE
M: Kees Cook <kees@kernel.org>
S: Supported
diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index fb77baeeba27..3bb91623b157 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -2653,6 +2653,17 @@ config SENSORS_XGENE
If you say yes here you get support for the temperature
and power sensors for APM X-Gene SoC.
+config SENSORS_YOGAFAN
+ tristate "Lenovo Yoga/Legion Fan Hardware Monitoring"
+ depends on ACPI && HWMON
+ help
+ If you say yes here you get support for fan speed monitoring
+ on modern Lenovo Yoga and Legion laptops.
+
+ This driver can also be built as a module. If so, the module
+ will be called yogafan.
+
+
config SENSORS_INTEL_M10_BMC_HWMON
tristate "Intel MAX10 BMC Hardware Monitoring"
depends on MFD_INTEL_M10_BMC_CORE
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index 556e86d277b1..0fce31b43eb1 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -245,6 +245,7 @@ obj-$(CONFIG_SENSORS_W83L786NG) += w83l786ng.o
obj-$(CONFIG_SENSORS_WM831X) += wm831x-hwmon.o
obj-$(CONFIG_SENSORS_WM8350) += wm8350-hwmon.o
obj-$(CONFIG_SENSORS_XGENE) += xgene-hwmon.o
+obj-$(CONFIG_SENSORS_YOGAFAN) += yogafan.o
obj-$(CONFIG_SENSORS_OCC) += occ/
obj-$(CONFIG_SENSORS_PECI) += peci/
diff --git a/drivers/hwmon/yogafan.c b/drivers/hwmon/yogafan.c
new file mode 100644
index 000000000000..10c48fca8387
--- /dev/null
+++ b/drivers/hwmon/yogafan.c
@@ -0,0 +1,247 @@
+// 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/init.h>
+#include <linux/ktime.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/slab.h>
+#define DRVNAME "yogafan"
+#define MAX_FANS 8
+/* Filter Configuration Constants */
+#define TAU_MS 3000 /* Time constant for the first-order lag (ms) */
+#define MAX_SLEW_RPM_S 100 /* Maximum allowed change in RPM per second */
+#define MAX_SAMPLING 5000 /* Maximum allowed Ts for reset */
+
+struct yoga_fan_data {
+ const char *active_paths[MAX_FANS];
+ long filtered_val[MAX_FANS];
+ ktime_t last_update[MAX_FANS];
+ int fan_count;
+};
+/**
+ * apply_rllag_filter - Discrete-time filter update (Passive Multirate)
+ * @data: pointer to driver data
+ * @idx: fan index
+ * @raw_rpm: new raw value from ACPI
+ *
+ * Implements a Rate-Limited Lag (RLLag) filter using a multirate approach.
+ * Instead of a fixed-interval heartbeat, the sampling time (Ts) is calculated
+ * dynamically as the ktime delta between userspace read requests.
+ *
+ * This mimics a continuous-time First Order Plus Time Delay (FOPTD) model:
+ * rpm_k+1 = rpm_k + clamp(step, -limit, limit)
+ * where:
+ * step = (alpha * (raw_rpm - rpm_k))
+ * alpha = 1-exp(-Ts/Tau)
+ * Applying first order taylor approximation we get:
+ * alpha = Ts / (Tau + Ts)
+ * limit = MaxSlew * Ts
+ *
+ * This ensures physical consistency of the signal regardless of the
+ * userspace polling rate.
+ */
+
+static void apply_rllag_filter(struct yoga_fan_data *data, int idx, long raw_rpm)
+{
+ ktime_t now = ktime_get();
+ s64 dt_ms;
+ long delta, step, limit, alpha;
+ /* Initialize on first read to avoid starting from zero */
+ if (data->last_update[idx] == 0) {
+ data->filtered_val[idx] = raw_rpm;
+ data->last_update[idx] = now;
+ return;
+ }
+ dt_ms = ktime_to_ms(ktime_sub(now, data->last_update[idx]));
+ /* SANITATION: Reset filter if no reads occurred for MAX_SAMPLING
+ * milliseconds. This prevents massive 'lag_steps' if userspace polling resumes
+ * after a long pause or system suspend.
+ */
+ if (dt_ms > MAX_SAMPLING) {
+ data->filtered_val[idx] = raw_rpm;
+ data->last_update[idx] = now;
+ return;
+ }
+ /* SANITATION: Avoid division by zero or jitter from sub-millisecond reads */
+ if (dt_ms < 1)
+ return;
+ delta = raw_rpm - data->filtered_val[idx];
+ /* Alpha = dt / (Tau + dt) using 10-bit fixed point math.
+ * This mimics the physical inertia (FOPTD) of the fan blades.
+ */
+ alpha = (dt_ms << 10) / (TAU_MS + dt_ms);
+ step = (delta * alpha) >> 10;
+ /* Slew Limit = (MaxSlew * dt) / 1000 - Bound the rate of change */
+ limit = (MAX_SLEW_RPM_S * (long)dt_ms) / 1000;
+ if (step > limit)
+ step = limit;
+ else if (step < -limit)
+ step = -limit;
+ data->filtered_val[idx] += step;
+
+ /* SANITATION: Floor the value to zero if RPM is negligible */
+ if (data->filtered_val[idx] < 50)
+ data->filtered_val[idx] = 0;
+ data->last_update[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;
+ long rpm;
+
+ if (type != hwmon_fan || attr != hwmon_fan_input)
+ return -EOPNOTSUPP;
+
+ /* Implement better casting of status using ACPI typedef */
+ status = acpi_evaluate_integer(NULL, (acpi_string)data->active_paths[channel],
+ NULL, &raw_acpi);
+
+ if (ACPI_FAILURE(status))
+ return -EIO;
+
+ /* SANITATION: Lenovo EC typically reports RPM in hundreds for values <= 255.
+ * Values > 255 are treated as raw RPM. This handles different EC firmware styles.
+ */
+ rpm = (raw_acpi > 0 && raw_acpi <= 255) ? ((long)raw_acpi * 100) : (long)raw_acpi;
+ apply_rllag_filter(data, channel, rpm);
+ *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 int yoga_fan_probe(struct platform_device *pdev)
+{
+ struct yoga_fan_data *data;
+ struct device *hwmon_dev;
+ acpi_handle handle;
+ int i;
+ static const char * const fan_paths[] = {
+ "\\_SB.PCI0.LPC0.EC0.FANS", /* Primary Fan (Yoga 14c) */
+ "\\_SB.PCI0.LPC0.EC0.FA2S", /* Secondary Fan (Legion) */
+ "\\_SB.PCI0.LPC0.EC0.FAN0", /* IdeaPad / Slim */
+ "\\_SB.PCI0.LPC.EC.FAN0", /* Legacy */
+ "\\_SB.PCI0.LPC0.EC.FAN0", /* Alternate */
+ };
+ data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
+ if (!data)
+ return -ENOMEM;
+ data->fan_count = 0;
+
+ /* SANITATION: Verify ACPI path existence before indexing */
+ for (i = 0; i < ARRAY_SIZE(fan_paths); i++) {
+ if (ACPI_SUCCESS(acpi_get_handle(NULL, (char *)fan_paths[i], &handle))) {
+ data->active_paths[data->fan_count] = fan_paths[i];
+ data->fan_count++;
+
+ if (data->fan_count >= MAX_FANS)
+ break;
+ }
+ }
+
+ if (data->fan_count == 0)
+ return -ENODEV;
+ /* SANITATION: Anchoring drvdata to avoid NULL returns during unload */
+ platform_set_drvdata(pdev, data);
+ 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 const struct dmi_system_id yoga_dmi_table[] __initconst = {
+ {
+ .ident = "Lenovo",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
+ },
+ },
+ { }
+};
+MODULE_DEVICE_TABLE(dmi, yoga_dmi_table);
+
+static int __init yoga_fan_init(void)
+{
+ int ret;
+
+ if (!dmi_check_system(yoga_dmi_table))
+ return -ENODEV;
+ ret = platform_driver_register(&yoga_fan_driver);
+ if (ret)
+ return ret;
+ yoga_fan_device = platform_device_register_simple(DRVNAME, 0, 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");
base-commit: be8aad7a8a14151fd471aadf368e1582f91a7817
--
2.53.0
next reply other threads:[~2026-03-22 20:38 UTC|newest]
Thread overview: 16+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-22 20:38 Sergio Melas [this message]
2026-03-23 1:37 ` [PATCH] hwmon: (yogafan) Add support for Lenovo Yoga/Legion fan monitoring Guenter Roeck
2026-03-23 10:56 ` [PATCH v5] " Sergio Melas
2026-03-24 12:17 ` Guenter Roeck
2026-03-24 22:10 ` [PATCH v6] " Sergio Melas
2026-03-25 0:13 ` Guenter Roeck
2026-03-25 6:43 ` [PATCH v7] " Sergio Melas
2026-03-25 14:03 ` Guenter Roeck
2026-03-25 22:06 ` [PATCH v8] " Sergio Melas
2026-03-26 0:21 ` Guenter Roeck
2026-03-27 1:00 ` [PATCH v9] " Sergio Melas
2026-03-28 4:04 ` kernel test robot
2026-03-27 1:29 ` [PATCH v10] " Sergio Melas
2026-03-27 16:15 ` Guenter Roeck
2026-03-27 17:45 ` kernel test robot
2026-03-27 18:01 ` Guenter Roeck
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260322203809.44048-1-sergiomelas@gmail.com \
--to=sergiomelas@gmail.com \
--cc=linux-hwmon@vger.kernel.org \
--cc=linux@roeck-us.net \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox