From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from mail-wr1-f45.google.com (mail-wr1-f45.google.com [209.85.221.45]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id F03A138B7D2 for ; Mon, 23 Mar 2026 10:56:25 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.221.45 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1774263387; cv=none; b=UuP/AgkuCwtCV5XGtI/vghmRvlg8+DQoQNPEfOjtk1NsOA7R5SG/KiOqr5thNbG/wR/pneVuHW81avdot4lLskOTNeKKJySlMwZu2AZzD8LMeUHGHdM/OnIQb1gKCeL736rIzQN08efaLryMTzAYMAzfiM1CTUDZZ1V+PwOHDBk= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1774263387; c=relaxed/simple; bh=exUgN9ozvBVYjXHGO3+D7euoFahbifcZL94tiubUyaA=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=sNOAidyOP6hP+CZ7yHCQUHpiuq+nGWZYSmpguGXYeGDK8J5NgT7obzwJUCRi/o6o4u5tdiFxDDu6N9JkGJXLCfOgjQ0eNQ3d7bI3i4bsnuLmqI9pMvDcbnoDlNIv1kfuvHT4Vu1StN9O+hOPZjC1H2CFHduTYqnRoJf19OhaPb0= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=AXdVSNhm; arc=none smtp.client-ip=209.85.221.45 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="AXdVSNhm" Received: by mail-wr1-f45.google.com with SMTP id ffacd0b85a97d-439d8dc4ae4so3528038f8f.2 for ; Mon, 23 Mar 2026 03:56:25 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1774263384; x=1774868184; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=qNFMrIb7sOFleg8FVH+dkq3LR2z/UOdSzSRlRZxwf98=; b=AXdVSNhmDsHYq8Fkgp0J8jLGc4BX784n9oyAEG8glk8zPlAuDmcWFSao2rQvc7Z14F sMqHC1dbuimCFYHQ2tnAZ7u96YLnu2q/pmpupqIbqsyzeUBh32aZAZiQTYAuJ1CoPbh9 SGx5DKQ16FzlzV3c17vE20cGt1MjTWFoYZSEbw2Wt315VHYVIN8OEgYSqNQLiZn1hmOe A3Agx1BzhORRq8u8iMLCYXjZtKvm0pP++iuB4V6Rv6JcWmmkMgxiRAM0LX1e+jprHZX7 OEMuXNQ4rSRLQ1CPbXzG+aKUPq/gxNJKqADadpz8OaMAtfBp8AwwE6hh3MQv3QOVIWOC BANA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1774263384; x=1774868184; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=qNFMrIb7sOFleg8FVH+dkq3LR2z/UOdSzSRlRZxwf98=; b=l908DMLlEH9fUxenWUlQUtWQspJ2o9uWgYQJjaSsk0ngD+6MnTMgOX0LgOOA9a30Gv A+dynFVY5BBPAhZmhzB0/HYsBUfcrjP0jOPStFNsqJfHgabXgo0mG4FTwlD2SkwSxqK1 Tr0zWAqSH9fcs4WyLS5SjJFu2b+PKWqSUndWykEUduGJHhvaQRQtczwypG5yuBtTQAmj +isXbBLUZmI5EinHRMlUl2luGjD2NJFJs+efZxvG40a4PpkajeaTjxxZzxz8M7NVDYXo IhOwgkzbEIux00FC4XwGy/5ks1qe+IWaDZhjxwUZeorwoChrYlQjFJD1UX/1y3UBsWN9 hRoQ== X-Gm-Message-State: AOJu0Yz8SK/lVy6nLroGImW5oy+RksnG/sR5XSKhuZCReCDNlLqkmcL0 OjTyL8lywzfnzScbUd/vt74JDoMY4GGwLLO32icuE5DHCL2C5hECGx2x8uzdWNyW X-Gm-Gg: ATEYQzwUx499soLqRhbyTHKbURAIRCcffpYs9DUwYk/En/D2L5/9dJekAuc0GoxNzOQ yqUH9e+H7BDO/I69ONj612eNcJHk1ITwRU0+9RcCcOoSWDT53cfDiY2j7fKnB9uScUJoa6IfLNp 6G6zvZgLrpYFKwezpA5JTbzjjk1KWlvZWLy+dn1Cs+GkIRSCZIGSsGIDMOu1ns/t/yWDQnKipqJ 2jkL0V9h/eEcSeLescrJlnQhsAt+D99yEN1FVeiwoqzOnv4mzX3QMXJapBDK4qirXZoT4VK61FF ZMkhzYgE5KtafdcwgrS6xRVcu8VOgIKnboZs6pjQpAn93m924jlIGizejblxIxd0YI+w7dUBw5u eoxParlmoLFdjKUAzxsm2UEOzQO5VX9r30WDRSKCLL/DwPkadYeTiqU5ZDbwUOIpx5NM43H9DeT POyZMkN0wGs2UScfenUNssKDxM/8E= X-Received: by 2002:a5d:64c6:0:b0:43b:4faf:a496 with SMTP id ffacd0b85a97d-43b64262cfbmr18167584f8f.31.1774263382466; Mon, 23 Mar 2026 03:56:22 -0700 (PDT) Received: from sergio-82n7 ([134.255.161.117]) by smtp.gmail.com with ESMTPSA id ffacd0b85a97d-43b644bd923sm34027860f8f.12.2026.03.23.03.56.21 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 23 Mar 2026 03:56:22 -0700 (PDT) From: Sergio Melas To: Guenter Roeck Cc: linux-hwmon@vger.kernel.org, sergiomelas@gmail.com Subject: [PATCH v5] hwmon: (yogafan) Add support for Lenovo Yoga/Legion fan monitoring Date: Mon, 23 Mar 2026 11:56:12 +0100 Message-ID: <20260323105612.343381-1-sergiomelas@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <2e692427-592a-4a78-8f6b-547d506de86a@roeck-us.net> References: <2e692427-592a-4a78-8f6b-547d506de86a@roeck-us.net> Precedence: bulk X-Mailing-List: linux-hwmon@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit 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 --- 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. - Rebased on groeck/hwmon-next branch for clean application. 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. --- MAINTAINERS | 6 ++ drivers/hwmon/Kconfig | 11 ++ drivers/hwmon/Makefile | 1 + drivers/hwmon/yogafan.c | 225 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 243 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 +L: linux-hwmon@vger.kernel.org +S: Maintained +F: drivers/hwmon/yogafan.c + LOADPIN SECURITY MODULE M: Kees Cook S: Supported diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig index 7dd8381ba0d0..5f6f65dea155 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..92158fe47a98 --- /dev/null +++ b/drivers/hwmon/yogafan.c @@ -0,0 +1,225 @@ +// 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 + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +/* Driver Configuration Constants */ +#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 (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_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 + */ +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; + s64 temp_num; + + 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])); + if (dt_ms > MAX_SAMPLING) { + data->filtered_val[idx] = raw_rpm; + data->last_update[idx] = now; + return; + } + if (dt_ms < 1) + return; + delta = raw_rpm - data->filtered_val[idx]; + /* Use div64_s64 for 32-bit compatibility (Alpha = dt / (Tau + dt)) */ + temp_num = dt_ms << 10; + alpha = (long)div64_s64(temp_num, (s64)(TAU_MS + dt_ms)); + step = (delta * alpha) >> 10; + /* Fix the stall: Ensure limit is at least 1 if delta exists */ + limit = (MAX_SLEW_RPM_S * (long)dt_ms) / 1000; + if (limit == 0 && delta != 0) + limit = 1; + if (step > limit) + step = limit; + else if (step < -limit) + step = -limit; + data->filtered_val[idx] += step; + /* Sanitation: Floor logic triggered only when hardware reports 0 */ + if (data->filtered_val[idx] < RPM_FLOOR_LIMIT && raw_rpm == 0) + 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; + 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 "); +MODULE_DESCRIPTION("Lenovo Yoga/Legion Fan Monitor Driver"); +MODULE_LICENSE("GPL"); -- 2.53.0