[PATCH v7] hwmon: (yogafan) Add support for Lenovo Yoga/Legion fan monitoring

Sergio Melas posted 1 patch 1 week, 1 day ago
There is a newer version of this series
Documentation/hwmon/index.rst   |   1 +
Documentation/hwmon/yogafan.rst |  48 +++++++
MAINTAINERS                     |   8 ++
drivers/hwmon/Kconfig           |   8 ++
drivers/hwmon/Makefile          |   1 +
drivers/hwmon/yogafan.c         | 230 ++++++++++++++++++++++++++++++++
6 files changed, 296 insertions(+)
create mode 100644 Documentation/hwmon/yogafan.rst
create mode 100644 drivers/hwmon/yogafan.c
[PATCH v7] hwmon: (yogafan) Add support for Lenovo Yoga/Legion fan monitoring
Posted by Sergio Melas 1 week, 1 day ago
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>

---
v7:
  - Fixed Kconfig: Removed non-existent 'select MATH64'.
  - Fixed unused macro: Utilized RPM_FLOOR_LIMIT to implement an 
    immediate 0-RPM bypass in the filter.
  - Clarification: Previous "unified structure" comment meant that all 
    6 files (driver, docs, metadata) are now in this single atomic patch.
v6:
  - Unified patch structure (6 files changed).
  - Verified FOPTD (First-Order Plus Time Delay) model against hardware 
     traces (Yoga 14c) to ensure physical accuracy of the 1000ms time constant.
  - Fixed a rounding stall: added a +/- 1 RPM floor to the step calculation 
    to ensure convergence even at high polling frequencies.
  - Set MAX_SLEW_RPM_S to 1500 to match physical motor inertia.
  - Documentation: Updated to clarify 100-RPM hardware step resolution.
  - 32-bit safety: Implemented div64_s64 for coefficient precision.
v5:
  - Fixed 32-bit build failures by using div64_s64 for 64-bit division.
  - Extracted magic numbers into constants (RPM_UNIT_THRESHOLD, etc.).
  - Fixed filter stall by ensuring a minimum slew limit (limit = 1).
  - Refined RPM floor logic to trigger only when hardware reports 0 RPM.
  - Resolved 255/256 unit-jump bug by adjusting heuristic thresholds.
v4:
  - Rebased on groeck/hwmon-next branch for clean application.
  - Corrected alphabetical sorting in Kconfig and Makefile.
  - Technical Validation & FOPTD Verification:
    - Implemented RLLag (Rate-Limited Lag) first-order 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.
v3:
  - Added MAINTAINERS entry and full Documentation/hwmon/yogafan.rst.
  - Fixed integer overflow in filter math.
  - Added support for secondary fan paths (FA2S) for Legion laptops.
v2:
  - Migrated from background worker to passive multirate filtering.
  - Implemented dt-based scaling to maximize CPU sleep states.
  - Restricted driver to Lenovo hardware via DMI matching.
v1:
  - Initial submission with basic ACPI fan path support.
---
---
 Documentation/hwmon/index.rst   |   1 +
 Documentation/hwmon/yogafan.rst |  48 +++++++
 MAINTAINERS                     |   8 ++
 drivers/hwmon/Kconfig           |   8 ++
 drivers/hwmon/Makefile          |   1 +
 drivers/hwmon/yogafan.c         | 230 ++++++++++++++++++++++++++++++++
 6 files changed, 296 insertions(+)
 create mode 100644 Documentation/hwmon/yogafan.rst
 create mode 100644 drivers/hwmon/yogafan.c

diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst
index 559c32344cd3..199f35a75282 100644
--- a/Documentation/hwmon/index.rst
+++ b/Documentation/hwmon/index.rst
@@ -282,4 +282,5 @@ Hardware Monitoring Kernel Drivers
    xdp710
    xdpe12284
    xdpe152c4
+   yogafan
    zl6100
