mirror of
https://github.com/torvalds/linux.git
synced 2026-05-13 08:39:31 +02:00
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:
parent
331e5fd5bf
commit
c67c248ca4
|
|
@ -282,4 +282,5 @@ Hardware Monitoring Kernel Drivers
|
|||
xdp710
|
||||
xdpe12284
|
||||
xdpe152c4
|
||||
yogafan
|
||||
zl6100
|
||||
|
|
|
|||
130
Documentation/hwmon/yogafan.rst
Normal file
130
Documentation/hwmon/yogafan.rst
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
275
drivers/hwmon/yogafan.c
Normal 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");
|
||||
Loading…
Reference in New Issue
Block a user