From: Sergio Melas Date: Fri, 27 Mar 2026 22:16:02 +0000 (+0100) Subject: hwmon: (yogafan) Add support for Lenovo Yoga/Legion fan monitoring X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c67c248ca406a86cf8b20bf1b3af5e7f3e36581f;p=thirdparty%2Fkernel%2Flinux.git hwmon: (yogafan) Add support for Lenovo Yoga/Legion fan monitoring 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 Link: https://lore.kernel.org/r/20260327221602.18832-1-sergiomelas@gmail.com [groeck: Dropped double empty line in Kconfig] Signed-off-by: Guenter Roeck --- 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..c0a449aa8a36 --- /dev/null +++ b/Documentation/hwmon/yogafan.rst @@ -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 + +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 diff --git a/MAINTAINERS b/MAINTAINERS index 1c49613b142e..64acaa1e6f69 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -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 +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 S: Supported diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig index 9d49cfd4ef3d..152ab31298d9 100644 --- a/drivers/hwmon/Kconfig +++ b/drivers/hwmon/Kconfig @@ -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 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..605cc928f21f --- /dev/null +++ b/drivers/hwmon/yogafan.c @@ -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 + */ +#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 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 "); +MODULE_DESCRIPTION("Lenovo Yoga/Legion Fan Monitor Driver"); +MODULE_LICENSE("GPL");