Hi Karthik,

On 16-10-2026 00:37, Karthik Poosa wrote:
Expose pwm[1-3] fan duty attributes through hwmon.

This enables reading and writing user PWM values (0..255) for each
available fan channel.

Update Xe hwmon ABI documentation for pwm[1-3].

This sysfs node can be used for manual control of fan speed,
irrespective of fan curve.

v2:
 - Use xe helpers for dmesg logs.
 - Avoid using hwmon->num_fans to get fan count in
   xe_hwmon_pwm_is_visible().

v3:
 - Set PWM directly through the user table; avoid initializing it from the
   stock table. (Soham)
 - Move the unused xe_hwmon_activate_user_fan_table() to the subsequent
   patch.
 - Fail pwmN reads when no value has been written, rather than activating
   the user table from the stock table.
 - Define MIN_FAN_CONTROL_POINTS as 2.
 - Use MIN_FAN_CONTROL_POINTS for PWM configuration. (Soham)
 - Use batched writes for configuring fan control points when setting pwmN.
 - Add and rephrase couple debug logs.
 - Update the hwmon documentation.

Signed-off-by: Karthik Poosa <karthik.poosa@intel.com>
Assisted-by: LLM
---
 .../ABI/testing/sysfs-driver-intel-xe-hwmon   |   9 +
 drivers/gpu/drm/xe/xe_hwmon.c                 | 170 ++++++++++++++++++
 drivers/gpu/drm/xe/xe_pcode_api.h             |   3 +
 3 files changed, 182 insertions(+)

diff --git a/Documentation/ABI/testing/sysfs-driver-intel-xe-hwmon b/Documentation/ABI/testing/sysfs-driver-intel-xe-hwmon
index 5ee68aefa485..ce17e9ca3ed8 100644
--- a/Documentation/ABI/testing/sysfs-driver-intel-xe-hwmon
+++ b/Documentation/ABI/testing/sysfs-driver-intel-xe-hwmon
@@ -321,3 +321,12 @@ Description:	RW. Package burst power limit interval (Tau in PL2/Tau) in
 		milliseconds over which sustained power is averaged.
 
 		Only supported for particular Intel Xe graphics platforms.
+
+What:           /sys/bus/pci/drivers/xe/.../hwmon/hwmon<i>/pwm[1-3]
+Date:           October 2026
+KernelVersion:  7.3
+Contact:        intel-xe@lists.freedesktop.org
+Description:    RW. Target fan PWM duty value in the range 0..255. 0 is zero
+                fan speed and 255 is 100% fan speed.
+
+                Only supported for particular Intel Xe graphics platforms.
Use tab indentation after the field labels.
diff --git a/drivers/gpu/drm/xe/xe_hwmon.c b/drivers/gpu/drm/xe/xe_hwmon.c
index 32016c5d4770..a86ed741feb3 100644
--- a/drivers/gpu/drm/xe/xe_hwmon.c
+++ b/drivers/gpu/drm/xe/xe_hwmon.c
@@ -8,6 +8,7 @@
 #include <linux/jiffies.h>
 #include <linux/types.h>
 #include <linux/units.h>
+#include <linux/delay.h>
Is this header required ?
 
 #include <drm/drm_managed.h>
 #include "regs/xe_gt_regs.h"
@@ -153,6 +154,9 @@ enum fan_table_type {
 /* Maximum number of fan control points supported by each fan. */
 #define MAX_FAN_CONTROL_POINTS	(10)
 
+/* Minimum number of points required for a user fan table. */
+#define MIN_FAN_CONTROL_POINTS  (2)
Use tab indentation.
+
 /* Fan control point index: bit31 selects fan table type; bits30:0 control point index. */
 #define FCP_INDEX(FAN_TABLE_TYPE, POINT_NUM) (((FAN_TABLE_TYPE) << 31) | \
 						((POINT_NUM) & REG_GENMASK(30, 0)))
