mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: "Ilpo Järvinen" <ilpo.jarvinen@linux.intel.com>
To: Bartu Alev <bartualev@gmail.com>
Cc: Corentin Chary <corentin.chary@gmail.com>,
	 "Luke D. Jones" <luke@ljones.dev>,
	Denis Benato <denis.benato@linux.dev>,
	 Hans de Goede <hansg@kernel.org>,
	platform-driver-x86@vger.kernel.org,
	 LKML <linux-kernel@vger.kernel.org>,
	linux-hwmon@vger.kernel.org,  Guenter Roeck <linux@roeck-us.net>,
	linux-api@vger.kernel.org
Subject: Re: [PATCH 4/7] platform/x86: asus-wmi: read the firmware fan floor tables
Date: Mon, 9 Nov 2026 20:23:34 +0200 (EET)	[thread overview]
Message-ID: <9097a71a-a7f5-54ec-dd2b-7f55376b2a14@linux.intel.com> (raw)
In-Reply-To: <20261101-asus-fan-floors-v4-v1-4-b8d1c31a99a9@gmail.com>

On Sun, 1 Nov 2026, Bartu Alev wrote:

> On some ASUS laptops, the embedded controller enforces minimum fan
> duties on top of custom fan curves written via CPU_FAN_CURVE
> (0x00110024), GPU_FAN_CURVE (0x00110025), and the mid fan curve
> methods. The floors are exposed via read-only DSTS methods 0x00110026,
> 0x00110027, and 0x00110033.
> 
> The floor table is indexed by curve point: entry i (0-based, sysfs
> point i + 1) provides the minimum duty for that point. The EC maps
> each point to a temperature decade [30 + 10 * i, 39 + 10 * i] degrees
> Celsius, stepping between points with 5 degrees Celsius of hysteresis
> without interpolation on TUF series.
> 
> EC firmware dumps from an FA507 show floor tables of {0, 0, 0, 0, 25,
> 48, 58, 74}% for the CPU fan and {0, 0, 0, 0, 26, 57, 68, 91}% for the
> GPU fan, verified via live DSTS reads on an FA507NV.
> 
> Read the tables at probe and refresh them after power profile changes
> or curve resets, as the EC can reload defaults when operating modes
> change.  Profile switches did not alter table bytes on the FA507, so
> this refresh is precautionary.
> 
> Export the floors without altering user curves, rejecting replies
> shorter than eight bytes or containing values exceeding 100%.  If a
> table contains decreasing values along its points, warn once but still
> export it so userspace can inspect what firmware enforces.  When
> firmware returns 0xFFFFFFFE, mark the table unsupported for that fan
> to avoid repeating failed WMI calls on future refreshes; unreadable
> tables remain zeroed.
> 
> Protect the cached floor tables with a mutex against concurrent sysfs
> reads.
> 
> Also fix the stale comment claiming that firmware interpolates between
> curve points.
> 
> Signed-off-by: Bartu Alev <bartualev@gmail.com>
> ---
>  drivers/platform/x86/asus-wmi.c | 170 ++++++++++++++++++++++++++++++++++++++--
>  1 file changed, 165 insertions(+), 5 deletions(-)
> 
> diff --git a/drivers/platform/x86/asus-wmi.c b/drivers/platform/x86/asus-wmi.c
> index c55f469cb03dc..b6541183d4568 100644
> --- a/drivers/platform/x86/asus-wmi.c
> +++ b/drivers/platform/x86/asus-wmi.c
> @@ -239,6 +239,10 @@ struct fan_curve_data {
>  	u32 device_id;
>  	u8 temps[FAN_CURVE_POINTS];
>  	u8 percents[FAN_CURVE_POINTS];
> +	/* EC minimum duty per point, pwm 0-255, not percent */
> +	u8 floors[FAN_CURVE_POINTS];
> +	bool floors_valid;
> +	bool floors_unsupported;
>  };
>  
>  struct asus_wmi {
> @@ -320,6 +324,9 @@ struct asus_wmi {
>  	bool gpu_fan_curve_available;
>  	bool mid_fan_curve_available;
>  	struct fan_curve_data custom_fan_curves[3];
> +	/* protects custom_fan_curves[].floors */
> +	struct mutex fan_curve_lock;
> +	struct device *fan_curve_hwmon;
>  
>  	struct device *ppdev;
>  
> @@ -466,7 +473,8 @@ static int asus_wmi_evaluate_method5(u32 method_id,
>   * means that the method called is unsupported.
>   */
>  static int asus_wmi_evaluate_method_buf(u32 method_id,
> -		u32 arg0, u32 arg1, u8 *ret_buffer, size_t size)
> +		u32 arg0, u32 arg1, u8 *ret_buffer, size_t size,
> +		size_t *ret_len)
>  {
>  	struct bios_args args = {
>  		.arg0 = arg0,
> @@ -479,6 +487,9 @@ static int asus_wmi_evaluate_method_buf(u32 method_id,
>  	union acpi_object *obj;
>  	int err = 0;
>  
> +	if (ret_len)
> +		*ret_len = 0;
> +
>  	status = asus_wmi_evaluate_method_locked(method_id, &input, &output);
>  
>  	pr_debug("%s called (0x%08x) with args: 0x%08x, 0x%08x\n",
> @@ -503,6 +514,8 @@ static int asus_wmi_evaluate_method_buf(u32 method_id,
>  		}
>  
>  		memcpy(ret_buffer, obj->buffer.pointer, obj->buffer.length);
> +		if (ret_len)
> +			*ret_len = obj->buffer.length;
>  		break;
>  	case ACPI_TYPE_INTEGER:
>  		err = (u32)obj->integer.value;
> @@ -3631,7 +3644,7 @@ static int fan_curve_get_factory_default(struct asus_wmi *asus, u32 fan_dev)
>  		mode = 2;
>  
>  	err = asus_wmi_evaluate_method_buf(asus->dsts_id, fan_dev, mode, buf,
> -					   FAN_CURVE_BUF_LEN);
> +					   FAN_CURVE_BUF_LEN, NULL);
>  	if (err) {
>  		pr_warn("%s (0x%08x) failed: %d\n", __func__, fan_dev, err);
>  		return err;
> @@ -3651,7 +3664,136 @@ static int fan_curve_get_factory_default(struct asus_wmi *asus, u32 fan_dev)
>  	return 0;
>  }
>  
> -/* Check if capability exists, and populate defaults */
> +/*
> + * Read the firmware fan floor table. The EC enforces a minimum duty per
> + * curve point on top of the custom curve. Reading is best effort: at
> + * probe time a failed read leaves the table zeroed and invalid, and a
> + * failed refresh keeps the last table that was read.
> + */
> +static int fan_curve_load_floors(struct asus_wmi *asus, u32 fan_dev)
> +{
> +	struct fan_curve_data *curves;
> +	u8 buf[FAN_CURVE_BUF_LEN] = {};
> +	size_t len;
> +	int err, fan_idx, i;

Reverse xmas-tree order.

I seems to only use positive values(?) => unsigned int 

> +	switch (fan_dev) {
> +	case ASUS_WMI_DEVID_CPU_FAN_CURVE:
> +		fan_idx = FAN_CURVE_DEV_CPU;
> +		fan_dev = ASUS_WMI_DEVID_CPU_FAN_FLOOR;
> +		break;
> +	case ASUS_WMI_DEVID_GPU_FAN_CURVE:
> +		fan_idx = FAN_CURVE_DEV_GPU;
> +		fan_dev = ASUS_WMI_DEVID_GPU_FAN_FLOOR;
> +		break;
> +	case ASUS_WMI_DEVID_MID_FAN_CURVE:
> +		fan_idx = FAN_CURVE_DEV_MID;
> +		fan_dev = ASUS_WMI_DEVID_MID_FAN_FLOOR;
> +		break;
> +	default:
> +		return -EOPNOTSUPP;
> +	}
> +
> +	curves = &asus->custom_fan_curves[fan_idx];
> +	if (curves->floors_unsupported)
> +		return -ENODEV;
> +
> +	err = asus_wmi_evaluate_method_buf(asus->dsts_id, fan_dev, 0, buf,
> +					   FAN_CURVE_BUF_LEN, &len);
> +	if (err) {
> +		pr_debug("%s (0x%08x) failed: %d\n", __func__, fan_dev, err);

Below you use dev_*() printing, so why pr_*() here??

This also seems to lack include for dev_*() so lets finally add that one.

> +		/* 0xFFFFFFFE means the firmware has no such method */

Drop comment.

> +		if (err == -ENODEV)
> +			curves->floors_unsupported = true;
> +		return err;
> +	}
> +
> +	if (len < FAN_CURVE_POINTS) {
> +		pr_debug("%s (0x%08x): short reply of %zu bytes\n",
> +			 __func__, fan_dev, len);

dev_...()

> +		return -ENODATA;
> +	}
> +
> +	/* A percentage above 100 would overflow the pwm scale below */

Drop.

> +	for (i = 0; i < FAN_CURVE_POINTS; i++) {
> +		if (buf[i] > 100) {
> +			pr_debug("%s (0x%08x): implausible floor table\n",
> +				 __func__, fan_dev);

dev_xx().

Why is this only debug level? It would seem like something that has 
relevance beyond that.

When making it err/warn level, improve/simplify the message too to 
simply state that floor table entry > 100 instead of something cryptic 
like the above :-). And never print __func__ for messages intended for 
normal user.

> +			return -EINVAL;
> +		}
> +	}
> +
> +	/*
> +	 * The floors observed so far do not decrease along the points, but
> +	 * that is only proven on the FA507.  Do not reject a table that
> +	 * does: the values are still usable as per-point minimums, and
> +	 * hiding them would leave userspace no way to see what the
> +	 * firmware enforces.
> +	 */
> +	for (i = 1; i < FAN_CURVE_POINTS; i++) {
> +		if (buf[i] < buf[i - 1]) {

Reverse the compare sides to make it more logical.

> +			dev_warn_once(&asus->platform_device->dev,
> +				      "fan curve (0x%08x): floor table decreases at point %d\n",
> +				      fan_dev, i + 1);
> +			break;
> +		}
> +	}
> +
> +	guard(mutex)(&asus->fan_curve_lock);
> +	for (i = 0; i < FAN_CURVE_POINTS; i++)
> +		curves->floors[i] = DIV_ROUND_UP(255 * buf[i], 100);

Add include.

> +	curves->floors_valid = true;
> +
> +	return 0;
> +}
> +
> +static void fan_curve_notify_floors(struct asus_wmi *asus)
> +{
> +	struct device *hwmon = asus->fan_curve_hwmon;
> +	bool valid[ARRAY_SIZE(asus->custom_fan_curves)];
> +	char name[32];
> +	int fan, point;
> +
> +	if (!hwmon)
> +		return;
> +
> +	scoped_guard(mutex, &asus->fan_curve_lock) {
> +		for (fan = 0; fan < ARRAY_SIZE(valid); fan++)

Add include.

> +			valid[fan] = asus->custom_fan_curves[fan].floors_valid;
> +	}
> +
> +	for (fan = 0; fan < ARRAY_SIZE(valid); fan++) {
> +		if (!valid[fan])
> +			continue;
> +
> +		for (point = 0; point < FAN_CURVE_POINTS; point++) {
> +			snprintf(name, sizeof(name),

scnprintf()

> +				 "pwm%d_auto_point%d_floor_pwm",
> +				 fan + 1, point + 1);
> +			sysfs_notify(&hwmon->kobj, NULL, name);

Add include.

> +		}
> +	}
> +}
> +
> +/*
> + * The EC re-runs its defaults reload when the mode byte it keys on
> + * changes, which a thermal profile switch can do.  Re-read the tables
> + * so that the exported values follow the firmware; on the FA507 the
> + * tables are profile independent, so this is defensive.
> + */
> +static void fan_curve_refresh_floors(struct asus_wmi *asus)
> +{
> +	if (asus->cpu_fan_curve_available)
> +		fan_curve_load_floors(asus, ASUS_WMI_DEVID_CPU_FAN_CURVE);
> +	if (asus->gpu_fan_curve_available)
> +		fan_curve_load_floors(asus, ASUS_WMI_DEVID_GPU_FAN_CURVE);
> +	if (asus->mid_fan_curve_available)
> +		fan_curve_load_floors(asus, ASUS_WMI_DEVID_MID_FAN_CURVE);
> +
> +	fan_curve_notify_floors(asus);
> +}
> +
> +/* Check if the capability exists, populate its defaults and read its floor table */
>  static int fan_curve_check_present(struct asus_wmi *asus, bool *available,
>  				   u32 fan_dev)
>  {
> @@ -3668,6 +3810,16 @@ static int fan_curve_check_present(struct asus_wmi *asus, bool *available,
>  	}
>  
>  	*available = true;
> +
> +	err = fan_curve_load_floors(asus, fan_dev);
> +	if (err == -ENODEV)
> +		dev_dbg(&asus->platform_device->dev,
> +			"fan curve (0x%08x): no floor table\n", fan_dev);
> +	else if (err)
> +		dev_warn(&asus->platform_device->dev,
> +			 "fan curve (0x%08x): unusable floor table (%d)\n",
> +			 fan_dev, err);
> +
>  	return 0;
>  }
>  
> @@ -3738,8 +3890,9 @@ static int fan_curve_write(struct asus_wmi *asus,
>  }
>  
>  /*
> - * A fan curve is a set of points the firmware interpolates between, so it
> - * only makes sense if neither temperature nor PWM ever decreases along it.
> + * A fan curve is a set of points the firmware steps through, without
> + * interpolating between them, so it only makes sense if neither temperature
> + * nor PWM ever decreases along it.
>   */
>  static int fan_curve_validate(struct device *dev, struct fan_curve_data *data)
>  {
> @@ -3870,6 +4023,7 @@ static ssize_t fan_curve_enable_store(struct device *dev,
>  			err = asus_fan_set_auto(asus);
>  			if (err)
>  				return err;
> +			fan_curve_refresh_floors(asus);
>  		} else {
>  			/* Safeguard against fautly ACPI tables */
>  			err = fan_curve_get_factory_default(asus, data->device_id);
> @@ -3878,6 +4032,7 @@ static ssize_t fan_curve_enable_store(struct device *dev,
>  			err = fan_curve_write(asus, data);
>  			if (err)
>  				return err;
> +			fan_curve_refresh_floors(asus);
>  		}
>  	}
>  	return count;
> @@ -4125,6 +4280,8 @@ static int asus_wmi_custom_fan_curve_init(struct asus_wmi *asus)
>  		return PTR_ERR(hwmon);
>  	}
>  
> +	asus->fan_curve_hwmon = hwmon;
> +
>  	return 0;
>  }
>  
> @@ -4195,6 +4352,8 @@ static int throttle_thermal_policy_write(struct asus_wmi *asus)
>  	if (asus->mid_fan_curve_available)
>  		asus->custom_fan_curves[FAN_CURVE_DEV_MID].enabled = false;
>  
> +	fan_curve_refresh_floors(asus);
> +
>  	return 0;
>  }
>  
> @@ -5202,6 +5361,7 @@ static int asus_wmi_add(struct platform_device *pdev)
>  	asus->platform_device = pdev;
>  	wdrv->platform_device = pdev;
>  	platform_set_drvdata(asus->platform_device, asus);
> +	mutex_init(&asus->fan_curve_lock);
>  
>  	if (wdrv->detect_quirks)
>  		wdrv->detect_quirks(asus->driver);
> 
> 

-- 
 i.


  reply	other threads:[~2026-11-09 18:23 UTC|newest]

Thread overview: 12+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-01 10:46 [PATCH 0/7] platform/x86: asus-wmi: expose " Bartu Alev
2026-11-01 10:46 ` [PATCH 1/7] platform/x86: asus-wmi: dump buffer replies in the debugfs dsts file Bartu Alev
2026-11-09 18:09   ` Ilpo Järvinen
2026-11-01 10:46 ` [PATCH 2/7] Documentation: ABI: document the ASUS custom fan curve attributes Bartu Alev
2026-11-01 10:46 ` [PATCH 3/7] platform/x86: asus-wmi: define the fan floor table DEVIDs Bartu Alev
2026-11-01 10:46 ` [PATCH 4/7] platform/x86: asus-wmi: read the firmware fan floor tables Bartu Alev
2026-11-09 18:23   ` Ilpo Järvinen [this message]
2026-11-01 10:46 ` [PATCH 5/7] platform/x86: asus-wmi: reject short fan curve replies Bartu Alev
2026-11-01 10:46 ` [PATCH 6/7] platform/x86: asus-wmi: expose the fan floor tables via hwmon Bartu Alev
2026-11-09 18:34   ` Ilpo Järvinen
2026-11-01 10:46 ` [PATCH 7/7] platform/x86: asus-wmi: show the effective duty for enabled curves Bartu Alev
2026-11-09 18:39   ` Ilpo Järvinen

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=9097a71a-a7f5-54ec-dd2b-7f55376b2a14@linux.intel.com \
    --to=ilpo.jarvinen@linux.intel.com \
    --cc=bartualev@gmail.com \
    --cc=corentin.chary@gmail.com \
    --cc=denis.benato@linux.dev \
    --cc=hansg@kernel.org \
    --cc=linux-api@vger.kernel.org \
    --cc=linux-hwmon@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux@roeck-us.net \
    --cc=luke@ljones.dev \
    --cc=platform-driver-x86@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®