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 <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>
This commit is contained in:
Sergio Melas 2026-03-27 23:16:02 +01:00 committed by Guenter Roeck
parent 331e5fd5bf
commit c67c248ca4
6 changed files with 427 additions and 0 deletions

View File

@ -282,4 +282,5 @@ Hardware Monitoring Kernel Drivers
xdp710
xdpe12284
xdpe152c4
yogafan
zl6100

View File

@ -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

View 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

View 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

View 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/

275
drivers/hwmon/yogafan.c Normal file
View File

@ -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");