[PATCH 6/7] platform/x86: asus-wmi: expose the fan floor tables via hwmon

From: Bartu Alev

Date: Sun Nov 01 2026 - 05:49:56 EST


Firmware enforces minimum duties on fan curves without updating the
reported curve buffers, leaving userspace unable to determine which
duty ranges are unreachable.

Export read-only pwm[1-3]_auto_point[1-8]_floor_pwm attributes on the
hwmon device. Each file returns the minimum duty (0-255 PWM) enforced
for that curve point. The attributes are registered for every
supported fan curve and return -ENODATA when firmware provides no
floor table.

Standard hwmon curve attributes (pwmY_auto_pointZ_pwm) assume
writable points and cannot represent a separate, hardware enforced
floor. Dedicated read-only floor attributes provide these values
while adhering to the one value per file rule.

Document these attributes in
Documentation/ABI/testing/sysfs-platform-asus-wmi.

Signed-off-by: Bartu Alev <bartualev@xxxxxxxxx>
---
Documentation/ABI/testing/sysfs-platform-asus-wmi | 23 ++++
drivers/platform/x86/asus-wmi.c | 141 +++++++++++++++++++++-
2 files changed, 163 insertions(+), 1 deletion(-)

diff --git a/Documentation/ABI/testing/sysfs-platform-asus-wmi b/Documentation/ABI/testing/sysfs-platform-asus-wmi
index 775ea88625c0d..24931cc97ca86 100644
--- a/Documentation/ABI/testing/sysfs-platform-asus-wmi
+++ b/Documentation/ABI/testing/sysfs-platform-asus-wmi
@@ -297,3 +297,26 @@ Contact: Luke D. Jones <luke@xxxxxxxxxx>, Denis Benato <denis.benato@xxxxxxxxx>,
Description:
Same as the pwm1 and pwm2 curve points, for the mid fan
(pwm3) of laptops whose firmware exposes a third fan curve.
+
+What: /sys/class/hwmon/hwmonX/pwm[1-3]_auto_point[1-8]_floor_pwm
+Date: Nov 2026
+KernelVersion: 7.5
+Contact: Bartu Alev <bartualev@xxxxxxxxx>
+Description:
+ Read-only minimum duty, in pwm units (0-255), that the
+ embedded controller enforces for curve point Z of fan Y.
+ The firmware reports the floor as a percentage; the driver
+ converts it with DIV_ROUND_UP(255 * percent, 100), which maps
+ every 0-100 percentage back to itself.
+ Point Z (Z = 1..8) is bound to the temperature decade
+ [30 + 10 * (Z - 1), 39 + 10 * (Z - 1)] degrees Celsius and
+ the value is the floor of that decade. The controller applies
+ max(curve, floor) for every point on each thermal tick, so
+ the floor is a hardware limit on top of the curve written
+ through pwmY_auto_pointZ_pwm.
+
+ The attribute exists for every fan with a custom fan curve.
+ Reading it returns -ENODATA when the firmware of that fan
+ does not provide a floor table. A zero value is a real
+ floor: the controller then enforces no minimum duty for that
+ point.
diff --git a/drivers/platform/x86/asus-wmi.c b/drivers/platform/x86/asus-wmi.c
index dc1e69b964c99..74476904cb99c 100644
--- a/drivers/platform/x86/asus-wmi.c
+++ b/drivers/platform/x86/asus-wmi.c
@@ -3868,6 +3868,23 @@ static ssize_t fan_curve_show(struct device *dev,
return sysfs_emit(buf, "%d\n", value);
}

+static ssize_t fan_curve_floor_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct sensor_device_attribute_2 *dev_attr = to_sensor_dev_attr_2(attr);
+ struct asus_wmi *asus = dev_get_drvdata(dev);
+ struct fan_curve_data *data = &asus->custom_fan_curves[dev_attr->nr];
+ u8 floor;
+
+ guard(mutex)(&asus->fan_curve_lock);
+ if (!data->floors_valid)
+ return -ENODATA;
+
+ floor = data->floors[dev_attr->index];
+
+ return sysfs_emit(buf, "%u\n", floor);
+}
+
/*
* "fan_dev" is the related WMI method such as ASUS_WMI_DEVID_CPU_FAN_CURVE.
*/
@@ -4153,6 +4170,58 @@ static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point7_pwm, fan_curve,
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point8_pwm, fan_curve,
FAN_CURVE_DEV_MID | FAN_CURVE_PWM_MASK, 7);