diff --git a/Documentation/hwmon/yogafan.rst b/Documentation/hwmon/yogafan.rst
new file mode 100644
index 000000000000..e0f2b060aabc
--- /dev/null
+++ b/Documentation/hwmon/yogafan.rst
@@ -0,0 +1,48 @@
+.. SPDX-License-Identifier: GPL-2.0-only
+
+Kernel driver yogafan
+=====================
+
+Supported chips:
+
+  * Lenovo Yoga, Legion, and IdeaPad Embedded Controllers
+    Prefix: 'yogafan'
+    Addresses: ACPI handle (see probe list in driver)
+
+Author: Sergio Melas <sergiomelas@gmail.com>
+
+Description
+-----------
+
+This driver provides fan speed monitoring for modern Lenovo laptops.
+Most Lenovo laptops do not provide fan tachometer data through standard
+ISA/LPC hardware monitoring chips. Instead, the data is stored in the
+Embedded Controller (EC) and exposed via ACPI.
+
+The driver implements a **Rate-Limited Lag (RLLag)** filter to handle
+the low-resolution and jittery sampling found in Lenovo EC firmware.
+
+Filter Details:
+---------------
+
+The RLLag filter is a discrete-time first-order lag model that ensures:
+  - **Smoothing:** Jittery 1000-RPM step increments are smoothed into 1-RPM increments.
+  - **Slew-Rate Limiting:** Prevents "teleporting" readings by capping the change
+    to 1500 RPM/s, matching physical fan inertia.
+  - **Polling Independence:** The filter math scales based on the time delta
+    between userspace reads, ensuring the same physical curve regardless
+    of whether you poll at 1Hz or 1000Hz.
+
+Usage
+-----
+
+The driver exposes standard hwmon sysfs attributes:
+
+================  =============================================================
+Attribute         Description
+================  =============================================================
+fanX_input        Filtered fan speed in RPM.
+================  =============================================================
+
+Note: If the hardware reports 0 RPM, the filter is bypassed and 0 is reported
+immediately to ensure the user knows the fan has stopped.
diff --git a/MAINTAINERS b/MAINTAINERS
index 830c6f076b00..94416af57b28 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14873,6 +14873,14 @@ W:	https://linuxtv.org
 Q:	http://patchwork.linuxtv.org/project/linux-media/list/
 F:	drivers/media/usb/dvb-usb-v2/lmedm04*
 
+LENOVO YOGA FAN DRIVER
+M:	Sergio Melas <sergiomelas@gmail.com>
+L:	linux-hwmon@vger.kernel.org
+W:	https://github.com/sergiomelas
+S:	Maintained
+F:	Documentation/hwmon/yogafan.rst
+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 7dd8381ba0d0..644b52c6ba66 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -2653,6 +2653,14 @@ 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 Fan Hardware Monitoring"
+	depends on ACPI && HWMON
+	help
+	  Say Y here if you want to monitor fan speeds on Lenovo Yoga
+	  and Legion laptops.
+
+
 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..4c27f6884b4a