@@ -913,6 +917,7 @@ static const struct hwmon_channel_info * const hwmon_info[] = {
 	HWMON_CHANNEL_INFO(energy, HWMON_E_INPUT | HWMON_E_LABEL, HWMON_E_INPUT | HWMON_E_LABEL),
 	HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT | HWMON_F_MAX, HWMON_F_INPUT | HWMON_F_MAX,
 			   HWMON_F_INPUT | HWMON_F_MAX),
+	HWMON_CHANNEL_INFO(pwm, HWMON_PWM_INPUT, HWMON_PWM_INPUT, HWMON_PWM_INPUT),
 	NULL
 };
 
@@ -1154,6 +1159,89 @@ static int xe_hwmon_get_fan_enable_mask(struct xe_hwmon *hwmon)
 	return 0;
 }
 
+static int xe_hwmon_get_fan_point_count(struct xe_hwmon *hwmon, u8 fan, u32 *point_count,
+					enum fan_table_type table_type)
+{
+	int ret;
+
+	ret = xe_hwmon_pcode_read_fan_control(hwmon,
+					      (table_type == USER_FAN_TABLE) ?
+					      FSC_READ_USER_FAN_CONTROL_POINTS :
+					      FSC_READ_STOCK_FAN_CONTROL_POINTS,
+					      fan, point_count);
+	if (ret) {
+		xe_err(hwmon->xe, "failed to read fan %d %s point count, ret=%d\n", fan,
+		       (table_type == USER_FAN_TABLE) ? "user" : "stock", ret);
+		return ret;
+	}
+
+	if (*point_count > MAX_FAN_CONTROL_POINTS) {
+		xe_err(hwmon->xe, "fan %d %s point count %u exceeds max %u\n", fan,
+		       (table_type == USER_FAN_TABLE) ? "user" : "stock", *point_count,
+		       MAX_FAN_CONTROL_POINTS);
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+/* Temperature to set when setting the fan PWM with pwmN. */
+#define FIXED_PWM_TEMP	(1)
+
+static int xe_hwmon_set_user_fan_pwm(struct xe_hwmon *hwmon, u8 fan, u8 pwm)
+{
+	struct xe_hwmon_fan_info *fi = &hwmon->fi[fan];
+	struct xe_pcode_write writes[MIN_FAN_CONTROL_POINTS + 1];
+	u32 point_count = 0;
+	u8 clamped_pwm;
+	int ret, i;
+
+	pwm = DIV_ROUND_CLOSEST(pwm * 100, U8_MAX);
+	clamped_pwm = max_t(u8, pwm, min_t(u32, fi->min_pwm, U8_MAX));
Is clamped_pwm and  fi->min_pwm in percentage ? If so use 100  instead of U8_MAX to be the max value.
+
+	/*
+	 * Set the first fan control point in the user table to the clamped PWM value.
+	 * Use only the first fan control point for setting the PWM value.
It says "only the first fan control point" but the loop end up writing 2 points.
+	 */
+
+	for (i = 0; i < MIN_FAN_CONTROL_POINTS; i++) {
+		writes[i].mbox = PCODE_MBOX(FAN_SPEED_CONTROL, FSC_WRITE_FAN_TABLE, fan);
+		writes[i].data = FIELD_PREP(FAN_CONTROL_POINT_TEMP_MASK, FIXED_PWM_TEMP + (i)) |
+					    FIELD_PREP(FAN_CONTROL_POINT_SPEED_MASK, clamped_pwm);
+		xe_dbg(hwmon->xe, "prepared fan control point %d with temp=%d, speed=%u\n", i,
+		       FIXED_PWM_TEMP + (i), clamped_pwm);
+	}
+
+	writes[i].mbox = PCODE_MBOX(FAN_SPEED_CONTROL, FSC_WRITE_NUM_FAN_CONTROL_POINTS, fan);
+	writes[i].data = MIN_FAN_CONTROL_POINTS;
+	ret = xe_pcode_write_batch(xe_device_get_root_tile(hwmon->xe), writes, ARRAY_SIZE(writes));
+	if (ret) {
+		xe_err(hwmon->xe, "failed to write pwm %d for fan %d, ret=%d\n", pwm, fan, ret);
"pwm" has been reassigned to a percentage by this point, so either print the original 0–255 value (keep it in a separate variable) or label this one as a percentage.
+		return ret;
+	}
+	xe_dbg(hwmon->xe, "applied fixed pwm %u for fan %d\n", clamped_pwm, fan);
+
+	/* Verify if the fan points are set */
+	ret = xe_hwmon_get_fan_point_count(hwmon, fan, &point_count, USER_FAN_TABLE);
+	if (ret && point_count == 0) {
On failure point_count is already 0, so && point_count == 0 seems to be redundant.
Instead could this be more usable :
if (ret || point_count != MIN_FAN_CONTROL_POINTS),  if the valid write needed to be checked ?

+		xe_err(hwmon->xe, "pwm set verify failed for fan %d, ret=%d, point count=%u\n", fan,
+		       ret, point_count);
+		return ret;
+	}
+
+	/* Update the user fan table cache. */
+	for (i = 0; i < MIN_FAN_CONTROL_POINTS; i++) {
+		fi->fan_table[USER_FAN_TABLE].fcp[i].temp = FIXED_PWM_TEMP + (i);
Drop the parentheses around i: FIXED_PWM_TEMP + i.
+		fi->fan_table[USER_FAN_TABLE].fcp[i].speed = clamped_pwm;
+		xe_dbg(hwmon->xe, "fan %d user table cache point %d with temp=%d, speed=%u\n", fan,
+		       i, fi->fan_table[USER_FAN_TABLE].fcp[i].temp,
+			  fi->fan_table[USER_FAN_TABLE].fcp[i].speed);
The arguments don't align under the opening (.
+	}
+	fi->fan_table[USER_FAN_TABLE].point_count = MIN_FAN_CONTROL_POINTS;
+
+	return 0;
+}
+
 static int xe_hwmon_read_fan_control_info(struct xe_hwmon *hwmon)
 {
 	struct xe_tile *root_tile = xe_device_get_root_tile(hwmon->xe);
@@ -1774,6 +1862,20 @@ xe_hwmon_fan_is_visible(struct xe_hwmon *hwmon, u32 attr, int channel)
 	}
 }
 
+static umode_t
+xe_hwmon_pwm_is_visible(struct xe_hwmon *hwmon, u32 attr, int channel)
+{
+	if (!hwmon->xe->info.has_fan_control || !IS_FAN_ENABLED(channel))
+		return 0;
+
+	switch (attr) {
+	case hwmon_pwm_input:
+		return 0644;
+	default:
+		return 0;
+	}
+}
+
 static int
 xe_hwmon_fan_input_read(struct xe_hwmon *hwmon, int channel, long *val)
 {
@@ -1828,6 +1930,67 @@ xe_hwmon_fan_read(struct xe_hwmon *hwmon, u32 attr, int channel, long *val)
 	}
 }
 