+/* Firmware enforced minimum duty of each point, read-only */
+static SENSOR_DEVICE_ATTR_2_RO(pwm1_auto_point1_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_CPU, 0);
+static SENSOR_DEVICE_ATTR_2_RO(pwm1_auto_point2_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_CPU, 1);
+static SENSOR_DEVICE_ATTR_2_RO(pwm1_auto_point3_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_CPU, 2);
+static SENSOR_DEVICE_ATTR_2_RO(pwm1_auto_point4_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_CPU, 3);
+static SENSOR_DEVICE_ATTR_2_RO(pwm1_auto_point5_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_CPU, 4);
+static SENSOR_DEVICE_ATTR_2_RO(pwm1_auto_point6_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_CPU, 5);
+static SENSOR_DEVICE_ATTR_2_RO(pwm1_auto_point7_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_CPU, 6);
+static SENSOR_DEVICE_ATTR_2_RO(pwm1_auto_point8_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_CPU, 7);
+
+static SENSOR_DEVICE_ATTR_2_RO(pwm2_auto_point1_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_GPU, 0);
+static SENSOR_DEVICE_ATTR_2_RO(pwm2_auto_point2_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_GPU, 1);
+static SENSOR_DEVICE_ATTR_2_RO(pwm2_auto_point3_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_GPU, 2);
+static SENSOR_DEVICE_ATTR_2_RO(pwm2_auto_point4_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_GPU, 3);
+static SENSOR_DEVICE_ATTR_2_RO(pwm2_auto_point5_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_GPU, 4);
+static SENSOR_DEVICE_ATTR_2_RO(pwm2_auto_point6_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_GPU, 5);
+static SENSOR_DEVICE_ATTR_2_RO(pwm2_auto_point7_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_GPU, 6);
+static SENSOR_DEVICE_ATTR_2_RO(pwm2_auto_point8_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_GPU, 7);
+
+static SENSOR_DEVICE_ATTR_2_RO(pwm3_auto_point1_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_MID, 0);
+static SENSOR_DEVICE_ATTR_2_RO(pwm3_auto_point2_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_MID, 1);
+static SENSOR_DEVICE_ATTR_2_RO(pwm3_auto_point3_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_MID, 2);
+static SENSOR_DEVICE_ATTR_2_RO(pwm3_auto_point4_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_MID, 3);
+static SENSOR_DEVICE_ATTR_2_RO(pwm3_auto_point5_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_MID, 4);
+static SENSOR_DEVICE_ATTR_2_RO(pwm3_auto_point6_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_MID, 5);
+static SENSOR_DEVICE_ATTR_2_RO(pwm3_auto_point7_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_MID, 6);
+static SENSOR_DEVICE_ATTR_2_RO(pwm3_auto_point8_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_MID, 7);
+
static struct attribute *asus_fan_curve_attr[] = {
/* CPU */
&sensor_dev_attr_pwm1_enable.dev_attr.attr,
@@ -4211,6 +4280,37 @@ static struct attribute *asus_fan_curve_attr[] = {
NULL
};

+static struct attribute *asus_fan_curve_floor_attr[] = {
+ /* CPU */
+ &sensor_dev_attr_pwm1_auto_point1_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm1_auto_point2_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm1_auto_point3_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm1_auto_point4_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm1_auto_point5_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm1_auto_point6_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm1_auto_point7_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm1_auto_point8_floor_pwm.dev_attr.attr,
+ /* GPU */
+ &sensor_dev_attr_pwm2_auto_point1_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm2_auto_point2_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm2_auto_point3_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm2_auto_point4_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm2_auto_point5_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm2_auto_point6_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm2_auto_point7_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm2_auto_point8_floor_pwm.dev_attr.attr,
+ /* MID */
+ &sensor_dev_attr_pwm3_auto_point1_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm3_auto_point2_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm3_auto_point3_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm3_auto_point4_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm3_auto_point5_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm3_auto_point6_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm3_auto_point7_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm3_auto_point8_floor_pwm.dev_attr.attr,
+ NULL
+};
+
static umode_t asus_fan_curve_is_visible(struct kobject *kobj,
struct attribute *attr, int idx)
{
@@ -4233,11 +4333,50 @@ static umode_t asus_fan_curve_is_visible(struct kobject *kobj,
return 0;
}

+static bool fan_curve_available(struct asus_wmi *asus, int fan_idx)
+{
+ switch (fan_idx) {
+ case FAN_CURVE_DEV_CPU:
+ return asus->cpu_fan_curve_available;
+ case FAN_CURVE_DEV_GPU:
+ return asus->gpu_fan_curve_available;
+ case FAN_CURVE_DEV_MID:
+ return asus->mid_fan_curve_available;
+ default:
+ return false;
+ }
+}
+
+static umode_t asus_fan_curve_floor_is_visible(struct kobject *kobj,
+ struct attribute *attr, int idx)
+{
+ struct device *dev = kobj_to_dev(kobj);
+ struct asus_wmi *asus = dev_get_drvdata(dev->parent);
+ struct device_attribute *dattr =
+ container_of(attr, struct device_attribute, attr);
+ int fan = to_sensor_dev_attr_2(dattr)->nr;
+
+ if (!fan_curve_available(asus, fan))
+ return 0;
+
+ return 0444;
+}
+
static const struct attribute_group asus_fan_curve_attr_group = {
.is_visible = asus_fan_curve_is_visible,
.attrs = asus_fan_curve_attr,
};
-__ATTRIBUTE_GROUPS(asus_fan_curve_attr);
+
+static const struct attribute_group asus_fan_curve_floor_attr_group = {
+ .is_visible = asus_fan_curve_floor_is_visible,
+ .attrs = asus_fan_curve_floor_attr,
+};
+
+static const struct attribute_group *asus_fan_curve_attr_groups[] = {
+ &asus_fan_curve_attr_group,
+ &asus_fan_curve_floor_attr_group,
+ NULL,
+};

/*
* Must be initialised after throttle_thermal_policy_dev is set as

--
2.56.0