--- /dev/null
+++ b/drivers/hwmon/yogafan.c
@@ -0,0 +1,230 @@
+// 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>
+#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) */
+/* RPM Heuristic and Sanitation Constants */
+#define RPM_UNIT_THRESHOLD 500  /* Values below this treated as units of 100 */
+#define RPM_UNIT_MULT      100  /* Multiplier for low-res EC readings */
+#define RPM_FLOOR_LIMIT    50   /* Snap filtered value to 0 if raw is 0 */
+
+struct yoga_fan_data {
+	const char *active_paths[MAX_FANS];
+	long filtered_val[MAX_FANS];
+	ktime_t last_sample[MAX_FANS]; /* Renamed from last_update for consistency */
+	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
+ */
+static void apply_rllag_filter(struct yoga_fan_data *data, int idx, long raw_rpm)
+{
+	ktime_t now = ktime_get();
+	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;
+	}
+	/* Initialize on first run or after long sleep/stall */
+	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 <= 0) return;
+
+	delta = raw_rpm - data->filtered_val[idx];
+	if (delta == 0) {
+		data->last_sample[idx] = now;
+		return;
+	}
+	/* Alpha with 12-bit precision to prevent alpha=0 on fast polls */
+	temp_num = dt_ms << 12;
+	alpha = (long)div64_s64(temp_num, (s64)(TAU_MS + dt_ms));
+	step = (delta * alpha) >> 12;
+	/* FIX THE STALL: Force a move of 1 RPM if alpha*delta rounds to zero */
+	if (step == 0 && delta != 0)
+		step = (delta > 0) ? 1 : -1;
+	/* SLEW RATE LIMITING: Scaled by time delta */
+	limit = (MAX_SLEW_RPM_S * (long)dt_ms) / 1000;
+	if (limit < 1) limit = 1;
+	/* Clamp step to physical slew rate */
+	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;
+	long rpm;
+
+	if (type != hwmon_fan || attr != hwmon_fan_input)
+		return -EOPNOTSUPP;
+	status = acpi_evaluate_integer(NULL, (acpi_string)data->active_paths[channel],
+					NULL, &raw_acpi);
+	if (ACPI_FAILURE(status))
+		return -EIO;
+	/* * Heuristic: Convert units-of-100 to raw RPM.
+	 * Most Yoga/Legion ECs return a single byte (0-255).
+	 * We use 500 as a safety threshold to distinguish from raw 16-bit RPM.
+	 */
+	rpm = (long)raw_acpi;
+	if (rpm > 0 && rpm < RPM_UNIT_THRESHOLD)
+		rpm *= RPM_UNIT_MULT;
+	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;
+	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;
+	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");
-- 
2.53.0
Re: [PATCH v7] hwmon: (yogafan) Add support for Lenovo Yoga/Legion fan monitoring
Posted by Guenter Roeck 1 week, 1 day ago
Hi Sergio,

On 3/24/26 23:43, Sergio Melas wrote:
> 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>

The AI still has valid concerns.

https://sashiko.dev/#/patchset/20260325064358.533849-1-sergiomelas%40gmail.com

Specifically:

- The heuristic used to determine slow/fast speeds is really fragile.
   This refers both to using 500 RPM as cut-over point and to re-evaluating
   it whenever reading the fan speed. At the very least, after it has been
   determined that the RPM was once higher than the threshold, it seems
   unnecessary to ever look at it again and re-evaluate.
   It would be better if that could be made deterministic, for example by
   listing specific system models using one or the other, or even better
   finding some ACPI handle which contains information about the multiplication
   factor. Windows must handle this somehow, and I don't think they use
   the heuristics.
- The concern about behavior after suspend/resume is also valid.
   The best would be to implement suspend/resume support, but the suggested
   alternative might also help.
- I agree with the sentiment about the DMI match. Can the DMI match somehow
   be restricted further to a subset of Lenovo devices ?

Thanks,
Guenter

