Hi Karthik,
Use tab indentation after the field labels.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.
Is this header required ?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>
#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.
It says "only the first fan control point" but the loop end up writing 2 points.+ + /* + * 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.
+ */
+
+ 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.
+ 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.
The arguments don't align under the opening (.+ 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);
+ }
+ 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.
+ 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)