]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
hwmon: (yogafan) Add support for Lenovo Yoga/Legion fan monitoring
authorSergio Melas <sergiomelas@gmail.com>
Fri, 27 Mar 2026 22:16:02 +0000 (23:16 +0100)
committerGuenter Roeck <linux@roeck-us.net>
Fri, 3 Apr 2026 18:09:09 +0000 (11:09 -0700)
This driver provides fan speed monitoring for Lenovo Yoga, Legion, and
IdeaPad laptops by interfacing with the Embedded Controller (EC) via ACPI.

To address low-resolution sampling in Lenovo EC firmware, a Rate-Limited
Lag (RLLag) filter is implemented. The filter ensures a consistent physical
curve regardless of userspace polling frequency.

Hardware identification is performed via DMI-based quirk tables, which
map specific ACPI object paths and register widths (8-bit vs 16-bit)
deterministically.

Signed-off-by: Sergio Melas <sergiomelas@gmail.com>
Link: https://lore.kernel.org/r/20260327221602.18832-1-sergiomelas@gmail.com
[groeck: Dropped double empty line in Kconfig]
Signed-off-by: Guenter Roeck <linux@roeck-us.net>
Documentation/hwmon/index.rst
Documentation/hwmon/yogafan.rst [new file with mode: 0644]
MAINTAINERS
drivers/hwmon/Kconfig
drivers/hwmon/Makefile
drivers/hwmon/yogafan.c [new file with mode: 0644]