> 
> ---
> v7:
>    - Fixed Kconfig: Removed non-existent 'select MATH64'.
>    - Fixed unused macro: Utilized RPM_FLOOR_LIMIT to implement an
>      immediate 0-RPM bypass in the filter.
>    - Clarification: Previous "unified structure" comment meant that all
>      6 files (driver, docs, metadata) are now in this single atomic patch.
> v6:
>    - Unified patch structure (6 files changed).
>    - Verified FOPTD (First-Order Plus Time Delay) model against hardware
>       traces (Yoga 14c) to ensure physical accuracy of the 1000ms time constant.
>    - Fixed a rounding stall: added a +/- 1 RPM floor to the step calculation
>      to ensure convergence even at high polling frequencies.
>    - Set MAX_SLEW_RPM_S to 1500 to match physical motor inertia.
>    - Documentation: Updated to clarify 100-RPM hardware step resolution.
>    - 32-bit safety: Implemented div64_s64 for coefficient precision.
> v5:
>    - Fixed 32-bit build failures by using div64_s64 for 64-bit division.
>    - Extracted magic numbers into constants (RPM_UNIT_THRESHOLD, etc.).
>    - Fixed filter stall by ensuring a minimum slew limit (limit = 1).
>    - Refined RPM floor logic to trigger only when hardware reports 0 RPM.
>    - Resolved 255/256 unit-jump bug by adjusting heuristic thresholds.
> v4:
>    - Rebased on groeck/hwmon-next branch for clean application.
>    - Corrected alphabetical sorting in Kconfig and Makefile.
>    - Technical Validation & FOPTD Verification:
>      - Implemented RLLag (Rate-Limited Lag) first-order 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.
> v3:
>    - Added MAINTAINERS entry and full Documentation/hwmon/yogafan.rst.
>    - Fixed integer overflow in filter math.
>    - Added support for secondary fan paths (FA2S) for Legion laptops.
> v2:
>    - Migrated from background worker to passive multirate filtering.
>    - Implemented dt-based scaling to maximize CPU sleep states.
>    - Restricted driver to Lenovo hardware via DMI matching.
> v1:
>    - Initial submission with basic ACPI fan path support.
> ---
> ---
>   Documentation/hwmon/index.rst   |   1 +
>   Documentation/hwmon/yogafan.rst |  48 +++++++
>   MAINTAINERS                     |   8 ++
>   drivers/hwmon/Kconfig           |   8 ++
>   drivers/hwmon/Makefile          |   1 +
>   drivers/hwmon/yogafan.c         | 230 ++++++++++++++++++++++++++++++++
>   6 files changed, 296 insertions(+)
>   create mode 100644 Documentation/hwmon/yogafan.rst
>   create mode 100644 drivers/hwmon/yogafan.c
> 
> diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst
> index 559c32344cd3..199f35a75282 100644
> --- a/Documentation/hwmon/index.rst
> +++ b/Documentation/hwmon/index.rst
> @@ -282,4 +282,5 @@ Hardware Monitoring Kernel Drivers
>      xdp710
>      xdpe12284
>      xdpe152c4
> +   yogafan
>      zl6100
> diff --git a/Documentation/hwmon/yogafan.rst b/Documentation/hwmon/yogafan.rst
> new file mode 100644
> index 000000000000..e0f2b060aabc
> --- /dev/null
> +++ b/Documentation/hwmon/yogafan.rst
> @@ -0,0 +1,48 @@
> +.. SPDX-License-Identifier: GPL-2.0-only
> +
> +Kernel driver yogafan
> +=====================
> +
> +Supported chips:
> +
> +  * Lenovo Yoga, Legion, and IdeaPad Embedded Controllers
> +    Prefix: 'yogafan'
> +    Addresses: ACPI handle (see probe list in driver)
> +
> +Author: Sergio Melas <sergiomelas@gmail.com>
> +
> +Description
> +-----------
> +
> +This driver provides fan speed monitoring for modern Lenovo laptops.
> +Most Lenovo laptops do not provide fan tachometer data through standard
> +ISA/LPC hardware monitoring chips. Instead, the data is stored in the
> +Embedded Controller (EC) and exposed via ACPI.
> +
> +The driver implements a **Rate-Limited Lag (RLLag)** filter to handle
> +the low-resolution and jittery sampling found in Lenovo EC firmware.
> +
> +Filter Details:
> +---------------
> +
> +The RLLag filter is a discrete-time first-order lag model that ensures:
> +  - **Smoothing:** Jittery 1000-RPM step increments are smoothed into 1-RPM increments.
> +  - **Slew-Rate Limiting:** Prevents "teleporting" readings by capping the change
> +    to 1500 RPM/s, matching physical fan inertia.
> +  - **Polling Independence:** The filter math scales based on the time delta
> +    between userspace reads, ensuring the same physical curve regardless
> +    of whether you poll at 1Hz or 1000Hz.
> +
> +Usage
> +-----
> +
> +The driver exposes standard hwmon sysfs attributes:
> +
> +================  =============================================================
> +Attribute         Description
> +================  =============================================================
> +fanX_input        Filtered fan speed in RPM.
> +================  =============================================================
> +
> +Note: If the hardware reports 0 RPM, the filter is bypassed and 0 is reported
> +immediately to ensure the user knows the fan has stopped.
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 830c6f076b00..94416af57b28 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -14873,6 +14873,14 @@ W:	https://linuxtv.org
>   Q:	http://patchwork.linuxtv.org/project/linux-media/list/
>   F:	drivers/media/usb/dvb-usb-v2/lmedm04*
>   
> +LENOVO YOGA FAN DRIVER
> +M:	Sergio Melas <sergiomelas@gmail.com>
> +L:	linux-hwmon@vger.kernel.org
> +W:	https://github.com/sergiomelas
> +S:	Maintained
> +F:	Documentation/hwmon/yogafan.rst
> +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 7dd8381ba0d0..644b52c6ba66 100644
> --- a/drivers/hwmon/Kconfig
> +++ b/drivers/hwmon/Kconfig
> @@ -2653,6 +2653,14 @@ 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 Fan Hardware Monitoring"
> +	depends on ACPI && HWMON
> +	help
> +	  Say Y here if you want to monitor fan speeds on Lenovo Yoga
> +	  and Legion laptops.
> +
> +
>   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..4c27f6884b4a
> --- /dev/null
> +++ b/drivers/hwmon/yogafan.c
> @@ -0,0 +1,230 @@
> +// 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>
> +#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) */
> +/* RPM Heuristic and Sanitation Constants */
> +#define RPM_UNIT_THRESHOLD 500  /* Values below this treated as units of 100 */
> +#define RPM_UNIT_MULT      100  /* Multiplier for low-res EC readings */
> +#define RPM_FLOOR_LIMIT    50   /* Snap filtered value to 0 if raw is 0 */
> +
> +struct yoga_fan_data {
> +	const char *active_paths[MAX_FANS];
> +	long filtered_val[MAX_FANS];
> +	ktime_t last_sample[MAX_FANS]; /* Renamed from last_update for consistency */
> +	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
> + */
> +static void apply_rllag_filter(struct yoga_fan_data *data, int idx, long raw_rpm)
> +{
> +	ktime_t now = ktime_get();
> +	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;
> +	}
> +	/* Initialize on first run or after long sleep/stall */
> +	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 <= 0) return;
> +
> +	delta = raw_rpm - data->filtered_val[idx];
> +	if (delta == 0) {
> +		data->last_sample[idx] = now;
> +		return;
> +	}
> +	/* Alpha with 12-bit precision to prevent alpha=0 on fast polls */
> +	temp_num = dt_ms << 12;
> +	alpha = (long)div64_s64(temp_num, (s64)(TAU_MS + dt_ms));
> +	step = (delta * alpha) >> 12;
> +	/* FIX THE STALL: Force a move of 1 RPM if alpha*delta rounds to zero */
> +	if (step == 0 && delta != 0)
> +		step = (delta > 0) ? 1 : -1;
> +	/* SLEW RATE LIMITING: Scaled by time delta */
> +	limit = (MAX_SLEW_RPM_S * (long)dt_ms) / 1000;
> +	if (limit < 1) limit = 1;
> +	/* Clamp step to physical slew rate */
> +	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;
> +	long rpm;
> +
> +	if (type != hwmon_fan || attr != hwmon_fan_input)
> +		return -EOPNOTSUPP;
> +	status = acpi_evaluate_integer(NULL, (acpi_string)data->active_paths[channel],
> +					NULL, &raw_acpi);
> +	if (ACPI_FAILURE(status))
> +		return -EIO;
> +	/* * Heuristic: Convert units-of-100 to raw RPM.
> +	 * Most Yoga/Legion ECs return a single byte (0-255).
> +	 * We use 500 as a safety threshold to distinguish from raw 16-bit RPM.
> +	 */
> +	rpm = (long)raw_acpi;
> +	if (rpm > 0 && rpm < RPM_UNIT_THRESHOLD)
> +		rpm *= RPM_UNIT_MULT;
> +	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;
> +	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;
> +	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");