[<prev] [next>] [<thread-prev] [day] [month] [year] [list]
Message-Id: <20251005222513.7794-7-W_Armin@gmx.de>
Date: Mon, 6 Oct 2025 00:25:13 +0200
From: Armin Wolf <W_Armin@....de>
To: rafael@...nel.org,
lenb@...nel.org
Cc: linux-acpi@...r.kernel.org,
linux-kernel@...r.kernel.org
Subject: [PATCH 6/6] ACPI: fan: Add support for Microsoft fan extensions
Microsoft has designed a set of extensions for the ACPI fan device
allowing the OS to specify a set of fan speed trip points. The
platform firmware will then notify the ACPI fan device when one
of the trip points is triggered.
Unfortunatly, some device manufacturers (like HP) blindly assume
that the OS will use said extensions and thus only update the values
returned by the _FST control method when receiving such a
notification. As a result the ACPI fan driver is currently unusable
on such machines, always reporting a constant value.
Fix this by adding support for the Microsoft extensions. During probe
and when resuming from suspend the driver will attempt to trigger an
initial notification that will update the values returned by _FST.
Said trip points will be updated each time a notification is received
from the platform firmware to ensure that the values returned by
the _FST control method are updated.
Closes: https://github.com/lm-sensors/lm-sensors/issues/506
Signed-off-by: Armin Wolf <W_Armin@....de>
---
drivers/acpi/fan.h | 2 +
drivers/acpi/fan_core.c | 176 +++++++++++++++++++++++++++++++++++++++-
2 files changed, 176 insertions(+), 2 deletions(-)
diff --git a/drivers/acpi/fan.h b/drivers/acpi/fan.h
index dcc1ad3118ff..f85f9a0fbfcd 100644
--- a/drivers/acpi/fan.h
+++ b/drivers/acpi/fan.h
@@ -56,6 +56,8 @@ struct acpi_fan {
struct acpi_fan_fif fif;
struct acpi_fan_fps *fps;
int fps_count;
+ /* A value of 0 means that trippoint-related functions are not supported */
+ u32 fan_trip_granularity;
#if IS_REACHABLE(CONFIG_HWMON)
struct device *hdev;
#endif
diff --git a/drivers/acpi/fan_core.c b/drivers/acpi/fan_core.c
index 3dbd22bc3a48..ca8c291ac8cc 100644
--- a/drivers/acpi/fan_core.c
+++ b/drivers/acpi/fan_core.c
@@ -7,11 +7,16 @@
* Copyright (C) 2022 Intel Corporation. All rights reserved.
*/
+#include <linux/bits.h>
#include <linux/kernel.h>
+#include <linux/limits.h>
+#include <linux/math.h>
+#include <linux/math64.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/uaccess.h>
+#include <linux/uuid.h>
#include <linux/thermal.h>
#include <linux/acpi.h>
#include <linux/platform_device.h>
@@ -21,6 +26,20 @@
#define ACPI_FAN_NOTIFY_STATE_CHANGED 0x80
+static const guid_t acpi_fan_microsoft_guid = GUID_INIT(0xA7611840, 0x99FE, 0x41AE, 0xA4, 0x88,
+ 0x35, 0xC7, 0x59, 0x26, 0xC8, 0xEB);
+#define ACPI_FAN_DSM_GET_TRIP_POINT_GRANULARITY 1
+#define ACPI_FAN_DSM_SET_TRIP_POINTS 2
+#define ACPI_FAN_DSM_GET_OPERATING_RANGES 3
+
+/*
+ * Ensures that fans with a very low trip point granularity
+ * do not send too many notifications.
+ */
+static uint min_trip_distance = 100;
+module_param(min_trip_distance, uint, 0);
+MODULE_PARM_DESC(min_trip_distance, "Minimum distance between fan speed trip points in RPM");
+
static const struct acpi_device_id fan_device_ids[] = {
ACPI_FAN_DEVICE_IDS,
{"", 0},
@@ -310,6 +329,138 @@ static int acpi_fan_get_fps(struct acpi_device *device)
return status;
}
+static int acpi_fan_dsm_init(struct device *dev)
+{
+ union acpi_object dummy = {
+ .package = {
+ .type = ACPI_TYPE_PACKAGE,
+ .count = 0,
+ .elements = NULL,
+ },
+ };
+ struct acpi_fan *fan = dev_get_drvdata(dev);
+ union acpi_object *obj;
+ int ret = 0;
+
+ if (!acpi_check_dsm(fan->handle, &acpi_fan_microsoft_guid, 0,
+ BIT(ACPI_FAN_DSM_GET_TRIP_POINT_GRANULARITY) |
+ BIT(ACPI_FAN_DSM_SET_TRIP_POINTS)))
+ return 0;
+
+ dev_info(dev, "Using Microsoft fan extensions\n");
+
+ obj = acpi_evaluate_dsm(fan->handle, &acpi_fan_microsoft_guid, 0,
+ ACPI_FAN_DSM_GET_TRIP_POINT_GRANULARITY, &dummy);
+ if (!obj)
+ return -EIO;
+
+ if (obj->type != ACPI_TYPE_INTEGER) {
+ ret = -EPROTO;
+ goto out_free;
+ }
+
+ if (obj->integer.value > U32_MAX) {
+ ret = -EOVERFLOW;
+ goto out_free;
+ }
+
+ fan->fan_trip_granularity = obj->integer.value;
+
+out_free:
+ kfree(obj);
+
+ return ret;
+}
+
+static int acpi_fan_dsm_set_trip_points(struct device *dev, u64 upper, u64 lower)
+{
+ union acpi_object args[2] = {
+ {
+ .integer = {
+ .type = ACPI_TYPE_INTEGER,
+ .value = lower,
+ },
+ },
+ {
+ .integer = {
+ .type = ACPI_TYPE_INTEGER,
+ .value = upper,
+ },
+ },
+ };
+ struct acpi_fan *fan = dev_get_drvdata(dev);
+ union acpi_object in = {
+ .package = {
+ .type = ACPI_TYPE_PACKAGE,
+ .count = ARRAY_SIZE(args),
+ .elements = args,
+ },
+ };
+ union acpi_object *obj;
+
+ obj = acpi_evaluate_dsm(fan->handle, &acpi_fan_microsoft_guid, 0,
+ ACPI_FAN_DSM_SET_TRIP_POINTS, &in);
+ kfree(obj);
+
+ return 0;
+}
+
+static int acpi_fan_dsm_start(struct device *dev)
+{
+ struct acpi_fan *fan = dev_get_drvdata(dev);
+ int ret;
+
+ if (!fan->fan_trip_granularity)
+ return 0;
+
+ /*
+ * Some firmware implementations only update the values returned by the
+ * _FST control method when a notification is received. This usually works
+ * with Microsoft Windows as setting up trip points will keep triggering
+ * said notifications, but will cause issues when using _FST without the
+ * Microsoft-specific trip point extension.
+ *
+ * Because of this we have to ensure that an initial notification is triggered
+ * to start the cycle of trip points updates. We achive this by setting the trip
+ * points sequencially to two separate ranges. As by the Microsoft specification
+ * the firmware should trigger a notification immediately if the fan speed is outside
+ * of the trip point range. This _should_ result in at least one notification as both
+ * ranges do not overlap, meaning that the current fan speed needs to be outside of
+ * at least one range.
+ */
+ ret = acpi_fan_dsm_set_trip_points(dev, fan->fan_trip_granularity, 0);
+ if (ret < 0)
+ return ret;
+
+ return acpi_fan_dsm_set_trip_points(dev, fan->fan_trip_granularity * 3,
+ fan->fan_trip_granularity * 2);
+}
+
+static int acpi_fan_dsm_update_trips_points(struct device *dev, struct acpi_fan_fst *fst)
+{
+ struct acpi_fan *fan = dev_get_drvdata(dev);
+ u64 upper, lower;
+
+ if (!fan->fan_trip_granularity)
+ return 0;
+
+ if (!acpi_fan_speed_valid(fst->speed))
+ return -EINVAL;
+
+ upper = roundup_u64(fst->speed + min_trip_distance, fan->fan_trip_granularity);
+ if (fst->speed <= min_trip_distance) {
+ lower = 0;
+ } else {
+ /*
+ * Valid fan speed values cannot be larger than 32 bit, so
+ * we can safely assume that no overflow will happen here.
+ */
+ lower = rounddown((u32)fst->speed - min_trip_distance, fan->fan_trip_granularity);
+ }
+
+ return acpi_fan_dsm_set_trip_points(dev, upper, lower);
+}
+
static void acpi_fan_notify_handler(acpi_handle handle, u32 event, void *context)
{
struct device *dev = context;
@@ -323,8 +474,13 @@ static void acpi_fan_notify_handler(acpi_handle handle, u32 event, void *context
* receive an ACPI event indicating that the fan state has changed.
*/
ret = acpi_fan_get_fst(handle, &fst);
- if (ret < 0)
+ if (ret < 0) {
dev_err(dev, "Error retrieving current fan status: %d\n", ret);
+ } else {
+ ret = acpi_fan_dsm_update_trips_points(dev, &fst);
+ if (ret < 0)
+ dev_err(dev, "Failed to update trip points: %d\n", ret);
+ }
acpi_fan_notify_hwmon(dev);
break;
@@ -393,6 +549,10 @@ static int acpi_fan_probe(struct platform_device *pdev)
}
if (fan->has_fst) {
+ result = acpi_fan_dsm_init(&pdev->dev);
+ if (result)
+ return result;
+
result = devm_acpi_fan_create_hwmon(&pdev->dev);
if (result)
return result;
@@ -401,6 +561,12 @@ static int acpi_fan_probe(struct platform_device *pdev)
if (result)
return result;
+ result = acpi_fan_dsm_start(&pdev->dev);
+ if (result) {
+ dev_err(&pdev->dev, "Failed to start Microsoft fan extensions\n");
+ return result;
+ }
+
result = acpi_fan_create_attributes(device);
if (result)
return result;
@@ -486,8 +652,14 @@ static int acpi_fan_suspend(struct device *dev)
static int acpi_fan_resume(struct device *dev)
{
- int result;
struct acpi_fan *fan = dev_get_drvdata(dev);
+ int result;
+
+ if (fan->has_fst) {
+ result = acpi_fan_dsm_start(dev);
+ if (result)
+ dev_err(dev, "Failed to start Microsoft fan extensions: %d\n", result);
+ }
if (fan->acpi4)
return 0;
--
2.39.5
Powered by blists - more mailing lists