index 559c32344cd38a9b5bf01a0b7570213f5ae1ab8b..199f35a7528236e261311d47729aebd95a5e7923 100644 (file)
@@ -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 (file)
index 0000000..c0a449a
--- /dev/null
@@ -0,0 +1,130 @@
+.. SPDX-License-Identifier: GPL-2.0-only
+===============================================================================================
+Kernel driver yogafan
+===============================================================================================
+
+Supported chips:
+
+  * Lenovo Yoga, Legion, IdeaPad, Slim, Flex, and LOQ Embedded Controllers
+    Prefix: 'yogafan'
+    Addresses: ACPI handle (See Database Below)
+
+Author: Sergio Melas <sergiomelas@gmail.com>
+
+Description
+-----------
+
+This driver provides fan speed monitoring for modern Lenovo consumer 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.
+
+Hardware Identification and Multiplier Logic
+--------------------------------------------
+
+The driver supports two distinct EC architectures. Differentiation is handled
+deterministically via a DMI Product Family quirk table during the probe phase,
+eliminating the need for runtime heuristics.
+
+1. 8-bit EC Architecture (Multiplier: 100)
+   - **Families:** Yoga, IdeaPad, Slim, Flex.
+   - **Technical Detail:** These models allocate a single 8-bit register for
+   tachometer data. Since 8-bit fields are limited to a value of 255, the
+   BIOS stores fan speed in units of 100 RPM (e.g., 42 = 4200 RPM).
+
+2. 16-bit EC Architecture (Multiplier: 1)
+   - **Families:** Legion, LOQ.
+   - **Technical Detail:** High-performance gaming models require greater
+   precision for fans exceeding 6000 RPM. These use a 16-bit word (2 bytes)
+   storing the raw RPM value directly.
+
+Filter Details:
+---------------
+
+The RLLag filter is a passive discrete-time first-order lag model that ensures:
+  - **Smoothing:** Low-resolution step increments are smoothed into 1-RPM increments.
+  - **Slew-Rate Limiting:** Prevents unrealistic 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 a consistent physical curve regardless
+    of polling frequency.
+
+Suspend and Resume
+------------------
+
+The driver utilizes the boottime clock (ktime_get_boottime()) to calculate the
+sampling delta. This ensures that time spent in system suspend is accounted
+for. If the delta exceeds 5 seconds (e.g., after waking the laptop), the
+filter automatically resets to the current hardware value to prevent
+reporting "ghost" RPM data from before the sleep state.
+
+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.
+
+
+====================================================================================================
+                 LENOVO FAN CONTROLLER: MASTER REFERENCE DATABASE (2026)
+====================================================================================================
+
+MODEL (DMI PN) | FAMILY / SERIES  | EC OFFSET | FULL ACPI OBJECT PATH          | WIDTH  | MULTiplier
+----------------------------------------------------------------------------------------------------
+82N7           | Yoga 14cACN      | 0x06      | \_SB.PCI0.LPC0.EC0.FANS        |  8-bit | 100
+80V2 / 81C3    | Yoga 710/720     | 0x06      | \_SB.PCI0.LPC0.EC0.FAN0        |  8-bit | 100
+83E2 / 83DN    | Yoga Pro 7/9     | 0xFE      | \_SB.PCI0.LPC0.EC0.FANS        |  8-bit | 100
+82A2 / 82A3    | Yoga Slim 7      | 0x06      | \_SB.PCI0.LPC0.EC0.FANS        |  8-bit | 100
+81YM / 82FG    | IdeaPad 5        | 0x06      | \_SB.PCI0.LPC0.EC0.FAN0        |  8-bit | 100
+82JW / 82JU    | Legion 5 (AMD)   | 0xFE/0xFF | \_SB.PCI0.LPC0.EC0.FANS (Fan1) | 16-bit | 1
+82JW / 82JU    | Legion 5 (AMD)   | 0xFE/0xFF | \_SB.PCI0.LPC0.EC0.FA2S (Fan2) | 16-bit | 1
+82WQ           | Legion 7i (Int)  | 0xFE/0xFF | \_SB.PCI0.LPC0.EC0.FANS (Fan1) | 16-bit | 1
+82WQ           | Legion 7i (Int)  | 0xFE/0xFF | \_SB.PCI0.LPC0.EC0.FA2S (Fan2) | 16-bit | 1
+82XV / 83DV    | LOQ 15/16        | 0xFE/0xFF | \_SB.PCI0.LPC0.EC0.FANS /FA2S  | 16-bit | 1
+83AK           | ThinkBook G6     | 0x06      | \_SB.PCI0.LPC0.EC0.FAN0        |  8-bit | 100
+81X1           | Flex 5           | 0x06      | \_SB.PCI0.LPC0.EC0.FAN0        |  8-bit | 100
+*Legacy*       | Pre-2020 Models  | 0x06      | \_SB.PCI0.LPC.EC.FAN0          |  8-bit | 100
+----------------------------------------------------------------------------------------------------
+
+METHODOLOGY & IDENTIFICATION:
+
+1. DSDT ANALYSIS (THE PATH):
+   BIOS ACPI tables were analyzed using 'iasl' and cross-referenced with
+   public dumps. Internal labels (FANS, FAN0, FA2S) are mapped to
+   EmbeddedControl OperationRegion offsets.
+
+2. EC MEMORY MAPPING (THE OFFSET):
+   Validated by matching NBFC (NoteBook FanControl) XML logic with DSDT Field
+   definitions found in BIOS firmware.
+
+3. DATA-WIDTH ANALYSIS (THE MULTIPLIER):
+   - 8-bit (Multiplier 100): Standard for Yoga/IdeaPad. Raw values (0-255).
+   - 16-bit (Multiplier 1): Standard for Legion/LOQ. Two registers (0xFE/0xFF).
+
+
+References
+----------
+
+1. **ACPI Specification (Field Objects):** Documentation on how 8-bit vs 16-bit
+   fields are accessed in OperationRegions.
+   https://uefi.org/specs/ACPI/6.5/05_ACPI_Software_Programming_Model.html#field-objects
+
+2. **NBFC Projects:** Community-driven reverse engineering
+   of Lenovo Legion/LOQ EC memory maps (16-bit raw registers).
+   https://github.com/hirschmann/nbfc/tree/master/Configs
+
+3. **Linux Kernel Timekeeping API:** Documentation for ktime_get_boottime() and
+   handling deltas across suspend states.
+   https://www.kernel.org/doc/html/latest/core-api/timekeeping.html
+
+4. **Lenovo IdeaPad Laptop Driver:** Reference for DMI-based hardware
+   feature gating in Lenovo laptops.
+   https://github.com/torvalds/linux/blob/master/drivers/platform/x86/ideapad-laptop.c
index 1c49613b142ecfeb321ffe08dce76e4201879093..64acaa1e6f69f0fcb6d37e11a248434839bd6c46 100644 (file)
@@ -14878,6 +14878,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
+S:     Maintained
+W:     https://github.com/sergiomelas
+F:     Documentation/hwmon/yogafan.rst
+F:     drivers/hwmon/yogafan.c
+
 LOADPIN SECURITY MODULE
 M:     Kees Cook <kees@kernel.org>
 S:     Supported
index 9d49cfd4ef3d222ecf1b366fefe65788b0ada7ed..152ab31298d94ec3eb62d09d8b68561da026b2cf 100644 (file)
@@ -2651,6 +2651,18 @@ 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 && DMI
+       help
+         If you say yes here you get support for fan speed monitoring
+         on Lenovo Yoga, Legion, IdeaPad, Slim and LOQ laptops.
+         The driver interfaces with the Embedded Controller via ACPI
+         and uses a Rate-Limited Lag filter to smooth RPM readings.
+
+         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
index 556e86d277b195d8c98798012f0069c78a2f37dc..0fce31b43eb1509b135dc35821dd74631dc27419 100644 (file)
@@ -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 (file)
index 0000000..605cc92
--- /dev/null
@@ -0,0 +1,275 @@
+// 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 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 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");