* [RFC v4 0/4] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls
@ 2026-03-09 20:51 Antheas Kapenekakis
2026-03-09 20:51 ` [RFC v4 1/4] Documentation: firmware-attributes: generalize save_settings entry Antheas Kapenekakis
` (4 more replies)
0 siblings, 5 replies; 21+ messages in thread
From: Antheas Kapenekakis @ 2026-03-09 20:51 UTC (permalink / raw)
To: Mario.Limonciello
Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato,
i, linux-kernel, platform-driver-x86, Antheas Kapenekakis
Many AMD-based handheld PCs (GPD, AYANEO, OneXPlayer, AOKZOE, OrangePi)
ship with the AGESA ALIB method at \_SB.ALIB, which accepts Function 0x0C
(the Dynamic Power and Thermal Configuration Interface, DPTCi). This
allows software to adjust APU power and thermal parameters at runtime:
sustained power limit (SPL/STAPM + skin), slow PPT, fast PPT, and the
thermal control target.
Unlike mainstream AMD laptops, these devices do not implement vendor-
specific WMI or EC hooks for TDP control. The ones that do, use DPTCi
under the hood. For these devices, ALIB is the only viable mechanism for
the OS to adjust power limits, making a dedicated kernel driver the
correct approach rather than relying on the out-of-tree acpi_call module
or ryzenadj.
The driver provides two layers of control:
* Platform profile integration (low-power / balanced / performance /
custom), with per-device preset tunings derived from thermal envelope
data. Selecting a non-custom profile applies values immediately and
locks the individual tunables to read-only. The default profile is
"custom", leaving the device at firmware defaults until userspace
explicitly selects a profile.
* Four firmware-attributes tunables (ppt_pl1_spl, ppt_pl2_sppt,
ppt_pl3_fppt, cpu_temp) that become writable in "custom" mode.
Values are enforced against per-device limits (smin..smax), with an
"expanded_limits" toggle that widens the range to the full hardware-
validated envelope (min..max). A save_settings attribute controls
whether writes commit immediately ("single") or are staged for an
explicit bulk "save".
On resume, the active profile or staged values are re-applied so that
suspend/resume cycles do not silently revert to firmware defaults unless
in custom and in bulk mode. In that case, defer to userspace.
Device limits are supplied for GPD Win Mini / Win 4 / Win 5 / Win Max 2 /
Duo / Pocket 4, OrangePi NEO-01, AOKZOE A1/A2, OneXPlayer F1/2/X1/G1,
and numerous AYANEO models. The SoC table covers Ryzen 5000, 6000, 7040,
8000, AI 9 HX 370, and the Ryzen AI MAX series.
Tested on a GPD Win 5 (Ryzen AI MAX+ 395). Confirmed with ryzenadj -i
that committed values are applied to hardware, and that fast/slow PPT
limits are honoured under a full-CPU stress load.
Responsible disclosure: The development of this driver is AI assisted.
This includes its writing, testing, reviewing, and readmes. The driver was
manually reviewed line by line, but there may still be small leftover
quirks. This is an RFC, not a final version. Let's push these tools to
their limits and see where it takes us.
Assisted-by: Claude:claude-opus-4-6
---
Changes in v4:
- Align dptc_params continuation lines to opening brace
- Reflow all dptc_device_limits structs: expand zipped braces into
separate .params and .profiles blocks with proper indentation
- Extract enum dptc_save_mode from struct dptc_priv to a standalone
declaration
- Replace vague mutex comment with explicit list of protected members
- Use &buf[off + 1] form for put_unaligned_le32
- Fix misaligned '=' in ACPI in_params block
- Factor out dptc_alib_fill_param() helper to deduplicate ALIB buffer
construction in dptc_alib_send_one() and dptc_alib_save(); use int
instead of size_t for element counts throughout
- Consolidate dptc_current_value_store: single guard(mutex) instead of
two, eliminating duplicated profile check
- Return -EBUSY instead of -EPERM when profile is not custom
- Add blank line after early returns for readability
- Consolidate dptc_alib_save() call into dptc_apply_profile(), which
now returns int; simplify dptc_pp_set() and dptc_resume() callers
- Squash device_create formatting fix into the patch that introduced it
- Remove bogus "AMD Ryzen AI HX 360" SoC entry (no such model exists)
- Return -ENOENT instead of -EINVAL from dptc_alib_call() when no
parameters are staged, matching think-lmi save_settings semantics
- Remove bool has_staged[] array: use staged[i] == 0 as the "not
staged" sentinel, raise expanded_min from 0 to 1 W to ensure 0 is
never a valid user value
- Remove unnecessary braces from if/else if/else chain in dptc_resume()
- Drop Ryzen Z1 SoC entries: Z1 devices (Lenovo/Asus) use vendor EC/PMF
drivers, not ALIB DPTCi
Changes in v3:
- Split single driver patch into 3: core driver, platform profile,
device entries
- Rename DRIVER_NAME from "amd_dptc" to "amd-dptc" (match subsystem
convention for platform drivers and firmware-attributes devices)
- Add scale field to dptc_param_desc: sysfs values in user units (W, C),
driver multiplies by scale (1000 for mW) before sending to ALIB
- Rename struct fields: min/smin/smax/max ->
expanded_min/device_min/device_max/expanded_max
- Remove comment "ALIB parameter IDs (AGESA spec Appendix E.5, Table
E-52)"
- Move ALIB method check after SoC/DMI validation, change pr_debug to
pr_warn
- Reorder local variable declaration in dptc_init (dptc after other vars)
- Add commit subject prefix "dptc:" to all driver patches
- Remove early-return for empty save in dptc_alib_save; let
dptc_alib_call handle the empty case
- Remove max_power as we do not do DC/AC validation in the driver
- Fix Ayaneo AIR device matches to reflect their wattage, add 15W profile
for original AIR. Cheers to the AIR Plus user who had helped tune the
AIR Plus profile so it was correct.
Changes in v2:
- Use a platform_device base instead of raw inits + exit, hook into devm
helpers referencing samsung-galaxybook
- Add platform_profile support (low-power / balanced / performance /
custom) with per-device energy presets; non-custom profiles lock
tunables to read-only. We default to custom to avoid writing values
- Reduce exposed parameters from seven to four (ppt_pl1_spl,
ppt_pl2_sppt, ppt_pl3_fppt, cpu_temp); drop time constants and
separate skin/STAPM limits in favour of a single SPL that sets both.
For devices where the max tdp offers thermals that are not suitable
for day to day use (e.g., excessive fan noise), MAX_POWER is added
- Remove CONFIG_AMD_DPTC_EXTENDED and the "soc"/"unbound" limit tiers;
keep only device (smin..smax) and expanded (min..max) and soc match
(certain devices ship multiple SoCs on the same motherboard/thermal
envelope, make sure we only hook into validated SoCs)
- Rename "commit" attribute to "save_settings" per firmware-attributes
ABI; rename limit_mode to expanded_limits
- Change expanded_limits attribute type from "enumeration" to "integer"
(min=0, max=1) since firmware-attributes has no bool type
- Remove all global vars, limit _dev access to init and exit
- Use u32 accessors to set values for ALIB call
- Clean up verbose comments throughout
- Add Ayn Loki Max / Tectoy Zeenix Pro
V3: https://lore.kernel.org/all/20260307115516.26892-1-lkml@antheas.dev/
V2: https://lore.kernel.org/all/20260305181751.3642846-1-lkml@antheas.dev/
V1: https://lore.kernel.org/all/20260303181707.2920261-1-lkml@antheas.dev/
Antheas Kapenekakis (4):
Documentation: firmware-attributes: generalize save_settings entry
platform/x86/amd: dptc: Add AMD DPTCi driver
platform/x86/amd: dptc: Add platform profile support
platform/x86/amd: dptc: Add device entries for handheld PCs
.../testing/sysfs-class-firmware-attributes | 41 +-
MAINTAINERS | 6 +
drivers/platform/x86/amd/Kconfig | 15 +
drivers/platform/x86/amd/Makefile | 2 +
drivers/platform/x86/amd/dptc.c | 1271 +++++++++++++++++
5 files changed, 1320 insertions(+), 15 deletions(-)
create mode 100644 drivers/platform/x86/amd/dptc.c
base-commit: 4ae12d8bd9a830799db335ee661d6cbc6597f838
--
2.52.0
^ permalink raw reply [flat|nested] 21+ messages in thread* [RFC v4 1/4] Documentation: firmware-attributes: generalize save_settings entry 2026-03-09 20:51 [RFC v4 0/4] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Antheas Kapenekakis @ 2026-03-09 20:51 ` Antheas Kapenekakis 2026-03-09 20:51 ` [RFC v4 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver Antheas Kapenekakis ` (3 subsequent siblings) 4 siblings, 0 replies; 21+ messages in thread From: Antheas Kapenekakis @ 2026-03-09 20:51 UTC (permalink / raw) To: Mario.Limonciello Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato, i, linux-kernel, platform-driver-x86, Antheas Kapenekakis The save_settings interface is also implemented by amd_dptc, which has the same bulk/single/save semantics but no save-count limitation. Generalize the description to cover both drivers: move the Lenovo 48-save architectural constraint into a driver-specific notes section and add the amd_dptc behavior alongside it. Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev> --- .../testing/sysfs-class-firmware-attributes | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/Documentation/ABI/testing/sysfs-class-firmware-attributes b/Documentation/ABI/testing/sysfs-class-firmware-attributes index 2713efa509b4..e0b43b14fa19 100644 --- a/Documentation/ABI/testing/sysfs-class-firmware-attributes +++ b/Documentation/ABI/testing/sysfs-class-firmware-attributes @@ -388,31 +388,42 @@ What: /sys/class/firmware-attributes/*/attributes/save_settings Date: August 2023 KernelVersion: 6.6 Contact: Mark Pearson <mpearson-lenovo@squebb.ca> + Antheas Kapenekakis <lkml@antheas.dev> Description: - On Lenovo platforms there is a limitation in the number of times an attribute can be - saved. This is an architectural limitation and it limits the number of attributes - that can be modified to 48. - A solution for this is instead of the attribute being saved after every modification, - to allow a user to bulk set the attributes, and then trigger a final save. This allows - unlimited attributes. + Controls how writes to current_value are applied to the hardware. Read the attribute to check what save mode is enabled (single or bulk). E.g: - # cat /sys/class/firmware-attributes/thinklmi/attributes/save_settings + # cat /sys/class/firmware-attributes/*/attributes/save_settings single Write the attribute with 'bulk' to enable bulk save mode. - Write the attribute with 'single' to enable saving, after every attribute set. - The default setting is single mode. + Write the attribute with 'single' to enable saving, after every + attribute set. The default setting is single mode. E.g: - # echo bulk > /sys/class/firmware-attributes/thinklmi/attributes/save_settings + # echo bulk > /sys/class/firmware-attributes/*/attributes/save_settings - When in bulk mode write 'save' to trigger a save of all currently modified attributes. - Note, once a save has been triggered, in bulk mode, attributes can no longer be set and - will return a permissions error. This is to prevent users hitting the 48+ save limitation - (which requires entering the BIOS to clear the error condition) + When in bulk mode write 'save' to trigger an apply of all + currently staged attributes. E.g: - # echo save > /sys/class/firmware-attributes/thinklmi/attributes/save_settings + # echo save > /sys/class/firmware-attributes/*/attributes/save_settings + + Driver-specific notes: + + thinklmi (Lenovo): On Lenovo platforms there is a limitation in + the number of times an attribute can be saved. This is an + architectural limitation and it limits the number of attributes + that can be modified to 48. + + Once a save has been triggered in bulk mode, attributes can no + longer be set and will return a permissions error. This is to + prevent users hitting the 48+ save limitation (which requires + entering the BIOS to clear the error condition). + + amd-dptc (AMD DPTC): No save-count limitation. 'save' can be + called any number of times. Returns -ENOENT if no values have + been staged. In addition, when in 'single' mode, the driver + uses pm ops to trigger a save of staged attributes on resume. What: /sys/class/firmware-attributes/*/attributes/debug_cmd Date: July 2021 -- 2.52.0 ^ permalink raw reply related [flat|nested] 21+ messages in thread
* [RFC v4 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver 2026-03-09 20:51 [RFC v4 0/4] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Antheas Kapenekakis 2026-03-09 20:51 ` [RFC v4 1/4] Documentation: firmware-attributes: generalize save_settings entry Antheas Kapenekakis @ 2026-03-09 20:51 ` Antheas Kapenekakis 2026-03-10 4:01 ` Mario Limonciello 2026-03-09 20:51 ` [RFC v4 3/4] platform/x86/amd: dptc: Add platform profile support Antheas Kapenekakis ` (2 subsequent siblings) 4 siblings, 1 reply; 21+ messages in thread From: Antheas Kapenekakis @ 2026-03-09 20:51 UTC (permalink / raw) To: Mario.Limonciello Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato, i, linux-kernel, platform-driver-x86, Antheas Kapenekakis Add a driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and Thermal Configuration interface (DPTCi). This exposes TDP and thermal parameters for AMD APU-based handheld devices via the firmware-attributes sysfs ABI. Parameters are staged and atomically committed through ALIB. The driver supports two save modes: "single" (apply immediately on write) and "bulk" (stage values, then commit with "save"). An "expanded_limits" toggle widens the allowed parameter ranges beyond device defaults. Initial device support: GPD Win 5 (AMD Ryzen AI MAX). Assisted-by: Claude:claude-opus-4-6 Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev> --- MAINTAINERS | 6 + drivers/platform/x86/amd/Kconfig | 14 + drivers/platform/x86/amd/Makefile | 2 + drivers/platform/x86/amd/dptc.c | 746 ++++++++++++++++++++++++++++++ 4 files changed, 768 insertions(+) create mode 100644 drivers/platform/x86/amd/dptc.c diff --git a/MAINTAINERS b/MAINTAINERS index 89007f9ed35e..ebda8e82bf35 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1103,6 +1103,12 @@ S: Supported F: drivers/gpu/drm/amd/display/dc/dml/ F: drivers/gpu/drm/amd/display/dc/dml2_0/ +AMD DPTC DRIVER +M: Antheas Kapenekakis <lkml@antheas.dev> +L: platform-driver-x86@vger.kernel.org +S: Maintained +F: drivers/platform/x86/amd/dptc.c + AMD FAM15H PROCESSOR POWER MONITORING DRIVER M: Huang Rui <ray.huang@amd.com> L: linux-hwmon@vger.kernel.org diff --git a/drivers/platform/x86/amd/Kconfig b/drivers/platform/x86/amd/Kconfig index b813f9265368..d610092467fc 100644 --- a/drivers/platform/x86/amd/Kconfig +++ b/drivers/platform/x86/amd/Kconfig @@ -44,3 +44,17 @@ config AMD_ISP_PLATFORM This driver can also be built as a module. If so, the module will be called amd_isp4. + +config AMD_DPTC + tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)" + depends on X86_64 && ACPI && DMI + select FIRMWARE_ATTRIBUTES_CLASS + help + Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and + Thermal Configuration Interface (DPTCi). Exposes TDP and thermal + parameters for AMD APU-based handheld devices via the + firmware-attributes sysfs ABI, allowing userspace tools to stage + and atomically commit power limit settings. Requires a DMI match + for the device and a recognized AMD SoC. + + If built as a module, the module will be called amd_dptc. diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile index f6ff0c837f34..862a609bfe38 100644 --- a/drivers/platform/x86/amd/Makefile +++ b/drivers/platform/x86/amd/Makefile @@ -12,3 +12,5 @@ obj-$(CONFIG_AMD_PMF) += pmf/ obj-$(CONFIG_AMD_WBRF) += wbrf.o obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o obj-$(CONFIG_AMD_HFI) += hfi/ +obj-$(CONFIG_AMD_DPTC) += amd_dptc.o +amd_dptc-y := dptc.o diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c new file mode 100644 index 000000000000..b884cdfa3f82 --- /dev/null +++ b/drivers/platform/x86/amd/dptc.c @@ -0,0 +1,746 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver + * + * Exposes AMD APU power and thermal parameters via the firmware-attributes + * sysfs ABI. Parameters are staged and atomically committed through the + * AGESA ALIB Function 0x0C (Dynamic Power and Thermal Configuration + * interface). + * + * Reference: AMD AGESA Publication #44065, Appendix E.5 + * https://docs.amd.com/v/u/en-US/44065_Arch2008 + * + * Copyright (C) 2026 Antheas Kapenekakis <lkml@antheas.dev> + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/acpi.h> +#include <linux/cleanup.h> +#include <linux/dmi.h> +#include <linux/init.h> +#include <linux/kobject.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/platform_device.h> +#include <linux/processor.h> +#include <linux/sysfs.h> +#include <linux/unaligned.h> + +#include "../firmware_attributes_class.h" + +#define DRIVER_NAME "amd-dptc" + +#define ALIB_FUNC_DPTC 0x0C +#define ALIB_PATH "\\_SB.ALIB" + +#define ALIB_ID_TEMP_TARGET 0x03 +#define ALIB_ID_STAPM_LIMIT 0x05 +#define ALIB_ID_FAST_LIMIT 0x06 +#define ALIB_ID_SLOW_LIMIT 0x07 +#define ALIB_ID_SKIN_LIMIT 0x2E + +enum dptc_param_idx { + DPTC_PPT_PL1_SPL, /* STAPM + skin limit (set together) */ + DPTC_PPT_PL2_SPPT, /* slow PPT limit */ + DPTC_PPT_PL3_FPPT, /* fast PPT limit */ + DPTC_CPU_TEMP, /* thermal control target */ + DPTC_NUM_PARAMS, +}; + +struct dptc_param_limits { + u32 expanded_min; + u32 device_min; + u32 def; + u32 device_max; + u32 expanded_max; +}; + +struct dptc_device_limits { + struct dptc_param_limits params[DPTC_NUM_PARAMS]; +}; + +struct dptc_param_desc { + const char *name; + const char *display_name; + u16 scale; /* sysfs-to-ALIB multiplier (e.g. 1000 for W->mW) */ + u8 param_id; + u8 param_id2; /* secondary ALIB ID, 0 if none */ +}; + +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = { + [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (W)", + 1000, ALIB_ID_STAPM_LIMIT, + ALIB_ID_SKIN_LIMIT }, + [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (W)", + 1000, ALIB_ID_SLOW_LIMIT }, + [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (W)", + 1000, ALIB_ID_FAST_LIMIT }, + [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)", + 1, ALIB_ID_TEMP_TARGET }, +}; + +/* AI MAX Handheld class: GPD Win 5 */ +static const struct dptc_device_limits limits_maxhh = { + .params = { + [DPTC_PPT_PL1_SPL] = { 1, 4, 25, 80, 100 }, + [DPTC_PPT_PL2_SPPT] = { 1, 4, 27, 82, 100 }, + [DPTC_PPT_PL3_FPPT] = { 1, 4, 40, 85, 100 }, + [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 }, + }, +}; + +/* Substring matches against boot_cpu_data.x86_model_id; order matters. */ +static const char * const dptc_soc_table[] = { + /* AI MAX */ + "AMD RYZEN AI MAX+ 395", + "AMD RYZEN AI MAX+ 385", + "AMD RYZEN AI MAX 380", + NULL, +}; + +static const struct dmi_system_id dptc_dmi_table[] = { + /* GPD */ + { + .ident = "GPD Win 5", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"), + }, + .driver_data = (void *)&limits_maxhh, + }, + { } +}; +MODULE_DEVICE_TABLE(dmi, dptc_dmi_table); + +enum dptc_save_mode { SAVE_SINGLE, SAVE_BULK }; + +struct dptc_priv; + +struct dptc_attr_sysfs { + struct dptc_priv *priv; + struct kobj_attribute current_value; + struct kobj_attribute default_value; + struct kobj_attribute min_value; + struct kobj_attribute max_value; + struct kobj_attribute scalar_increment; + struct kobj_attribute display_name; + struct kobj_attribute type; + struct attribute *attrs[8]; + struct attribute_group group; + int idx; +}; + +struct dptc_priv { + struct device *fw_attr_dev; + struct kset *fw_attr_kset; + + const struct dptc_device_limits *dev_limits; + + bool expanded; + + enum dptc_save_mode save_mode; + + u32 staged[DPTC_NUM_PARAMS]; + + /* Protects staged, expanded, and save_mode */ + struct mutex lock; + + struct dptc_attr_sysfs params[DPTC_NUM_PARAMS]; + struct dptc_attr_sysfs expanded_attr; + struct kobj_attribute save_settings_attr; +}; + +static struct platform_device *dptc_pdev; + +static u32 dptc_get_min(struct dptc_priv *dptc, int idx) +{ + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_min + : dptc->dev_limits->params[idx].device_min; +} + +static u32 dptc_get_max(struct dptc_priv *dptc, int idx) +{ + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_max + : dptc->dev_limits->params[idx].device_max; +} + +static u32 dptc_get_default(struct dptc_priv *dptc, int idx) +{ + return dptc->dev_limits->params[idx].def; +} + +static int dptc_alib_call(const u8 *ids, const u32 *vals, int count) +{ + union acpi_object in_params[2]; + struct acpi_object_list input; + acpi_status status; + u32 buf_size; + int i, off; + u8 *buf; + + if (count == 0) + return -ENOENT; + + /* Buffer layout: WORD total_size + count * (BYTE id + DWORD value) */ + buf_size = 2 + count * 5; + buf = kzalloc(buf_size, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + put_unaligned_le16(buf_size, buf); + + for (i = 0; i < count; i++) { + off = 2 + i * 5; + buf[off] = ids[i]; + put_unaligned_le32(vals[i], &buf[off + 1]); + } + + in_params[0].type = ACPI_TYPE_INTEGER; + in_params[0].integer.value = ALIB_FUNC_DPTC; + in_params[1].type = ACPI_TYPE_BUFFER; + in_params[1].buffer.length = buf_size; + in_params[1].buffer.pointer = buf; + + input.count = 2; + input.pointer = in_params; + + status = acpi_evaluate_object(NULL, ALIB_PATH, &input, NULL); + kfree(buf); + + if (ACPI_FAILURE(status)) { + pr_err("ALIB call failed: %s\n", + acpi_format_exception(status)); + return -EIO; + } + + pr_debug("sent %d ALIB parameter(s)\n", count); + return 0; +} + +static int dptc_alib_fill_param(u8 *ids, u32 *vals, int offset, + enum dptc_param_idx param, u32 val) +{ + u32 hw_val = val * dptc_params[param].scale; + + ids[offset] = dptc_params[param].param_id; + vals[offset++] = hw_val; + + if (dptc_params[param].param_id2) { + ids[offset] = dptc_params[param].param_id2; + vals[offset++] = hw_val; + } + + return offset; +} + +static int dptc_alib_send_one(int idx, u32 val) +{ + u32 vals[2]; + u8 ids[2]; + + return dptc_alib_call(ids, vals, + dptc_alib_fill_param(ids, vals, 0, idx, val)); +} + +static int dptc_alib_save(struct dptc_priv *dptc) +{ + u32 vals[DPTC_NUM_PARAMS * 2]; + u8 ids[DPTC_NUM_PARAMS * 2]; + int count = 0; + int i; + + for (i = 0; i < DPTC_NUM_PARAMS; i++) { + if (!dptc->staged[i]) + continue; + count = dptc_alib_fill_param(ids, vals, count, i, + dptc->staged[i]); + } + + return dptc_alib_call(ids, vals, count); +} + +/* Sysfs callbacks */ + +static ssize_t dptc_current_value_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct dptc_attr_sysfs *ps = + container_of(attr, struct dptc_attr_sysfs, current_value); + struct dptc_priv *dptc = ps->priv; + + guard(mutex)(&dptc->lock); + + if (!dptc->staged[ps->idx]) + return sysfs_emit(buf, "\n"); + return sysfs_emit(buf, "%u\n", dptc->staged[ps->idx]); +} + +static ssize_t dptc_current_value_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct dptc_attr_sysfs *ps = + container_of(attr, struct dptc_attr_sysfs, current_value); + struct dptc_priv *dptc = ps->priv; + u32 val, min, max; + int ret; + + guard(mutex)(&dptc->lock); + + if (count == 1 && buf[0] == '\n') { + dptc->staged[ps->idx] = 0; + return count; + } + + ret = kstrtou32(buf, 10, &val); + if (ret) + return ret; + + min = dptc_get_min(dptc, ps->idx); + max = dptc_get_max(dptc, ps->idx); + if (val < min || (max && val > max)) + return -EINVAL; + dptc->staged[ps->idx] = val; + if (dptc->save_mode == SAVE_SINGLE) + ret = dptc_alib_send_one(ps->idx, val); + + return ret ? ret : count; +} + +static ssize_t dptc_default_value_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct dptc_attr_sysfs *ps = + container_of(attr, struct dptc_attr_sysfs, default_value); + + return sysfs_emit(buf, "%u\n", dptc_get_default(ps->priv, ps->idx)); +} + +static ssize_t dptc_min_value_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct dptc_attr_sysfs *ps = + container_of(attr, struct dptc_attr_sysfs, min_value); + struct dptc_priv *dptc = ps->priv; + + guard(mutex)(&dptc->lock); + + return sysfs_emit(buf, "%u\n", dptc_get_min(dptc, ps->idx)); +} + +static ssize_t dptc_max_value_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct dptc_attr_sysfs *ps = + container_of(attr, struct dptc_attr_sysfs, max_value); + struct dptc_priv *dptc = ps->priv; + + guard(mutex)(&dptc->lock); + + return sysfs_emit(buf, "%u\n", dptc_get_max(dptc, ps->idx)); +} + +static ssize_t dptc_scalar_increment_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "1\n"); +} + +static ssize_t dptc_display_name_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct dptc_attr_sysfs *ps = + container_of(attr, struct dptc_attr_sysfs, display_name); + return sysfs_emit(buf, "%s\n", dptc_params[ps->idx].display_name); +} + +static ssize_t dptc_type_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "integer\n"); +} + +static ssize_t dptc_save_settings_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct dptc_priv *dptc = + container_of(attr, struct dptc_priv, save_settings_attr); + + guard(mutex)(&dptc->lock); + + if (dptc->save_mode == SAVE_SINGLE) + return sysfs_emit(buf, "single\n"); + return sysfs_emit(buf, "bulk\n"); +} + +static ssize_t dptc_save_settings_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct dptc_priv *dptc = + container_of(attr, struct dptc_priv, save_settings_attr); + int ret = 0; + + guard(mutex)(&dptc->lock); + + if (sysfs_streq(buf, "save")) + ret = dptc_alib_save(dptc); + else if (sysfs_streq(buf, "single")) + dptc->save_mode = SAVE_SINGLE; + else if (sysfs_streq(buf, "bulk")) + dptc->save_mode = SAVE_BULK; + else + return -EINVAL; + + return ret ? ret : count; +} + +static ssize_t dptc_expanded_current_value_show(struct kobject *kobj, + struct kobj_attribute *attr, + char *buf) +{ + struct dptc_attr_sysfs *ps = + container_of(attr, struct dptc_attr_sysfs, current_value); + struct dptc_priv *dptc = ps->priv; + + guard(mutex)(&dptc->lock); + + return sysfs_emit(buf, "%d\n", dptc->expanded); +} + +static ssize_t dptc_expanded_current_value_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct dptc_attr_sysfs *ps = + container_of(attr, struct dptc_attr_sysfs, current_value); + struct dptc_priv *dptc = ps->priv; + bool val; + int ret; + + ret = kstrtobool(buf, &val); + if (ret) + return ret; + + guard(mutex)(&dptc->lock); + + dptc->expanded = val; + /* Clear staged values: limits changed, old values may be out of range */ + memset(dptc->staged, 0, sizeof(dptc->staged)); + + return count; +} + +static ssize_t dptc_expanded_default_value_show(struct kobject *kobj, + struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "0\n"); +} + +static ssize_t dptc_expanded_min_value_show(struct kobject *kobj, + struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "0\n"); +} + +static ssize_t dptc_expanded_max_value_show(struct kobject *kobj, + struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "1\n"); +} + +static ssize_t dptc_expanded_scalar_increment_show(struct kobject *kobj, + struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "1\n"); +} + +static ssize_t dptc_expanded_display_name_show(struct kobject *kobj, + struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "Expanded Limits\n"); +} + +static ssize_t dptc_expanded_type_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "integer\n"); +} + +/* Sysfs setup */ + +static void dptc_setup_param_sysfs(struct dptc_priv *dptc, + struct dptc_attr_sysfs *ps, int idx) +{ + ps->priv = dptc; + ps->idx = idx; + + sysfs_attr_init(&ps->current_value.attr); + ps->current_value.attr.name = "current_value"; + ps->current_value.attr.mode = 0644; + ps->current_value.show = dptc_current_value_show; + ps->current_value.store = dptc_current_value_store; + + sysfs_attr_init(&ps->default_value.attr); + ps->default_value.attr.name = "default_value"; + ps->default_value.attr.mode = 0444; + ps->default_value.show = dptc_default_value_show; + + sysfs_attr_init(&ps->min_value.attr); + ps->min_value.attr.name = "min_value"; + ps->min_value.attr.mode = 0444; + ps->min_value.show = dptc_min_value_show; + + sysfs_attr_init(&ps->max_value.attr); + ps->max_value.attr.name = "max_value"; + ps->max_value.attr.mode = 0444; + ps->max_value.show = dptc_max_value_show; + + sysfs_attr_init(&ps->scalar_increment.attr); + ps->scalar_increment.attr.name = "scalar_increment"; + ps->scalar_increment.attr.mode = 0444; + ps->scalar_increment.show = dptc_scalar_increment_show; + + sysfs_attr_init(&ps->display_name.attr); + ps->display_name.attr.name = "display_name"; + ps->display_name.attr.mode = 0444; + ps->display_name.show = dptc_display_name_show; + + sysfs_attr_init(&ps->type.attr); + ps->type.attr.name = "type"; + ps->type.attr.mode = 0444; + ps->type.show = dptc_type_show; + + ps->attrs[0] = &ps->current_value.attr; + ps->attrs[1] = &ps->default_value.attr; + ps->attrs[2] = &ps->min_value.attr; + ps->attrs[3] = &ps->max_value.attr; + ps->attrs[4] = &ps->scalar_increment.attr; + ps->attrs[5] = &ps->display_name.attr; + ps->attrs[6] = &ps->type.attr; + ps->attrs[7] = NULL; + + ps->group.name = dptc_params[idx].name; + ps->group.attrs = ps->attrs; +} + +static void dptc_setup_expanded_sysfs(struct dptc_priv *dptc, + struct dptc_attr_sysfs *ps) +{ + ps->priv = dptc; + sysfs_attr_init(&ps->current_value.attr); + ps->current_value.attr.name = "current_value"; + ps->current_value.attr.mode = 0644; + ps->current_value.show = dptc_expanded_current_value_show; + ps->current_value.store = dptc_expanded_current_value_store; + + sysfs_attr_init(&ps->default_value.attr); + ps->default_value.attr.name = "default_value"; + ps->default_value.attr.mode = 0444; + ps->default_value.show = dptc_expanded_default_value_show; + + sysfs_attr_init(&ps->min_value.attr); + ps->min_value.attr.name = "min_value"; + ps->min_value.attr.mode = 0444; + ps->min_value.show = dptc_expanded_min_value_show; + + sysfs_attr_init(&ps->max_value.attr); + ps->max_value.attr.name = "max_value"; + ps->max_value.attr.mode = 0444; + ps->max_value.show = dptc_expanded_max_value_show; + + sysfs_attr_init(&ps->scalar_increment.attr); + ps->scalar_increment.attr.name = "scalar_increment"; + ps->scalar_increment.attr.mode = 0444; + ps->scalar_increment.show = dptc_expanded_scalar_increment_show; + + sysfs_attr_init(&ps->display_name.attr); + ps->display_name.attr.name = "display_name"; + ps->display_name.attr.mode = 0444; + ps->display_name.show = dptc_expanded_display_name_show; + + sysfs_attr_init(&ps->type.attr); + ps->type.attr.name = "type"; + ps->type.attr.mode = 0444; + ps->type.show = dptc_expanded_type_show; + + ps->attrs[0] = &ps->current_value.attr; + ps->attrs[1] = &ps->default_value.attr; + ps->attrs[2] = &ps->min_value.attr; + ps->attrs[3] = &ps->max_value.attr; + ps->attrs[4] = &ps->scalar_increment.attr; + ps->attrs[5] = &ps->display_name.attr; + ps->attrs[6] = &ps->type.attr; + ps->attrs[7] = NULL; + + ps->group.name = "expanded_limits"; + ps->group.attrs = ps->attrs; +} + +static void dptc_fw_dev_unregister(void *data) +{ + device_unregister(data); +} + +static void dptc_kset_unregister(void *data) +{ + kset_unregister(data); +} + +static int dptc_resume(struct device *dev) +{ + struct dptc_priv *dptc = dev_get_drvdata(dev); + int ret; + + guard(mutex)(&dptc->lock); + + /* In bulk mode, do not use pm ops for userspace flexibility. */ + if (dptc->save_mode == SAVE_SINGLE) + ret = dptc_alib_save(dptc); + else + ret = 0; + + if (ret && ret != -ENOENT) + dev_warn(dev, "failed to restore settings on resume: %d\n", ret); + + return 0; +} + +static DEFINE_SIMPLE_DEV_PM_OPS(dptc_pm_ops, NULL, dptc_resume); + +static int dptc_probe(struct platform_device *pdev) +{ + const struct dmi_system_id *dmi_match = dev_get_platdata(&pdev->dev); + struct device *dev = &pdev->dev; + struct dptc_priv *dptc; + int i, ret; + + dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL); + if (!dptc) + return -ENOMEM; + + platform_set_drvdata(pdev, dptc); + + ret = devm_mutex_init(dev, &dptc->lock); + if (ret) + return ret; + + dptc->dev_limits = dmi_match->driver_data; + dev_info(dev, "%s (%s)\n", dmi_match->ident, + boot_cpu_data.x86_model_id); + + dptc->fw_attr_dev = device_create(&firmware_attributes_class, + NULL, MKDEV(0, 0), NULL, DRIVER_NAME); + if (IS_ERR(dptc->fw_attr_dev)) + return PTR_ERR(dptc->fw_attr_dev); + + ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister, + dptc->fw_attr_dev); + if (ret) + return ret; + + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL, + &dptc->fw_attr_dev->kobj); + if (!dptc->fw_attr_kset) + return -ENOMEM; + + ret = devm_add_action_or_reset(dev, dptc_kset_unregister, + dptc->fw_attr_kset); + if (ret) + return ret; + + for (i = 0; i < DPTC_NUM_PARAMS; i++) { + dptc_setup_param_sysfs(dptc, &dptc->params[i], i); + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj, + &dptc->params[i].group); + if (ret) + return ret; + } + + dptc_setup_expanded_sysfs(dptc, &dptc->expanded_attr); + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj, + &dptc->expanded_attr.group); + if (ret) + return ret; + + sysfs_attr_init(&dptc->save_settings_attr.attr); + dptc->save_settings_attr.attr.name = "save_settings"; + dptc->save_settings_attr.attr.mode = 0644; + dptc->save_settings_attr.show = dptc_save_settings_show; + dptc->save_settings_attr.store = dptc_save_settings_store; + ret = sysfs_create_file(&dptc->fw_attr_kset->kobj, + &dptc->save_settings_attr.attr); + if (ret) + return ret; + + return 0; +} + +static struct platform_driver dptc_driver = { + .driver = { + .name = DRIVER_NAME, + .pm = pm_sleep_ptr(&dptc_pm_ops), + }, + .probe = dptc_probe, +}; + +static int __init dptc_init(void) +{ + const struct dmi_system_id *match; + bool soc_found = false; + int i, ret; + + match = dmi_first_match(dptc_dmi_table); + if (!match) + return -ENODEV; + + if (!acpi_has_method(NULL, ALIB_PATH)) { + pr_warn("ALIB method not present\n"); + return -ENODEV; + } + + for (i = 0; dptc_soc_table[i]; i++) { + if (strstr(boot_cpu_data.x86_model_id, + dptc_soc_table[i])) { + soc_found = true; + break; + } + } + if (!soc_found) { + pr_warn("unrecognized SoC '%s'\n", + boot_cpu_data.x86_model_id); + return -ENODEV; + } + + dptc_pdev = platform_device_register_data(NULL, DRIVER_NAME, -1, + match, sizeof(*match)); + if (IS_ERR(dptc_pdev)) + return PTR_ERR(dptc_pdev); + + ret = platform_driver_register(&dptc_driver); + if (ret) { + platform_device_unregister(dptc_pdev); + return ret; + } + + return 0; +} + +static void __exit dptc_exit(void) +{ + platform_driver_unregister(&dptc_driver); + platform_device_unregister(dptc_pdev); +} + +module_init(dptc_init); +module_exit(dptc_exit); + +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>"); +MODULE_DESCRIPTION("AMD DPTCi ACPI Driver"); +MODULE_LICENSE("GPL"); -- 2.52.0 ^ permalink raw reply related [flat|nested] 21+ messages in thread
* Re: [RFC v4 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver 2026-03-09 20:51 ` [RFC v4 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver Antheas Kapenekakis @ 2026-03-10 4:01 ` Mario Limonciello 2026-03-10 8:02 ` Antheas Kapenekakis 0 siblings, 1 reply; 21+ messages in thread From: Mario Limonciello @ 2026-03-10 4:01 UTC (permalink / raw) To: Antheas Kapenekakis Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato, i, linux-kernel, platform-driver-x86 On 3/9/2026 3:51 PM, Antheas Kapenekakis wrote: > Add a driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and > Thermal Configuration interface (DPTCi). This exposes TDP and thermal > parameters for AMD APU-based handheld devices via the > firmware-attributes sysfs ABI. > > Parameters are staged and atomically committed through ALIB. The driver > supports two save modes: "single" (apply immediately on write) and > "bulk" (stage values, then commit with "save"). An "expanded_limits" > toggle widens the allowed parameter ranges beyond device defaults. > > Initial device support: GPD Win 5 (AMD Ryzen AI MAX). > > Assisted-by: Claude:claude-opus-4-6 > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev> > --- > MAINTAINERS | 6 + > drivers/platform/x86/amd/Kconfig | 14 + > drivers/platform/x86/amd/Makefile | 2 + > drivers/platform/x86/amd/dptc.c | 746 ++++++++++++++++++++++++++++++ > 4 files changed, 768 insertions(+) > create mode 100644 drivers/platform/x86/amd/dptc.c > > diff --git a/MAINTAINERS b/MAINTAINERS > index 89007f9ed35e..ebda8e82bf35 100644 > --- a/MAINTAINERS > +++ b/MAINTAINERS > @@ -1103,6 +1103,12 @@ S: Supported > F: drivers/gpu/drm/amd/display/dc/dml/ > F: drivers/gpu/drm/amd/display/dc/dml2_0/ > > +AMD DPTC DRIVER > +M: Antheas Kapenekakis <lkml@antheas.dev> > +L: platform-driver-x86@vger.kernel.org > +S: Maintained > +F: drivers/platform/x86/amd/dptc.c > + > AMD FAM15H PROCESSOR POWER MONITORING DRIVER > M: Huang Rui <ray.huang@amd.com> > L: linux-hwmon@vger.kernel.org > diff --git a/drivers/platform/x86/amd/Kconfig b/drivers/platform/x86/amd/Kconfig > index b813f9265368..d610092467fc 100644 > --- a/drivers/platform/x86/amd/Kconfig > +++ b/drivers/platform/x86/amd/Kconfig > @@ -44,3 +44,17 @@ config AMD_ISP_PLATFORM > > This driver can also be built as a module. If so, the module > will be called amd_isp4. > + > +config AMD_DPTC > + tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)" > + depends on X86_64 && ACPI && DMI > + select FIRMWARE_ATTRIBUTES_CLASS > + help > + Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and > + Thermal Configuration Interface (DPTCi). Exposes TDP and thermal > + parameters for AMD APU-based handheld devices via the > + firmware-attributes sysfs ABI, allowing userspace tools to stage > + and atomically commit power limit settings. Requires a DMI match > + for the device and a recognized AMD SoC. > + > + If built as a module, the module will be called amd_dptc. > diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile > index f6ff0c837f34..862a609bfe38 100644 > --- a/drivers/platform/x86/amd/Makefile > +++ b/drivers/platform/x86/amd/Makefile > @@ -12,3 +12,5 @@ obj-$(CONFIG_AMD_PMF) += pmf/ > obj-$(CONFIG_AMD_WBRF) += wbrf.o > obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o > obj-$(CONFIG_AMD_HFI) += hfi/ > +obj-$(CONFIG_AMD_DPTC) += amd_dptc.o > +amd_dptc-y := dptc.o > diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c > new file mode 100644 > index 000000000000..b884cdfa3f82 > --- /dev/null > +++ b/drivers/platform/x86/amd/dptc.c > @@ -0,0 +1,746 @@ > +// SPDX-License-Identifier: GPL-2.0-only > +/* > + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver > + * > + * Exposes AMD APU power and thermal parameters via the firmware-attributes > + * sysfs ABI. Parameters are staged and atomically committed through the > + * AGESA ALIB Function 0x0C (Dynamic Power and Thermal Configuration > + * interface). > + * > + * Reference: AMD AGESA Publication #44065, Appendix E.5 > + * https://docs.amd.com/v/u/en-US/44065_Arch2008 > + * > + * Copyright (C) 2026 Antheas Kapenekakis <lkml@antheas.dev> > + */ > + > +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt > + > +#include <linux/acpi.h> > +#include <linux/cleanup.h> > +#include <linux/dmi.h> > +#include <linux/init.h> > +#include <linux/kobject.h> > +#include <linux/module.h> > +#include <linux/mutex.h> > +#include <linux/platform_device.h> > +#include <linux/processor.h> > +#include <linux/sysfs.h> > +#include <linux/unaligned.h> > + > +#include "../firmware_attributes_class.h" > + > +#define DRIVER_NAME "amd-dptc" > + > +#define ALIB_FUNC_DPTC 0x0C > +#define ALIB_PATH "\\_SB.ALIB" > + > +#define ALIB_ID_TEMP_TARGET 0x03 > +#define ALIB_ID_STAPM_LIMIT 0x05 > +#define ALIB_ID_FAST_LIMIT 0x06 > +#define ALIB_ID_SLOW_LIMIT 0x07 > +#define ALIB_ID_SKIN_LIMIT 0x2E > + > +enum dptc_param_idx { > + DPTC_PPT_PL1_SPL, /* STAPM + skin limit (set together) */ > + DPTC_PPT_PL2_SPPT, /* slow PPT limit */ > + DPTC_PPT_PL3_FPPT, /* fast PPT limit */ > + DPTC_CPU_TEMP, /* thermal control target */ > + DPTC_NUM_PARAMS, > +}; > + > +struct dptc_param_limits { > + u32 expanded_min; > + u32 device_min; > + u32 def; > + u32 device_max; > + u32 expanded_max; > +}; > + > +struct dptc_device_limits { > + struct dptc_param_limits params[DPTC_NUM_PARAMS]; > +}; > + > +struct dptc_param_desc { > + const char *name; > + const char *display_name; > + u16 scale; /* sysfs-to-ALIB multiplier (e.g. 1000 for W->mW) */ > + u8 param_id; > + u8 param_id2; /* secondary ALIB ID, 0 if none */ > +}; > + > +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = { > + [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (W)", > + 1000, ALIB_ID_STAPM_LIMIT, > + ALIB_ID_SKIN_LIMIT }, > + [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (W)", > + 1000, ALIB_ID_SLOW_LIMIT }, > + [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (W)", > + 1000, ALIB_ID_FAST_LIMIT }, > + [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)", > + 1, ALIB_ID_TEMP_TARGET }, > +}; > + > +/* AI MAX Handheld class: GPD Win 5 */ > +static const struct dptc_device_limits limits_maxhh = { > + .params = { > + [DPTC_PPT_PL1_SPL] = { 1, 4, 25, 80, 100 }, > + [DPTC_PPT_PL2_SPPT] = { 1, 4, 27, 82, 100 }, > + [DPTC_PPT_PL3_FPPT] = { 1, 4, 40, 85, 100 }, > + [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 }, > + }, > +}; > + > +/* Substring matches against boot_cpu_data.x86_model_id; order matters. */ > +static const char * const dptc_soc_table[] = { > + /* AI MAX */ > + "AMD RYZEN AI MAX+ 395", > + "AMD RYZEN AI MAX+ 385", > + "AMD RYZEN AI MAX 380", > + NULL, > +}; I feel like I commented this before; but I don't really understand the purpose of this table. If you have a system that is quirked already, why would you need to cross reference this table? > + > +static const struct dmi_system_id dptc_dmi_table[] = { > + /* GPD */ > + { > + .ident = "GPD Win 5", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"), > + }, > + .driver_data = (void *)&limits_maxhh, > + }, > + { } > +}; > +MODULE_DEVICE_TABLE(dmi, dptc_dmi_table); > + > +enum dptc_save_mode { SAVE_SINGLE, SAVE_BULK }; > + > +struct dptc_priv; > + > +struct dptc_attr_sysfs { > + struct dptc_priv *priv; > + struct kobj_attribute current_value; > + struct kobj_attribute default_value; > + struct kobj_attribute min_value; > + struct kobj_attribute max_value; > + struct kobj_attribute scalar_increment; > + struct kobj_attribute display_name; > + struct kobj_attribute type; > + struct attribute *attrs[8]; > + struct attribute_group group; > + int idx; > +}; > + > +struct dptc_priv { > + struct device *fw_attr_dev; > + struct kset *fw_attr_kset; > + > + const struct dptc_device_limits *dev_limits; > + > + bool expanded; > + > + enum dptc_save_mode save_mode; > + > + u32 staged[DPTC_NUM_PARAMS]; > + > + /* Protects staged, expanded, and save_mode */ > + struct mutex lock; > + > + struct dptc_attr_sysfs params[DPTC_NUM_PARAMS]; > + struct dptc_attr_sysfs expanded_attr; > + struct kobj_attribute save_settings_attr; > +}; > + > +static struct platform_device *dptc_pdev; > + > +static u32 dptc_get_min(struct dptc_priv *dptc, int idx) > +{ > + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_min > + : dptc->dev_limits->params[idx].device_min; > +} > + > +static u32 dptc_get_max(struct dptc_priv *dptc, int idx) > +{ > + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_max > + : dptc->dev_limits->params[idx].device_max; > +} > + > +static u32 dptc_get_default(struct dptc_priv *dptc, int idx) > +{ > + return dptc->dev_limits->params[idx].def; > +} > + > +static int dptc_alib_call(const u8 *ids, const u32 *vals, int count) > +{ > + union acpi_object in_params[2]; > + struct acpi_object_list input; > + acpi_status status; > + u32 buf_size; > + int i, off; > + u8 *buf; > + > + if (count == 0) > + return -ENOENT; > + > + /* Buffer layout: WORD total_size + count * (BYTE id + DWORD value) */ > + buf_size = 2 + count * 5; > + buf = kzalloc(buf_size, GFP_KERNEL); > + if (!buf) > + return -ENOMEM; > + > + put_unaligned_le16(buf_size, buf); > + > + for (i = 0; i < count; i++) { > + off = 2 + i * 5; > + buf[off] = ids[i]; > + put_unaligned_le32(vals[i], &buf[off + 1]); > + } > + > + in_params[0].type = ACPI_TYPE_INTEGER; > + in_params[0].integer.value = ALIB_FUNC_DPTC; > + in_params[1].type = ACPI_TYPE_BUFFER; > + in_params[1].buffer.length = buf_size; > + in_params[1].buffer.pointer = buf; > + > + input.count = 2; > + input.pointer = in_params; > + > + status = acpi_evaluate_object(NULL, ALIB_PATH, &input, NULL); > + kfree(buf); > + > + if (ACPI_FAILURE(status)) { > + pr_err("ALIB call failed: %s\n", > + acpi_format_exception(status)); > + return -EIO; > + } > + > + pr_debug("sent %d ALIB parameter(s)\n", count); > + return 0; > +} > + > +static int dptc_alib_fill_param(u8 *ids, u32 *vals, int offset, > + enum dptc_param_idx param, u32 val) > +{ > + u32 hw_val = val * dptc_params[param].scale; > + > + ids[offset] = dptc_params[param].param_id; > + vals[offset++] = hw_val; > + > + if (dptc_params[param].param_id2) { > + ids[offset] = dptc_params[param].param_id2; > + vals[offset++] = hw_val; > + } > + > + return offset; > +} > + > +static int dptc_alib_send_one(int idx, u32 val) > +{ > + u32 vals[2]; > + u8 ids[2]; > + > + return dptc_alib_call(ids, vals, > + dptc_alib_fill_param(ids, vals, 0, idx, val)); > +} > + > +static int dptc_alib_save(struct dptc_priv *dptc) > +{ > + u32 vals[DPTC_NUM_PARAMS * 2]; > + u8 ids[DPTC_NUM_PARAMS * 2]; > + int count = 0; > + int i; > + > + for (i = 0; i < DPTC_NUM_PARAMS; i++) { > + if (!dptc->staged[i]) > + continue; > + count = dptc_alib_fill_param(ids, vals, count, i, > + dptc->staged[i]); > + } > + > + return dptc_alib_call(ids, vals, count); > +} > + > +/* Sysfs callbacks */ > + > +static ssize_t dptc_current_value_show(struct kobject *kobj, > + struct kobj_attribute *attr, char *buf) > +{ > + struct dptc_attr_sysfs *ps = > + container_of(attr, struct dptc_attr_sysfs, current_value); > + struct dptc_priv *dptc = ps->priv; > + > + guard(mutex)(&dptc->lock); > + > + if (!dptc->staged[ps->idx]) > + return sysfs_emit(buf, "\n"); > + return sysfs_emit(buf, "%u\n", dptc->staged[ps->idx]); > +} > + > +static ssize_t dptc_current_value_store(struct kobject *kobj, > + struct kobj_attribute *attr, > + const char *buf, size_t count) > +{ > + struct dptc_attr_sysfs *ps = > + container_of(attr, struct dptc_attr_sysfs, current_value); > + struct dptc_priv *dptc = ps->priv; > + u32 val, min, max; > + int ret; > + > + guard(mutex)(&dptc->lock); > + > + if (count == 1 && buf[0] == '\n') { > + dptc->staged[ps->idx] = 0; > + return count; > + } > + > + ret = kstrtou32(buf, 10, &val); > + if (ret) > + return ret; > + > + min = dptc_get_min(dptc, ps->idx); > + max = dptc_get_max(dptc, ps->idx); > + if (val < min || (max && val > max)) > + return -EINVAL; > + dptc->staged[ps->idx] = val; > + if (dptc->save_mode == SAVE_SINGLE) > + ret = dptc_alib_send_one(ps->idx, val); > + > + return ret ? ret : count; > +} > + > +static ssize_t dptc_default_value_show(struct kobject *kobj, > + struct kobj_attribute *attr, char *buf) > +{ > + struct dptc_attr_sysfs *ps = > + container_of(attr, struct dptc_attr_sysfs, default_value); > + > + return sysfs_emit(buf, "%u\n", dptc_get_default(ps->priv, ps->idx)); > +} > + > +static ssize_t dptc_min_value_show(struct kobject *kobj, > + struct kobj_attribute *attr, char *buf) > +{ > + struct dptc_attr_sysfs *ps = > + container_of(attr, struct dptc_attr_sysfs, min_value); > + struct dptc_priv *dptc = ps->priv; > + > + guard(mutex)(&dptc->lock); > + > + return sysfs_emit(buf, "%u\n", dptc_get_min(dptc, ps->idx)); > +} > + > +static ssize_t dptc_max_value_show(struct kobject *kobj, > + struct kobj_attribute *attr, char *buf) > +{ > + struct dptc_attr_sysfs *ps = > + container_of(attr, struct dptc_attr_sysfs, max_value); > + struct dptc_priv *dptc = ps->priv; > + > + guard(mutex)(&dptc->lock); > + > + return sysfs_emit(buf, "%u\n", dptc_get_max(dptc, ps->idx)); > +} > + > +static ssize_t dptc_scalar_increment_show(struct kobject *kobj, > + struct kobj_attribute *attr, char *buf) > +{ > + return sysfs_emit(buf, "1\n"); > +} > + > +static ssize_t dptc_display_name_show(struct kobject *kobj, > + struct kobj_attribute *attr, char *buf) > +{ > + struct dptc_attr_sysfs *ps = > + container_of(attr, struct dptc_attr_sysfs, display_name); > + return sysfs_emit(buf, "%s\n", dptc_params[ps->idx].display_name); > +} > + > +static ssize_t dptc_type_show(struct kobject *kobj, > + struct kobj_attribute *attr, char *buf) > +{ > + return sysfs_emit(buf, "integer\n"); > +} > + > +static ssize_t dptc_save_settings_show(struct kobject *kobj, > + struct kobj_attribute *attr, char *buf) > +{ > + struct dptc_priv *dptc = > + container_of(attr, struct dptc_priv, save_settings_attr); > + > + guard(mutex)(&dptc->lock); > + > + if (dptc->save_mode == SAVE_SINGLE) > + return sysfs_emit(buf, "single\n"); > + return sysfs_emit(buf, "bulk\n"); > +} > + > +static ssize_t dptc_save_settings_store(struct kobject *kobj, > + struct kobj_attribute *attr, > + const char *buf, size_t count) > +{ > + struct dptc_priv *dptc = > + container_of(attr, struct dptc_priv, save_settings_attr); > + int ret = 0; > + > + guard(mutex)(&dptc->lock); > + > + if (sysfs_streq(buf, "save")) > + ret = dptc_alib_save(dptc); > + else if (sysfs_streq(buf, "single")) > + dptc->save_mode = SAVE_SINGLE; > + else if (sysfs_streq(buf, "bulk")) > + dptc->save_mode = SAVE_BULK; > + else > + return -EINVAL; > + > + return ret ? ret : count; > +} > + > +static ssize_t dptc_expanded_current_value_show(struct kobject *kobj, > + struct kobj_attribute *attr, > + char *buf) > +{ > + struct dptc_attr_sysfs *ps = > + container_of(attr, struct dptc_attr_sysfs, current_value); > + struct dptc_priv *dptc = ps->priv; > + > + guard(mutex)(&dptc->lock); > + > + return sysfs_emit(buf, "%d\n", dptc->expanded); > +} > + > +static ssize_t dptc_expanded_current_value_store(struct kobject *kobj, > + struct kobj_attribute *attr, > + const char *buf, size_t count) > +{ > + struct dptc_attr_sysfs *ps = > + container_of(attr, struct dptc_attr_sysfs, current_value); > + struct dptc_priv *dptc = ps->priv; > + bool val; > + int ret; > + > + ret = kstrtobool(buf, &val); > + if (ret) > + return ret; > + > + guard(mutex)(&dptc->lock); > + > + dptc->expanded = val; > + /* Clear staged values: limits changed, old values may be out of range */ > + memset(dptc->staged, 0, sizeof(dptc->staged)); > + > + return count; > +} > + > +static ssize_t dptc_expanded_default_value_show(struct kobject *kobj, > + struct kobj_attribute *attr, > + char *buf) > +{ > + return sysfs_emit(buf, "0\n"); > +} > + > +static ssize_t dptc_expanded_min_value_show(struct kobject *kobj, > + struct kobj_attribute *attr, > + char *buf) > +{ > + return sysfs_emit(buf, "0\n"); > +} > + > +static ssize_t dptc_expanded_max_value_show(struct kobject *kobj, > + struct kobj_attribute *attr, > + char *buf) > +{ > + return sysfs_emit(buf, "1\n"); > +} > + > +static ssize_t dptc_expanded_scalar_increment_show(struct kobject *kobj, > + struct kobj_attribute *attr, > + char *buf) > +{ > + return sysfs_emit(buf, "1\n"); > +} > + > +static ssize_t dptc_expanded_display_name_show(struct kobject *kobj, > + struct kobj_attribute *attr, > + char *buf) > +{ > + return sysfs_emit(buf, "Expanded Limits\n"); > +} > + > +static ssize_t dptc_expanded_type_show(struct kobject *kobj, > + struct kobj_attribute *attr, char *buf) > +{ > + return sysfs_emit(buf, "integer\n"); > +} > + > +/* Sysfs setup */ > + > +static void dptc_setup_param_sysfs(struct dptc_priv *dptc, > + struct dptc_attr_sysfs *ps, int idx) > +{ > + ps->priv = dptc; > + ps->idx = idx; > + > + sysfs_attr_init(&ps->current_value.attr); > + ps->current_value.attr.name = "current_value"; > + ps->current_value.attr.mode = 0644; > + ps->current_value.show = dptc_current_value_show; > + ps->current_value.store = dptc_current_value_store; > + > + sysfs_attr_init(&ps->default_value.attr); > + ps->default_value.attr.name = "default_value"; > + ps->default_value.attr.mode = 0444; > + ps->default_value.show = dptc_default_value_show; > + > + sysfs_attr_init(&ps->min_value.attr); > + ps->min_value.attr.name = "min_value"; > + ps->min_value.attr.mode = 0444; > + ps->min_value.show = dptc_min_value_show; > + > + sysfs_attr_init(&ps->max_value.attr); > + ps->max_value.attr.name = "max_value"; > + ps->max_value.attr.mode = 0444; > + ps->max_value.show = dptc_max_value_show; > + > + sysfs_attr_init(&ps->scalar_increment.attr); > + ps->scalar_increment.attr.name = "scalar_increment"; > + ps->scalar_increment.attr.mode = 0444; > + ps->scalar_increment.show = dptc_scalar_increment_show; > + > + sysfs_attr_init(&ps->display_name.attr); > + ps->display_name.attr.name = "display_name"; > + ps->display_name.attr.mode = 0444; > + ps->display_name.show = dptc_display_name_show; > + > + sysfs_attr_init(&ps->type.attr); > + ps->type.attr.name = "type"; > + ps->type.attr.mode = 0444; > + ps->type.show = dptc_type_show; > + > + ps->attrs[0] = &ps->current_value.attr; > + ps->attrs[1] = &ps->default_value.attr; > + ps->attrs[2] = &ps->min_value.attr; > + ps->attrs[3] = &ps->max_value.attr; > + ps->attrs[4] = &ps->scalar_increment.attr; > + ps->attrs[5] = &ps->display_name.attr; > + ps->attrs[6] = &ps->type.attr; > + ps->attrs[7] = NULL; > + > + ps->group.name = dptc_params[idx].name; > + ps->group.attrs = ps->attrs; > +} > + > +static void dptc_setup_expanded_sysfs(struct dptc_priv *dptc, > + struct dptc_attr_sysfs *ps) > +{ > + ps->priv = dptc; > + sysfs_attr_init(&ps->current_value.attr); > + ps->current_value.attr.name = "current_value"; > + ps->current_value.attr.mode = 0644; > + ps->current_value.show = dptc_expanded_current_value_show; > + ps->current_value.store = dptc_expanded_current_value_store; > + > + sysfs_attr_init(&ps->default_value.attr); > + ps->default_value.attr.name = "default_value"; > + ps->default_value.attr.mode = 0444; > + ps->default_value.show = dptc_expanded_default_value_show; > + > + sysfs_attr_init(&ps->min_value.attr); > + ps->min_value.attr.name = "min_value"; > + ps->min_value.attr.mode = 0444; > + ps->min_value.show = dptc_expanded_min_value_show; > + > + sysfs_attr_init(&ps->max_value.attr); > + ps->max_value.attr.name = "max_value"; > + ps->max_value.attr.mode = 0444; > + ps->max_value.show = dptc_expanded_max_value_show; > + > + sysfs_attr_init(&ps->scalar_increment.attr); > + ps->scalar_increment.attr.name = "scalar_increment"; > + ps->scalar_increment.attr.mode = 0444; > + ps->scalar_increment.show = dptc_expanded_scalar_increment_show; > + > + sysfs_attr_init(&ps->display_name.attr); > + ps->display_name.attr.name = "display_name"; > + ps->display_name.attr.mode = 0444; > + ps->display_name.show = dptc_expanded_display_name_show; > + > + sysfs_attr_init(&ps->type.attr); > + ps->type.attr.name = "type"; > + ps->type.attr.mode = 0444; > + ps->type.show = dptc_expanded_type_show; > + > + ps->attrs[0] = &ps->current_value.attr; > + ps->attrs[1] = &ps->default_value.attr; > + ps->attrs[2] = &ps->min_value.attr; > + ps->attrs[3] = &ps->max_value.attr; > + ps->attrs[4] = &ps->scalar_increment.attr; > + ps->attrs[5] = &ps->display_name.attr; > + ps->attrs[6] = &ps->type.attr; > + ps->attrs[7] = NULL; > + > + ps->group.name = "expanded_limits"; > + ps->group.attrs = ps->attrs; > +} > + > +static void dptc_fw_dev_unregister(void *data) > +{ > + device_unregister(data); > +} > + > +static void dptc_kset_unregister(void *data) > +{ > + kset_unregister(data); > +} > + > +static int dptc_resume(struct device *dev) > +{ > + struct dptc_priv *dptc = dev_get_drvdata(dev); > + int ret; > + > + guard(mutex)(&dptc->lock); > + > + /* In bulk mode, do not use pm ops for userspace flexibility. */ > + if (dptc->save_mode == SAVE_SINGLE) > + ret = dptc_alib_save(dptc); > + else > + ret = 0; > + > + if (ret && ret != -ENOENT) > + dev_warn(dev, "failed to restore settings on resume: %d\n", ret); > + > + return 0; > +} > + > +static DEFINE_SIMPLE_DEV_PM_OPS(dptc_pm_ops, NULL, dptc_resume); > + > +static int dptc_probe(struct platform_device *pdev) > +{ > + const struct dmi_system_id *dmi_match = dev_get_platdata(&pdev->dev); > + struct device *dev = &pdev->dev; > + struct dptc_priv *dptc; > + int i, ret; > + > + dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL); > + if (!dptc) > + return -ENOMEM; > + > + platform_set_drvdata(pdev, dptc); > + > + ret = devm_mutex_init(dev, &dptc->lock); > + if (ret) > + return ret; > + > + dptc->dev_limits = dmi_match->driver_data; > + dev_info(dev, "%s (%s)\n", dmi_match->ident, > + boot_cpu_data.x86_model_id); > + > + dptc->fw_attr_dev = device_create(&firmware_attributes_class, > + NULL, MKDEV(0, 0), NULL, DRIVER_NAME); > + if (IS_ERR(dptc->fw_attr_dev)) > + return PTR_ERR(dptc->fw_attr_dev); > + > + ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister, > + dptc->fw_attr_dev); > + if (ret) > + return ret; > + > + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL, > + &dptc->fw_attr_dev->kobj); > + if (!dptc->fw_attr_kset) > + return -ENOMEM; > + > + ret = devm_add_action_or_reset(dev, dptc_kset_unregister, > + dptc->fw_attr_kset); > + if (ret) > + return ret; > + > + for (i = 0; i < DPTC_NUM_PARAMS; i++) { > + dptc_setup_param_sysfs(dptc, &dptc->params[i], i); > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj, > + &dptc->params[i].group); > + if (ret) > + return ret; > + } > + > + dptc_setup_expanded_sysfs(dptc, &dptc->expanded_attr); > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj, > + &dptc->expanded_attr.group); > + if (ret) > + return ret; > + > + sysfs_attr_init(&dptc->save_settings_attr.attr); > + dptc->save_settings_attr.attr.name = "save_settings"; > + dptc->save_settings_attr.attr.mode = 0644; > + dptc->save_settings_attr.show = dptc_save_settings_show; > + dptc->save_settings_attr.store = dptc_save_settings_store; > + ret = sysfs_create_file(&dptc->fw_attr_kset->kobj, > + &dptc->save_settings_attr.attr); > + if (ret) > + return ret; > + > + return 0; > +} > + > +static struct platform_driver dptc_driver = { > + .driver = { > + .name = DRIVER_NAME, > + .pm = pm_sleep_ptr(&dptc_pm_ops), > + }, > + .probe = dptc_probe, > +}; > + > +static int __init dptc_init(void) > +{ > + const struct dmi_system_id *match; > + bool soc_found = false; > + int i, ret; > + > + match = dmi_first_match(dptc_dmi_table); > + if (!match) > + return -ENODEV; > + > + if (!acpi_has_method(NULL, ALIB_PATH)) { > + pr_warn("ALIB method not present\n"); > + return -ENODEV; > + } > + > + for (i = 0; dptc_soc_table[i]; i++) { > + if (strstr(boot_cpu_data.x86_model_id, > + dptc_soc_table[i])) { > + soc_found = true; > + break; > + } > + } > + if (!soc_found) { > + pr_warn("unrecognized SoC '%s'\n", > + boot_cpu_data.x86_model_id); > + return -ENODEV; > + } > + > + dptc_pdev = platform_device_register_data(NULL, DRIVER_NAME, -1, > + match, sizeof(*match)); > + if (IS_ERR(dptc_pdev)) > + return PTR_ERR(dptc_pdev); > + > + ret = platform_driver_register(&dptc_driver); > + if (ret) { > + platform_device_unregister(dptc_pdev); > + return ret; > + } > + > + return 0; > +} > + > +static void __exit dptc_exit(void) > +{ > + platform_driver_unregister(&dptc_driver); > + platform_device_unregister(dptc_pdev); > +} > + > +module_init(dptc_init); > +module_exit(dptc_exit); > + > +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>"); > +MODULE_DESCRIPTION("AMD DPTCi ACPI Driver"); > +MODULE_LICENSE("GPL"); ^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [RFC v4 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver 2026-03-10 4:01 ` Mario Limonciello @ 2026-03-10 8:02 ` Antheas Kapenekakis 2026-03-10 16:26 ` Mario Limonciello 0 siblings, 1 reply; 21+ messages in thread From: Antheas Kapenekakis @ 2026-03-10 8:02 UTC (permalink / raw) To: Mario Limonciello Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato, i, linux-kernel, platform-driver-x86 On Tue, 10 Mar 2026 at 05:01, Mario Limonciello <mario.limonciello@amd.com> wrote: > > > > On 3/9/2026 3:51 PM, Antheas Kapenekakis wrote: > > Add a driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and > > Thermal Configuration interface (DPTCi). This exposes TDP and thermal > > parameters for AMD APU-based handheld devices via the > > firmware-attributes sysfs ABI. > > > > Parameters are staged and atomically committed through ALIB. The driver > > supports two save modes: "single" (apply immediately on write) and > > "bulk" (stage values, then commit with "save"). An "expanded_limits" > > toggle widens the allowed parameter ranges beyond device defaults. > > > > Initial device support: GPD Win 5 (AMD Ryzen AI MAX). > > > > Assisted-by: Claude:claude-opus-4-6 > > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev> > > --- > > MAINTAINERS | 6 + > > drivers/platform/x86/amd/Kconfig | 14 + > > drivers/platform/x86/amd/Makefile | 2 + > > drivers/platform/x86/amd/dptc.c | 746 ++++++++++++++++++++++++++++++ > > 4 files changed, 768 insertions(+) > > create mode 100644 drivers/platform/x86/amd/dptc.c > > > > diff --git a/MAINTAINERS b/MAINTAINERS > > index 89007f9ed35e..ebda8e82bf35 100644 > > --- a/MAINTAINERS > > +++ b/MAINTAINERS > > @@ -1103,6 +1103,12 @@ S: Supported > > F: drivers/gpu/drm/amd/display/dc/dml/ > > F: drivers/gpu/drm/amd/display/dc/dml2_0/ > > > > +AMD DPTC DRIVER > > +M: Antheas Kapenekakis <lkml@antheas.dev> > > +L: platform-driver-x86@vger.kernel.org > > +S: Maintained > > +F: drivers/platform/x86/amd/dptc.c > > + > > AMD FAM15H PROCESSOR POWER MONITORING DRIVER > > M: Huang Rui <ray.huang@amd.com> > > L: linux-hwmon@vger.kernel.org > > diff --git a/drivers/platform/x86/amd/Kconfig b/drivers/platform/x86/amd/Kconfig > > index b813f9265368..d610092467fc 100644 > > --- a/drivers/platform/x86/amd/Kconfig > > +++ b/drivers/platform/x86/amd/Kconfig > > @@ -44,3 +44,17 @@ config AMD_ISP_PLATFORM > > > > This driver can also be built as a module. If so, the module > > will be called amd_isp4. > > + > > +config AMD_DPTC > > + tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)" > > + depends on X86_64 && ACPI && DMI > > + select FIRMWARE_ATTRIBUTES_CLASS > > + help > > + Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and > > + Thermal Configuration Interface (DPTCi). Exposes TDP and thermal > > + parameters for AMD APU-based handheld devices via the > > + firmware-attributes sysfs ABI, allowing userspace tools to stage > > + and atomically commit power limit settings. Requires a DMI match > > + for the device and a recognized AMD SoC. > > + > > + If built as a module, the module will be called amd_dptc. > > diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile > > index f6ff0c837f34..862a609bfe38 100644 > > --- a/drivers/platform/x86/amd/Makefile > > +++ b/drivers/platform/x86/amd/Makefile > > @@ -12,3 +12,5 @@ obj-$(CONFIG_AMD_PMF) += pmf/ > > obj-$(CONFIG_AMD_WBRF) += wbrf.o > > obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o > > obj-$(CONFIG_AMD_HFI) += hfi/ > > +obj-$(CONFIG_AMD_DPTC) += amd_dptc.o > > +amd_dptc-y := dptc.o > > diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c > > new file mode 100644 > > index 000000000000..b884cdfa3f82 > > --- /dev/null > > +++ b/drivers/platform/x86/amd/dptc.c > > @@ -0,0 +1,746 @@ > > +// SPDX-License-Identifier: GPL-2.0-only > > +/* > > + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver > > + * > > + * Exposes AMD APU power and thermal parameters via the firmware-attributes > > + * sysfs ABI. Parameters are staged and atomically committed through the > > + * AGESA ALIB Function 0x0C (Dynamic Power and Thermal Configuration > > + * interface). > > + * > > + * Reference: AMD AGESA Publication #44065, Appendix E.5 > > + * https://docs.amd.com/v/u/en-US/44065_Arch2008 > > + * > > + * Copyright (C) 2026 Antheas Kapenekakis <lkml@antheas.dev> > > + */ > > + > > +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt > > + > > +#include <linux/acpi.h> > > +#include <linux/cleanup.h> > > +#include <linux/dmi.h> > > +#include <linux/init.h> > > +#include <linux/kobject.h> > > +#include <linux/module.h> > > +#include <linux/mutex.h> > > +#include <linux/platform_device.h> > > +#include <linux/processor.h> > > +#include <linux/sysfs.h> > > +#include <linux/unaligned.h> > > + > > +#include "../firmware_attributes_class.h" > > + > > +#define DRIVER_NAME "amd-dptc" > > + > > +#define ALIB_FUNC_DPTC 0x0C > > +#define ALIB_PATH "\\_SB.ALIB" > > + > > +#define ALIB_ID_TEMP_TARGET 0x03 > > +#define ALIB_ID_STAPM_LIMIT 0x05 > > +#define ALIB_ID_FAST_LIMIT 0x06 > > +#define ALIB_ID_SLOW_LIMIT 0x07 > > +#define ALIB_ID_SKIN_LIMIT 0x2E > > + > > +enum dptc_param_idx { > > + DPTC_PPT_PL1_SPL, /* STAPM + skin limit (set together) */ > > + DPTC_PPT_PL2_SPPT, /* slow PPT limit */ > > + DPTC_PPT_PL3_FPPT, /* fast PPT limit */ > > + DPTC_CPU_TEMP, /* thermal control target */ > > + DPTC_NUM_PARAMS, > > +}; > > + > > +struct dptc_param_limits { > > + u32 expanded_min; > > + u32 device_min; > > + u32 def; > > + u32 device_max; > > + u32 expanded_max; > > +}; > > + > > +struct dptc_device_limits { > > + struct dptc_param_limits params[DPTC_NUM_PARAMS]; > > +}; > > + > > +struct dptc_param_desc { > > + const char *name; > > + const char *display_name; > > + u16 scale; /* sysfs-to-ALIB multiplier (e.g. 1000 for W->mW) */ > > + u8 param_id; > > + u8 param_id2; /* secondary ALIB ID, 0 if none */ > > +}; > > + > > +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = { > > + [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (W)", > > + 1000, ALIB_ID_STAPM_LIMIT, > > + ALIB_ID_SKIN_LIMIT }, > > + [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (W)", > > + 1000, ALIB_ID_SLOW_LIMIT }, > > + [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (W)", > > + 1000, ALIB_ID_FAST_LIMIT }, > > + [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)", > > + 1, ALIB_ID_TEMP_TARGET }, > > +}; > > + > > +/* AI MAX Handheld class: GPD Win 5 */ > > +static const struct dptc_device_limits limits_maxhh = { > > + .params = { > > + [DPTC_PPT_PL1_SPL] = { 1, 4, 25, 80, 100 }, > > + [DPTC_PPT_PL2_SPPT] = { 1, 4, 27, 82, 100 }, > > + [DPTC_PPT_PL3_FPPT] = { 1, 4, 40, 85, 100 }, > > + [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 }, > > + }, > > +}; > > + > > +/* Substring matches against boot_cpu_data.x86_model_id; order matters. */ > > +static const char * const dptc_soc_table[] = { > > + /* AI MAX */ > > + "AMD RYZEN AI MAX+ 395", > > + "AMD RYZEN AI MAX+ 385", > > + "AMD RYZEN AI MAX 380", > > + NULL, > > +}; > > I feel like I commented this before; but I don't really understand the > purpose of this table. > > If you have a system that is quirked already, why would you need to > cross reference this table? Yes, and I addressed it on a reply. It is an additional safety feature. A lot of these devices ship different SoC SKUs with the same DMI. Most of them actually. But it is not needed beyond that, it can be removed. I will defer to you. > > + > > +static const struct dmi_system_id dptc_dmi_table[] = { > > + /* GPD */ > > + { > > + .ident = "GPD Win 5", > > + .matches = { > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"), > > + }, > > + .driver_data = (void *)&limits_maxhh, > > + }, > > + { } > > +}; > > +MODULE_DEVICE_TABLE(dmi, dptc_dmi_table); > > + > > +enum dptc_save_mode { SAVE_SINGLE, SAVE_BULK }; > > + > > +struct dptc_priv; > > + > > +struct dptc_attr_sysfs { > > + struct dptc_priv *priv; > > + struct kobj_attribute current_value; > > + struct kobj_attribute default_value; > > + struct kobj_attribute min_value; > > + struct kobj_attribute max_value; > > + struct kobj_attribute scalar_increment; > > + struct kobj_attribute display_name; > > + struct kobj_attribute type; > > + struct attribute *attrs[8]; > > + struct attribute_group group; > > + int idx; > > +}; > > + > > +struct dptc_priv { > > + struct device *fw_attr_dev; > > + struct kset *fw_attr_kset; > > + > > + const struct dptc_device_limits *dev_limits; > > + > > + bool expanded; > > + > > + enum dptc_save_mode save_mode; > > + > > + u32 staged[DPTC_NUM_PARAMS]; > > + > > + /* Protects staged, expanded, and save_mode */ > > + struct mutex lock; > > + > > + struct dptc_attr_sysfs params[DPTC_NUM_PARAMS]; > > + struct dptc_attr_sysfs expanded_attr; > > + struct kobj_attribute save_settings_attr; > > +}; > > + > > +static struct platform_device *dptc_pdev; > > + > > +static u32 dptc_get_min(struct dptc_priv *dptc, int idx) > > +{ > > + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_min > > + : dptc->dev_limits->params[idx].device_min; > > +} > > + > > +static u32 dptc_get_max(struct dptc_priv *dptc, int idx) > > +{ > > + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_max > > + : dptc->dev_limits->params[idx].device_max; > > +} > > + > > +static u32 dptc_get_default(struct dptc_priv *dptc, int idx) > > +{ > > + return dptc->dev_limits->params[idx].def; > > +} > > + > > +static int dptc_alib_call(const u8 *ids, const u32 *vals, int count) > > +{ > > + union acpi_object in_params[2]; > > + struct acpi_object_list input; > > + acpi_status status; > > + u32 buf_size; > > + int i, off; > > + u8 *buf; > > + > > + if (count == 0) > > + return -ENOENT; > > + > > + /* Buffer layout: WORD total_size + count * (BYTE id + DWORD value) */ > > + buf_size = 2 + count * 5; > > + buf = kzalloc(buf_size, GFP_KERNEL); > > + if (!buf) > > + return -ENOMEM; > > + > > + put_unaligned_le16(buf_size, buf); > > + > > + for (i = 0; i < count; i++) { > > + off = 2 + i * 5; > > + buf[off] = ids[i]; > > + put_unaligned_le32(vals[i], &buf[off + 1]); > > + } > > + > > + in_params[0].type = ACPI_TYPE_INTEGER; > > + in_params[0].integer.value = ALIB_FUNC_DPTC; > > + in_params[1].type = ACPI_TYPE_BUFFER; > > + in_params[1].buffer.length = buf_size; > > + in_params[1].buffer.pointer = buf; > > + > > + input.count = 2; > > + input.pointer = in_params; > > + > > + status = acpi_evaluate_object(NULL, ALIB_PATH, &input, NULL); > > + kfree(buf); > > + > > + if (ACPI_FAILURE(status)) { > > + pr_err("ALIB call failed: %s\n", > > + acpi_format_exception(status)); > > + return -EIO; > > + } > > + > > + pr_debug("sent %d ALIB parameter(s)\n", count); > > + return 0; > > +} > > + > > +static int dptc_alib_fill_param(u8 *ids, u32 *vals, int offset, > > + enum dptc_param_idx param, u32 val) > > +{ > > + u32 hw_val = val * dptc_params[param].scale; > > + > > + ids[offset] = dptc_params[param].param_id; > > + vals[offset++] = hw_val; > > + > > + if (dptc_params[param].param_id2) { > > + ids[offset] = dptc_params[param].param_id2; > > + vals[offset++] = hw_val; > > + } > > + > > + return offset; > > +} > > + > > +static int dptc_alib_send_one(int idx, u32 val) > > +{ > > + u32 vals[2]; > > + u8 ids[2]; > > + > > + return dptc_alib_call(ids, vals, > > + dptc_alib_fill_param(ids, vals, 0, idx, val)); > > +} > > + > > +static int dptc_alib_save(struct dptc_priv *dptc) > > +{ > > + u32 vals[DPTC_NUM_PARAMS * 2]; > > + u8 ids[DPTC_NUM_PARAMS * 2]; > > + int count = 0; > > + int i; > > + > > + for (i = 0; i < DPTC_NUM_PARAMS; i++) { > > + if (!dptc->staged[i]) > > + continue; > > + count = dptc_alib_fill_param(ids, vals, count, i, > > + dptc->staged[i]); > > + } > > + > > + return dptc_alib_call(ids, vals, count); > > +} > > + > > +/* Sysfs callbacks */ > > + > > +static ssize_t dptc_current_value_show(struct kobject *kobj, > > + struct kobj_attribute *attr, char *buf) > > +{ > > + struct dptc_attr_sysfs *ps = > > + container_of(attr, struct dptc_attr_sysfs, current_value); > > + struct dptc_priv *dptc = ps->priv; > > + > > + guard(mutex)(&dptc->lock); > > + > > + if (!dptc->staged[ps->idx]) > > + return sysfs_emit(buf, "\n"); > > + return sysfs_emit(buf, "%u\n", dptc->staged[ps->idx]); > > +} > > + > > +static ssize_t dptc_current_value_store(struct kobject *kobj, > > + struct kobj_attribute *attr, > > + const char *buf, size_t count) > > +{ > > + struct dptc_attr_sysfs *ps = > > + container_of(attr, struct dptc_attr_sysfs, current_value); > > + struct dptc_priv *dptc = ps->priv; > > + u32 val, min, max; > > + int ret; > > + > > + guard(mutex)(&dptc->lock); > > + > > + if (count == 1 && buf[0] == '\n') { > > + dptc->staged[ps->idx] = 0; > > + return count; > > + } > > + > > + ret = kstrtou32(buf, 10, &val); > > + if (ret) > > + return ret; > > + > > + min = dptc_get_min(dptc, ps->idx); > > + max = dptc_get_max(dptc, ps->idx); > > + if (val < min || (max && val > max)) > > + return -EINVAL; > > + dptc->staged[ps->idx] = val; > > + if (dptc->save_mode == SAVE_SINGLE) > > + ret = dptc_alib_send_one(ps->idx, val); > > + > > + return ret ? ret : count; > > +} > > + > > +static ssize_t dptc_default_value_show(struct kobject *kobj, > > + struct kobj_attribute *attr, char *buf) > > +{ > > + struct dptc_attr_sysfs *ps = > > + container_of(attr, struct dptc_attr_sysfs, default_value); > > + > > + return sysfs_emit(buf, "%u\n", dptc_get_default(ps->priv, ps->idx)); > > +} > > + > > +static ssize_t dptc_min_value_show(struct kobject *kobj, > > + struct kobj_attribute *attr, char *buf) > > +{ > > + struct dptc_attr_sysfs *ps = > > + container_of(attr, struct dptc_attr_sysfs, min_value); > > + struct dptc_priv *dptc = ps->priv; > > + > > + guard(mutex)(&dptc->lock); > > + > > + return sysfs_emit(buf, "%u\n", dptc_get_min(dptc, ps->idx)); > > +} > > + > > +static ssize_t dptc_max_value_show(struct kobject *kobj, > > + struct kobj_attribute *attr, char *buf) > > +{ > > + struct dptc_attr_sysfs *ps = > > + container_of(attr, struct dptc_attr_sysfs, max_value); > > + struct dptc_priv *dptc = ps->priv; > > + > > + guard(mutex)(&dptc->lock); > > + > > + return sysfs_emit(buf, "%u\n", dptc_get_max(dptc, ps->idx)); > > +} > > + > > +static ssize_t dptc_scalar_increment_show(struct kobject *kobj, > > + struct kobj_attribute *attr, char *buf) > > +{ > > + return sysfs_emit(buf, "1\n"); > > +} > > + > > +static ssize_t dptc_display_name_show(struct kobject *kobj, > > + struct kobj_attribute *attr, char *buf) > > +{ > > + struct dptc_attr_sysfs *ps = > > + container_of(attr, struct dptc_attr_sysfs, display_name); > > + return sysfs_emit(buf, "%s\n", dptc_params[ps->idx].display_name); > > +} > > + > > +static ssize_t dptc_type_show(struct kobject *kobj, > > + struct kobj_attribute *attr, char *buf) > > +{ > > + return sysfs_emit(buf, "integer\n"); > > +} > > + > > +static ssize_t dptc_save_settings_show(struct kobject *kobj, > > + struct kobj_attribute *attr, char *buf) > > +{ > > + struct dptc_priv *dptc = > > + container_of(attr, struct dptc_priv, save_settings_attr); > > + > > + guard(mutex)(&dptc->lock); > > + > > + if (dptc->save_mode == SAVE_SINGLE) > > + return sysfs_emit(buf, "single\n"); > > + return sysfs_emit(buf, "bulk\n"); > > +} > > + > > +static ssize_t dptc_save_settings_store(struct kobject *kobj, > > + struct kobj_attribute *attr, > > + const char *buf, size_t count) > > +{ > > + struct dptc_priv *dptc = > > + container_of(attr, struct dptc_priv, save_settings_attr); > > + int ret = 0; > > + > > + guard(mutex)(&dptc->lock); > > + > > + if (sysfs_streq(buf, "save")) > > + ret = dptc_alib_save(dptc); > > + else if (sysfs_streq(buf, "single")) > > + dptc->save_mode = SAVE_SINGLE; > > + else if (sysfs_streq(buf, "bulk")) > > + dptc->save_mode = SAVE_BULK; > > + else > > + return -EINVAL; > > + > > + return ret ? ret : count; > > +} > > + > > +static ssize_t dptc_expanded_current_value_show(struct kobject *kobj, > > + struct kobj_attribute *attr, > > + char *buf) > > +{ > > + struct dptc_attr_sysfs *ps = > > + container_of(attr, struct dptc_attr_sysfs, current_value); > > + struct dptc_priv *dptc = ps->priv; > > + > > + guard(mutex)(&dptc->lock); > > + > > + return sysfs_emit(buf, "%d\n", dptc->expanded); > > +} > > + > > +static ssize_t dptc_expanded_current_value_store(struct kobject *kobj, > > + struct kobj_attribute *attr, > > + const char *buf, size_t count) > > +{ > > + struct dptc_attr_sysfs *ps = > > + container_of(attr, struct dptc_attr_sysfs, current_value); > > + struct dptc_priv *dptc = ps->priv; > > + bool val; > > + int ret; > > + > > + ret = kstrtobool(buf, &val); > > + if (ret) > > + return ret; > > + > > + guard(mutex)(&dptc->lock); > > + > > + dptc->expanded = val; > > + /* Clear staged values: limits changed, old values may be out of range */ > > + memset(dptc->staged, 0, sizeof(dptc->staged)); > > + > > + return count; > > +} > > + > > +static ssize_t dptc_expanded_default_value_show(struct kobject *kobj, > > + struct kobj_attribute *attr, > > + char *buf) > > +{ > > + return sysfs_emit(buf, "0\n"); > > +} > > + > > +static ssize_t dptc_expanded_min_value_show(struct kobject *kobj, > > + struct kobj_attribute *attr, > > + char *buf) > > +{ > > + return sysfs_emit(buf, "0\n"); > > +} > > + > > +static ssize_t dptc_expanded_max_value_show(struct kobject *kobj, > > + struct kobj_attribute *attr, > > + char *buf) > > +{ > > + return sysfs_emit(buf, "1\n"); > > +} > > + > > +static ssize_t dptc_expanded_scalar_increment_show(struct kobject *kobj, > > + struct kobj_attribute *attr, > > + char *buf) > > +{ > > + return sysfs_emit(buf, "1\n"); > > +} > > + > > +static ssize_t dptc_expanded_display_name_show(struct kobject *kobj, > > + struct kobj_attribute *attr, > > + char *buf) > > +{ > > + return sysfs_emit(buf, "Expanded Limits\n"); > > +} > > + > > +static ssize_t dptc_expanded_type_show(struct kobject *kobj, > > + struct kobj_attribute *attr, char *buf) > > +{ > > + return sysfs_emit(buf, "integer\n"); > > +} > > + > > +/* Sysfs setup */ > > + > > +static void dptc_setup_param_sysfs(struct dptc_priv *dptc, > > + struct dptc_attr_sysfs *ps, int idx) > > +{ > > + ps->priv = dptc; > > + ps->idx = idx; > > + > > + sysfs_attr_init(&ps->current_value.attr); > > + ps->current_value.attr.name = "current_value"; > > + ps->current_value.attr.mode = 0644; > > + ps->current_value.show = dptc_current_value_show; > > + ps->current_value.store = dptc_current_value_store; > > + > > + sysfs_attr_init(&ps->default_value.attr); > > + ps->default_value.attr.name = "default_value"; > > + ps->default_value.attr.mode = 0444; > > + ps->default_value.show = dptc_default_value_show; > > + > > + sysfs_attr_init(&ps->min_value.attr); > > + ps->min_value.attr.name = "min_value"; > > + ps->min_value.attr.mode = 0444; > > + ps->min_value.show = dptc_min_value_show; > > + > > + sysfs_attr_init(&ps->max_value.attr); > > + ps->max_value.attr.name = "max_value"; > > + ps->max_value.attr.mode = 0444; > > + ps->max_value.show = dptc_max_value_show; > > + > > + sysfs_attr_init(&ps->scalar_increment.attr); > > + ps->scalar_increment.attr.name = "scalar_increment"; > > + ps->scalar_increment.attr.mode = 0444; > > + ps->scalar_increment.show = dptc_scalar_increment_show; > > + > > + sysfs_attr_init(&ps->display_name.attr); > > + ps->display_name.attr.name = "display_name"; > > + ps->display_name.attr.mode = 0444; > > + ps->display_name.show = dptc_display_name_show; > > + > > + sysfs_attr_init(&ps->type.attr); > > + ps->type.attr.name = "type"; > > + ps->type.attr.mode = 0444; > > + ps->type.show = dptc_type_show; > > + > > + ps->attrs[0] = &ps->current_value.attr; > > + ps->attrs[1] = &ps->default_value.attr; > > + ps->attrs[2] = &ps->min_value.attr; > > + ps->attrs[3] = &ps->max_value.attr; > > + ps->attrs[4] = &ps->scalar_increment.attr; > > + ps->attrs[5] = &ps->display_name.attr; > > + ps->attrs[6] = &ps->type.attr; > > + ps->attrs[7] = NULL; > > + > > + ps->group.name = dptc_params[idx].name; > > + ps->group.attrs = ps->attrs; > > +} > > + > > +static void dptc_setup_expanded_sysfs(struct dptc_priv *dptc, > > + struct dptc_attr_sysfs *ps) > > +{ > > + ps->priv = dptc; > > + sysfs_attr_init(&ps->current_value.attr); > > + ps->current_value.attr.name = "current_value"; > > + ps->current_value.attr.mode = 0644; > > + ps->current_value.show = dptc_expanded_current_value_show; > > + ps->current_value.store = dptc_expanded_current_value_store; > > + > > + sysfs_attr_init(&ps->default_value.attr); > > + ps->default_value.attr.name = "default_value"; > > + ps->default_value.attr.mode = 0444; > > + ps->default_value.show = dptc_expanded_default_value_show; > > + > > + sysfs_attr_init(&ps->min_value.attr); > > + ps->min_value.attr.name = "min_value"; > > + ps->min_value.attr.mode = 0444; > > + ps->min_value.show = dptc_expanded_min_value_show; > > + > > + sysfs_attr_init(&ps->max_value.attr); > > + ps->max_value.attr.name = "max_value"; > > + ps->max_value.attr.mode = 0444; > > + ps->max_value.show = dptc_expanded_max_value_show; > > + > > + sysfs_attr_init(&ps->scalar_increment.attr); > > + ps->scalar_increment.attr.name = "scalar_increment"; > > + ps->scalar_increment.attr.mode = 0444; > > + ps->scalar_increment.show = dptc_expanded_scalar_increment_show; > > + > > + sysfs_attr_init(&ps->display_name.attr); > > + ps->display_name.attr.name = "display_name"; > > + ps->display_name.attr.mode = 0444; > > + ps->display_name.show = dptc_expanded_display_name_show; > > + > > + sysfs_attr_init(&ps->type.attr); > > + ps->type.attr.name = "type"; > > + ps->type.attr.mode = 0444; > > + ps->type.show = dptc_expanded_type_show; > > + > > + ps->attrs[0] = &ps->current_value.attr; > > + ps->attrs[1] = &ps->default_value.attr; > > + ps->attrs[2] = &ps->min_value.attr; > > + ps->attrs[3] = &ps->max_value.attr; > > + ps->attrs[4] = &ps->scalar_increment.attr; > > + ps->attrs[5] = &ps->display_name.attr; > > + ps->attrs[6] = &ps->type.attr; > > + ps->attrs[7] = NULL; > > + > > + ps->group.name = "expanded_limits"; > > + ps->group.attrs = ps->attrs; > > +} > > + > > +static void dptc_fw_dev_unregister(void *data) > > +{ > > + device_unregister(data); > > +} > > + > > +static void dptc_kset_unregister(void *data) > > +{ > > + kset_unregister(data); > > +} > > + > > +static int dptc_resume(struct device *dev) > > +{ > > + struct dptc_priv *dptc = dev_get_drvdata(dev); > > + int ret; > > + > > + guard(mutex)(&dptc->lock); > > + > > + /* In bulk mode, do not use pm ops for userspace flexibility. */ > > + if (dptc->save_mode == SAVE_SINGLE) > > + ret = dptc_alib_save(dptc); > > + else > > + ret = 0; > > + > > + if (ret && ret != -ENOENT) > > + dev_warn(dev, "failed to restore settings on resume: %d\n", ret); > > + > > + return 0; > > +} > > + > > +static DEFINE_SIMPLE_DEV_PM_OPS(dptc_pm_ops, NULL, dptc_resume); > > + > > +static int dptc_probe(struct platform_device *pdev) > > +{ > > + const struct dmi_system_id *dmi_match = dev_get_platdata(&pdev->dev); > > + struct device *dev = &pdev->dev; > > + struct dptc_priv *dptc; > > + int i, ret; > > + > > + dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL); > > + if (!dptc) > > + return -ENOMEM; > > + > > + platform_set_drvdata(pdev, dptc); > > + > > + ret = devm_mutex_init(dev, &dptc->lock); > > + if (ret) > > + return ret; > > + > > + dptc->dev_limits = dmi_match->driver_data; > > + dev_info(dev, "%s (%s)\n", dmi_match->ident, > > + boot_cpu_data.x86_model_id); > > + > > + dptc->fw_attr_dev = device_create(&firmware_attributes_class, > > + NULL, MKDEV(0, 0), NULL, DRIVER_NAME); > > + if (IS_ERR(dptc->fw_attr_dev)) > > + return PTR_ERR(dptc->fw_attr_dev); > > + > > + ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister, > > + dptc->fw_attr_dev); > > + if (ret) > > + return ret; > > + > > + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL, > > + &dptc->fw_attr_dev->kobj); > > + if (!dptc->fw_attr_kset) > > + return -ENOMEM; > > + > > + ret = devm_add_action_or_reset(dev, dptc_kset_unregister, > > + dptc->fw_attr_kset); > > + if (ret) > > + return ret; > > + > > + for (i = 0; i < DPTC_NUM_PARAMS; i++) { > > + dptc_setup_param_sysfs(dptc, &dptc->params[i], i); > > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj, > > + &dptc->params[i].group); > > + if (ret) > > + return ret; > > + } > > + > > + dptc_setup_expanded_sysfs(dptc, &dptc->expanded_attr); > > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj, > > + &dptc->expanded_attr.group); > > + if (ret) > > + return ret; > > + > > + sysfs_attr_init(&dptc->save_settings_attr.attr); > > + dptc->save_settings_attr.attr.name = "save_settings"; > > + dptc->save_settings_attr.attr.mode = 0644; > > + dptc->save_settings_attr.show = dptc_save_settings_show; > > + dptc->save_settings_attr.store = dptc_save_settings_store; > > + ret = sysfs_create_file(&dptc->fw_attr_kset->kobj, > > + &dptc->save_settings_attr.attr); > > + if (ret) > > + return ret; > > + > > + return 0; > > +} > > + > > +static struct platform_driver dptc_driver = { > > + .driver = { > > + .name = DRIVER_NAME, > > + .pm = pm_sleep_ptr(&dptc_pm_ops), > > + }, > > + .probe = dptc_probe, > > +}; > > + > > +static int __init dptc_init(void) > > +{ > > + const struct dmi_system_id *match; > > + bool soc_found = false; > > + int i, ret; > > + > > + match = dmi_first_match(dptc_dmi_table); > > + if (!match) > > + return -ENODEV; > > + > > + if (!acpi_has_method(NULL, ALIB_PATH)) { > > + pr_warn("ALIB method not present\n"); > > + return -ENODEV; > > + } > > + > > + for (i = 0; dptc_soc_table[i]; i++) { > > + if (strstr(boot_cpu_data.x86_model_id, > > + dptc_soc_table[i])) { > > + soc_found = true; > > + break; > > + } > > + } > > + if (!soc_found) { > > + pr_warn("unrecognized SoC '%s'\n", > > + boot_cpu_data.x86_model_id); > > + return -ENODEV; > > + } > > + > > + dptc_pdev = platform_device_register_data(NULL, DRIVER_NAME, -1, > > + match, sizeof(*match)); > > + if (IS_ERR(dptc_pdev)) > > + return PTR_ERR(dptc_pdev); > > + > > + ret = platform_driver_register(&dptc_driver); > > + if (ret) { > > + platform_device_unregister(dptc_pdev); > > + return ret; > > + } > > + > > + return 0; > > +} > > + > > +static void __exit dptc_exit(void) > > +{ > > + platform_driver_unregister(&dptc_driver); > > + platform_device_unregister(dptc_pdev); > > +} > > + > > +module_init(dptc_init); > > +module_exit(dptc_exit); > > + > > +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>"); > > +MODULE_DESCRIPTION("AMD DPTCi ACPI Driver"); > > +MODULE_LICENSE("GPL"); > > ^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [RFC v4 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver 2026-03-10 8:02 ` Antheas Kapenekakis @ 2026-03-10 16:26 ` Mario Limonciello 2026-03-11 19:09 ` Antheas Kapenekakis 0 siblings, 1 reply; 21+ messages in thread From: Mario Limonciello @ 2026-03-10 16:26 UTC (permalink / raw) To: Antheas Kapenekakis Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato, i, linux-kernel, platform-driver-x86 On 3/10/26 3:02 AM, Antheas Kapenekakis wrote: > On Tue, 10 Mar 2026 at 05:01, Mario Limonciello > <mario.limonciello@amd.com> wrote: >> >> >> >> On 3/9/2026 3:51 PM, Antheas Kapenekakis wrote: >>> Add a driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and >>> Thermal Configuration interface (DPTCi). This exposes TDP and thermal >>> parameters for AMD APU-based handheld devices via the >>> firmware-attributes sysfs ABI. >>> >>> Parameters are staged and atomically committed through ALIB. The driver >>> supports two save modes: "single" (apply immediately on write) and >>> "bulk" (stage values, then commit with "save"). An "expanded_limits" >>> toggle widens the allowed parameter ranges beyond device defaults. >>> >>> Initial device support: GPD Win 5 (AMD Ryzen AI MAX). >>> >>> Assisted-by: Claude:claude-opus-4-6 >>> Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev> >>> --- >>> MAINTAINERS | 6 + >>> drivers/platform/x86/amd/Kconfig | 14 + >>> drivers/platform/x86/amd/Makefile | 2 + >>> drivers/platform/x86/amd/dptc.c | 746 ++++++++++++++++++++++++++++++ >>> 4 files changed, 768 insertions(+) >>> create mode 100644 drivers/platform/x86/amd/dptc.c >>> >>> diff --git a/MAINTAINERS b/MAINTAINERS >>> index 89007f9ed35e..ebda8e82bf35 100644 >>> --- a/MAINTAINERS >>> +++ b/MAINTAINERS >>> @@ -1103,6 +1103,12 @@ S: Supported >>> F: drivers/gpu/drm/amd/display/dc/dml/ >>> F: drivers/gpu/drm/amd/display/dc/dml2_0/ >>> >>> +AMD DPTC DRIVER >>> +M: Antheas Kapenekakis <lkml@antheas.dev> >>> +L: platform-driver-x86@vger.kernel.org >>> +S: Maintained >>> +F: drivers/platform/x86/amd/dptc.c >>> + >>> AMD FAM15H PROCESSOR POWER MONITORING DRIVER >>> M: Huang Rui <ray.huang@amd.com> >>> L: linux-hwmon@vger.kernel.org >>> diff --git a/drivers/platform/x86/amd/Kconfig b/drivers/platform/x86/amd/Kconfig >>> index b813f9265368..d610092467fc 100644 >>> --- a/drivers/platform/x86/amd/Kconfig >>> +++ b/drivers/platform/x86/amd/Kconfig >>> @@ -44,3 +44,17 @@ config AMD_ISP_PLATFORM >>> >>> This driver can also be built as a module. If so, the module >>> will be called amd_isp4. >>> + >>> +config AMD_DPTC >>> + tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)" >>> + depends on X86_64 && ACPI && DMI >>> + select FIRMWARE_ATTRIBUTES_CLASS >>> + help >>> + Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and >>> + Thermal Configuration Interface (DPTCi). Exposes TDP and thermal >>> + parameters for AMD APU-based handheld devices via the >>> + firmware-attributes sysfs ABI, allowing userspace tools to stage >>> + and atomically commit power limit settings. Requires a DMI match >>> + for the device and a recognized AMD SoC. >>> + >>> + If built as a module, the module will be called amd_dptc. >>> diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile >>> index f6ff0c837f34..862a609bfe38 100644 >>> --- a/drivers/platform/x86/amd/Makefile >>> +++ b/drivers/platform/x86/amd/Makefile >>> @@ -12,3 +12,5 @@ obj-$(CONFIG_AMD_PMF) += pmf/ >>> obj-$(CONFIG_AMD_WBRF) += wbrf.o >>> obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o >>> obj-$(CONFIG_AMD_HFI) += hfi/ >>> +obj-$(CONFIG_AMD_DPTC) += amd_dptc.o >>> +amd_dptc-y := dptc.o >>> diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c >>> new file mode 100644 >>> index 000000000000..b884cdfa3f82 >>> --- /dev/null >>> +++ b/drivers/platform/x86/amd/dptc.c >>> @@ -0,0 +1,746 @@ >>> +// SPDX-License-Identifier: GPL-2.0-only >>> +/* >>> + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver >>> + * >>> + * Exposes AMD APU power and thermal parameters via the firmware-attributes >>> + * sysfs ABI. Parameters are staged and atomically committed through the >>> + * AGESA ALIB Function 0x0C (Dynamic Power and Thermal Configuration >>> + * interface). >>> + * >>> + * Reference: AMD AGESA Publication #44065, Appendix E.5 >>> + * https://docs.amd.com/v/u/en-US/44065_Arch2008 >>> + * >>> + * Copyright (C) 2026 Antheas Kapenekakis <lkml@antheas.dev> >>> + */ >>> + >>> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt >>> + >>> +#include <linux/acpi.h> >>> +#include <linux/cleanup.h> >>> +#include <linux/dmi.h> >>> +#include <linux/init.h> >>> +#include <linux/kobject.h> >>> +#include <linux/module.h> >>> +#include <linux/mutex.h> >>> +#include <linux/platform_device.h> >>> +#include <linux/processor.h> >>> +#include <linux/sysfs.h> >>> +#include <linux/unaligned.h> >>> + >>> +#include "../firmware_attributes_class.h" >>> + >>> +#define DRIVER_NAME "amd-dptc" >>> + >>> +#define ALIB_FUNC_DPTC 0x0C >>> +#define ALIB_PATH "\\_SB.ALIB" >>> + >>> +#define ALIB_ID_TEMP_TARGET 0x03 >>> +#define ALIB_ID_STAPM_LIMIT 0x05 >>> +#define ALIB_ID_FAST_LIMIT 0x06 >>> +#define ALIB_ID_SLOW_LIMIT 0x07 >>> +#define ALIB_ID_SKIN_LIMIT 0x2E >>> + >>> +enum dptc_param_idx { >>> + DPTC_PPT_PL1_SPL, /* STAPM + skin limit (set together) */ >>> + DPTC_PPT_PL2_SPPT, /* slow PPT limit */ >>> + DPTC_PPT_PL3_FPPT, /* fast PPT limit */ >>> + DPTC_CPU_TEMP, /* thermal control target */ >>> + DPTC_NUM_PARAMS, >>> +}; >>> + >>> +struct dptc_param_limits { >>> + u32 expanded_min; >>> + u32 device_min; >>> + u32 def; >>> + u32 device_max; >>> + u32 expanded_max; >>> +}; >>> + >>> +struct dptc_device_limits { >>> + struct dptc_param_limits params[DPTC_NUM_PARAMS]; >>> +}; >>> + >>> +struct dptc_param_desc { >>> + const char *name; >>> + const char *display_name; >>> + u16 scale; /* sysfs-to-ALIB multiplier (e.g. 1000 for W->mW) */ >>> + u8 param_id; >>> + u8 param_id2; /* secondary ALIB ID, 0 if none */ >>> +}; >>> + >>> +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = { >>> + [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (W)", >>> + 1000, ALIB_ID_STAPM_LIMIT, >>> + ALIB_ID_SKIN_LIMIT }, >>> + [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (W)", >>> + 1000, ALIB_ID_SLOW_LIMIT }, >>> + [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (W)", >>> + 1000, ALIB_ID_FAST_LIMIT }, >>> + [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)", >>> + 1, ALIB_ID_TEMP_TARGET }, >>> +}; >>> + >>> +/* AI MAX Handheld class: GPD Win 5 */ >>> +static const struct dptc_device_limits limits_maxhh = { >>> + .params = { >>> + [DPTC_PPT_PL1_SPL] = { 1, 4, 25, 80, 100 }, >>> + [DPTC_PPT_PL2_SPPT] = { 1, 4, 27, 82, 100 }, >>> + [DPTC_PPT_PL3_FPPT] = { 1, 4, 40, 85, 100 }, >>> + [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 }, >>> + }, >>> +}; >>> + >>> +/* Substring matches against boot_cpu_data.x86_model_id; order matters. */ >>> +static const char * const dptc_soc_table[] = { >>> + /* AI MAX */ >>> + "AMD RYZEN AI MAX+ 395", >>> + "AMD RYZEN AI MAX+ 385", >>> + "AMD RYZEN AI MAX 380", >>> + NULL, >>> +}; >> >> I feel like I commented this before; but I don't really understand the >> purpose of this table. >> >> If you have a system that is quirked already, why would you need to >> cross reference this table? > > Yes, and I addressed it on a reply. > > It is an additional safety feature. A lot of these devices ship > different SoC SKUs with the same DMI. Most of them actually. But it is > not needed beyond that, it can be removed. I will defer to you. > Exact same SMBIOS? There are a lot of fields that can be keyed off. Can you share a dmidecode from two such systems? > >>> + >>> +static const struct dmi_system_id dptc_dmi_table[] = { >>> + /* GPD */ >>> + { >>> + .ident = "GPD Win 5", >>> + .matches = { >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"), >>> + }, >>> + .driver_data = (void *)&limits_maxhh, >>> + }, >>> + { } >>> +}; >>> +MODULE_DEVICE_TABLE(dmi, dptc_dmi_table); >>> + >>> +enum dptc_save_mode { SAVE_SINGLE, SAVE_BULK }; >>> + >>> +struct dptc_priv; >>> + >>> +struct dptc_attr_sysfs { >>> + struct dptc_priv *priv; >>> + struct kobj_attribute current_value; >>> + struct kobj_attribute default_value; >>> + struct kobj_attribute min_value; >>> + struct kobj_attribute max_value; >>> + struct kobj_attribute scalar_increment; >>> + struct kobj_attribute display_name; >>> + struct kobj_attribute type; >>> + struct attribute *attrs[8]; >>> + struct attribute_group group; >>> + int idx; >>> +}; >>> + >>> +struct dptc_priv { >>> + struct device *fw_attr_dev; >>> + struct kset *fw_attr_kset; >>> + >>> + const struct dptc_device_limits *dev_limits; >>> + >>> + bool expanded; >>> + >>> + enum dptc_save_mode save_mode; >>> + >>> + u32 staged[DPTC_NUM_PARAMS]; >>> + >>> + /* Protects staged, expanded, and save_mode */ >>> + struct mutex lock; >>> + >>> + struct dptc_attr_sysfs params[DPTC_NUM_PARAMS]; >>> + struct dptc_attr_sysfs expanded_attr; >>> + struct kobj_attribute save_settings_attr; >>> +}; >>> + >>> +static struct platform_device *dptc_pdev; >>> + >>> +static u32 dptc_get_min(struct dptc_priv *dptc, int idx) >>> +{ >>> + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_min >>> + : dptc->dev_limits->params[idx].device_min; >>> +} >>> + >>> +static u32 dptc_get_max(struct dptc_priv *dptc, int idx) >>> +{ >>> + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_max >>> + : dptc->dev_limits->params[idx].device_max; >>> +} >>> + >>> +static u32 dptc_get_default(struct dptc_priv *dptc, int idx) >>> +{ >>> + return dptc->dev_limits->params[idx].def; >>> +} >>> + >>> +static int dptc_alib_call(const u8 *ids, const u32 *vals, int count) >>> +{ >>> + union acpi_object in_params[2]; >>> + struct acpi_object_list input; >>> + acpi_status status; >>> + u32 buf_size; >>> + int i, off; >>> + u8 *buf; >>> + >>> + if (count == 0) >>> + return -ENOENT; >>> + >>> + /* Buffer layout: WORD total_size + count * (BYTE id + DWORD value) */ >>> + buf_size = 2 + count * 5; >>> + buf = kzalloc(buf_size, GFP_KERNEL); >>> + if (!buf) >>> + return -ENOMEM; >>> + >>> + put_unaligned_le16(buf_size, buf); >>> + >>> + for (i = 0; i < count; i++) { >>> + off = 2 + i * 5; >>> + buf[off] = ids[i]; >>> + put_unaligned_le32(vals[i], &buf[off + 1]); >>> + } >>> + >>> + in_params[0].type = ACPI_TYPE_INTEGER; >>> + in_params[0].integer.value = ALIB_FUNC_DPTC; >>> + in_params[1].type = ACPI_TYPE_BUFFER; >>> + in_params[1].buffer.length = buf_size; >>> + in_params[1].buffer.pointer = buf; >>> + >>> + input.count = 2; >>> + input.pointer = in_params; >>> + >>> + status = acpi_evaluate_object(NULL, ALIB_PATH, &input, NULL); >>> + kfree(buf); >>> + >>> + if (ACPI_FAILURE(status)) { >>> + pr_err("ALIB call failed: %s\n", >>> + acpi_format_exception(status)); >>> + return -EIO; >>> + } >>> + >>> + pr_debug("sent %d ALIB parameter(s)\n", count); >>> + return 0; >>> +} >>> + >>> +static int dptc_alib_fill_param(u8 *ids, u32 *vals, int offset, >>> + enum dptc_param_idx param, u32 val) >>> +{ >>> + u32 hw_val = val * dptc_params[param].scale; >>> + >>> + ids[offset] = dptc_params[param].param_id; >>> + vals[offset++] = hw_val; >>> + >>> + if (dptc_params[param].param_id2) { >>> + ids[offset] = dptc_params[param].param_id2; >>> + vals[offset++] = hw_val; >>> + } >>> + >>> + return offset; >>> +} >>> + >>> +static int dptc_alib_send_one(int idx, u32 val) >>> +{ >>> + u32 vals[2]; >>> + u8 ids[2]; >>> + >>> + return dptc_alib_call(ids, vals, >>> + dptc_alib_fill_param(ids, vals, 0, idx, val)); >>> +} >>> + >>> +static int dptc_alib_save(struct dptc_priv *dptc) >>> +{ >>> + u32 vals[DPTC_NUM_PARAMS * 2]; >>> + u8 ids[DPTC_NUM_PARAMS * 2]; >>> + int count = 0; >>> + int i; >>> + >>> + for (i = 0; i < DPTC_NUM_PARAMS; i++) { >>> + if (!dptc->staged[i]) >>> + continue; >>> + count = dptc_alib_fill_param(ids, vals, count, i, >>> + dptc->staged[i]); >>> + } >>> + >>> + return dptc_alib_call(ids, vals, count); >>> +} >>> + >>> +/* Sysfs callbacks */ >>> + >>> +static ssize_t dptc_current_value_show(struct kobject *kobj, >>> + struct kobj_attribute *attr, char *buf) >>> +{ >>> + struct dptc_attr_sysfs *ps = >>> + container_of(attr, struct dptc_attr_sysfs, current_value); >>> + struct dptc_priv *dptc = ps->priv; >>> + >>> + guard(mutex)(&dptc->lock); >>> + >>> + if (!dptc->staged[ps->idx]) >>> + return sysfs_emit(buf, "\n"); >>> + return sysfs_emit(buf, "%u\n", dptc->staged[ps->idx]); >>> +} >>> + >>> +static ssize_t dptc_current_value_store(struct kobject *kobj, >>> + struct kobj_attribute *attr, >>> + const char *buf, size_t count) >>> +{ >>> + struct dptc_attr_sysfs *ps = >>> + container_of(attr, struct dptc_attr_sysfs, current_value); >>> + struct dptc_priv *dptc = ps->priv; >>> + u32 val, min, max; >>> + int ret; >>> + >>> + guard(mutex)(&dptc->lock); >>> + >>> + if (count == 1 && buf[0] == '\n') { >>> + dptc->staged[ps->idx] = 0; >>> + return count; >>> + } >>> + >>> + ret = kstrtou32(buf, 10, &val); >>> + if (ret) >>> + return ret; >>> + >>> + min = dptc_get_min(dptc, ps->idx); >>> + max = dptc_get_max(dptc, ps->idx); >>> + if (val < min || (max && val > max)) >>> + return -EINVAL; >>> + dptc->staged[ps->idx] = val; >>> + if (dptc->save_mode == SAVE_SINGLE) >>> + ret = dptc_alib_send_one(ps->idx, val); >>> + >>> + return ret ? ret : count; >>> +} >>> + >>> +static ssize_t dptc_default_value_show(struct kobject *kobj, >>> + struct kobj_attribute *attr, char *buf) >>> +{ >>> + struct dptc_attr_sysfs *ps = >>> + container_of(attr, struct dptc_attr_sysfs, default_value); >>> + >>> + return sysfs_emit(buf, "%u\n", dptc_get_default(ps->priv, ps->idx)); >>> +} >>> + >>> +static ssize_t dptc_min_value_show(struct kobject *kobj, >>> + struct kobj_attribute *attr, char *buf) >>> +{ >>> + struct dptc_attr_sysfs *ps = >>> + container_of(attr, struct dptc_attr_sysfs, min_value); >>> + struct dptc_priv *dptc = ps->priv; >>> + >>> + guard(mutex)(&dptc->lock); >>> + >>> + return sysfs_emit(buf, "%u\n", dptc_get_min(dptc, ps->idx)); >>> +} >>> + >>> +static ssize_t dptc_max_value_show(struct kobject *kobj, >>> + struct kobj_attribute *attr, char *buf) >>> +{ >>> + struct dptc_attr_sysfs *ps = >>> + container_of(attr, struct dptc_attr_sysfs, max_value); >>> + struct dptc_priv *dptc = ps->priv; >>> + >>> + guard(mutex)(&dptc->lock); >>> + >>> + return sysfs_emit(buf, "%u\n", dptc_get_max(dptc, ps->idx)); >>> +} >>> + >>> +static ssize_t dptc_scalar_increment_show(struct kobject *kobj, >>> + struct kobj_attribute *attr, char *buf) >>> +{ >>> + return sysfs_emit(buf, "1\n"); >>> +} >>> + >>> +static ssize_t dptc_display_name_show(struct kobject *kobj, >>> + struct kobj_attribute *attr, char *buf) >>> +{ >>> + struct dptc_attr_sysfs *ps = >>> + container_of(attr, struct dptc_attr_sysfs, display_name); >>> + return sysfs_emit(buf, "%s\n", dptc_params[ps->idx].display_name); >>> +} >>> + >>> +static ssize_t dptc_type_show(struct kobject *kobj, >>> + struct kobj_attribute *attr, char *buf) >>> +{ >>> + return sysfs_emit(buf, "integer\n"); >>> +} >>> + >>> +static ssize_t dptc_save_settings_show(struct kobject *kobj, >>> + struct kobj_attribute *attr, char *buf) >>> +{ >>> + struct dptc_priv *dptc = >>> + container_of(attr, struct dptc_priv, save_settings_attr); >>> + >>> + guard(mutex)(&dptc->lock); >>> + >>> + if (dptc->save_mode == SAVE_SINGLE) >>> + return sysfs_emit(buf, "single\n"); >>> + return sysfs_emit(buf, "bulk\n"); >>> +} >>> + >>> +static ssize_t dptc_save_settings_store(struct kobject *kobj, >>> + struct kobj_attribute *attr, >>> + const char *buf, size_t count) >>> +{ >>> + struct dptc_priv *dptc = >>> + container_of(attr, struct dptc_priv, save_settings_attr); >>> + int ret = 0; >>> + >>> + guard(mutex)(&dptc->lock); >>> + >>> + if (sysfs_streq(buf, "save")) >>> + ret = dptc_alib_save(dptc); >>> + else if (sysfs_streq(buf, "single")) >>> + dptc->save_mode = SAVE_SINGLE; >>> + else if (sysfs_streq(buf, "bulk")) >>> + dptc->save_mode = SAVE_BULK; >>> + else >>> + return -EINVAL; >>> + >>> + return ret ? ret : count; >>> +} >>> + >>> +static ssize_t dptc_expanded_current_value_show(struct kobject *kobj, >>> + struct kobj_attribute *attr, >>> + char *buf) >>> +{ >>> + struct dptc_attr_sysfs *ps = >>> + container_of(attr, struct dptc_attr_sysfs, current_value); >>> + struct dptc_priv *dptc = ps->priv; >>> + >>> + guard(mutex)(&dptc->lock); >>> + >>> + return sysfs_emit(buf, "%d\n", dptc->expanded); >>> +} >>> + >>> +static ssize_t dptc_expanded_current_value_store(struct kobject *kobj, >>> + struct kobj_attribute *attr, >>> + const char *buf, size_t count) >>> +{ >>> + struct dptc_attr_sysfs *ps = >>> + container_of(attr, struct dptc_attr_sysfs, current_value); >>> + struct dptc_priv *dptc = ps->priv; >>> + bool val; >>> + int ret; >>> + >>> + ret = kstrtobool(buf, &val); >>> + if (ret) >>> + return ret; >>> + >>> + guard(mutex)(&dptc->lock); >>> + >>> + dptc->expanded = val; >>> + /* Clear staged values: limits changed, old values may be out of range */ >>> + memset(dptc->staged, 0, sizeof(dptc->staged)); >>> + >>> + return count; >>> +} >>> + >>> +static ssize_t dptc_expanded_default_value_show(struct kobject *kobj, >>> + struct kobj_attribute *attr, >>> + char *buf) >>> +{ >>> + return sysfs_emit(buf, "0\n"); >>> +} >>> + >>> +static ssize_t dptc_expanded_min_value_show(struct kobject *kobj, >>> + struct kobj_attribute *attr, >>> + char *buf) >>> +{ >>> + return sysfs_emit(buf, "0\n"); >>> +} >>> + >>> +static ssize_t dptc_expanded_max_value_show(struct kobject *kobj, >>> + struct kobj_attribute *attr, >>> + char *buf) >>> +{ >>> + return sysfs_emit(buf, "1\n"); >>> +} >>> + >>> +static ssize_t dptc_expanded_scalar_increment_show(struct kobject *kobj, >>> + struct kobj_attribute *attr, >>> + char *buf) >>> +{ >>> + return sysfs_emit(buf, "1\n"); >>> +} >>> + >>> +static ssize_t dptc_expanded_display_name_show(struct kobject *kobj, >>> + struct kobj_attribute *attr, >>> + char *buf) >>> +{ >>> + return sysfs_emit(buf, "Expanded Limits\n"); >>> +} >>> + >>> +static ssize_t dptc_expanded_type_show(struct kobject *kobj, >>> + struct kobj_attribute *attr, char *buf) >>> +{ >>> + return sysfs_emit(buf, "integer\n"); >>> +} >>> + >>> +/* Sysfs setup */ >>> + >>> +static void dptc_setup_param_sysfs(struct dptc_priv *dptc, >>> + struct dptc_attr_sysfs *ps, int idx) >>> +{ >>> + ps->priv = dptc; >>> + ps->idx = idx; >>> + >>> + sysfs_attr_init(&ps->current_value.attr); >>> + ps->current_value.attr.name = "current_value"; >>> + ps->current_value.attr.mode = 0644; >>> + ps->current_value.show = dptc_current_value_show; >>> + ps->current_value.store = dptc_current_value_store; >>> + >>> + sysfs_attr_init(&ps->default_value.attr); >>> + ps->default_value.attr.name = "default_value"; >>> + ps->default_value.attr.mode = 0444; >>> + ps->default_value.show = dptc_default_value_show; >>> + >>> + sysfs_attr_init(&ps->min_value.attr); >>> + ps->min_value.attr.name = "min_value"; >>> + ps->min_value.attr.mode = 0444; >>> + ps->min_value.show = dptc_min_value_show; >>> + >>> + sysfs_attr_init(&ps->max_value.attr); >>> + ps->max_value.attr.name = "max_value"; >>> + ps->max_value.attr.mode = 0444; >>> + ps->max_value.show = dptc_max_value_show; >>> + >>> + sysfs_attr_init(&ps->scalar_increment.attr); >>> + ps->scalar_increment.attr.name = "scalar_increment"; >>> + ps->scalar_increment.attr.mode = 0444; >>> + ps->scalar_increment.show = dptc_scalar_increment_show; >>> + >>> + sysfs_attr_init(&ps->display_name.attr); >>> + ps->display_name.attr.name = "display_name"; >>> + ps->display_name.attr.mode = 0444; >>> + ps->display_name.show = dptc_display_name_show; >>> + >>> + sysfs_attr_init(&ps->type.attr); >>> + ps->type.attr.name = "type"; >>> + ps->type.attr.mode = 0444; >>> + ps->type.show = dptc_type_show; >>> + >>> + ps->attrs[0] = &ps->current_value.attr; >>> + ps->attrs[1] = &ps->default_value.attr; >>> + ps->attrs[2] = &ps->min_value.attr; >>> + ps->attrs[3] = &ps->max_value.attr; >>> + ps->attrs[4] = &ps->scalar_increment.attr; >>> + ps->attrs[5] = &ps->display_name.attr; >>> + ps->attrs[6] = &ps->type.attr; >>> + ps->attrs[7] = NULL; >>> + >>> + ps->group.name = dptc_params[idx].name; >>> + ps->group.attrs = ps->attrs; >>> +} >>> + >>> +static void dptc_setup_expanded_sysfs(struct dptc_priv *dptc, >>> + struct dptc_attr_sysfs *ps) >>> +{ >>> + ps->priv = dptc; >>> + sysfs_attr_init(&ps->current_value.attr); >>> + ps->current_value.attr.name = "current_value"; >>> + ps->current_value.attr.mode = 0644; >>> + ps->current_value.show = dptc_expanded_current_value_show; >>> + ps->current_value.store = dptc_expanded_current_value_store; >>> + >>> + sysfs_attr_init(&ps->default_value.attr); >>> + ps->default_value.attr.name = "default_value"; >>> + ps->default_value.attr.mode = 0444; >>> + ps->default_value.show = dptc_expanded_default_value_show; >>> + >>> + sysfs_attr_init(&ps->min_value.attr); >>> + ps->min_value.attr.name = "min_value"; >>> + ps->min_value.attr.mode = 0444; >>> + ps->min_value.show = dptc_expanded_min_value_show; >>> + >>> + sysfs_attr_init(&ps->max_value.attr); >>> + ps->max_value.attr.name = "max_value"; >>> + ps->max_value.attr.mode = 0444; >>> + ps->max_value.show = dptc_expanded_max_value_show; >>> + >>> + sysfs_attr_init(&ps->scalar_increment.attr); >>> + ps->scalar_increment.attr.name = "scalar_increment"; >>> + ps->scalar_increment.attr.mode = 0444; >>> + ps->scalar_increment.show = dptc_expanded_scalar_increment_show; >>> + >>> + sysfs_attr_init(&ps->display_name.attr); >>> + ps->display_name.attr.name = "display_name"; >>> + ps->display_name.attr.mode = 0444; >>> + ps->display_name.show = dptc_expanded_display_name_show; >>> + >>> + sysfs_attr_init(&ps->type.attr); >>> + ps->type.attr.name = "type"; >>> + ps->type.attr.mode = 0444; >>> + ps->type.show = dptc_expanded_type_show; >>> + >>> + ps->attrs[0] = &ps->current_value.attr; >>> + ps->attrs[1] = &ps->default_value.attr; >>> + ps->attrs[2] = &ps->min_value.attr; >>> + ps->attrs[3] = &ps->max_value.attr; >>> + ps->attrs[4] = &ps->scalar_increment.attr; >>> + ps->attrs[5] = &ps->display_name.attr; >>> + ps->attrs[6] = &ps->type.attr; >>> + ps->attrs[7] = NULL; >>> + >>> + ps->group.name = "expanded_limits"; >>> + ps->group.attrs = ps->attrs; >>> +} >>> + >>> +static void dptc_fw_dev_unregister(void *data) >>> +{ >>> + device_unregister(data); >>> +} >>> + >>> +static void dptc_kset_unregister(void *data) >>> +{ >>> + kset_unregister(data); >>> +} >>> + >>> +static int dptc_resume(struct device *dev) >>> +{ >>> + struct dptc_priv *dptc = dev_get_drvdata(dev); >>> + int ret; >>> + >>> + guard(mutex)(&dptc->lock); >>> + >>> + /* In bulk mode, do not use pm ops for userspace flexibility. */ >>> + if (dptc->save_mode == SAVE_SINGLE) >>> + ret = dptc_alib_save(dptc); >>> + else >>> + ret = 0; >>> + >>> + if (ret && ret != -ENOENT) >>> + dev_warn(dev, "failed to restore settings on resume: %d\n", ret); >>> + >>> + return 0; >>> +} >>> + >>> +static DEFINE_SIMPLE_DEV_PM_OPS(dptc_pm_ops, NULL, dptc_resume); >>> + >>> +static int dptc_probe(struct platform_device *pdev) >>> +{ >>> + const struct dmi_system_id *dmi_match = dev_get_platdata(&pdev->dev); >>> + struct device *dev = &pdev->dev; >>> + struct dptc_priv *dptc; >>> + int i, ret; >>> + >>> + dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL); >>> + if (!dptc) >>> + return -ENOMEM; >>> + >>> + platform_set_drvdata(pdev, dptc); >>> + >>> + ret = devm_mutex_init(dev, &dptc->lock); >>> + if (ret) >>> + return ret; >>> + >>> + dptc->dev_limits = dmi_match->driver_data; >>> + dev_info(dev, "%s (%s)\n", dmi_match->ident, >>> + boot_cpu_data.x86_model_id); >>> + >>> + dptc->fw_attr_dev = device_create(&firmware_attributes_class, >>> + NULL, MKDEV(0, 0), NULL, DRIVER_NAME); >>> + if (IS_ERR(dptc->fw_attr_dev)) >>> + return PTR_ERR(dptc->fw_attr_dev); >>> + >>> + ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister, >>> + dptc->fw_attr_dev); >>> + if (ret) >>> + return ret; >>> + >>> + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL, >>> + &dptc->fw_attr_dev->kobj); >>> + if (!dptc->fw_attr_kset) >>> + return -ENOMEM; >>> + >>> + ret = devm_add_action_or_reset(dev, dptc_kset_unregister, >>> + dptc->fw_attr_kset); >>> + if (ret) >>> + return ret; >>> + >>> + for (i = 0; i < DPTC_NUM_PARAMS; i++) { >>> + dptc_setup_param_sysfs(dptc, &dptc->params[i], i); >>> + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj, >>> + &dptc->params[i].group); >>> + if (ret) >>> + return ret; >>> + } >>> + >>> + dptc_setup_expanded_sysfs(dptc, &dptc->expanded_attr); >>> + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj, >>> + &dptc->expanded_attr.group); >>> + if (ret) >>> + return ret; >>> + >>> + sysfs_attr_init(&dptc->save_settings_attr.attr); >>> + dptc->save_settings_attr.attr.name = "save_settings"; >>> + dptc->save_settings_attr.attr.mode = 0644; >>> + dptc->save_settings_attr.show = dptc_save_settings_show; >>> + dptc->save_settings_attr.store = dptc_save_settings_store; >>> + ret = sysfs_create_file(&dptc->fw_attr_kset->kobj, >>> + &dptc->save_settings_attr.attr); >>> + if (ret) >>> + return ret; >>> + >>> + return 0; >>> +} >>> + >>> +static struct platform_driver dptc_driver = { >>> + .driver = { >>> + .name = DRIVER_NAME, >>> + .pm = pm_sleep_ptr(&dptc_pm_ops), >>> + }, >>> + .probe = dptc_probe, >>> +}; >>> + >>> +static int __init dptc_init(void) >>> +{ >>> + const struct dmi_system_id *match; >>> + bool soc_found = false; >>> + int i, ret; >>> + >>> + match = dmi_first_match(dptc_dmi_table); >>> + if (!match) >>> + return -ENODEV; >>> + >>> + if (!acpi_has_method(NULL, ALIB_PATH)) { >>> + pr_warn("ALIB method not present\n"); >>> + return -ENODEV; >>> + } >>> + >>> + for (i = 0; dptc_soc_table[i]; i++) { >>> + if (strstr(boot_cpu_data.x86_model_id, >>> + dptc_soc_table[i])) { >>> + soc_found = true; >>> + break; >>> + } >>> + } >>> + if (!soc_found) { >>> + pr_warn("unrecognized SoC '%s'\n", >>> + boot_cpu_data.x86_model_id); >>> + return -ENODEV; >>> + } >>> + >>> + dptc_pdev = platform_device_register_data(NULL, DRIVER_NAME, -1, >>> + match, sizeof(*match)); >>> + if (IS_ERR(dptc_pdev)) >>> + return PTR_ERR(dptc_pdev); >>> + >>> + ret = platform_driver_register(&dptc_driver); >>> + if (ret) { >>> + platform_device_unregister(dptc_pdev); >>> + return ret; >>> + } >>> + >>> + return 0; >>> +} >>> + >>> +static void __exit dptc_exit(void) >>> +{ >>> + platform_driver_unregister(&dptc_driver); >>> + platform_device_unregister(dptc_pdev); >>> +} >>> + >>> +module_init(dptc_init); >>> +module_exit(dptc_exit); >>> + >>> +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>"); >>> +MODULE_DESCRIPTION("AMD DPTCi ACPI Driver"); >>> +MODULE_LICENSE("GPL"); >> >> > ^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [RFC v4 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver 2026-03-10 16:26 ` Mario Limonciello @ 2026-03-11 19:09 ` Antheas Kapenekakis 2026-03-12 13:24 ` Mario Limonciello 0 siblings, 1 reply; 21+ messages in thread From: Antheas Kapenekakis @ 2026-03-11 19:09 UTC (permalink / raw) To: Mario Limonciello Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato, i, linux-kernel, platform-driver-x86 On Tue, 10 Mar 2026 at 17:26, Mario Limonciello <mario.limonciello@amd.com> wrote: > > On 3/10/26 3:02 AM, Antheas Kapenekakis wrote: > > On Tue, 10 Mar 2026 at 05:01, Mario Limonciello > > <mario.limonciello@amd.com> wrote: > >> > >> > >> > >> On 3/9/2026 3:51 PM, Antheas Kapenekakis wrote: > >>> Add a driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and > >>> Thermal Configuration interface (DPTCi). This exposes TDP and thermal > >>> parameters for AMD APU-based handheld devices via the > >>> firmware-attributes sysfs ABI. > >>> > >>> Parameters are staged and atomically committed through ALIB. The driver > >>> supports two save modes: "single" (apply immediately on write) and > >>> "bulk" (stage values, then commit with "save"). An "expanded_limits" > >>> toggle widens the allowed parameter ranges beyond device defaults. > >>> > >>> Initial device support: GPD Win 5 (AMD Ryzen AI MAX). > >>> > >>> Assisted-by: Claude:claude-opus-4-6 > >>> Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev> > >>> --- > >>> MAINTAINERS | 6 + > >>> drivers/platform/x86/amd/Kconfig | 14 + > >>> drivers/platform/x86/amd/Makefile | 2 + > >>> drivers/platform/x86/amd/dptc.c | 746 ++++++++++++++++++++++++++++++ > >>> 4 files changed, 768 insertions(+) > >>> create mode 100644 drivers/platform/x86/amd/dptc.c > >>> > >>> diff --git a/MAINTAINERS b/MAINTAINERS > >>> index 89007f9ed35e..ebda8e82bf35 100644 > >>> --- a/MAINTAINERS > >>> +++ b/MAINTAINERS > >>> @@ -1103,6 +1103,12 @@ S: Supported > >>> F: drivers/gpu/drm/amd/display/dc/dml/ > >>> F: drivers/gpu/drm/amd/display/dc/dml2_0/ > >>> > >>> +AMD DPTC DRIVER > >>> +M: Antheas Kapenekakis <lkml@antheas.dev> > >>> +L: platform-driver-x86@vger.kernel.org > >>> +S: Maintained > >>> +F: drivers/platform/x86/amd/dptc.c > >>> + > >>> AMD FAM15H PROCESSOR POWER MONITORING DRIVER > >>> M: Huang Rui <ray.huang@amd.com> > >>> L: linux-hwmon@vger.kernel.org > >>> diff --git a/drivers/platform/x86/amd/Kconfig b/drivers/platform/x86/amd/Kconfig > >>> index b813f9265368..d610092467fc 100644 > >>> --- a/drivers/platform/x86/amd/Kconfig > >>> +++ b/drivers/platform/x86/amd/Kconfig > >>> @@ -44,3 +44,17 @@ config AMD_ISP_PLATFORM > >>> > >>> This driver can also be built as a module. If so, the module > >>> will be called amd_isp4. > >>> + > >>> +config AMD_DPTC > >>> + tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)" > >>> + depends on X86_64 && ACPI && DMI > >>> + select FIRMWARE_ATTRIBUTES_CLASS > >>> + help > >>> + Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and > >>> + Thermal Configuration Interface (DPTCi). Exposes TDP and thermal > >>> + parameters for AMD APU-based handheld devices via the > >>> + firmware-attributes sysfs ABI, allowing userspace tools to stage > >>> + and atomically commit power limit settings. Requires a DMI match > >>> + for the device and a recognized AMD SoC. > >>> + > >>> + If built as a module, the module will be called amd_dptc. > >>> diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile > >>> index f6ff0c837f34..862a609bfe38 100644 > >>> --- a/drivers/platform/x86/amd/Makefile > >>> +++ b/drivers/platform/x86/amd/Makefile > >>> @@ -12,3 +12,5 @@ obj-$(CONFIG_AMD_PMF) += pmf/ > >>> obj-$(CONFIG_AMD_WBRF) += wbrf.o > >>> obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o > >>> obj-$(CONFIG_AMD_HFI) += hfi/ > >>> +obj-$(CONFIG_AMD_DPTC) += amd_dptc.o > >>> +amd_dptc-y := dptc.o > >>> diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c > >>> new file mode 100644 > >>> index 000000000000..b884cdfa3f82 > >>> --- /dev/null > >>> +++ b/drivers/platform/x86/amd/dptc.c > >>> @@ -0,0 +1,746 @@ > >>> +// SPDX-License-Identifier: GPL-2.0-only > >>> +/* > >>> + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver > >>> + * > >>> + * Exposes AMD APU power and thermal parameters via the firmware-attributes > >>> + * sysfs ABI. Parameters are staged and atomically committed through the > >>> + * AGESA ALIB Function 0x0C (Dynamic Power and Thermal Configuration > >>> + * interface). > >>> + * > >>> + * Reference: AMD AGESA Publication #44065, Appendix E.5 > >>> + * https://docs.amd.com/v/u/en-US/44065_Arch2008 > >>> + * > >>> + * Copyright (C) 2026 Antheas Kapenekakis <lkml@antheas.dev> > >>> + */ > >>> + > >>> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt > >>> + > >>> +#include <linux/acpi.h> > >>> +#include <linux/cleanup.h> > >>> +#include <linux/dmi.h> > >>> +#include <linux/init.h> > >>> +#include <linux/kobject.h> > >>> +#include <linux/module.h> > >>> +#include <linux/mutex.h> > >>> +#include <linux/platform_device.h> > >>> +#include <linux/processor.h> > >>> +#include <linux/sysfs.h> > >>> +#include <linux/unaligned.h> > >>> + > >>> +#include "../firmware_attributes_class.h" > >>> + > >>> +#define DRIVER_NAME "amd-dptc" > >>> + > >>> +#define ALIB_FUNC_DPTC 0x0C > >>> +#define ALIB_PATH "\\_SB.ALIB" > >>> + > >>> +#define ALIB_ID_TEMP_TARGET 0x03 > >>> +#define ALIB_ID_STAPM_LIMIT 0x05 > >>> +#define ALIB_ID_FAST_LIMIT 0x06 > >>> +#define ALIB_ID_SLOW_LIMIT 0x07 > >>> +#define ALIB_ID_SKIN_LIMIT 0x2E > >>> + > >>> +enum dptc_param_idx { > >>> + DPTC_PPT_PL1_SPL, /* STAPM + skin limit (set together) */ > >>> + DPTC_PPT_PL2_SPPT, /* slow PPT limit */ > >>> + DPTC_PPT_PL3_FPPT, /* fast PPT limit */ > >>> + DPTC_CPU_TEMP, /* thermal control target */ > >>> + DPTC_NUM_PARAMS, > >>> +}; > >>> + > >>> +struct dptc_param_limits { > >>> + u32 expanded_min; > >>> + u32 device_min; > >>> + u32 def; > >>> + u32 device_max; > >>> + u32 expanded_max; > >>> +}; > >>> + > >>> +struct dptc_device_limits { > >>> + struct dptc_param_limits params[DPTC_NUM_PARAMS]; > >>> +}; > >>> + > >>> +struct dptc_param_desc { > >>> + const char *name; > >>> + const char *display_name; > >>> + u16 scale; /* sysfs-to-ALIB multiplier (e.g. 1000 for W->mW) */ > >>> + u8 param_id; > >>> + u8 param_id2; /* secondary ALIB ID, 0 if none */ > >>> +}; > >>> + > >>> +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = { > >>> + [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (W)", > >>> + 1000, ALIB_ID_STAPM_LIMIT, > >>> + ALIB_ID_SKIN_LIMIT }, > >>> + [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (W)", > >>> + 1000, ALIB_ID_SLOW_LIMIT }, > >>> + [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (W)", > >>> + 1000, ALIB_ID_FAST_LIMIT }, > >>> + [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)", > >>> + 1, ALIB_ID_TEMP_TARGET }, > >>> +}; > >>> + > >>> +/* AI MAX Handheld class: GPD Win 5 */ > >>> +static const struct dptc_device_limits limits_maxhh = { > >>> + .params = { > >>> + [DPTC_PPT_PL1_SPL] = { 1, 4, 25, 80, 100 }, > >>> + [DPTC_PPT_PL2_SPPT] = { 1, 4, 27, 82, 100 }, > >>> + [DPTC_PPT_PL3_FPPT] = { 1, 4, 40, 85, 100 }, > >>> + [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 }, > >>> + }, > >>> +}; > >>> + > >>> +/* Substring matches against boot_cpu_data.x86_model_id; order matters. */ > >>> +static const char * const dptc_soc_table[] = { > >>> + /* AI MAX */ > >>> + "AMD RYZEN AI MAX+ 395", > >>> + "AMD RYZEN AI MAX+ 385", > >>> + "AMD RYZEN AI MAX 380", > >>> + NULL, > >>> +}; > >> > >> I feel like I commented this before; but I don't really understand the > >> purpose of this table. > >> > >> If you have a system that is quirked already, why would you need to > >> cross reference this table? > > > > Yes, and I addressed it on a reply. > > > > It is an additional safety feature. A lot of these devices ship > > different SoC SKUs with the same DMI. Most of them actually. But it is > > not needed beyond that, it can be removed. I will defer to you. > > > > Exact same SMBIOS? There are a lot of fields that can be keyed off. > Can you share a dmidecode from two such systems? Ah, it's difficult to verify currently, I don't have two spares and varies per manufacturer. What I can say for now is that it varies per manufactuer. E.g., GPD only updates their DMI data only for major revisions. For example, all GPD Win 4s, 3 years of device generations, have the same DMI (6800/ 7840/ 8840u, hx 370). Same for the Win Minis, but for the 2025 generation GPD switched the ODM for the controller motherboard and I assume more, so that one. However, the thermal differences are minimal so its appropriate to use the same values for all. All chips can handle more, the chassis is the limitation. Onexplayer typically changes DMI per generation, but within the same generation they can release multiple chip variants. It is unclear to me if they use the same BIOS capsule. Ayaneo is also very similar. All Ayaneo 3s are named Ayaneo 3. I have an 8840u but that device also came out with an HX 370. Antheas > > > >>> + > >>> +static const struct dmi_system_id dptc_dmi_table[] = { > >>> + /* GPD */ > >>> + { > >>> + .ident = "GPD Win 5", > >>> + .matches = { > >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"), > >>> + }, > >>> + .driver_data = (void *)&limits_maxhh, > >>> + }, > >>> + { } > >>> +}; > >>> +MODULE_DEVICE_TABLE(dmi, dptc_dmi_table); > >>> + > >>> +enum dptc_save_mode { SAVE_SINGLE, SAVE_BULK }; > >>> + > >>> +struct dptc_priv; > >>> + > >>> +struct dptc_attr_sysfs { > >>> + struct dptc_priv *priv; > >>> + struct kobj_attribute current_value; > >>> + struct kobj_attribute default_value; > >>> + struct kobj_attribute min_value; > >>> + struct kobj_attribute max_value; > >>> + struct kobj_attribute scalar_increment; > >>> + struct kobj_attribute display_name; > >>> + struct kobj_attribute type; > >>> + struct attribute *attrs[8]; > >>> + struct attribute_group group; > >>> + int idx; > >>> +}; > >>> + > >>> +struct dptc_priv { > >>> + struct device *fw_attr_dev; > >>> + struct kset *fw_attr_kset; > >>> + > >>> + const struct dptc_device_limits *dev_limits; > >>> + > >>> + bool expanded; > >>> + > >>> + enum dptc_save_mode save_mode; > >>> + > >>> + u32 staged[DPTC_NUM_PARAMS]; > >>> + > >>> + /* Protects staged, expanded, and save_mode */ > >>> + struct mutex lock; > >>> + > >>> + struct dptc_attr_sysfs params[DPTC_NUM_PARAMS]; > >>> + struct dptc_attr_sysfs expanded_attr; > >>> + struct kobj_attribute save_settings_attr; > >>> +}; > >>> + > >>> +static struct platform_device *dptc_pdev; > >>> + > >>> +static u32 dptc_get_min(struct dptc_priv *dptc, int idx) > >>> +{ > >>> + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_min > >>> + : dptc->dev_limits->params[idx].device_min; > >>> +} > >>> + > >>> +static u32 dptc_get_max(struct dptc_priv *dptc, int idx) > >>> +{ > >>> + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_max > >>> + : dptc->dev_limits->params[idx].device_max; > >>> +} > >>> + > >>> +static u32 dptc_get_default(struct dptc_priv *dptc, int idx) > >>> +{ > >>> + return dptc->dev_limits->params[idx].def; > >>> +} > >>> + > >>> +static int dptc_alib_call(const u8 *ids, const u32 *vals, int count) > >>> +{ > >>> + union acpi_object in_params[2]; > >>> + struct acpi_object_list input; > >>> + acpi_status status; > >>> + u32 buf_size; > >>> + int i, off; > >>> + u8 *buf; > >>> + > >>> + if (count == 0) > >>> + return -ENOENT; > >>> + > >>> + /* Buffer layout: WORD total_size + count * (BYTE id + DWORD value) */ > >>> + buf_size = 2 + count * 5; > >>> + buf = kzalloc(buf_size, GFP_KERNEL); > >>> + if (!buf) > >>> + return -ENOMEM; > >>> + > >>> + put_unaligned_le16(buf_size, buf); > >>> + > >>> + for (i = 0; i < count; i++) { > >>> + off = 2 + i * 5; > >>> + buf[off] = ids[i]; > >>> + put_unaligned_le32(vals[i], &buf[off + 1]); > >>> + } > >>> + > >>> + in_params[0].type = ACPI_TYPE_INTEGER; > >>> + in_params[0].integer.value = ALIB_FUNC_DPTC; > >>> + in_params[1].type = ACPI_TYPE_BUFFER; > >>> + in_params[1].buffer.length = buf_size; > >>> + in_params[1].buffer.pointer = buf; > >>> + > >>> + input.count = 2; > >>> + input.pointer = in_params; > >>> + > >>> + status = acpi_evaluate_object(NULL, ALIB_PATH, &input, NULL); > >>> + kfree(buf); > >>> + > >>> + if (ACPI_FAILURE(status)) { > >>> + pr_err("ALIB call failed: %s\n", > >>> + acpi_format_exception(status)); > >>> + return -EIO; > >>> + } > >>> + > >>> + pr_debug("sent %d ALIB parameter(s)\n", count); > >>> + return 0; > >>> +} > >>> + > >>> +static int dptc_alib_fill_param(u8 *ids, u32 *vals, int offset, > >>> + enum dptc_param_idx param, u32 val) > >>> +{ > >>> + u32 hw_val = val * dptc_params[param].scale; > >>> + > >>> + ids[offset] = dptc_params[param].param_id; > >>> + vals[offset++] = hw_val; > >>> + > >>> + if (dptc_params[param].param_id2) { > >>> + ids[offset] = dptc_params[param].param_id2; > >>> + vals[offset++] = hw_val; > >>> + } > >>> + > >>> + return offset; > >>> +} > >>> + > >>> +static int dptc_alib_send_one(int idx, u32 val) > >>> +{ > >>> + u32 vals[2]; > >>> + u8 ids[2]; > >>> + > >>> + return dptc_alib_call(ids, vals, > >>> + dptc_alib_fill_param(ids, vals, 0, idx, val)); > >>> +} > >>> + > >>> +static int dptc_alib_save(struct dptc_priv *dptc) > >>> +{ > >>> + u32 vals[DPTC_NUM_PARAMS * 2]; > >>> + u8 ids[DPTC_NUM_PARAMS * 2]; > >>> + int count = 0; > >>> + int i; > >>> + > >>> + for (i = 0; i < DPTC_NUM_PARAMS; i++) { > >>> + if (!dptc->staged[i]) > >>> + continue; > >>> + count = dptc_alib_fill_param(ids, vals, count, i, > >>> + dptc->staged[i]); > >>> + } > >>> + > >>> + return dptc_alib_call(ids, vals, count); > >>> +} > >>> + > >>> +/* Sysfs callbacks */ > >>> + > >>> +static ssize_t dptc_current_value_show(struct kobject *kobj, > >>> + struct kobj_attribute *attr, char *buf) > >>> +{ > >>> + struct dptc_attr_sysfs *ps = > >>> + container_of(attr, struct dptc_attr_sysfs, current_value); > >>> + struct dptc_priv *dptc = ps->priv; > >>> + > >>> + guard(mutex)(&dptc->lock); > >>> + > >>> + if (!dptc->staged[ps->idx]) > >>> + return sysfs_emit(buf, "\n"); > >>> + return sysfs_emit(buf, "%u\n", dptc->staged[ps->idx]); > >>> +} > >>> + > >>> +static ssize_t dptc_current_value_store(struct kobject *kobj, > >>> + struct kobj_attribute *attr, > >>> + const char *buf, size_t count) > >>> +{ > >>> + struct dptc_attr_sysfs *ps = > >>> + container_of(attr, struct dptc_attr_sysfs, current_value); > >>> + struct dptc_priv *dptc = ps->priv; > >>> + u32 val, min, max; > >>> + int ret; > >>> + > >>> + guard(mutex)(&dptc->lock); > >>> + > >>> + if (count == 1 && buf[0] == '\n') { > >>> + dptc->staged[ps->idx] = 0; > >>> + return count; > >>> + } > >>> + > >>> + ret = kstrtou32(buf, 10, &val); > >>> + if (ret) > >>> + return ret; > >>> + > >>> + min = dptc_get_min(dptc, ps->idx); > >>> + max = dptc_get_max(dptc, ps->idx); > >>> + if (val < min || (max && val > max)) > >>> + return -EINVAL; > >>> + dptc->staged[ps->idx] = val; > >>> + if (dptc->save_mode == SAVE_SINGLE) > >>> + ret = dptc_alib_send_one(ps->idx, val); > >>> + > >>> + return ret ? ret : count; > >>> +} > >>> + > >>> +static ssize_t dptc_default_value_show(struct kobject *kobj, > >>> + struct kobj_attribute *attr, char *buf) > >>> +{ > >>> + struct dptc_attr_sysfs *ps = > >>> + container_of(attr, struct dptc_attr_sysfs, default_value); > >>> + > >>> + return sysfs_emit(buf, "%u\n", dptc_get_default(ps->priv, ps->idx)); > >>> +} > >>> + > >>> +static ssize_t dptc_min_value_show(struct kobject *kobj, > >>> + struct kobj_attribute *attr, char *buf) > >>> +{ > >>> + struct dptc_attr_sysfs *ps = > >>> + container_of(attr, struct dptc_attr_sysfs, min_value); > >>> + struct dptc_priv *dptc = ps->priv; > >>> + > >>> + guard(mutex)(&dptc->lock); > >>> + > >>> + return sysfs_emit(buf, "%u\n", dptc_get_min(dptc, ps->idx)); > >>> +} > >>> + > >>> +static ssize_t dptc_max_value_show(struct kobject *kobj, > >>> + struct kobj_attribute *attr, char *buf) > >>> +{ > >>> + struct dptc_attr_sysfs *ps = > >>> + container_of(attr, struct dptc_attr_sysfs, max_value); > >>> + struct dptc_priv *dptc = ps->priv; > >>> + > >>> + guard(mutex)(&dptc->lock); > >>> + > >>> + return sysfs_emit(buf, "%u\n", dptc_get_max(dptc, ps->idx)); > >>> +} > >>> + > >>> +static ssize_t dptc_scalar_increment_show(struct kobject *kobj, > >>> + struct kobj_attribute *attr, char *buf) > >>> +{ > >>> + return sysfs_emit(buf, "1\n"); > >>> +} > >>> + > >>> +static ssize_t dptc_display_name_show(struct kobject *kobj, > >>> + struct kobj_attribute *attr, char *buf) > >>> +{ > >>> + struct dptc_attr_sysfs *ps = > >>> + container_of(attr, struct dptc_attr_sysfs, display_name); > >>> + return sysfs_emit(buf, "%s\n", dptc_params[ps->idx].display_name); > >>> +} > >>> + > >>> +static ssize_t dptc_type_show(struct kobject *kobj, > >>> + struct kobj_attribute *attr, char *buf) > >>> +{ > >>> + return sysfs_emit(buf, "integer\n"); > >>> +} > >>> + > >>> +static ssize_t dptc_save_settings_show(struct kobject *kobj, > >>> + struct kobj_attribute *attr, char *buf) > >>> +{ > >>> + struct dptc_priv *dptc = > >>> + container_of(attr, struct dptc_priv, save_settings_attr); > >>> + > >>> + guard(mutex)(&dptc->lock); > >>> + > >>> + if (dptc->save_mode == SAVE_SINGLE) > >>> + return sysfs_emit(buf, "single\n"); > >>> + return sysfs_emit(buf, "bulk\n"); > >>> +} > >>> + > >>> +static ssize_t dptc_save_settings_store(struct kobject *kobj, > >>> + struct kobj_attribute *attr, > >>> + const char *buf, size_t count) > >>> +{ > >>> + struct dptc_priv *dptc = > >>> + container_of(attr, struct dptc_priv, save_settings_attr); > >>> + int ret = 0; > >>> + > >>> + guard(mutex)(&dptc->lock); > >>> + > >>> + if (sysfs_streq(buf, "save")) > >>> + ret = dptc_alib_save(dptc); > >>> + else if (sysfs_streq(buf, "single")) > >>> + dptc->save_mode = SAVE_SINGLE; > >>> + else if (sysfs_streq(buf, "bulk")) > >>> + dptc->save_mode = SAVE_BULK; > >>> + else > >>> + return -EINVAL; > >>> + > >>> + return ret ? ret : count; > >>> +} > >>> + > >>> +static ssize_t dptc_expanded_current_value_show(struct kobject *kobj, > >>> + struct kobj_attribute *attr, > >>> + char *buf) > >>> +{ > >>> + struct dptc_attr_sysfs *ps = > >>> + container_of(attr, struct dptc_attr_sysfs, current_value); > >>> + struct dptc_priv *dptc = ps->priv; > >>> + > >>> + guard(mutex)(&dptc->lock); > >>> + > >>> + return sysfs_emit(buf, "%d\n", dptc->expanded); > >>> +} > >>> + > >>> +static ssize_t dptc_expanded_current_value_store(struct kobject *kobj, > >>> + struct kobj_attribute *attr, > >>> + const char *buf, size_t count) > >>> +{ > >>> + struct dptc_attr_sysfs *ps = > >>> + container_of(attr, struct dptc_attr_sysfs, current_value); > >>> + struct dptc_priv *dptc = ps->priv; > >>> + bool val; > >>> + int ret; > >>> + > >>> + ret = kstrtobool(buf, &val); > >>> + if (ret) > >>> + return ret; > >>> + > >>> + guard(mutex)(&dptc->lock); > >>> + > >>> + dptc->expanded = val; > >>> + /* Clear staged values: limits changed, old values may be out of range */ > >>> + memset(dptc->staged, 0, sizeof(dptc->staged)); > >>> + > >>> + return count; > >>> +} > >>> + > >>> +static ssize_t dptc_expanded_default_value_show(struct kobject *kobj, > >>> + struct kobj_attribute *attr, > >>> + char *buf) > >>> +{ > >>> + return sysfs_emit(buf, "0\n"); > >>> +} > >>> + > >>> +static ssize_t dptc_expanded_min_value_show(struct kobject *kobj, > >>> + struct kobj_attribute *attr, > >>> + char *buf) > >>> +{ > >>> + return sysfs_emit(buf, "0\n"); > >>> +} > >>> + > >>> +static ssize_t dptc_expanded_max_value_show(struct kobject *kobj, > >>> + struct kobj_attribute *attr, > >>> + char *buf) > >>> +{ > >>> + return sysfs_emit(buf, "1\n"); > >>> +} > >>> + > >>> +static ssize_t dptc_expanded_scalar_increment_show(struct kobject *kobj, > >>> + struct kobj_attribute *attr, > >>> + char *buf) > >>> +{ > >>> + return sysfs_emit(buf, "1\n"); > >>> +} > >>> + > >>> +static ssize_t dptc_expanded_display_name_show(struct kobject *kobj, > >>> + struct kobj_attribute *attr, > >>> + char *buf) > >>> +{ > >>> + return sysfs_emit(buf, "Expanded Limits\n"); > >>> +} > >>> + > >>> +static ssize_t dptc_expanded_type_show(struct kobject *kobj, > >>> + struct kobj_attribute *attr, char *buf) > >>> +{ > >>> + return sysfs_emit(buf, "integer\n"); > >>> +} > >>> + > >>> +/* Sysfs setup */ > >>> + > >>> +static void dptc_setup_param_sysfs(struct dptc_priv *dptc, > >>> + struct dptc_attr_sysfs *ps, int idx) > >>> +{ > >>> + ps->priv = dptc; > >>> + ps->idx = idx; > >>> + > >>> + sysfs_attr_init(&ps->current_value.attr); > >>> + ps->current_value.attr.name = "current_value"; > >>> + ps->current_value.attr.mode = 0644; > >>> + ps->current_value.show = dptc_current_value_show; > >>> + ps->current_value.store = dptc_current_value_store; > >>> + > >>> + sysfs_attr_init(&ps->default_value.attr); > >>> + ps->default_value.attr.name = "default_value"; > >>> + ps->default_value.attr.mode = 0444; > >>> + ps->default_value.show = dptc_default_value_show; > >>> + > >>> + sysfs_attr_init(&ps->min_value.attr); > >>> + ps->min_value.attr.name = "min_value"; > >>> + ps->min_value.attr.mode = 0444; > >>> + ps->min_value.show = dptc_min_value_show; > >>> + > >>> + sysfs_attr_init(&ps->max_value.attr); > >>> + ps->max_value.attr.name = "max_value"; > >>> + ps->max_value.attr.mode = 0444; > >>> + ps->max_value.show = dptc_max_value_show; > >>> + > >>> + sysfs_attr_init(&ps->scalar_increment.attr); > >>> + ps->scalar_increment.attr.name = "scalar_increment"; > >>> + ps->scalar_increment.attr.mode = 0444; > >>> + ps->scalar_increment.show = dptc_scalar_increment_show; > >>> + > >>> + sysfs_attr_init(&ps->display_name.attr); > >>> + ps->display_name.attr.name = "display_name"; > >>> + ps->display_name.attr.mode = 0444; > >>> + ps->display_name.show = dptc_display_name_show; > >>> + > >>> + sysfs_attr_init(&ps->type.attr); > >>> + ps->type.attr.name = "type"; > >>> + ps->type.attr.mode = 0444; > >>> + ps->type.show = dptc_type_show; > >>> + > >>> + ps->attrs[0] = &ps->current_value.attr; > >>> + ps->attrs[1] = &ps->default_value.attr; > >>> + ps->attrs[2] = &ps->min_value.attr; > >>> + ps->attrs[3] = &ps->max_value.attr; > >>> + ps->attrs[4] = &ps->scalar_increment.attr; > >>> + ps->attrs[5] = &ps->display_name.attr; > >>> + ps->attrs[6] = &ps->type.attr; > >>> + ps->attrs[7] = NULL; > >>> + > >>> + ps->group.name = dptc_params[idx].name; > >>> + ps->group.attrs = ps->attrs; > >>> +} > >>> + > >>> +static void dptc_setup_expanded_sysfs(struct dptc_priv *dptc, > >>> + struct dptc_attr_sysfs *ps) > >>> +{ > >>> + ps->priv = dptc; > >>> + sysfs_attr_init(&ps->current_value.attr); > >>> + ps->current_value.attr.name = "current_value"; > >>> + ps->current_value.attr.mode = 0644; > >>> + ps->current_value.show = dptc_expanded_current_value_show; > >>> + ps->current_value.store = dptc_expanded_current_value_store; > >>> + > >>> + sysfs_attr_init(&ps->default_value.attr); > >>> + ps->default_value.attr.name = "default_value"; > >>> + ps->default_value.attr.mode = 0444; > >>> + ps->default_value.show = dptc_expanded_default_value_show; > >>> + > >>> + sysfs_attr_init(&ps->min_value.attr); > >>> + ps->min_value.attr.name = "min_value"; > >>> + ps->min_value.attr.mode = 0444; > >>> + ps->min_value.show = dptc_expanded_min_value_show; > >>> + > >>> + sysfs_attr_init(&ps->max_value.attr); > >>> + ps->max_value.attr.name = "max_value"; > >>> + ps->max_value.attr.mode = 0444; > >>> + ps->max_value.show = dptc_expanded_max_value_show; > >>> + > >>> + sysfs_attr_init(&ps->scalar_increment.attr); > >>> + ps->scalar_increment.attr.name = "scalar_increment"; > >>> + ps->scalar_increment.attr.mode = 0444; > >>> + ps->scalar_increment.show = dptc_expanded_scalar_increment_show; > >>> + > >>> + sysfs_attr_init(&ps->display_name.attr); > >>> + ps->display_name.attr.name = "display_name"; > >>> + ps->display_name.attr.mode = 0444; > >>> + ps->display_name.show = dptc_expanded_display_name_show; > >>> + > >>> + sysfs_attr_init(&ps->type.attr); > >>> + ps->type.attr.name = "type"; > >>> + ps->type.attr.mode = 0444; > >>> + ps->type.show = dptc_expanded_type_show; > >>> + > >>> + ps->attrs[0] = &ps->current_value.attr; > >>> + ps->attrs[1] = &ps->default_value.attr; > >>> + ps->attrs[2] = &ps->min_value.attr; > >>> + ps->attrs[3] = &ps->max_value.attr; > >>> + ps->attrs[4] = &ps->scalar_increment.attr; > >>> + ps->attrs[5] = &ps->display_name.attr; > >>> + ps->attrs[6] = &ps->type.attr; > >>> + ps->attrs[7] = NULL; > >>> + > >>> + ps->group.name = "expanded_limits"; > >>> + ps->group.attrs = ps->attrs; > >>> +} > >>> + > >>> +static void dptc_fw_dev_unregister(void *data) > >>> +{ > >>> + device_unregister(data); > >>> +} > >>> + > >>> +static void dptc_kset_unregister(void *data) > >>> +{ > >>> + kset_unregister(data); > >>> +} > >>> + > >>> +static int dptc_resume(struct device *dev) > >>> +{ > >>> + struct dptc_priv *dptc = dev_get_drvdata(dev); > >>> + int ret; > >>> + > >>> + guard(mutex)(&dptc->lock); > >>> + > >>> + /* In bulk mode, do not use pm ops for userspace flexibility. */ > >>> + if (dptc->save_mode == SAVE_SINGLE) > >>> + ret = dptc_alib_save(dptc); > >>> + else > >>> + ret = 0; > >>> + > >>> + if (ret && ret != -ENOENT) > >>> + dev_warn(dev, "failed to restore settings on resume: %d\n", ret); > >>> + > >>> + return 0; > >>> +} > >>> + > >>> +static DEFINE_SIMPLE_DEV_PM_OPS(dptc_pm_ops, NULL, dptc_resume); > >>> + > >>> +static int dptc_probe(struct platform_device *pdev) > >>> +{ > >>> + const struct dmi_system_id *dmi_match = dev_get_platdata(&pdev->dev); > >>> + struct device *dev = &pdev->dev; > >>> + struct dptc_priv *dptc; > >>> + int i, ret; > >>> + > >>> + dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL); > >>> + if (!dptc) > >>> + return -ENOMEM; > >>> + > >>> + platform_set_drvdata(pdev, dptc); > >>> + > >>> + ret = devm_mutex_init(dev, &dptc->lock); > >>> + if (ret) > >>> + return ret; > >>> + > >>> + dptc->dev_limits = dmi_match->driver_data; > >>> + dev_info(dev, "%s (%s)\n", dmi_match->ident, > >>> + boot_cpu_data.x86_model_id); > >>> + > >>> + dptc->fw_attr_dev = device_create(&firmware_attributes_class, > >>> + NULL, MKDEV(0, 0), NULL, DRIVER_NAME); > >>> + if (IS_ERR(dptc->fw_attr_dev)) > >>> + return PTR_ERR(dptc->fw_attr_dev); > >>> + > >>> + ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister, > >>> + dptc->fw_attr_dev); > >>> + if (ret) > >>> + return ret; > >>> + > >>> + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL, > >>> + &dptc->fw_attr_dev->kobj); > >>> + if (!dptc->fw_attr_kset) > >>> + return -ENOMEM; > >>> + > >>> + ret = devm_add_action_or_reset(dev, dptc_kset_unregister, > >>> + dptc->fw_attr_kset); > >>> + if (ret) > >>> + return ret; > >>> + > >>> + for (i = 0; i < DPTC_NUM_PARAMS; i++) { > >>> + dptc_setup_param_sysfs(dptc, &dptc->params[i], i); > >>> + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj, > >>> + &dptc->params[i].group); > >>> + if (ret) > >>> + return ret; > >>> + } > >>> + > >>> + dptc_setup_expanded_sysfs(dptc, &dptc->expanded_attr); > >>> + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj, > >>> + &dptc->expanded_attr.group); > >>> + if (ret) > >>> + return ret; > >>> + > >>> + sysfs_attr_init(&dptc->save_settings_attr.attr); > >>> + dptc->save_settings_attr.attr.name = "save_settings"; > >>> + dptc->save_settings_attr.attr.mode = 0644; > >>> + dptc->save_settings_attr.show = dptc_save_settings_show; > >>> + dptc->save_settings_attr.store = dptc_save_settings_store; > >>> + ret = sysfs_create_file(&dptc->fw_attr_kset->kobj, > >>> + &dptc->save_settings_attr.attr); > >>> + if (ret) > >>> + return ret; > >>> + > >>> + return 0; > >>> +} > >>> + > >>> +static struct platform_driver dptc_driver = { > >>> + .driver = { > >>> + .name = DRIVER_NAME, > >>> + .pm = pm_sleep_ptr(&dptc_pm_ops), > >>> + }, > >>> + .probe = dptc_probe, > >>> +}; > >>> + > >>> +static int __init dptc_init(void) > >>> +{ > >>> + const struct dmi_system_id *match; > >>> + bool soc_found = false; > >>> + int i, ret; > >>> + > >>> + match = dmi_first_match(dptc_dmi_table); > >>> + if (!match) > >>> + return -ENODEV; > >>> + > >>> + if (!acpi_has_method(NULL, ALIB_PATH)) { > >>> + pr_warn("ALIB method not present\n"); > >>> + return -ENODEV; > >>> + } > >>> + > >>> + for (i = 0; dptc_soc_table[i]; i++) { > >>> + if (strstr(boot_cpu_data.x86_model_id, > >>> + dptc_soc_table[i])) { > >>> + soc_found = true; > >>> + break; > >>> + } > >>> + } > >>> + if (!soc_found) { > >>> + pr_warn("unrecognized SoC '%s'\n", > >>> + boot_cpu_data.x86_model_id); > >>> + return -ENODEV; > >>> + } > >>> + > >>> + dptc_pdev = platform_device_register_data(NULL, DRIVER_NAME, -1, > >>> + match, sizeof(*match)); > >>> + if (IS_ERR(dptc_pdev)) > >>> + return PTR_ERR(dptc_pdev); > >>> + > >>> + ret = platform_driver_register(&dptc_driver); > >>> + if (ret) { > >>> + platform_device_unregister(dptc_pdev); > >>> + return ret; > >>> + } > >>> + > >>> + return 0; > >>> +} > >>> + > >>> +static void __exit dptc_exit(void) > >>> +{ > >>> + platform_driver_unregister(&dptc_driver); > >>> + platform_device_unregister(dptc_pdev); > >>> +} > >>> + > >>> +module_init(dptc_init); > >>> +module_exit(dptc_exit); > >>> + > >>> +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>"); > >>> +MODULE_DESCRIPTION("AMD DPTCi ACPI Driver"); > >>> +MODULE_LICENSE("GPL"); > >> > >> > > > > ^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [RFC v4 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver 2026-03-11 19:09 ` Antheas Kapenekakis @ 2026-03-12 13:24 ` Mario Limonciello 2026-03-12 13:47 ` Antheas Kapenekakis 0 siblings, 1 reply; 21+ messages in thread From: Mario Limonciello @ 2026-03-12 13:24 UTC (permalink / raw) To: Antheas Kapenekakis Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato, i, linux-kernel, platform-driver-x86 On 3/11/26 14:09, Antheas Kapenekakis wrote: > On Tue, 10 Mar 2026 at 17:26, Mario Limonciello > <mario.limonciello@amd.com> wrote: >> >> On 3/10/26 3:02 AM, Antheas Kapenekakis wrote: >>> On Tue, 10 Mar 2026 at 05:01, Mario Limonciello >>> <mario.limonciello@amd.com> wrote: >>>> >>>> >>>> >>>> On 3/9/2026 3:51 PM, Antheas Kapenekakis wrote: >>>>> Add a driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and >>>>> Thermal Configuration interface (DPTCi). This exposes TDP and thermal >>>>> parameters for AMD APU-based handheld devices via the >>>>> firmware-attributes sysfs ABI. >>>>> >>>>> Parameters are staged and atomically committed through ALIB. The driver >>>>> supports two save modes: "single" (apply immediately on write) and >>>>> "bulk" (stage values, then commit with "save"). An "expanded_limits" >>>>> toggle widens the allowed parameter ranges beyond device defaults. >>>>> >>>>> Initial device support: GPD Win 5 (AMD Ryzen AI MAX). >>>>> >>>>> Assisted-by: Claude:claude-opus-4-6 >>>>> Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev> >>>>> --- >>>>> MAINTAINERS | 6 + >>>>> drivers/platform/x86/amd/Kconfig | 14 + >>>>> drivers/platform/x86/amd/Makefile | 2 + >>>>> drivers/platform/x86/amd/dptc.c | 746 ++++++++++++++++++++++++++++++ >>>>> 4 files changed, 768 insertions(+) >>>>> create mode 100644 drivers/platform/x86/amd/dptc.c >>>>> >>>>> diff --git a/MAINTAINERS b/MAINTAINERS >>>>> index 89007f9ed35e..ebda8e82bf35 100644 >>>>> --- a/MAINTAINERS >>>>> +++ b/MAINTAINERS >>>>> @@ -1103,6 +1103,12 @@ S: Supported >>>>> F: drivers/gpu/drm/amd/display/dc/dml/ >>>>> F: drivers/gpu/drm/amd/display/dc/dml2_0/ >>>>> >>>>> +AMD DPTC DRIVER >>>>> +M: Antheas Kapenekakis <lkml@antheas.dev> >>>>> +L: platform-driver-x86@vger.kernel.org >>>>> +S: Maintained >>>>> +F: drivers/platform/x86/amd/dptc.c >>>>> + >>>>> AMD FAM15H PROCESSOR POWER MONITORING DRIVER >>>>> M: Huang Rui <ray.huang@amd.com> >>>>> L: linux-hwmon@vger.kernel.org >>>>> diff --git a/drivers/platform/x86/amd/Kconfig b/drivers/platform/x86/amd/Kconfig >>>>> index b813f9265368..d610092467fc 100644 >>>>> --- a/drivers/platform/x86/amd/Kconfig >>>>> +++ b/drivers/platform/x86/amd/Kconfig >>>>> @@ -44,3 +44,17 @@ config AMD_ISP_PLATFORM >>>>> >>>>> This driver can also be built as a module. If so, the module >>>>> will be called amd_isp4. >>>>> + >>>>> +config AMD_DPTC >>>>> + tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)" >>>>> + depends on X86_64 && ACPI && DMI >>>>> + select FIRMWARE_ATTRIBUTES_CLASS >>>>> + help >>>>> + Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and >>>>> + Thermal Configuration Interface (DPTCi). Exposes TDP and thermal >>>>> + parameters for AMD APU-based handheld devices via the >>>>> + firmware-attributes sysfs ABI, allowing userspace tools to stage >>>>> + and atomically commit power limit settings. Requires a DMI match >>>>> + for the device and a recognized AMD SoC. >>>>> + >>>>> + If built as a module, the module will be called amd_dptc. >>>>> diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile >>>>> index f6ff0c837f34..862a609bfe38 100644 >>>>> --- a/drivers/platform/x86/amd/Makefile >>>>> +++ b/drivers/platform/x86/amd/Makefile >>>>> @@ -12,3 +12,5 @@ obj-$(CONFIG_AMD_PMF) += pmf/ >>>>> obj-$(CONFIG_AMD_WBRF) += wbrf.o >>>>> obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o >>>>> obj-$(CONFIG_AMD_HFI) += hfi/ >>>>> +obj-$(CONFIG_AMD_DPTC) += amd_dptc.o >>>>> +amd_dptc-y := dptc.o >>>>> diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c >>>>> new file mode 100644 >>>>> index 000000000000..b884cdfa3f82 >>>>> --- /dev/null >>>>> +++ b/drivers/platform/x86/amd/dptc.c >>>>> @@ -0,0 +1,746 @@ >>>>> +// SPDX-License-Identifier: GPL-2.0-only >>>>> +/* >>>>> + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver >>>>> + * >>>>> + * Exposes AMD APU power and thermal parameters via the firmware-attributes >>>>> + * sysfs ABI. Parameters are staged and atomically committed through the >>>>> + * AGESA ALIB Function 0x0C (Dynamic Power and Thermal Configuration >>>>> + * interface). >>>>> + * >>>>> + * Reference: AMD AGESA Publication #44065, Appendix E.5 >>>>> + * https://docs.amd.com/v/u/en-US/44065_Arch2008 >>>>> + * >>>>> + * Copyright (C) 2026 Antheas Kapenekakis <lkml@antheas.dev> >>>>> + */ >>>>> + >>>>> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt >>>>> + >>>>> +#include <linux/acpi.h> >>>>> +#include <linux/cleanup.h> >>>>> +#include <linux/dmi.h> >>>>> +#include <linux/init.h> >>>>> +#include <linux/kobject.h> >>>>> +#include <linux/module.h> >>>>> +#include <linux/mutex.h> >>>>> +#include <linux/platform_device.h> >>>>> +#include <linux/processor.h> >>>>> +#include <linux/sysfs.h> >>>>> +#include <linux/unaligned.h> >>>>> + >>>>> +#include "../firmware_attributes_class.h" >>>>> + >>>>> +#define DRIVER_NAME "amd-dptc" >>>>> + >>>>> +#define ALIB_FUNC_DPTC 0x0C >>>>> +#define ALIB_PATH "\\_SB.ALIB" >>>>> + >>>>> +#define ALIB_ID_TEMP_TARGET 0x03 >>>>> +#define ALIB_ID_STAPM_LIMIT 0x05 >>>>> +#define ALIB_ID_FAST_LIMIT 0x06 >>>>> +#define ALIB_ID_SLOW_LIMIT 0x07 >>>>> +#define ALIB_ID_SKIN_LIMIT 0x2E >>>>> + >>>>> +enum dptc_param_idx { >>>>> + DPTC_PPT_PL1_SPL, /* STAPM + skin limit (set together) */ >>>>> + DPTC_PPT_PL2_SPPT, /* slow PPT limit */ >>>>> + DPTC_PPT_PL3_FPPT, /* fast PPT limit */ >>>>> + DPTC_CPU_TEMP, /* thermal control target */ >>>>> + DPTC_NUM_PARAMS, >>>>> +}; >>>>> + >>>>> +struct dptc_param_limits { >>>>> + u32 expanded_min; >>>>> + u32 device_min; >>>>> + u32 def; >>>>> + u32 device_max; >>>>> + u32 expanded_max; >>>>> +}; >>>>> + >>>>> +struct dptc_device_limits { >>>>> + struct dptc_param_limits params[DPTC_NUM_PARAMS]; >>>>> +}; >>>>> + >>>>> +struct dptc_param_desc { >>>>> + const char *name; >>>>> + const char *display_name; >>>>> + u16 scale; /* sysfs-to-ALIB multiplier (e.g. 1000 for W->mW) */ >>>>> + u8 param_id; >>>>> + u8 param_id2; /* secondary ALIB ID, 0 if none */ >>>>> +}; >>>>> + >>>>> +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = { >>>>> + [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (W)", >>>>> + 1000, ALIB_ID_STAPM_LIMIT, >>>>> + ALIB_ID_SKIN_LIMIT }, >>>>> + [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (W)", >>>>> + 1000, ALIB_ID_SLOW_LIMIT }, >>>>> + [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (W)", >>>>> + 1000, ALIB_ID_FAST_LIMIT }, >>>>> + [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)", >>>>> + 1, ALIB_ID_TEMP_TARGET }, >>>>> +}; >>>>> + >>>>> +/* AI MAX Handheld class: GPD Win 5 */ >>>>> +static const struct dptc_device_limits limits_maxhh = { >>>>> + .params = { >>>>> + [DPTC_PPT_PL1_SPL] = { 1, 4, 25, 80, 100 }, >>>>> + [DPTC_PPT_PL2_SPPT] = { 1, 4, 27, 82, 100 }, >>>>> + [DPTC_PPT_PL3_FPPT] = { 1, 4, 40, 85, 100 }, >>>>> + [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 }, >>>>> + }, >>>>> +}; >>>>> + >>>>> +/* Substring matches against boot_cpu_data.x86_model_id; order matters. */ >>>>> +static const char * const dptc_soc_table[] = { >>>>> + /* AI MAX */ >>>>> + "AMD RYZEN AI MAX+ 395", >>>>> + "AMD RYZEN AI MAX+ 385", >>>>> + "AMD RYZEN AI MAX 380", >>>>> + NULL, >>>>> +}; >>>> >>>> I feel like I commented this before; but I don't really understand the >>>> purpose of this table. >>>> >>>> If you have a system that is quirked already, why would you need to >>>> cross reference this table? >>> >>> Yes, and I addressed it on a reply. >>> >>> It is an additional safety feature. A lot of these devices ship >>> different SoC SKUs with the same DMI. Most of them actually. But it is >>> not needed beyond that, it can be removed. I will defer to you. >>> >> >> Exact same SMBIOS? There are a lot of fields that can be keyed off. >> Can you share a dmidecode from two such systems? > > Ah, it's difficult to verify currently, I don't have two spares and > varies per manufacturer. > > What I can say for now is that it varies per manufactuer. E.g., GPD > only updates their DMI data only for major revisions. For example, all > GPD Win 4s, 3 years of device generations, have the same DMI (6800/ > 7840/ 8840u, hx 370). Same for the Win Minis, but for the 2025 > generation GPD switched the ODM for the controller motherboard and I > assume more, so that one. However, the thermal differences are minimal > so its appropriate to use the same values for all. All chips can > handle more, the chassis is the limitation. Onexplayer typically > changes DMI per generation, but within the same generation they can > release multiple chip variants. It is unclear to me if they use the > same BIOS capsule. Ayaneo is also very similar. All Ayaneo 3s are > named Ayaneo 3. I have an 8840u but that device also came out with an > HX 370. > > Antheas I worry about making assumptions that a chassis thermal design will be the same from one device generation to the next. Keying off SMBIOS data that is generic each year will lead to innacuracies. Again - a reason why it's better to store the actual data for the platform in the BIOS or EC for the PMF driver to use (notice the theme of my broken record). If it does come down to the fact that SMBIOS data is totally identical year to year this would be spectacularly surprising to me. I had thought *Microsoft* requires the ProductSku field to change. So please; do not make assumptions based on a lack of hard data. If you don't have data for multiple years of a system or multiple chips in the system, leave it off your quirk list. They can always be added later when the data is available. > >>> >>>>> + >>>>> +static const struct dmi_system_id dptc_dmi_table[] = { >>>>> + /* GPD */ >>>>> + { >>>>> + .ident = "GPD Win 5", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>>>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_maxhh, >>>>> + }, >>>>> + { } >>>>> +}; >>>>> +MODULE_DEVICE_TABLE(dmi, dptc_dmi_table); >>>>> + >>>>> +enum dptc_save_mode { SAVE_SINGLE, SAVE_BULK }; >>>>> + >>>>> +struct dptc_priv; >>>>> + >>>>> +struct dptc_attr_sysfs { >>>>> + struct dptc_priv *priv; >>>>> + struct kobj_attribute current_value; >>>>> + struct kobj_attribute default_value; >>>>> + struct kobj_attribute min_value; >>>>> + struct kobj_attribute max_value; >>>>> + struct kobj_attribute scalar_increment; >>>>> + struct kobj_attribute display_name; >>>>> + struct kobj_attribute type; >>>>> + struct attribute *attrs[8]; >>>>> + struct attribute_group group; >>>>> + int idx; >>>>> +}; >>>>> + >>>>> +struct dptc_priv { >>>>> + struct device *fw_attr_dev; >>>>> + struct kset *fw_attr_kset; >>>>> + >>>>> + const struct dptc_device_limits *dev_limits; >>>>> + >>>>> + bool expanded; >>>>> + >>>>> + enum dptc_save_mode save_mode; >>>>> + >>>>> + u32 staged[DPTC_NUM_PARAMS]; >>>>> + >>>>> + /* Protects staged, expanded, and save_mode */ >>>>> + struct mutex lock; >>>>> + >>>>> + struct dptc_attr_sysfs params[DPTC_NUM_PARAMS]; >>>>> + struct dptc_attr_sysfs expanded_attr; >>>>> + struct kobj_attribute save_settings_attr; >>>>> +}; >>>>> + >>>>> +static struct platform_device *dptc_pdev; >>>>> + >>>>> +static u32 dptc_get_min(struct dptc_priv *dptc, int idx) >>>>> +{ >>>>> + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_min >>>>> + : dptc->dev_limits->params[idx].device_min; >>>>> +} >>>>> + >>>>> +static u32 dptc_get_max(struct dptc_priv *dptc, int idx) >>>>> +{ >>>>> + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_max >>>>> + : dptc->dev_limits->params[idx].device_max; >>>>> +} >>>>> + >>>>> +static u32 dptc_get_default(struct dptc_priv *dptc, int idx) >>>>> +{ >>>>> + return dptc->dev_limits->params[idx].def; >>>>> +} >>>>> + >>>>> +static int dptc_alib_call(const u8 *ids, const u32 *vals, int count) >>>>> +{ >>>>> + union acpi_object in_params[2]; >>>>> + struct acpi_object_list input; >>>>> + acpi_status status; >>>>> + u32 buf_size; >>>>> + int i, off; >>>>> + u8 *buf; >>>>> + >>>>> + if (count == 0) >>>>> + return -ENOENT; >>>>> + >>>>> + /* Buffer layout: WORD total_size + count * (BYTE id + DWORD value) */ >>>>> + buf_size = 2 + count * 5; >>>>> + buf = kzalloc(buf_size, GFP_KERNEL); >>>>> + if (!buf) >>>>> + return -ENOMEM; >>>>> + >>>>> + put_unaligned_le16(buf_size, buf); >>>>> + >>>>> + for (i = 0; i < count; i++) { >>>>> + off = 2 + i * 5; >>>>> + buf[off] = ids[i]; >>>>> + put_unaligned_le32(vals[i], &buf[off + 1]); >>>>> + } >>>>> + >>>>> + in_params[0].type = ACPI_TYPE_INTEGER; >>>>> + in_params[0].integer.value = ALIB_FUNC_DPTC; >>>>> + in_params[1].type = ACPI_TYPE_BUFFER; >>>>> + in_params[1].buffer.length = buf_size; >>>>> + in_params[1].buffer.pointer = buf; >>>>> + >>>>> + input.count = 2; >>>>> + input.pointer = in_params; >>>>> + >>>>> + status = acpi_evaluate_object(NULL, ALIB_PATH, &input, NULL); >>>>> + kfree(buf); >>>>> + >>>>> + if (ACPI_FAILURE(status)) { >>>>> + pr_err("ALIB call failed: %s\n", >>>>> + acpi_format_exception(status)); >>>>> + return -EIO; >>>>> + } >>>>> + >>>>> + pr_debug("sent %d ALIB parameter(s)\n", count); >>>>> + return 0; >>>>> +} >>>>> + >>>>> +static int dptc_alib_fill_param(u8 *ids, u32 *vals, int offset, >>>>> + enum dptc_param_idx param, u32 val) >>>>> +{ >>>>> + u32 hw_val = val * dptc_params[param].scale; >>>>> + >>>>> + ids[offset] = dptc_params[param].param_id; >>>>> + vals[offset++] = hw_val; >>>>> + >>>>> + if (dptc_params[param].param_id2) { >>>>> + ids[offset] = dptc_params[param].param_id2; >>>>> + vals[offset++] = hw_val; >>>>> + } >>>>> + >>>>> + return offset; >>>>> +} >>>>> + >>>>> +static int dptc_alib_send_one(int idx, u32 val) >>>>> +{ >>>>> + u32 vals[2]; >>>>> + u8 ids[2]; >>>>> + >>>>> + return dptc_alib_call(ids, vals, >>>>> + dptc_alib_fill_param(ids, vals, 0, idx, val)); >>>>> +} >>>>> + >>>>> +static int dptc_alib_save(struct dptc_priv *dptc) >>>>> +{ >>>>> + u32 vals[DPTC_NUM_PARAMS * 2]; >>>>> + u8 ids[DPTC_NUM_PARAMS * 2]; >>>>> + int count = 0; >>>>> + int i; >>>>> + >>>>> + for (i = 0; i < DPTC_NUM_PARAMS; i++) { >>>>> + if (!dptc->staged[i]) >>>>> + continue; >>>>> + count = dptc_alib_fill_param(ids, vals, count, i, >>>>> + dptc->staged[i]); >>>>> + } >>>>> + >>>>> + return dptc_alib_call(ids, vals, count); >>>>> +} >>>>> + >>>>> +/* Sysfs callbacks */ >>>>> + >>>>> +static ssize_t dptc_current_value_show(struct kobject *kobj, >>>>> + struct kobj_attribute *attr, char *buf) >>>>> +{ >>>>> + struct dptc_attr_sysfs *ps = >>>>> + container_of(attr, struct dptc_attr_sysfs, current_value); >>>>> + struct dptc_priv *dptc = ps->priv; >>>>> + >>>>> + guard(mutex)(&dptc->lock); >>>>> + >>>>> + if (!dptc->staged[ps->idx]) >>>>> + return sysfs_emit(buf, "\n"); >>>>> + return sysfs_emit(buf, "%u\n", dptc->staged[ps->idx]); >>>>> +} >>>>> + >>>>> +static ssize_t dptc_current_value_store(struct kobject *kobj, >>>>> + struct kobj_attribute *attr, >>>>> + const char *buf, size_t count) >>>>> +{ >>>>> + struct dptc_attr_sysfs *ps = >>>>> + container_of(attr, struct dptc_attr_sysfs, current_value); >>>>> + struct dptc_priv *dptc = ps->priv; >>>>> + u32 val, min, max; >>>>> + int ret; >>>>> + >>>>> + guard(mutex)(&dptc->lock); >>>>> + >>>>> + if (count == 1 && buf[0] == '\n') { >>>>> + dptc->staged[ps->idx] = 0; >>>>> + return count; >>>>> + } >>>>> + >>>>> + ret = kstrtou32(buf, 10, &val); >>>>> + if (ret) >>>>> + return ret; >>>>> + >>>>> + min = dptc_get_min(dptc, ps->idx); >>>>> + max = dptc_get_max(dptc, ps->idx); >>>>> + if (val < min || (max && val > max)) >>>>> + return -EINVAL; >>>>> + dptc->staged[ps->idx] = val; >>>>> + if (dptc->save_mode == SAVE_SINGLE) >>>>> + ret = dptc_alib_send_one(ps->idx, val); >>>>> + >>>>> + return ret ? ret : count; >>>>> +} >>>>> + >>>>> +static ssize_t dptc_default_value_show(struct kobject *kobj, >>>>> + struct kobj_attribute *attr, char *buf) >>>>> +{ >>>>> + struct dptc_attr_sysfs *ps = >>>>> + container_of(attr, struct dptc_attr_sysfs, default_value); >>>>> + >>>>> + return sysfs_emit(buf, "%u\n", dptc_get_default(ps->priv, ps->idx)); >>>>> +} >>>>> + >>>>> +static ssize_t dptc_min_value_show(struct kobject *kobj, >>>>> + struct kobj_attribute *attr, char *buf) >>>>> +{ >>>>> + struct dptc_attr_sysfs *ps = >>>>> + container_of(attr, struct dptc_attr_sysfs, min_value); >>>>> + struct dptc_priv *dptc = ps->priv; >>>>> + >>>>> + guard(mutex)(&dptc->lock); >>>>> + >>>>> + return sysfs_emit(buf, "%u\n", dptc_get_min(dptc, ps->idx)); >>>>> +} >>>>> + >>>>> +static ssize_t dptc_max_value_show(struct kobject *kobj, >>>>> + struct kobj_attribute *attr, char *buf) >>>>> +{ >>>>> + struct dptc_attr_sysfs *ps = >>>>> + container_of(attr, struct dptc_attr_sysfs, max_value); >>>>> + struct dptc_priv *dptc = ps->priv; >>>>> + >>>>> + guard(mutex)(&dptc->lock); >>>>> + >>>>> + return sysfs_emit(buf, "%u\n", dptc_get_max(dptc, ps->idx)); >>>>> +} >>>>> + >>>>> +static ssize_t dptc_scalar_increment_show(struct kobject *kobj, >>>>> + struct kobj_attribute *attr, char *buf) >>>>> +{ >>>>> + return sysfs_emit(buf, "1\n"); >>>>> +} >>>>> + >>>>> +static ssize_t dptc_display_name_show(struct kobject *kobj, >>>>> + struct kobj_attribute *attr, char *buf) >>>>> +{ >>>>> + struct dptc_attr_sysfs *ps = >>>>> + container_of(attr, struct dptc_attr_sysfs, display_name); >>>>> + return sysfs_emit(buf, "%s\n", dptc_params[ps->idx].display_name); >>>>> +} >>>>> + >>>>> +static ssize_t dptc_type_show(struct kobject *kobj, >>>>> + struct kobj_attribute *attr, char *buf) >>>>> +{ >>>>> + return sysfs_emit(buf, "integer\n"); >>>>> +} >>>>> + >>>>> +static ssize_t dptc_save_settings_show(struct kobject *kobj, >>>>> + struct kobj_attribute *attr, char *buf) >>>>> +{ >>>>> + struct dptc_priv *dptc = >>>>> + container_of(attr, struct dptc_priv, save_settings_attr); >>>>> + >>>>> + guard(mutex)(&dptc->lock); >>>>> + >>>>> + if (dptc->save_mode == SAVE_SINGLE) >>>>> + return sysfs_emit(buf, "single\n"); >>>>> + return sysfs_emit(buf, "bulk\n"); >>>>> +} >>>>> + >>>>> +static ssize_t dptc_save_settings_store(struct kobject *kobj, >>>>> + struct kobj_attribute *attr, >>>>> + const char *buf, size_t count) >>>>> +{ >>>>> + struct dptc_priv *dptc = >>>>> + container_of(attr, struct dptc_priv, save_settings_attr); >>>>> + int ret = 0; >>>>> + >>>>> + guard(mutex)(&dptc->lock); >>>>> + >>>>> + if (sysfs_streq(buf, "save")) >>>>> + ret = dptc_alib_save(dptc); >>>>> + else if (sysfs_streq(buf, "single")) >>>>> + dptc->save_mode = SAVE_SINGLE; >>>>> + else if (sysfs_streq(buf, "bulk")) >>>>> + dptc->save_mode = SAVE_BULK; >>>>> + else >>>>> + return -EINVAL; >>>>> + >>>>> + return ret ? ret : count; >>>>> +} >>>>> + >>>>> +static ssize_t dptc_expanded_current_value_show(struct kobject *kobj, >>>>> + struct kobj_attribute *attr, >>>>> + char *buf) >>>>> +{ >>>>> + struct dptc_attr_sysfs *ps = >>>>> + container_of(attr, struct dptc_attr_sysfs, current_value); >>>>> + struct dptc_priv *dptc = ps->priv; >>>>> + >>>>> + guard(mutex)(&dptc->lock); >>>>> + >>>>> + return sysfs_emit(buf, "%d\n", dptc->expanded); >>>>> +} >>>>> + >>>>> +static ssize_t dptc_expanded_current_value_store(struct kobject *kobj, >>>>> + struct kobj_attribute *attr, >>>>> + const char *buf, size_t count) >>>>> +{ >>>>> + struct dptc_attr_sysfs *ps = >>>>> + container_of(attr, struct dptc_attr_sysfs, current_value); >>>>> + struct dptc_priv *dptc = ps->priv; >>>>> + bool val; >>>>> + int ret; >>>>> + >>>>> + ret = kstrtobool(buf, &val); >>>>> + if (ret) >>>>> + return ret; >>>>> + >>>>> + guard(mutex)(&dptc->lock); >>>>> + >>>>> + dptc->expanded = val; >>>>> + /* Clear staged values: limits changed, old values may be out of range */ >>>>> + memset(dptc->staged, 0, sizeof(dptc->staged)); >>>>> + >>>>> + return count; >>>>> +} >>>>> + >>>>> +static ssize_t dptc_expanded_default_value_show(struct kobject *kobj, >>>>> + struct kobj_attribute *attr, >>>>> + char *buf) >>>>> +{ >>>>> + return sysfs_emit(buf, "0\n"); >>>>> +} >>>>> + >>>>> +static ssize_t dptc_expanded_min_value_show(struct kobject *kobj, >>>>> + struct kobj_attribute *attr, >>>>> + char *buf) >>>>> +{ >>>>> + return sysfs_emit(buf, "0\n"); >>>>> +} >>>>> + >>>>> +static ssize_t dptc_expanded_max_value_show(struct kobject *kobj, >>>>> + struct kobj_attribute *attr, >>>>> + char *buf) >>>>> +{ >>>>> + return sysfs_emit(buf, "1\n"); >>>>> +} >>>>> + >>>>> +static ssize_t dptc_expanded_scalar_increment_show(struct kobject *kobj, >>>>> + struct kobj_attribute *attr, >>>>> + char *buf) >>>>> +{ >>>>> + return sysfs_emit(buf, "1\n"); >>>>> +} >>>>> + >>>>> +static ssize_t dptc_expanded_display_name_show(struct kobject *kobj, >>>>> + struct kobj_attribute *attr, >>>>> + char *buf) >>>>> +{ >>>>> + return sysfs_emit(buf, "Expanded Limits\n"); >>>>> +} >>>>> + >>>>> +static ssize_t dptc_expanded_type_show(struct kobject *kobj, >>>>> + struct kobj_attribute *attr, char *buf) >>>>> +{ >>>>> + return sysfs_emit(buf, "integer\n"); >>>>> +} >>>>> + >>>>> +/* Sysfs setup */ >>>>> + >>>>> +static void dptc_setup_param_sysfs(struct dptc_priv *dptc, >>>>> + struct dptc_attr_sysfs *ps, int idx) >>>>> +{ >>>>> + ps->priv = dptc; >>>>> + ps->idx = idx; >>>>> + >>>>> + sysfs_attr_init(&ps->current_value.attr); >>>>> + ps->current_value.attr.name = "current_value"; >>>>> + ps->current_value.attr.mode = 0644; >>>>> + ps->current_value.show = dptc_current_value_show; >>>>> + ps->current_value.store = dptc_current_value_store; >>>>> + >>>>> + sysfs_attr_init(&ps->default_value.attr); >>>>> + ps->default_value.attr.name = "default_value"; >>>>> + ps->default_value.attr.mode = 0444; >>>>> + ps->default_value.show = dptc_default_value_show; >>>>> + >>>>> + sysfs_attr_init(&ps->min_value.attr); >>>>> + ps->min_value.attr.name = "min_value"; >>>>> + ps->min_value.attr.mode = 0444; >>>>> + ps->min_value.show = dptc_min_value_show; >>>>> + >>>>> + sysfs_attr_init(&ps->max_value.attr); >>>>> + ps->max_value.attr.name = "max_value"; >>>>> + ps->max_value.attr.mode = 0444; >>>>> + ps->max_value.show = dptc_max_value_show; >>>>> + >>>>> + sysfs_attr_init(&ps->scalar_increment.attr); >>>>> + ps->scalar_increment.attr.name = "scalar_increment"; >>>>> + ps->scalar_increment.attr.mode = 0444; >>>>> + ps->scalar_increment.show = dptc_scalar_increment_show; >>>>> + >>>>> + sysfs_attr_init(&ps->display_name.attr); >>>>> + ps->display_name.attr.name = "display_name"; >>>>> + ps->display_name.attr.mode = 0444; >>>>> + ps->display_name.show = dptc_display_name_show; >>>>> + >>>>> + sysfs_attr_init(&ps->type.attr); >>>>> + ps->type.attr.name = "type"; >>>>> + ps->type.attr.mode = 0444; >>>>> + ps->type.show = dptc_type_show; >>>>> + >>>>> + ps->attrs[0] = &ps->current_value.attr; >>>>> + ps->attrs[1] = &ps->default_value.attr; >>>>> + ps->attrs[2] = &ps->min_value.attr; >>>>> + ps->attrs[3] = &ps->max_value.attr; >>>>> + ps->attrs[4] = &ps->scalar_increment.attr; >>>>> + ps->attrs[5] = &ps->display_name.attr; >>>>> + ps->attrs[6] = &ps->type.attr; >>>>> + ps->attrs[7] = NULL; >>>>> + >>>>> + ps->group.name = dptc_params[idx].name; >>>>> + ps->group.attrs = ps->attrs; >>>>> +} >>>>> + >>>>> +static void dptc_setup_expanded_sysfs(struct dptc_priv *dptc, >>>>> + struct dptc_attr_sysfs *ps) >>>>> +{ >>>>> + ps->priv = dptc; >>>>> + sysfs_attr_init(&ps->current_value.attr); >>>>> + ps->current_value.attr.name = "current_value"; >>>>> + ps->current_value.attr.mode = 0644; >>>>> + ps->current_value.show = dptc_expanded_current_value_show; >>>>> + ps->current_value.store = dptc_expanded_current_value_store; >>>>> + >>>>> + sysfs_attr_init(&ps->default_value.attr); >>>>> + ps->default_value.attr.name = "default_value"; >>>>> + ps->default_value.attr.mode = 0444; >>>>> + ps->default_value.show = dptc_expanded_default_value_show; >>>>> + >>>>> + sysfs_attr_init(&ps->min_value.attr); >>>>> + ps->min_value.attr.name = "min_value"; >>>>> + ps->min_value.attr.mode = 0444; >>>>> + ps->min_value.show = dptc_expanded_min_value_show; >>>>> + >>>>> + sysfs_attr_init(&ps->max_value.attr); >>>>> + ps->max_value.attr.name = "max_value"; >>>>> + ps->max_value.attr.mode = 0444; >>>>> + ps->max_value.show = dptc_expanded_max_value_show; >>>>> + >>>>> + sysfs_attr_init(&ps->scalar_increment.attr); >>>>> + ps->scalar_increment.attr.name = "scalar_increment"; >>>>> + ps->scalar_increment.attr.mode = 0444; >>>>> + ps->scalar_increment.show = dptc_expanded_scalar_increment_show; >>>>> + >>>>> + sysfs_attr_init(&ps->display_name.attr); >>>>> + ps->display_name.attr.name = "display_name"; >>>>> + ps->display_name.attr.mode = 0444; >>>>> + ps->display_name.show = dptc_expanded_display_name_show; >>>>> + >>>>> + sysfs_attr_init(&ps->type.attr); >>>>> + ps->type.attr.name = "type"; >>>>> + ps->type.attr.mode = 0444; >>>>> + ps->type.show = dptc_expanded_type_show; >>>>> + >>>>> + ps->attrs[0] = &ps->current_value.attr; >>>>> + ps->attrs[1] = &ps->default_value.attr; >>>>> + ps->attrs[2] = &ps->min_value.attr; >>>>> + ps->attrs[3] = &ps->max_value.attr; >>>>> + ps->attrs[4] = &ps->scalar_increment.attr; >>>>> + ps->attrs[5] = &ps->display_name.attr; >>>>> + ps->attrs[6] = &ps->type.attr; >>>>> + ps->attrs[7] = NULL; >>>>> + >>>>> + ps->group.name = "expanded_limits"; >>>>> + ps->group.attrs = ps->attrs; >>>>> +} >>>>> + >>>>> +static void dptc_fw_dev_unregister(void *data) >>>>> +{ >>>>> + device_unregister(data); >>>>> +} >>>>> + >>>>> +static void dptc_kset_unregister(void *data) >>>>> +{ >>>>> + kset_unregister(data); >>>>> +} >>>>> + >>>>> +static int dptc_resume(struct device *dev) >>>>> +{ >>>>> + struct dptc_priv *dptc = dev_get_drvdata(dev); >>>>> + int ret; >>>>> + >>>>> + guard(mutex)(&dptc->lock); >>>>> + >>>>> + /* In bulk mode, do not use pm ops for userspace flexibility. */ >>>>> + if (dptc->save_mode == SAVE_SINGLE) >>>>> + ret = dptc_alib_save(dptc); >>>>> + else >>>>> + ret = 0; >>>>> + >>>>> + if (ret && ret != -ENOENT) >>>>> + dev_warn(dev, "failed to restore settings on resume: %d\n", ret); >>>>> + >>>>> + return 0; >>>>> +} >>>>> + >>>>> +static DEFINE_SIMPLE_DEV_PM_OPS(dptc_pm_ops, NULL, dptc_resume); >>>>> + >>>>> +static int dptc_probe(struct platform_device *pdev) >>>>> +{ >>>>> + const struct dmi_system_id *dmi_match = dev_get_platdata(&pdev->dev); >>>>> + struct device *dev = &pdev->dev; >>>>> + struct dptc_priv *dptc; >>>>> + int i, ret; >>>>> + >>>>> + dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL); >>>>> + if (!dptc) >>>>> + return -ENOMEM; >>>>> + >>>>> + platform_set_drvdata(pdev, dptc); >>>>> + >>>>> + ret = devm_mutex_init(dev, &dptc->lock); >>>>> + if (ret) >>>>> + return ret; >>>>> + >>>>> + dptc->dev_limits = dmi_match->driver_data; >>>>> + dev_info(dev, "%s (%s)\n", dmi_match->ident, >>>>> + boot_cpu_data.x86_model_id); >>>>> + >>>>> + dptc->fw_attr_dev = device_create(&firmware_attributes_class, >>>>> + NULL, MKDEV(0, 0), NULL, DRIVER_NAME); >>>>> + if (IS_ERR(dptc->fw_attr_dev)) >>>>> + return PTR_ERR(dptc->fw_attr_dev); >>>>> + >>>>> + ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister, >>>>> + dptc->fw_attr_dev); >>>>> + if (ret) >>>>> + return ret; >>>>> + >>>>> + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL, >>>>> + &dptc->fw_attr_dev->kobj); >>>>> + if (!dptc->fw_attr_kset) >>>>> + return -ENOMEM; >>>>> + >>>>> + ret = devm_add_action_or_reset(dev, dptc_kset_unregister, >>>>> + dptc->fw_attr_kset); >>>>> + if (ret) >>>>> + return ret; >>>>> + >>>>> + for (i = 0; i < DPTC_NUM_PARAMS; i++) { >>>>> + dptc_setup_param_sysfs(dptc, &dptc->params[i], i); >>>>> + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj, >>>>> + &dptc->params[i].group); >>>>> + if (ret) >>>>> + return ret; >>>>> + } >>>>> + >>>>> + dptc_setup_expanded_sysfs(dptc, &dptc->expanded_attr); >>>>> + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj, >>>>> + &dptc->expanded_attr.group); >>>>> + if (ret) >>>>> + return ret; >>>>> + >>>>> + sysfs_attr_init(&dptc->save_settings_attr.attr); >>>>> + dptc->save_settings_attr.attr.name = "save_settings"; >>>>> + dptc->save_settings_attr.attr.mode = 0644; >>>>> + dptc->save_settings_attr.show = dptc_save_settings_show; >>>>> + dptc->save_settings_attr.store = dptc_save_settings_store; >>>>> + ret = sysfs_create_file(&dptc->fw_attr_kset->kobj, >>>>> + &dptc->save_settings_attr.attr); >>>>> + if (ret) >>>>> + return ret; >>>>> + >>>>> + return 0; >>>>> +} >>>>> + >>>>> +static struct platform_driver dptc_driver = { >>>>> + .driver = { >>>>> + .name = DRIVER_NAME, >>>>> + .pm = pm_sleep_ptr(&dptc_pm_ops), >>>>> + }, >>>>> + .probe = dptc_probe, >>>>> +}; >>>>> + >>>>> +static int __init dptc_init(void) >>>>> +{ >>>>> + const struct dmi_system_id *match; >>>>> + bool soc_found = false; >>>>> + int i, ret; >>>>> + >>>>> + match = dmi_first_match(dptc_dmi_table); >>>>> + if (!match) >>>>> + return -ENODEV; >>>>> + >>>>> + if (!acpi_has_method(NULL, ALIB_PATH)) { >>>>> + pr_warn("ALIB method not present\n"); >>>>> + return -ENODEV; >>>>> + } >>>>> + >>>>> + for (i = 0; dptc_soc_table[i]; i++) { >>>>> + if (strstr(boot_cpu_data.x86_model_id, >>>>> + dptc_soc_table[i])) { >>>>> + soc_found = true; >>>>> + break; >>>>> + } >>>>> + } >>>>> + if (!soc_found) { >>>>> + pr_warn("unrecognized SoC '%s'\n", >>>>> + boot_cpu_data.x86_model_id); >>>>> + return -ENODEV; >>>>> + } >>>>> + >>>>> + dptc_pdev = platform_device_register_data(NULL, DRIVER_NAME, -1, >>>>> + match, sizeof(*match)); >>>>> + if (IS_ERR(dptc_pdev)) >>>>> + return PTR_ERR(dptc_pdev); >>>>> + >>>>> + ret = platform_driver_register(&dptc_driver); >>>>> + if (ret) { >>>>> + platform_device_unregister(dptc_pdev); >>>>> + return ret; >>>>> + } >>>>> + >>>>> + return 0; >>>>> +} >>>>> + >>>>> +static void __exit dptc_exit(void) >>>>> +{ >>>>> + platform_driver_unregister(&dptc_driver); >>>>> + platform_device_unregister(dptc_pdev); >>>>> +} >>>>> + >>>>> +module_init(dptc_init); >>>>> +module_exit(dptc_exit); >>>>> + >>>>> +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>"); >>>>> +MODULE_DESCRIPTION("AMD DPTCi ACPI Driver"); >>>>> +MODULE_LICENSE("GPL"); >>>> >>>> >>> >> >> > ^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [RFC v4 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver 2026-03-12 13:24 ` Mario Limonciello @ 2026-03-12 13:47 ` Antheas Kapenekakis 2026-03-12 16:05 ` Mario Limonciello 0 siblings, 1 reply; 21+ messages in thread From: Antheas Kapenekakis @ 2026-03-12 13:47 UTC (permalink / raw) To: Mario Limonciello Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato, i, linux-kernel, platform-driver-x86 On Thu, 12 Mar 2026 at 14:25, Mario Limonciello <mario.limonciello@amd.com> wrote: > > > > On 3/11/26 14:09, Antheas Kapenekakis wrote: > > On Tue, 10 Mar 2026 at 17:26, Mario Limonciello > > <mario.limonciello@amd.com> wrote: > >> > >> On 3/10/26 3:02 AM, Antheas Kapenekakis wrote: > >>> On Tue, 10 Mar 2026 at 05:01, Mario Limonciello > >>> <mario.limonciello@amd.com> wrote: > >>>> > >>>> > >>>> > >>>> On 3/9/2026 3:51 PM, Antheas Kapenekakis wrote: > >>>>> Add a driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and > >>>>> Thermal Configuration interface (DPTCi). This exposes TDP and thermal > >>>>> parameters for AMD APU-based handheld devices via the > >>>>> firmware-attributes sysfs ABI. > >>>>> > >>>>> Parameters are staged and atomically committed through ALIB. The driver > >>>>> supports two save modes: "single" (apply immediately on write) and > >>>>> "bulk" (stage values, then commit with "save"). An "expanded_limits" > >>>>> toggle widens the allowed parameter ranges beyond device defaults. > >>>>> > >>>>> Initial device support: GPD Win 5 (AMD Ryzen AI MAX). > >>>>> > >>>>> Assisted-by: Claude:claude-opus-4-6 > >>>>> Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev> > >>>>> --- > >>>>> MAINTAINERS | 6 + > >>>>> drivers/platform/x86/amd/Kconfig | 14 + > >>>>> drivers/platform/x86/amd/Makefile | 2 + > >>>>> drivers/platform/x86/amd/dptc.c | 746 ++++++++++++++++++++++++++++++ > >>>>> 4 files changed, 768 insertions(+) > >>>>> create mode 100644 drivers/platform/x86/amd/dptc.c > >>>>> > >>>>> diff --git a/MAINTAINERS b/MAINTAINERS > >>>>> index 89007f9ed35e..ebda8e82bf35 100644 > >>>>> --- a/MAINTAINERS > >>>>> +++ b/MAINTAINERS > >>>>> @@ -1103,6 +1103,12 @@ S: Supported > >>>>> F: drivers/gpu/drm/amd/display/dc/dml/ > >>>>> F: drivers/gpu/drm/amd/display/dc/dml2_0/ > >>>>> > >>>>> +AMD DPTC DRIVER > >>>>> +M: Antheas Kapenekakis <lkml@antheas.dev> > >>>>> +L: platform-driver-x86@vger.kernel.org > >>>>> +S: Maintained > >>>>> +F: drivers/platform/x86/amd/dptc.c > >>>>> + > >>>>> AMD FAM15H PROCESSOR POWER MONITORING DRIVER > >>>>> M: Huang Rui <ray.huang@amd.com> > >>>>> L: linux-hwmon@vger.kernel.org > >>>>> diff --git a/drivers/platform/x86/amd/Kconfig b/drivers/platform/x86/amd/Kconfig > >>>>> index b813f9265368..d610092467fc 100644 > >>>>> --- a/drivers/platform/x86/amd/Kconfig > >>>>> +++ b/drivers/platform/x86/amd/Kconfig > >>>>> @@ -44,3 +44,17 @@ config AMD_ISP_PLATFORM > >>>>> > >>>>> This driver can also be built as a module. If so, the module > >>>>> will be called amd_isp4. > >>>>> + > >>>>> +config AMD_DPTC > >>>>> + tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)" > >>>>> + depends on X86_64 && ACPI && DMI > >>>>> + select FIRMWARE_ATTRIBUTES_CLASS > >>>>> + help > >>>>> + Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and > >>>>> + Thermal Configuration Interface (DPTCi). Exposes TDP and thermal > >>>>> + parameters for AMD APU-based handheld devices via the > >>>>> + firmware-attributes sysfs ABI, allowing userspace tools to stage > >>>>> + and atomically commit power limit settings. Requires a DMI match > >>>>> + for the device and a recognized AMD SoC. > >>>>> + > >>>>> + If built as a module, the module will be called amd_dptc. > >>>>> diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile > >>>>> index f6ff0c837f34..862a609bfe38 100644 > >>>>> --- a/drivers/platform/x86/amd/Makefile > >>>>> +++ b/drivers/platform/x86/amd/Makefile > >>>>> @@ -12,3 +12,5 @@ obj-$(CONFIG_AMD_PMF) += pmf/ > >>>>> obj-$(CONFIG_AMD_WBRF) += wbrf.o > >>>>> obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o > >>>>> obj-$(CONFIG_AMD_HFI) += hfi/ > >>>>> +obj-$(CONFIG_AMD_DPTC) += amd_dptc.o > >>>>> +amd_dptc-y := dptc.o > >>>>> diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c > >>>>> new file mode 100644 > >>>>> index 000000000000..b884cdfa3f82 > >>>>> --- /dev/null > >>>>> +++ b/drivers/platform/x86/amd/dptc.c > >>>>> @@ -0,0 +1,746 @@ > >>>>> +// SPDX-License-Identifier: GPL-2.0-only > >>>>> +/* > >>>>> + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver > >>>>> + * > >>>>> + * Exposes AMD APU power and thermal parameters via the firmware-attributes > >>>>> + * sysfs ABI. Parameters are staged and atomically committed through the > >>>>> + * AGESA ALIB Function 0x0C (Dynamic Power and Thermal Configuration > >>>>> + * interface). > >>>>> + * > >>>>> + * Reference: AMD AGESA Publication #44065, Appendix E.5 > >>>>> + * https://docs.amd.com/v/u/en-US/44065_Arch2008 > >>>>> + * > >>>>> + * Copyright (C) 2026 Antheas Kapenekakis <lkml@antheas.dev> > >>>>> + */ > >>>>> + > >>>>> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt > >>>>> + > >>>>> +#include <linux/acpi.h> > >>>>> +#include <linux/cleanup.h> > >>>>> +#include <linux/dmi.h> > >>>>> +#include <linux/init.h> > >>>>> +#include <linux/kobject.h> > >>>>> +#include <linux/module.h> > >>>>> +#include <linux/mutex.h> > >>>>> +#include <linux/platform_device.h> > >>>>> +#include <linux/processor.h> > >>>>> +#include <linux/sysfs.h> > >>>>> +#include <linux/unaligned.h> > >>>>> + > >>>>> +#include "../firmware_attributes_class.h" > >>>>> + > >>>>> +#define DRIVER_NAME "amd-dptc" > >>>>> + > >>>>> +#define ALIB_FUNC_DPTC 0x0C > >>>>> +#define ALIB_PATH "\\_SB.ALIB" > >>>>> + > >>>>> +#define ALIB_ID_TEMP_TARGET 0x03 > >>>>> +#define ALIB_ID_STAPM_LIMIT 0x05 > >>>>> +#define ALIB_ID_FAST_LIMIT 0x06 > >>>>> +#define ALIB_ID_SLOW_LIMIT 0x07 > >>>>> +#define ALIB_ID_SKIN_LIMIT 0x2E > >>>>> + > >>>>> +enum dptc_param_idx { > >>>>> + DPTC_PPT_PL1_SPL, /* STAPM + skin limit (set together) */ > >>>>> + DPTC_PPT_PL2_SPPT, /* slow PPT limit */ > >>>>> + DPTC_PPT_PL3_FPPT, /* fast PPT limit */ > >>>>> + DPTC_CPU_TEMP, /* thermal control target */ > >>>>> + DPTC_NUM_PARAMS, > >>>>> +}; > >>>>> + > >>>>> +struct dptc_param_limits { > >>>>> + u32 expanded_min; > >>>>> + u32 device_min; > >>>>> + u32 def; > >>>>> + u32 device_max; > >>>>> + u32 expanded_max; > >>>>> +}; > >>>>> + > >>>>> +struct dptc_device_limits { > >>>>> + struct dptc_param_limits params[DPTC_NUM_PARAMS]; > >>>>> +}; > >>>>> + > >>>>> +struct dptc_param_desc { > >>>>> + const char *name; > >>>>> + const char *display_name; > >>>>> + u16 scale; /* sysfs-to-ALIB multiplier (e.g. 1000 for W->mW) */ > >>>>> + u8 param_id; > >>>>> + u8 param_id2; /* secondary ALIB ID, 0 if none */ > >>>>> +}; > >>>>> + > >>>>> +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = { > >>>>> + [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (W)", > >>>>> + 1000, ALIB_ID_STAPM_LIMIT, > >>>>> + ALIB_ID_SKIN_LIMIT }, > >>>>> + [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (W)", > >>>>> + 1000, ALIB_ID_SLOW_LIMIT }, > >>>>> + [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (W)", > >>>>> + 1000, ALIB_ID_FAST_LIMIT }, > >>>>> + [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)", > >>>>> + 1, ALIB_ID_TEMP_TARGET }, > >>>>> +}; > >>>>> + > >>>>> +/* AI MAX Handheld class: GPD Win 5 */ > >>>>> +static const struct dptc_device_limits limits_maxhh = { > >>>>> + .params = { > >>>>> + [DPTC_PPT_PL1_SPL] = { 1, 4, 25, 80, 100 }, > >>>>> + [DPTC_PPT_PL2_SPPT] = { 1, 4, 27, 82, 100 }, > >>>>> + [DPTC_PPT_PL3_FPPT] = { 1, 4, 40, 85, 100 }, > >>>>> + [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 }, > >>>>> + }, > >>>>> +}; > >>>>> + > >>>>> +/* Substring matches against boot_cpu_data.x86_model_id; order matters. */ > >>>>> +static const char * const dptc_soc_table[] = { > >>>>> + /* AI MAX */ > >>>>> + "AMD RYZEN AI MAX+ 395", > >>>>> + "AMD RYZEN AI MAX+ 385", > >>>>> + "AMD RYZEN AI MAX 380", > >>>>> + NULL, > >>>>> +}; > >>>> > >>>> I feel like I commented this before; but I don't really understand the > >>>> purpose of this table. > >>>> > >>>> If you have a system that is quirked already, why would you need to > >>>> cross reference this table? > >>> > >>> Yes, and I addressed it on a reply. > >>> > >>> It is an additional safety feature. A lot of these devices ship > >>> different SoC SKUs with the same DMI. Most of them actually. But it is > >>> not needed beyond that, it can be removed. I will defer to you. > >>> > >> > >> Exact same SMBIOS? There are a lot of fields that can be keyed off. > >> Can you share a dmidecode from two such systems? > > > > Ah, it's difficult to verify currently, I don't have two spares and > > varies per manufacturer. > > > > What I can say for now is that it varies per manufactuer. E.g., GPD > > only updates their DMI data only for major revisions. For example, all > > GPD Win 4s, 3 years of device generations, have the same DMI (6800/ > > 7840/ 8840u, hx 370). Same for the Win Minis, but for the 2025 > > generation GPD switched the ODM for the controller motherboard and I > > assume more, so that one. However, the thermal differences are minimal > > so its appropriate to use the same values for all. All chips can > > handle more, the chassis is the limitation. Onexplayer typically > > changes DMI per generation, but within the same generation they can > > release multiple chip variants. It is unclear to me if they use the > > same BIOS capsule. Ayaneo is also very similar. All Ayaneo 3s are > > named Ayaneo 3. I have an 8840u but that device also came out with an > > HX 370. > > > > Antheas > > I worry about making assumptions that a chassis thermal design will be > the same from one device generation to the next. Keying off SMBIOS data > that is generic each year will lead to innacuracies. Again - a reason > why it's better to store the actual data for the platform in the BIOS or > EC for the PMF driver to use (notice the theme of my broken record). I do agree with manufacturers providing tunings in BIOS when possible. These devices are already released so it is not possible. Here I will also note the need for a slider for a majority of users, perhaps around 40%. I would not call that optional. So pmf will need some work to accommodate, and the faster that happens the less devices will need to be added to this driver. > If it does come down to the fact that SMBIOS data is totally identical > year to year this would be spectacularly surprising to me. I had > thought *Microsoft* requires the ProductSku field to change. > > So please; do not make assumptions based on a lack of hard data. > If you don't have data for multiple years of a system or multiple chips > in the system, leave it off your quirk list. They can always be added > later when the data is available. Specifically for GPD, the userspace implementation has been tested on all generations. It is fine to choose a (lower) limit that works on all of them. The thermals did not change between generations. Saturation above 20W means that there is little difference between 22W, 25W, 28W, 30W, etc. If there is doubt, going lower is fine. Even if one device can do 5W more it does not matter (but the chassis, battery, and cooler are exactly the same; only the board was revised for the new SoC). 8-20W is the sweet spot for these APUs. 8W and 15W are sane for low-power/balanced. The only question is how close to 30W should be the maximum and where performance should point to. This version does 25W for performance. 20W might be better for daily use until AC/DC detection. We do not need to complicate things. All this driver needs to do is expose a slider from 4-30W. This covers 6800-HX370 for all vendors and matches user expectation. AI Max will need further research as I do not have a lot of data on its performance curve. Ayaneo had some drops between 2022-2023 that had lower TDP limits, but there is less than 1k units in existence. However, I do not have device duplicates in my possession or kept hand of exact SMBIOS data. I only monitored changes to DMI. And it is hard for me to collect now Microsoft can require things, but then again, a lot of these devices come with Secureboot disabled, and mixed setup flows for Windows. So I doubt changing the product sku was on the priority list. Antheas > > > >>> > >>>>> + > >>>>> +static const struct dmi_system_id dptc_dmi_table[] = { > >>>>> + /* GPD */ > >>>>> + { > >>>>> + .ident = "GPD Win 5", > >>>>> + .matches = { > >>>>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > >>>>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"), > >>>>> + }, > >>>>> + .driver_data = (void *)&limits_maxhh, > >>>>> + }, > >>>>> + { } > >>>>> +}; > >>>>> +MODULE_DEVICE_TABLE(dmi, dptc_dmi_table); > >>>>> + > >>>>> +enum dptc_save_mode { SAVE_SINGLE, SAVE_BULK }; > >>>>> + > >>>>> +struct dptc_priv; > >>>>> + > >>>>> +struct dptc_attr_sysfs { > >>>>> + struct dptc_priv *priv; > >>>>> + struct kobj_attribute current_value; > >>>>> + struct kobj_attribute default_value; > >>>>> + struct kobj_attribute min_value; > >>>>> + struct kobj_attribute max_value; > >>>>> + struct kobj_attribute scalar_increment; > >>>>> + struct kobj_attribute display_name; > >>>>> + struct kobj_attribute type; > >>>>> + struct attribute *attrs[8]; > >>>>> + struct attribute_group group; > >>>>> + int idx; > >>>>> +}; > >>>>> + > >>>>> +struct dptc_priv { > >>>>> + struct device *fw_attr_dev; > >>>>> + struct kset *fw_attr_kset; > >>>>> + > >>>>> + const struct dptc_device_limits *dev_limits; > >>>>> + > >>>>> + bool expanded; > >>>>> + > >>>>> + enum dptc_save_mode save_mode; > >>>>> + > >>>>> + u32 staged[DPTC_NUM_PARAMS]; > >>>>> + > >>>>> + /* Protects staged, expanded, and save_mode */ > >>>>> + struct mutex lock; > >>>>> + > >>>>> + struct dptc_attr_sysfs params[DPTC_NUM_PARAMS]; > >>>>> + struct dptc_attr_sysfs expanded_attr; > >>>>> + struct kobj_attribute save_settings_attr; > >>>>> +}; > >>>>> + > >>>>> +static struct platform_device *dptc_pdev; > >>>>> + > >>>>> +static u32 dptc_get_min(struct dptc_priv *dptc, int idx) > >>>>> +{ > >>>>> + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_min > >>>>> + : dptc->dev_limits->params[idx].device_min; > >>>>> +} > >>>>> + > >>>>> +static u32 dptc_get_max(struct dptc_priv *dptc, int idx) > >>>>> +{ > >>>>> + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_max > >>>>> + : dptc->dev_limits->params[idx].device_max; > >>>>> +} > >>>>> + > >>>>> +static u32 dptc_get_default(struct dptc_priv *dptc, int idx) > >>>>> +{ > >>>>> + return dptc->dev_limits->params[idx].def; > >>>>> +} > >>>>> + > >>>>> +static int dptc_alib_call(const u8 *ids, const u32 *vals, int count) > >>>>> +{ > >>>>> + union acpi_object in_params[2]; > >>>>> + struct acpi_object_list input; > >>>>> + acpi_status status; > >>>>> + u32 buf_size; > >>>>> + int i, off; > >>>>> + u8 *buf; > >>>>> + > >>>>> + if (count == 0) > >>>>> + return -ENOENT; > >>>>> + > >>>>> + /* Buffer layout: WORD total_size + count * (BYTE id + DWORD value) */ > >>>>> + buf_size = 2 + count * 5; > >>>>> + buf = kzalloc(buf_size, GFP_KERNEL); > >>>>> + if (!buf) > >>>>> + return -ENOMEM; > >>>>> + > >>>>> + put_unaligned_le16(buf_size, buf); > >>>>> + > >>>>> + for (i = 0; i < count; i++) { > >>>>> + off = 2 + i * 5; > >>>>> + buf[off] = ids[i]; > >>>>> + put_unaligned_le32(vals[i], &buf[off + 1]); > >>>>> + } > >>>>> + > >>>>> + in_params[0].type = ACPI_TYPE_INTEGER; > >>>>> + in_params[0].integer.value = ALIB_FUNC_DPTC; > >>>>> + in_params[1].type = ACPI_TYPE_BUFFER; > >>>>> + in_params[1].buffer.length = buf_size; > >>>>> + in_params[1].buffer.pointer = buf; > >>>>> + > >>>>> + input.count = 2; > >>>>> + input.pointer = in_params; > >>>>> + > >>>>> + status = acpi_evaluate_object(NULL, ALIB_PATH, &input, NULL); > >>>>> + kfree(buf); > >>>>> + > >>>>> + if (ACPI_FAILURE(status)) { > >>>>> + pr_err("ALIB call failed: %s\n", > >>>>> + acpi_format_exception(status)); > >>>>> + return -EIO; > >>>>> + } > >>>>> + > >>>>> + pr_debug("sent %d ALIB parameter(s)\n", count); > >>>>> + return 0; > >>>>> +} > >>>>> + > >>>>> +static int dptc_alib_fill_param(u8 *ids, u32 *vals, int offset, > >>>>> + enum dptc_param_idx param, u32 val) > >>>>> +{ > >>>>> + u32 hw_val = val * dptc_params[param].scale; > >>>>> + > >>>>> + ids[offset] = dptc_params[param].param_id; > >>>>> + vals[offset++] = hw_val; > >>>>> + > >>>>> + if (dptc_params[param].param_id2) { > >>>>> + ids[offset] = dptc_params[param].param_id2; > >>>>> + vals[offset++] = hw_val; > >>>>> + } > >>>>> + > >>>>> + return offset; > >>>>> +} > >>>>> + > >>>>> +static int dptc_alib_send_one(int idx, u32 val) > >>>>> +{ > >>>>> + u32 vals[2]; > >>>>> + u8 ids[2]; > >>>>> + > >>>>> + return dptc_alib_call(ids, vals, > >>>>> + dptc_alib_fill_param(ids, vals, 0, idx, val)); > >>>>> +} > >>>>> + > >>>>> +static int dptc_alib_save(struct dptc_priv *dptc) > >>>>> +{ > >>>>> + u32 vals[DPTC_NUM_PARAMS * 2]; > >>>>> + u8 ids[DPTC_NUM_PARAMS * 2]; > >>>>> + int count = 0; > >>>>> + int i; > >>>>> + > >>>>> + for (i = 0; i < DPTC_NUM_PARAMS; i++) { > >>>>> + if (!dptc->staged[i]) > >>>>> + continue; > >>>>> + count = dptc_alib_fill_param(ids, vals, count, i, > >>>>> + dptc->staged[i]); > >>>>> + } > >>>>> + > >>>>> + return dptc_alib_call(ids, vals, count); > >>>>> +} > >>>>> + > >>>>> +/* Sysfs callbacks */ > >>>>> + > >>>>> +static ssize_t dptc_current_value_show(struct kobject *kobj, > >>>>> + struct kobj_attribute *attr, char *buf) > >>>>> +{ > >>>>> + struct dptc_attr_sysfs *ps = > >>>>> + container_of(attr, struct dptc_attr_sysfs, current_value); > >>>>> + struct dptc_priv *dptc = ps->priv; > >>>>> + > >>>>> + guard(mutex)(&dptc->lock); > >>>>> + > >>>>> + if (!dptc->staged[ps->idx]) > >>>>> + return sysfs_emit(buf, "\n"); > >>>>> + return sysfs_emit(buf, "%u\n", dptc->staged[ps->idx]); > >>>>> +} > >>>>> + > >>>>> +static ssize_t dptc_current_value_store(struct kobject *kobj, > >>>>> + struct kobj_attribute *attr, > >>>>> + const char *buf, size_t count) > >>>>> +{ > >>>>> + struct dptc_attr_sysfs *ps = > >>>>> + container_of(attr, struct dptc_attr_sysfs, current_value); > >>>>> + struct dptc_priv *dptc = ps->priv; > >>>>> + u32 val, min, max; > >>>>> + int ret; > >>>>> + > >>>>> + guard(mutex)(&dptc->lock); > >>>>> + > >>>>> + if (count == 1 && buf[0] == '\n') { > >>>>> + dptc->staged[ps->idx] = 0; > >>>>> + return count; > >>>>> + } > >>>>> + > >>>>> + ret = kstrtou32(buf, 10, &val); > >>>>> + if (ret) > >>>>> + return ret; > >>>>> + > >>>>> + min = dptc_get_min(dptc, ps->idx); > >>>>> + max = dptc_get_max(dptc, ps->idx); > >>>>> + if (val < min || (max && val > max)) > >>>>> + return -EINVAL; > >>>>> + dptc->staged[ps->idx] = val; > >>>>> + if (dptc->save_mode == SAVE_SINGLE) > >>>>> + ret = dptc_alib_send_one(ps->idx, val); > >>>>> + > >>>>> + return ret ? ret : count; > >>>>> +} > >>>>> + > >>>>> +static ssize_t dptc_default_value_show(struct kobject *kobj, > >>>>> + struct kobj_attribute *attr, char *buf) > >>>>> +{ > >>>>> + struct dptc_attr_sysfs *ps = > >>>>> + container_of(attr, struct dptc_attr_sysfs, default_value); > >>>>> + > >>>>> + return sysfs_emit(buf, "%u\n", dptc_get_default(ps->priv, ps->idx)); > >>>>> +} > >>>>> + > >>>>> +static ssize_t dptc_min_value_show(struct kobject *kobj, > >>>>> + struct kobj_attribute *attr, char *buf) > >>>>> +{ > >>>>> + struct dptc_attr_sysfs *ps = > >>>>> + container_of(attr, struct dptc_attr_sysfs, min_value); > >>>>> + struct dptc_priv *dptc = ps->priv; > >>>>> + > >>>>> + guard(mutex)(&dptc->lock); > >>>>> + > >>>>> + return sysfs_emit(buf, "%u\n", dptc_get_min(dptc, ps->idx)); > >>>>> +} > >>>>> + > >>>>> +static ssize_t dptc_max_value_show(struct kobject *kobj, > >>>>> + struct kobj_attribute *attr, char *buf) > >>>>> +{ > >>>>> + struct dptc_attr_sysfs *ps = > >>>>> + container_of(attr, struct dptc_attr_sysfs, max_value); > >>>>> + struct dptc_priv *dptc = ps->priv; > >>>>> + > >>>>> + guard(mutex)(&dptc->lock); > >>>>> + > >>>>> + return sysfs_emit(buf, "%u\n", dptc_get_max(dptc, ps->idx)); > >>>>> +} > >>>>> + > >>>>> +static ssize_t dptc_scalar_increment_show(struct kobject *kobj, > >>>>> + struct kobj_attribute *attr, char *buf) > >>>>> +{ > >>>>> + return sysfs_emit(buf, "1\n"); > >>>>> +} > >>>>> + > >>>>> +static ssize_t dptc_display_name_show(struct kobject *kobj, > >>>>> + struct kobj_attribute *attr, char *buf) > >>>>> +{ > >>>>> + struct dptc_attr_sysfs *ps = > >>>>> + container_of(attr, struct dptc_attr_sysfs, display_name); > >>>>> + return sysfs_emit(buf, "%s\n", dptc_params[ps->idx].display_name); > >>>>> +} > >>>>> + > >>>>> +static ssize_t dptc_type_show(struct kobject *kobj, > >>>>> + struct kobj_attribute *attr, char *buf) > >>>>> +{ > >>>>> + return sysfs_emit(buf, "integer\n"); > >>>>> +} > >>>>> + > >>>>> +static ssize_t dptc_save_settings_show(struct kobject *kobj, > >>>>> + struct kobj_attribute *attr, char *buf) > >>>>> +{ > >>>>> + struct dptc_priv *dptc = > >>>>> + container_of(attr, struct dptc_priv, save_settings_attr); > >>>>> + > >>>>> + guard(mutex)(&dptc->lock); > >>>>> + > >>>>> + if (dptc->save_mode == SAVE_SINGLE) > >>>>> + return sysfs_emit(buf, "single\n"); > >>>>> + return sysfs_emit(buf, "bulk\n"); > >>>>> +} > >>>>> + > >>>>> +static ssize_t dptc_save_settings_store(struct kobject *kobj, > >>>>> + struct kobj_attribute *attr, > >>>>> + const char *buf, size_t count) > >>>>> +{ > >>>>> + struct dptc_priv *dptc = > >>>>> + container_of(attr, struct dptc_priv, save_settings_attr); > >>>>> + int ret = 0; > >>>>> + > >>>>> + guard(mutex)(&dptc->lock); > >>>>> + > >>>>> + if (sysfs_streq(buf, "save")) > >>>>> + ret = dptc_alib_save(dptc); > >>>>> + else if (sysfs_streq(buf, "single")) > >>>>> + dptc->save_mode = SAVE_SINGLE; > >>>>> + else if (sysfs_streq(buf, "bulk")) > >>>>> + dptc->save_mode = SAVE_BULK; > >>>>> + else > >>>>> + return -EINVAL; > >>>>> + > >>>>> + return ret ? ret : count; > >>>>> +} > >>>>> + > >>>>> +static ssize_t dptc_expanded_current_value_show(struct kobject *kobj, > >>>>> + struct kobj_attribute *attr, > >>>>> + char *buf) > >>>>> +{ > >>>>> + struct dptc_attr_sysfs *ps = > >>>>> + container_of(attr, struct dptc_attr_sysfs, current_value); > >>>>> + struct dptc_priv *dptc = ps->priv; > >>>>> + > >>>>> + guard(mutex)(&dptc->lock); > >>>>> + > >>>>> + return sysfs_emit(buf, "%d\n", dptc->expanded); > >>>>> +} > >>>>> + > >>>>> +static ssize_t dptc_expanded_current_value_store(struct kobject *kobj, > >>>>> + struct kobj_attribute *attr, > >>>>> + const char *buf, size_t count) > >>>>> +{ > >>>>> + struct dptc_attr_sysfs *ps = > >>>>> + container_of(attr, struct dptc_attr_sysfs, current_value); > >>>>> + struct dptc_priv *dptc = ps->priv; > >>>>> + bool val; > >>>>> + int ret; > >>>>> + > >>>>> + ret = kstrtobool(buf, &val); > >>>>> + if (ret) > >>>>> + return ret; > >>>>> + > >>>>> + guard(mutex)(&dptc->lock); > >>>>> + > >>>>> + dptc->expanded = val; > >>>>> + /* Clear staged values: limits changed, old values may be out of range */ > >>>>> + memset(dptc->staged, 0, sizeof(dptc->staged)); > >>>>> + > >>>>> + return count; > >>>>> +} > >>>>> + > >>>>> +static ssize_t dptc_expanded_default_value_show(struct kobject *kobj, > >>>>> + struct kobj_attribute *attr, > >>>>> + char *buf) > >>>>> +{ > >>>>> + return sysfs_emit(buf, "0\n"); > >>>>> +} > >>>>> + > >>>>> +static ssize_t dptc_expanded_min_value_show(struct kobject *kobj, > >>>>> + struct kobj_attribute *attr, > >>>>> + char *buf) > >>>>> +{ > >>>>> + return sysfs_emit(buf, "0\n"); > >>>>> +} > >>>>> + > >>>>> +static ssize_t dptc_expanded_max_value_show(struct kobject *kobj, > >>>>> + struct kobj_attribute *attr, > >>>>> + char *buf) > >>>>> +{ > >>>>> + return sysfs_emit(buf, "1\n"); > >>>>> +} > >>>>> + > >>>>> +static ssize_t dptc_expanded_scalar_increment_show(struct kobject *kobj, > >>>>> + struct kobj_attribute *attr, > >>>>> + char *buf) > >>>>> +{ > >>>>> + return sysfs_emit(buf, "1\n"); > >>>>> +} > >>>>> + > >>>>> +static ssize_t dptc_expanded_display_name_show(struct kobject *kobj, > >>>>> + struct kobj_attribute *attr, > >>>>> + char *buf) > >>>>> +{ > >>>>> + return sysfs_emit(buf, "Expanded Limits\n"); > >>>>> +} > >>>>> + > >>>>> +static ssize_t dptc_expanded_type_show(struct kobject *kobj, > >>>>> + struct kobj_attribute *attr, char *buf) > >>>>> +{ > >>>>> + return sysfs_emit(buf, "integer\n"); > >>>>> +} > >>>>> + > >>>>> +/* Sysfs setup */ > >>>>> + > >>>>> +static void dptc_setup_param_sysfs(struct dptc_priv *dptc, > >>>>> + struct dptc_attr_sysfs *ps, int idx) > >>>>> +{ > >>>>> + ps->priv = dptc; > >>>>> + ps->idx = idx; > >>>>> + > >>>>> + sysfs_attr_init(&ps->current_value.attr); > >>>>> + ps->current_value.attr.name = "current_value"; > >>>>> + ps->current_value.attr.mode = 0644; > >>>>> + ps->current_value.show = dptc_current_value_show; > >>>>> + ps->current_value.store = dptc_current_value_store; > >>>>> + > >>>>> + sysfs_attr_init(&ps->default_value.attr); > >>>>> + ps->default_value.attr.name = "default_value"; > >>>>> + ps->default_value.attr.mode = 0444; > >>>>> + ps->default_value.show = dptc_default_value_show; > >>>>> + > >>>>> + sysfs_attr_init(&ps->min_value.attr); > >>>>> + ps->min_value.attr.name = "min_value"; > >>>>> + ps->min_value.attr.mode = 0444; > >>>>> + ps->min_value.show = dptc_min_value_show; > >>>>> + > >>>>> + sysfs_attr_init(&ps->max_value.attr); > >>>>> + ps->max_value.attr.name = "max_value"; > >>>>> + ps->max_value.attr.mode = 0444; > >>>>> + ps->max_value.show = dptc_max_value_show; > >>>>> + > >>>>> + sysfs_attr_init(&ps->scalar_increment.attr); > >>>>> + ps->scalar_increment.attr.name = "scalar_increment"; > >>>>> + ps->scalar_increment.attr.mode = 0444; > >>>>> + ps->scalar_increment.show = dptc_scalar_increment_show; > >>>>> + > >>>>> + sysfs_attr_init(&ps->display_name.attr); > >>>>> + ps->display_name.attr.name = "display_name"; > >>>>> + ps->display_name.attr.mode = 0444; > >>>>> + ps->display_name.show = dptc_display_name_show; > >>>>> + > >>>>> + sysfs_attr_init(&ps->type.attr); > >>>>> + ps->type.attr.name = "type"; > >>>>> + ps->type.attr.mode = 0444; > >>>>> + ps->type.show = dptc_type_show; > >>>>> + > >>>>> + ps->attrs[0] = &ps->current_value.attr; > >>>>> + ps->attrs[1] = &ps->default_value.attr; > >>>>> + ps->attrs[2] = &ps->min_value.attr; > >>>>> + ps->attrs[3] = &ps->max_value.attr; > >>>>> + ps->attrs[4] = &ps->scalar_increment.attr; > >>>>> + ps->attrs[5] = &ps->display_name.attr; > >>>>> + ps->attrs[6] = &ps->type.attr; > >>>>> + ps->attrs[7] = NULL; > >>>>> + > >>>>> + ps->group.name = dptc_params[idx].name; > >>>>> + ps->group.attrs = ps->attrs; > >>>>> +} > >>>>> + > >>>>> +static void dptc_setup_expanded_sysfs(struct dptc_priv *dptc, > >>>>> + struct dptc_attr_sysfs *ps) > >>>>> +{ > >>>>> + ps->priv = dptc; > >>>>> + sysfs_attr_init(&ps->current_value.attr); > >>>>> + ps->current_value.attr.name = "current_value"; > >>>>> + ps->current_value.attr.mode = 0644; > >>>>> + ps->current_value.show = dptc_expanded_current_value_show; > >>>>> + ps->current_value.store = dptc_expanded_current_value_store; > >>>>> + > >>>>> + sysfs_attr_init(&ps->default_value.attr); > >>>>> + ps->default_value.attr.name = "default_value"; > >>>>> + ps->default_value.attr.mode = 0444; > >>>>> + ps->default_value.show = dptc_expanded_default_value_show; > >>>>> + > >>>>> + sysfs_attr_init(&ps->min_value.attr); > >>>>> + ps->min_value.attr.name = "min_value"; > >>>>> + ps->min_value.attr.mode = 0444; > >>>>> + ps->min_value.show = dptc_expanded_min_value_show; > >>>>> + > >>>>> + sysfs_attr_init(&ps->max_value.attr); > >>>>> + ps->max_value.attr.name = "max_value"; > >>>>> + ps->max_value.attr.mode = 0444; > >>>>> + ps->max_value.show = dptc_expanded_max_value_show; > >>>>> + > >>>>> + sysfs_attr_init(&ps->scalar_increment.attr); > >>>>> + ps->scalar_increment.attr.name = "scalar_increment"; > >>>>> + ps->scalar_increment.attr.mode = 0444; > >>>>> + ps->scalar_increment.show = dptc_expanded_scalar_increment_show; > >>>>> + > >>>>> + sysfs_attr_init(&ps->display_name.attr); > >>>>> + ps->display_name.attr.name = "display_name"; > >>>>> + ps->display_name.attr.mode = 0444; > >>>>> + ps->display_name.show = dptc_expanded_display_name_show; > >>>>> + > >>>>> + sysfs_attr_init(&ps->type.attr); > >>>>> + ps->type.attr.name = "type"; > >>>>> + ps->type.attr.mode = 0444; > >>>>> + ps->type.show = dptc_expanded_type_show; > >>>>> + > >>>>> + ps->attrs[0] = &ps->current_value.attr; > >>>>> + ps->attrs[1] = &ps->default_value.attr; > >>>>> + ps->attrs[2] = &ps->min_value.attr; > >>>>> + ps->attrs[3] = &ps->max_value.attr; > >>>>> + ps->attrs[4] = &ps->scalar_increment.attr; > >>>>> + ps->attrs[5] = &ps->display_name.attr; > >>>>> + ps->attrs[6] = &ps->type.attr; > >>>>> + ps->attrs[7] = NULL; > >>>>> + > >>>>> + ps->group.name = "expanded_limits"; > >>>>> + ps->group.attrs = ps->attrs; > >>>>> +} > >>>>> + > >>>>> +static void dptc_fw_dev_unregister(void *data) > >>>>> +{ > >>>>> + device_unregister(data); > >>>>> +} > >>>>> + > >>>>> +static void dptc_kset_unregister(void *data) > >>>>> +{ > >>>>> + kset_unregister(data); > >>>>> +} > >>>>> + > >>>>> +static int dptc_resume(struct device *dev) > >>>>> +{ > >>>>> + struct dptc_priv *dptc = dev_get_drvdata(dev); > >>>>> + int ret; > >>>>> + > >>>>> + guard(mutex)(&dptc->lock); > >>>>> + > >>>>> + /* In bulk mode, do not use pm ops for userspace flexibility. */ > >>>>> + if (dptc->save_mode == SAVE_SINGLE) > >>>>> + ret = dptc_alib_save(dptc); > >>>>> + else > >>>>> + ret = 0; > >>>>> + > >>>>> + if (ret && ret != -ENOENT) > >>>>> + dev_warn(dev, "failed to restore settings on resume: %d\n", ret); > >>>>> + > >>>>> + return 0; > >>>>> +} > >>>>> + > >>>>> +static DEFINE_SIMPLE_DEV_PM_OPS(dptc_pm_ops, NULL, dptc_resume); > >>>>> + > >>>>> +static int dptc_probe(struct platform_device *pdev) > >>>>> +{ > >>>>> + const struct dmi_system_id *dmi_match = dev_get_platdata(&pdev->dev); > >>>>> + struct device *dev = &pdev->dev; > >>>>> + struct dptc_priv *dptc; > >>>>> + int i, ret; > >>>>> + > >>>>> + dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL); > >>>>> + if (!dptc) > >>>>> + return -ENOMEM; > >>>>> + > >>>>> + platform_set_drvdata(pdev, dptc); > >>>>> + > >>>>> + ret = devm_mutex_init(dev, &dptc->lock); > >>>>> + if (ret) > >>>>> + return ret; > >>>>> + > >>>>> + dptc->dev_limits = dmi_match->driver_data; > >>>>> + dev_info(dev, "%s (%s)\n", dmi_match->ident, > >>>>> + boot_cpu_data.x86_model_id); > >>>>> + > >>>>> + dptc->fw_attr_dev = device_create(&firmware_attributes_class, > >>>>> + NULL, MKDEV(0, 0), NULL, DRIVER_NAME); > >>>>> + if (IS_ERR(dptc->fw_attr_dev)) > >>>>> + return PTR_ERR(dptc->fw_attr_dev); > >>>>> + > >>>>> + ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister, > >>>>> + dptc->fw_attr_dev); > >>>>> + if (ret) > >>>>> + return ret; > >>>>> + > >>>>> + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL, > >>>>> + &dptc->fw_attr_dev->kobj); > >>>>> + if (!dptc->fw_attr_kset) > >>>>> + return -ENOMEM; > >>>>> + > >>>>> + ret = devm_add_action_or_reset(dev, dptc_kset_unregister, > >>>>> + dptc->fw_attr_kset); > >>>>> + if (ret) > >>>>> + return ret; > >>>>> + > >>>>> + for (i = 0; i < DPTC_NUM_PARAMS; i++) { > >>>>> + dptc_setup_param_sysfs(dptc, &dptc->params[i], i); > >>>>> + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj, > >>>>> + &dptc->params[i].group); > >>>>> + if (ret) > >>>>> + return ret; > >>>>> + } > >>>>> + > >>>>> + dptc_setup_expanded_sysfs(dptc, &dptc->expanded_attr); > >>>>> + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj, > >>>>> + &dptc->expanded_attr.group); > >>>>> + if (ret) > >>>>> + return ret; > >>>>> + > >>>>> + sysfs_attr_init(&dptc->save_settings_attr.attr); > >>>>> + dptc->save_settings_attr.attr.name = "save_settings"; > >>>>> + dptc->save_settings_attr.attr.mode = 0644; > >>>>> + dptc->save_settings_attr.show = dptc_save_settings_show; > >>>>> + dptc->save_settings_attr.store = dptc_save_settings_store; > >>>>> + ret = sysfs_create_file(&dptc->fw_attr_kset->kobj, > >>>>> + &dptc->save_settings_attr.attr); > >>>>> + if (ret) > >>>>> + return ret; > >>>>> + > >>>>> + return 0; > >>>>> +} > >>>>> + > >>>>> +static struct platform_driver dptc_driver = { > >>>>> + .driver = { > >>>>> + .name = DRIVER_NAME, > >>>>> + .pm = pm_sleep_ptr(&dptc_pm_ops), > >>>>> + }, > >>>>> + .probe = dptc_probe, > >>>>> +}; > >>>>> + > >>>>> +static int __init dptc_init(void) > >>>>> +{ > >>>>> + const struct dmi_system_id *match; > >>>>> + bool soc_found = false; > >>>>> + int i, ret; > >>>>> + > >>>>> + match = dmi_first_match(dptc_dmi_table); > >>>>> + if (!match) > >>>>> + return -ENODEV; > >>>>> + > >>>>> + if (!acpi_has_method(NULL, ALIB_PATH)) { > >>>>> + pr_warn("ALIB method not present\n"); > >>>>> + return -ENODEV; > >>>>> + } > >>>>> + > >>>>> + for (i = 0; dptc_soc_table[i]; i++) { > >>>>> + if (strstr(boot_cpu_data.x86_model_id, > >>>>> + dptc_soc_table[i])) { > >>>>> + soc_found = true; > >>>>> + break; > >>>>> + } > >>>>> + } > >>>>> + if (!soc_found) { > >>>>> + pr_warn("unrecognized SoC '%s'\n", > >>>>> + boot_cpu_data.x86_model_id); > >>>>> + return -ENODEV; > >>>>> + } > >>>>> + > >>>>> + dptc_pdev = platform_device_register_data(NULL, DRIVER_NAME, -1, > >>>>> + match, sizeof(*match)); > >>>>> + if (IS_ERR(dptc_pdev)) > >>>>> + return PTR_ERR(dptc_pdev); > >>>>> + > >>>>> + ret = platform_driver_register(&dptc_driver); > >>>>> + if (ret) { > >>>>> + platform_device_unregister(dptc_pdev); > >>>>> + return ret; > >>>>> + } > >>>>> + > >>>>> + return 0; > >>>>> +} > >>>>> + > >>>>> +static void __exit dptc_exit(void) > >>>>> +{ > >>>>> + platform_driver_unregister(&dptc_driver); > >>>>> + platform_device_unregister(dptc_pdev); > >>>>> +} > >>>>> + > >>>>> +module_init(dptc_init); > >>>>> +module_exit(dptc_exit); > >>>>> + > >>>>> +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>"); > >>>>> +MODULE_DESCRIPTION("AMD DPTCi ACPI Driver"); > >>>>> +MODULE_LICENSE("GPL"); > >>>> > >>>> > >>> > >> > >> > > > > ^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [RFC v4 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver 2026-03-12 13:47 ` Antheas Kapenekakis @ 2026-03-12 16:05 ` Mario Limonciello 2026-03-12 16:19 ` Antheas Kapenekakis 0 siblings, 1 reply; 21+ messages in thread From: Mario Limonciello @ 2026-03-12 16:05 UTC (permalink / raw) To: Antheas Kapenekakis Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato, i, linux-kernel, platform-driver-x86 >> >> So please; do not make assumptions based on a lack of hard data. >> If you don't have data for multiple years of a system or multiple chips >> in the system, leave it off your quirk list. They can always be added >> later when the data is available. > > Specifically for GPD, the userspace implementation has been tested on > all generations. It is fine to choose a (lower) limit that works on > all of them. The thermals did not change between generations. > Saturation above 20W means that there is little difference between > 22W, 25W, 28W, 30W, etc. If there is doubt, going lower is fine. Even > if one device can do 5W more it does not matter (but the chassis, > battery, and cooler are exactly the same; only the board was revised > for the new SoC). > > 8-20W is the sweet spot for these APUs. 8W and 15W are sane for > low-power/balanced. The only question is how close to 30W should be > the maximum and where performance should point to. This version does > 25W for performance. 20W might be better for daily use until AC/DC > detection. > > We do not need to complicate things. All this driver needs to do is > expose a slider from 4-30W. This covers 6800-HX370 for all vendors and > matches user expectation. AI Max will need further research as I do > not have a lot of data on its performance curve. Ayaneo had some drops > between 2022-2023 that had lower TDP limits, but there is less than 1k > units in existence. User expectation and reality aren't necessarily the same. Performance is tied to a thermal design. A user might want to run a handheld at the max performance, but max performance is not purely a function of the APU. It's a function of the APU + cooling solution + power distribution. That's why I have been saying there need to be different values for different parts, and you can't make assumptions year to year. Even if the external chassis plastics are identical, a different fan or heatpipe might be used and those are non-trivial impacts. The values used should be calculated by performance measurements of the entire solution in a thermal chamber. I know you're not going to be able to do that, but that's what manufacturers /should/ be doing. ^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [RFC v4 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver 2026-03-12 16:05 ` Mario Limonciello @ 2026-03-12 16:19 ` Antheas Kapenekakis 0 siblings, 0 replies; 21+ messages in thread From: Antheas Kapenekakis @ 2026-03-12 16:19 UTC (permalink / raw) To: Mario Limonciello Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato, i, linux-kernel, platform-driver-x86 On Thu, 12 Mar 2026 at 17:06, Mario Limonciello <mario.limonciello@amd.com> wrote: > > > >> > >> So please; do not make assumptions based on a lack of hard data. > >> If you don't have data for multiple years of a system or multiple chips > >> in the system, leave it off your quirk list. They can always be added > >> later when the data is available. > > > > Specifically for GPD, the userspace implementation has been tested on > > all generations. It is fine to choose a (lower) limit that works on > > all of them. The thermals did not change between generations. > > Saturation above 20W means that there is little difference between > > 22W, 25W, 28W, 30W, etc. If there is doubt, going lower is fine. Even > > if one device can do 5W more it does not matter (but the chassis, > > battery, and cooler are exactly the same; only the board was revised > > for the new SoC). > > > > 8-20W is the sweet spot for these APUs. 8W and 15W are sane for > > low-power/balanced. The only question is how close to 30W should be > > the maximum and where performance should point to. This version does > > 25W for performance. 20W might be better for daily use until AC/DC > > detection. > > > > We do not need to complicate things. All this driver needs to do is > > expose a slider from 4-30W. This covers 6800-HX370 for all vendors and > > matches user expectation. AI Max will need further research as I do > > not have a lot of data on its performance curve. Ayaneo had some drops > > between 2022-2023 that had lower TDP limits, but there is less than 1k > > units in existence. > > User expectation and reality aren't necessarily the same. > > Performance is tied to a thermal design. A user might want to run a > handheld at the max performance, but max performance is not purely a > function of the APU. > > It's a function of the APU + cooling solution + power distribution. > > That's why I have been saying there need to be different values for > different parts, and you can't make assumptions year to year. > > Even if the external chassis plastics are identical, a different fan or > heatpipe might be used and those are non-trivial impacts. > > The values used should be calculated by performance measurements of the > entire solution in a thermal chamber. > > I know you're not going to be able to do that, but that's what > manufacturers /should/ be doing. Yes. Pretty much. For this class of devices max performance is not that important. Users value being able to select 18W over 20W because that means they get an additional 30m of battery life more than say running at 30W. So it is not that big of a deal to go a bit lower. 30w vs 20w is already 15% tops but the resulting dc in is 55W vs 27W. I know that when it comes to manufacturers, an additional consideration is having an edge over competition in mainstream benchmarks for max performance, so aggressive tuning matters. Esp. for laptops. But mainstream benchmarks reach for the TDP slider now for these devices so that is off the table. This is because they also want to account for battery life. "Manufacturer X gets 20% more performance" is meaningless when you get half the battery. Not only that, but if the presets are too aggressively tuned, users skip them and use the slider anyway. Antheas ^ permalink raw reply [flat|nested] 21+ messages in thread
* [RFC v4 3/4] platform/x86/amd: dptc: Add platform profile support 2026-03-09 20:51 [RFC v4 0/4] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Antheas Kapenekakis 2026-03-09 20:51 ` [RFC v4 1/4] Documentation: firmware-attributes: generalize save_settings entry Antheas Kapenekakis 2026-03-09 20:51 ` [RFC v4 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver Antheas Kapenekakis @ 2026-03-09 20:51 ` Antheas Kapenekakis 2026-03-10 4:07 ` Mario Limonciello 2026-03-09 20:51 ` [RFC v4 4/4] platform/x86/amd: dptc: Add device entries for handheld PCs Antheas Kapenekakis 2026-03-10 2:43 ` [RFC v4 0/4] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Mario Limonciello 4 siblings, 1 reply; 21+ messages in thread From: Antheas Kapenekakis @ 2026-03-09 20:51 UTC (permalink / raw) To: Mario.Limonciello Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato, i, linux-kernel, platform-driver-x86, Antheas Kapenekakis Register a platform_profile handler so the driver exposes standard power profiles (low-power, balanced, performance) alongside the manual tunable interface. When a non-custom profile is active, parameter writes are blocked (-EBUSY) and current_value reflects the profile's preset values. Selecting the "custom" profile returns control to the user for manual staging and committing. On resume, the active profile is automatically re-applied. Assisted-by: Claude:claude-opus-4-6 Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev> --- drivers/platform/x86/amd/Kconfig | 1 + drivers/platform/x86/amd/dptc.c | 109 ++++++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/drivers/platform/x86/amd/Kconfig b/drivers/platform/x86/amd/Kconfig index d610092467fc..41ffbd722524 100644 --- a/drivers/platform/x86/amd/Kconfig +++ b/drivers/platform/x86/amd/Kconfig @@ -48,6 +48,7 @@ config AMD_ISP_PLATFORM config AMD_DPTC tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)" depends on X86_64 && ACPI && DMI + select ACPI_PLATFORM_PROFILE select FIRMWARE_ATTRIBUTES_CLASS help Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c index b884cdfa3f82..f4db95affb1b 100644 --- a/drivers/platform/x86/amd/dptc.c +++ b/drivers/platform/x86/amd/dptc.c @@ -23,6 +23,7 @@ #include <linux/module.h> #include <linux/mutex.h> #include <linux/platform_device.h> +#include <linux/platform_profile.h> #include <linux/processor.h> #include <linux/sysfs.h> #include <linux/unaligned.h> @@ -56,8 +57,13 @@ struct dptc_param_limits { u32 expanded_max; }; +struct dptc_profile { + u32 vals[DPTC_NUM_PARAMS]; /* 0 = don't set / unstage this param */ +}; + struct dptc_device_limits { struct dptc_param_limits params[DPTC_NUM_PARAMS]; + struct dptc_profile profiles[PLATFORM_PROFILE_LAST]; }; struct dptc_param_desc { @@ -88,6 +94,11 @@ static const struct dptc_device_limits limits_maxhh = { [DPTC_PPT_PL3_FPPT] = { 1, 4, 40, 85, 100 }, [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 }, }, + .profiles = { + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 15, 15, 25, 0 } }, + [PLATFORM_PROFILE_BALANCED] = { .vals = { 25, 27, 40, 0 } }, + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 60, 63, 85, 0 } }, + }, }; /* Substring matches against boot_cpu_data.x86_model_id; order matters. */ @@ -139,11 +150,14 @@ struct dptc_priv { bool expanded; + enum platform_profile_option profile; + struct device *ppdev; + enum dptc_save_mode save_mode; u32 staged[DPTC_NUM_PARAMS]; - /* Protects staged, expanded, and save_mode */ + /* Protects staged, expanded, save_mode, and profile */ struct mutex lock; struct dptc_attr_sysfs params[DPTC_NUM_PARAMS]; @@ -271,6 +285,14 @@ static ssize_t dptc_current_value_show(struct kobject *kobj, guard(mutex)(&dptc->lock); + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) { + u32 val = dptc->dev_limits->profiles[dptc->profile].vals[ps->idx]; + + if (!val) + return sysfs_emit(buf, "\n"); + return sysfs_emit(buf, "%u\n", val); + } + if (!dptc->staged[ps->idx]) return sysfs_emit(buf, "\n"); return sysfs_emit(buf, "%u\n", dptc->staged[ps->idx]); @@ -288,6 +310,9 @@ static ssize_t dptc_current_value_store(struct kobject *kobj, guard(mutex)(&dptc->lock); + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) + return -EBUSY; + if (count == 1 && buf[0] == '\n') { dptc->staged[ps->idx] = 0; return count; @@ -425,6 +450,9 @@ static ssize_t dptc_expanded_current_value_store(struct kobject *kobj, guard(mutex)(&dptc->lock); + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) + return -EBUSY; + dptc->expanded = val; /* Clear staged values: limits changed, old values may be out of range */ memset(dptc->staged, 0, sizeof(dptc->staged)); @@ -593,6 +621,75 @@ static void dptc_kset_unregister(void *data) kset_unregister(data); } +/* Platform profile */ + +static int dptc_apply_profile(struct dptc_priv *dptc, + enum platform_profile_option profile) +{ + const struct dptc_profile *pp; + int i; + + memset(dptc->staged, 0, sizeof(dptc->staged)); + + if (profile == PLATFORM_PROFILE_CUSTOM) + return 0; + + pp = &dptc->dev_limits->profiles[profile]; + for (i = 0; i < DPTC_NUM_PARAMS; i++) { + if (!pp->vals[i]) + continue; + dptc->staged[i] = pp->vals[i]; + } + + return dptc_alib_save(dptc); +} + +static int dptc_pp_probe(void *drvdata, unsigned long *choices) +{ + struct dptc_priv *dptc = drvdata; + int i, j; + + set_bit(PLATFORM_PROFILE_CUSTOM, choices); + for (i = 0; i < PLATFORM_PROFILE_LAST; i++) { + for (j = 0; j < DPTC_NUM_PARAMS; j++) { + if (dptc->dev_limits->profiles[i].vals[j]) { + set_bit(i, choices); + break; + } + } + } + return 0; +} + +static int dptc_pp_get(struct device *dev, + enum platform_profile_option *profile) +{ + struct dptc_priv *dptc = dev_get_drvdata(dev); + + guard(mutex)(&dptc->lock); + + *profile = dptc->profile; + return 0; +} + +static int dptc_pp_set(struct device *dev, + enum platform_profile_option profile) +{ + struct dptc_priv *dptc = dev_get_drvdata(dev); + + guard(mutex)(&dptc->lock); + + dptc->profile = profile; + + return dptc_apply_profile(dptc, profile); +} + +static const struct platform_profile_ops dptc_pp_ops = { + .probe = dptc_pp_probe, + .profile_get = dptc_pp_get, + .profile_set = dptc_pp_set, +}; + static int dptc_resume(struct device *dev) { struct dptc_priv *dptc = dev_get_drvdata(dev); @@ -601,7 +698,9 @@ static int dptc_resume(struct device *dev) guard(mutex)(&dptc->lock); /* In bulk mode, do not use pm ops for userspace flexibility. */ - if (dptc->save_mode == SAVE_SINGLE) + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) + ret = dptc_apply_profile(dptc, dptc->profile); + else if (dptc->save_mode == SAVE_SINGLE) ret = dptc_alib_save(dptc); else ret = 0; @@ -679,6 +778,12 @@ static int dptc_probe(struct platform_device *pdev) if (ret) return ret; + dptc->profile = PLATFORM_PROFILE_CUSTOM; + dptc->ppdev = devm_platform_profile_register(dev, "amd-dptc", dptc, + &dptc_pp_ops); + if (IS_ERR(dptc->ppdev)) + return PTR_ERR(dptc->ppdev); + return 0; } -- 2.52.0 ^ permalink raw reply related [flat|nested] 21+ messages in thread
* Re: [RFC v4 3/4] platform/x86/amd: dptc: Add platform profile support 2026-03-09 20:51 ` [RFC v4 3/4] platform/x86/amd: dptc: Add platform profile support Antheas Kapenekakis @ 2026-03-10 4:07 ` Mario Limonciello 0 siblings, 0 replies; 21+ messages in thread From: Mario Limonciello @ 2026-03-10 4:07 UTC (permalink / raw) To: Antheas Kapenekakis Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato, i, linux-kernel, platform-driver-x86 On 3/9/2026 3:51 PM, Antheas Kapenekakis wrote: > Register a platform_profile handler so the driver exposes standard > power profiles (low-power, balanced, performance) alongside the manual > tunable interface. > > When a non-custom profile is active, parameter writes are blocked > (-EBUSY) and current_value reflects the profile's preset values. > Selecting the "custom" profile returns control to the user for manual > staging and committing. On resume, the active profile is automatically > re-applied. > > Assisted-by: Claude:claude-opus-4-6 > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev> > --- > drivers/platform/x86/amd/Kconfig | 1 + > drivers/platform/x86/amd/dptc.c | 109 ++++++++++++++++++++++++++++++- > 2 files changed, 108 insertions(+), 2 deletions(-) > > diff --git a/drivers/platform/x86/amd/Kconfig b/drivers/platform/x86/amd/Kconfig > index d610092467fc..41ffbd722524 100644 > --- a/drivers/platform/x86/amd/Kconfig > +++ b/drivers/platform/x86/amd/Kconfig > @@ -48,6 +48,7 @@ config AMD_ISP_PLATFORM > config AMD_DPTC > tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)" > depends on X86_64 && ACPI && DMI > + select ACPI_PLATFORM_PROFILE > select FIRMWARE_ATTRIBUTES_CLASS > help > Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and Your description doesn't talk about platform profile (which I expect is what the default should be for 99% of people). > diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c > index b884cdfa3f82..f4db95affb1b 100644 > --- a/drivers/platform/x86/amd/dptc.c > +++ b/drivers/platform/x86/amd/dptc.c > @@ -23,6 +23,7 @@ > #include <linux/module.h> > #include <linux/mutex.h> > #include <linux/platform_device.h> > +#include <linux/platform_profile.h> > #include <linux/processor.h> > #include <linux/sysfs.h> > #include <linux/unaligned.h> > @@ -56,8 +57,13 @@ struct dptc_param_limits { > u32 expanded_max; > }; > > +struct dptc_profile { > + u32 vals[DPTC_NUM_PARAMS]; /* 0 = don't set / unstage this param */ > +}; > + > struct dptc_device_limits { > struct dptc_param_limits params[DPTC_NUM_PARAMS]; > + struct dptc_profile profiles[PLATFORM_PROFILE_LAST]; > }; > > struct dptc_param_desc { > @@ -88,6 +94,11 @@ static const struct dptc_device_limits limits_maxhh = { > [DPTC_PPT_PL3_FPPT] = { 1, 4, 40, 85, 100 }, > [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 }, > }, > + .profiles = { > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 15, 15, 25, 0 } }, > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 25, 27, 40, 0 } }, > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 60, 63, 85, 0 } }, > + }, > }; > > /* Substring matches against boot_cpu_data.x86_model_id; order matters. */ > @@ -139,11 +150,14 @@ struct dptc_priv { > > bool expanded; > > + enum platform_profile_option profile; > + struct device *ppdev; > + > enum dptc_save_mode save_mode; > > u32 staged[DPTC_NUM_PARAMS]; > > - /* Protects staged, expanded, and save_mode */ > + /* Protects staged, expanded, save_mode, and profile */ > struct mutex lock; > > struct dptc_attr_sysfs params[DPTC_NUM_PARAMS]; > @@ -271,6 +285,14 @@ static ssize_t dptc_current_value_show(struct kobject *kobj, > > guard(mutex)(&dptc->lock); > > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) { > + u32 val = dptc->dev_limits->profiles[dptc->profile].vals[ps->idx]; > + > + if (!val) > + return sysfs_emit(buf, "\n"); > + return sysfs_emit(buf, "%u\n", val); > + } > + > if (!dptc->staged[ps->idx]) > return sysfs_emit(buf, "\n"); > return sysfs_emit(buf, "%u\n", dptc->staged[ps->idx]); > @@ -288,6 +310,9 @@ static ssize_t dptc_current_value_store(struct kobject *kobj, > > guard(mutex)(&dptc->lock); > > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) > + return -EBUSY; > + > if (count == 1 && buf[0] == '\n') { > dptc->staged[ps->idx] = 0; > return count; > @@ -425,6 +450,9 @@ static ssize_t dptc_expanded_current_value_store(struct kobject *kobj, > > guard(mutex)(&dptc->lock); > > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) > + return -EBUSY; > + > dptc->expanded = val; > /* Clear staged values: limits changed, old values may be out of range */ > memset(dptc->staged, 0, sizeof(dptc->staged)); > @@ -593,6 +621,75 @@ static void dptc_kset_unregister(void *data) > kset_unregister(data); > } > > +/* Platform profile */ > + > +static int dptc_apply_profile(struct dptc_priv *dptc, > + enum platform_profile_option profile) > +{ > + const struct dptc_profile *pp; > + int i; > + > + memset(dptc->staged, 0, sizeof(dptc->staged)); > + > + if (profile == PLATFORM_PROFILE_CUSTOM) > + return 0; > + > + pp = &dptc->dev_limits->profiles[profile]; > + for (i = 0; i < DPTC_NUM_PARAMS; i++) { > + if (!pp->vals[i]) > + continue; > + dptc->staged[i] = pp->vals[i]; > + } > + > + return dptc_alib_save(dptc); > +} > + > +static int dptc_pp_probe(void *drvdata, unsigned long *choices) > +{ > + struct dptc_priv *dptc = drvdata; > + int i, j; > + > + set_bit(PLATFORM_PROFILE_CUSTOM, choices); > + for (i = 0; i < PLATFORM_PROFILE_LAST; i++) { > + for (j = 0; j < DPTC_NUM_PARAMS; j++) { > + if (dptc->dev_limits->profiles[i].vals[j]) { > + set_bit(i, choices); > + break; > + } > + } > + } > + return 0; > +} > + > +static int dptc_pp_get(struct device *dev, > + enum platform_profile_option *profile) > +{ > + struct dptc_priv *dptc = dev_get_drvdata(dev); > + > + guard(mutex)(&dptc->lock); > + > + *profile = dptc->profile; > + return 0; > +} > + > +static int dptc_pp_set(struct device *dev, > + enum platform_profile_option profile) > +{ > + struct dptc_priv *dptc = dev_get_drvdata(dev); > + > + guard(mutex)(&dptc->lock); > + > + dptc->profile = profile; > + > + return dptc_apply_profile(dptc, profile); > +} > + > +static const struct platform_profile_ops dptc_pp_ops = { > + .probe = dptc_pp_probe, > + .profile_get = dptc_pp_get, > + .profile_set = dptc_pp_set, > +}; > + > static int dptc_resume(struct device *dev) > { > struct dptc_priv *dptc = dev_get_drvdata(dev); > @@ -601,7 +698,9 @@ static int dptc_resume(struct device *dev) > guard(mutex)(&dptc->lock); > > /* In bulk mode, do not use pm ops for userspace flexibility. */ > - if (dptc->save_mode == SAVE_SINGLE) > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) > + ret = dptc_apply_profile(dptc, dptc->profile); > + else if (dptc->save_mode == SAVE_SINGLE) > ret = dptc_alib_save(dptc); > else > ret = 0; > @@ -679,6 +778,12 @@ static int dptc_probe(struct platform_device *pdev) > if (ret) > return ret; > > + dptc->profile = PLATFORM_PROFILE_CUSTOM; > + dptc->ppdev = devm_platform_profile_register(dev, "amd-dptc", dptc, > + &dptc_pp_ops); > + if (IS_ERR(dptc->ppdev)) > + return PTR_ERR(dptc->ppdev); > + > return 0; > } > ^ permalink raw reply [flat|nested] 21+ messages in thread
* [RFC v4 4/4] platform/x86/amd: dptc: Add device entries for handheld PCs 2026-03-09 20:51 [RFC v4 0/4] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Antheas Kapenekakis ` (2 preceding siblings ...) 2026-03-09 20:51 ` [RFC v4 3/4] platform/x86/amd: dptc: Add platform profile support Antheas Kapenekakis @ 2026-03-09 20:51 ` Antheas Kapenekakis 2026-03-10 4:11 ` Mario Limonciello 2026-03-10 2:43 ` [RFC v4 0/4] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Mario Limonciello 4 siblings, 1 reply; 21+ messages in thread From: Antheas Kapenekakis @ 2026-03-09 20:51 UTC (permalink / raw) To: Mario.Limonciello Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato, i, linux-kernel, platform-driver-x86, Antheas Kapenekakis Add limit tables and DMI entries for AMD APU-based handheld devices: - 15W class: AYANEO AIR - 18W class: AYANEO AIR Plus/Pro - 25W class: AYANEO AIR 1S, NEXT/KUN - 28W class: GPD Win Mini/4/Max 2, GPD Duo, GPD Pocket 4, AYANEO 2, OrangePi NEO-01, SuiPlay0X1 - 30W class: AYANEO FLIP/GEEK/SLIDE/3, AOKZOE A1/A1X/A2, OneXPlayer F1Pro/2/X1/G1, AYN Loki Max, Zeenix Pro Also add SoC strings for Ryzen AI, Ryzen 8000/7040/6000/5000 families, extending support beyond the initial AI MAX series. Assisted-by: Claude:claude-opus-4-6 Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev> --- drivers/platform/x86/amd/dptc.c | 420 ++++++++++++++++++++++++++++++++ 1 file changed, 420 insertions(+) diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c index f4db95affb1b..b2820f8652f1 100644 --- a/drivers/platform/x86/amd/dptc.c +++ b/drivers/platform/x86/amd/dptc.c @@ -86,6 +86,81 @@ static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = { 1, ALIB_ID_TEMP_TARGET }, }; +/* 15W class: AYANEO AIR (Ryzen 5 5560U) */ +static const struct dptc_device_limits limits_15w = { + .params = { + [DPTC_PPT_PL1_SPL] = { 1, 5, 10, 15, 18 }, + [DPTC_PPT_PL2_SPPT] = { 1, 5, 10, 15, 18 }, + [DPTC_PPT_PL3_FPPT] = { 1, 5, 12, 18, 22 }, + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, + }, + .profiles = { + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } }, + [PLATFORM_PROFILE_BALANCED] = { .vals = { 10, 12, 15, 0 } }, + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 15, 15, 18, 0 } }, + }, +}; + +/* 18W class: AYANEO AIR Plus/Pro (Ryzen 5 5560U, Ryzen 7 5825U) */ +static const struct dptc_device_limits limits_18w = { + .params = { + [DPTC_PPT_PL1_SPL] = { 1, 5, 15, 18, 22 }, + [DPTC_PPT_PL2_SPPT] = { 1, 5, 15, 18, 22 }, + [DPTC_PPT_PL3_FPPT] = { 1, 5, 15, 20, 25 }, + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, + }, + .profiles = { + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } }, + [PLATFORM_PROFILE_BALANCED] = { .vals = { 12, 14, 15, 0 } }, + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 18, 18, 20, 0 } }, + }, +}; + +/* 25W class: Ryzen 5000 handhelds (AYANEO NEXT, KUN) */ +static const struct dptc_device_limits limits_25w = { + .params = { + [DPTC_PPT_PL1_SPL] = { 1, 4, 15, 25, 32 }, + [DPTC_PPT_PL2_SPPT] = { 1, 4, 20, 27, 35 }, + [DPTC_PPT_PL3_FPPT] = { 1, 4, 25, 30, 37 }, + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, + }, + .profiles = { + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } }, + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 20, 0 } }, + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 27, 30, 0 } }, + }, +}; + +/* 28W class: GPD Win series, AYANEO 2, OrangePi NEO-01 */ +static const struct dptc_device_limits limits_28w = { + .params = { + [DPTC_PPT_PL1_SPL] = { 1, 4, 15, 28, 32 }, + [DPTC_PPT_PL2_SPPT] = { 1, 4, 20, 30, 35 }, + [DPTC_PPT_PL3_FPPT] = { 1, 4, 25, 32, 37 }, + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, + }, + .profiles = { + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } }, + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 22, 0 } }, + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 32, 0 } }, + }, +}; + +/* 30W class: OneXPlayer, AYANEO FLIP/GEEK/SLIDE/3, AOKZOE */ +static const struct dptc_device_limits limits_30w = { + .params = { + [DPTC_PPT_PL1_SPL] = { 1, 4, 15, 30, 40 }, + [DPTC_PPT_PL2_SPPT] = { 1, 4, 20, 32, 43 }, + [DPTC_PPT_PL3_FPPT] = { 1, 4, 25, 41, 50 }, + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, + }, + .profiles = { + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } }, + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 25, 0 } }, + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 41, 0 } }, + }, +}; + /* AI MAX Handheld class: GPD Win 5 */ static const struct dptc_device_limits limits_maxhh = { .params = { @@ -107,11 +182,56 @@ static const char * const dptc_soc_table[] = { "AMD RYZEN AI MAX+ 395", "AMD RYZEN AI MAX+ 385", "AMD RYZEN AI MAX 380", + /* Ryzen AI */ + "AMD Ryzen AI 9 HX 370", + /* Ryzen 8000 */ + "AMD Ryzen 7 8840U", + /* Ryzen 7040 */ + "AMD Ryzen 7 7840U", + /* Ryzen 6000 */ + "AMD Ryzen 7 6800U", + "AMD Ryzen 7 6600U", + /* Ryzen 5000 */ + "AMD Ryzen 7 5800U", + "AMD Ryzen 7 5700U", + "AMD Ryzen 5 5560U", NULL, }; static const struct dmi_system_id dptc_dmi_table[] = { /* GPD */ + { + .ident = "GPD Win Mini", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-01"), + }, + .driver_data = (void *)&limits_28w, + }, + { + .ident = "GPD Win Mini 2024", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02"), + }, + .driver_data = (void *)&limits_28w, + }, + { + .ident = "GPD Win Mini 2024", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02-L"), + }, + .driver_data = (void *)&limits_28w, + }, + { + .ident = "GPD Win 4", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-04"), + }, + .driver_data = (void *)&limits_28w, + }, { .ident = "GPD Win 5", .matches = { @@ -120,6 +240,306 @@ static const struct dmi_system_id dptc_dmi_table[] = { }, .driver_data = (void *)&limits_maxhh, }, + { + .ident = "GPD Win Max 2", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-04"), + }, + .driver_data = (void *)&limits_28w, + }, + { + .ident = "GPD Win Max 2 2024", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-05"), + }, + .driver_data = (void *)&limits_28w, + }, + { + .ident = "GPD Duo", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01"), + }, + .driver_data = (void *)&limits_28w, + }, + { + .ident = "GPD Duo", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01-L"), + }, + .driver_data = (void *)&limits_28w, + }, + { + .ident = "GPD Pocket 4", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04"), + }, + .driver_data = (void *)&limits_28w, + }, + { + .ident = "GPD Pocket 4", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04-L"), + }, + .driver_data = (void *)&limits_28w, + }, + /* OrangePi */ + { + .ident = "OrangePi NEO-01", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"), + }, + .driver_data = (void *)&limits_28w, + }, + /* AYN */ + { + .ident = "AYN Loki Max", + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ayn"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Loki Max"), + }, + .driver_data = (void *)&limits_30w, + }, + /* Tectoy (Zeenix Pro = Loki Max) */ + { + .ident = "Zeenix Pro", + .matches = { + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Tectoy"), + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Zeenix Pro"), + }, + .driver_data = (void *)&limits_30w, + }, + /* AOKZOE */ + { + .ident = "AOKZOE A1 AR07", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 AR07"), + }, + .driver_data = (void *)&limits_30w, + }, + { + .ident = "AOKZOE A1 Pro", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 Pro"), + }, + .driver_data = (void *)&limits_30w, + }, + { + .ident = "AOKZOE A1X", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1X"), + }, + .driver_data = (void *)&limits_30w, + }, + { + .ident = "AOKZOE A2 Pro", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A2 Pro"), + }, + .driver_data = (void *)&limits_30w, + }, + /* OneXPlayer (Intel variants filtered by SoC table) */ + { + .ident = "ONEXPLAYER F1Pro", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1Pro"), + }, + .driver_data = (void *)&limits_30w, + }, + { + .ident = "ONEXPLAYER F1 EVA-02", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1 EVA-02"), + }, + .driver_data = (void *)&limits_30w, + }, + { + .ident = "ONEXPLAYER 2", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_MATCH(DMI_BOARD_NAME, "ONEXPLAYER 2"), + }, + .driver_data = (void *)&limits_30w, + }, + { + .ident = "ONEXPLAYER X1 A", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1 A"), + }, + .driver_data = (void *)&limits_30w, + }, + { + .ident = "ONEXPLAYER X1z", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1z"), + }, + .driver_data = (void *)&limits_30w, + }, + { + .ident = "ONEXPLAYER X1Pro", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Pro"), + }, + .driver_data = (void *)&limits_30w, + }, + { + .ident = "ONEXPLAYER G1 A", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 A"), + }, + .driver_data = (void *)&limits_30w, + }, + /* AYANEO - 15W */ + { + .ident = "AYANEO AIR", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR"), + }, + .driver_data = (void *)&limits_15w, + }, + /* AYANEO - 18W */ + { + .ident = "AYANEO AIR Plus", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Plus"), + }, + .driver_data = (void *)&limits_18w, + }, + { + .ident = "AYANEO AIR Pro", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Pro"), + }, + .driver_data = (void *)&limits_18w, + }, + /* AYANEO - 25W */ + { + .ident = "AYANEO AIR 1S", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_MATCH(DMI_BOARD_NAME, "AIR 1S"), + }, + .driver_data = (void *)&limits_25w, + }, + { + .ident = "AYANEO NEXT Advance", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Advance"), + }, + .driver_data = (void *)&limits_25w, + }, + { + .ident = "AYANEO NEXT Lite", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Lite"), + }, + .driver_data = (void *)&limits_25w, + }, + { + .ident = "AYANEO NEXT Pro", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Pro"), + }, + .driver_data = (void *)&limits_25w, + }, + { + .ident = "AYANEO NEXT", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT"), + }, + .driver_data = (void *)&limits_25w, + }, + { + .ident = "AYANEO KUN", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"), + }, + .driver_data = (void *)&limits_25w, + }, + { + .ident = "AYANEO KUN", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO KUN"), + }, + .driver_data = (void *)&limits_25w, + }, + /* AYANEO - 28W */ + { + .ident = "AYANEO 2", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 2"), + }, + .driver_data = (void *)&limits_28w, + }, + { + .ident = "SuiPlay0X1", + .matches = { + DMI_MATCH(DMI_SYS_VENDOR, "Mysten Labs, Inc."), + DMI_MATCH(DMI_PRODUCT_NAME, "SuiPlay0X1"), + }, + .driver_data = (void *)&limits_28w, + }, + /* AYANEO - 30W */ + { + /* DMI_MATCH catches all FLIP variants (DS, KB, 1S DS, 1S KB) */ + .ident = "AYANEO FLIP", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_MATCH(DMI_BOARD_NAME, "FLIP"), + }, + .driver_data = (void *)&limits_30w, + }, + { + /* DMI_MATCH catches GEEK and GEEK 1S */ + .ident = "AYANEO GEEK", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_MATCH(DMI_BOARD_NAME, "GEEK"), + }, + .driver_data = (void *)&limits_30w, + }, + { + .ident = "AYANEO SLIDE", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "SLIDE"), + }, + .driver_data = (void *)&limits_30w, + }, + { + .ident = "AYANEO 3", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO 3"), + }, + .driver_data = (void *)&limits_30w, + }, { } }; MODULE_DEVICE_TABLE(dmi, dptc_dmi_table); -- 2.52.0 ^ permalink raw reply related [flat|nested] 21+ messages in thread
* Re: [RFC v4 4/4] platform/x86/amd: dptc: Add device entries for handheld PCs 2026-03-09 20:51 ` [RFC v4 4/4] platform/x86/amd: dptc: Add device entries for handheld PCs Antheas Kapenekakis @ 2026-03-10 4:11 ` Mario Limonciello 2026-03-10 8:13 ` Antheas Kapenekakis 0 siblings, 1 reply; 21+ messages in thread From: Mario Limonciello @ 2026-03-10 4:11 UTC (permalink / raw) To: Antheas Kapenekakis Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato, i, linux-kernel, platform-driver-x86 On 3/9/2026 3:51 PM, Antheas Kapenekakis wrote: > Add limit tables and DMI entries for AMD APU-based handheld devices: > > - 15W class: AYANEO AIR > - 18W class: AYANEO AIR Plus/Pro > - 25W class: AYANEO AIR 1S, NEXT/KUN > - 28W class: GPD Win Mini/4/Max 2, GPD Duo, GPD Pocket 4, AYANEO 2, > OrangePi NEO-01, SuiPlay0X1 > - 30W class: AYANEO FLIP/GEEK/SLIDE/3, AOKZOE A1/A1X/A2, > OneXPlayer F1Pro/2/X1/G1, AYN Loki Max, Zeenix Pro > > Also add SoC strings for Ryzen AI, Ryzen 8000/7040/6000/5000 > families, extending support beyond the initial AI MAX series. There are a lot of numbers here. Where did they all come from? What are guesses? What came from RE? What came from documentation? How do you know these are good for the thermal design of the platform? I appreciate some of them could be cargo culted from another codebase, but I think it would be a good idea to document which ones are more likely to be accurate and why. > > Assisted-by: Claude:claude-opus-4-6 > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev> > --- > drivers/platform/x86/amd/dptc.c | 420 ++++++++++++++++++++++++++++++++ > 1 file changed, 420 insertions(+) > > diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c > index f4db95affb1b..b2820f8652f1 100644 > --- a/drivers/platform/x86/amd/dptc.c > +++ b/drivers/platform/x86/amd/dptc.c > @@ -86,6 +86,81 @@ static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = { > 1, ALIB_ID_TEMP_TARGET }, > }; > > +/* 15W class: AYANEO AIR (Ryzen 5 5560U) */ > +static const struct dptc_device_limits limits_15w = { > + .params = { > + [DPTC_PPT_PL1_SPL] = { 1, 5, 10, 15, 18 }, > + [DPTC_PPT_PL2_SPPT] = { 1, 5, 10, 15, 18 }, > + [DPTC_PPT_PL3_FPPT] = { 1, 5, 12, 18, 22 }, > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, > + }, > + .profiles = { > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } }, > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 10, 12, 15, 0 } }, > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 15, 15, 18, 0 } }, > + }, > +}; > + > +/* 18W class: AYANEO AIR Plus/Pro (Ryzen 5 5560U, Ryzen 7 5825U) */ > +static const struct dptc_device_limits limits_18w = { > + .params = { > + [DPTC_PPT_PL1_SPL] = { 1, 5, 15, 18, 22 }, > + [DPTC_PPT_PL2_SPPT] = { 1, 5, 15, 18, 22 }, > + [DPTC_PPT_PL3_FPPT] = { 1, 5, 15, 20, 25 }, > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, > + }, > + .profiles = { > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } }, > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 12, 14, 15, 0 } }, > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 18, 18, 20, 0 } }, > + }, > +}; > + > +/* 25W class: Ryzen 5000 handhelds (AYANEO NEXT, KUN) */ > +static const struct dptc_device_limits limits_25w = { > + .params = { > + [DPTC_PPT_PL1_SPL] = { 1, 4, 15, 25, 32 }, > + [DPTC_PPT_PL2_SPPT] = { 1, 4, 20, 27, 35 }, > + [DPTC_PPT_PL3_FPPT] = { 1, 4, 25, 30, 37 }, > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, > + }, > + .profiles = { > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } }, > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 20, 0 } }, > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 27, 30, 0 } }, > + }, > +}; > + > +/* 28W class: GPD Win series, AYANEO 2, OrangePi NEO-01 */ > +static const struct dptc_device_limits limits_28w = { > + .params = { > + [DPTC_PPT_PL1_SPL] = { 1, 4, 15, 28, 32 }, > + [DPTC_PPT_PL2_SPPT] = { 1, 4, 20, 30, 35 }, > + [DPTC_PPT_PL3_FPPT] = { 1, 4, 25, 32, 37 }, > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, > + }, > + .profiles = { > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } }, > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 22, 0 } }, > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 32, 0 } }, > + }, > +}; > + > +/* 30W class: OneXPlayer, AYANEO FLIP/GEEK/SLIDE/3, AOKZOE */ > +static const struct dptc_device_limits limits_30w = { > + .params = { > + [DPTC_PPT_PL1_SPL] = { 1, 4, 15, 30, 40 }, > + [DPTC_PPT_PL2_SPPT] = { 1, 4, 20, 32, 43 }, > + [DPTC_PPT_PL3_FPPT] = { 1, 4, 25, 41, 50 }, > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, > + }, > + .profiles = { > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } }, > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 25, 0 } }, > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 41, 0 } }, > + }, > +}; > + > /* AI MAX Handheld class: GPD Win 5 */ > static const struct dptc_device_limits limits_maxhh = { > .params = { > @@ -107,11 +182,56 @@ static const char * const dptc_soc_table[] = { > "AMD RYZEN AI MAX+ 395", > "AMD RYZEN AI MAX+ 385", > "AMD RYZEN AI MAX 380", > + /* Ryzen AI */ > + "AMD Ryzen AI 9 HX 370", > + /* Ryzen 8000 */ > + "AMD Ryzen 7 8840U", > + /* Ryzen 7040 */ > + "AMD Ryzen 7 7840U", > + /* Ryzen 6000 */ > + "AMD Ryzen 7 6800U", > + "AMD Ryzen 7 6600U", > + /* Ryzen 5000 */ > + "AMD Ryzen 7 5800U", > + "AMD Ryzen 7 5700U", > + "AMD Ryzen 5 5560U", Still don't understand the point of this list. > NULL, > }; > > static const struct dmi_system_id dptc_dmi_table[] = { > /* GPD */ > + { > + .ident = "GPD Win Mini", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-01"), > + }, > + .driver_data = (void *)&limits_28w, > + }, > + { > + .ident = "GPD Win Mini 2024", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02"), > + }, > + .driver_data = (void *)&limits_28w, > + }, > + { > + .ident = "GPD Win Mini 2024", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02-L"), > + }, > + .driver_data = (void *)&limits_28w, > + }, > + { > + .ident = "GPD Win 4", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-04"), > + }, > + .driver_data = (void *)&limits_28w, > + }, > { > .ident = "GPD Win 5", > .matches = { > @@ -120,6 +240,306 @@ static const struct dmi_system_id dptc_dmi_table[] = { > }, > .driver_data = (void *)&limits_maxhh, > }, > + { > + .ident = "GPD Win Max 2", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-04"), > + }, > + .driver_data = (void *)&limits_28w, > + }, > + { > + .ident = "GPD Win Max 2 2024", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-05"), > + }, > + .driver_data = (void *)&limits_28w, > + }, > + { > + .ident = "GPD Duo", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01"), > + }, > + .driver_data = (void *)&limits_28w, > + }, > + { > + .ident = "GPD Duo", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01-L"), > + }, > + .driver_data = (void *)&limits_28w, > + }, > + { > + .ident = "GPD Pocket 4", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04"), > + }, > + .driver_data = (void *)&limits_28w, > + }, > + { > + .ident = "GPD Pocket 4", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04-L"), > + }, > + .driver_data = (void *)&limits_28w, > + }, > + /* OrangePi */ > + { > + .ident = "OrangePi NEO-01", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"), > + }, > + .driver_data = (void *)&limits_28w, > + }, > + /* AYN */ > + { > + .ident = "AYN Loki Max", > + .matches = { > + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ayn"), > + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Loki Max"), > + }, > + .driver_data = (void *)&limits_30w, > + }, > + /* Tectoy (Zeenix Pro = Loki Max) */ > + { > + .ident = "Zeenix Pro", > + .matches = { > + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Tectoy"), > + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Zeenix Pro"), > + }, > + .driver_data = (void *)&limits_30w, > + }, > + /* AOKZOE */ > + { > + .ident = "AOKZOE A1 AR07", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 AR07"), > + }, > + .driver_data = (void *)&limits_30w, > + }, > + { > + .ident = "AOKZOE A1 Pro", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 Pro"), > + }, > + .driver_data = (void *)&limits_30w, > + }, > + { > + .ident = "AOKZOE A1X", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1X"), > + }, > + .driver_data = (void *)&limits_30w, > + }, > + { > + .ident = "AOKZOE A2 Pro", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A2 Pro"), > + }, > + .driver_data = (void *)&limits_30w, > + }, > + /* OneXPlayer (Intel variants filtered by SoC table) */ Are you telling me the Intel variants actually have the same ACPI methods this series looks for? > + { > + .ident = "ONEXPLAYER F1Pro", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1Pro"), > + }, > + .driver_data = (void *)&limits_30w, > + }, > + { > + .ident = "ONEXPLAYER F1 EVA-02", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1 EVA-02"), > + }, > + .driver_data = (void *)&limits_30w, > + }, > + { > + .ident = "ONEXPLAYER 2", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), > + DMI_MATCH(DMI_BOARD_NAME, "ONEXPLAYER 2"), > + }, > + .driver_data = (void *)&limits_30w, > + }, > + { > + .ident = "ONEXPLAYER X1 A", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1 A"), > + }, > + .driver_data = (void *)&limits_30w, > + }, > + { > + .ident = "ONEXPLAYER X1z", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1z"), > + }, > + .driver_data = (void *)&limits_30w, > + }, > + { > + .ident = "ONEXPLAYER X1Pro", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Pro"), > + }, > + .driver_data = (void *)&limits_30w, > + }, > + { > + .ident = "ONEXPLAYER G1 A", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 A"), > + }, > + .driver_data = (void *)&limits_30w, > + }, > + /* AYANEO - 15W */ > + { > + .ident = "AYANEO AIR", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR"), > + }, > + .driver_data = (void *)&limits_15w, > + }, > + /* AYANEO - 18W */ > + { > + .ident = "AYANEO AIR Plus", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Plus"), > + }, > + .driver_data = (void *)&limits_18w, > + }, > + { > + .ident = "AYANEO AIR Pro", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Pro"), > + }, > + .driver_data = (void *)&limits_18w, > + }, > + /* AYANEO - 25W */ > + { > + .ident = "AYANEO AIR 1S", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > + DMI_MATCH(DMI_BOARD_NAME, "AIR 1S"), > + }, > + .driver_data = (void *)&limits_25w, > + }, > + { > + .ident = "AYANEO NEXT Advance", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Advance"), > + }, > + .driver_data = (void *)&limits_25w, > + }, > + { > + .ident = "AYANEO NEXT Lite", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Lite"), > + }, > + .driver_data = (void *)&limits_25w, > + }, > + { > + .ident = "AYANEO NEXT Pro", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Pro"), > + }, > + .driver_data = (void *)&limits_25w, > + }, > + { > + .ident = "AYANEO NEXT", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT"), > + }, > + .driver_data = (void *)&limits_25w, > + }, > + { > + .ident = "AYANEO KUN", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"), > + }, > + .driver_data = (void *)&limits_25w, > + }, > + { > + .ident = "AYANEO KUN", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO KUN"), > + }, > + .driver_data = (void *)&limits_25w, > + }, > + /* AYANEO - 28W */ > + { > + .ident = "AYANEO 2", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 2"), > + }, > + .driver_data = (void *)&limits_28w, > + }, > + { > + .ident = "SuiPlay0X1", > + .matches = { > + DMI_MATCH(DMI_SYS_VENDOR, "Mysten Labs, Inc."), > + DMI_MATCH(DMI_PRODUCT_NAME, "SuiPlay0X1"), > + }, > + .driver_data = (void *)&limits_28w, > + }, > + /* AYANEO - 30W */ > + { > + /* DMI_MATCH catches all FLIP variants (DS, KB, 1S DS, 1S KB) */ > + .ident = "AYANEO FLIP", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > + DMI_MATCH(DMI_BOARD_NAME, "FLIP"), > + }, > + .driver_data = (void *)&limits_30w, > + }, > + { > + /* DMI_MATCH catches GEEK and GEEK 1S */ > + .ident = "AYANEO GEEK", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > + DMI_MATCH(DMI_BOARD_NAME, "GEEK"), > + }, > + .driver_data = (void *)&limits_30w, > + }, > + { > + .ident = "AYANEO SLIDE", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "SLIDE"), > + }, > + .driver_data = (void *)&limits_30w, > + }, > + { > + .ident = "AYANEO 3", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO 3"), > + }, > + .driver_data = (void *)&limits_30w, > + }, > { } > }; > MODULE_DEVICE_TABLE(dmi, dptc_dmi_table); ^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [RFC v4 4/4] platform/x86/amd: dptc: Add device entries for handheld PCs 2026-03-10 4:11 ` Mario Limonciello @ 2026-03-10 8:13 ` Antheas Kapenekakis 2026-03-10 16:35 ` Mario Limonciello 0 siblings, 1 reply; 21+ messages in thread From: Antheas Kapenekakis @ 2026-03-10 8:13 UTC (permalink / raw) To: Mario Limonciello Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato, i, linux-kernel, platform-driver-x86 On Tue, 10 Mar 2026 at 05:11, Mario Limonciello <mario.limonciello@amd.com> wrote: > > > > On 3/9/2026 3:51 PM, Antheas Kapenekakis wrote: > > Add limit tables and DMI entries for AMD APU-based handheld devices: > > > > - 15W class: AYANEO AIR > > - 18W class: AYANEO AIR Plus/Pro > > - 25W class: AYANEO AIR 1S, NEXT/KUN > > - 28W class: GPD Win Mini/4/Max 2, GPD Duo, GPD Pocket 4, AYANEO 2, > > OrangePi NEO-01, SuiPlay0X1 > > - 30W class: AYANEO FLIP/GEEK/SLIDE/3, AOKZOE A1/A1X/A2, > > OneXPlayer F1Pro/2/X1/G1, AYN Loki Max, Zeenix Pro > > > > Also add SoC strings for Ryzen AI, Ryzen 8000/7040/6000/5000 > > families, extending support beyond the initial AI MAX series. > > There are a lot of numbers here. Where did they all come from? What > are guesses? What came from RE? What came from documentation? > How do you know these are good for the thermal design of the platform? > > I appreciate some of them could be cargo culted from another codebase, > but I think it would be a good idea to document which ones are more > likely to be accurate and why. Ok, here is the logic behind the limits. 15/18/25 are manufacturer claims by Ayaneo. Ayaneo is the only manufacturer that made devices prior to the new ones that support 30W limits. 28W is used for GPD because their devices are very portable and 30W makes e.g. the bottom pan of Win Minis too hot. It is also used for the Ayaneo 2/Sui/OPI, because these devices have a fullwidth glass front that is used for heat dissipation. So the screen gets very hot. The rest of the devices use 30W. As for which software each OEM has. GPD: GPD does not ship first party software for TDP controls. They collaborate with an independent developer developing "Motion Assistant", which they also ship with their devices and features a TDP slider. Ayaneo uses Ayaspace, which is backed by Ryzenadj, and they give slider control to users, typically with a 30W limit. They also use userspace customizable presets for some default e.g. 20W profiles. Onexplayer has oxpconsole with a slider as well that does 30W usually. Ayn exited the market on WIndows handhelds, no software. OPI, you'd need to ask Phil but this would be the primary driver for it. There is no alternative that can also go to SteamOS. The reasoning behind the profiles is simple. 8W is power savings territory. Going below can yield around 1W more, but the performance degradation is not worth it. 15W is a good sweetspot for 30W processors. Then, 25W is good for these devices as they saturate above 22ish Watts. We could even go lower to 20W, as Lenovo did on the original Go. But their newer devices use higher AC values. For their boost clocks, sPPT is set to around 2W higher than spl (what is it called now?) to give a small boost. fPPT is set to a reasonable amount for that device class and interpolated vs spl. The exception is low-power where we do not want boost Antheas > > > > Assisted-by: Claude:claude-opus-4-6 > > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev> > > --- > > drivers/platform/x86/amd/dptc.c | 420 ++++++++++++++++++++++++++++++++ > > 1 file changed, 420 insertions(+) > > > > diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c > > index f4db95affb1b..b2820f8652f1 100644 > > --- a/drivers/platform/x86/amd/dptc.c > > +++ b/drivers/platform/x86/amd/dptc.c > > @@ -86,6 +86,81 @@ static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = { > > 1, ALIB_ID_TEMP_TARGET }, > > }; > > > > +/* 15W class: AYANEO AIR (Ryzen 5 5560U) */ > > +static const struct dptc_device_limits limits_15w = { > > + .params = { > > + [DPTC_PPT_PL1_SPL] = { 1, 5, 10, 15, 18 }, > > + [DPTC_PPT_PL2_SPPT] = { 1, 5, 10, 15, 18 }, > > + [DPTC_PPT_PL3_FPPT] = { 1, 5, 12, 18, 22 }, > > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, > > + }, > > + .profiles = { > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } }, > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 10, 12, 15, 0 } }, > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 15, 15, 18, 0 } }, > > + }, > > +}; > > + > > +/* 18W class: AYANEO AIR Plus/Pro (Ryzen 5 5560U, Ryzen 7 5825U) */ > > +static const struct dptc_device_limits limits_18w = { > > + .params = { > > + [DPTC_PPT_PL1_SPL] = { 1, 5, 15, 18, 22 }, > > + [DPTC_PPT_PL2_SPPT] = { 1, 5, 15, 18, 22 }, > > + [DPTC_PPT_PL3_FPPT] = { 1, 5, 15, 20, 25 }, > > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, > > + }, > > + .profiles = { > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } }, > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 12, 14, 15, 0 } }, > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 18, 18, 20, 0 } }, > > + }, > > +}; > > + > > +/* 25W class: Ryzen 5000 handhelds (AYANEO NEXT, KUN) */ > > +static const struct dptc_device_limits limits_25w = { > > + .params = { > > + [DPTC_PPT_PL1_SPL] = { 1, 4, 15, 25, 32 }, > > + [DPTC_PPT_PL2_SPPT] = { 1, 4, 20, 27, 35 }, > > + [DPTC_PPT_PL3_FPPT] = { 1, 4, 25, 30, 37 }, > > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, > > + }, > > + .profiles = { > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } }, > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 20, 0 } }, > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 27, 30, 0 } }, > > + }, > > +}; > > + > > +/* 28W class: GPD Win series, AYANEO 2, OrangePi NEO-01 */ > > +static const struct dptc_device_limits limits_28w = { > > + .params = { > > + [DPTC_PPT_PL1_SPL] = { 1, 4, 15, 28, 32 }, > > + [DPTC_PPT_PL2_SPPT] = { 1, 4, 20, 30, 35 }, > > + [DPTC_PPT_PL3_FPPT] = { 1, 4, 25, 32, 37 }, > > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, > > + }, > > + .profiles = { > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } }, > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 22, 0 } }, > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 32, 0 } }, > > + }, > > +}; > > + > > +/* 30W class: OneXPlayer, AYANEO FLIP/GEEK/SLIDE/3, AOKZOE */ > > +static const struct dptc_device_limits limits_30w = { > > + .params = { > > + [DPTC_PPT_PL1_SPL] = { 1, 4, 15, 30, 40 }, > > + [DPTC_PPT_PL2_SPPT] = { 1, 4, 20, 32, 43 }, > > + [DPTC_PPT_PL3_FPPT] = { 1, 4, 25, 41, 50 }, > > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, > > + }, > > + .profiles = { > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } }, > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 25, 0 } }, > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 41, 0 } }, > > + }, > > +}; > > + > > /* AI MAX Handheld class: GPD Win 5 */ > > static const struct dptc_device_limits limits_maxhh = { > > .params = { > > @@ -107,11 +182,56 @@ static const char * const dptc_soc_table[] = { > > "AMD RYZEN AI MAX+ 395", > > "AMD RYZEN AI MAX+ 385", > > "AMD RYZEN AI MAX 380", > > + /* Ryzen AI */ > > + "AMD Ryzen AI 9 HX 370", > > + /* Ryzen 8000 */ > > + "AMD Ryzen 7 8840U", > > + /* Ryzen 7040 */ > > + "AMD Ryzen 7 7840U", > > + /* Ryzen 6000 */ > > + "AMD Ryzen 7 6800U", > > + "AMD Ryzen 7 6600U", > > + /* Ryzen 5000 */ > > + "AMD Ryzen 7 5800U", > > + "AMD Ryzen 7 5700U", > > + "AMD Ryzen 5 5560U", > > Still don't understand the point of this list. Parallel reply. > > NULL, > > }; > > > > static const struct dmi_system_id dptc_dmi_table[] = { > > /* GPD */ > > + { > > + .ident = "GPD Win Mini", > > + .matches = { > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-01"), > > + }, > > + .driver_data = (void *)&limits_28w, > > + }, > > + { > > + .ident = "GPD Win Mini 2024", > > + .matches = { > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02"), > > + }, > > + .driver_data = (void *)&limits_28w, > > + }, > > + { > > + .ident = "GPD Win Mini 2024", > > + .matches = { > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02-L"), > > + }, > > + .driver_data = (void *)&limits_28w, > > + }, > > + { > > + .ident = "GPD Win 4", > > + .matches = { > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-04"), > > + }, > > + .driver_data = (void *)&limits_28w, > > + }, > > { > > .ident = "GPD Win 5", > > .matches = { > > @@ -120,6 +240,306 @@ static const struct dmi_system_id dptc_dmi_table[] = { > > }, > > .driver_data = (void *)&limits_maxhh, > > }, > > + { > > + .ident = "GPD Win Max 2", > > + .matches = { > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-04"), > > + }, > > + .driver_data = (void *)&limits_28w, > > + }, > > + { > > + .ident = "GPD Win Max 2 2024", > > + .matches = { > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-05"), > > + }, > > + .driver_data = (void *)&limits_28w, > > + }, > > + { > > + .ident = "GPD Duo", > > + .matches = { > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01"), > > + }, > > + .driver_data = (void *)&limits_28w, > > + }, > > + { > > + .ident = "GPD Duo", > > + .matches = { > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01-L"), > > + }, > > + .driver_data = (void *)&limits_28w, > > + }, > > + { > > + .ident = "GPD Pocket 4", > > + .matches = { > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04"), > > + }, > > + .driver_data = (void *)&limits_28w, > > + }, > > + { > > + .ident = "GPD Pocket 4", > > + .matches = { > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04-L"), > > + }, > > + .driver_data = (void *)&limits_28w, > > + }, > > + /* OrangePi */ > > + { > > + .ident = "OrangePi NEO-01", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"), > > + }, > > + .driver_data = (void *)&limits_28w, > > + }, > > + /* AYN */ > > + { > > + .ident = "AYN Loki Max", > > + .matches = { > > + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ayn"), > > + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Loki Max"), > > + }, > > + .driver_data = (void *)&limits_30w, > > + }, > > + /* Tectoy (Zeenix Pro = Loki Max) */ > > + { > > + .ident = "Zeenix Pro", > > + .matches = { > > + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Tectoy"), > > + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Zeenix Pro"), > > + }, > > + .driver_data = (void *)&limits_30w, > > + }, > > + /* AOKZOE */ > > + { > > + .ident = "AOKZOE A1 AR07", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 AR07"), > > + }, > > + .driver_data = (void *)&limits_30w, > > + }, > > + { > > + .ident = "AOKZOE A1 Pro", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 Pro"), > > + }, > > + .driver_data = (void *)&limits_30w, > > + }, > > + { > > + .ident = "AOKZOE A1X", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1X"), > > + }, > > + .driver_data = (void *)&limits_30w, > > + }, > > + { > > + .ident = "AOKZOE A2 Pro", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A2 Pro"), > > + }, > > + .driver_data = (void *)&limits_30w, > > + }, > > + /* OneXPlayer (Intel variants filtered by SoC table) */ > > Are you telling me the Intel variants actually have the same ACPI > methods this series looks for? They do not. > > + { > > + .ident = "ONEXPLAYER F1Pro", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1Pro"), > > + }, > > + .driver_data = (void *)&limits_30w, > > + }, > > + { > > + .ident = "ONEXPLAYER F1 EVA-02", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1 EVA-02"), > > + }, > > + .driver_data = (void *)&limits_30w, > > + }, > > + { > > + .ident = "ONEXPLAYER 2", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), > > + DMI_MATCH(DMI_BOARD_NAME, "ONEXPLAYER 2"), > > + }, > > + .driver_data = (void *)&limits_30w, > > + }, > > + { > > + .ident = "ONEXPLAYER X1 A", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1 A"), > > + }, > > + .driver_data = (void *)&limits_30w, > > + }, > > + { > > + .ident = "ONEXPLAYER X1z", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1z"), > > + }, > > + .driver_data = (void *)&limits_30w, > > + }, > > + { > > + .ident = "ONEXPLAYER X1Pro", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Pro"), > > + }, > > + .driver_data = (void *)&limits_30w, > > + }, > > + { > > + .ident = "ONEXPLAYER G1 A", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 A"), > > + }, > > + .driver_data = (void *)&limits_30w, > > + }, > > + /* AYANEO - 15W */ > > + { > > + .ident = "AYANEO AIR", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR"), > > + }, > > + .driver_data = (void *)&limits_15w, > > + }, > > + /* AYANEO - 18W */ > > + { > > + .ident = "AYANEO AIR Plus", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Plus"), > > + }, > > + .driver_data = (void *)&limits_18w, > > + }, > > + { > > + .ident = "AYANEO AIR Pro", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Pro"), > > + }, > > + .driver_data = (void *)&limits_18w, > > + }, > > + /* AYANEO - 25W */ > > + { > > + .ident = "AYANEO AIR 1S", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > > + DMI_MATCH(DMI_BOARD_NAME, "AIR 1S"), > > + }, > > + .driver_data = (void *)&limits_25w, > > + }, > > + { > > + .ident = "AYANEO NEXT Advance", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Advance"), > > + }, > > + .driver_data = (void *)&limits_25w, > > + }, > > + { > > + .ident = "AYANEO NEXT Lite", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Lite"), > > + }, > > + .driver_data = (void *)&limits_25w, > > + }, > > + { > > + .ident = "AYANEO NEXT Pro", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Pro"), > > + }, > > + .driver_data = (void *)&limits_25w, > > + }, > > + { > > + .ident = "AYANEO NEXT", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT"), > > + }, > > + .driver_data = (void *)&limits_25w, > > + }, > > + { > > + .ident = "AYANEO KUN", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"), > > + }, > > + .driver_data = (void *)&limits_25w, > > + }, > > + { > > + .ident = "AYANEO KUN", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO KUN"), > > + }, > > + .driver_data = (void *)&limits_25w, > > + }, > > + /* AYANEO - 28W */ > > + { > > + .ident = "AYANEO 2", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > > + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 2"), > > + }, > > + .driver_data = (void *)&limits_28w, > > + }, > > + { > > + .ident = "SuiPlay0X1", > > + .matches = { > > + DMI_MATCH(DMI_SYS_VENDOR, "Mysten Labs, Inc."), > > + DMI_MATCH(DMI_PRODUCT_NAME, "SuiPlay0X1"), > > + }, > > + .driver_data = (void *)&limits_28w, > > + }, > > + /* AYANEO - 30W */ > > + { > > + /* DMI_MATCH catches all FLIP variants (DS, KB, 1S DS, 1S KB) */ > > + .ident = "AYANEO FLIP", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > > + DMI_MATCH(DMI_BOARD_NAME, "FLIP"), > > + }, > > + .driver_data = (void *)&limits_30w, > > + }, > > + { > > + /* DMI_MATCH catches GEEK and GEEK 1S */ > > + .ident = "AYANEO GEEK", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > > + DMI_MATCH(DMI_BOARD_NAME, "GEEK"), > > + }, > > + .driver_data = (void *)&limits_30w, > > + }, > > + { > > + .ident = "AYANEO SLIDE", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "SLIDE"), > > + }, > > + .driver_data = (void *)&limits_30w, > > + }, > > + { > > + .ident = "AYANEO 3", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO 3"), > > + }, > > + .driver_data = (void *)&limits_30w, > > + }, > > { } > > }; > > MODULE_DEVICE_TABLE(dmi, dptc_dmi_table); > > ^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [RFC v4 4/4] platform/x86/amd: dptc: Add device entries for handheld PCs 2026-03-10 8:13 ` Antheas Kapenekakis @ 2026-03-10 16:35 ` Mario Limonciello 2026-03-11 19:13 ` Antheas Kapenekakis 0 siblings, 1 reply; 21+ messages in thread From: Mario Limonciello @ 2026-03-10 16:35 UTC (permalink / raw) To: Antheas Kapenekakis Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato, i, linux-kernel, platform-driver-x86 On 3/10/26 3:13 AM, Antheas Kapenekakis wrote: > On Tue, 10 Mar 2026 at 05:11, Mario Limonciello > <mario.limonciello@amd.com> wrote: >> >> >> >> On 3/9/2026 3:51 PM, Antheas Kapenekakis wrote: >>> Add limit tables and DMI entries for AMD APU-based handheld devices: >>> >>> - 15W class: AYANEO AIR >>> - 18W class: AYANEO AIR Plus/Pro >>> - 25W class: AYANEO AIR 1S, NEXT/KUN >>> - 28W class: GPD Win Mini/4/Max 2, GPD Duo, GPD Pocket 4, AYANEO 2, >>> OrangePi NEO-01, SuiPlay0X1 >>> - 30W class: AYANEO FLIP/GEEK/SLIDE/3, AOKZOE A1/A1X/A2, >>> OneXPlayer F1Pro/2/X1/G1, AYN Loki Max, Zeenix Pro >>> >>> Also add SoC strings for Ryzen AI, Ryzen 8000/7040/6000/5000 >>> families, extending support beyond the initial AI MAX series. >> >> There are a lot of numbers here. Where did they all come from? What >> are guesses? What came from RE? What came from documentation? >> How do you know these are good for the thermal design of the platform? >> >> I appreciate some of them could be cargo culted from another codebase, >> but I think it would be a good idea to document which ones are more >> likely to be accurate and why. > > Ok, here is the logic behind the limits. > > 15/18/25 are manufacturer claims by Ayaneo. Ayaneo is the only > manufacturer that made devices prior to the new ones that support 30W > limits. > > 28W is used for GPD because their devices are very portable and 30W > makes e.g. the bottom pan of Win Minis too hot. It is also used for > the Ayaneo 2/Sui/OPI, because these devices have a fullwidth glass > front that is used for heat dissipation. So the screen gets very hot. > > The rest of the devices use 30W. > > As for which software each OEM has. GPD: GPD does not ship first party > software for TDP controls. They collaborate with an independent > developer developing "Motion Assistant", which they also ship with > their devices and features a TDP slider. Ayaneo uses Ayaspace, which > is backed by Ryzenadj, and they give slider control to users, > typically with a 30W limit. They also use userspace customizable > presets for some default e.g. 20W profiles. Onexplayer has oxpconsole > with a slider as well that does 30W usually. Ayn exited the market on > WIndows handhelds, no software. OPI, you'd need to ask Phil but this > would be the primary driver for it. There is no alternative that can > also go to SteamOS. > > The reasoning behind the profiles is simple. 8W is power savings > territory. Going below can yield around 1W more, but the performance > degradation is not worth it. 15W is a good sweetspot for 30W > processors. Then, 25W is good for these devices as they saturate above > 22ish Watts. We could even go lower to 20W, as Lenovo did on the > original Go. But their newer devices use higher AC values. > > For their boost clocks, sPPT is set to around 2W higher than spl (what > is it called now?) to give a small boost. fPPT is set to a reasonable > amount for that device class and interpolated vs spl. The exception is > low-power where we do not want boost > > Antheas OK, each and every one of these cases needs to be documented. I suggest that they each go in their own patches, and then if there is any disagreement it's a single patch to revert or drop from the rest of the series. > >>> >>> Assisted-by: Claude:claude-opus-4-6 >>> Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev> >>> --- >>> drivers/platform/x86/amd/dptc.c | 420 ++++++++++++++++++++++++++++++++ >>> 1 file changed, 420 insertions(+) >>> >>> diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c >>> index f4db95affb1b..b2820f8652f1 100644 >>> --- a/drivers/platform/x86/amd/dptc.c >>> +++ b/drivers/platform/x86/amd/dptc.c >>> @@ -86,6 +86,81 @@ static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = { >>> 1, ALIB_ID_TEMP_TARGET }, >>> }; >>> >>> +/* 15W class: AYANEO AIR (Ryzen 5 5560U) */ >>> +static const struct dptc_device_limits limits_15w = { >>> + .params = { >>> + [DPTC_PPT_PL1_SPL] = { 1, 5, 10, 15, 18 }, >>> + [DPTC_PPT_PL2_SPPT] = { 1, 5, 10, 15, 18 }, >>> + [DPTC_PPT_PL3_FPPT] = { 1, 5, 12, 18, 22 }, >>> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, >>> + }, >>> + .profiles = { >>> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } }, >>> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 10, 12, 15, 0 } }, >>> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 15, 15, 18, 0 } }, >>> + }, >>> +}; >>> + >>> +/* 18W class: AYANEO AIR Plus/Pro (Ryzen 5 5560U, Ryzen 7 5825U) */ >>> +static const struct dptc_device_limits limits_18w = { >>> + .params = { >>> + [DPTC_PPT_PL1_SPL] = { 1, 5, 15, 18, 22 }, >>> + [DPTC_PPT_PL2_SPPT] = { 1, 5, 15, 18, 22 }, >>> + [DPTC_PPT_PL3_FPPT] = { 1, 5, 15, 20, 25 }, >>> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, >>> + }, >>> + .profiles = { >>> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } }, >>> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 12, 14, 15, 0 } }, >>> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 18, 18, 20, 0 } }, >>> + }, >>> +}; >>> + >>> +/* 25W class: Ryzen 5000 handhelds (AYANEO NEXT, KUN) */ >>> +static const struct dptc_device_limits limits_25w = { >>> + .params = { >>> + [DPTC_PPT_PL1_SPL] = { 1, 4, 15, 25, 32 }, >>> + [DPTC_PPT_PL2_SPPT] = { 1, 4, 20, 27, 35 }, >>> + [DPTC_PPT_PL3_FPPT] = { 1, 4, 25, 30, 37 }, >>> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, >>> + }, >>> + .profiles = { >>> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } }, >>> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 20, 0 } }, >>> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 27, 30, 0 } }, >>> + }, >>> +}; >>> + >>> +/* 28W class: GPD Win series, AYANEO 2, OrangePi NEO-01 */ >>> +static const struct dptc_device_limits limits_28w = { >>> + .params = { >>> + [DPTC_PPT_PL1_SPL] = { 1, 4, 15, 28, 32 }, >>> + [DPTC_PPT_PL2_SPPT] = { 1, 4, 20, 30, 35 }, >>> + [DPTC_PPT_PL3_FPPT] = { 1, 4, 25, 32, 37 }, >>> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, >>> + }, >>> + .profiles = { >>> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } }, >>> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 22, 0 } }, >>> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 32, 0 } }, >>> + }, >>> +}; >>> + >>> +/* 30W class: OneXPlayer, AYANEO FLIP/GEEK/SLIDE/3, AOKZOE */ >>> +static const struct dptc_device_limits limits_30w = { >>> + .params = { >>> + [DPTC_PPT_PL1_SPL] = { 1, 4, 15, 30, 40 }, >>> + [DPTC_PPT_PL2_SPPT] = { 1, 4, 20, 32, 43 }, >>> + [DPTC_PPT_PL3_FPPT] = { 1, 4, 25, 41, 50 }, >>> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, >>> + }, >>> + .profiles = { >>> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } }, >>> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 25, 0 } }, >>> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 41, 0 } }, >>> + }, >>> +}; >>> + >>> /* AI MAX Handheld class: GPD Win 5 */ >>> static const struct dptc_device_limits limits_maxhh = { >>> .params = { >>> @@ -107,11 +182,56 @@ static const char * const dptc_soc_table[] = { >>> "AMD RYZEN AI MAX+ 395", >>> "AMD RYZEN AI MAX+ 385", >>> "AMD RYZEN AI MAX 380", >>> + /* Ryzen AI */ >>> + "AMD Ryzen AI 9 HX 370", >>> + /* Ryzen 8000 */ >>> + "AMD Ryzen 7 8840U", >>> + /* Ryzen 7040 */ >>> + "AMD Ryzen 7 7840U", >>> + /* Ryzen 6000 */ >>> + "AMD Ryzen 7 6800U", >>> + "AMD Ryzen 7 6600U", >>> + /* Ryzen 5000 */ >>> + "AMD Ryzen 7 5800U", >>> + "AMD Ryzen 7 5700U", >>> + "AMD Ryzen 5 5560U", >> >> Still don't understand the point of this list. > > Parallel reply. > >>> NULL, >>> }; >>> >>> static const struct dmi_system_id dptc_dmi_table[] = { >>> /* GPD */ >>> + { >>> + .ident = "GPD Win Mini", >>> + .matches = { >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-01"), >>> + }, >>> + .driver_data = (void *)&limits_28w, >>> + }, >>> + { >>> + .ident = "GPD Win Mini 2024", >>> + .matches = { >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02"), >>> + }, >>> + .driver_data = (void *)&limits_28w, >>> + }, >>> + { >>> + .ident = "GPD Win Mini 2024", >>> + .matches = { >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02-L"), >>> + }, >>> + .driver_data = (void *)&limits_28w, >>> + }, >>> + { >>> + .ident = "GPD Win 4", >>> + .matches = { >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-04"), >>> + }, >>> + .driver_data = (void *)&limits_28w, >>> + }, >>> { >>> .ident = "GPD Win 5", >>> .matches = { >>> @@ -120,6 +240,306 @@ static const struct dmi_system_id dptc_dmi_table[] = { >>> }, >>> .driver_data = (void *)&limits_maxhh, >>> }, >>> + { >>> + .ident = "GPD Win Max 2", >>> + .matches = { >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-04"), >>> + }, >>> + .driver_data = (void *)&limits_28w, >>> + }, >>> + { >>> + .ident = "GPD Win Max 2 2024", >>> + .matches = { >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-05"), >>> + }, >>> + .driver_data = (void *)&limits_28w, >>> + }, >>> + { >>> + .ident = "GPD Duo", >>> + .matches = { >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01"), >>> + }, >>> + .driver_data = (void *)&limits_28w, >>> + }, >>> + { >>> + .ident = "GPD Duo", >>> + .matches = { >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01-L"), >>> + }, >>> + .driver_data = (void *)&limits_28w, >>> + }, >>> + { >>> + .ident = "GPD Pocket 4", >>> + .matches = { >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04"), >>> + }, >>> + .driver_data = (void *)&limits_28w, >>> + }, >>> + { >>> + .ident = "GPD Pocket 4", >>> + .matches = { >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04-L"), >>> + }, >>> + .driver_data = (void *)&limits_28w, >>> + }, >>> + /* OrangePi */ >>> + { >>> + .ident = "OrangePi NEO-01", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"), >>> + }, >>> + .driver_data = (void *)&limits_28w, >>> + }, >>> + /* AYN */ >>> + { >>> + .ident = "AYN Loki Max", >>> + .matches = { >>> + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ayn"), >>> + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Loki Max"), >>> + }, >>> + .driver_data = (void *)&limits_30w, >>> + }, >>> + /* Tectoy (Zeenix Pro = Loki Max) */ >>> + { >>> + .ident = "Zeenix Pro", >>> + .matches = { >>> + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Tectoy"), >>> + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Zeenix Pro"), >>> + }, >>> + .driver_data = (void *)&limits_30w, >>> + }, >>> + /* AOKZOE */ >>> + { >>> + .ident = "AOKZOE A1 AR07", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 AR07"), >>> + }, >>> + .driver_data = (void *)&limits_30w, >>> + }, >>> + { >>> + .ident = "AOKZOE A1 Pro", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 Pro"), >>> + }, >>> + .driver_data = (void *)&limits_30w, >>> + }, >>> + { >>> + .ident = "AOKZOE A1X", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1X"), >>> + }, >>> + .driver_data = (void *)&limits_30w, >>> + }, >>> + { >>> + .ident = "AOKZOE A2 Pro", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A2 Pro"), >>> + }, >>> + .driver_data = (void *)&limits_30w, >>> + }, >>> + /* OneXPlayer (Intel variants filtered by SoC table) */ >> >> Are you telling me the Intel variants actually have the same ACPI >> methods this series looks for? > > They do not. OK then this comment appears wrong. > >>> + { >>> + .ident = "ONEXPLAYER F1Pro", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1Pro"), >>> + }, >>> + .driver_data = (void *)&limits_30w, >>> + }, >>> + { >>> + .ident = "ONEXPLAYER F1 EVA-02", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1 EVA-02"), >>> + }, >>> + .driver_data = (void *)&limits_30w, >>> + }, >>> + { >>> + .ident = "ONEXPLAYER 2", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), >>> + DMI_MATCH(DMI_BOARD_NAME, "ONEXPLAYER 2"), >>> + }, >>> + .driver_data = (void *)&limits_30w, >>> + }, >>> + { >>> + .ident = "ONEXPLAYER X1 A", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1 A"), >>> + }, >>> + .driver_data = (void *)&limits_30w, >>> + }, >>> + { >>> + .ident = "ONEXPLAYER X1z", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1z"), >>> + }, >>> + .driver_data = (void *)&limits_30w, >>> + }, >>> + { >>> + .ident = "ONEXPLAYER X1Pro", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Pro"), >>> + }, >>> + .driver_data = (void *)&limits_30w, >>> + }, >>> + { >>> + .ident = "ONEXPLAYER G1 A", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 A"), >>> + }, >>> + .driver_data = (void *)&limits_30w, >>> + }, >>> + /* AYANEO - 15W */ >>> + { >>> + .ident = "AYANEO AIR", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR"), >>> + }, >>> + .driver_data = (void *)&limits_15w, >>> + }, >>> + /* AYANEO - 18W */ >>> + { >>> + .ident = "AYANEO AIR Plus", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Plus"), >>> + }, >>> + .driver_data = (void *)&limits_18w, >>> + }, >>> + { >>> + .ident = "AYANEO AIR Pro", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Pro"), >>> + }, >>> + .driver_data = (void *)&limits_18w, >>> + }, >>> + /* AYANEO - 25W */ >>> + { >>> + .ident = "AYANEO AIR 1S", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>> + DMI_MATCH(DMI_BOARD_NAME, "AIR 1S"), >>> + }, >>> + .driver_data = (void *)&limits_25w, >>> + }, >>> + { >>> + .ident = "AYANEO NEXT Advance", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Advance"), >>> + }, >>> + .driver_data = (void *)&limits_25w, >>> + }, >>> + { >>> + .ident = "AYANEO NEXT Lite", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Lite"), >>> + }, >>> + .driver_data = (void *)&limits_25w, >>> + }, >>> + { >>> + .ident = "AYANEO NEXT Pro", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Pro"), >>> + }, >>> + .driver_data = (void *)&limits_25w, >>> + }, >>> + { >>> + .ident = "AYANEO NEXT", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT"), >>> + }, >>> + .driver_data = (void *)&limits_25w, >>> + }, >>> + { >>> + .ident = "AYANEO KUN", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"), >>> + }, >>> + .driver_data = (void *)&limits_25w, >>> + }, >>> + { >>> + .ident = "AYANEO KUN", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO KUN"), >>> + }, >>> + .driver_data = (void *)&limits_25w, >>> + }, >>> + /* AYANEO - 28W */ >>> + { >>> + .ident = "AYANEO 2", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>> + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 2"), >>> + }, >>> + .driver_data = (void *)&limits_28w, >>> + }, >>> + { >>> + .ident = "SuiPlay0X1", >>> + .matches = { >>> + DMI_MATCH(DMI_SYS_VENDOR, "Mysten Labs, Inc."), >>> + DMI_MATCH(DMI_PRODUCT_NAME, "SuiPlay0X1"), >>> + }, >>> + .driver_data = (void *)&limits_28w, >>> + }, >>> + /* AYANEO - 30W */ >>> + { >>> + /* DMI_MATCH catches all FLIP variants (DS, KB, 1S DS, 1S KB) */ >>> + .ident = "AYANEO FLIP", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>> + DMI_MATCH(DMI_BOARD_NAME, "FLIP"), >>> + }, >>> + .driver_data = (void *)&limits_30w, >>> + }, >>> + { >>> + /* DMI_MATCH catches GEEK and GEEK 1S */ >>> + .ident = "AYANEO GEEK", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>> + DMI_MATCH(DMI_BOARD_NAME, "GEEK"), >>> + }, >>> + .driver_data = (void *)&limits_30w, >>> + }, >>> + { >>> + .ident = "AYANEO SLIDE", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "SLIDE"), >>> + }, >>> + .driver_data = (void *)&limits_30w, >>> + }, >>> + { >>> + .ident = "AYANEO 3", >>> + .matches = { >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO 3"), >>> + }, >>> + .driver_data = (void *)&limits_30w, >>> + }, >>> { } >>> }; >>> MODULE_DEVICE_TABLE(dmi, dptc_dmi_table); >> >> > ^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [RFC v4 4/4] platform/x86/amd: dptc: Add device entries for handheld PCs 2026-03-10 16:35 ` Mario Limonciello @ 2026-03-11 19:13 ` Antheas Kapenekakis 2026-03-12 13:26 ` Mario Limonciello 0 siblings, 1 reply; 21+ messages in thread From: Antheas Kapenekakis @ 2026-03-11 19:13 UTC (permalink / raw) To: Mario Limonciello Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato, i, linux-kernel, platform-driver-x86 On Tue, 10 Mar 2026 at 17:35, Mario Limonciello <mario.limonciello@amd.com> wrote: > > On 3/10/26 3:13 AM, Antheas Kapenekakis wrote: > > On Tue, 10 Mar 2026 at 05:11, Mario Limonciello > > <mario.limonciello@amd.com> wrote: > >> > >> > >> > >> On 3/9/2026 3:51 PM, Antheas Kapenekakis wrote: > >>> Add limit tables and DMI entries for AMD APU-based handheld devices: > >>> > >>> - 15W class: AYANEO AIR > >>> - 18W class: AYANEO AIR Plus/Pro > >>> - 25W class: AYANEO AIR 1S, NEXT/KUN > >>> - 28W class: GPD Win Mini/4/Max 2, GPD Duo, GPD Pocket 4, AYANEO 2, > >>> OrangePi NEO-01, SuiPlay0X1 > >>> - 30W class: AYANEO FLIP/GEEK/SLIDE/3, AOKZOE A1/A1X/A2, > >>> OneXPlayer F1Pro/2/X1/G1, AYN Loki Max, Zeenix Pro > >>> > >>> Also add SoC strings for Ryzen AI, Ryzen 8000/7040/6000/5000 > >>> families, extending support beyond the initial AI MAX series. > >> > >> There are a lot of numbers here. Where did they all come from? What > >> are guesses? What came from RE? What came from documentation? > >> How do you know these are good for the thermal design of the platform? > >> > >> I appreciate some of them could be cargo culted from another codebase, > >> but I think it would be a good idea to document which ones are more > >> likely to be accurate and why. > > > > Ok, here is the logic behind the limits. > > > > 15/18/25 are manufacturer claims by Ayaneo. Ayaneo is the only > > manufacturer that made devices prior to the new ones that support 30W > > limits. > > > > 28W is used for GPD because their devices are very portable and 30W > > makes e.g. the bottom pan of Win Minis too hot. It is also used for > > the Ayaneo 2/Sui/OPI, because these devices have a fullwidth glass > > front that is used for heat dissipation. So the screen gets very hot. > > > > The rest of the devices use 30W. > > > > As for which software each OEM has. GPD: GPD does not ship first party > > software for TDP controls. They collaborate with an independent > > developer developing "Motion Assistant", which they also ship with > > their devices and features a TDP slider. Ayaneo uses Ayaspace, which > > is backed by Ryzenadj, and they give slider control to users, > > typically with a 30W limit. They also use userspace customizable > > presets for some default e.g. 20W profiles. Onexplayer has oxpconsole > > with a slider as well that does 30W usually. Ayn exited the market on > > WIndows handhelds, no software. OPI, you'd need to ask Phil but this > > would be the primary driver for it. There is no alternative that can > > also go to SteamOS. > > > > The reasoning behind the profiles is simple. 8W is power savings > > territory. Going below can yield around 1W more, but the performance > > degradation is not worth it. 15W is a good sweetspot for 30W > > processors. Then, 25W is good for these devices as they saturate above > > 22ish Watts. We could even go lower to 20W, as Lenovo did on the > > original Go. But their newer devices use higher AC values. > > > > For their boost clocks, sPPT is set to around 2W higher than spl (what > > is it called now?) to give a small boost. fPPT is set to a reasonable > > amount for that device class and interpolated vs spl. The exception is > > low-power where we do not want boost > > > > Antheas > > OK, each and every one of these cases needs to be documented. > > I suggest that they each go in their own patches, and then if there is > any disagreement it's a single patch to revert or drop from the rest of > the series. Ok, so here is what I suggest going forward. We keep the last patch while this is an RFC and annotate it as planned to be dropped so the series can still be tested. Then, once we exit RFC we merge initially for Win 5 to reduce email bloat. After merge, I go through the DMIs and group them by device type, adding information about the devices on the commit message. Then, these go to for-fixes with a cc stable. While doing that, I will drop all non EXACT matches. Specifically, the Ayaneo 1S had a few variants with different DMIs (1S Limited being one of them if I recall). This will allow us to drop the CPU match with high certainty. I will replace it with just an is AMD check with no logs or perhaps a debug log, to avoid polluting Intel variant's dmesg if any. Best, Antheas > > > >>> > >>> Assisted-by: Claude:claude-opus-4-6 > >>> Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev> > >>> --- > >>> drivers/platform/x86/amd/dptc.c | 420 ++++++++++++++++++++++++++++++++ > >>> 1 file changed, 420 insertions(+) > >>> > >>> diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c > >>> index f4db95affb1b..b2820f8652f1 100644 > >>> --- a/drivers/platform/x86/amd/dptc.c > >>> +++ b/drivers/platform/x86/amd/dptc.c > >>> @@ -86,6 +86,81 @@ static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = { > >>> 1, ALIB_ID_TEMP_TARGET }, > >>> }; > >>> > >>> +/* 15W class: AYANEO AIR (Ryzen 5 5560U) */ > >>> +static const struct dptc_device_limits limits_15w = { > >>> + .params = { > >>> + [DPTC_PPT_PL1_SPL] = { 1, 5, 10, 15, 18 }, > >>> + [DPTC_PPT_PL2_SPPT] = { 1, 5, 10, 15, 18 }, > >>> + [DPTC_PPT_PL3_FPPT] = { 1, 5, 12, 18, 22 }, > >>> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, > >>> + }, > >>> + .profiles = { > >>> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } }, > >>> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 10, 12, 15, 0 } }, > >>> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 15, 15, 18, 0 } }, > >>> + }, > >>> +}; > >>> + > >>> +/* 18W class: AYANEO AIR Plus/Pro (Ryzen 5 5560U, Ryzen 7 5825U) */ > >>> +static const struct dptc_device_limits limits_18w = { > >>> + .params = { > >>> + [DPTC_PPT_PL1_SPL] = { 1, 5, 15, 18, 22 }, > >>> + [DPTC_PPT_PL2_SPPT] = { 1, 5, 15, 18, 22 }, > >>> + [DPTC_PPT_PL3_FPPT] = { 1, 5, 15, 20, 25 }, > >>> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, > >>> + }, > >>> + .profiles = { > >>> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } }, > >>> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 12, 14, 15, 0 } }, > >>> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 18, 18, 20, 0 } }, > >>> + }, > >>> +}; > >>> + > >>> +/* 25W class: Ryzen 5000 handhelds (AYANEO NEXT, KUN) */ > >>> +static const struct dptc_device_limits limits_25w = { > >>> + .params = { > >>> + [DPTC_PPT_PL1_SPL] = { 1, 4, 15, 25, 32 }, > >>> + [DPTC_PPT_PL2_SPPT] = { 1, 4, 20, 27, 35 }, > >>> + [DPTC_PPT_PL3_FPPT] = { 1, 4, 25, 30, 37 }, > >>> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, > >>> + }, > >>> + .profiles = { > >>> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } }, > >>> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 20, 0 } }, > >>> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 27, 30, 0 } }, > >>> + }, > >>> +}; > >>> + > >>> +/* 28W class: GPD Win series, AYANEO 2, OrangePi NEO-01 */ > >>> +static const struct dptc_device_limits limits_28w = { > >>> + .params = { > >>> + [DPTC_PPT_PL1_SPL] = { 1, 4, 15, 28, 32 }, > >>> + [DPTC_PPT_PL2_SPPT] = { 1, 4, 20, 30, 35 }, > >>> + [DPTC_PPT_PL3_FPPT] = { 1, 4, 25, 32, 37 }, > >>> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, > >>> + }, > >>> + .profiles = { > >>> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } }, > >>> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 22, 0 } }, > >>> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 32, 0 } }, > >>> + }, > >>> +}; > >>> + > >>> +/* 30W class: OneXPlayer, AYANEO FLIP/GEEK/SLIDE/3, AOKZOE */ > >>> +static const struct dptc_device_limits limits_30w = { > >>> + .params = { > >>> + [DPTC_PPT_PL1_SPL] = { 1, 4, 15, 30, 40 }, > >>> + [DPTC_PPT_PL2_SPPT] = { 1, 4, 20, 32, 43 }, > >>> + [DPTC_PPT_PL3_FPPT] = { 1, 4, 25, 41, 50 }, > >>> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, > >>> + }, > >>> + .profiles = { > >>> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } }, > >>> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 25, 0 } }, > >>> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 41, 0 } }, > >>> + }, > >>> +}; > >>> + > >>> /* AI MAX Handheld class: GPD Win 5 */ > >>> static const struct dptc_device_limits limits_maxhh = { > >>> .params = { > >>> @@ -107,11 +182,56 @@ static const char * const dptc_soc_table[] = { > >>> "AMD RYZEN AI MAX+ 395", > >>> "AMD RYZEN AI MAX+ 385", > >>> "AMD RYZEN AI MAX 380", > >>> + /* Ryzen AI */ > >>> + "AMD Ryzen AI 9 HX 370", > >>> + /* Ryzen 8000 */ > >>> + "AMD Ryzen 7 8840U", > >>> + /* Ryzen 7040 */ > >>> + "AMD Ryzen 7 7840U", > >>> + /* Ryzen 6000 */ > >>> + "AMD Ryzen 7 6800U", > >>> + "AMD Ryzen 7 6600U", > >>> + /* Ryzen 5000 */ > >>> + "AMD Ryzen 7 5800U", > >>> + "AMD Ryzen 7 5700U", > >>> + "AMD Ryzen 5 5560U", > >> > >> Still don't understand the point of this list. > > > > Parallel reply. > > > >>> NULL, > >>> }; > >>> > >>> static const struct dmi_system_id dptc_dmi_table[] = { > >>> /* GPD */ > >>> + { > >>> + .ident = "GPD Win Mini", > >>> + .matches = { > >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-01"), > >>> + }, > >>> + .driver_data = (void *)&limits_28w, > >>> + }, > >>> + { > >>> + .ident = "GPD Win Mini 2024", > >>> + .matches = { > >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02"), > >>> + }, > >>> + .driver_data = (void *)&limits_28w, > >>> + }, > >>> + { > >>> + .ident = "GPD Win Mini 2024", > >>> + .matches = { > >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02-L"), > >>> + }, > >>> + .driver_data = (void *)&limits_28w, > >>> + }, > >>> + { > >>> + .ident = "GPD Win 4", > >>> + .matches = { > >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-04"), > >>> + }, > >>> + .driver_data = (void *)&limits_28w, > >>> + }, > >>> { > >>> .ident = "GPD Win 5", > >>> .matches = { > >>> @@ -120,6 +240,306 @@ static const struct dmi_system_id dptc_dmi_table[] = { > >>> }, > >>> .driver_data = (void *)&limits_maxhh, > >>> }, > >>> + { > >>> + .ident = "GPD Win Max 2", > >>> + .matches = { > >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-04"), > >>> + }, > >>> + .driver_data = (void *)&limits_28w, > >>> + }, > >>> + { > >>> + .ident = "GPD Win Max 2 2024", > >>> + .matches = { > >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-05"), > >>> + }, > >>> + .driver_data = (void *)&limits_28w, > >>> + }, > >>> + { > >>> + .ident = "GPD Duo", > >>> + .matches = { > >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01"), > >>> + }, > >>> + .driver_data = (void *)&limits_28w, > >>> + }, > >>> + { > >>> + .ident = "GPD Duo", > >>> + .matches = { > >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01-L"), > >>> + }, > >>> + .driver_data = (void *)&limits_28w, > >>> + }, > >>> + { > >>> + .ident = "GPD Pocket 4", > >>> + .matches = { > >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04"), > >>> + }, > >>> + .driver_data = (void *)&limits_28w, > >>> + }, > >>> + { > >>> + .ident = "GPD Pocket 4", > >>> + .matches = { > >>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > >>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04-L"), > >>> + }, > >>> + .driver_data = (void *)&limits_28w, > >>> + }, > >>> + /* OrangePi */ > >>> + { > >>> + .ident = "OrangePi NEO-01", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"), > >>> + }, > >>> + .driver_data = (void *)&limits_28w, > >>> + }, > >>> + /* AYN */ > >>> + { > >>> + .ident = "AYN Loki Max", > >>> + .matches = { > >>> + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ayn"), > >>> + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Loki Max"), > >>> + }, > >>> + .driver_data = (void *)&limits_30w, > >>> + }, > >>> + /* Tectoy (Zeenix Pro = Loki Max) */ > >>> + { > >>> + .ident = "Zeenix Pro", > >>> + .matches = { > >>> + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Tectoy"), > >>> + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Zeenix Pro"), > >>> + }, > >>> + .driver_data = (void *)&limits_30w, > >>> + }, > >>> + /* AOKZOE */ > >>> + { > >>> + .ident = "AOKZOE A1 AR07", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 AR07"), > >>> + }, > >>> + .driver_data = (void *)&limits_30w, > >>> + }, > >>> + { > >>> + .ident = "AOKZOE A1 Pro", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 Pro"), > >>> + }, > >>> + .driver_data = (void *)&limits_30w, > >>> + }, > >>> + { > >>> + .ident = "AOKZOE A1X", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1X"), > >>> + }, > >>> + .driver_data = (void *)&limits_30w, > >>> + }, > >>> + { > >>> + .ident = "AOKZOE A2 Pro", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A2 Pro"), > >>> + }, > >>> + .driver_data = (void *)&limits_30w, > >>> + }, > >>> + /* OneXPlayer (Intel variants filtered by SoC table) */ > >> > >> Are you telling me the Intel variants actually have the same ACPI > >> methods this series looks for? > > > > They do not. > > OK then this comment appears wrong. Ack See above. > > > >>> + { > >>> + .ident = "ONEXPLAYER F1Pro", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1Pro"), > >>> + }, > >>> + .driver_data = (void *)&limits_30w, > >>> + }, > >>> + { > >>> + .ident = "ONEXPLAYER F1 EVA-02", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1 EVA-02"), > >>> + }, > >>> + .driver_data = (void *)&limits_30w, > >>> + }, > >>> + { > >>> + .ident = "ONEXPLAYER 2", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), > >>> + DMI_MATCH(DMI_BOARD_NAME, "ONEXPLAYER 2"), > >>> + }, > >>> + .driver_data = (void *)&limits_30w, > >>> + }, > >>> + { > >>> + .ident = "ONEXPLAYER X1 A", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1 A"), > >>> + }, > >>> + .driver_data = (void *)&limits_30w, > >>> + }, > >>> + { > >>> + .ident = "ONEXPLAYER X1z", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1z"), > >>> + }, > >>> + .driver_data = (void *)&limits_30w, > >>> + }, > >>> + { > >>> + .ident = "ONEXPLAYER X1Pro", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Pro"), > >>> + }, > >>> + .driver_data = (void *)&limits_30w, > >>> + }, > >>> + { > >>> + .ident = "ONEXPLAYER G1 A", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 A"), > >>> + }, > >>> + .driver_data = (void *)&limits_30w, > >>> + }, > >>> + /* AYANEO - 15W */ > >>> + { > >>> + .ident = "AYANEO AIR", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR"), > >>> + }, > >>> + .driver_data = (void *)&limits_15w, > >>> + }, > >>> + /* AYANEO - 18W */ > >>> + { > >>> + .ident = "AYANEO AIR Plus", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Plus"), > >>> + }, > >>> + .driver_data = (void *)&limits_18w, > >>> + }, > >>> + { > >>> + .ident = "AYANEO AIR Pro", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Pro"), > >>> + }, > >>> + .driver_data = (void *)&limits_18w, > >>> + }, > >>> + /* AYANEO - 25W */ > >>> + { > >>> + .ident = "AYANEO AIR 1S", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > >>> + DMI_MATCH(DMI_BOARD_NAME, "AIR 1S"), > >>> + }, > >>> + .driver_data = (void *)&limits_25w, > >>> + }, > >>> + { > >>> + .ident = "AYANEO NEXT Advance", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Advance"), > >>> + }, > >>> + .driver_data = (void *)&limits_25w, > >>> + }, > >>> + { > >>> + .ident = "AYANEO NEXT Lite", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Lite"), > >>> + }, > >>> + .driver_data = (void *)&limits_25w, > >>> + }, > >>> + { > >>> + .ident = "AYANEO NEXT Pro", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Pro"), > >>> + }, > >>> + .driver_data = (void *)&limits_25w, > >>> + }, > >>> + { > >>> + .ident = "AYANEO NEXT", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT"), > >>> + }, > >>> + .driver_data = (void *)&limits_25w, > >>> + }, > >>> + { > >>> + .ident = "AYANEO KUN", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"), > >>> + }, > >>> + .driver_data = (void *)&limits_25w, > >>> + }, > >>> + { > >>> + .ident = "AYANEO KUN", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO KUN"), > >>> + }, > >>> + .driver_data = (void *)&limits_25w, > >>> + }, > >>> + /* AYANEO - 28W */ > >>> + { > >>> + .ident = "AYANEO 2", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > >>> + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 2"), > >>> + }, > >>> + .driver_data = (void *)&limits_28w, > >>> + }, > >>> + { > >>> + .ident = "SuiPlay0X1", > >>> + .matches = { > >>> + DMI_MATCH(DMI_SYS_VENDOR, "Mysten Labs, Inc."), > >>> + DMI_MATCH(DMI_PRODUCT_NAME, "SuiPlay0X1"), > >>> + }, > >>> + .driver_data = (void *)&limits_28w, > >>> + }, > >>> + /* AYANEO - 30W */ > >>> + { > >>> + /* DMI_MATCH catches all FLIP variants (DS, KB, 1S DS, 1S KB) */ > >>> + .ident = "AYANEO FLIP", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > >>> + DMI_MATCH(DMI_BOARD_NAME, "FLIP"), > >>> + }, > >>> + .driver_data = (void *)&limits_30w, > >>> + }, > >>> + { > >>> + /* DMI_MATCH catches GEEK and GEEK 1S */ > >>> + .ident = "AYANEO GEEK", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > >>> + DMI_MATCH(DMI_BOARD_NAME, "GEEK"), > >>> + }, > >>> + .driver_data = (void *)&limits_30w, > >>> + }, > >>> + { > >>> + .ident = "AYANEO SLIDE", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "SLIDE"), > >>> + }, > >>> + .driver_data = (void *)&limits_30w, > >>> + }, > >>> + { > >>> + .ident = "AYANEO 3", > >>> + .matches = { > >>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > >>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO 3"), > >>> + }, > >>> + .driver_data = (void *)&limits_30w, > >>> + }, > >>> { } > >>> }; > >>> MODULE_DEVICE_TABLE(dmi, dptc_dmi_table); > >> > >> > > > > ^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [RFC v4 4/4] platform/x86/amd: dptc: Add device entries for handheld PCs 2026-03-11 19:13 ` Antheas Kapenekakis @ 2026-03-12 13:26 ` Mario Limonciello 0 siblings, 0 replies; 21+ messages in thread From: Mario Limonciello @ 2026-03-12 13:26 UTC (permalink / raw) To: Antheas Kapenekakis Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato, i, linux-kernel, platform-driver-x86 On 3/11/26 14:13, Antheas Kapenekakis wrote: > On Tue, 10 Mar 2026 at 17:35, Mario Limonciello > <mario.limonciello@amd.com> wrote: >> >> On 3/10/26 3:13 AM, Antheas Kapenekakis wrote: >>> On Tue, 10 Mar 2026 at 05:11, Mario Limonciello >>> <mario.limonciello@amd.com> wrote: >>>> >>>> >>>> >>>> On 3/9/2026 3:51 PM, Antheas Kapenekakis wrote: >>>>> Add limit tables and DMI entries for AMD APU-based handheld devices: >>>>> >>>>> - 15W class: AYANEO AIR >>>>> - 18W class: AYANEO AIR Plus/Pro >>>>> - 25W class: AYANEO AIR 1S, NEXT/KUN >>>>> - 28W class: GPD Win Mini/4/Max 2, GPD Duo, GPD Pocket 4, AYANEO 2, >>>>> OrangePi NEO-01, SuiPlay0X1 >>>>> - 30W class: AYANEO FLIP/GEEK/SLIDE/3, AOKZOE A1/A1X/A2, >>>>> OneXPlayer F1Pro/2/X1/G1, AYN Loki Max, Zeenix Pro >>>>> >>>>> Also add SoC strings for Ryzen AI, Ryzen 8000/7040/6000/5000 >>>>> families, extending support beyond the initial AI MAX series. >>>> >>>> There are a lot of numbers here. Where did they all come from? What >>>> are guesses? What came from RE? What came from documentation? >>>> How do you know these are good for the thermal design of the platform? >>>> >>>> I appreciate some of them could be cargo culted from another codebase, >>>> but I think it would be a good idea to document which ones are more >>>> likely to be accurate and why. >>> >>> Ok, here is the logic behind the limits. >>> >>> 15/18/25 are manufacturer claims by Ayaneo. Ayaneo is the only >>> manufacturer that made devices prior to the new ones that support 30W >>> limits. >>> >>> 28W is used for GPD because their devices are very portable and 30W >>> makes e.g. the bottom pan of Win Minis too hot. It is also used for >>> the Ayaneo 2/Sui/OPI, because these devices have a fullwidth glass >>> front that is used for heat dissipation. So the screen gets very hot. >>> >>> The rest of the devices use 30W. >>> >>> As for which software each OEM has. GPD: GPD does not ship first party >>> software for TDP controls. They collaborate with an independent >>> developer developing "Motion Assistant", which they also ship with >>> their devices and features a TDP slider. Ayaneo uses Ayaspace, which >>> is backed by Ryzenadj, and they give slider control to users, >>> typically with a 30W limit. They also use userspace customizable >>> presets for some default e.g. 20W profiles. Onexplayer has oxpconsole >>> with a slider as well that does 30W usually. Ayn exited the market on >>> WIndows handhelds, no software. OPI, you'd need to ask Phil but this >>> would be the primary driver for it. There is no alternative that can >>> also go to SteamOS. >>> >>> The reasoning behind the profiles is simple. 8W is power savings >>> territory. Going below can yield around 1W more, but the performance >>> degradation is not worth it. 15W is a good sweetspot for 30W >>> processors. Then, 25W is good for these devices as they saturate above >>> 22ish Watts. We could even go lower to 20W, as Lenovo did on the >>> original Go. But their newer devices use higher AC values. >>> >>> For their boost clocks, sPPT is set to around 2W higher than spl (what >>> is it called now?) to give a small boost. fPPT is set to a reasonable >>> amount for that device class and interpolated vs spl. The exception is >>> low-power where we do not want boost >>> >>> Antheas >> >> OK, each and every one of these cases needs to be documented. >> >> I suggest that they each go in their own patches, and then if there is >> any disagreement it's a single patch to revert or drop from the rest of >> the series. > > Ok, so here is what I suggest going forward. > > We keep the last patch while this is an RFC and annotate it as planned > to be dropped so the series can still be tested. Then, once we exit > RFC we merge initially for Win 5 to reduce email bloat. After merge, I > go through the DMIs and group them by device type, adding information > about the devices on the commit message. Then, these go to for-fixes > with a cc stable. > > While doing that, I will drop all non EXACT matches. Specifically, the > Ayaneo 1S had a few variants with different DMIs (1S Limited being one > of them if I recall). > > This will allow us to drop the CPU match with high certainty. I will > replace it with just an is AMD check with no logs or perhaps a debug > log, to avoid polluting Intel variant's dmesg if any. I suggest using X86_FEATURE_ZEN if you need a check to prove it's AMD. > > Best, > Antheas > >>> >>>>> >>>>> Assisted-by: Claude:claude-opus-4-6 >>>>> Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev> >>>>> --- >>>>> drivers/platform/x86/amd/dptc.c | 420 ++++++++++++++++++++++++++++++++ >>>>> 1 file changed, 420 insertions(+) >>>>> >>>>> diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c >>>>> index f4db95affb1b..b2820f8652f1 100644 >>>>> --- a/drivers/platform/x86/amd/dptc.c >>>>> +++ b/drivers/platform/x86/amd/dptc.c >>>>> @@ -86,6 +86,81 @@ static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = { >>>>> 1, ALIB_ID_TEMP_TARGET }, >>>>> }; >>>>> >>>>> +/* 15W class: AYANEO AIR (Ryzen 5 5560U) */ >>>>> +static const struct dptc_device_limits limits_15w = { >>>>> + .params = { >>>>> + [DPTC_PPT_PL1_SPL] = { 1, 5, 10, 15, 18 }, >>>>> + [DPTC_PPT_PL2_SPPT] = { 1, 5, 10, 15, 18 }, >>>>> + [DPTC_PPT_PL3_FPPT] = { 1, 5, 12, 18, 22 }, >>>>> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, >>>>> + }, >>>>> + .profiles = { >>>>> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } }, >>>>> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 10, 12, 15, 0 } }, >>>>> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 15, 15, 18, 0 } }, >>>>> + }, >>>>> +}; >>>>> + >>>>> +/* 18W class: AYANEO AIR Plus/Pro (Ryzen 5 5560U, Ryzen 7 5825U) */ >>>>> +static const struct dptc_device_limits limits_18w = { >>>>> + .params = { >>>>> + [DPTC_PPT_PL1_SPL] = { 1, 5, 15, 18, 22 }, >>>>> + [DPTC_PPT_PL2_SPPT] = { 1, 5, 15, 18, 22 }, >>>>> + [DPTC_PPT_PL3_FPPT] = { 1, 5, 15, 20, 25 }, >>>>> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, >>>>> + }, >>>>> + .profiles = { >>>>> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } }, >>>>> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 12, 14, 15, 0 } }, >>>>> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 18, 18, 20, 0 } }, >>>>> + }, >>>>> +}; >>>>> + >>>>> +/* 25W class: Ryzen 5000 handhelds (AYANEO NEXT, KUN) */ >>>>> +static const struct dptc_device_limits limits_25w = { >>>>> + .params = { >>>>> + [DPTC_PPT_PL1_SPL] = { 1, 4, 15, 25, 32 }, >>>>> + [DPTC_PPT_PL2_SPPT] = { 1, 4, 20, 27, 35 }, >>>>> + [DPTC_PPT_PL3_FPPT] = { 1, 4, 25, 30, 37 }, >>>>> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, >>>>> + }, >>>>> + .profiles = { >>>>> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } }, >>>>> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 20, 0 } }, >>>>> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 27, 30, 0 } }, >>>>> + }, >>>>> +}; >>>>> + >>>>> +/* 28W class: GPD Win series, AYANEO 2, OrangePi NEO-01 */ >>>>> +static const struct dptc_device_limits limits_28w = { >>>>> + .params = { >>>>> + [DPTC_PPT_PL1_SPL] = { 1, 4, 15, 28, 32 }, >>>>> + [DPTC_PPT_PL2_SPPT] = { 1, 4, 20, 30, 35 }, >>>>> + [DPTC_PPT_PL3_FPPT] = { 1, 4, 25, 32, 37 }, >>>>> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, >>>>> + }, >>>>> + .profiles = { >>>>> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } }, >>>>> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 22, 0 } }, >>>>> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 32, 0 } }, >>>>> + }, >>>>> +}; >>>>> + >>>>> +/* 30W class: OneXPlayer, AYANEO FLIP/GEEK/SLIDE/3, AOKZOE */ >>>>> +static const struct dptc_device_limits limits_30w = { >>>>> + .params = { >>>>> + [DPTC_PPT_PL1_SPL] = { 1, 4, 15, 30, 40 }, >>>>> + [DPTC_PPT_PL2_SPPT] = { 1, 4, 20, 32, 43 }, >>>>> + [DPTC_PPT_PL3_FPPT] = { 1, 4, 25, 41, 50 }, >>>>> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 }, >>>>> + }, >>>>> + .profiles = { >>>>> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } }, >>>>> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 25, 0 } }, >>>>> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 41, 0 } }, >>>>> + }, >>>>> +}; >>>>> + >>>>> /* AI MAX Handheld class: GPD Win 5 */ >>>>> static const struct dptc_device_limits limits_maxhh = { >>>>> .params = { >>>>> @@ -107,11 +182,56 @@ static const char * const dptc_soc_table[] = { >>>>> "AMD RYZEN AI MAX+ 395", >>>>> "AMD RYZEN AI MAX+ 385", >>>>> "AMD RYZEN AI MAX 380", >>>>> + /* Ryzen AI */ >>>>> + "AMD Ryzen AI 9 HX 370", >>>>> + /* Ryzen 8000 */ >>>>> + "AMD Ryzen 7 8840U", >>>>> + /* Ryzen 7040 */ >>>>> + "AMD Ryzen 7 7840U", >>>>> + /* Ryzen 6000 */ >>>>> + "AMD Ryzen 7 6800U", >>>>> + "AMD Ryzen 7 6600U", >>>>> + /* Ryzen 5000 */ >>>>> + "AMD Ryzen 7 5800U", >>>>> + "AMD Ryzen 7 5700U", >>>>> + "AMD Ryzen 5 5560U", >>>> >>>> Still don't understand the point of this list. >>> >>> Parallel reply. >>> >>>>> NULL, >>>>> }; >>>>> >>>>> static const struct dmi_system_id dptc_dmi_table[] = { >>>>> /* GPD */ >>>>> + { >>>>> + .ident = "GPD Win Mini", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>>>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-01"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_28w, >>>>> + }, >>>>> + { >>>>> + .ident = "GPD Win Mini 2024", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>>>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_28w, >>>>> + }, >>>>> + { >>>>> + .ident = "GPD Win Mini 2024", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>>>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02-L"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_28w, >>>>> + }, >>>>> + { >>>>> + .ident = "GPD Win 4", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>>>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-04"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_28w, >>>>> + }, >>>>> { >>>>> .ident = "GPD Win 5", >>>>> .matches = { >>>>> @@ -120,6 +240,306 @@ static const struct dmi_system_id dptc_dmi_table[] = { >>>>> }, >>>>> .driver_data = (void *)&limits_maxhh, >>>>> }, >>>>> + { >>>>> + .ident = "GPD Win Max 2", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>>>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-04"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_28w, >>>>> + }, >>>>> + { >>>>> + .ident = "GPD Win Max 2 2024", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>>>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-05"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_28w, >>>>> + }, >>>>> + { >>>>> + .ident = "GPD Duo", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>>>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_28w, >>>>> + }, >>>>> + { >>>>> + .ident = "GPD Duo", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>>>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01-L"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_28w, >>>>> + }, >>>>> + { >>>>> + .ident = "GPD Pocket 4", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>>>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_28w, >>>>> + }, >>>>> + { >>>>> + .ident = "GPD Pocket 4", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), >>>>> + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04-L"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_28w, >>>>> + }, >>>>> + /* OrangePi */ >>>>> + { >>>>> + .ident = "OrangePi NEO-01", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_28w, >>>>> + }, >>>>> + /* AYN */ >>>>> + { >>>>> + .ident = "AYN Loki Max", >>>>> + .matches = { >>>>> + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ayn"), >>>>> + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Loki Max"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_30w, >>>>> + }, >>>>> + /* Tectoy (Zeenix Pro = Loki Max) */ >>>>> + { >>>>> + .ident = "Zeenix Pro", >>>>> + .matches = { >>>>> + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Tectoy"), >>>>> + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Zeenix Pro"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_30w, >>>>> + }, >>>>> + /* AOKZOE */ >>>>> + { >>>>> + .ident = "AOKZOE A1 AR07", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 AR07"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_30w, >>>>> + }, >>>>> + { >>>>> + .ident = "AOKZOE A1 Pro", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 Pro"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_30w, >>>>> + }, >>>>> + { >>>>> + .ident = "AOKZOE A1X", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1X"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_30w, >>>>> + }, >>>>> + { >>>>> + .ident = "AOKZOE A2 Pro", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A2 Pro"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_30w, >>>>> + }, >>>>> + /* OneXPlayer (Intel variants filtered by SoC table) */ >>>> >>>> Are you telling me the Intel variants actually have the same ACPI >>>> methods this series looks for? >>> >>> They do not. >> >> OK then this comment appears wrong. > > Ack See above. > >>> >>>>> + { >>>>> + .ident = "ONEXPLAYER F1Pro", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1Pro"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_30w, >>>>> + }, >>>>> + { >>>>> + .ident = "ONEXPLAYER F1 EVA-02", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1 EVA-02"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_30w, >>>>> + }, >>>>> + { >>>>> + .ident = "ONEXPLAYER 2", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), >>>>> + DMI_MATCH(DMI_BOARD_NAME, "ONEXPLAYER 2"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_30w, >>>>> + }, >>>>> + { >>>>> + .ident = "ONEXPLAYER X1 A", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1 A"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_30w, >>>>> + }, >>>>> + { >>>>> + .ident = "ONEXPLAYER X1z", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1z"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_30w, >>>>> + }, >>>>> + { >>>>> + .ident = "ONEXPLAYER X1Pro", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Pro"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_30w, >>>>> + }, >>>>> + { >>>>> + .ident = "ONEXPLAYER G1 A", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 A"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_30w, >>>>> + }, >>>>> + /* AYANEO - 15W */ >>>>> + { >>>>> + .ident = "AYANEO AIR", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_15w, >>>>> + }, >>>>> + /* AYANEO - 18W */ >>>>> + { >>>>> + .ident = "AYANEO AIR Plus", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Plus"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_18w, >>>>> + }, >>>>> + { >>>>> + .ident = "AYANEO AIR Pro", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Pro"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_18w, >>>>> + }, >>>>> + /* AYANEO - 25W */ >>>>> + { >>>>> + .ident = "AYANEO AIR 1S", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>>>> + DMI_MATCH(DMI_BOARD_NAME, "AIR 1S"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_25w, >>>>> + }, >>>>> + { >>>>> + .ident = "AYANEO NEXT Advance", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Advance"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_25w, >>>>> + }, >>>>> + { >>>>> + .ident = "AYANEO NEXT Lite", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Lite"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_25w, >>>>> + }, >>>>> + { >>>>> + .ident = "AYANEO NEXT Pro", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Pro"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_25w, >>>>> + }, >>>>> + { >>>>> + .ident = "AYANEO NEXT", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_25w, >>>>> + }, >>>>> + { >>>>> + .ident = "AYANEO KUN", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_25w, >>>>> + }, >>>>> + { >>>>> + .ident = "AYANEO KUN", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO KUN"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_25w, >>>>> + }, >>>>> + /* AYANEO - 28W */ >>>>> + { >>>>> + .ident = "AYANEO 2", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>>>> + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 2"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_28w, >>>>> + }, >>>>> + { >>>>> + .ident = "SuiPlay0X1", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_SYS_VENDOR, "Mysten Labs, Inc."), >>>>> + DMI_MATCH(DMI_PRODUCT_NAME, "SuiPlay0X1"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_28w, >>>>> + }, >>>>> + /* AYANEO - 30W */ >>>>> + { >>>>> + /* DMI_MATCH catches all FLIP variants (DS, KB, 1S DS, 1S KB) */ >>>>> + .ident = "AYANEO FLIP", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>>>> + DMI_MATCH(DMI_BOARD_NAME, "FLIP"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_30w, >>>>> + }, >>>>> + { >>>>> + /* DMI_MATCH catches GEEK and GEEK 1S */ >>>>> + .ident = "AYANEO GEEK", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>>>> + DMI_MATCH(DMI_BOARD_NAME, "GEEK"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_30w, >>>>> + }, >>>>> + { >>>>> + .ident = "AYANEO SLIDE", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "SLIDE"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_30w, >>>>> + }, >>>>> + { >>>>> + .ident = "AYANEO 3", >>>>> + .matches = { >>>>> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), >>>>> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO 3"), >>>>> + }, >>>>> + .driver_data = (void *)&limits_30w, >>>>> + }, >>>>> { } >>>>> }; >>>>> MODULE_DEVICE_TABLE(dmi, dptc_dmi_table); >>>> >>>> >>> >> >> > ^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [RFC v4 0/4] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls 2026-03-09 20:51 [RFC v4 0/4] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Antheas Kapenekakis ` (3 preceding siblings ...) 2026-03-09 20:51 ` [RFC v4 4/4] platform/x86/amd: dptc: Add device entries for handheld PCs Antheas Kapenekakis @ 2026-03-10 2:43 ` Mario Limonciello 2026-03-10 7:59 ` Antheas Kapenekakis 4 siblings, 1 reply; 21+ messages in thread From: Mario Limonciello @ 2026-03-10 2:43 UTC (permalink / raw) To: Antheas Kapenekakis Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato, i, linux-kernel, platform-driver-x86 On 3/9/2026 3:51 PM, Antheas Kapenekakis wrote: > Many AMD-based handheld PCs (GPD, AYANEO, OneXPlayer, AOKZOE, OrangePi) > ship with the AGESA ALIB method at \_SB.ALIB, which accepts Function 0x0C > (the Dynamic Power and Thermal Configuration Interface, DPTCi). This > allows software to adjust APU power and thermal parameters at runtime: > sustained power limit (SPL/STAPM + skin), slow PPT, fast PPT, and the > thermal control target. > > Unlike mainstream AMD laptops, these devices do not implement vendor- > specific WMI or EC hooks for TDP control. The ones that do, use DPTCi > under the hood. For these devices, ALIB is the only viable mechanism for > the OS to adjust power limits, making a dedicated kernel driver the > correct approach rather than relying on the out-of-tree acpi_call module > or ryzenadj. > > The driver provides two layers of control: > > * Platform profile integration (low-power / balanced / performance / > custom), with per-device preset tunings derived from thermal envelope > data. Selecting a non-custom profile applies values immediately and > locks the individual tunables to read-only. The default profile is > "custom", leaving the device at firmware defaults until userspace > explicitly selects a profile. > > * Four firmware-attributes tunables (ppt_pl1_spl, ppt_pl2_sppt, > ppt_pl3_fppt, cpu_temp) that become writable in "custom" mode. > Values are enforced against per-device limits (smin..smax), with an > "expanded_limits" toggle that widens the range to the full hardware- > validated envelope (min..max). A save_settings attribute controls > whether writes commit immediately ("single") or are staged for an > explicit bulk "save". > > On resume, the active profile or staged values are re-applied so that > suspend/resume cycles do not silently revert to firmware defaults unless > in custom and in bulk mode. In that case, defer to userspace. > > Device limits are supplied for GPD Win Mini / Win 4 / Win 5 / Win Max 2 / > Duo / Pocket 4, OrangePi NEO-01, AOKZOE A1/A2, OneXPlayer F1/2/X1/G1, > and numerous AYANEO models. The SoC table covers Ryzen 5000, 6000, 7040, > 8000, AI 9 HX 370, and the Ryzen AI MAX series. > > Tested on a GPD Win 5 (Ryzen AI MAX+ 395). Confirmed with ryzenadj -i > that committed values are applied to hardware, and that fast/slow PPT > limits are honoured under a full-CPU stress load. > > Responsible disclosure: The development of this driver is AI assisted. > This includes its writing, testing, reviewing, and readmes. The driver was > manually reviewed line by line, but there may still be small leftover > quirks. This is an RFC, not a final version. Let's push these tools to > their limits and see where it takes us. > I'll tell you where the most immediate limit is. Humans can't review RFC changes that go in daily. Maybe you can pass this to a robot and it can give you actionable feedback, but a human sitting down and reviewing a patch series in a serious way takes time. Over my weekend I see multiple copies of the series in my inbox. To me they must have obviously been garbage because you sent them so quickly instead of waiting for feedback. So I'll delete those from my inbox and review the most recent one. That's somewhat tongue in cheek. I know you're excited for this series, but please make sure you take time to gather and take feedback into account before posting another series. > Assisted-by: Claude:claude-opus-4-6 > > --- > > Changes in v4: > - Align dptc_params continuation lines to opening brace > - Reflow all dptc_device_limits structs: expand zipped braces into > separate .params and .profiles blocks with proper indentation > - Extract enum dptc_save_mode from struct dptc_priv to a standalone > declaration > - Replace vague mutex comment with explicit list of protected members > - Use &buf[off + 1] form for put_unaligned_le32 > - Fix misaligned '=' in ACPI in_params block > - Factor out dptc_alib_fill_param() helper to deduplicate ALIB buffer > construction in dptc_alib_send_one() and dptc_alib_save(); use int > instead of size_t for element counts throughout > - Consolidate dptc_current_value_store: single guard(mutex) instead of > two, eliminating duplicated profile check > - Return -EBUSY instead of -EPERM when profile is not custom > - Add blank line after early returns for readability > - Consolidate dptc_alib_save() call into dptc_apply_profile(), which > now returns int; simplify dptc_pp_set() and dptc_resume() callers > - Squash device_create formatting fix into the patch that introduced it > - Remove bogus "AMD Ryzen AI HX 360" SoC entry (no such model exists) > - Return -ENOENT instead of -EINVAL from dptc_alib_call() when no > parameters are staged, matching think-lmi save_settings semantics > - Remove bool has_staged[] array: use staged[i] == 0 as the "not > staged" sentinel, raise expanded_min from 0 to 1 W to ensure 0 is > never a valid user value > - Remove unnecessary braces from if/else if/else chain in dptc_resume() > - Drop Ryzen Z1 SoC entries: Z1 devices (Lenovo/Asus) use vendor EC/PMF > drivers, not ALIB DPTCi > > Changes in v3: > - Split single driver patch into 3: core driver, platform profile, > device entries > - Rename DRIVER_NAME from "amd_dptc" to "amd-dptc" (match subsystem > convention for platform drivers and firmware-attributes devices) > - Add scale field to dptc_param_desc: sysfs values in user units (W, C), > driver multiplies by scale (1000 for mW) before sending to ALIB > - Rename struct fields: min/smin/smax/max -> > expanded_min/device_min/device_max/expanded_max > - Remove comment "ALIB parameter IDs (AGESA spec Appendix E.5, Table > E-52)" > - Move ALIB method check after SoC/DMI validation, change pr_debug to > pr_warn > - Reorder local variable declaration in dptc_init (dptc after other vars) > - Add commit subject prefix "dptc:" to all driver patches > - Remove early-return for empty save in dptc_alib_save; let > dptc_alib_call handle the empty case > - Remove max_power as we do not do DC/AC validation in the driver > - Fix Ayaneo AIR device matches to reflect their wattage, add 15W profile > for original AIR. Cheers to the AIR Plus user who had helped tune the > AIR Plus profile so it was correct. > > Changes in v2: > - Use a platform_device base instead of raw inits + exit, hook into devm > helpers referencing samsung-galaxybook > - Add platform_profile support (low-power / balanced / performance / > custom) with per-device energy presets; non-custom profiles lock > tunables to read-only. We default to custom to avoid writing values > - Reduce exposed parameters from seven to four (ppt_pl1_spl, > ppt_pl2_sppt, ppt_pl3_fppt, cpu_temp); drop time constants and > separate skin/STAPM limits in favour of a single SPL that sets both. > For devices where the max tdp offers thermals that are not suitable > for day to day use (e.g., excessive fan noise), MAX_POWER is added > - Remove CONFIG_AMD_DPTC_EXTENDED and the "soc"/"unbound" limit tiers; > keep only device (smin..smax) and expanded (min..max) and soc match > (certain devices ship multiple SoCs on the same motherboard/thermal > envelope, make sure we only hook into validated SoCs) > - Rename "commit" attribute to "save_settings" per firmware-attributes > ABI; rename limit_mode to expanded_limits > - Change expanded_limits attribute type from "enumeration" to "integer" > (min=0, max=1) since firmware-attributes has no bool type > - Remove all global vars, limit _dev access to init and exit > - Use u32 accessors to set values for ALIB call > - Clean up verbose comments throughout > - Add Ayn Loki Max / Tectoy Zeenix Pro > > V3: https://lore.kernel.org/all/20260307115516.26892-1-lkml@antheas.dev/ > V2: https://lore.kernel.org/all/20260305181751.3642846-1-lkml@antheas.dev/ > V1: https://lore.kernel.org/all/20260303181707.2920261-1-lkml@antheas.dev/ > > Antheas Kapenekakis (4): > Documentation: firmware-attributes: generalize save_settings entry > platform/x86/amd: dptc: Add AMD DPTCi driver > platform/x86/amd: dptc: Add platform profile support > platform/x86/amd: dptc: Add device entries for handheld PCs > > .../testing/sysfs-class-firmware-attributes | 41 +- > MAINTAINERS | 6 + > drivers/platform/x86/amd/Kconfig | 15 + > drivers/platform/x86/amd/Makefile | 2 + > drivers/platform/x86/amd/dptc.c | 1271 +++++++++++++++++ > 5 files changed, 1320 insertions(+), 15 deletions(-) > create mode 100644 drivers/platform/x86/amd/dptc.c > > > base-commit: 4ae12d8bd9a830799db335ee661d6cbc6597f838 ^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [RFC v4 0/4] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls 2026-03-10 2:43 ` [RFC v4 0/4] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Mario Limonciello @ 2026-03-10 7:59 ` Antheas Kapenekakis 0 siblings, 0 replies; 21+ messages in thread From: Antheas Kapenekakis @ 2026-03-10 7:59 UTC (permalink / raw) To: Mario Limonciello Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato, i, linux-kernel, platform-driver-x86 On Tue, 10 Mar 2026 at 03:43, Mario Limonciello <mario.limonciello@amd.com> wrote: > > > > On 3/9/2026 3:51 PM, Antheas Kapenekakis wrote: > > Many AMD-based handheld PCs (GPD, AYANEO, OneXPlayer, AOKZOE, OrangePi) > > ship with the AGESA ALIB method at \_SB.ALIB, which accepts Function 0x0C > > (the Dynamic Power and Thermal Configuration Interface, DPTCi). This > > allows software to adjust APU power and thermal parameters at runtime: > > sustained power limit (SPL/STAPM + skin), slow PPT, fast PPT, and the > > thermal control target. > > > > Unlike mainstream AMD laptops, these devices do not implement vendor- > > specific WMI or EC hooks for TDP control. The ones that do, use DPTCi > > under the hood. For these devices, ALIB is the only viable mechanism for > > the OS to adjust power limits, making a dedicated kernel driver the > > correct approach rather than relying on the out-of-tree acpi_call module > > or ryzenadj. > > > > The driver provides two layers of control: > > > > * Platform profile integration (low-power / balanced / performance / > > custom), with per-device preset tunings derived from thermal envelope > > data. Selecting a non-custom profile applies values immediately and > > locks the individual tunables to read-only. The default profile is > > "custom", leaving the device at firmware defaults until userspace > > explicitly selects a profile. > > > > * Four firmware-attributes tunables (ppt_pl1_spl, ppt_pl2_sppt, > > ppt_pl3_fppt, cpu_temp) that become writable in "custom" mode. > > Values are enforced against per-device limits (smin..smax), with an > > "expanded_limits" toggle that widens the range to the full hardware- > > validated envelope (min..max). A save_settings attribute controls > > whether writes commit immediately ("single") or are staged for an > > explicit bulk "save". > > > > On resume, the active profile or staged values are re-applied so that > > suspend/resume cycles do not silently revert to firmware defaults unless > > in custom and in bulk mode. In that case, defer to userspace. > > > > Device limits are supplied for GPD Win Mini / Win 4 / Win 5 / Win Max 2 / > > Duo / Pocket 4, OrangePi NEO-01, AOKZOE A1/A2, OneXPlayer F1/2/X1/G1, > > and numerous AYANEO models. The SoC table covers Ryzen 5000, 6000, 7040, > > 8000, AI 9 HX 370, and the Ryzen AI MAX series. > > > > Tested on a GPD Win 5 (Ryzen AI MAX+ 395). Confirmed with ryzenadj -i > > that committed values are applied to hardware, and that fast/slow PPT > > limits are honoured under a full-CPU stress load. > > > > Responsible disclosure: The development of this driver is AI assisted. > > This includes its writing, testing, reviewing, and readmes. The driver was > > manually reviewed line by line, but there may still be small leftover > > quirks. This is an RFC, not a final version. Let's push these tools to > > their limits and see where it takes us. > > > > I'll tell you where the most immediate limit is. Humans can't review > RFC changes that go in daily. > > Over my weekend I see multiple copies of the series in my inbox. To me > they must have obviously been garbage because you sent them so quickly > instead of waiting for feedback. So I'll delete those from my inbox and > review the most recent one. > > That's somewhat tongue in cheek. I know you're excited for this series, > but please make sure you take time to gather and take feedback into > account before posting another series. It's true, I've been having a bit of fun. But the series is stabilized now, I don't plan to send another version soon unless I messed up something. V3 incorporated your feedback/derek's and V4 incorporated rong's feedback. V4 is minor, it is just nits. > Maybe you can pass this to a robot and it > can give you actionable feedback, but a human sitting down and reviewing > a patch series in a serious way takes time. I'm using this series as a testbed to make some tooling for this, maybe I will post it soon. > > Assisted-by: Claude:claude-opus-4-6 > > > > --- > > > > Changes in v4: > > - Align dptc_params continuation lines to opening brace > > - Reflow all dptc_device_limits structs: expand zipped braces into > > separate .params and .profiles blocks with proper indentation > > - Extract enum dptc_save_mode from struct dptc_priv to a standalone > > declaration > > - Replace vague mutex comment with explicit list of protected members > > - Use &buf[off + 1] form for put_unaligned_le32 > > - Fix misaligned '=' in ACPI in_params block > > - Factor out dptc_alib_fill_param() helper to deduplicate ALIB buffer > > construction in dptc_alib_send_one() and dptc_alib_save(); use int > > instead of size_t for element counts throughout > > - Consolidate dptc_current_value_store: single guard(mutex) instead of > > two, eliminating duplicated profile check > > - Return -EBUSY instead of -EPERM when profile is not custom > > - Add blank line after early returns for readability > > - Consolidate dptc_alib_save() call into dptc_apply_profile(), which > > now returns int; simplify dptc_pp_set() and dptc_resume() callers > > - Squash device_create formatting fix into the patch that introduced it > > - Remove bogus "AMD Ryzen AI HX 360" SoC entry (no such model exists) > > - Return -ENOENT instead of -EINVAL from dptc_alib_call() when no > > parameters are staged, matching think-lmi save_settings semantics > > - Remove bool has_staged[] array: use staged[i] == 0 as the "not > > staged" sentinel, raise expanded_min from 0 to 1 W to ensure 0 is > > never a valid user value > > - Remove unnecessary braces from if/else if/else chain in dptc_resume() > > - Drop Ryzen Z1 SoC entries: Z1 devices (Lenovo/Asus) use vendor EC/PMF > > drivers, not ALIB DPTCi > > > > Changes in v3: > > - Split single driver patch into 3: core driver, platform profile, > > device entries > > - Rename DRIVER_NAME from "amd_dptc" to "amd-dptc" (match subsystem > > convention for platform drivers and firmware-attributes devices) > > - Add scale field to dptc_param_desc: sysfs values in user units (W, C), > > driver multiplies by scale (1000 for mW) before sending to ALIB > > - Rename struct fields: min/smin/smax/max -> > > expanded_min/device_min/device_max/expanded_max > > - Remove comment "ALIB parameter IDs (AGESA spec Appendix E.5, Table > > E-52)" > > - Move ALIB method check after SoC/DMI validation, change pr_debug to > > pr_warn > > - Reorder local variable declaration in dptc_init (dptc after other vars) > > - Add commit subject prefix "dptc:" to all driver patches > > - Remove early-return for empty save in dptc_alib_save; let > > dptc_alib_call handle the empty case > > - Remove max_power as we do not do DC/AC validation in the driver > > - Fix Ayaneo AIR device matches to reflect their wattage, add 15W profile > > for original AIR. Cheers to the AIR Plus user who had helped tune the > > AIR Plus profile so it was correct. > > > > Changes in v2: > > - Use a platform_device base instead of raw inits + exit, hook into devm > > helpers referencing samsung-galaxybook > > - Add platform_profile support (low-power / balanced / performance / > > custom) with per-device energy presets; non-custom profiles lock > > tunables to read-only. We default to custom to avoid writing values > > - Reduce exposed parameters from seven to four (ppt_pl1_spl, > > ppt_pl2_sppt, ppt_pl3_fppt, cpu_temp); drop time constants and > > separate skin/STAPM limits in favour of a single SPL that sets both. > > For devices where the max tdp offers thermals that are not suitable > > for day to day use (e.g., excessive fan noise), MAX_POWER is added > > - Remove CONFIG_AMD_DPTC_EXTENDED and the "soc"/"unbound" limit tiers; > > keep only device (smin..smax) and expanded (min..max) and soc match > > (certain devices ship multiple SoCs on the same motherboard/thermal > > envelope, make sure we only hook into validated SoCs) > > - Rename "commit" attribute to "save_settings" per firmware-attributes > > ABI; rename limit_mode to expanded_limits > > - Change expanded_limits attribute type from "enumeration" to "integer" > > (min=0, max=1) since firmware-attributes has no bool type > > - Remove all global vars, limit _dev access to init and exit > > - Use u32 accessors to set values for ALIB call > > - Clean up verbose comments throughout > > - Add Ayn Loki Max / Tectoy Zeenix Pro > > > > V3: https://lore.kernel.org/all/20260307115516.26892-1-lkml@antheas.dev/ > > V2: https://lore.kernel.org/all/20260305181751.3642846-1-lkml@antheas.dev/ > > V1: https://lore.kernel.org/all/20260303181707.2920261-1-lkml@antheas.dev/ > > > > Antheas Kapenekakis (4): > > Documentation: firmware-attributes: generalize save_settings entry > > platform/x86/amd: dptc: Add AMD DPTCi driver > > platform/x86/amd: dptc: Add platform profile support > > platform/x86/amd: dptc: Add device entries for handheld PCs > > > > .../testing/sysfs-class-firmware-attributes | 41 +- > > MAINTAINERS | 6 + > > drivers/platform/x86/amd/Kconfig | 15 + > > drivers/platform/x86/amd/Makefile | 2 + > > drivers/platform/x86/amd/dptc.c | 1271 +++++++++++++++++ > > 5 files changed, 1320 insertions(+), 15 deletions(-) > > create mode 100644 drivers/platform/x86/amd/dptc.c > > > > > > base-commit: 4ae12d8bd9a830799db335ee661d6cbc6597f838 > > ^ permalink raw reply [flat|nested] 21+ messages in thread
end of thread, other threads:[~2026-03-12 16:19 UTC | newest] Thread overview: 21+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-03-09 20:51 [RFC v4 0/4] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Antheas Kapenekakis 2026-03-09 20:51 ` [RFC v4 1/4] Documentation: firmware-attributes: generalize save_settings entry Antheas Kapenekakis 2026-03-09 20:51 ` [RFC v4 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver Antheas Kapenekakis 2026-03-10 4:01 ` Mario Limonciello 2026-03-10 8:02 ` Antheas Kapenekakis 2026-03-10 16:26 ` Mario Limonciello 2026-03-11 19:09 ` Antheas Kapenekakis 2026-03-12 13:24 ` Mario Limonciello 2026-03-12 13:47 ` Antheas Kapenekakis 2026-03-12 16:05 ` Mario Limonciello 2026-03-12 16:19 ` Antheas Kapenekakis 2026-03-09 20:51 ` [RFC v4 3/4] platform/x86/amd: dptc: Add platform profile support Antheas Kapenekakis 2026-03-10 4:07 ` Mario Limonciello 2026-03-09 20:51 ` [RFC v4 4/4] platform/x86/amd: dptc: Add device entries for handheld PCs Antheas Kapenekakis 2026-03-10 4:11 ` Mario Limonciello 2026-03-10 8:13 ` Antheas Kapenekakis 2026-03-10 16:35 ` Mario Limonciello 2026-03-11 19:13 ` Antheas Kapenekakis 2026-03-12 13:26 ` Mario Limonciello 2026-03-10 2:43 ` [RFC v4 0/4] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Mario Limonciello 2026-03-10 7:59 ` Antheas Kapenekakis
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.