All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v1] hwmon: (yogafan) Add support for Lenovo Yoga/Legion fan monitoring
@ 2026-06-27  8:40 Sergio Melas
  2026-06-27  8:49 ` sashiko-bot
  0 siblings, 1 reply; 3+ messages in thread
From: Sergio Melas @ 2026-06-27  8:40 UTC (permalink / raw)
  To: Guenter Roeck; +Cc: linux-hwmon, linux-kernel, Sergio Melas

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>
---
v1:
  - Initial patch for the new driver series.
  - Prepared the unified Hardware Abstraction Layer (HAL) framework.
  - Implemented the dynamic Nmax/Rmax hybrid scaling engine logic for discrete ECs.
  - Added a WMI Coexistence Guard to automatically yield control WMI GUIDs are detected.
  - Added deterministic DMI quirk tables mapping explicit ACPI object paths.
  - Integrated 12-bit fixed-point RLLag filtering with 1500 RPM/s slew limiting.
  - Ensured 32-bit architecture compliance using div64_s64 for division.
---
 Documentation/hwmon/yogafan.rst |  35 +++++---
 drivers/hwmon/yogafan.c         | 139 ++++++++++++++++++++++++++++----
 2 files changed, 149 insertions(+), 25 deletions(-)

diff --git a/Documentation/hwmon/yogafan.rst b/Documentation/hwmon/yogafan.rst
index 68761947a..000fe032d 100644
--- a/Documentation/hwmon/yogafan.rst
+++ b/Documentation/hwmon/yogafan.rst
@@ -23,6 +23,9 @@ 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.
 
+The driver includes a WMI Coexistence Guard that automatically yields hardware
+register control to lenovo-wmi-other when modern gaming GUID blocks are active.
+
 Hardware Identification and Multiplier Logic
 --------------------------------------------
 
@@ -69,10 +72,11 @@ Usage
 
 The driver exposes standard hwmon sysfs attributes:
 
-===============   ============================
-Attribute         Description
-fanX_input        Filtered fan speed in RPM.
-===============   ============================
+===============  ======================================================
+Attribute        Description
+fanX_input       Filtered fan speed in RPM.
+fanX_max         Maximum design capability threshold limit in RPM.
+===============  ======================================================
 
 
 Note: If the hardware reports 0 RPM, the filter is bypassed and 0 is reported
@@ -99,7 +103,7 @@ immediately to ensure the user knows the fan has stopped.
  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
+ *Legacy* | Pre-2020 Models  | 0x06      | \_SB.PCI0.LPC.EC.FAN0          |  8-bit | 100
  ----------------------------------------------------------------------------------------------------
 
 METHODOLOGY & IDENTIFICATION:
@@ -122,17 +126,30 @@ References
 ----------
 
 1. **ACPI Specification (Field Objects):** Documentation on how 8-bit vs 16-bit
-   fields are accessed in OperationRegions.
+   [cite_start]fields are accessed in OperationRegions[cite: 57].
    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).
+   [cite_start]of Lenovo Legion/LOQ EC memory maps (16-bit raw registers)[cite: 58].
    https://github.com/hirschmann/nbfc/tree/master/Configs
 
 3. **Linux Kernel Timekeeping API:** Documentation for ktime_get_boottime() and
-   handling deltas across suspend states.
+   [cite_start]handling deltas across suspend states[cite: 59].
    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.
+   [cite_start]feature gating in Lenovo laptops[cite: 60].
    https://github.com/torvalds/linux/blob/master/drivers/platform/x86/lenovo/ideapad-laptop.c
+
+5. **Lenovo Product Specifications Reference (PSREF):** Official hardware layout index
+   [cite_start]and spec sheets for active and withdrawn Lenovo laptop models[cite: 68].
+   https://psref.lenovo.com/l/withdrawn/
+
+6. **Yogafan Master Quirk Database:** Master spreadsheet mapping Lenovo Product
+   [cite_start]Specifications Reference (PSREF) to explicit EC offsets, register widths, paths, and multipliers[cite: 68].
+   https://github.com/sergiomelas/lenovo-linux-drivers/blob/main/Lenovo_Drivers/Prototype/PSREF/yogafan_v3_quirks_database.ods
+
+7. **Yogafan ACPI DSDT Repository:** Central repository containing user-contributed raw
+   [cite_start]and decompiled ACPI DSDT firmware dumps used for path verification and hardware expansions[cite: 61].
+   https://github.com/sergiomelas/lenovo-linux-drivers/tree/main/Lenovo_Drivers/Prototype/DSDT
+
diff --git a/drivers/hwmon/yogafan.c b/drivers/hwmon/yogafan.c
index 605cc928f..7d66d563e 100644
--- a/drivers/hwmon/yogafan.c
+++ b/drivers/hwmon/yogafan.c
@@ -24,10 +24,13 @@
 #include <linux/platform_device.h>
 #include <linux/slab.h>
 #include <linux/math64.h>
+#include <linux/minmax.h>
+#include <linux/hwmon-sysfs.h>
+#include <linux/wmi.h>
 
 /* Driver Configuration Constants */
 #define DRVNAME			"yogafan"
-#define MAX_FANS		8
+#define MAX_FANS		5
 
 /* Filter Configuration Constants */
 #define TAU_MS			1000	/* Time constant for the first-order lag (ms) */
@@ -36,11 +39,20 @@
 #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 */
+#define MIN_THRESHOLD_RPM	10	/* Minimum safety floor for per-model stop thresholds */
+
+/* GUID of WMI interface Lenovo */
+#define LENOVO_WMI_OTHER_MODE_GUID	"DC2A8805-3A8C-41BA-A6F7-092E0089CD3B"
+#define LENOVO_CAPABILITY_DATA_00_GUID	"362A3AFE-3D96-4665-8530-96DAD5BB300E"
+#define LENOVO_FAN_TEST_DATA_GUID	"B642801B-3D21-45DE-90AE-6E86F164FB21"
 
 struct yogafan_config {
 	int multiplier;
 	int fan_count;
+	int r_max;		        /* Maximum physical RPM for UI scaling */
+	unsigned int tau_ms;		/* To store the smoothing speed */
+	unsigned int slew_time_s;	/* To store the acceleration limit */
+	unsigned int stop_threshold;	/* To store the RPM floor */
 	const char *paths[2];
 };
 
@@ -50,48 +62,109 @@ struct yoga_fan_data {
 	ktime_t last_sample[MAX_FANS];
 	int multiplier;
 	int fan_count;
+	int device_max_rpm;	/* Stores the active maximum RPM ceiling */
+	unsigned int internal_tau_ms;
+	unsigned int internal_max_slew_rpm_s;
+	const struct yogafan_config *config;
 };
 
 /* Specific configurations mapped via DMI */
 static const struct yogafan_config yoga_8bit_fans_cfg = {
 	.multiplier = 100,
 	.fan_count = 1,
+	.r_max = 5500,
+	.tau_ms = 1000,
+	.slew_time_s = 4,
+	.stop_threshold = 50,
 	.paths = { "\\_SB.PCI0.LPC0.EC0.FANS", NULL }
 };
 
 static const struct yogafan_config ideapad_8bit_fan0_cfg = {
 	.multiplier = 100,
 	.fan_count = 1,
+	.r_max = 4500,
+	.tau_ms = 1000,
+	.slew_time_s = 4,
+	.stop_threshold = 50,
 	.paths = { "\\_SB.PCI0.LPC0.EC0.FAN0", NULL }
 };
 
 static const struct yogafan_config legion_16bit_dual_cfg = {
 	.multiplier = 1,
 	.fan_count = 2,
+	.r_max = 6500,
+	.tau_ms = 1300,
+	.slew_time_s = 5,
+	.stop_threshold = 50,
 	.paths = { "\\_SB.PCI0.LPC0.EC0.FANS", "\\_SB.PCI0.LPC0.EC0.FA2S" }
 };
 
+/*
+ * Filter Physics (RLLag) - Deterministic Telemetry
+ * ---------------------
+ * To address low-resolution tachometer sampling in the Embedded Controller,
+ * the driver implements a passive discrete-time first-order lag filter
+ * with slew-rate limiting (RLLag).
+ *
+ * The filter update equation is:
+ * RPM_state[t+1] = RPM_state[t] + Clamp(Alpha * (raw_RPM[t] - RPM_state[t]),
+ * -limit[t], limit[t])
+ * Where:
+ * Ts[t]    = Sys_time[t+1] - Sys_time[t]  (Time delta between reads)
+ * Alpha    = 1 - exp(-Ts[t] / Tau)        (Low-pass smoothing factor)
+ * limit[t] = Slew_Limit * Ts[t]           (Time-normalized slew limit)
+ *
+ * To avoid expensive floating-point exponential calculations in the kernel,
+ * we use a first-order Taylor/Bilinear approximation:
+ * Alpha = Ts / (Tau + Ts)
+ *
+ * Implementing this in the driver state machine:
+ * Ts             = current_time - last_sample_time
+ * Alpha          = Ts / (Tau + Ts)
+ * Physics Principles:
+ * step           = Alpha * (raw_RPM - RPM_old)
+ * limit          = Slew_Limit * Ts
+ * step_clamped   = clamp(step, -limit, limit)
+ * RPM_new        = RPM_old + step_clamped
+ *
+ * Attributes of the RLLag model:
+ * - Smoothing: Low-resolution step increments are smoothed into 1-RPM increments.
+ * - Slew-Rate Limiting: Capping change to ~1500 RPM/s to match physical inertia.
+ * - Polling Independence: Math scales based on Ts, ensuring a consistent physical
+ * curve regardless of userspace polling frequency.
+ * Fixed-point math (2^12) is used to maintain precision without floating-point
+ * overhead, ensuring jitter-free telemetry for thermal management.
+ */
 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]));
+	s64 raw_dt_ms;
 	long delta, step, limit, alpha;
 	s64 temp_num;
+	u32 dt_ms;
+
+	/* 1. PHYSICAL CLAMP: Use per-device device_max_rpm */
+	if (raw_rpm > (long)data->device_max_rpm)
+		raw_rpm = (long)data->device_max_rpm;
 
-	if (raw_rpm < RPM_FLOOR_LIMIT) {
+	/* 2. Threshold logic: Deterministic safe-state */
+	if (raw_rpm < (long)max_t(u32, MIN_THRESHOLD_RPM, data->config->stop_threshold)) {
 		data->filtered_val[idx] = 0;
 		data->last_sample[idx] = now;
 		return;
 	}
 
-	if (data->last_sample[idx] == 0 || dt_ms > MAX_SAMPLING) {
+	/* 3. Auto-Reset Logic: Snap to hardware value after long gaps (>5s) */
+	/*   Ref: [TAG: INIT_STATE, STALE_DATA_THRESHOLD] */
+	raw_dt_ms = ktime_to_ms(ktime_sub(now, data->last_sample[idx]));
+
+	if (data->last_sample[idx] == 0 || raw_dt_ms < MIN_SAMPLING || raw_dt_ms > MAX_SAMPLING) {
 		data->filtered_val[idx] = raw_rpm;
 		data->last_sample[idx] = now;
 		return;
 	}
 
-	if (dt_ms < MIN_SAMPLING)
-		return;
+	dt_ms = (u32)raw_dt_ms;
 
 	delta = raw_rpm - data->filtered_val[idx];
 	if (delta == 0) {
@@ -99,14 +172,20 @@ static void apply_rllag_filter(struct yoga_fan_data *data, int idx, long raw_rpm
 		return;
 	}
 
-	temp_num = dt_ms << 12;
-	alpha = (long)div64_s64(temp_num, (s64)(TAU_MS + dt_ms));
+	/* 4. Physics Engine: Discretized RLLAG filter (Fixed-Point 2^12) */
+	/* Ref: [TAG: MODEL_CONST, ALPHA_DERIVATION, ANTI_STALL_LOGIC] */
+	temp_num = (s64)dt_ms << 12;
+	alpha = div64_u64(temp_num, data->internal_tau_ms + dt_ms);
 	step = (delta * alpha) >> 12;
 
+	/* Ensure minimal movement for small deltas */
 	if (step == 0 && delta != 0)
 		step = (delta > 0) ? 1 : -1;
 
-	limit = (MAX_SLEW_RPM_S * (long)dt_ms) / 1000;
+	/* 5. Dynamic Slew Limiting: Applied per-model inertia ramp */
+	/* Ref: [TAG: SLEW_RATE_MAX, SLOPE_CALC, MIN_SLEW_LIMIT] */
+	limit = (data->internal_max_slew_rpm_s * dt_ms) / 1000;
+
 	if (limit < 1)
 		limit = 1;
 
@@ -115,6 +194,7 @@ static void apply_rllag_filter(struct yoga_fan_data *data, int idx, long raw_rpm
 	else if (step < -limit)
 		step = -limit;
 
+	/* 6. Update internal state */
 	data->filtered_val[idx] += step;
 	data->last_sample[idx] = now;
 }
@@ -126,7 +206,16 @@ static int yoga_fan_read(struct device *dev, enum hwmon_sensor_types type,
 	unsigned long long raw_acpi;
 	acpi_status status;
 
-	if (type != hwmon_fan || attr != hwmon_fan_input)
+	if (type != hwmon_fan)
+		return -EOPNOTSUPP;
+
+	/* Intercept MAX attribute queries to feed the UI scale framework */
+	if (attr == hwmon_fan_max) {
+		*val = (long)data->device_max_rpm;
+		return 0;
+	}
+
+	if (attr != hwmon_fan_input)
 		return -EOPNOTSUPP;
 
 	status = acpi_evaluate_integer(data->active_handles[channel], NULL, NULL, &raw_acpi);
@@ -155,12 +244,15 @@ static const struct hwmon_ops yoga_fan_hwmon_ops = {
 	.read = yoga_fan_read,
 };
 
-static const struct hwmon_channel_info *yoga_fan_info[] = {
+/* Static configuration for the hwmon core */
+static const struct hwmon_channel_info *const 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),
+			   HWMON_F_INPUT | HWMON_F_MAX,
+			   HWMON_F_INPUT | HWMON_F_MAX,
+			   HWMON_F_INPUT | HWMON_F_MAX,
+			   HWMON_F_INPUT | HWMON_F_MAX,
+			   HWMON_F_INPUT | HWMON_F_MAX,
+			   HWMON_F_INPUT | HWMON_F_MAX),
 	NULL
 };
 
@@ -206,6 +298,17 @@ static int yoga_fan_probe(struct platform_device *pdev)
 	struct device *hwmon_dev;
 	int i;
 
+	/* Check for WMI interfaces that handle fan/thermal management. */
+	/*  If present, we yield to the WMI driver to prevent double-reporting. */
+#if IS_REACHABLE(CONFIG_ACPI_WMI)
+	if (wmi_has_guid(LENOVO_WMI_OTHER_MODE_GUID) &&
+	    wmi_has_guid(LENOVO_CAPABILITY_DATA_00_GUID) &&
+	    wmi_has_guid(LENOVO_FAN_TEST_DATA_GUID)) {
+		dev_info(&pdev->dev, "Lenovo WMI management interface detected; yielding to WMI driver\n");
+		return -ENODEV;
+	}
+#endif
+
 	dmi_id = dmi_first_match(yogafan_quirks);
 	if (!dmi_id)
 		return -ENODEV;
@@ -215,7 +318,11 @@ static int yoga_fan_probe(struct platform_device *pdev)
 	if (!data)
 		return -ENOMEM;
 
+	data->config = cfg;
 	data->multiplier = cfg->multiplier;
+	data->device_max_rpm = cfg->r_max ?: 5000; /* Fallback safety baseline */
+	data->internal_tau_ms = cfg->tau_ms ?: 1000; /* Robustness: Prevent zero-division */
+	data->internal_max_slew_rpm_s = data->device_max_rpm / (cfg->slew_time_s ?: 1);
 
 	for (i = 0; i < cfg->fan_count; i++) {
 		acpi_status status;
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-06-27 15:22 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-27  8:40 [PATCH v1] hwmon: (yogafan) Add support for Lenovo Yoga/Legion fan monitoring Sergio Melas
2026-06-27  8:49 ` sashiko-bot
2026-06-27 15:22   ` Guenter Roeck

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.