+static int
+xe_hwmon_pwm_read(struct xe_hwmon *hwmon, u32 attr, int channel, long *val)
+{
+	struct xe_hwmon_fan_info *fi;
+	int ret = 0;
+
+	if (channel < 0 || channel >= FAN_MAX)
Add a short xe_dbg/xe_warn before these -EINVAL returns for clarity.
+		return -EINVAL;
+
+	mutex_lock(&hwmon->hwmon_lock);
+
+	fi = &hwmon->fi[channel];
+
+	switch (attr) {
+	case hwmon_pwm_input:
+		/* Check if user fan table is set else activate it.*/
Where does it activate ? Use space before */.
+		if (!fi->fan_table[USER_FAN_TABLE].point_count) {
+			xe_warn(hwmon->xe, "pwm not set for fan %d\n", channel);
+			ret = -ENXIO;
+			goto unlock;
+		}
+		*val = DIV_ROUND_CLOSEST(fi->fan_table[USER_FAN_TABLE].fcp[0].speed * U8_MAX, 100);
The 0–255 <-> percentage round-trip is lossy for small values; Better to add a one-line note since the ABI says a full 0–255 range.
+		xe_dbg(hwmon->xe, "pwm read for fan %d: %ld\n", channel, *val);
+		break;
+	default:
+		ret = -EOPNOTSUPP;
+		break;
+	}
+
+unlock:
+	mutex_unlock(&hwmon->hwmon_lock);
+	return ret;
+}
+
+static int
+xe_hwmon_pwm_write(struct xe_hwmon *hwmon, u32 attr, int channel, long val)
+{
+	int ret = 0;
+
+	if (channel < 0 || channel >= FAN_MAX)

Add a short xe_dbg/xe_warn before these -EINVAL returns for clarity.


Regards,
Soham
+		return -EINVAL;
+
+	mutex_lock(&hwmon->hwmon_lock);
+
+	switch (attr) {
+	case hwmon_pwm_input:
+		if (val < 0 || val > U8_MAX) {
+			ret = -EINVAL;
+			break;
+		}
+		ret = xe_hwmon_set_user_fan_pwm(hwmon, channel, (u8)val);
+		break;
+	default:
+		ret = -EOPNOTSUPP;
+		break;
+	}
+	mutex_unlock(&hwmon->hwmon_lock);
+
+	return ret;
+}
+
 static umode_t
 xe_hwmon_is_visible(const void *drvdata, enum hwmon_sensor_types type,
 		    u32 attr, int channel)
