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.
next prev parent reply other threads:[~2026-11-09 18:23 UTC|newest]
Thread overview: 19+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-01 10:46 [PATCH 0/7] platform/x86: asus-wmi: expose the firmware fan floor tables 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-01 10:51 ` sashiko-bot
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:50 ` sashiko-bot
2026-11-01 10:46 ` [PATCH 3/7] platform/x86: asus-wmi: define the fan floor table DEVIDs Bartu Alev
2026-11-01 10:49 ` sashiko-bot
2026-11-01 10:46 ` [PATCH 4/7] platform/x86: asus-wmi: read the firmware fan floor tables Bartu Alev
2026-11-01 11:02 ` sashiko-bot
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:55 ` sashiko-bot
2026-11-01 10:46 ` [PATCH 6/7] platform/x86: asus-wmi: expose the fan floor tables via hwmon Bartu Alev
2026-11-01 10:52 ` sashiko-bot
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-01 11:00 ` sashiko-bot
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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.