@@ -1854,6 +2017,9 @@ xe_hwmon_is_visible(const void *drvdata, enum hwmon_sensor_types type,
 	case hwmon_fan:
 		ret = xe_hwmon_fan_is_visible(hwmon, attr, channel);
 		break;
+	case hwmon_pwm:
+		ret = xe_hwmon_pwm_is_visible(hwmon, attr, channel);
+		break;
 	default:
 		ret = 0;
 		break;
@@ -1883,6 +2049,8 @@ xe_hwmon_read(struct device *dev, enum hwmon_sensor_types type, u32 attr,
 		return xe_hwmon_energy_read(hwmon, attr, channel, val);
 	case hwmon_fan:
 		return xe_hwmon_fan_read(hwmon, attr, channel, val);
+	case hwmon_pwm:
+		return xe_hwmon_pwm_read(hwmon, attr, channel, val);
 	default:
 		return -EOPNOTSUPP;
 	}
@@ -1901,6 +2069,8 @@ xe_hwmon_write(struct device *dev, enum hwmon_sensor_types type, u32 attr,
 		return xe_hwmon_power_write(hwmon, attr, channel, val);
 	case hwmon_curr:
 		return xe_hwmon_curr_write(hwmon, attr, channel, val);
+	case hwmon_pwm:
+		return xe_hwmon_pwm_write(hwmon, attr, channel, val);
 	default:
 		return -EOPNOTSUPP;
 	}
diff --git a/drivers/gpu/drm/xe/xe_pcode_api.h b/drivers/gpu/drm/xe/xe_pcode_api.h
index 32bac74c4281..80642bee0ac8 100644
--- a/drivers/gpu/drm/xe/xe_pcode_api.h
+++ b/drivers/gpu/drm/xe/xe_pcode_api.h
@@ -82,9 +82,12 @@
 #define     PCODE_MBOX_DOMAIN_HBM		0x2
 
 #define   FAN_SPEED_CONTROL			0x7D
+#define     FSC_WRITE_NUM_FAN_CONTROL_POINTS	0x0
+#define     FSC_WRITE_FAN_TABLE			0x1
 #define     FSC_READ_MAX_FAN_RPS        0x3
 #define     FSC_READ_NUM_FANS			0x4
 #define     FSC_READ_STOCK_FAN_CONTROL_POINTS	0x5
+#define     FSC_READ_USER_FAN_CONTROL_POINTS	0x6
 #define     FSC_READ_FAN_TABLE			0x7
 #define     FAN_CONTROL_POINT_TEMP_MASK		REG_GENMASK(7, 0)
 #define     FAN_CONTROL_POINT_SPEED_MASK	REG_GENMASK(15, 8)