* [RFC v1 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls
@ 2026-03-03 18:17 Antheas Kapenekakis
2026-03-03 18:17 ` [RFC v1 1/2] Documentation: firmware-attributes: generalize save_settings entry Antheas Kapenekakis
` (2 more replies)
0 siblings, 3 replies; 18+ messages in thread
From: Antheas Kapenekakis @ 2026-03-03 18:17 UTC (permalink / raw)
To: Mario.Limonciello; +Cc: 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:
STAPM limit, fast/slow PPT limits, skin-temperature TDP limit, slow/STAPM
time constants, and the thermal control target.
Until now userspace has reached this interface through the acpi_call out-
of-tree module or ryzenadj, which carry no ABI guarantees and no per-device
safety limits. This driver replaces that with a proper in-kernel
implementation that:
* Exposes all seven parameters through the firmware-attributes sysfs ABI,
so that standard tools (fwupd, systemd-bios-vendor, etc.) can enumerate
and modify them without device-specific knowledge.
* Enforces tiered per-device and per-SoC limits. The default "device"
mode restricts writes to a curated safe range (smin..smax) derived from
the device's thermal design. An "expanded" mode exposes the full
hardware-validated range. An optional CONFIG_AMD_DPTC_EXTENDED Kconfig
adds "soc" (raw ALIB_PARAMS envelope) and "unbound" tiers for advanced
use. The active tier is itself a firmware-attribute, switchable at
runtime.
* Stages values and commits them atomically in a single ALIB call,
matching the protocol's intended bulk-update semantics. A save_settings
attribute (per firmware-attributes ABI) controls whether writes commit
immediately ("single" mode) or are held until an explicit "save".
* When in "single" mode, re-applies staged values after system resume,
so suspend/resume cycles do not silently revert to firmware defaults.
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, Z1, 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.
@Mario: can you suggest a CC list for V2? Thanks. Even if not merged, this
driver is still good for downstream use.
---
Usage
-----
List all exposed attributes (read-only, no root required):
$ fwupdmgr get-bios-settings
This enumerates every attribute under /sys/class/firmware-attributes/,
including current_value, default_value, min_value, max_value, and
display_name for each DPTCi parameter.
Sysfs direct usage
------------------
All paths are under:
ATTR=/sys/class/firmware-attributes/amd_dptc/attributes
Inspect a parameter (no root needed):
$ cat $ATTR/stapm_limit/{display_name,min_value,max_value,default_value,current_value}
Sustained TDP (mW)
4000
85000
25000
<- empty: nothing staged yet
Stage values (held in memory, not yet sent to firmware):
$ echo 25000 | sudo tee $ATTR/stapm_limit/current_value
$ echo 40000 | sudo tee $ATTR/fast_limit/current_value
$ echo 27000 | sudo tee $ATTR/slow_limit/current_value
$ echo 25000 | sudo tee $ATTR/skin_limit/current_value
$ echo 85 | sudo tee $ATTR/temp_target/current_value
Commit all staged values in one ALIB call:
$ echo save | sudo tee $ATTR/save_settings
Switch to auto-commit (each write commits immediately):
$ echo single | sudo tee $ATTR/save_settings
Return to bulk mode:
$ echo bulk | sudo tee $ATTR/save_settings
Clear a staged value without committing:
$ echo | sudo tee $ATTR/stapm_limit/current_value
Query or change the active limit tier (device/expanded/soc/unbound):
$ cat $ATTR/limit_mode/possible_values
device;expanded;soc;unbound
$ echo expanded | sudo tee $ATTR/limit_mode/current_value
Switching tiers clears all staged values (old values may fall outside the
new range). Stages and commits must be redone after a mode switch.
Antheas Kapenekakis (2):
Documentation: firmware-attributes: generalize save_settings entry
platform/x86/amd: Add AMD DPTCi driver
.../testing/sysfs-class-firmware-attributes | 41 +-
MAINTAINERS | 6 +
drivers/platform/x86/amd/Kconfig | 27 +
drivers/platform/x86/amd/Makefile | 2 +
drivers/platform/x86/amd/dptc.c | 1325 +++++++++++++++++
5 files changed, 1386 insertions(+), 15 deletions(-)
create mode 100644 drivers/platform/x86/amd/dptc.c
base-commit: c89ce241c1909d2c2bdde88334c33f3000d364fb
--
2.52.0
^ permalink raw reply [flat|nested] 18+ messages in thread* [RFC v1 1/2] Documentation: firmware-attributes: generalize save_settings entry 2026-03-03 18:17 [RFC v1 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Antheas Kapenekakis @ 2026-03-03 18:17 ` Antheas Kapenekakis 2026-03-03 18:17 ` [RFC v1 2/2] platform/x86/amd: Add AMD DPTCi driver Antheas Kapenekakis 2026-03-03 18:59 ` [RFC v1 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Mario Limonciello 2 siblings, 0 replies; 18+ messages in thread From: Antheas Kapenekakis @ 2026-03-03 18:17 UTC (permalink / raw) To: Mario.Limonciello; +Cc: 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..c762bed50de8 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 -EINVAL 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] 18+ messages in thread
* [RFC v1 2/2] platform/x86/amd: Add AMD DPTCi driver 2026-03-03 18:17 [RFC v1 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Antheas Kapenekakis 2026-03-03 18:17 ` [RFC v1 1/2] Documentation: firmware-attributes: generalize save_settings entry Antheas Kapenekakis @ 2026-03-03 18:17 ` Antheas Kapenekakis 2026-03-03 20:10 ` Mario Limonciello (AMD) (kernel.org) 2026-03-03 18:59 ` [RFC v1 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Mario Limonciello 2 siblings, 1 reply; 18+ messages in thread From: Antheas Kapenekakis @ 2026-03-03 18:17 UTC (permalink / raw) To: Mario.Limonciello; +Cc: linux-kernel, platform-driver-x86, Antheas Kapenekakis Implement a driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and Thermal Configuration Interface (DPTCi). This function allows userspace to configure APU power and thermal parameters at runtime by calling the \_SB.ALIB ACPI method with a packed parameter buffer. Unlike mainstream AMD laptops, the handheld devices targeted by this driver do not implement vendor-specific WMI or EC hooks for TDP control. The ones that do, use DPTCi under the hood. For these devices, exposing the ALIB interface is the only viable mechanism for the OS to adjust power limits, making a dedicated kernel driver the correct approach rather than relying on unrestricted access to /dev/mem or ACPI method invocation from userspace. The driver exposes seven parameters (stapm_limit, fast_limit, slow_limit, skin_limit, slow_time, stapm_time, temp_target) through the firmware-attributes sysfs ABI. Values are staged in driver memory and sent to firmware atomically on a single write to the commit attribute, avoiding partial-update races. Four limit tiers gate the accepted value range: device - per-device safe range validated in the DMI table (smin..smax) expanded - full per-device OEM range (min..max) soc - APU generation envelope (e.g. 0..54 W for Ryzen 7040) unbound - no firmware-side range enforced The active tier is controlled by the limit_mode module parameter and can be switched at runtime via the limit_mode sysfs attribute. The soc and unbound tiers require CONFIG_AMD_DPTC_EXTENDED=y; without it only device and expanded are available and the default is "device". With it the default becomes "unbound". DMI entries cover 39 device variants across GPD, AYANEO, OneXPlayer, AOKZOE, OrangePi, and SuiPlay. SoC limits are detected via substring match on boot_cpu_data.x86_model_id for Ryzen 5000/6000/7040/8040, Ryzen Z1, Ryzen AI HX 370/360, and Ryzen AI MAX+ 380/385/395 series. MODULE_DEVICE_TABLE(dmi, dptc_dmi_table) is declared so udev autoloads the module on DMI-matched devices. Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev> --- MAINTAINERS | 6 + drivers/platform/x86/amd/Kconfig | 27 + drivers/platform/x86/amd/Makefile | 2 + drivers/platform/x86/amd/dptc.c | 1325 +++++++++++++++++++++++++++++ 4 files changed, 1360 insertions(+) create mode 100644 drivers/platform/x86/amd/dptc.c diff --git a/MAINTAINERS b/MAINTAINERS index e08767323763..915293594641 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1096,6 +1096,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..bd74e2bcc42c 100644 --- a/drivers/platform/x86/amd/Kconfig +++ b/drivers/platform/x86/amd/Kconfig @@ -44,3 +44,30 @@ 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. + + The driver requires a recognized AMD SoC and will only expose + device-specific limits when the system is present in the DMI table. + + If built as a module, the module will be called amd_dptc. + +config AMD_DPTC_EXTENDED + bool "AMD DPTCi extended limit modes (soc, unbound)" + depends on AMD_DPTC + help + Expose the soc and unbound limit modes, which allow setting TDP + values beyond the device-specific safe range validated in the DMI + table. When enabled, the default limit_mode is unbound. + + Only enable this if you know what you are doing. Incorrect power + limit values can cause thermal or stability issues. 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..9fb13e47b813 --- /dev/null +++ b/drivers/platform/x86/amd/dptc.c @@ -0,0 +1,1325 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver + * + * Implements AGESA ALIB Function 0x0C via the firmware-attributes sysfs ABI. + * Allows userspace to stage and atomically commit APU power/thermal parameters. + * + * Reference: AMD AGESA Publication #44065, Appendix E.5 + * + * Copyright (C) 2025 Antheas Kapenekakis <lkml@antheas.dev> + */ + +#include <linux/acpi.h> +#include <linux/dmi.h> +#include <linux/init.h> +#include <linux/kobject.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/processor.h> +#include <linux/suspend.h> +#include <linux/sysfs.h> + +#include "../firmware_attributes_class.h" + +#define DRIVER_NAME "amd_dptc" + +/* AGESA ALIB Function 0x0C - Dynamic Power and Thermal Configuration */ +#define ALIB_FUNC_DPTC 0x0C +#define ALIB_PATH "\\_SB.ALIB" + +/* ALIB parameter IDs (AGESA spec Appendix E.5, Table E-52) */ +#define ALIB_ID_STAPM_TIME 0x01 +#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_SLOW_TIME 0x08 +#define ALIB_ID_SKIN_LIMIT 0x2E + +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>"); +MODULE_DESCRIPTION("AMD DPTCi (ALIB Function 0x0C) firmware attributes driver"); +MODULE_LICENSE("GPL"); + +#ifdef CONFIG_AMD_DPTC_EXTENDED +static char *limit_mode = "unbound"; +#else +static char *limit_mode = "device"; +#endif +#ifdef CONFIG_AMD_DPTC_EXTENDED +#define DPTC_LIMIT_MODE_DESC "Maximum limit tier to expose: device, expanded, soc, unbound" +#else +#define DPTC_LIMIT_MODE_DESC "Maximum limit tier to expose: device, expanded" +#endif +module_param(limit_mode, charp, 0444); +MODULE_PARM_DESC(limit_mode, DPTC_LIMIT_MODE_DESC); + +/* ========================================================================= + * Enums + * ========================================================================= + */ + +enum dptc_param_idx { + DPTC_STAPM_LIMIT, + DPTC_FAST_LIMIT, + DPTC_SLOW_LIMIT, + DPTC_SKIN_LIMIT, + DPTC_SLOW_TIME, + DPTC_STAPM_TIME, + DPTC_TEMP_TARGET, + DPTC_NUM_PARAMS, +}; + +enum dptc_limit_mode { + LIMIT_DEVICE, /* smin..smax: safe operating range for this device */ + LIMIT_EXPANDED, /* min..max: full hardware range for this device */ + LIMIT_SOC, /* APU hardware limits from ALIB_PARAMS */ + LIMIT_UNBOUND, /* no firmware-side limits enforced */ +}; + +/* ========================================================================= + * Data structures + * ========================================================================= + */ + +/* + * Per-parameter limits for DMI-matched devices. + * TDP params (stapm/fast/slow/skin) in milliwatts (watts * 1000). + * Time params in seconds, temp in degrees Celsius. + */ +struct dptc_param_limits { + u32 min; /* expanded floor: widest safe hardware minimum */ + u32 smin; /* device floor: safe operating minimum */ + u32 def; /* default hint for userspace */ + u32 smax; /* device ceiling: safe operating maximum */ + u32 max; /* expanded ceiling: widest safe hardware maximum */ +}; + +struct dptc_device_limits { + struct dptc_param_limits p[DPTC_NUM_PARAMS]; +}; + +/* Per-parameter limits from ALIB_PARAMS - the APU's own hardware envelope */ +struct dptc_soc_range { + u32 min; + u32 max; +}; + +struct dptc_soc_limits { + struct dptc_soc_range p[DPTC_NUM_PARAMS]; +}; + +struct dptc_param_desc { + const char *name; + const char *display_name; + u8 param_id; +}; + +struct dptc_soc_entry { + const char *cpu_id; /* substring of x86_model_id */ + const struct dptc_soc_limits *limits; +}; + +/* ========================================================================= + * Parameter descriptor table + * ========================================================================= + */ + +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = { + [DPTC_STAPM_LIMIT] = { "stapm_limit", "Sustained TDP (mW)", + ALIB_ID_STAPM_LIMIT }, + [DPTC_FAST_LIMIT] = { "fast_limit", "Fast PPT limit (mW)", + ALIB_ID_FAST_LIMIT }, + [DPTC_SLOW_LIMIT] = { "slow_limit", "Slow PPT limit (mW)", + ALIB_ID_SLOW_LIMIT }, + [DPTC_SKIN_LIMIT] = { "skin_limit", "Skin temperature TDP limit (mW)", + ALIB_ID_SKIN_LIMIT }, + [DPTC_SLOW_TIME] = { "slow_time", "Slow PPT time constant (s)", + ALIB_ID_SLOW_TIME }, + [DPTC_STAPM_TIME] = { "stapm_time", "STAPM time constant (s)", + ALIB_ID_STAPM_TIME }, + [DPTC_TEMP_TARGET] = { "temp_target", "Thermal control limit (C)", + ALIB_ID_TEMP_TARGET }, +}; + +/* ========================================================================= + * Device limit classes TDP values multiplied by 1000 (milliwatts). + * ========================================================================= + */ + +/* 18W class: AYANEO AIR Plus (Ryzen 5 5560U) */ +static const struct dptc_device_limits limits_18w = { .p = { + [DPTC_STAPM_LIMIT] = { 0, 5000, 15000, 18000, 22000 }, + [DPTC_FAST_LIMIT] = { 0, 5000, 15000, 20000, 25000 }, + [DPTC_SLOW_LIMIT] = { 0, 5000, 15000, 18000, 22000 }, + [DPTC_SKIN_LIMIT] = { 0, 5000, 15000, 18000, 22000 }, + [DPTC_SLOW_TIME] = { 5, 5, 10, 10, 10 }, + [DPTC_STAPM_TIME] = { 100, 100, 100, 200, 200 }, + [DPTC_TEMP_TARGET] = { 60, 70, 85, 90, 100 }, +}}; + +/* 25W class: Ryzen 5000 handhelds (AYANEO NEXT, KUN) */ +static const struct dptc_device_limits limits_25w = { .p = { + [DPTC_STAPM_LIMIT] = { 0, 4000, 15000, 25000, 32000 }, + [DPTC_FAST_LIMIT] = { 0, 4000, 25000, 30000, 37000 }, + [DPTC_SLOW_LIMIT] = { 0, 4000, 20000, 27000, 35000 }, + [DPTC_SKIN_LIMIT] = { 0, 4000, 15000, 25000, 32000 }, + [DPTC_SLOW_TIME] = { 5, 5, 10, 10, 10 }, + [DPTC_STAPM_TIME] = { 100, 100, 100, 200, 200 }, + [DPTC_TEMP_TARGET] = { 60, 70, 85, 90, 100 }, +}}; + +/* 28W class: GPD Win series, AYANEO 2, OrangePi NEO-01 */ +static const struct dptc_device_limits limits_28w = { .p = { + [DPTC_STAPM_LIMIT] = { 0, 4000, 15000, 28000, 32000 }, + [DPTC_FAST_LIMIT] = { 0, 4000, 25000, 32000, 37000 }, + [DPTC_SLOW_LIMIT] = { 0, 4000, 20000, 30000, 35000 }, + [DPTC_SKIN_LIMIT] = { 0, 4000, 15000, 28000, 32000 }, + [DPTC_SLOW_TIME] = { 5, 5, 10, 10, 10 }, + [DPTC_STAPM_TIME] = { 100, 100, 100, 200, 200 }, + [DPTC_TEMP_TARGET] = { 60, 70, 85, 90, 100 }, +}}; + +/* 30W class: OneXPlayer, AYANEO AIR/FLIP/GEEK/SLIDE/3, AOKZOE */ +static const struct dptc_device_limits limits_30w = { .p = { + [DPTC_STAPM_LIMIT] = { 0, 4000, 15000, 30000, 40000 }, + [DPTC_FAST_LIMIT] = { 0, 4000, 25000, 41000, 50000 }, + [DPTC_SLOW_LIMIT] = { 0, 4000, 20000, 32000, 43000 }, + [DPTC_SKIN_LIMIT] = { 0, 4000, 15000, 30000, 40000 }, + [DPTC_SLOW_TIME] = { 5, 5, 10, 10, 10 }, + [DPTC_STAPM_TIME] = { 100, 100, 100, 200, 200 }, + [DPTC_TEMP_TARGET] = { 60, 70, 85, 90, 100 }, +}}; + +/* Win 5 class: GPD Win 5 */ +static const struct dptc_device_limits limits_win5 = { .p = { + [DPTC_STAPM_LIMIT] = { 0, 4000, 25000, 85000, 100000 }, + [DPTC_FAST_LIMIT] = { 0, 4000, 40000, 85000, 100000 }, + [DPTC_SLOW_LIMIT] = { 0, 4000, 27000, 85000, 100000 }, + [DPTC_SKIN_LIMIT] = { 0, 4000, 25000, 85000, 100000 }, + [DPTC_SLOW_TIME] = { 5, 5, 10, 10, 10 }, + [DPTC_STAPM_TIME] = { 100, 100, 100, 200, 200 }, + [DPTC_TEMP_TARGET] = { 60, 70, 95, 95, 100 }, +}}; + +/* ========================================================================= + * SoC limit classes + * ========================================================================= + */ + +/* Standard SoC: Ryzen 5000 through 8000, Z1, AI 9 HX 370 */ +static const struct dptc_soc_limits soc_standard = { .p = { + [DPTC_STAPM_LIMIT] = { 0, 54000 }, + [DPTC_FAST_LIMIT] = { 0, 54000 }, + [DPTC_SLOW_LIMIT] = { 0, 54000 }, + [DPTC_SKIN_LIMIT] = { 0, 54000 }, + [DPTC_SLOW_TIME] = { 0, 30 }, + [DPTC_STAPM_TIME] = { 0, 300 }, + [DPTC_TEMP_TARGET] = { 0, 105 }, +}}; + +/* AI MAX SoC: Ryzen AI MAX series */ +static const struct dptc_soc_limits soc_aimax = { .p = { + [DPTC_STAPM_LIMIT] = { 0, 120000 }, + [DPTC_FAST_LIMIT] = { 0, 120000 }, + [DPTC_SLOW_LIMIT] = { 0, 120000 }, + [DPTC_SKIN_LIMIT] = { 0, 120000 }, + [DPTC_SLOW_TIME] = { 0, 30 }, + [DPTC_STAPM_TIME] = { 0, 300 }, + [DPTC_TEMP_TARGET] = { 0, 105 }, +}}; + +/* ========================================================================= + * SoC CPU table + * Substring matches against boot_cpu_data.x86_model_id. + * Order matters: more specific strings before broader ones. + * ========================================================================= + */ + +static const struct dptc_soc_entry dptc_soc_table[] = { + /* AI MAX - must precede "AMD Ryzen AI"; 395 before 385 to avoid short match */ + { "AMD RYZEN AI MAX+ 395", &soc_aimax }, + { "AMD RYZEN AI MAX+ 385", &soc_aimax }, + { "AMD RYZEN AI MAX 380", &soc_aimax }, + /* Ryzen AI */ + { "AMD Ryzen AI 9 HX 370", &soc_standard }, + { "AMD Ryzen AI HX 360", &soc_standard }, + /* Z1 - Extreme before plain Z1 */ + { "AMD Ryzen Z1 Extreme", &soc_standard }, + { "AMD Ryzen Z1", &soc_standard }, + /* Ryzen 8000 */ + { "AMD Ryzen 7 8840U", &soc_standard }, + /* Ryzen 7040 */ + { "AMD Ryzen 7 7840U", &soc_standard }, + /* Ryzen 6000 */ + { "AMD Ryzen 7 6800U", &soc_standard }, + { "AMD Ryzen 7 6600U", &soc_standard }, + /* Ryzen 5000 */ + { "AMD Ryzen 7 5800U", &soc_standard }, + { "AMD Ryzen 7 5700U", &soc_standard }, + { "AMD Ryzen 5 5560U", &soc_standard }, + { } +}; + +/* ========================================================================= + * DMI device table + * Excluded: ASUS ROG, Lenovo Legion devices. + * ========================================================================= + */ + +static const struct dmi_system_id dptc_dmi_table[] = { + /* --- GPD (DMI_SYS_VENDOR + DMI_PRODUCT_NAME) --- */ + { + .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 = { + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"), + }, + .driver_data = (void *)&limits_win5, + }, + { + .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 (DMI_BOARD_VENDOR + DMI_BOARD_NAME) --- */ + { + .ident = "OrangePi NEO-01", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"), + }, + .driver_data = (void *)&limits_28w, + }, + /* --- AOKZOE (DMI_BOARD_VENDOR + DMI_BOARD_NAME) --- */ + { + .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 (DMI_BOARD_VENDOR "ONE-NETBOOK" + DMI_BOARD_NAME) --- + * AMD-based devices only; Intel variants share board names but we + * rely on the SoC table to reject non-AMD CPUs. + */ + { + .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 (DMI_BOARD_VENDOR + DMI_BOARD_NAME) --- */ + /* 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, + }, + /* 25W - Ryzen 5000 */ + { + .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, + }, + /* 28W - Ryzen 6000 */ + { + .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, + }, + /* 30W - Ryzen 7040 / Z1 */ + { + /* Must come before the shorter "AIR" match */ + .ident = "AYANEO AIR 1S", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_MATCH(DMI_BOARD_NAME, "AIR 1S"), + }, + .driver_data = (void *)&limits_30w, + }, + { + .ident = "AYANEO AIR Pro", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Pro"), + }, + .driver_data = (void *)&limits_30w, + }, + { + .ident = "AYANEO AIR", + .matches = { + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR"), + }, + .driver_data = (void *)&limits_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); + +/* ========================================================================= + * Per-parameter sysfs state + * ========================================================================= + */ + +struct dptc_param_sysfs { + 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]; /* 7 attrs + NULL */ + struct attribute_group group; + int idx; +}; + +struct dptc_mode_sysfs { + struct kobj_attribute current_value; + struct kobj_attribute possible_values; + struct kobj_attribute default_value; + struct kobj_attribute display_name; + struct kobj_attribute type; + struct attribute *attrs[6]; /* 5 attrs + NULL */ + struct attribute_group group; +}; + +/* ========================================================================= + * Driver private state + * ========================================================================= + */ + +struct dptc_priv { + struct device *fw_attr_dev; + struct kset *fw_attr_kset; + + const struct dptc_device_limits *dev_limits; /* NULL if no DMI match */ + const struct dptc_soc_limits *soc_limits; /* NULL if SoC unrecognized */ + + enum dptc_limit_mode active_mode; + enum dptc_limit_mode max_mode; + + enum dptc_commit_mode { COMMIT_AUTO, COMMIT_MANUAL } commit_mode; + + u32 staged[DPTC_NUM_PARAMS]; + bool has_staged[DPTC_NUM_PARAMS]; + + /* Protects staged values and has_staged flags */ + struct mutex lock; + + struct dptc_param_sysfs params[DPTC_NUM_PARAMS]; + struct dptc_mode_sysfs mode_attr; + struct kobj_attribute save_settings_attr; +}; + +static struct dptc_priv *dptc; + +/* ========================================================================= + * Limit accessors + * ========================================================================= + */ + +static u32 dptc_get_min(int idx) +{ + switch (dptc->active_mode) { + case LIMIT_DEVICE: return dptc->dev_limits->p[idx].smin; + case LIMIT_EXPANDED: return dptc->dev_limits->p[idx].min; + case LIMIT_SOC: return dptc->soc_limits->p[idx].min; + case LIMIT_UNBOUND: return 0; + } + return 0; +} + +static u32 dptc_get_max(int idx) +{ + switch (dptc->active_mode) { + case LIMIT_DEVICE: return dptc->dev_limits->p[idx].smax; + case LIMIT_EXPANDED: return dptc->dev_limits->p[idx].max; + case LIMIT_SOC: return dptc->soc_limits->p[idx].max; + case LIMIT_UNBOUND: return U32_MAX; + } + return 0; +} + +/* Default hint comes from the device DMI table; 0 if no DMI match */ +static u32 dptc_get_default(int idx) +{ + if (dptc->dev_limits) + return dptc->dev_limits->p[idx].def; + return 0; +} + +/* ========================================================================= + * ALIB call + * ========================================================================= + */ + +static int dptc_alib_commit(void) +{ + union acpi_object in_params[2]; + struct acpi_object_list input; + u32 vals[DPTC_NUM_PARAMS]; + u8 ids[DPTC_NUM_PARAMS]; + acpi_status status; + int i, off; + int count = 0; + u32 buf_size; + u8 *buf; + + for (i = 0; i < DPTC_NUM_PARAMS; i++) { + if (!dptc->has_staged[i]) + continue; + ids[count] = dptc_params[i].param_id; + vals[count] = dptc->staged[i]; + count++; + } + + if (count == 0) + return -EINVAL; + + /* 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; + + buf[0] = buf_size & 0xff; + buf[1] = (buf_size >> 8) & 0xff; + + for (i = 0; i < count; i++) { + off = 2 + i * 5; + buf[off] = ids[i]; + buf[off + 1] = vals[i] & 0xff; + buf[off + 2] = (vals[i] >> 8) & 0xff; + buf[off + 3] = (vals[i] >> 16) & 0xff; + buf[off + 4] = (vals[i] >> 24) & 0xff; + } + + 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(DRIVER_NAME ": ALIB call failed: %s\n", + acpi_format_exception(status)); + return -EIO; + } + + pr_debug(DRIVER_NAME ": committed %d parameter(s)\n", count); + return 0; +} + +/* ========================================================================= + * Sysfs callbacks - per-parameter attributes + * ========================================================================= + */ + +static ssize_t dptc_current_value_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct dptc_param_sysfs *ps = + container_of(attr, struct dptc_param_sysfs, current_value); + + if (!dptc->has_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_param_sysfs *ps = + container_of(attr, struct dptc_param_sysfs, current_value); + u32 val, min, max; + int ret; + + /* Empty write clears the staged value */ + if (count == 0 || (count == 1 && buf[0] == '\n')) { + mutex_lock(&dptc->lock); + dptc->has_staged[ps->idx] = false; + mutex_unlock(&dptc->lock); + return count; + } + + ret = kstrtou32(buf, 10, &val); + if (ret) + return ret; + + mutex_lock(&dptc->lock); + min = dptc_get_min(ps->idx); + max = dptc_get_max(ps->idx); + if (val < min || (max != U32_MAX && val > max)) { + mutex_unlock(&dptc->lock); + return -EINVAL; + } + dptc->staged[ps->idx] = val; + dptc->has_staged[ps->idx] = true; + if (dptc->commit_mode == COMMIT_AUTO) + ret = dptc_alib_commit(); + mutex_unlock(&dptc->lock); + + return ret ? ret : count; +} + +static ssize_t dptc_default_value_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct dptc_param_sysfs *ps = + container_of(attr, struct dptc_param_sysfs, default_value); + u32 def = dptc_get_default(ps->idx); + + if (!dptc->dev_limits) + return sysfs_emit(buf, "\n"); + return sysfs_emit(buf, "%u\n", def); +} + +static ssize_t dptc_min_value_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct dptc_param_sysfs *ps = + container_of(attr, struct dptc_param_sysfs, min_value); + return sysfs_emit(buf, "%u\n", dptc_get_min(ps->idx)); +} + +static ssize_t dptc_max_value_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct dptc_param_sysfs *ps = + container_of(attr, struct dptc_param_sysfs, max_value); + u32 max = dptc_get_max(ps->idx); + + if (max == U32_MAX) + return sysfs_emit(buf, "\n"); + return sysfs_emit(buf, "%u\n", max); +} + +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_param_sysfs *ps = + container_of(attr, struct dptc_param_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) +{ + if (dptc->commit_mode == COMMIT_AUTO) + 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) +{ + int ret = 0; + + if (sysfs_streq(buf, "save")) { + mutex_lock(&dptc->lock); + ret = dptc_alib_commit(); + mutex_unlock(&dptc->lock); + } else if (sysfs_streq(buf, "single")) { + mutex_lock(&dptc->lock); + dptc->commit_mode = COMMIT_AUTO; + mutex_unlock(&dptc->lock); + } else if (sysfs_streq(buf, "bulk")) { + mutex_lock(&dptc->lock); + dptc->commit_mode = COMMIT_MANUAL; + mutex_unlock(&dptc->lock); + } else { + return -EINVAL; + } + + return ret ? ret : count; +} + +static const char * const mode_names[] = { + [LIMIT_DEVICE] = "device", + [LIMIT_EXPANDED] = "expanded", + [LIMIT_SOC] = "soc", + [LIMIT_UNBOUND] = "unbound", +}; + +static bool dptc_mode_available(enum dptc_limit_mode mode) +{ + if (mode == LIMIT_DEVICE || mode == LIMIT_EXPANDED) + return dptc->dev_limits; + if (mode == LIMIT_SOC) + return dptc->soc_limits; + return true; /* LIMIT_UNBOUND always available */ +} + +static ssize_t dptc_mode_current_value_show(struct kobject *kobj, + struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%s\n", mode_names[dptc->active_mode]); +} + +static ssize_t dptc_mode_current_value_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + enum dptc_limit_mode new_mode; + int m; + + for (m = 0; m <= LIMIT_UNBOUND; m++) { + if (sysfs_streq(buf, mode_names[m])) { + new_mode = m; + goto found; + } + } + return -EINVAL; + +found: + if (new_mode > dptc->max_mode) + return -EPERM; + if (!dptc_mode_available(new_mode)) + return -ENODEV; + + mutex_lock(&dptc->lock); + dptc->active_mode = new_mode; + /* Clear staged values: limits changed, old values may be out of range */ + memset(dptc->has_staged, 0, sizeof(dptc->has_staged)); + mutex_unlock(&dptc->lock); + + return count; +} + +static ssize_t dptc_mode_possible_values_show(struct kobject *kobj, + struct kobj_attribute *attr, + char *buf) +{ + char tmp[64]; + char *p = tmp; + bool first = true; + int m; + + for (m = 0; m <= (int)dptc->max_mode; m++) { + if (!dptc_mode_available(m)) + continue; + if (!first) + *p++ = ';'; + first = false; + p += snprintf(p, tmp + sizeof(tmp) - p, "%s", mode_names[m]); + } + *p = '\0'; + + return sysfs_emit(buf, "%s\n", tmp); +} + +static ssize_t dptc_mode_default_value_show(struct kobject *kobj, + struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "device\n"); +} + +static ssize_t dptc_mode_display_name_show(struct kobject *kobj, + struct kobj_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "TDP Limit Mode\n"); +} + +static ssize_t dptc_mode_type_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "enumeration\n"); +} + +/* ========================================================================= + * Sysfs setup + * ========================================================================= + */ + +static void dptc_setup_param_sysfs(struct dptc_param_sysfs *ps, int idx) +{ + 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_mode_sysfs(struct dptc_mode_sysfs *ms) +{ + sysfs_attr_init(&ms->current_value.attr); + ms->current_value.attr.name = "current_value"; + ms->current_value.attr.mode = 0644; + ms->current_value.show = dptc_mode_current_value_show; + ms->current_value.store = dptc_mode_current_value_store; + + sysfs_attr_init(&ms->possible_values.attr); + ms->possible_values.attr.name = "possible_values"; + ms->possible_values.attr.mode = 0444; + ms->possible_values.show = dptc_mode_possible_values_show; + + sysfs_attr_init(&ms->default_value.attr); + ms->default_value.attr.name = "default_value"; + ms->default_value.attr.mode = 0444; + ms->default_value.show = dptc_mode_default_value_show; + + sysfs_attr_init(&ms->display_name.attr); + ms->display_name.attr.name = "display_name"; + ms->display_name.attr.mode = 0444; + ms->display_name.show = dptc_mode_display_name_show; + + sysfs_attr_init(&ms->type.attr); + ms->type.attr.name = "type"; + ms->type.attr.mode = 0444; + ms->type.show = dptc_mode_type_show; + + ms->attrs[0] = &ms->current_value.attr; + ms->attrs[1] = &ms->possible_values.attr; + ms->attrs[2] = &ms->default_value.attr; + ms->attrs[3] = &ms->display_name.attr; + ms->attrs[4] = &ms->type.attr; + ms->attrs[5] = NULL; + + ms->group.name = "limit_mode"; + ms->group.attrs = ms->attrs; +} + +/* ========================================================================= + * PM notifier - re-apply staged values after resume + * ========================================================================= + */ + +static int dptc_pm_notify(struct notifier_block *nb, unsigned long action, + void *data) +{ + if ((action == PM_POST_SUSPEND || action == PM_POST_HIBERNATION) && + dptc->commit_mode == COMMIT_AUTO) { + mutex_lock(&dptc->lock); + dptc_alib_commit(); + mutex_unlock(&dptc->lock); + } + return NOTIFY_OK; +} + +static struct notifier_block dptc_pm_nb = { + .notifier_call = dptc_pm_notify, +}; + +/* ========================================================================= + * Module init / exit + * ========================================================================= + */ + +static int __init dptc_init(void) +{ + const struct dptc_soc_entry *soc_entry = NULL; + const struct dmi_system_id *dmi_entry; + enum dptc_limit_mode max_mode; + int i, ret; + + /* Check ALIB ACPI method presence */ + if (!acpi_has_method(NULL, ALIB_PATH)) { + pr_debug(DRIVER_NAME ": ALIB ACPI method not present\n"); + return -ENODEV; + } + + /* Parse limit_mode module parameter */ + if (!strcmp(limit_mode, "device")) { + max_mode = LIMIT_DEVICE; + } else if (!strcmp(limit_mode, "expanded")) { + max_mode = LIMIT_EXPANDED; + } else if (!strcmp(limit_mode, "soc")) { + max_mode = LIMIT_SOC; + } else if (!strcmp(limit_mode, "unbound")) { + max_mode = LIMIT_UNBOUND; + } else { + pr_err(DRIVER_NAME ": unknown limit_mode '%s'\n", limit_mode); + return -EINVAL; + } +#ifndef CONFIG_AMD_DPTC_EXTENDED + if (max_mode > LIMIT_EXPANDED) { + pr_err(DRIVER_NAME ": limit_mode '%s' requires CONFIG_AMD_DPTC_EXTENDED\n", + limit_mode); + return -EINVAL; + } +#endif + + /* SoC match - required for device/expanded/soc, optional for unbound */ + for (i = 0; dptc_soc_table[i].cpu_id; i++) { + if (strstr(boot_cpu_data.x86_model_id, dptc_soc_table[i].cpu_id)) { + soc_entry = &dptc_soc_table[i]; + break; + } + } + if (!soc_entry && max_mode < LIMIT_UNBOUND) { + pr_debug(DRIVER_NAME ": unrecognized SoC '%s'\n", + boot_cpu_data.x86_model_id); + return -ENODEV; + } + + /* Optional device DMI match */ + dmi_entry = dmi_first_match(dptc_dmi_table); + + /* + * device and expanded modes require a DMI match; refuse to load if + * the user requested one of those tiers but the device is unknown. + */ + if (max_mode <= LIMIT_EXPANDED && !dmi_entry) { + pr_debug(DRIVER_NAME + ": limit_mode='%s' requires a device DMI match\n", + limit_mode); + return -ENODEV; + } + + dptc = kzalloc(sizeof(*dptc), GFP_KERNEL); + if (!dptc) + return -ENOMEM; + + mutex_init(&dptc->lock); + dptc->soc_limits = soc_entry ? soc_entry->limits : NULL; + dptc->max_mode = max_mode; + if (dmi_entry) + dptc->dev_limits = dmi_entry->driver_data; + + if (dptc->dev_limits) + dptc->active_mode = LIMIT_DEVICE; + else if (dptc->soc_limits) + dptc->active_mode = LIMIT_SOC; + else + dptc->active_mode = LIMIT_UNBOUND; + + /* ---- Probe logging ---- */ + if (soc_entry) + pr_info(DRIVER_NAME ": SoC: %s\n", soc_entry->cpu_id); + else + pr_info(DRIVER_NAME ": SoC unrecognized ('%s'), running unbound\n", + boot_cpu_data.x86_model_id); + + if (dmi_entry) { + pr_info(DRIVER_NAME ": Device: %s\n", dmi_entry->ident); + pr_info(DRIVER_NAME ": Device limits (device mode):\n"); + for (i = 0; i < DPTC_NUM_PARAMS; i++) { + pr_info(DRIVER_NAME ": %-16s smin=%-8u smax=%-8u def=%u\n", + dptc_params[i].name, + dptc->dev_limits->p[i].smin, + dptc->dev_limits->p[i].smax, + dptc->dev_limits->p[i].def); + } + if (max_mode >= LIMIT_EXPANDED) { + pr_info(DRIVER_NAME ": Device limits (expanded mode):\n"); + for (i = 0; i < DPTC_NUM_PARAMS; i++) { + pr_info(DRIVER_NAME ": %-16s min=%-8u max=%u\n", + dptc_params[i].name, + dptc->dev_limits->p[i].min, + dptc->dev_limits->p[i].max); + } + } + } else { + pr_info(DRIVER_NAME ": No device DMI match\n"); + } + + if (max_mode >= LIMIT_SOC && dptc->soc_limits) { + pr_info(DRIVER_NAME ": SoC limits:\n"); + for (i = 0; i < DPTC_NUM_PARAMS; i++) { + pr_info(DRIVER_NAME ": %-16s max=%u\n", + dptc_params[i].name, + dptc->soc_limits->p[i].max); + } + } + + pr_info(DRIVER_NAME ": active_mode=%s max_mode=%s\n", + mode_names[dptc->active_mode], mode_names[dptc->max_mode]); + + /* ---- Firmware-attributes class device ---- */ + dptc->fw_attr_dev = device_create(&firmware_attributes_class, + NULL, MKDEV(0, 0), NULL, + DRIVER_NAME); + if (IS_ERR(dptc->fw_attr_dev)) { + ret = PTR_ERR(dptc->fw_attr_dev); + goto err_free; + } + + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL, + &dptc->fw_attr_dev->kobj); + if (!dptc->fw_attr_kset) { + ret = -ENOMEM; + goto err_dev; + } + + for (i = 0; i < DPTC_NUM_PARAMS; i++) { + dptc_setup_param_sysfs(&dptc->params[i], i); + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj, + &dptc->params[i].group); + if (ret) { + while (--i >= 0) + sysfs_remove_group(&dptc->fw_attr_kset->kobj, + &dptc->params[i].group); + goto err_kset; + } + } + + dptc_setup_mode_sysfs(&dptc->mode_attr); + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj, + &dptc->mode_attr.group); + if (ret) { + for (i = 0; i < DPTC_NUM_PARAMS; i++) + sysfs_remove_group(&dptc->fw_attr_kset->kobj, + &dptc->params[i].group); + goto err_kset; + } + + 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) { + sysfs_remove_group(&dptc->fw_attr_kset->kobj, &dptc->mode_attr.group); + for (i = 0; i < DPTC_NUM_PARAMS; i++) + sysfs_remove_group(&dptc->fw_attr_kset->kobj, + &dptc->params[i].group); + goto err_kset; + } + + register_pm_notifier(&dptc_pm_nb); + pr_info(DRIVER_NAME ": loaded\n"); + return 0; + +err_kset: + kset_unregister(dptc->fw_attr_kset); +err_dev: + device_destroy(&firmware_attributes_class, MKDEV(0, 0)); +err_free: + mutex_destroy(&dptc->lock); + kfree(dptc); + dptc = NULL; + return ret; +} + +static void __exit dptc_exit(void) +{ + int i; + + unregister_pm_notifier(&dptc_pm_nb); + sysfs_remove_file(&dptc->fw_attr_kset->kobj, &dptc->save_settings_attr.attr); + sysfs_remove_group(&dptc->fw_attr_kset->kobj, &dptc->mode_attr.group); + for (i = 0; i < DPTC_NUM_PARAMS; i++) + sysfs_remove_group(&dptc->fw_attr_kset->kobj, + &dptc->params[i].group); + kset_unregister(dptc->fw_attr_kset); + device_destroy(&firmware_attributes_class, MKDEV(0, 0)); + mutex_destroy(&dptc->lock); + kfree(dptc); + dptc = NULL; +} + +module_init(dptc_init); +module_exit(dptc_exit); -- 2.52.0 ^ permalink raw reply related [flat|nested] 18+ messages in thread
* Re: [RFC v1 2/2] platform/x86/amd: Add AMD DPTCi driver 2026-03-03 18:17 ` [RFC v1 2/2] platform/x86/amd: Add AMD DPTCi driver Antheas Kapenekakis @ 2026-03-03 20:10 ` Mario Limonciello (AMD) (kernel.org) 2026-03-03 20:40 ` Antheas Kapenekakis 0 siblings, 1 reply; 18+ messages in thread From: Mario Limonciello (AMD) (kernel.org) @ 2026-03-03 20:10 UTC (permalink / raw) To: Antheas Kapenekakis; +Cc: linux-kernel, platform-driver-x86 I'll preface this by saying - I don't have a problem with using an AI to help write a driver, but please disclose that it was done and that in this case even you haven't closely audited the results. I personally would never submit something generated by an LLM that I didn't audit and add a S-o-b tag to it (asserting I am willing to stand by the code). I'm glad that I found out it was AI written before I started to review the code, I would have had a lot more candid comments for you. There is a lot of weird stuff in this driver that I'm not going to comment on and nitpick, but I'll leave a few broad strokes things. On 3/3/2026 12:17 PM, Antheas Kapenekakis wrote: > Implement a driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power > and Thermal Configuration Interface (DPTCi). This function allows > userspace to configure APU power and thermal parameters at runtime by > calling the \_SB.ALIB ACPI method with a packed parameter buffer. > > Unlike mainstream AMD laptops, the handheld devices targeted by this > driver do not implement vendor-specific WMI or EC hooks for TDP control. > The ones that do, use DPTCi under the hood. For these devices, exposing > the ALIB interface is the only viable mechanism for the OS to adjust > power limits, making a dedicated kernel driver the correct approach > rather than relying on unrestricted access to /dev/mem or ACPI method > invocation from userspace. > > The driver exposes seven parameters (stapm_limit, fast_limit, > slow_limit, skin_limit, slow_time, stapm_time, temp_target) through the > firmware-attributes sysfs ABI. Values are staged in driver memory and > sent to firmware atomically on a single write to the commit attribute, > avoiding partial-update races. Let me ask - why do all these files need to be exposed in the first place to firmware attributes API? The vast majority of people don't need to to change all these settings at once, and that's why there is a power slider concept. Shouldn't this driver register a platform profile and all the settings tie to a platform profile? If you really want tuning, that's what custom platform profile is for. You can look at how the other drivers that implement it do this. > > Four limit tiers gate the accepted value range: > > device - per-device safe range validated in the DMI table (smin..smax) > expanded - full per-device OEM range (min..max) > soc - APU generation envelope (e.g. 0..54 W for Ryzen 7040) > unbound - no firmware-side range enforced > > The active tier is controlled by the limit_mode module parameter and can > be switched at runtime via the limit_mode sysfs attribute. The soc and > unbound tiers require CONFIG_AMD_DPTC_EXTENDED=y; without it only device > and expanded are available and the default is "device". With it the > default becomes "unbound". > > DMI entries cover 39 device variants across GPD, AYANEO, OneXPlayer, > AOKZOE, OrangePi, and SuiPlay. SoC limits are detected via substring > match on boot_cpu_data.x86_model_id for Ryzen 5000/6000/7040/8040, > Ryzen Z1, Ryzen AI HX 370/360, and Ryzen AI MAX+ 380/385/395 series. > > MODULE_DEVICE_TABLE(dmi, dptc_dmi_table) is declared so udev autoloads > the module on DMI-matched devices. Duh? > > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev> > --- > MAINTAINERS | 6 + > drivers/platform/x86/amd/Kconfig | 27 + > drivers/platform/x86/amd/Makefile | 2 + > drivers/platform/x86/amd/dptc.c | 1325 +++++++++++++++++++++++++++++ > 4 files changed, 1360 insertions(+) > create mode 100644 drivers/platform/x86/amd/dptc.c > > diff --git a/MAINTAINERS b/MAINTAINERS > index e08767323763..915293594641 100644 > --- a/MAINTAINERS > +++ b/MAINTAINERS > @@ -1096,6 +1096,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..bd74e2bcc42c 100644 > --- a/drivers/platform/x86/amd/Kconfig > +++ b/drivers/platform/x86/amd/Kconfig > @@ -44,3 +44,30 @@ 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. > + > + The driver requires a recognized AMD SoC and will only expose > + device-specific limits when the system is present in the DMI table. > + > + If built as a module, the module will be called amd_dptc. > + > +config AMD_DPTC_EXTENDED > + bool "AMD DPTCi extended limit modes (soc, unbound)" > + depends on AMD_DPTC > + help > + Expose the soc and unbound limit modes, which allow setting TDP > + values beyond the device-specific safe range validated in the DMI > + table. When enabled, the default limit_mode is unbound. Something dangerous should taint the machine and in my view hidden in debugfs. > + > + Only enable this if you know what you are doing. Incorrect power > + limit values can cause thermal or stability issues. > 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..9fb13e47b813 > --- /dev/null > +++ b/drivers/platform/x86/amd/dptc.c > @@ -0,0 +1,1325 @@ > +// SPDX-License-Identifier: GPL-2.0-only > +/* > + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver > + * > + * Implements AGESA ALIB Function 0x0C via the firmware-attributes sysfs ABI. > + * Allows userspace to stage and atomically commit APU power/thermal parameters. > + * > + * Reference: AMD AGESA Publication #44065, Appendix E.5 I feel that you should include a link to the document. https://docs.amd.com/v/u/en-US/44065_Arch2008 > + * > + * Copyright (C) 2025 Antheas Kapenekakis <lkml@antheas.dev> > + */ > + > +#include <linux/acpi.h> > +#include <linux/dmi.h> > +#include <linux/init.h> > +#include <linux/kobject.h> > +#include <linux/module.h> > +#include <linux/mutex.h> > +#include <linux/processor.h> > +#include <linux/suspend.h> > +#include <linux/sysfs.h> > + > +#include "../firmware_attributes_class.h" > + > +#define DRIVER_NAME "amd_dptc" > + > +/* AGESA ALIB Function 0x0C - Dynamic Power and Thermal Configuration */ > +#define ALIB_FUNC_DPTC 0x0C > +#define ALIB_PATH "\\_SB.ALIB" > + > +/* ALIB parameter IDs (AGESA spec Appendix E.5, Table E-52) */ > +#define ALIB_ID_STAPM_TIME 0x01 > +#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_SLOW_TIME 0x08 > +#define ALIB_ID_SKIN_LIMIT 0x2E > + > +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>"); > +MODULE_DESCRIPTION("AMD DPTCi (ALIB Function 0x0C) firmware attributes driver"); > +MODULE_LICENSE("GPL"); > + > +#ifdef CONFIG_AMD_DPTC_EXTENDED > +static char *limit_mode = "unbound"; > +#else > +static char *limit_mode = "device"; > +#endif > +#ifdef CONFIG_AMD_DPTC_EXTENDED > +#define DPTC_LIMIT_MODE_DESC "Maximum limit tier to expose: device, expanded, soc, unbound" > +#else > +#define DPTC_LIMIT_MODE_DESC "Maximum limit tier to expose: device, expanded" > +#endif > +module_param(limit_mode, charp, 0444); > +MODULE_PARM_DESC(limit_mode, DPTC_LIMIT_MODE_DESC); > + > +/* ========================================================================= > + * Enums > + * ========================================================================= > + */ > + > +enum dptc_param_idx { > + DPTC_STAPM_LIMIT, > + DPTC_FAST_LIMIT, > + DPTC_SLOW_LIMIT, > + DPTC_SKIN_LIMIT, > + DPTC_SLOW_TIME, > + DPTC_STAPM_TIME, > + DPTC_TEMP_TARGET, > + DPTC_NUM_PARAMS, > +}; > + > +enum dptc_limit_mode { > + LIMIT_DEVICE, /* smin..smax: safe operating range for this device */ > + LIMIT_EXPANDED, /* min..max: full hardware range for this device */ > + LIMIT_SOC, /* APU hardware limits from ALIB_PARAMS */ > + LIMIT_UNBOUND, /* no firmware-side limits enforced */ > +}; > + > +/* ========================================================================= > + * Data structures > + * ========================================================================= > + */ > + > +/* > + * Per-parameter limits for DMI-matched devices. > + * TDP params (stapm/fast/slow/skin) in milliwatts (watts * 1000). > + * Time params in seconds, temp in degrees Celsius. > + */ > +struct dptc_param_limits { > + u32 min; /* expanded floor: widest safe hardware minimum */ > + u32 smin; /* device floor: safe operating minimum */ > + u32 def; /* default hint for userspace */ > + u32 smax; /* device ceiling: safe operating maximum */ > + u32 max; /* expanded ceiling: widest safe hardware maximum */ > +}; > + > +struct dptc_device_limits { > + struct dptc_param_limits p[DPTC_NUM_PARAMS]; > +}; > + > +/* Per-parameter limits from ALIB_PARAMS - the APU's own hardware envelope */ > +struct dptc_soc_range { > + u32 min; > + u32 max; > +}; > + > +struct dptc_soc_limits { > + struct dptc_soc_range p[DPTC_NUM_PARAMS]; > +}; > + > +struct dptc_param_desc { > + const char *name; > + const char *display_name; > + u8 param_id; > +}; > + > +struct dptc_soc_entry { > + const char *cpu_id; /* substring of x86_model_id */ > + const struct dptc_soc_limits *limits; > +}; > + > +/* ========================================================================= > + * Parameter descriptor table > + * ========================================================================= > + */ > + > +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = { > + [DPTC_STAPM_LIMIT] = { "stapm_limit", "Sustained TDP (mW)", > + ALIB_ID_STAPM_LIMIT }, > + [DPTC_FAST_LIMIT] = { "fast_limit", "Fast PPT limit (mW)", > + ALIB_ID_FAST_LIMIT }, > + [DPTC_SLOW_LIMIT] = { "slow_limit", "Slow PPT limit (mW)", > + ALIB_ID_SLOW_LIMIT }, > + [DPTC_SKIN_LIMIT] = { "skin_limit", "Skin temperature TDP limit (mW)", > + ALIB_ID_SKIN_LIMIT }, > + [DPTC_SLOW_TIME] = { "slow_time", "Slow PPT time constant (s)", > + ALIB_ID_SLOW_TIME }, > + [DPTC_STAPM_TIME] = { "stapm_time", "STAPM time constant (s)", > + ALIB_ID_STAPM_TIME }, > + [DPTC_TEMP_TARGET] = { "temp_target", "Thermal control limit (C)", > + ALIB_ID_TEMP_TARGET }, > +}; > + > +/* ========================================================================= > + * Device limit classes TDP values multiplied by 1000 (milliwatts). > + * ========================================================================= > + */ > + > +/* 18W class: AYANEO AIR Plus (Ryzen 5 5560U) */ > +static const struct dptc_device_limits limits_18w = { .p = { > + [DPTC_STAPM_LIMIT] = { 0, 5000, 15000, 18000, 22000 }, > + [DPTC_FAST_LIMIT] = { 0, 5000, 15000, 20000, 25000 }, > + [DPTC_SLOW_LIMIT] = { 0, 5000, 15000, 18000, 22000 }, > + [DPTC_SKIN_LIMIT] = { 0, 5000, 15000, 18000, 22000 }, > + [DPTC_SLOW_TIME] = { 5, 5, 10, 10, 10 }, > + [DPTC_STAPM_TIME] = { 100, 100, 100, 200, 200 }, > + [DPTC_TEMP_TARGET] = { 60, 70, 85, 90, 100 }, > +}}; > + > +/* 25W class: Ryzen 5000 handhelds (AYANEO NEXT, KUN) */ > +static const struct dptc_device_limits limits_25w = { .p = { > + [DPTC_STAPM_LIMIT] = { 0, 4000, 15000, 25000, 32000 }, > + [DPTC_FAST_LIMIT] = { 0, 4000, 25000, 30000, 37000 }, > + [DPTC_SLOW_LIMIT] = { 0, 4000, 20000, 27000, 35000 }, > + [DPTC_SKIN_LIMIT] = { 0, 4000, 15000, 25000, 32000 }, > + [DPTC_SLOW_TIME] = { 5, 5, 10, 10, 10 }, > + [DPTC_STAPM_TIME] = { 100, 100, 100, 200, 200 }, > + [DPTC_TEMP_TARGET] = { 60, 70, 85, 90, 100 }, > +}}; > + > +/* 28W class: GPD Win series, AYANEO 2, OrangePi NEO-01 */ > +static const struct dptc_device_limits limits_28w = { .p = { > + [DPTC_STAPM_LIMIT] = { 0, 4000, 15000, 28000, 32000 }, > + [DPTC_FAST_LIMIT] = { 0, 4000, 25000, 32000, 37000 }, > + [DPTC_SLOW_LIMIT] = { 0, 4000, 20000, 30000, 35000 }, > + [DPTC_SKIN_LIMIT] = { 0, 4000, 15000, 28000, 32000 }, > + [DPTC_SLOW_TIME] = { 5, 5, 10, 10, 10 }, > + [DPTC_STAPM_TIME] = { 100, 100, 100, 200, 200 }, > + [DPTC_TEMP_TARGET] = { 60, 70, 85, 90, 100 }, > +}}; > + > +/* 30W class: OneXPlayer, AYANEO AIR/FLIP/GEEK/SLIDE/3, AOKZOE */ > +static const struct dptc_device_limits limits_30w = { .p = { > + [DPTC_STAPM_LIMIT] = { 0, 4000, 15000, 30000, 40000 }, > + [DPTC_FAST_LIMIT] = { 0, 4000, 25000, 41000, 50000 }, > + [DPTC_SLOW_LIMIT] = { 0, 4000, 20000, 32000, 43000 }, > + [DPTC_SKIN_LIMIT] = { 0, 4000, 15000, 30000, 40000 }, > + [DPTC_SLOW_TIME] = { 5, 5, 10, 10, 10 }, > + [DPTC_STAPM_TIME] = { 100, 100, 100, 200, 200 }, > + [DPTC_TEMP_TARGET] = { 60, 70, 85, 90, 100 }, > +}}; > + > +/* Win 5 class: GPD Win 5 */ > +static const struct dptc_device_limits limits_win5 = { .p = { > + [DPTC_STAPM_LIMIT] = { 0, 4000, 25000, 85000, 100000 }, > + [DPTC_FAST_LIMIT] = { 0, 4000, 40000, 85000, 100000 }, > + [DPTC_SLOW_LIMIT] = { 0, 4000, 27000, 85000, 100000 }, > + [DPTC_SKIN_LIMIT] = { 0, 4000, 25000, 85000, 100000 }, > + [DPTC_SLOW_TIME] = { 5, 5, 10, 10, 10 }, > + [DPTC_STAPM_TIME] = { 100, 100, 100, 200, 200 }, > + [DPTC_TEMP_TARGET] = { 60, 70, 95, 95, 100 }, > +}}; > + > +/* ========================================================================= > + * SoC limit classes > + * ========================================================================= > + */ > + > +/* Standard SoC: Ryzen 5000 through 8000, Z1, AI 9 HX 370 */ > +static const struct dptc_soc_limits soc_standard = { .p = { > + [DPTC_STAPM_LIMIT] = { 0, 54000 }, > + [DPTC_FAST_LIMIT] = { 0, 54000 }, > + [DPTC_SLOW_LIMIT] = { 0, 54000 }, > + [DPTC_SKIN_LIMIT] = { 0, 54000 }, > + [DPTC_SLOW_TIME] = { 0, 30 }, > + [DPTC_STAPM_TIME] = { 0, 300 }, > + [DPTC_TEMP_TARGET] = { 0, 105 }, > +}}; > + > +/* AI MAX SoC: Ryzen AI MAX series */ > +static const struct dptc_soc_limits soc_aimax = { .p = { > + [DPTC_STAPM_LIMIT] = { 0, 120000 }, > + [DPTC_FAST_LIMIT] = { 0, 120000 }, > + [DPTC_SLOW_LIMIT] = { 0, 120000 }, > + [DPTC_SKIN_LIMIT] = { 0, 120000 }, > + [DPTC_SLOW_TIME] = { 0, 30 }, > + [DPTC_STAPM_TIME] = { 0, 300 }, > + [DPTC_TEMP_TARGET] = { 0, 105 }, > +}}; > + > +/* ========================================================================= > + * SoC CPU table > + * Substring matches against boot_cpu_data.x86_model_id. > + * Order matters: more specific strings before broader ones. > + * ========================================================================= > + */ > + > +static const struct dptc_soc_entry dptc_soc_table[] = { > + /* AI MAX - must precede "AMD Ryzen AI"; 395 before 385 to avoid short match */ > + { "AMD RYZEN AI MAX+ 395", &soc_aimax }, > + { "AMD RYZEN AI MAX+ 385", &soc_aimax }, > + { "AMD RYZEN AI MAX 380", &soc_aimax }, > + /* Ryzen AI */ > + { "AMD Ryzen AI 9 HX 370", &soc_standard }, > + { "AMD Ryzen AI HX 360", &soc_standard }, > + /* Z1 - Extreme before plain Z1 */ > + { "AMD Ryzen Z1 Extreme", &soc_standard }, > + { "AMD Ryzen Z1", &soc_standard }, > + /* Ryzen 8000 */ > + { "AMD Ryzen 7 8840U", &soc_standard }, > + /* Ryzen 7040 */ > + { "AMD Ryzen 7 7840U", &soc_standard }, > + /* Ryzen 6000 */ > + { "AMD Ryzen 7 6800U", &soc_standard }, > + { "AMD Ryzen 7 6600U", &soc_standard }, > + /* Ryzen 5000 */ > + { "AMD Ryzen 7 5800U", &soc_standard }, > + { "AMD Ryzen 7 5700U", &soc_standard }, > + { "AMD Ryzen 5 5560U", &soc_standard }, > + { } > +}; > + > +/* ========================================================================= > + * DMI device table > + * Excluded: ASUS ROG, Lenovo Legion devices. > + * ========================================================================= > + */ > + > +static const struct dmi_system_id dptc_dmi_table[] = { > + /* --- GPD (DMI_SYS_VENDOR + DMI_PRODUCT_NAME) --- */ > + { > + .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 = { > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"), > + }, > + .driver_data = (void *)&limits_win5, > + }, > + { > + .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 (DMI_BOARD_VENDOR + DMI_BOARD_NAME) --- */ > + { > + .ident = "OrangePi NEO-01", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"), > + }, > + .driver_data = (void *)&limits_28w, > + }, > + /* --- AOKZOE (DMI_BOARD_VENDOR + DMI_BOARD_NAME) --- */ > + { > + .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 (DMI_BOARD_VENDOR "ONE-NETBOOK" + DMI_BOARD_NAME) --- > + * AMD-based devices only; Intel variants share board names but we > + * rely on the SoC table to reject non-AMD CPUs. > + */ > + { > + .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 (DMI_BOARD_VENDOR + DMI_BOARD_NAME) --- */ > + /* 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, > + }, > + /* 25W - Ryzen 5000 */ > + { > + .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, > + }, > + /* 28W - Ryzen 6000 */ > + { > + .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, > + }, > + /* 30W - Ryzen 7040 / Z1 */ > + { > + /* Must come before the shorter "AIR" match */ > + .ident = "AYANEO AIR 1S", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > + DMI_MATCH(DMI_BOARD_NAME, "AIR 1S"), > + }, > + .driver_data = (void *)&limits_30w, > + }, > + { > + .ident = "AYANEO AIR Pro", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Pro"), > + }, > + .driver_data = (void *)&limits_30w, > + }, > + { > + .ident = "AYANEO AIR", > + .matches = { > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR"), > + }, > + .driver_data = (void *)&limits_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); > + > +/* ========================================================================= > + * Per-parameter sysfs state > + * ========================================================================= > + */ > + > +struct dptc_param_sysfs { > + 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]; /* 7 attrs + NULL */ > + struct attribute_group group; > + int idx; > +}; > + > +struct dptc_mode_sysfs { > + struct kobj_attribute current_value; > + struct kobj_attribute possible_values; > + struct kobj_attribute default_value; > + struct kobj_attribute display_name; > + struct kobj_attribute type; > + struct attribute *attrs[6]; /* 5 attrs + NULL */ > + struct attribute_group group; > +}; > + > +/* ========================================================================= > + * Driver private state > + * ========================================================================= > + */ > + > +struct dptc_priv { > + struct device *fw_attr_dev; > + struct kset *fw_attr_kset; > + > + const struct dptc_device_limits *dev_limits; /* NULL if no DMI match */ > + const struct dptc_soc_limits *soc_limits; /* NULL if SoC unrecognized */ > + > + enum dptc_limit_mode active_mode; > + enum dptc_limit_mode max_mode; > + > + enum dptc_commit_mode { COMMIT_AUTO, COMMIT_MANUAL } commit_mode; > + > + u32 staged[DPTC_NUM_PARAMS]; > + bool has_staged[DPTC_NUM_PARAMS]; > + > + /* Protects staged values and has_staged flags */ > + struct mutex lock; > + > + struct dptc_param_sysfs params[DPTC_NUM_PARAMS]; > + struct dptc_mode_sysfs mode_attr; > + struct kobj_attribute save_settings_attr; > +}; > + > +static struct dptc_priv *dptc; > + > +/* ========================================================================= > + * Limit accessors > + * ========================================================================= > + */ > + > +static u32 dptc_get_min(int idx) > +{ > + switch (dptc->active_mode) { > + case LIMIT_DEVICE: return dptc->dev_limits->p[idx].smin; > + case LIMIT_EXPANDED: return dptc->dev_limits->p[idx].min; > + case LIMIT_SOC: return dptc->soc_limits->p[idx].min; > + case LIMIT_UNBOUND: return 0; > + } > + return 0; > +} > + > +static u32 dptc_get_max(int idx) > +{ > + switch (dptc->active_mode) { > + case LIMIT_DEVICE: return dptc->dev_limits->p[idx].smax; > + case LIMIT_EXPANDED: return dptc->dev_limits->p[idx].max; > + case LIMIT_SOC: return dptc->soc_limits->p[idx].max; > + case LIMIT_UNBOUND: return U32_MAX; > + } > + return 0; > +} > + > +/* Default hint comes from the device DMI table; 0 if no DMI match */ > +static u32 dptc_get_default(int idx) > +{ > + if (dptc->dev_limits) > + return dptc->dev_limits->p[idx].def; > + return 0; > +} > + > +/* ========================================================================= > + * ALIB call > + * ========================================================================= > + */ > + > +static int dptc_alib_commit(void) > +{ > + union acpi_object in_params[2]; > + struct acpi_object_list input; > + u32 vals[DPTC_NUM_PARAMS]; > + u8 ids[DPTC_NUM_PARAMS]; > + acpi_status status; > + int i, off; > + int count = 0; > + u32 buf_size; > + u8 *buf; > + > + for (i = 0; i < DPTC_NUM_PARAMS; i++) { > + if (!dptc->has_staged[i]) > + continue; > + ids[count] = dptc_params[i].param_id; > + vals[count] = dptc->staged[i]; > + count++; > + } > + > + if (count == 0) > + return -EINVAL; > + > + /* 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; > + > + buf[0] = buf_size & 0xff; > + buf[1] = (buf_size >> 8) & 0xff; > + > + for (i = 0; i < count; i++) { > + off = 2 + i * 5; > + buf[off] = ids[i]; > + buf[off + 1] = vals[i] & 0xff; > + buf[off + 2] = (vals[i] >> 8) & 0xff; > + buf[off + 3] = (vals[i] >> 16) & 0xff; > + buf[off + 4] = (vals[i] >> 24) & 0xff; > + } > + > + 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(DRIVER_NAME ": ALIB call failed: %s\n", > + acpi_format_exception(status)); > + return -EIO; > + } > + > + pr_debug(DRIVER_NAME ": committed %d parameter(s)\n", count); > + return 0; > +} > + > +/* ========================================================================= > + * Sysfs callbacks - per-parameter attributes > + * ========================================================================= > + */ > + > +static ssize_t dptc_current_value_show(struct kobject *kobj, > + struct kobj_attribute *attr, char *buf) > +{ > + struct dptc_param_sysfs *ps = > + container_of(attr, struct dptc_param_sysfs, current_value); > + > + if (!dptc->has_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_param_sysfs *ps = > + container_of(attr, struct dptc_param_sysfs, current_value); > + u32 val, min, max; > + int ret; > + > + /* Empty write clears the staged value */ > + if (count == 0 || (count == 1 && buf[0] == '\n')) { > + mutex_lock(&dptc->lock); > + dptc->has_staged[ps->idx] = false; > + mutex_unlock(&dptc->lock); > + return count; > + } > + > + ret = kstrtou32(buf, 10, &val); > + if (ret) > + return ret; > + > + mutex_lock(&dptc->lock); > + min = dptc_get_min(ps->idx); > + max = dptc_get_max(ps->idx); > + if (val < min || (max != U32_MAX && val > max)) { > + mutex_unlock(&dptc->lock); > + return -EINVAL; > + } > + dptc->staged[ps->idx] = val; > + dptc->has_staged[ps->idx] = true; > + if (dptc->commit_mode == COMMIT_AUTO) > + ret = dptc_alib_commit(); > + mutex_unlock(&dptc->lock); > + > + return ret ? ret : count; > +} > + > +static ssize_t dptc_default_value_show(struct kobject *kobj, > + struct kobj_attribute *attr, char *buf) > +{ > + struct dptc_param_sysfs *ps = > + container_of(attr, struct dptc_param_sysfs, default_value); > + u32 def = dptc_get_default(ps->idx); > + > + if (!dptc->dev_limits) > + return sysfs_emit(buf, "\n"); > + return sysfs_emit(buf, "%u\n", def); > +} > + > +static ssize_t dptc_min_value_show(struct kobject *kobj, > + struct kobj_attribute *attr, char *buf) > +{ > + struct dptc_param_sysfs *ps = > + container_of(attr, struct dptc_param_sysfs, min_value); > + return sysfs_emit(buf, "%u\n", dptc_get_min(ps->idx)); > +} > + > +static ssize_t dptc_max_value_show(struct kobject *kobj, > + struct kobj_attribute *attr, char *buf) > +{ > + struct dptc_param_sysfs *ps = > + container_of(attr, struct dptc_param_sysfs, max_value); > + u32 max = dptc_get_max(ps->idx); > + > + if (max == U32_MAX) > + return sysfs_emit(buf, "\n"); > + return sysfs_emit(buf, "%u\n", max); > +} > + > +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_param_sysfs *ps = > + container_of(attr, struct dptc_param_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) > +{ > + if (dptc->commit_mode == COMMIT_AUTO) > + 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) > +{ > + int ret = 0; > + > + if (sysfs_streq(buf, "save")) { > + mutex_lock(&dptc->lock); > + ret = dptc_alib_commit(); > + mutex_unlock(&dptc->lock); > + } else if (sysfs_streq(buf, "single")) { > + mutex_lock(&dptc->lock); > + dptc->commit_mode = COMMIT_AUTO; > + mutex_unlock(&dptc->lock); > + } else if (sysfs_streq(buf, "bulk")) { > + mutex_lock(&dptc->lock); > + dptc->commit_mode = COMMIT_MANUAL; > + mutex_unlock(&dptc->lock); > + } else { > + return -EINVAL; > + } > + > + return ret ? ret : count; > +} > + > +static const char * const mode_names[] = { > + [LIMIT_DEVICE] = "device", > + [LIMIT_EXPANDED] = "expanded", > + [LIMIT_SOC] = "soc", > + [LIMIT_UNBOUND] = "unbound", > +}; > + > +static bool dptc_mode_available(enum dptc_limit_mode mode) > +{ > + if (mode == LIMIT_DEVICE || mode == LIMIT_EXPANDED) > + return dptc->dev_limits; > + if (mode == LIMIT_SOC) > + return dptc->soc_limits; > + return true; /* LIMIT_UNBOUND always available */ > +} > + > +static ssize_t dptc_mode_current_value_show(struct kobject *kobj, > + struct kobj_attribute *attr, > + char *buf) > +{ > + return sysfs_emit(buf, "%s\n", mode_names[dptc->active_mode]); > +} > + > +static ssize_t dptc_mode_current_value_store(struct kobject *kobj, > + struct kobj_attribute *attr, > + const char *buf, size_t count) > +{ > + enum dptc_limit_mode new_mode; > + int m; > + > + for (m = 0; m <= LIMIT_UNBOUND; m++) { > + if (sysfs_streq(buf, mode_names[m])) { > + new_mode = m; > + goto found; > + } > + } > + return -EINVAL; > + > +found: > + if (new_mode > dptc->max_mode) > + return -EPERM; > + if (!dptc_mode_available(new_mode)) > + return -ENODEV; > + > + mutex_lock(&dptc->lock); > + dptc->active_mode = new_mode; > + /* Clear staged values: limits changed, old values may be out of range */ > + memset(dptc->has_staged, 0, sizeof(dptc->has_staged)); > + mutex_unlock(&dptc->lock); > + > + return count; > +} > + > +static ssize_t dptc_mode_possible_values_show(struct kobject *kobj, > + struct kobj_attribute *attr, > + char *buf) > +{ > + char tmp[64]; > + char *p = tmp; > + bool first = true; > + int m; > + > + for (m = 0; m <= (int)dptc->max_mode; m++) { > + if (!dptc_mode_available(m)) > + continue; > + if (!first) > + *p++ = ';'; > + first = false; > + p += snprintf(p, tmp + sizeof(tmp) - p, "%s", mode_names[m]); > + } > + *p = '\0'; > + > + return sysfs_emit(buf, "%s\n", tmp); > +} > + > +static ssize_t dptc_mode_default_value_show(struct kobject *kobj, > + struct kobj_attribute *attr, > + char *buf) > +{ > + return sysfs_emit(buf, "device\n"); > +} > + > +static ssize_t dptc_mode_display_name_show(struct kobject *kobj, > + struct kobj_attribute *attr, > + char *buf) > +{ > + return sysfs_emit(buf, "TDP Limit Mode\n"); > +} > + > +static ssize_t dptc_mode_type_show(struct kobject *kobj, > + struct kobj_attribute *attr, char *buf) > +{ > + return sysfs_emit(buf, "enumeration\n"); > +} > + > +/* ========================================================================= > + * Sysfs setup > + * ========================================================================= > + */ > + > +static void dptc_setup_param_sysfs(struct dptc_param_sysfs *ps, int idx) > +{ > + 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_mode_sysfs(struct dptc_mode_sysfs *ms) > +{ > + sysfs_attr_init(&ms->current_value.attr); > + ms->current_value.attr.name = "current_value"; > + ms->current_value.attr.mode = 0644; > + ms->current_value.show = dptc_mode_current_value_show; > + ms->current_value.store = dptc_mode_current_value_store; > + > + sysfs_attr_init(&ms->possible_values.attr); > + ms->possible_values.attr.name = "possible_values"; > + ms->possible_values.attr.mode = 0444; > + ms->possible_values.show = dptc_mode_possible_values_show; > + > + sysfs_attr_init(&ms->default_value.attr); > + ms->default_value.attr.name = "default_value"; > + ms->default_value.attr.mode = 0444; > + ms->default_value.show = dptc_mode_default_value_show; > + > + sysfs_attr_init(&ms->display_name.attr); > + ms->display_name.attr.name = "display_name"; > + ms->display_name.attr.mode = 0444; > + ms->display_name.show = dptc_mode_display_name_show; > + > + sysfs_attr_init(&ms->type.attr); > + ms->type.attr.name = "type"; > + ms->type.attr.mode = 0444; > + ms->type.show = dptc_mode_type_show; > + > + ms->attrs[0] = &ms->current_value.attr; > + ms->attrs[1] = &ms->possible_values.attr; > + ms->attrs[2] = &ms->default_value.attr; > + ms->attrs[3] = &ms->display_name.attr; > + ms->attrs[4] = &ms->type.attr; > + ms->attrs[5] = NULL; > + > + ms->group.name = "limit_mode"; > + ms->group.attrs = ms->attrs; > +} > + > +/* ========================================================================= > + * PM notifier - re-apply staged values after resume > + * ========================================================================= > + */ > + > +static int dptc_pm_notify(struct notifier_block *nb, unsigned long action, > + void *data) > +{ > + if ((action == PM_POST_SUSPEND || action == PM_POST_HIBERNATION) && > + dptc->commit_mode == COMMIT_AUTO) { > + mutex_lock(&dptc->lock); > + dptc_alib_commit(); > + mutex_unlock(&dptc->lock); > + } > + return NOTIFY_OK; > +} > + > +static struct notifier_block dptc_pm_nb = { > + .notifier_call = dptc_pm_notify, > +}; Why is this using notification blocks? Usually those are for very specific purposes that things need to run at a certain time. It can't run as part of regular device suspend/resume routines? > + > +/* ========================================================================= > + * Module init / exit > + * ========================================================================= > + */ > + > +static int __init dptc_init(void) Shouldn't this be a platform driver with a probe routine? > +{ > + const struct dptc_soc_entry *soc_entry = NULL; > + const struct dmi_system_id *dmi_entry; > + enum dptc_limit_mode max_mode; > + int i, ret; > + > + /* Check ALIB ACPI method presence */ > + if (!acpi_has_method(NULL, ALIB_PATH)) { > + pr_debug(DRIVER_NAME ": ALIB ACPI method not present\n"); > + return -ENODEV; > + } > + > + /* Parse limit_mode module parameter */ > + if (!strcmp(limit_mode, "device")) { > + max_mode = LIMIT_DEVICE; > + } else if (!strcmp(limit_mode, "expanded")) { > + max_mode = LIMIT_EXPANDED; > + } else if (!strcmp(limit_mode, "soc")) { > + max_mode = LIMIT_SOC; > + } else if (!strcmp(limit_mode, "unbound")) { > + max_mode = LIMIT_UNBOUND; > + } else { > + pr_err(DRIVER_NAME ": unknown limit_mode '%s'\n", limit_mode); > + return -EINVAL; > + } > +#ifndef CONFIG_AMD_DPTC_EXTENDED > + if (max_mode > LIMIT_EXPANDED) { > + pr_err(DRIVER_NAME ": limit_mode '%s' requires CONFIG_AMD_DPTC_EXTENDED\n", > + limit_mode); > + return -EINVAL; > + } > +#endif > + > + /* SoC match - required for device/expanded/soc, optional for unbound */ > + for (i = 0; dptc_soc_table[i].cpu_id; i++) { > + if (strstr(boot_cpu_data.x86_model_id, dptc_soc_table[i].cpu_id)) { > + soc_entry = &dptc_soc_table[i]; > + break; > + } > + } > + if (!soc_entry && max_mode < LIMIT_UNBOUND) { > + pr_debug(DRIVER_NAME ": unrecognized SoC '%s'\n", > + boot_cpu_data.x86_model_id); > + return -ENODEV; > + } > + > + /* Optional device DMI match */ > + dmi_entry = dmi_first_match(dptc_dmi_table); IMV - NO WAY. Device matches are mandatory. I'm not letting a driver like this bind to any random piece of hardware in the wild. The thermal design of each system is different. Each system needs it's own quirks/table. > + > + /* > + * device and expanded modes require a DMI match; refuse to load if > + * the user requested one of those tiers but the device is unknown. > + */ > + if (max_mode <= LIMIT_EXPANDED && !dmi_entry) { > + pr_debug(DRIVER_NAME > + ": limit_mode='%s' requires a device DMI match\n", > + limit_mode); > + return -ENODEV; > + } > + > + dptc = kzalloc(sizeof(*dptc), GFP_KERNEL); > + if (!dptc) > + return -ENOMEM; > + > + mutex_init(&dptc->lock); > + dptc->soc_limits = soc_entry ? soc_entry->limits : NULL; > + dptc->max_mode = max_mode; > + if (dmi_entry) > + dptc->dev_limits = dmi_entry->driver_data; > + > + if (dptc->dev_limits) > + dptc->active_mode = LIMIT_DEVICE; > + else if (dptc->soc_limits) > + dptc->active_mode = LIMIT_SOC; > + else > + dptc->active_mode = LIMIT_UNBOUND; > + > + /* ---- Probe logging ---- */ > + if (soc_entry) > + pr_info(DRIVER_NAME ": SoC: %s\n", soc_entry->cpu_id); > + else > + pr_info(DRIVER_NAME ": SoC unrecognized ('%s'), running unbound\n", > + boot_cpu_data.x86_model_id); > + > + if (dmi_entry) { > + pr_info(DRIVER_NAME ": Device: %s\n", dmi_entry->ident); > + pr_info(DRIVER_NAME ": Device limits (device mode):\n"); > + for (i = 0; i < DPTC_NUM_PARAMS; i++) { > + pr_info(DRIVER_NAME ": %-16s smin=%-8u smax=%-8u def=%u\n", > + dptc_params[i].name, > + dptc->dev_limits->p[i].smin, > + dptc->dev_limits->p[i].smax, > + dptc->dev_limits->p[i].def); > + } > + if (max_mode >= LIMIT_EXPANDED) { > + pr_info(DRIVER_NAME ": Device limits (expanded mode):\n"); > + for (i = 0; i < DPTC_NUM_PARAMS; i++) { > + pr_info(DRIVER_NAME ": %-16s min=%-8u max=%u\n", > + dptc_params[i].name, > + dptc->dev_limits->p[i].min, > + dptc->dev_limits->p[i].max); > + } > + } > + } else { > + pr_info(DRIVER_NAME ": No device DMI match\n"); > + } > + > + if (max_mode >= LIMIT_SOC && dptc->soc_limits) { > + pr_info(DRIVER_NAME ": SoC limits:\n"); > + for (i = 0; i < DPTC_NUM_PARAMS; i++) { > + pr_info(DRIVER_NAME ": %-16s max=%u\n", > + dptc_params[i].name, > + dptc->soc_limits->p[i].max); > + } > + } > + > + pr_info(DRIVER_NAME ": active_mode=%s max_mode=%s\n", > + mode_names[dptc->active_mode], mode_names[dptc->max_mode]); > + > + /* ---- Firmware-attributes class device ---- */ This seems like it doesn't use any of the firmware attributes helpers that exist. A lot of wheel re-inventing below. > + dptc->fw_attr_dev = device_create(&firmware_attributes_class, > + NULL, MKDEV(0, 0), NULL, > + DRIVER_NAME); > + if (IS_ERR(dptc->fw_attr_dev)) { > + ret = PTR_ERR(dptc->fw_attr_dev); > + goto err_free; > + } > + > + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL, > + &dptc->fw_attr_dev->kobj); > + if (!dptc->fw_attr_kset) { > + ret = -ENOMEM; > + goto err_dev; > + } > + > + for (i = 0; i < DPTC_NUM_PARAMS; i++) { > + dptc_setup_param_sysfs(&dptc->params[i], i); > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj, > + &dptc->params[i].group); > + if (ret) { > + while (--i >= 0) > + sysfs_remove_group(&dptc->fw_attr_kset->kobj, > + &dptc->params[i].group); > + goto err_kset; > + } > + } > + > + dptc_setup_mode_sysfs(&dptc->mode_attr); > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj, > + &dptc->mode_attr.group); > + if (ret) { > + for (i = 0; i < DPTC_NUM_PARAMS; i++) > + sysfs_remove_group(&dptc->fw_attr_kset->kobj, > + &dptc->params[i].group); > + goto err_kset; > + } > + > + 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) { > + sysfs_remove_group(&dptc->fw_attr_kset->kobj, &dptc->mode_attr.group); > + for (i = 0; i < DPTC_NUM_PARAMS; i++) > + sysfs_remove_group(&dptc->fw_attr_kset->kobj, > + &dptc->params[i].group); > + goto err_kset; > + } > + > + register_pm_notifier(&dptc_pm_nb); > + pr_info(DRIVER_NAME ": loaded\n"); This function is INCREDIBLY noisy. Why so many pr_info()? At most it should be one for probe, but probably none. > + return 0; > + > +err_kset: > + kset_unregister(dptc->fw_attr_kset); > +err_dev: > + device_destroy(&firmware_attributes_class, MKDEV(0, 0)); > +err_free: > + mutex_destroy(&dptc->lock); > + kfree(dptc); > + dptc = NULL; > + return ret; Looks like a lot of code duplication with dptc_exit(). If you drop the boilerplate and use the right helpers you probably can reduce the majority of it. > +} > + > +static void __exit dptc_exit(void) > +{ > + int i; > + > + unregister_pm_notifier(&dptc_pm_nb); > + sysfs_remove_file(&dptc->fw_attr_kset->kobj, &dptc->save_settings_attr.attr); > + sysfs_remove_group(&dptc->fw_attr_kset->kobj, &dptc->mode_attr.group); > + for (i = 0; i < DPTC_NUM_PARAMS; i++) > + sysfs_remove_group(&dptc->fw_attr_kset->kobj, > + &dptc->params[i].group); > + kset_unregister(dptc->fw_attr_kset); > + device_destroy(&firmware_attributes_class, MKDEV(0, 0)); > + mutex_destroy(&dptc->lock); > + kfree(dptc); > + dptc = NULL; > +} > + > +module_init(dptc_init); > +module_exit(dptc_exit); ^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC v1 2/2] platform/x86/amd: Add AMD DPTCi driver 2026-03-03 20:10 ` Mario Limonciello (AMD) (kernel.org) @ 2026-03-03 20:40 ` Antheas Kapenekakis 2026-03-03 20:54 ` Mario Limonciello (AMD) (kernel.org) 0 siblings, 1 reply; 18+ messages in thread From: Antheas Kapenekakis @ 2026-03-03 20:40 UTC (permalink / raw) To: Mario Limonciello (AMD) (kernel.org); +Cc: linux-kernel, platform-driver-x86 On Tue, 3 Mar 2026 at 21:10, Mario Limonciello (AMD) (kernel.org) <superm1@kernel.org> wrote: > > I'll preface this by saying - I don't have a problem with using an AI to > help write a driver, but please disclose that it was done and that in > this case even you haven't closely audited the results. > > I personally would never submit something generated by an LLM that I > didn't audit and add a S-o-b tag to it (asserting I am willing to stand > by the code). > > I'm glad that I found out it was AI written before I started to review > the code, I would have had a lot more candid comments for you. > > There is a lot of weird stuff in this driver that I'm not going to > comment on and nitpick, but I'll leave a few broad strokes things. Of course, to that end, feel free to skip a full review until I get to properly rewriting it. What is the current stance on Co-bys for that? I'm trying to follow the discussion but I missed the news. I can lint it properly next time. From my perspective, pouring a month into a driver like this without having a firm commitment that it will go somewhere is a bit hard to stomach. > On 3/3/2026 12:17 PM, Antheas Kapenekakis wrote: > > Implement a driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power > > and Thermal Configuration Interface (DPTCi). This function allows > > userspace to configure APU power and thermal parameters at runtime by > > calling the \_SB.ALIB ACPI method with a packed parameter buffer. > > > > Unlike mainstream AMD laptops, the handheld devices targeted by this > > driver do not implement vendor-specific WMI or EC hooks for TDP control. > > The ones that do, use DPTCi under the hood. For these devices, exposing > > the ALIB interface is the only viable mechanism for the OS to adjust > > power limits, making a dedicated kernel driver the correct approach > > rather than relying on unrestricted access to /dev/mem or ACPI method > > invocation from userspace. > > > > The driver exposes seven parameters (stapm_limit, fast_limit, > > slow_limit, skin_limit, slow_time, stapm_time, temp_target) through the > > firmware-attributes sysfs ABI. Values are staged in driver memory and > > sent to firmware atomically on a single write to the commit attribute, > > avoiding partial-update races. > > Let me ask - why do all these files need to be exposed in the first > place to firmware attributes API? This is a good question. I recall asking Armin/you the same when the ABI for the lenovo/armoury driver was being considered and being veto'ed. But these drivers use firmware attributes now and so does my MSI platform draft. Not only that, but the accepted asus-wmi ABI is deprecated now. So, I am in a hard position when it comes to this. I do not have a good solution. But this is the accepted approach as it stands. > The vast majority of people don't need to to change all these settings > at once, and that's why there is a power slider concept. > > Shouldn't this driver register a platform profile and all the settings > tie to a platform profile? > > If you really want tuning, that's what custom platform profile is for. > You can look at how the other drivers that implement it do this. This driver does not have a concept of platform profile. The devices by definition do not have presets and users of handhelds are accustomed to a ppt slider (where userspace interpolates for fppt/sppt or sets them the same). We could layer a platform profile on top of it by extending the driver more and adding suggested preselected profiles for low-power, balanced, and performance. Then custom unlocks the sliders. This is the approach I do currently when I hook to the ppd dbus protocol and it works quite well. As for custom profile, it unlocks the firmware attributes in other drivers. The only difference in this one is the naming of the attributes. > > > > Four limit tiers gate the accepted value range: > > > > device - per-device safe range validated in the DMI table (smin..smax) > > expanded - full per-device OEM range (min..max) > > soc - APU generation envelope (e.g. 0..54 W for Ryzen 7040) > > unbound - no firmware-side range enforced > > > > The active tier is controlled by the limit_mode module parameter and can > > be switched at runtime via the limit_mode sysfs attribute. The soc and > > unbound tiers require CONFIG_AMD_DPTC_EXTENDED=y; without it only device > > and expanded are available and the default is "device". With it the > > default becomes "unbound". > > > > DMI entries cover 39 device variants across GPD, AYANEO, OneXPlayer, > > AOKZOE, OrangePi, and SuiPlay. SoC limits are detected via substring > > match on boot_cpu_data.x86_model_id for Ryzen 5000/6000/7040/8040, > > Ryzen Z1, Ryzen AI HX 370/360, and Ryzen AI MAX+ 380/385/395 series. > > > > MODULE_DEVICE_TABLE(dmi, dptc_dmi_table) is declared so udev autoloads > > the module on DMI-matched devices. > > Duh? > > > > > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev> > > --- > > MAINTAINERS | 6 + > > drivers/platform/x86/amd/Kconfig | 27 + > > drivers/platform/x86/amd/Makefile | 2 + > > drivers/platform/x86/amd/dptc.c | 1325 +++++++++++++++++++++++++++++ > > 4 files changed, 1360 insertions(+) > > create mode 100644 drivers/platform/x86/amd/dptc.c > > > > diff --git a/MAINTAINERS b/MAINTAINERS > > index e08767323763..915293594641 100644 > > --- a/MAINTAINERS > > +++ b/MAINTAINERS > > @@ -1096,6 +1096,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..bd74e2bcc42c 100644 > > --- a/drivers/platform/x86/amd/Kconfig > > +++ b/drivers/platform/x86/amd/Kconfig > > @@ -44,3 +44,30 @@ 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. > > + > > + The driver requires a recognized AMD SoC and will only expose > > + device-specific limits when the system is present in the DMI table. > > + > > + If built as a module, the module will be called amd_dptc. > > + > > +config AMD_DPTC_EXTENDED > > + bool "AMD DPTCi extended limit modes (soc, unbound)" > > + depends on AMD_DPTC > > + help > > + Expose the soc and unbound limit modes, which allow setting TDP > > + values beyond the device-specific safe range validated in the DMI > > + table. When enabled, the default limit_mode is unbound. > > Something dangerous should taint the machine and in my view hidden in > debugfs. Noted. > > + > > + Only enable this if you know what you are doing. Incorrect power > > + limit values can cause thermal or stability issues. > > 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..9fb13e47b813 > > --- /dev/null > > +++ b/drivers/platform/x86/amd/dptc.c > > @@ -0,0 +1,1325 @@ > > +// SPDX-License-Identifier: GPL-2.0-only > > +/* > > + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver > > + * > > + * Implements AGESA ALIB Function 0x0C via the firmware-attributes sysfs ABI. > > + * Allows userspace to stage and atomically commit APU power/thermal parameters. > > + * > > + * Reference: AMD AGESA Publication #44065, Appendix E.5 > > I feel that you should include a link to the document. > > https://docs.amd.com/v/u/en-US/44065_Arch2008 Noted. > > + * > > + * Copyright (C) 2025 Antheas Kapenekakis <lkml@antheas.dev> > > + */ > > + > > +#include <linux/acpi.h> > > +#include <linux/dmi.h> > > +#include <linux/init.h> > > +#include <linux/kobject.h> > > +#include <linux/module.h> > > +#include <linux/mutex.h> > > +#include <linux/processor.h> > > +#include <linux/suspend.h> > > +#include <linux/sysfs.h> > > + > > +#include "../firmware_attributes_class.h" > > + > > +#define DRIVER_NAME "amd_dptc" > > + > > +/* AGESA ALIB Function 0x0C - Dynamic Power and Thermal Configuration */ > > +#define ALIB_FUNC_DPTC 0x0C > > +#define ALIB_PATH "\\_SB.ALIB" > > + > > +/* ALIB parameter IDs (AGESA spec Appendix E.5, Table E-52) */ > > +#define ALIB_ID_STAPM_TIME 0x01 > > +#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_SLOW_TIME 0x08 > > +#define ALIB_ID_SKIN_LIMIT 0x2E > > + > > +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>"); > > +MODULE_DESCRIPTION("AMD DPTCi (ALIB Function 0x0C) firmware attributes driver"); > > +MODULE_LICENSE("GPL"); > > + > > +#ifdef CONFIG_AMD_DPTC_EXTENDED > > +static char *limit_mode = "unbound"; > > +#else > > +static char *limit_mode = "device"; > > +#endif > > +#ifdef CONFIG_AMD_DPTC_EXTENDED > > +#define DPTC_LIMIT_MODE_DESC "Maximum limit tier to expose: device, expanded, soc, unbound" > > +#else > > +#define DPTC_LIMIT_MODE_DESC "Maximum limit tier to expose: device, expanded" > > +#endif > > +module_param(limit_mode, charp, 0444); > > +MODULE_PARM_DESC(limit_mode, DPTC_LIMIT_MODE_DESC); > > + > > +/* ========================================================================= > > + * Enums > > + * ========================================================================= > > + */ > > + > > +enum dptc_param_idx { > > + DPTC_STAPM_LIMIT, > > + DPTC_FAST_LIMIT, > > + DPTC_SLOW_LIMIT, > > + DPTC_SKIN_LIMIT, > > + DPTC_SLOW_TIME, > > + DPTC_STAPM_TIME, > > + DPTC_TEMP_TARGET, > > + DPTC_NUM_PARAMS, > > +}; > > + > > +enum dptc_limit_mode { > > + LIMIT_DEVICE, /* smin..smax: safe operating range for this device */ > > + LIMIT_EXPANDED, /* min..max: full hardware range for this device */ > > + LIMIT_SOC, /* APU hardware limits from ALIB_PARAMS */ > > + LIMIT_UNBOUND, /* no firmware-side limits enforced */ > > +}; > > + > > +/* ========================================================================= > > + * Data structures > > + * ========================================================================= > > + */ > > + > > +/* > > + * Per-parameter limits for DMI-matched devices. > > + * TDP params (stapm/fast/slow/skin) in milliwatts (watts * 1000). > > + * Time params in seconds, temp in degrees Celsius. > > + */ > > +struct dptc_param_limits { > > + u32 min; /* expanded floor: widest safe hardware minimum */ > > + u32 smin; /* device floor: safe operating minimum */ > > + u32 def; /* default hint for userspace */ > > + u32 smax; /* device ceiling: safe operating maximum */ > > + u32 max; /* expanded ceiling: widest safe hardware maximum */ > > +}; > > + > > +struct dptc_device_limits { > > + struct dptc_param_limits p[DPTC_NUM_PARAMS]; > > +}; > > + > > +/* Per-parameter limits from ALIB_PARAMS - the APU's own hardware envelope */ > > +struct dptc_soc_range { > > + u32 min; > > + u32 max; > > +}; > > + > > +struct dptc_soc_limits { > > + struct dptc_soc_range p[DPTC_NUM_PARAMS]; > > +}; > > + > > +struct dptc_param_desc { > > + const char *name; > > + const char *display_name; > > + u8 param_id; > > +}; > > + > > +struct dptc_soc_entry { > > + const char *cpu_id; /* substring of x86_model_id */ > > + const struct dptc_soc_limits *limits; > > +}; > > + > > +/* ========================================================================= > > + * Parameter descriptor table > > + * ========================================================================= > > + */ > > + > > +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = { > > + [DPTC_STAPM_LIMIT] = { "stapm_limit", "Sustained TDP (mW)", > > + ALIB_ID_STAPM_LIMIT }, > > + [DPTC_FAST_LIMIT] = { "fast_limit", "Fast PPT limit (mW)", > > + ALIB_ID_FAST_LIMIT }, > > + [DPTC_SLOW_LIMIT] = { "slow_limit", "Slow PPT limit (mW)", > > + ALIB_ID_SLOW_LIMIT }, > > + [DPTC_SKIN_LIMIT] = { "skin_limit", "Skin temperature TDP limit (mW)", > > + ALIB_ID_SKIN_LIMIT }, > > + [DPTC_SLOW_TIME] = { "slow_time", "Slow PPT time constant (s)", > > + ALIB_ID_SLOW_TIME }, > > + [DPTC_STAPM_TIME] = { "stapm_time", "STAPM time constant (s)", > > + ALIB_ID_STAPM_TIME }, > > + [DPTC_TEMP_TARGET] = { "temp_target", "Thermal control limit (C)", > > + ALIB_ID_TEMP_TARGET }, > > +}; > > + > > +/* ========================================================================= > > + * Device limit classes TDP values multiplied by 1000 (milliwatts). > > + * ========================================================================= > > + */ > > + > > +/* 18W class: AYANEO AIR Plus (Ryzen 5 5560U) */ > > +static const struct dptc_device_limits limits_18w = { .p = { > > + [DPTC_STAPM_LIMIT] = { 0, 5000, 15000, 18000, 22000 }, > > + [DPTC_FAST_LIMIT] = { 0, 5000, 15000, 20000, 25000 }, > > + [DPTC_SLOW_LIMIT] = { 0, 5000, 15000, 18000, 22000 }, > > + [DPTC_SKIN_LIMIT] = { 0, 5000, 15000, 18000, 22000 }, > > + [DPTC_SLOW_TIME] = { 5, 5, 10, 10, 10 }, > > + [DPTC_STAPM_TIME] = { 100, 100, 100, 200, 200 }, > > + [DPTC_TEMP_TARGET] = { 60, 70, 85, 90, 100 }, > > +}}; > > + > > +/* 25W class: Ryzen 5000 handhelds (AYANEO NEXT, KUN) */ > > +static const struct dptc_device_limits limits_25w = { .p = { > > + [DPTC_STAPM_LIMIT] = { 0, 4000, 15000, 25000, 32000 }, > > + [DPTC_FAST_LIMIT] = { 0, 4000, 25000, 30000, 37000 }, > > + [DPTC_SLOW_LIMIT] = { 0, 4000, 20000, 27000, 35000 }, > > + [DPTC_SKIN_LIMIT] = { 0, 4000, 15000, 25000, 32000 }, > > + [DPTC_SLOW_TIME] = { 5, 5, 10, 10, 10 }, > > + [DPTC_STAPM_TIME] = { 100, 100, 100, 200, 200 }, > > + [DPTC_TEMP_TARGET] = { 60, 70, 85, 90, 100 }, > > +}}; > > + > > +/* 28W class: GPD Win series, AYANEO 2, OrangePi NEO-01 */ > > +static const struct dptc_device_limits limits_28w = { .p = { > > + [DPTC_STAPM_LIMIT] = { 0, 4000, 15000, 28000, 32000 }, > > + [DPTC_FAST_LIMIT] = { 0, 4000, 25000, 32000, 37000 }, > > + [DPTC_SLOW_LIMIT] = { 0, 4000, 20000, 30000, 35000 }, > > + [DPTC_SKIN_LIMIT] = { 0, 4000, 15000, 28000, 32000 }, > > + [DPTC_SLOW_TIME] = { 5, 5, 10, 10, 10 }, > > + [DPTC_STAPM_TIME] = { 100, 100, 100, 200, 200 }, > > + [DPTC_TEMP_TARGET] = { 60, 70, 85, 90, 100 }, > > +}}; > > + > > +/* 30W class: OneXPlayer, AYANEO AIR/FLIP/GEEK/SLIDE/3, AOKZOE */ > > +static const struct dptc_device_limits limits_30w = { .p = { > > + [DPTC_STAPM_LIMIT] = { 0, 4000, 15000, 30000, 40000 }, > > + [DPTC_FAST_LIMIT] = { 0, 4000, 25000, 41000, 50000 }, > > + [DPTC_SLOW_LIMIT] = { 0, 4000, 20000, 32000, 43000 }, > > + [DPTC_SKIN_LIMIT] = { 0, 4000, 15000, 30000, 40000 }, > > + [DPTC_SLOW_TIME] = { 5, 5, 10, 10, 10 }, > > + [DPTC_STAPM_TIME] = { 100, 100, 100, 200, 200 }, > > + [DPTC_TEMP_TARGET] = { 60, 70, 85, 90, 100 }, > > +}}; > > + > > +/* Win 5 class: GPD Win 5 */ > > +static const struct dptc_device_limits limits_win5 = { .p = { > > + [DPTC_STAPM_LIMIT] = { 0, 4000, 25000, 85000, 100000 }, > > + [DPTC_FAST_LIMIT] = { 0, 4000, 40000, 85000, 100000 }, > > + [DPTC_SLOW_LIMIT] = { 0, 4000, 27000, 85000, 100000 }, > > + [DPTC_SKIN_LIMIT] = { 0, 4000, 25000, 85000, 100000 }, > > + [DPTC_SLOW_TIME] = { 5, 5, 10, 10, 10 }, > > + [DPTC_STAPM_TIME] = { 100, 100, 100, 200, 200 }, > > + [DPTC_TEMP_TARGET] = { 60, 70, 95, 95, 100 }, > > +}}; > > + > > +/* ========================================================================= > > + * SoC limit classes > > + * ========================================================================= > > + */ > > + > > +/* Standard SoC: Ryzen 5000 through 8000, Z1, AI 9 HX 370 */ > > +static const struct dptc_soc_limits soc_standard = { .p = { > > + [DPTC_STAPM_LIMIT] = { 0, 54000 }, > > + [DPTC_FAST_LIMIT] = { 0, 54000 }, > > + [DPTC_SLOW_LIMIT] = { 0, 54000 }, > > + [DPTC_SKIN_LIMIT] = { 0, 54000 }, > > + [DPTC_SLOW_TIME] = { 0, 30 }, > > + [DPTC_STAPM_TIME] = { 0, 300 }, > > + [DPTC_TEMP_TARGET] = { 0, 105 }, > > +}}; > > + > > +/* AI MAX SoC: Ryzen AI MAX series */ > > +static const struct dptc_soc_limits soc_aimax = { .p = { > > + [DPTC_STAPM_LIMIT] = { 0, 120000 }, > > + [DPTC_FAST_LIMIT] = { 0, 120000 }, > > + [DPTC_SLOW_LIMIT] = { 0, 120000 }, > > + [DPTC_SKIN_LIMIT] = { 0, 120000 }, > > + [DPTC_SLOW_TIME] = { 0, 30 }, > > + [DPTC_STAPM_TIME] = { 0, 300 }, > > + [DPTC_TEMP_TARGET] = { 0, 105 }, > > +}}; > > + > > +/* ========================================================================= > > + * SoC CPU table > > + * Substring matches against boot_cpu_data.x86_model_id. > > + * Order matters: more specific strings before broader ones. > > + * ========================================================================= > > + */ > > + > > +static const struct dptc_soc_entry dptc_soc_table[] = { > > + /* AI MAX - must precede "AMD Ryzen AI"; 395 before 385 to avoid short match */ > > + { "AMD RYZEN AI MAX+ 395", &soc_aimax }, > > + { "AMD RYZEN AI MAX+ 385", &soc_aimax }, > > + { "AMD RYZEN AI MAX 380", &soc_aimax }, > > + /* Ryzen AI */ > > + { "AMD Ryzen AI 9 HX 370", &soc_standard }, > > + { "AMD Ryzen AI HX 360", &soc_standard }, > > + /* Z1 - Extreme before plain Z1 */ > > + { "AMD Ryzen Z1 Extreme", &soc_standard }, > > + { "AMD Ryzen Z1", &soc_standard }, > > + /* Ryzen 8000 */ > > + { "AMD Ryzen 7 8840U", &soc_standard }, > > + /* Ryzen 7040 */ > > + { "AMD Ryzen 7 7840U", &soc_standard }, > > + /* Ryzen 6000 */ > > + { "AMD Ryzen 7 6800U", &soc_standard }, > > + { "AMD Ryzen 7 6600U", &soc_standard }, > > + /* Ryzen 5000 */ > > + { "AMD Ryzen 7 5800U", &soc_standard }, > > + { "AMD Ryzen 7 5700U", &soc_standard }, > > + { "AMD Ryzen 5 5560U", &soc_standard }, > > + { } > > +}; > > + > > +/* ========================================================================= > > + * DMI device table > > + * Excluded: ASUS ROG, Lenovo Legion devices. > > + * ========================================================================= > > + */ > > + > > +static const struct dmi_system_id dptc_dmi_table[] = { > > + /* --- GPD (DMI_SYS_VENDOR + DMI_PRODUCT_NAME) --- */ > > + { > > + .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 = { > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"), > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"), > > + }, > > + .driver_data = (void *)&limits_win5, > > + }, > > + { > > + .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 (DMI_BOARD_VENDOR + DMI_BOARD_NAME) --- */ > > + { > > + .ident = "OrangePi NEO-01", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"), > > + }, > > + .driver_data = (void *)&limits_28w, > > + }, > > + /* --- AOKZOE (DMI_BOARD_VENDOR + DMI_BOARD_NAME) --- */ > > + { > > + .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 (DMI_BOARD_VENDOR "ONE-NETBOOK" + DMI_BOARD_NAME) --- > > + * AMD-based devices only; Intel variants share board names but we > > + * rely on the SoC table to reject non-AMD CPUs. > > + */ > > + { > > + .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 (DMI_BOARD_VENDOR + DMI_BOARD_NAME) --- */ > > + /* 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, > > + }, > > + /* 25W - Ryzen 5000 */ > > + { > > + .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, > > + }, > > + /* 28W - Ryzen 6000 */ > > + { > > + .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, > > + }, > > + /* 30W - Ryzen 7040 / Z1 */ > > + { > > + /* Must come before the shorter "AIR" match */ > > + .ident = "AYANEO AIR 1S", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > > + DMI_MATCH(DMI_BOARD_NAME, "AIR 1S"), > > + }, > > + .driver_data = (void *)&limits_30w, > > + }, > > + { > > + .ident = "AYANEO AIR Pro", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Pro"), > > + }, > > + .driver_data = (void *)&limits_30w, > > + }, > > + { > > + .ident = "AYANEO AIR", > > + .matches = { > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"), > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR"), > > + }, > > + .driver_data = (void *)&limits_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); > > + > > +/* ========================================================================= > > + * Per-parameter sysfs state > > + * ========================================================================= > > + */ > > + > > +struct dptc_param_sysfs { > > + 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]; /* 7 attrs + NULL */ > > + struct attribute_group group; > > + int idx; > > +}; > > + > > +struct dptc_mode_sysfs { > > + struct kobj_attribute current_value; > > + struct kobj_attribute possible_values; > > + struct kobj_attribute default_value; > > + struct kobj_attribute display_name; > > + struct kobj_attribute type; > > + struct attribute *attrs[6]; /* 5 attrs + NULL */ > > + struct attribute_group group; > > +}; > > + > > +/* ========================================================================= > > + * Driver private state > > + * ========================================================================= > > + */ > > + > > +struct dptc_priv { > > + struct device *fw_attr_dev; > > + struct kset *fw_attr_kset; > > + > > + const struct dptc_device_limits *dev_limits; /* NULL if no DMI match */ > > + const struct dptc_soc_limits *soc_limits; /* NULL if SoC unrecognized */ > > + > > + enum dptc_limit_mode active_mode; > > + enum dptc_limit_mode max_mode; > > + > > + enum dptc_commit_mode { COMMIT_AUTO, COMMIT_MANUAL } commit_mode; > > + > > + u32 staged[DPTC_NUM_PARAMS]; > > + bool has_staged[DPTC_NUM_PARAMS]; > > + > > + /* Protects staged values and has_staged flags */ > > + struct mutex lock; > > + > > + struct dptc_param_sysfs params[DPTC_NUM_PARAMS]; > > + struct dptc_mode_sysfs mode_attr; > > + struct kobj_attribute save_settings_attr; > > +}; > > + > > +static struct dptc_priv *dptc; > > + > > +/* ========================================================================= > > + * Limit accessors > > + * ========================================================================= > > + */ > > + > > +static u32 dptc_get_min(int idx) > > +{ > > + switch (dptc->active_mode) { > > + case LIMIT_DEVICE: return dptc->dev_limits->p[idx].smin; > > + case LIMIT_EXPANDED: return dptc->dev_limits->p[idx].min; > > + case LIMIT_SOC: return dptc->soc_limits->p[idx].min; > > + case LIMIT_UNBOUND: return 0; > > + } > > + return 0; > > +} > > + > > +static u32 dptc_get_max(int idx) > > +{ > > + switch (dptc->active_mode) { > > + case LIMIT_DEVICE: return dptc->dev_limits->p[idx].smax; > > + case LIMIT_EXPANDED: return dptc->dev_limits->p[idx].max; > > + case LIMIT_SOC: return dptc->soc_limits->p[idx].max; > > + case LIMIT_UNBOUND: return U32_MAX; > > + } > > + return 0; > > +} > > + > > +/* Default hint comes from the device DMI table; 0 if no DMI match */ > > +static u32 dptc_get_default(int idx) > > +{ > > + if (dptc->dev_limits) > > + return dptc->dev_limits->p[idx].def; > > + return 0; > > +} > > + > > +/* ========================================================================= > > + * ALIB call > > + * ========================================================================= > > + */ > > + > > +static int dptc_alib_commit(void) > > +{ > > + union acpi_object in_params[2]; > > + struct acpi_object_list input; > > + u32 vals[DPTC_NUM_PARAMS]; > > + u8 ids[DPTC_NUM_PARAMS]; > > + acpi_status status; > > + int i, off; > > + int count = 0; > > + u32 buf_size; > > + u8 *buf; > > + > > + for (i = 0; i < DPTC_NUM_PARAMS; i++) { > > + if (!dptc->has_staged[i]) > > + continue; > > + ids[count] = dptc_params[i].param_id; > > + vals[count] = dptc->staged[i]; > > + count++; > > + } > > + > > + if (count == 0) > > + return -EINVAL; > > + > > + /* 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; > > + > > + buf[0] = buf_size & 0xff; > > + buf[1] = (buf_size >> 8) & 0xff; > > + > > + for (i = 0; i < count; i++) { > > + off = 2 + i * 5; > > + buf[off] = ids[i]; > > + buf[off + 1] = vals[i] & 0xff; > > + buf[off + 2] = (vals[i] >> 8) & 0xff; > > + buf[off + 3] = (vals[i] >> 16) & 0xff; > > + buf[off + 4] = (vals[i] >> 24) & 0xff; > > + } > > + > > + 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(DRIVER_NAME ": ALIB call failed: %s\n", > > + acpi_format_exception(status)); > > + return -EIO; > > + } > > + > > + pr_debug(DRIVER_NAME ": committed %d parameter(s)\n", count); > > + return 0; > > +} > > + > > +/* ========================================================================= > > + * Sysfs callbacks - per-parameter attributes > > + * ========================================================================= > > + */ > > + > > +static ssize_t dptc_current_value_show(struct kobject *kobj, > > + struct kobj_attribute *attr, char *buf) > > +{ > > + struct dptc_param_sysfs *ps = > > + container_of(attr, struct dptc_param_sysfs, current_value); > > + > > + if (!dptc->has_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_param_sysfs *ps = > > + container_of(attr, struct dptc_param_sysfs, current_value); > > + u32 val, min, max; > > + int ret; > > + > > + /* Empty write clears the staged value */ > > + if (count == 0 || (count == 1 && buf[0] == '\n')) { > > + mutex_lock(&dptc->lock); > > + dptc->has_staged[ps->idx] = false; > > + mutex_unlock(&dptc->lock); > > + return count; > > + } > > + > > + ret = kstrtou32(buf, 10, &val); > > + if (ret) > > + return ret; > > + > > + mutex_lock(&dptc->lock); > > + min = dptc_get_min(ps->idx); > > + max = dptc_get_max(ps->idx); > > + if (val < min || (max != U32_MAX && val > max)) { > > + mutex_unlock(&dptc->lock); > > + return -EINVAL; > > + } > > + dptc->staged[ps->idx] = val; > > + dptc->has_staged[ps->idx] = true; > > + if (dptc->commit_mode == COMMIT_AUTO) > > + ret = dptc_alib_commit(); > > + mutex_unlock(&dptc->lock); > > + > > + return ret ? ret : count; > > +} > > + > > +static ssize_t dptc_default_value_show(struct kobject *kobj, > > + struct kobj_attribute *attr, char *buf) > > +{ > > + struct dptc_param_sysfs *ps = > > + container_of(attr, struct dptc_param_sysfs, default_value); > > + u32 def = dptc_get_default(ps->idx); > > + > > + if (!dptc->dev_limits) > > + return sysfs_emit(buf, "\n"); > > + return sysfs_emit(buf, "%u\n", def); > > +} > > + > > +static ssize_t dptc_min_value_show(struct kobject *kobj, > > + struct kobj_attribute *attr, char *buf) > > +{ > > + struct dptc_param_sysfs *ps = > > + container_of(attr, struct dptc_param_sysfs, min_value); > > + return sysfs_emit(buf, "%u\n", dptc_get_min(ps->idx)); > > +} > > + > > +static ssize_t dptc_max_value_show(struct kobject *kobj, > > + struct kobj_attribute *attr, char *buf) > > +{ > > + struct dptc_param_sysfs *ps = > > + container_of(attr, struct dptc_param_sysfs, max_value); > > + u32 max = dptc_get_max(ps->idx); > > + > > + if (max == U32_MAX) > > + return sysfs_emit(buf, "\n"); > > + return sysfs_emit(buf, "%u\n", max); > > +} > > + > > +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_param_sysfs *ps = > > + container_of(attr, struct dptc_param_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) > > +{ > > + if (dptc->commit_mode == COMMIT_AUTO) > > + 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) > > +{ > > + int ret = 0; > > + > > + if (sysfs_streq(buf, "save")) { > > + mutex_lock(&dptc->lock); > > + ret = dptc_alib_commit(); > > + mutex_unlock(&dptc->lock); > > + } else if (sysfs_streq(buf, "single")) { > > + mutex_lock(&dptc->lock); > > + dptc->commit_mode = COMMIT_AUTO; > > + mutex_unlock(&dptc->lock); > > + } else if (sysfs_streq(buf, "bulk")) { > > + mutex_lock(&dptc->lock); > > + dptc->commit_mode = COMMIT_MANUAL; > > + mutex_unlock(&dptc->lock); > > + } else { > > + return -EINVAL; > > + } > > + > > + return ret ? ret : count; > > +} > > + > > +static const char * const mode_names[] = { > > + [LIMIT_DEVICE] = "device", > > + [LIMIT_EXPANDED] = "expanded", > > + [LIMIT_SOC] = "soc", > > + [LIMIT_UNBOUND] = "unbound", > > +}; > > + > > +static bool dptc_mode_available(enum dptc_limit_mode mode) > > +{ > > + if (mode == LIMIT_DEVICE || mode == LIMIT_EXPANDED) > > + return dptc->dev_limits; > > + if (mode == LIMIT_SOC) > > + return dptc->soc_limits; > > + return true; /* LIMIT_UNBOUND always available */ > > +} > > + > > +static ssize_t dptc_mode_current_value_show(struct kobject *kobj, > > + struct kobj_attribute *attr, > > + char *buf) > > +{ > > + return sysfs_emit(buf, "%s\n", mode_names[dptc->active_mode]); > > +} > > + > > +static ssize_t dptc_mode_current_value_store(struct kobject *kobj, > > + struct kobj_attribute *attr, > > + const char *buf, size_t count) > > +{ > > + enum dptc_limit_mode new_mode; > > + int m; > > + > > + for (m = 0; m <= LIMIT_UNBOUND; m++) { > > + if (sysfs_streq(buf, mode_names[m])) { > > + new_mode = m; > > + goto found; > > + } > > + } > > + return -EINVAL; > > + > > +found: > > + if (new_mode > dptc->max_mode) > > + return -EPERM; > > + if (!dptc_mode_available(new_mode)) > > + return -ENODEV; > > + > > + mutex_lock(&dptc->lock); > > + dptc->active_mode = new_mode; > > + /* Clear staged values: limits changed, old values may be out of range */ > > + memset(dptc->has_staged, 0, sizeof(dptc->has_staged)); > > + mutex_unlock(&dptc->lock); > > + > > + return count; > > +} > > + > > +static ssize_t dptc_mode_possible_values_show(struct kobject *kobj, > > + struct kobj_attribute *attr, > > + char *buf) > > +{ > > + char tmp[64]; > > + char *p = tmp; > > + bool first = true; > > + int m; > > + > > + for (m = 0; m <= (int)dptc->max_mode; m++) { > > + if (!dptc_mode_available(m)) > > + continue; > > + if (!first) > > + *p++ = ';'; > > + first = false; > > + p += snprintf(p, tmp + sizeof(tmp) - p, "%s", mode_names[m]); > > + } > > + *p = '\0'; > > + > > + return sysfs_emit(buf, "%s\n", tmp); > > +} > > + > > +static ssize_t dptc_mode_default_value_show(struct kobject *kobj, > > + struct kobj_attribute *attr, > > + char *buf) > > +{ > > + return sysfs_emit(buf, "device\n"); > > +} > > + > > +static ssize_t dptc_mode_display_name_show(struct kobject *kobj, > > + struct kobj_attribute *attr, > > + char *buf) > > +{ > > + return sysfs_emit(buf, "TDP Limit Mode\n"); > > +} > > + > > +static ssize_t dptc_mode_type_show(struct kobject *kobj, > > + struct kobj_attribute *attr, char *buf) > > +{ > > + return sysfs_emit(buf, "enumeration\n"); > > +} > > + > > +/* ========================================================================= > > + * Sysfs setup > > + * ========================================================================= > > + */ > > + > > +static void dptc_setup_param_sysfs(struct dptc_param_sysfs *ps, int idx) > > +{ > > + 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_mode_sysfs(struct dptc_mode_sysfs *ms) > > +{ > > + sysfs_attr_init(&ms->current_value.attr); > > + ms->current_value.attr.name = "current_value"; > > + ms->current_value.attr.mode = 0644; > > + ms->current_value.show = dptc_mode_current_value_show; > > + ms->current_value.store = dptc_mode_current_value_store; > > + > > + sysfs_attr_init(&ms->possible_values.attr); > > + ms->possible_values.attr.name = "possible_values"; > > + ms->possible_values.attr.mode = 0444; > > + ms->possible_values.show = dptc_mode_possible_values_show; > > + > > + sysfs_attr_init(&ms->default_value.attr); > > + ms->default_value.attr.name = "default_value"; > > + ms->default_value.attr.mode = 0444; > > + ms->default_value.show = dptc_mode_default_value_show; > > + > > + sysfs_attr_init(&ms->display_name.attr); > > + ms->display_name.attr.name = "display_name"; > > + ms->display_name.attr.mode = 0444; > > + ms->display_name.show = dptc_mode_display_name_show; > > + > > + sysfs_attr_init(&ms->type.attr); > > + ms->type.attr.name = "type"; > > + ms->type.attr.mode = 0444; > > + ms->type.show = dptc_mode_type_show; > > + > > + ms->attrs[0] = &ms->current_value.attr; > > + ms->attrs[1] = &ms->possible_values.attr; > > + ms->attrs[2] = &ms->default_value.attr; > > + ms->attrs[3] = &ms->display_name.attr; > > + ms->attrs[4] = &ms->type.attr; > > + ms->attrs[5] = NULL; > > + > > + ms->group.name = "limit_mode"; > > + ms->group.attrs = ms->attrs; > > +} > > + > > +/* ========================================================================= > > + * PM notifier - re-apply staged values after resume > > + * ========================================================================= > > + */ > > + > > +static int dptc_pm_notify(struct notifier_block *nb, unsigned long action, > > + void *data) > > +{ > > + if ((action == PM_POST_SUSPEND || action == PM_POST_HIBERNATION) && > > + dptc->commit_mode == COMMIT_AUTO) { > > + mutex_lock(&dptc->lock); > > + dptc_alib_commit(); > > + mutex_unlock(&dptc->lock); > > + } > > + return NOTIFY_OK; > > +} > > + > > +static struct notifier_block dptc_pm_nb = { > > + .notifier_call = dptc_pm_notify, > > +}; > > Why is this using notification blocks? Usually those are for very > specific purposes that things need to run at a certain time. > > It can't run as part of regular device suspend/resume routines? If we add a platform driver, but see below. This driver does not host a device that can have the ops currently. > > + > > +/* ========================================================================= > > + * Module init / exit > > + * ========================================================================= > > + */ > > + > > +static int __init dptc_init(void) > > Shouldn't this be a platform driver with a probe routine? If we create a platform device, yes, and that allows for normal ops. But that device would hook into nothing and be empty. There is no ACPI / WMI device to hook into. init would still do the bulk of checks. I can re-review this approach but I did consider it. > > +{ > > + const struct dptc_soc_entry *soc_entry = NULL; > > + const struct dmi_system_id *dmi_entry; > > + enum dptc_limit_mode max_mode; > > + int i, ret; > > + > > + /* Check ALIB ACPI method presence */ > > + if (!acpi_has_method(NULL, ALIB_PATH)) { > > + pr_debug(DRIVER_NAME ": ALIB ACPI method not present\n"); > > + return -ENODEV; > > + } > > + > > + /* Parse limit_mode module parameter */ > > + if (!strcmp(limit_mode, "device")) { > > + max_mode = LIMIT_DEVICE; > > + } else if (!strcmp(limit_mode, "expanded")) { > > + max_mode = LIMIT_EXPANDED; > > + } else if (!strcmp(limit_mode, "soc")) { > > + max_mode = LIMIT_SOC; > > + } else if (!strcmp(limit_mode, "unbound")) { > > + max_mode = LIMIT_UNBOUND; > > + } else { > > + pr_err(DRIVER_NAME ": unknown limit_mode '%s'\n", limit_mode); > > + return -EINVAL; > > + } > > +#ifndef CONFIG_AMD_DPTC_EXTENDED > > + if (max_mode > LIMIT_EXPANDED) { > > + pr_err(DRIVER_NAME ": limit_mode '%s' requires CONFIG_AMD_DPTC_EXTENDED\n", > > + limit_mode); > > + return -EINVAL; > > + } > > +#endif > > + > > + /* SoC match - required for device/expanded/soc, optional for unbound */ > > + for (i = 0; dptc_soc_table[i].cpu_id; i++) { > > + if (strstr(boot_cpu_data.x86_model_id, dptc_soc_table[i].cpu_id)) { > > + soc_entry = &dptc_soc_table[i]; > > + break; > > + } > > + } > > + if (!soc_entry && max_mode < LIMIT_UNBOUND) { > > + pr_debug(DRIVER_NAME ": unrecognized SoC '%s'\n", > > + boot_cpu_data.x86_model_id); > > + return -ENODEV; > > + } > > + > > + /* Optional device DMI match */ > > + dmi_entry = dmi_first_match(dptc_dmi_table); > > IMV - NO WAY. > > Device matches are mandatory. I'm not letting a driver like this bind > to any random piece of hardware in the wild. The thermal design of each > system is different. Each system needs it's own quirks/table. I need to update the comments but this is gated behind the kconfig flag. In order for SoC/unbound to become available that needs to be on. If it is not, as you see below the driver ejects. > > + > > + /* > > + * device and expanded modes require a DMI match; refuse to load if > > + * the user requested one of those tiers but the device is unknown. > > + */ > > + if (max_mode <= LIMIT_EXPANDED && !dmi_entry) { > > + pr_debug(DRIVER_NAME > > + ": limit_mode='%s' requires a device DMI match\n", > > + limit_mode); > > + return -ENODEV; > > + } > > + > > + dptc = kzalloc(sizeof(*dptc), GFP_KERNEL); > > + if (!dptc) > > + return -ENOMEM; > > + > > + mutex_init(&dptc->lock); > > + dptc->soc_limits = soc_entry ? soc_entry->limits : NULL; > > + dptc->max_mode = max_mode; > > + if (dmi_entry) > > + dptc->dev_limits = dmi_entry->driver_data; > > + > > + if (dptc->dev_limits) > > + dptc->active_mode = LIMIT_DEVICE; > > + else if (dptc->soc_limits) > > + dptc->active_mode = LIMIT_SOC; > > + else > > + dptc->active_mode = LIMIT_UNBOUND; > > + > > + /* ---- Probe logging ---- */ > > + if (soc_entry) > > + pr_info(DRIVER_NAME ": SoC: %s\n", soc_entry->cpu_id); > > + else > > + pr_info(DRIVER_NAME ": SoC unrecognized ('%s'), running unbound\n", > > + boot_cpu_data.x86_model_id); > > + > > + if (dmi_entry) { > > + pr_info(DRIVER_NAME ": Device: %s\n", dmi_entry->ident); > > + pr_info(DRIVER_NAME ": Device limits (device mode):\n"); > > + for (i = 0; i < DPTC_NUM_PARAMS; i++) { > > + pr_info(DRIVER_NAME ": %-16s smin=%-8u smax=%-8u def=%u\n", > > + dptc_params[i].name, > > + dptc->dev_limits->p[i].smin, > > + dptc->dev_limits->p[i].smax, > > + dptc->dev_limits->p[i].def); > > + } > > + if (max_mode >= LIMIT_EXPANDED) { > > + pr_info(DRIVER_NAME ": Device limits (expanded mode):\n"); > > + for (i = 0; i < DPTC_NUM_PARAMS; i++) { > > + pr_info(DRIVER_NAME ": %-16s min=%-8u max=%u\n", > > + dptc_params[i].name, > > + dptc->dev_limits->p[i].min, > > + dptc->dev_limits->p[i].max); > > + } > > + } > > + } else { > > + pr_info(DRIVER_NAME ": No device DMI match\n"); > > + } > > + > > + if (max_mode >= LIMIT_SOC && dptc->soc_limits) { > > + pr_info(DRIVER_NAME ": SoC limits:\n"); > > + for (i = 0; i < DPTC_NUM_PARAMS; i++) { > > + pr_info(DRIVER_NAME ": %-16s max=%u\n", > > + dptc_params[i].name, > > + dptc->soc_limits->p[i].max); > > + } > > + } > > + > > + pr_info(DRIVER_NAME ": active_mode=%s max_mode=%s\n", > > + mode_names[dptc->active_mode], mode_names[dptc->max_mode]); > > + > > + /* ---- Firmware-attributes class device ---- */ > > This seems like it doesn't use any of the firmware attributes helpers > that exist. A lot of wheel re-inventing below. Those were merged recently if they exist. I will need to review. This driver is based on the samsung laptop one that was first merged so it probably still does not have them. Noted. > > + dptc->fw_attr_dev = device_create(&firmware_attributes_class, > > + NULL, MKDEV(0, 0), NULL, > > + DRIVER_NAME); > > + if (IS_ERR(dptc->fw_attr_dev)) { > > + ret = PTR_ERR(dptc->fw_attr_dev); > > + goto err_free; > > + } > > + > > + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL, > > + &dptc->fw_attr_dev->kobj); > > + if (!dptc->fw_attr_kset) { > > + ret = -ENOMEM; > > + goto err_dev; > > + } > > + > > + for (i = 0; i < DPTC_NUM_PARAMS; i++) { > > + dptc_setup_param_sysfs(&dptc->params[i], i); > > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj, > > + &dptc->params[i].group); > > + if (ret) { > > + while (--i >= 0) > > + sysfs_remove_group(&dptc->fw_attr_kset->kobj, > > + &dptc->params[i].group); > > + goto err_kset; > > + } > > + } > > + > > + dptc_setup_mode_sysfs(&dptc->mode_attr); > > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj, > > + &dptc->mode_attr.group); > > + if (ret) { > > + for (i = 0; i < DPTC_NUM_PARAMS; i++) > > + sysfs_remove_group(&dptc->fw_attr_kset->kobj, > > + &dptc->params[i].group); > > + goto err_kset; > > + } > > + > > + 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) { > > + sysfs_remove_group(&dptc->fw_attr_kset->kobj, &dptc->mode_attr.group); > > + for (i = 0; i < DPTC_NUM_PARAMS; i++) > > + sysfs_remove_group(&dptc->fw_attr_kset->kobj, > > + &dptc->params[i].group); > > + goto err_kset; > > + } > > + > > + register_pm_notifier(&dptc_pm_nb); > > + pr_info(DRIVER_NAME ": loaded\n"); > > This function is INCREDIBLY noisy. Why so many pr_info()? At most it > should be one for probe, but probably none. > > > + return 0; > > + > > +err_kset: > > + kset_unregister(dptc->fw_attr_kset); > > +err_dev: > > + device_destroy(&firmware_attributes_class, MKDEV(0, 0)); > > +err_free: > > + mutex_destroy(&dptc->lock); > > + kfree(dptc); > > + dptc = NULL; > > + return ret; > > Looks like a lot of code duplication with dptc_exit(). If you drop the > boilerplate and use the right helpers you probably can reduce the > majority of it. Noted. Antheas > > +} > > + > > +static void __exit dptc_exit(void) > > +{ > > + int i; > > + > > + unregister_pm_notifier(&dptc_pm_nb); > > + sysfs_remove_file(&dptc->fw_attr_kset->kobj, &dptc->save_settings_attr.attr); > > + sysfs_remove_group(&dptc->fw_attr_kset->kobj, &dptc->mode_attr.group); > > + for (i = 0; i < DPTC_NUM_PARAMS; i++) > > + sysfs_remove_group(&dptc->fw_attr_kset->kobj, > > + &dptc->params[i].group); > > + kset_unregister(dptc->fw_attr_kset); > > + device_destroy(&firmware_attributes_class, MKDEV(0, 0)); > > + mutex_destroy(&dptc->lock); > > + kfree(dptc); > > + dptc = NULL; > > +} > > + > > +module_init(dptc_init); > > +module_exit(dptc_exit); > > ^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC v1 2/2] platform/x86/amd: Add AMD DPTCi driver 2026-03-03 20:40 ` Antheas Kapenekakis @ 2026-03-03 20:54 ` Mario Limonciello (AMD) (kernel.org) 2026-03-03 21:20 ` Antheas Kapenekakis 2026-03-03 21:44 ` Sasha Levin 0 siblings, 2 replies; 18+ messages in thread From: Mario Limonciello (AMD) (kernel.org) @ 2026-03-03 20:54 UTC (permalink / raw) To: Antheas Kapenekakis; +Cc: linux-kernel, platform-driver-x86, Sasha Levin + Sasha On 3/3/2026 2:40 PM, Antheas Kapenekakis wrote: > On Tue, 3 Mar 2026 at 21:10, Mario Limonciello (AMD) (kernel.org) > <superm1@kernel.org> wrote: >> >> I'll preface this by saying - I don't have a problem with using an AI to >> help write a driver, but please disclose that it was done and that in >> this case even you haven't closely audited the results. >> >> I personally would never submit something generated by an LLM that I >> didn't audit and add a S-o-b tag to it (asserting I am willing to stand >> by the code). >> >> I'm glad that I found out it was AI written before I started to review >> the code, I would have had a lot more candid comments for you. >> >> There is a lot of weird stuff in this driver that I'm not going to >> comment on and nitpick, but I'll leave a few broad strokes things. > > Of course, to that end, feel free to skip a full review until I get to > properly rewriting it. > > What is the current stance on Co-bys for that? I'm trying to follow > the discussion but I missed the news. I can lint it properly next > time. > > From my perspective, pouring a month into a driver like this without > having a firm commitment that it will go somewhere is a bit hard to > stomach. Sure. I don't need a dedicated tag telling me what tool you wrote it with. I don't care if it was Opus, Gemini or Qwen3.5. They all can make mistakes that need to be audited. The most important part to me (or any reviewer) is a signal that I shouldn't invest more effort reviewing this than you did writing it and reviewing it. My feeling on this kind of RFC this is the most appropriate tag: Not-signed-off-by: Foo Bar <foo@bar.com> > This driver does not have a concept of platform profile. The devices > by definition do not have presets and users of handhelds are > accustomed to a ppt slider (where userspace interpolates for fppt/sppt > or sets them the same). > > We could layer a platform profile on top of it by extending the driver > more and adding suggested preselected profiles for low-power, > balanced, and performance. Then custom unlocks the sliders. This is > the approach I do currently when I hook to the ppd dbus protocol and > it works quite well. > > As for custom profile, it unlocks the firmware attributes in other > drivers. The only difference in this one is the naming of the > attributes. I feel for this to be viable in mainline kernel it should be easy to use by default with platform profile support. >> IMV - NO WAY. >> >> Device matches are mandatory. I'm not letting a driver like this bind >> to any random piece of hardware in the wild. The thermal design of each >> system is different. Each system needs it's own quirks/table. > > I need to update the comments but this is gated behind the kconfig > flag. In order for SoC/unbound to become available that needs to be > on. If it is not, as you see below the driver ejects. I don't even want a Kconfig flag to allow it to bind to something outside of the quirk list for any of this. if we don't have authoritative values to use we shouldn't have anything binding. If you need to add support for a new system then add a quirk for it. ^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC v1 2/2] platform/x86/amd: Add AMD DPTCi driver 2026-03-03 20:54 ` Mario Limonciello (AMD) (kernel.org) @ 2026-03-03 21:20 ` Antheas Kapenekakis 2026-03-03 21:44 ` Sasha Levin 1 sibling, 0 replies; 18+ messages in thread From: Antheas Kapenekakis @ 2026-03-03 21:20 UTC (permalink / raw) To: Mario Limonciello (AMD) (kernel.org) Cc: linux-kernel, platform-driver-x86, Sasha Levin On Tue, 3 Mar 2026 at 21:55, Mario Limonciello (AMD) (kernel.org) <superm1@kernel.org> wrote: > > + Sasha > > On 3/3/2026 2:40 PM, Antheas Kapenekakis wrote: > > On Tue, 3 Mar 2026 at 21:10, Mario Limonciello (AMD) (kernel.org) > > <superm1@kernel.org> wrote: > >> > >> I'll preface this by saying - I don't have a problem with using an AI to > >> help write a driver, but please disclose that it was done and that in > >> this case even you haven't closely audited the results. > >> > >> I personally would never submit something generated by an LLM that I > >> didn't audit and add a S-o-b tag to it (asserting I am willing to stand > >> by the code). > >> > >> I'm glad that I found out it was AI written before I started to review > >> the code, I would have had a lot more candid comments for you. > >> > >> There is a lot of weird stuff in this driver that I'm not going to > >> comment on and nitpick, but I'll leave a few broad strokes things. > > > > Of course, to that end, feel free to skip a full review until I get to > > properly rewriting it. > > > > What is the current stance on Co-bys for that? I'm trying to follow > > the discussion but I missed the news. I can lint it properly next > > time. > > > > From my perspective, pouring a month into a driver like this without > > having a firm commitment that it will go somewhere is a bit hard to > > stomach. > > Sure. > > I don't need a dedicated tag telling me what tool you wrote it with. I > don't care if it was Opus, Gemini or Qwen3.5. They all can make > mistakes that need to be audited. > > The most important part to me (or any reviewer) is a signal that I > shouldn't invest more effort reviewing this than you did writing it and > reviewing it. > > My feeling on this kind of RFC this is the most appropriate tag: > > Not-signed-off-by: Foo Bar <foo@bar.com> I did test the driver and go through it multiple times for what it counts, including fixing all of the checkpatch warnings. > > This driver does not have a concept of platform profile. The devices > > by definition do not have presets and users of handhelds are > > accustomed to a ppt slider (where userspace interpolates for fppt/sppt > > or sets them the same). > > > > We could layer a platform profile on top of it by extending the driver > > more and adding suggested preselected profiles for low-power, > > balanced, and performance. Then custom unlocks the sliders. This is > > the approach I do currently when I hook to the ppd dbus protocol and > > it works quite well. > > > > As for custom profile, it unlocks the firmware attributes in other > > drivers. The only difference in this one is the naming of the > > attributes. > > I feel for this to be viable in mainline kernel it should be easy to use > by default with platform profile support. Fair, I can look into integrating this. I already have preset values from my userspace implementation. In that case, I'd match the Legion ABI 1-1 including the names, perhaps drop the time params but keep tctl - I think lenovo-other is proposing a name for this, unsure if that merged. Although I am not a fan of the macro soup in those drivers, which is why this is based on the samsung driver. > >> IMV - NO WAY. > >> > >> Device matches are mandatory. I'm not letting a driver like this bind > >> to any random piece of hardware in the wild. The thermal design of each > >> system is different. Each system needs it's own quirks/table. > > > > I need to update the comments but this is gated behind the kconfig > > flag. In order for SoC/unbound to become available that needs to be > > on. If it is not, as you see below the driver ejects. > > I don't even want a Kconfig flag to allow it to bind to something > outside of the quirk list for any of this. if we don't have > authoritative values to use we shouldn't have anything binding. > > If you need to add support for a new system then add a quirk for it. > This driver does not bind to anything or autoload unless there is a DMI match. It is impossible to autoload actually, there is no backing device that can load via rules other than DMI match. The kconfig flag just allows to force load the driver with no max values after recompiling on devices that feature the method. I'd say it's important for niche cases such as devices with an EC like the Ally and debugging (such as unlocking the limits on devices that match). I'd rather keep functionality like this on mainline so that I don't need to carry a patch for it. I plan to add quirks for new devices. In any case, by default the exposed limits for devices are always based on their DMI match, even if Y is selected (the default mode is always "device"). "expanded" requires a command line arg if EXPANDED is N, which allows for a very soft increase. And EXPANDED unlocks the modes, as I am trying to eliminate cases where modifying the kernel command line is required, as modifying the kernel command line is an anti-pattern I am trying to eliminate. Antheas ^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC v1 2/2] platform/x86/amd: Add AMD DPTCi driver 2026-03-03 20:54 ` Mario Limonciello (AMD) (kernel.org) 2026-03-03 21:20 ` Antheas Kapenekakis @ 2026-03-03 21:44 ` Sasha Levin 2026-03-03 22:08 ` Antheas Kapenekakis 1 sibling, 1 reply; 18+ messages in thread From: Sasha Levin @ 2026-03-03 21:44 UTC (permalink / raw) To: Mario Limonciello (AMD) (kernel.org) Cc: Antheas Kapenekakis, linux-kernel, platform-driver-x86 On Tue, Mar 03, 2026 at 02:54:59PM -0600, Mario Limonciello (AMD) (kernel.org) wrote: >+ Sasha > >On 3/3/2026 2:40 PM, Antheas Kapenekakis wrote: >>On Tue, 3 Mar 2026 at 21:10, Mario Limonciello (AMD) (kernel.org) >><superm1@kernel.org> wrote: >>> >>>I'll preface this by saying - I don't have a problem with using an AI to >>>help write a driver, but please disclose that it was done and that in >>>this case even you haven't closely audited the results. >>> >>>I personally would never submit something generated by an LLM that I >>>didn't audit and add a S-o-b tag to it (asserting I am willing to stand >>>by the code). >>> >>>I'm glad that I found out it was AI written before I started to review >>>the code, I would have had a lot more candid comments for you. >>> >>>There is a lot of weird stuff in this driver that I'm not going to >>>comment on and nitpick, but I'll leave a few broad strokes things. >> >>Of course, to that end, feel free to skip a full review until I get to >>properly rewriting it. >> >>What is the current stance on Co-bys for that? I'm trying to follow >>the discussion but I missed the news. I can lint it properly next >>time. >> >> From my perspective, pouring a month into a driver like this without >>having a firm commitment that it will go somewhere is a bit hard to >>stomach. > >Sure. > >I don't need a dedicated tag telling me what tool you wrote it with. >I don't care if it was Opus, Gemini or Qwen3.5. They all can make >mistakes that need to be audited. > >The most important part to me (or any reviewer) is a signal that I >shouldn't invest more effort reviewing this than you did writing it >and reviewing it. > >My feeling on this kind of RFC this is the most appropriate tag: > >Not-signed-off-by: Foo Bar <foo@bar.com> We have docs for this now :) Both in the README file as well as Documentation/process/coding-assistants.rst . In particular, you should be upfront about using AI to generate code, using the Assisted-by: tag, and definitely giving it a very careful review making sure you completely understand what the code does. As the README says: The human submitter is responsible for: * Reviewing all AI-generated code * Ensuring compliance with licensing requirements * Adding their own Signed-off-by tag to certify the DCO * Taking full responsibility for the contribution -- Thanks, Sasha ^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC v1 2/2] platform/x86/amd: Add AMD DPTCi driver 2026-03-03 21:44 ` Sasha Levin @ 2026-03-03 22:08 ` Antheas Kapenekakis 0 siblings, 0 replies; 18+ messages in thread From: Antheas Kapenekakis @ 2026-03-03 22:08 UTC (permalink / raw) To: Sasha Levin Cc: Mario Limonciello (AMD) (kernel.org), linux-kernel, platform-driver-x86 On Tue, 3 Mar 2026 at 22:45, Sasha Levin <sashal@kernel.org> wrote: > > On Tue, Mar 03, 2026 at 02:54:59PM -0600, Mario Limonciello (AMD) (kernel.org) wrote: > >+ Sasha > > > >On 3/3/2026 2:40 PM, Antheas Kapenekakis wrote: > >>On Tue, 3 Mar 2026 at 21:10, Mario Limonciello (AMD) (kernel.org) > >><superm1@kernel.org> wrote: > >>> > >>>I'll preface this by saying - I don't have a problem with using an AI to > >>>help write a driver, but please disclose that it was done and that in > >>>this case even you haven't closely audited the results. > >>> > >>>I personally would never submit something generated by an LLM that I > >>>didn't audit and add a S-o-b tag to it (asserting I am willing to stand > >>>by the code). > >>> > >>>I'm glad that I found out it was AI written before I started to review > >>>the code, I would have had a lot more candid comments for you. > >>> > >>>There is a lot of weird stuff in this driver that I'm not going to > >>>comment on and nitpick, but I'll leave a few broad strokes things. > >> > >>Of course, to that end, feel free to skip a full review until I get to > >>properly rewriting it. > >> > >>What is the current stance on Co-bys for that? I'm trying to follow > >>the discussion but I missed the news. I can lint it properly next > >>time. > >> > >> From my perspective, pouring a month into a driver like this without > >>having a firm commitment that it will go somewhere is a bit hard to > >>stomach. > > > >Sure. > > > >I don't need a dedicated tag telling me what tool you wrote it with. > >I don't care if it was Opus, Gemini or Qwen3.5. They all can make > >mistakes that need to be audited. > > > >The most important part to me (or any reviewer) is a signal that I > >shouldn't invest more effort reviewing this than you did writing it > >and reviewing it. > > > >My feeling on this kind of RFC this is the most appropriate tag: > > > >Not-signed-off-by: Foo Bar <foo@bar.com> > > We have docs for this now :) Both in the README file as well as > Documentation/process/coding-assistants.rst . > > In particular, you should be upfront about using AI to generate code, using the > Assisted-by: tag, and definitely giving it a very careful review making sure > you completely understand what the code does. > > As the README says: > > The human submitter is responsible for: > > * Reviewing all AI-generated code > * Ensuring compliance with licensing requirements > * Adding their own Signed-off-by tag to certify the DCO > * Taking full responsibility for the contribution Ok, I will add the missing tag then :) Assisted-by: Claude:claude-4.6-opus I do stand by the code, as far as a V1 RFC is concerned at least. The driver uses basic primitives, has correct functionality, and I reviewed the code. Licensing is clean too. Comments need tweaking, but that's always the case for a V1. I would spend some TLC before I deploy it making sure it loads and unloads correctly and there are no edge cases, but for that to happen, I need a stable ABI otherwise my userspace will drift, and that's no different than not using AI assistance. Speaking of Nvidia and vibing, here is a complete OOT kernel driver written by an LLM [1] that enables full TDP controls and hwmon monitoring on a DGX spark using the Steam Deck ABI and its Mediatek chipset. Sasha, you might be interested in it. Claude decoded the DSDT tables, wrote the driver, and tested it. It wrote the readme too. Yeah, sure its ABI choices were not the best, but that's nothing some prompting can't fix. To be fair, it did not do everything, I tab aligned the constants and tweaked the readme and some of the comments :) Things are evolving rapidly to say the least. Antheas [1] https://github.com/antheas/spark_hwmon > -- > Thanks, > Sasha > ^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC v1 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls 2026-03-03 18:17 [RFC v1 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Antheas Kapenekakis 2026-03-03 18:17 ` [RFC v1 1/2] Documentation: firmware-attributes: generalize save_settings entry Antheas Kapenekakis 2026-03-03 18:17 ` [RFC v1 2/2] platform/x86/amd: Add AMD DPTCi driver Antheas Kapenekakis @ 2026-03-03 18:59 ` Mario Limonciello 2026-03-03 19:16 ` Antheas Kapenekakis 2 siblings, 1 reply; 18+ messages in thread From: Mario Limonciello @ 2026-03-03 18:59 UTC (permalink / raw) To: Antheas Kapenekakis; +Cc: linux-kernel, platform-driver-x86 A high level question - why aren't these vendors implementing PMF? It's 1000% less work to enable PMF. All the values that match the design get stored in BIOS, driver pulls the information and uses it. Same approach for Windows and Linux. More comments below. On 3/3/26 12:17 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: > STAPM limit, fast/slow PPT limits, skin-temperature TDP limit, slow/STAPM > time constants, and the thermal control target. > > Until now userspace has reached this interface through the acpi_call out- > of-tree module or ryzenadj, which carry no ABI guarantees and no per-device > safety limits. This driver replaces that with a proper in-kernel > implementation that: > > * Exposes all seven parameters through the firmware-attributes sysfs ABI, > so that standard tools (fwupd, systemd-bios-vendor, etc.) can enumerate What is systemd-bios-vendor? I guess I'm not familiar with this and a quick web search didn't turn anything obvious up. > and modify them without device-specific knowledge. > > * Enforces tiered per-device and per-SoC limits. The default "device" > mode restricts writes to a curated safe range (smin..smax) derived from > the device's thermal design. Can you please elaborate where you got all these numbers from? I don't know if they're accurate or not. Someone would probably need to cross reference them to be sure. > An "expanded" mode exposes the full > hardware-validated range. An optional CONFIG_AMD_DPTC_EXTENDED Kconfig > adds "soc" (raw ALIB_PARAMS envelope) and "unbound" tiers for advanced > use. The active tier is itself a firmware-attribute, switchable at > runtime. > > * Stages values and commits them atomically in a single ALIB call, > matching the protocol's intended bulk-update semantics. A save_settings > attribute (per firmware-attributes ABI) controls whether writes commit > immediately ("single" mode) or are held until an explicit "save". > > * When in "single" mode, re-applies staged values after system resume, > so suspend/resume cycles do not silently revert to firmware defaults. This isn't the only interface for setting power limits. How do you make sure that the EC for example isn't stepping on toes on these designs? I /guess/ it always will need to be opt-in a device by device basis. What happens if the vendor enables PMF in a BIOS update? How does this avoid conflicts? > > 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, Z1, 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. > > @Mario: can you suggest a CC list for V2? Thanks. Even if not merged, this > driver is still good for downstream use. You should include Shyam (AMD), Denis and Derek (community). > > --- > Usage > ----- > > List all exposed attributes (read-only, no root required): > > $ fwupdmgr get-bios-settings > > This enumerates every attribute under /sys/class/firmware-attributes/, > including current_value, default_value, min_value, max_value, and > display_name for each DPTCi parameter. AFAIK - fwupd doesn't understand "save_settings" today > > Sysfs direct usage > ------------------ > > All paths are under: > > ATTR=/sys/class/firmware-attributes/amd_dptc/attributes > > Inspect a parameter (no root needed): > > $ cat $ATTR/stapm_limit/{display_name,min_value,max_value,default_value,current_value} > Sustained TDP (mW) > 4000 > 85000 > 25000 > <- empty: nothing staged yet > > Stage values (held in memory, not yet sent to firmware): > > $ echo 25000 | sudo tee $ATTR/stapm_limit/current_value > $ echo 40000 | sudo tee $ATTR/fast_limit/current_value > $ echo 27000 | sudo tee $ATTR/slow_limit/current_value > $ echo 25000 | sudo tee $ATTR/skin_limit/current_value > $ echo 85 | sudo tee $ATTR/temp_target/current_value > > Commit all staged values in one ALIB call: > > $ echo save | sudo tee $ATTR/save_settings > > Switch to auto-commit (each write commits immediately): > > $ echo single | sudo tee $ATTR/save_settings > > Return to bulk mode: > > $ echo bulk | sudo tee $ATTR/save_settings > > Clear a staged value without committing: > > $ echo | sudo tee $ATTR/stapm_limit/current_value > > Query or change the active limit tier (device/expanded/soc/unbound): > > $ cat $ATTR/limit_mode/possible_values > device;expanded;soc;unbound > $ echo expanded | sudo tee $ATTR/limit_mode/current_value > > Switching tiers clears all staged values (old values may fall outside the > new range). Stages and commits must be redone after a mode switch. > > Antheas Kapenekakis (2): > Documentation: firmware-attributes: generalize save_settings entry > platform/x86/amd: Add AMD DPTCi driver > > .../testing/sysfs-class-firmware-attributes | 41 +- > MAINTAINERS | 6 + > drivers/platform/x86/amd/Kconfig | 27 + > drivers/platform/x86/amd/Makefile | 2 + > drivers/platform/x86/amd/dptc.c | 1325 +++++++++++++++++ > 5 files changed, 1386 insertions(+), 15 deletions(-) > create mode 100644 drivers/platform/x86/amd/dptc.c > > > base-commit: c89ce241c1909d2c2bdde88334c33f3000d364fb ^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC v1 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls 2026-03-03 18:59 ` [RFC v1 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Mario Limonciello @ 2026-03-03 19:16 ` Antheas Kapenekakis 2026-03-03 19:23 ` Antheas Kapenekakis ` (2 more replies) 0 siblings, 3 replies; 18+ messages in thread From: Antheas Kapenekakis @ 2026-03-03 19:16 UTC (permalink / raw) To: Mario Limonciello; +Cc: linux-kernel, platform-driver-x86 On Tue, 3 Mar 2026 at 19:59, Mario Limonciello <superm1@kernel.org> wrote: > > A high level question - why aren't these vendors implementing PMF? It's > 1000% less work to enable PMF. All the values that match the design get > stored in BIOS, driver pulls the information and uses it. From my understanding they do not implement anything and just use ryzenadj with their windows vendor software. > Same approach for Windows and Linux. > > More comments below. > > On 3/3/26 12:17 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: > > STAPM limit, fast/slow PPT limits, skin-temperature TDP limit, slow/STAPM > > time constants, and the thermal control target. > > > > Until now userspace has reached this interface through the acpi_call out- > > of-tree module or ryzenadj, which carry no ABI guarantees and no per-device > > safety limits. This driver replaces that with a proper in-kernel > > implementation that: > > > > * Exposes all seven parameters through the firmware-attributes sysfs ABI, > > so that standard tools (fwupd, systemd-bios-vendor, etc.) can enumerate > > What is systemd-bios-vendor? I guess I'm not familiar with this and a > quick web search didn't turn anything obvious up. I used some AI assistance to compile this from my userspace implementation and the ASEGA pdf from the AMD site. I need to go through _everything_ before this moves to non-RFC. Same with copyright year. I focused on the implementation doing the things I want it to do for now. > > and modify them without device-specific knowledge. > > > > * Enforces tiered per-device and per-SoC limits. The default "device" > > mode restricts writes to a curated safe range (smin..smax) derived from > > the device's thermal design. > > Can you please elaborate where you got all these numbers from? I don't > know if they're accurate or not. Someone would probably need to cross > reference them to be sure. Trial and error, research, references from Windows, etc. All of the devices in this driver have been tested with a userspace implementation using the same limits for ppt/sppt/fppt. Nobody has complained about them. To be honest, I usually do not set tctl slow and fast time limits, so those are referenced from the Legion Go and for tctl I go lower than what manufacturers usually set. Users like tctl because some of them like their device to stay cooler. The big idea for this driver is to allow locking /dev/mem and ACPI. Other than the legion go fan curves, and the Zotac Zone driver which needs a cleanup, everything else is handled in the kernel now. But this approach works fine for the Zotac Zone as they seem to be using a very simple WMI shim. > > An "expanded" mode exposes the full > > hardware-validated range. An optional CONFIG_AMD_DPTC_EXTENDED Kconfig > > adds "soc" (raw ALIB_PARAMS envelope) and "unbound" tiers for advanced > > use. The active tier is itself a firmware-attribute, switchable at > > runtime. > > > > * Stages values and commits them atomically in a single ALIB call, > > matching the protocol's intended bulk-update semantics. A save_settings > > attribute (per firmware-attributes ABI) controls whether writes commit > > immediately ("single" mode) or are held until an explicit "save". > > > > * When in "single" mode, re-applies staged values after system resume, > > so suspend/resume cycles do not silently revert to firmware defaults. > > This isn't the only interface for setting power limits. How do you make > sure that the EC for example isn't stepping on toes on these designs? For the DMI matched devices it is not. For the ones that are not matched, the driver does not autoload and needs a kconfig parameter to even load. OneXPlayer is a bit more complex with their turbo button doing TDP swaps but turbo takeover in oxpec takes care of that and then you are supposed use ryzenadj. > I /guess/ it always will need to be opt-in a device by device basis. > > What happens if the vendor enables PMF in a BIOS update? How does this > avoid conflicts? Some manufacturers enable pmf without implementing the tables. From my understanding none of them implement pmf with limits. If a manufacturer wants to move to pmf, we can amend the DMI entry with a bios match. However, from my understanding, there is no TDP slider equivalent for PMF. > > > > 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, Z1, 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. > > > > @Mario: can you suggest a CC list for V2? Thanks. Even if not merged, this > > driver is still good for downstream use. > > You should include Shyam (AMD), Denis and Derek (community). Sure. > > > > --- > > Usage > > ----- > > > > List all exposed attributes (read-only, no root required): > > > > $ fwupdmgr get-bios-settings > > > > This enumerates every attribute under /sys/class/firmware-attributes/, > > including current_value, default_value, min_value, max_value, and > > display_name for each DPTCi parameter. > > AFAIK - fwupd doesn't understand "save_settings" today Yes, I do not expect it to even allow writing, but at least you can preview the values which is useful. And from what I saw defaults are not shown either. Antheas > > > > Sysfs direct usage > > ------------------ > > > > All paths are under: > > > > ATTR=/sys/class/firmware-attributes/amd_dptc/attributes > > > > Inspect a parameter (no root needed): > > > > $ cat $ATTR/stapm_limit/{display_name,min_value,max_value,default_value,current_value} > > Sustained TDP (mW) > > 4000 > > 85000 > > 25000 > > <- empty: nothing staged yet > > > > Stage values (held in memory, not yet sent to firmware): > > > > $ echo 25000 | sudo tee $ATTR/stapm_limit/current_value > > $ echo 40000 | sudo tee $ATTR/fast_limit/current_value > > $ echo 27000 | sudo tee $ATTR/slow_limit/current_value > > $ echo 25000 | sudo tee $ATTR/skin_limit/current_value > > $ echo 85 | sudo tee $ATTR/temp_target/current_value > > > > Commit all staged values in one ALIB call: > > > > $ echo save | sudo tee $ATTR/save_settings > > > > Switch to auto-commit (each write commits immediately): > > > > $ echo single | sudo tee $ATTR/save_settings > > > > Return to bulk mode: > > > > $ echo bulk | sudo tee $ATTR/save_settings > > > > Clear a staged value without committing: > > > > $ echo | sudo tee $ATTR/stapm_limit/current_value > > > > Query or change the active limit tier (device/expanded/soc/unbound): > > > > $ cat $ATTR/limit_mode/possible_values > > device;expanded;soc;unbound > > $ echo expanded | sudo tee $ATTR/limit_mode/current_value > > > > Switching tiers clears all staged values (old values may fall outside the > > new range). Stages and commits must be redone after a mode switch. > > > > Antheas Kapenekakis (2): > > Documentation: firmware-attributes: generalize save_settings entry > > platform/x86/amd: Add AMD DPTCi driver > > > > .../testing/sysfs-class-firmware-attributes | 41 +- > > MAINTAINERS | 6 + > > drivers/platform/x86/amd/Kconfig | 27 + > > drivers/platform/x86/amd/Makefile | 2 + > > drivers/platform/x86/amd/dptc.c | 1325 +++++++++++++++++ > > 5 files changed, 1386 insertions(+), 15 deletions(-) > > create mode 100644 drivers/platform/x86/amd/dptc.c > > > > > > base-commit: c89ce241c1909d2c2bdde88334c33f3000d364fb > > ^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC v1 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls 2026-03-03 19:16 ` Antheas Kapenekakis @ 2026-03-03 19:23 ` Antheas Kapenekakis 2026-03-03 19:27 ` Armin Wolf 2026-03-03 19:51 ` Mario Limonciello (AMD) (kernel.org) 2 siblings, 0 replies; 18+ messages in thread From: Antheas Kapenekakis @ 2026-03-03 19:23 UTC (permalink / raw) To: Mario Limonciello; +Cc: linux-kernel, platform-driver-x86 On Tue, 3 Mar 2026 at 20:16, Antheas Kapenekakis <lkml@antheas.dev> wrote: > > On Tue, 3 Mar 2026 at 19:59, Mario Limonciello <superm1@kernel.org> wrote: > > > > A high level question - why aren't these vendors implementing PMF? It's > > 1000% less work to enable PMF. All the values that match the design get > > stored in BIOS, driver pulls the information and uses it. > > From my understanding they do not implement anything and just use > ryzenadj with their windows vendor software. > > > Same approach for Windows and Linux. > > > > More comments below. > > > > On 3/3/26 12:17 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: > > > STAPM limit, fast/slow PPT limits, skin-temperature TDP limit, slow/STAPM > > > time constants, and the thermal control target. > > > > > > Until now userspace has reached this interface through the acpi_call out- > > > of-tree module or ryzenadj, which carry no ABI guarantees and no per-device > > > safety limits. This driver replaces that with a proper in-kernel > > > implementation that: > > > > > > * Exposes all seven parameters through the firmware-attributes sysfs ABI, > > > so that standard tools (fwupd, systemd-bios-vendor, etc.) can enumerate > > > > What is systemd-bios-vendor? I guess I'm not familiar with this and a > > quick web search didn't turn anything obvious up. > > I used some AI assistance to compile this from my userspace > implementation and the ASEGA pdf from the AMD site. I need to go > through _everything_ before this moves to non-RFC. Same with > copyright year. I focused on the implementation doing the things I > want it to do for now. > > > > and modify them without device-specific knowledge. > > > > > > * Enforces tiered per-device and per-SoC limits. The default "device" > > > mode restricts writes to a curated safe range (smin..smax) derived from > > > the device's thermal design. > > > > Can you please elaborate where you got all these numbers from? I don't > > know if they're accurate or not. Someone would probably need to cross > > reference them to be sure. > > Trial and error, research, references from Windows, etc. All of the > devices in this driver have been tested with a userspace > implementation using the same limits for ppt/sppt/fppt. Nobody has > complained about them. To be honest, I usually do not set tctl slow > and fast time limits, so those are referenced from the Legion Go and > for tctl I go lower than what manufacturers usually set. Users like > tctl because some of them like their device to stay cooler. To be clear here, a userspace implementation using the same limits and the same interface [1]. This driver is a straight copy of that. [1] https://github.com/hhd-dev/hhd/blob/master/src/adjustor/core/const.py > The big idea for this driver is to allow locking /dev/mem and ACPI. > Other than the legion go fan curves, and the Zotac Zone driver which > needs a cleanup, everything else is handled in the kernel now. > > But this approach works fine for the Zotac Zone as they seem to be > using a very simple WMI shim. > > > > An "expanded" mode exposes the full > > > hardware-validated range. An optional CONFIG_AMD_DPTC_EXTENDED Kconfig > > > adds "soc" (raw ALIB_PARAMS envelope) and "unbound" tiers for advanced > > > use. The active tier is itself a firmware-attribute, switchable at > > > runtime. > > > > > > * Stages values and commits them atomically in a single ALIB call, > > > matching the protocol's intended bulk-update semantics. A save_settings > > > attribute (per firmware-attributes ABI) controls whether writes commit > > > immediately ("single" mode) or are held until an explicit "save". > > > > > > * When in "single" mode, re-applies staged values after system resume, > > > so suspend/resume cycles do not silently revert to firmware defaults. > > > > This isn't the only interface for setting power limits. How do you make > > sure that the EC for example isn't stepping on toes on these designs? > > For the DMI matched devices it is not. For the ones that are not > matched, the driver does not autoload and needs a kconfig parameter to > even load. > > OneXPlayer is a bit more complex with their turbo button doing TDP > swaps but turbo takeover in oxpec takes care of that and then you are > supposed use ryzenadj. > > > I /guess/ it always will need to be opt-in a device by device basis. > > > > What happens if the vendor enables PMF in a BIOS update? How does this > > avoid conflicts? > > Some manufacturers enable pmf without implementing the tables. From my > understanding none of them implement pmf with limits. If a > manufacturer wants to move to pmf, we can amend the DMI entry with a > bios match. However, from my understanding, there is no TDP slider > equivalent for PMF. > > > > > > > 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, Z1, 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. > > > > > > @Mario: can you suggest a CC list for V2? Thanks. Even if not merged, this > > > driver is still good for downstream use. > > > > You should include Shyam (AMD), Denis and Derek (community). > > Sure. > > > > > > > --- > > > Usage > > > ----- > > > > > > List all exposed attributes (read-only, no root required): > > > > > > $ fwupdmgr get-bios-settings > > > > > > This enumerates every attribute under /sys/class/firmware-attributes/, > > > including current_value, default_value, min_value, max_value, and > > > display_name for each DPTCi parameter. > > > > AFAIK - fwupd doesn't understand "save_settings" today > > Yes, I do not expect it to even allow writing, but at least you can > preview the values which is useful. And from what I saw defaults are > not shown either. > > Antheas > > > > > > > Sysfs direct usage > > > ------------------ > > > > > > All paths are under: > > > > > > ATTR=/sys/class/firmware-attributes/amd_dptc/attributes > > > > > > Inspect a parameter (no root needed): > > > > > > $ cat $ATTR/stapm_limit/{display_name,min_value,max_value,default_value,current_value} > > > Sustained TDP (mW) > > > 4000 > > > 85000 > > > 25000 > > > <- empty: nothing staged yet > > > > > > Stage values (held in memory, not yet sent to firmware): > > > > > > $ echo 25000 | sudo tee $ATTR/stapm_limit/current_value > > > $ echo 40000 | sudo tee $ATTR/fast_limit/current_value > > > $ echo 27000 | sudo tee $ATTR/slow_limit/current_value > > > $ echo 25000 | sudo tee $ATTR/skin_limit/current_value > > > $ echo 85 | sudo tee $ATTR/temp_target/current_value > > > > > > Commit all staged values in one ALIB call: > > > > > > $ echo save | sudo tee $ATTR/save_settings > > > > > > Switch to auto-commit (each write commits immediately): > > > > > > $ echo single | sudo tee $ATTR/save_settings > > > > > > Return to bulk mode: > > > > > > $ echo bulk | sudo tee $ATTR/save_settings > > > > > > Clear a staged value without committing: > > > > > > $ echo | sudo tee $ATTR/stapm_limit/current_value > > > > > > Query or change the active limit tier (device/expanded/soc/unbound): > > > > > > $ cat $ATTR/limit_mode/possible_values > > > device;expanded;soc;unbound > > > $ echo expanded | sudo tee $ATTR/limit_mode/current_value > > > > > > Switching tiers clears all staged values (old values may fall outside the > > > new range). Stages and commits must be redone after a mode switch. > > > > > > Antheas Kapenekakis (2): > > > Documentation: firmware-attributes: generalize save_settings entry > > > platform/x86/amd: Add AMD DPTCi driver > > > > > > .../testing/sysfs-class-firmware-attributes | 41 +- > > > MAINTAINERS | 6 + > > > drivers/platform/x86/amd/Kconfig | 27 + > > > drivers/platform/x86/amd/Makefile | 2 + > > > drivers/platform/x86/amd/dptc.c | 1325 +++++++++++++++++ > > > 5 files changed, 1386 insertions(+), 15 deletions(-) > > > create mode 100644 drivers/platform/x86/amd/dptc.c > > > > > > > > > base-commit: c89ce241c1909d2c2bdde88334c33f3000d364fb > > > > ^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC v1 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls 2026-03-03 19:16 ` Antheas Kapenekakis 2026-03-03 19:23 ` Antheas Kapenekakis @ 2026-03-03 19:27 ` Armin Wolf 2026-03-03 19:34 ` Antheas Kapenekakis 2026-03-03 19:51 ` Mario Limonciello (AMD) (kernel.org) 2 siblings, 1 reply; 18+ messages in thread From: Armin Wolf @ 2026-03-03 19:27 UTC (permalink / raw) To: Antheas Kapenekakis, Mario Limonciello; +Cc: linux-kernel, platform-driver-x86 Am 03.03.26 um 20:16 schrieb Antheas Kapenekakis: > On Tue, 3 Mar 2026 at 19:59, Mario Limonciello <superm1@kernel.org> wrote: >> A high level question - why aren't these vendors implementing PMF? It's >> 1000% less work to enable PMF. All the values that match the design get >> stored in BIOS, driver pulls the information and uses it. > From my understanding they do not implement anything and just use > ryzenadj with their windows vendor software. Do they implement a WMI ACPI device called "AOD"? AFAIK this WMI device is used by the Ryzen Master utility, so it could also allow users to change the TDP limits in a reasonable safe way. If they indeed only use ryzenadj, then shame on them. Thanks, Armin Wolf >> Same approach for Windows and Linux. >> >> More comments below. >> >> On 3/3/26 12:17 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: >>> STAPM limit, fast/slow PPT limits, skin-temperature TDP limit, slow/STAPM >>> time constants, and the thermal control target. >>> >>> Until now userspace has reached this interface through the acpi_call out- >>> of-tree module or ryzenadj, which carry no ABI guarantees and no per-device >>> safety limits. This driver replaces that with a proper in-kernel >>> implementation that: >>> >>> * Exposes all seven parameters through the firmware-attributes sysfs ABI, >>> so that standard tools (fwupd, systemd-bios-vendor, etc.) can enumerate >> What is systemd-bios-vendor? I guess I'm not familiar with this and a >> quick web search didn't turn anything obvious up. > I used some AI assistance to compile this from my userspace > implementation and the ASEGA pdf from the AMD site. I need to go > through _everything_ before this moves to non-RFC. Same with > copyright year. I focused on the implementation doing the things I > want it to do for now. > >>> and modify them without device-specific knowledge. >>> >>> * Enforces tiered per-device and per-SoC limits. The default "device" >>> mode restricts writes to a curated safe range (smin..smax) derived from >>> the device's thermal design. >> Can you please elaborate where you got all these numbers from? I don't >> know if they're accurate or not. Someone would probably need to cross >> reference them to be sure. > Trial and error, research, references from Windows, etc. All of the > devices in this driver have been tested with a userspace > implementation using the same limits for ppt/sppt/fppt. Nobody has > complained about them. To be honest, I usually do not set tctl slow > and fast time limits, so those are referenced from the Legion Go and > for tctl I go lower than what manufacturers usually set. Users like > tctl because some of them like their device to stay cooler. > > The big idea for this driver is to allow locking /dev/mem and ACPI. > Other than the legion go fan curves, and the Zotac Zone driver which > needs a cleanup, everything else is handled in the kernel now. > > But this approach works fine for the Zotac Zone as they seem to be > using a very simple WMI shim. > >>> An "expanded" mode exposes the full >>> hardware-validated range. An optional CONFIG_AMD_DPTC_EXTENDED Kconfig >>> adds "soc" (raw ALIB_PARAMS envelope) and "unbound" tiers for advanced >>> use. The active tier is itself a firmware-attribute, switchable at >>> runtime. >>> >>> * Stages values and commits them atomically in a single ALIB call, >>> matching the protocol's intended bulk-update semantics. A save_settings >>> attribute (per firmware-attributes ABI) controls whether writes commit >>> immediately ("single" mode) or are held until an explicit "save". >>> >>> * When in "single" mode, re-applies staged values after system resume, >>> so suspend/resume cycles do not silently revert to firmware defaults. >> This isn't the only interface for setting power limits. How do you make >> sure that the EC for example isn't stepping on toes on these designs? > For the DMI matched devices it is not. For the ones that are not > matched, the driver does not autoload and needs a kconfig parameter to > even load. > > OneXPlayer is a bit more complex with their turbo button doing TDP > swaps but turbo takeover in oxpec takes care of that and then you are > supposed use ryzenadj. > >> I /guess/ it always will need to be opt-in a device by device basis. >> >> What happens if the vendor enables PMF in a BIOS update? How does this >> avoid conflicts? > Some manufacturers enable pmf without implementing the tables. From my > understanding none of them implement pmf with limits. If a > manufacturer wants to move to pmf, we can amend the DMI entry with a > bios match. However, from my understanding, there is no TDP slider > equivalent for PMF. > >>> 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, Z1, 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. >>> >>> @Mario: can you suggest a CC list for V2? Thanks. Even if not merged, this >>> driver is still good for downstream use. >> You should include Shyam (AMD), Denis and Derek (community). > Sure. > >>> --- >>> Usage >>> ----- >>> >>> List all exposed attributes (read-only, no root required): >>> >>> $ fwupdmgr get-bios-settings >>> >>> This enumerates every attribute under /sys/class/firmware-attributes/, >>> including current_value, default_value, min_value, max_value, and >>> display_name for each DPTCi parameter. >> AFAIK - fwupd doesn't understand "save_settings" today > Yes, I do not expect it to even allow writing, but at least you can > preview the values which is useful. And from what I saw defaults are > not shown either. > > Antheas > >>> Sysfs direct usage >>> ------------------ >>> >>> All paths are under: >>> >>> ATTR=/sys/class/firmware-attributes/amd_dptc/attributes >>> >>> Inspect a parameter (no root needed): >>> >>> $ cat $ATTR/stapm_limit/{display_name,min_value,max_value,default_value,current_value} >>> Sustained TDP (mW) >>> 4000 >>> 85000 >>> 25000 >>> <- empty: nothing staged yet >>> >>> Stage values (held in memory, not yet sent to firmware): >>> >>> $ echo 25000 | sudo tee $ATTR/stapm_limit/current_value >>> $ echo 40000 | sudo tee $ATTR/fast_limit/current_value >>> $ echo 27000 | sudo tee $ATTR/slow_limit/current_value >>> $ echo 25000 | sudo tee $ATTR/skin_limit/current_value >>> $ echo 85 | sudo tee $ATTR/temp_target/current_value >>> >>> Commit all staged values in one ALIB call: >>> >>> $ echo save | sudo tee $ATTR/save_settings >>> >>> Switch to auto-commit (each write commits immediately): >>> >>> $ echo single | sudo tee $ATTR/save_settings >>> >>> Return to bulk mode: >>> >>> $ echo bulk | sudo tee $ATTR/save_settings >>> >>> Clear a staged value without committing: >>> >>> $ echo | sudo tee $ATTR/stapm_limit/current_value >>> >>> Query or change the active limit tier (device/expanded/soc/unbound): >>> >>> $ cat $ATTR/limit_mode/possible_values >>> device;expanded;soc;unbound >>> $ echo expanded | sudo tee $ATTR/limit_mode/current_value >>> >>> Switching tiers clears all staged values (old values may fall outside the >>> new range). Stages and commits must be redone after a mode switch. >>> >>> Antheas Kapenekakis (2): >>> Documentation: firmware-attributes: generalize save_settings entry >>> platform/x86/amd: Add AMD DPTCi driver >>> >>> .../testing/sysfs-class-firmware-attributes | 41 +- >>> MAINTAINERS | 6 + >>> drivers/platform/x86/amd/Kconfig | 27 + >>> drivers/platform/x86/amd/Makefile | 2 + >>> drivers/platform/x86/amd/dptc.c | 1325 +++++++++++++++++ >>> 5 files changed, 1386 insertions(+), 15 deletions(-) >>> create mode 100644 drivers/platform/x86/amd/dptc.c >>> >>> >>> base-commit: c89ce241c1909d2c2bdde88334c33f3000d364fb >> > ^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC v1 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls 2026-03-03 19:27 ` Armin Wolf @ 2026-03-03 19:34 ` Antheas Kapenekakis 2026-03-03 21:50 ` Armin Wolf 0 siblings, 1 reply; 18+ messages in thread From: Antheas Kapenekakis @ 2026-03-03 19:34 UTC (permalink / raw) To: Armin Wolf; +Cc: Mario Limonciello, linux-kernel, platform-driver-x86 On Tue, 3 Mar 2026 at 20:27, Armin Wolf <W_Armin@gmx.de> wrote: > > Am 03.03.26 um 20:16 schrieb Antheas Kapenekakis: > > > On Tue, 3 Mar 2026 at 19:59, Mario Limonciello <superm1@kernel.org> wrote: > >> A high level question - why aren't these vendors implementing PMF? It's > >> 1000% less work to enable PMF. All the values that match the design get > >> stored in BIOS, driver pulls the information and uses it. > > From my understanding they do not implement anything and just use > > ryzenadj with their windows vendor software. > > Do they implement a WMI ACPI device called "AOD"? AFAIK this WMI device > is used by the Ryzen Master utility, so it could also allow users to change > the TDP limits in a reasonable safe way. > > If they indeed only use ryzenadj, then shame on them. From my understanding, Ryzen Master has not been updated in a while. I was not aware of the AOD device. Moreover, it was my understanding that Ryzen Master hardcoded write and lookup addresses. From a quick lookup in my dump repository [2] it seems like not all of them feature an AOD device. See below for an AI assisted compilation. The good thing with this interface is that it handles rerouting to different mailbox addresses per generation, which makes implementation straightforward. Antheas [2] https://github.com/hhd-dev/hwinfo The following devices have an AOD device: Asus Z13 2025 AMD (AMDT=0x02, AMDI* HIDs) Legion_5_15ACH6H AMD Ryzen 5000 Legion_Slim_5_14APH8 AMD Legion_Slim_7_16APH8 AMD ally_x AMD Ryzen Z1 Extreme aokzoe a1x AMD ayaneo_3 AMD ayaneo_flip_kb AMD legion_go AMD (multiple ACPI versions) one xplayer AMD onexplayer f1pro AMD (AMDT=0x02, AMDI* HIDs) orangepi_neo AMD xbox_ally_x AMD Ryzen Z1 Extreme The following do not: ayaneo_airplus ayaneo_kun ayaneo_slide gpd_win4 gpd_win4_2024 gpd_win4_8840u gpd_winmini legion_go_s loki_max onexpalyer_fly onexplayer x1 onexplayer x1 mini onexplayer_mini_pro rog_ally win_max_2_hx370 xbox_ally > Thanks, > Armin Wolf > > >> Same approach for Windows and Linux. > >> > >> More comments below. > >> > >> On 3/3/26 12:17 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: > >>> STAPM limit, fast/slow PPT limits, skin-temperature TDP limit, slow/STAPM > >>> time constants, and the thermal control target. > >>> > >>> Until now userspace has reached this interface through the acpi_call out- > >>> of-tree module or ryzenadj, which carry no ABI guarantees and no per-device > >>> safety limits. This driver replaces that with a proper in-kernel > >>> implementation that: > >>> > >>> * Exposes all seven parameters through the firmware-attributes sysfs ABI, > >>> so that standard tools (fwupd, systemd-bios-vendor, etc.) can enumerate > >> What is systemd-bios-vendor? I guess I'm not familiar with this and a > >> quick web search didn't turn anything obvious up. > > I used some AI assistance to compile this from my userspace > > implementation and the ASEGA pdf from the AMD site. I need to go > > through _everything_ before this moves to non-RFC. Same with > > copyright year. I focused on the implementation doing the things I > > want it to do for now. > > > >>> and modify them without device-specific knowledge. > >>> > >>> * Enforces tiered per-device and per-SoC limits. The default "device" > >>> mode restricts writes to a curated safe range (smin..smax) derived from > >>> the device's thermal design. > >> Can you please elaborate where you got all these numbers from? I don't > >> know if they're accurate or not. Someone would probably need to cross > >> reference them to be sure. > > Trial and error, research, references from Windows, etc. All of the > > devices in this driver have been tested with a userspace > > implementation using the same limits for ppt/sppt/fppt. Nobody has > > complained about them. To be honest, I usually do not set tctl slow > > and fast time limits, so those are referenced from the Legion Go and > > for tctl I go lower than what manufacturers usually set. Users like > > tctl because some of them like their device to stay cooler. > > > > The big idea for this driver is to allow locking /dev/mem and ACPI. > > Other than the legion go fan curves, and the Zotac Zone driver which > > needs a cleanup, everything else is handled in the kernel now. > > > > But this approach works fine for the Zotac Zone as they seem to be > > using a very simple WMI shim. > > > >>> An "expanded" mode exposes the full > >>> hardware-validated range. An optional CONFIG_AMD_DPTC_EXTENDED Kconfig > >>> adds "soc" (raw ALIB_PARAMS envelope) and "unbound" tiers for advanced > >>> use. The active tier is itself a firmware-attribute, switchable at > >>> runtime. > >>> > >>> * Stages values and commits them atomically in a single ALIB call, > >>> matching the protocol's intended bulk-update semantics. A save_settings > >>> attribute (per firmware-attributes ABI) controls whether writes commit > >>> immediately ("single" mode) or are held until an explicit "save". > >>> > >>> * When in "single" mode, re-applies staged values after system resume, > >>> so suspend/resume cycles do not silently revert to firmware defaults. > >> This isn't the only interface for setting power limits. How do you make > >> sure that the EC for example isn't stepping on toes on these designs? > > For the DMI matched devices it is not. For the ones that are not > > matched, the driver does not autoload and needs a kconfig parameter to > > even load. > > > > OneXPlayer is a bit more complex with their turbo button doing TDP > > swaps but turbo takeover in oxpec takes care of that and then you are > > supposed use ryzenadj. > > > >> I /guess/ it always will need to be opt-in a device by device basis. > >> > >> What happens if the vendor enables PMF in a BIOS update? How does this > >> avoid conflicts? > > Some manufacturers enable pmf without implementing the tables. From my > > understanding none of them implement pmf with limits. If a > > manufacturer wants to move to pmf, we can amend the DMI entry with a > > bios match. However, from my understanding, there is no TDP slider > > equivalent for PMF. > > > >>> 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, Z1, 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. > >>> > >>> @Mario: can you suggest a CC list for V2? Thanks. Even if not merged, this > >>> driver is still good for downstream use. > >> You should include Shyam (AMD), Denis and Derek (community). > > Sure. > > > >>> --- > >>> Usage > >>> ----- > >>> > >>> List all exposed attributes (read-only, no root required): > >>> > >>> $ fwupdmgr get-bios-settings > >>> > >>> This enumerates every attribute under /sys/class/firmware-attributes/, > >>> including current_value, default_value, min_value, max_value, and > >>> display_name for each DPTCi parameter. > >> AFAIK - fwupd doesn't understand "save_settings" today > > Yes, I do not expect it to even allow writing, but at least you can > > preview the values which is useful. And from what I saw defaults are > > not shown either. > > > > Antheas > > > >>> Sysfs direct usage > >>> ------------------ > >>> > >>> All paths are under: > >>> > >>> ATTR=/sys/class/firmware-attributes/amd_dptc/attributes > >>> > >>> Inspect a parameter (no root needed): > >>> > >>> $ cat $ATTR/stapm_limit/{display_name,min_value,max_value,default_value,current_value} > >>> Sustained TDP (mW) > >>> 4000 > >>> 85000 > >>> 25000 > >>> <- empty: nothing staged yet > >>> > >>> Stage values (held in memory, not yet sent to firmware): > >>> > >>> $ echo 25000 | sudo tee $ATTR/stapm_limit/current_value > >>> $ echo 40000 | sudo tee $ATTR/fast_limit/current_value > >>> $ echo 27000 | sudo tee $ATTR/slow_limit/current_value > >>> $ echo 25000 | sudo tee $ATTR/skin_limit/current_value > >>> $ echo 85 | sudo tee $ATTR/temp_target/current_value > >>> > >>> Commit all staged values in one ALIB call: > >>> > >>> $ echo save | sudo tee $ATTR/save_settings > >>> > >>> Switch to auto-commit (each write commits immediately): > >>> > >>> $ echo single | sudo tee $ATTR/save_settings > >>> > >>> Return to bulk mode: > >>> > >>> $ echo bulk | sudo tee $ATTR/save_settings > >>> > >>> Clear a staged value without committing: > >>> > >>> $ echo | sudo tee $ATTR/stapm_limit/current_value > >>> > >>> Query or change the active limit tier (device/expanded/soc/unbound): > >>> > >>> $ cat $ATTR/limit_mode/possible_values > >>> device;expanded;soc;unbound > >>> $ echo expanded | sudo tee $ATTR/limit_mode/current_value > >>> > >>> Switching tiers clears all staged values (old values may fall outside the > >>> new range). Stages and commits must be redone after a mode switch. > >>> > >>> Antheas Kapenekakis (2): > >>> Documentation: firmware-attributes: generalize save_settings entry > >>> platform/x86/amd: Add AMD DPTCi driver > >>> > >>> .../testing/sysfs-class-firmware-attributes | 41 +- > >>> MAINTAINERS | 6 + > >>> drivers/platform/x86/amd/Kconfig | 27 + > >>> drivers/platform/x86/amd/Makefile | 2 + > >>> drivers/platform/x86/amd/dptc.c | 1325 +++++++++++++++++ > >>> 5 files changed, 1386 insertions(+), 15 deletions(-) > >>> create mode 100644 drivers/platform/x86/amd/dptc.c > >>> > >>> > >>> base-commit: c89ce241c1909d2c2bdde88334c33f3000d364fb > >> > > > ^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC v1 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls 2026-03-03 19:34 ` Antheas Kapenekakis @ 2026-03-03 21:50 ` Armin Wolf 2026-03-03 23:47 ` Antheas Kapenekakis 0 siblings, 1 reply; 18+ messages in thread From: Armin Wolf @ 2026-03-03 21:50 UTC (permalink / raw) To: Antheas Kapenekakis; +Cc: Mario Limonciello, linux-kernel, platform-driver-x86 Am 03.03.26 um 20:34 schrieb Antheas Kapenekakis: > On Tue, 3 Mar 2026 at 20:27, Armin Wolf <W_Armin@gmx.de> wrote: >> Am 03.03.26 um 20:16 schrieb Antheas Kapenekakis: >> >>> On Tue, 3 Mar 2026 at 19:59, Mario Limonciello <superm1@kernel.org> wrote: >>>> A high level question - why aren't these vendors implementing PMF? It's >>>> 1000% less work to enable PMF. All the values that match the design get >>>> stored in BIOS, driver pulls the information and uses it. >>> From my understanding they do not implement anything and just use >>> ryzenadj with their windows vendor software. >> Do they implement a WMI ACPI device called "AOD"? AFAIK this WMI device >> is used by the Ryzen Master utility, so it could also allow users to change >> the TDP limits in a reasonable safe way. >> >> If they indeed only use ryzenadj, then shame on them. > From my understanding, Ryzen Master has not been updated in a while. I > was not aware of the AOD device. Moreover, it was my understanding > that Ryzen Master hardcoded write and lookup addresses. AFAIK someone on the Phoronix forums has found out that Ryzen Master uses the AOD WMI device, at least on some platforms. > From a quick lookup in my dump repository [2] it seems like not all of > them feature an AOD device. See below for an AI assisted compilation. > > The good thing with this interface is that it handles rerouting to > different mailbox addresses per generation, which makes implementation > straightforward. Yes, but the WMI interface also signals what kinds of settings are supported on a given device, as well as limits and step sizes. We would however require some input from AMD to figure out how to properly use it. Otherwise i can run some test to figure out the underlying interface. On devices without the AOD device, using the ALIB interface indeed seems to be the least dangerous way. Thanks, Armin Wolf > Antheas > > [2] https://github.com/hhd-dev/hwinfo > > The following devices have an AOD device: > Asus Z13 2025 AMD (AMDT=0x02, AMDI* HIDs) > Legion_5_15ACH6H AMD Ryzen 5000 > Legion_Slim_5_14APH8 AMD > Legion_Slim_7_16APH8 AMD > ally_x AMD Ryzen Z1 Extreme > aokzoe a1x AMD > ayaneo_3 AMD > ayaneo_flip_kb AMD > legion_go AMD (multiple ACPI versions) > one xplayer AMD > onexplayer f1pro AMD (AMDT=0x02, AMDI* HIDs) > orangepi_neo AMD > xbox_ally_x AMD Ryzen Z1 Extreme > > The following do not: > ayaneo_airplus > ayaneo_kun > ayaneo_slide > gpd_win4 > gpd_win4_2024 > gpd_win4_8840u > gpd_winmini > legion_go_s > loki_max > onexpalyer_fly > onexplayer x1 > onexplayer x1 mini > onexplayer_mini_pro > rog_ally > win_max_2_hx370 > xbox_ally > >> Thanks, >> Armin Wolf >> >>>> Same approach for Windows and Linux. >>>> >>>> More comments below. >>>> >>>> On 3/3/26 12:17 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: >>>>> STAPM limit, fast/slow PPT limits, skin-temperature TDP limit, slow/STAPM >>>>> time constants, and the thermal control target. >>>>> >>>>> Until now userspace has reached this interface through the acpi_call out- >>>>> of-tree module or ryzenadj, which carry no ABI guarantees and no per-device >>>>> safety limits. This driver replaces that with a proper in-kernel >>>>> implementation that: >>>>> >>>>> * Exposes all seven parameters through the firmware-attributes sysfs ABI, >>>>> so that standard tools (fwupd, systemd-bios-vendor, etc.) can enumerate >>>> What is systemd-bios-vendor? I guess I'm not familiar with this and a >>>> quick web search didn't turn anything obvious up. >>> I used some AI assistance to compile this from my userspace >>> implementation and the ASEGA pdf from the AMD site. I need to go >>> through _everything_ before this moves to non-RFC. Same with >>> copyright year. I focused on the implementation doing the things I >>> want it to do for now. >>> >>>>> and modify them without device-specific knowledge. >>>>> >>>>> * Enforces tiered per-device and per-SoC limits. The default "device" >>>>> mode restricts writes to a curated safe range (smin..smax) derived from >>>>> the device's thermal design. >>>> Can you please elaborate where you got all these numbers from? I don't >>>> know if they're accurate or not. Someone would probably need to cross >>>> reference them to be sure. >>> Trial and error, research, references from Windows, etc. All of the >>> devices in this driver have been tested with a userspace >>> implementation using the same limits for ppt/sppt/fppt. Nobody has >>> complained about them. To be honest, I usually do not set tctl slow >>> and fast time limits, so those are referenced from the Legion Go and >>> for tctl I go lower than what manufacturers usually set. Users like >>> tctl because some of them like their device to stay cooler. >>> >>> The big idea for this driver is to allow locking /dev/mem and ACPI. >>> Other than the legion go fan curves, and the Zotac Zone driver which >>> needs a cleanup, everything else is handled in the kernel now. >>> >>> But this approach works fine for the Zotac Zone as they seem to be >>> using a very simple WMI shim. >>> >>>>> An "expanded" mode exposes the full >>>>> hardware-validated range. An optional CONFIG_AMD_DPTC_EXTENDED Kconfig >>>>> adds "soc" (raw ALIB_PARAMS envelope) and "unbound" tiers for advanced >>>>> use. The active tier is itself a firmware-attribute, switchable at >>>>> runtime. >>>>> >>>>> * Stages values and commits them atomically in a single ALIB call, >>>>> matching the protocol's intended bulk-update semantics. A save_settings >>>>> attribute (per firmware-attributes ABI) controls whether writes commit >>>>> immediately ("single" mode) or are held until an explicit "save". >>>>> >>>>> * When in "single" mode, re-applies staged values after system resume, >>>>> so suspend/resume cycles do not silently revert to firmware defaults. >>>> This isn't the only interface for setting power limits. How do you make >>>> sure that the EC for example isn't stepping on toes on these designs? >>> For the DMI matched devices it is not. For the ones that are not >>> matched, the driver does not autoload and needs a kconfig parameter to >>> even load. >>> >>> OneXPlayer is a bit more complex with their turbo button doing TDP >>> swaps but turbo takeover in oxpec takes care of that and then you are >>> supposed use ryzenadj. >>> >>>> I /guess/ it always will need to be opt-in a device by device basis. >>>> >>>> What happens if the vendor enables PMF in a BIOS update? How does this >>>> avoid conflicts? >>> Some manufacturers enable pmf without implementing the tables. From my >>> understanding none of them implement pmf with limits. If a >>> manufacturer wants to move to pmf, we can amend the DMI entry with a >>> bios match. However, from my understanding, there is no TDP slider >>> equivalent for PMF. >>> >>>>> 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, Z1, 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. >>>>> >>>>> @Mario: can you suggest a CC list for V2? Thanks. Even if not merged, this >>>>> driver is still good for downstream use. >>>> You should include Shyam (AMD), Denis and Derek (community). >>> Sure. >>> >>>>> --- >>>>> Usage >>>>> ----- >>>>> >>>>> List all exposed attributes (read-only, no root required): >>>>> >>>>> $ fwupdmgr get-bios-settings >>>>> >>>>> This enumerates every attribute under /sys/class/firmware-attributes/, >>>>> including current_value, default_value, min_value, max_value, and >>>>> display_name for each DPTCi parameter. >>>> AFAIK - fwupd doesn't understand "save_settings" today >>> Yes, I do not expect it to even allow writing, but at least you can >>> preview the values which is useful. And from what I saw defaults are >>> not shown either. >>> >>> Antheas >>> >>>>> Sysfs direct usage >>>>> ------------------ >>>>> >>>>> All paths are under: >>>>> >>>>> ATTR=/sys/class/firmware-attributes/amd_dptc/attributes >>>>> >>>>> Inspect a parameter (no root needed): >>>>> >>>>> $ cat $ATTR/stapm_limit/{display_name,min_value,max_value,default_value,current_value} >>>>> Sustained TDP (mW) >>>>> 4000 >>>>> 85000 >>>>> 25000 >>>>> <- empty: nothing staged yet >>>>> >>>>> Stage values (held in memory, not yet sent to firmware): >>>>> >>>>> $ echo 25000 | sudo tee $ATTR/stapm_limit/current_value >>>>> $ echo 40000 | sudo tee $ATTR/fast_limit/current_value >>>>> $ echo 27000 | sudo tee $ATTR/slow_limit/current_value >>>>> $ echo 25000 | sudo tee $ATTR/skin_limit/current_value >>>>> $ echo 85 | sudo tee $ATTR/temp_target/current_value >>>>> >>>>> Commit all staged values in one ALIB call: >>>>> >>>>> $ echo save | sudo tee $ATTR/save_settings >>>>> >>>>> Switch to auto-commit (each write commits immediately): >>>>> >>>>> $ echo single | sudo tee $ATTR/save_settings >>>>> >>>>> Return to bulk mode: >>>>> >>>>> $ echo bulk | sudo tee $ATTR/save_settings >>>>> >>>>> Clear a staged value without committing: >>>>> >>>>> $ echo | sudo tee $ATTR/stapm_limit/current_value >>>>> >>>>> Query or change the active limit tier (device/expanded/soc/unbound): >>>>> >>>>> $ cat $ATTR/limit_mode/possible_values >>>>> device;expanded;soc;unbound >>>>> $ echo expanded | sudo tee $ATTR/limit_mode/current_value >>>>> >>>>> Switching tiers clears all staged values (old values may fall outside the >>>>> new range). Stages and commits must be redone after a mode switch. >>>>> >>>>> Antheas Kapenekakis (2): >>>>> Documentation: firmware-attributes: generalize save_settings entry >>>>> platform/x86/amd: Add AMD DPTCi driver >>>>> >>>>> .../testing/sysfs-class-firmware-attributes | 41 +- >>>>> MAINTAINERS | 6 + >>>>> drivers/platform/x86/amd/Kconfig | 27 + >>>>> drivers/platform/x86/amd/Makefile | 2 + >>>>> drivers/platform/x86/amd/dptc.c | 1325 +++++++++++++++++ >>>>> 5 files changed, 1386 insertions(+), 15 deletions(-) >>>>> create mode 100644 drivers/platform/x86/amd/dptc.c >>>>> >>>>> >>>>> base-commit: c89ce241c1909d2c2bdde88334c33f3000d364fb ^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC v1 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls 2026-03-03 21:50 ` Armin Wolf @ 2026-03-03 23:47 ` Antheas Kapenekakis 0 siblings, 0 replies; 18+ messages in thread From: Antheas Kapenekakis @ 2026-03-03 23:47 UTC (permalink / raw) To: Armin Wolf; +Cc: Mario Limonciello, linux-kernel, platform-driver-x86 On Tue, 3 Mar 2026 at 22:51, Armin Wolf <W_Armin@gmx.de> wrote: > > Am 03.03.26 um 20:34 schrieb Antheas Kapenekakis: > > > On Tue, 3 Mar 2026 at 20:27, Armin Wolf <W_Armin@gmx.de> wrote: > >> Am 03.03.26 um 20:16 schrieb Antheas Kapenekakis: > >> > >>> On Tue, 3 Mar 2026 at 19:59, Mario Limonciello <superm1@kernel.org> wrote: > >>>> A high level question - why aren't these vendors implementing PMF? It's > >>>> 1000% less work to enable PMF. All the values that match the design get > >>>> stored in BIOS, driver pulls the information and uses it. > >>> From my understanding they do not implement anything and just use > >>> ryzenadj with their windows vendor software. > >> Do they implement a WMI ACPI device called "AOD"? AFAIK this WMI device > >> is used by the Ryzen Master utility, so it could also allow users to change > >> the TDP limits in a reasonable safe way. > >> > >> If they indeed only use ryzenadj, then shame on them. > > From my understanding, Ryzen Master has not been updated in a while. I > > was not aware of the AOD device. Moreover, it was my understanding > > that Ryzen Master hardcoded write and lookup addresses. > > AFAIK someone on the Phoronix forums has found out that Ryzen Master uses > the AOD WMI device, at least on some platforms. > > > From a quick lookup in my dump repository [2] it seems like not all of > > them feature an AOD device. See below for an AI assisted compilation. > > > > The good thing with this interface is that it handles rerouting to > > different mailbox addresses per generation, which makes implementation > > straightforward. > > Yes, but the WMI interface also signals what kinds of settings are supported > on a given device, as well as limits and step sizes. We would however require > some input from AMD to figure out how to properly use it. Otherwise i can run > some test to figure out the underlying interface. > > On devices without the AOD device, using the ALIB interface indeed seems to be > the least dangerous way. Seems like the AOD device just does clock tuning, but clocks are locked on these devices and boost is enough to exceed the thermal envelope. So I would not consider it a replacement in any case. Perhaps there's some potential for desktop CPUs, but that's not my area. Antheas > Thanks, > Armin Wolf > > > Antheas > > > > [2] https://github.com/hhd-dev/hwinfo > > > > The following devices have an AOD device: > > Asus Z13 2025 AMD (AMDT=0x02, AMDI* HIDs) > > Legion_5_15ACH6H AMD Ryzen 5000 > > Legion_Slim_5_14APH8 AMD > > Legion_Slim_7_16APH8 AMD > > ally_x AMD Ryzen Z1 Extreme > > aokzoe a1x AMD > > ayaneo_3 AMD > > ayaneo_flip_kb AMD > > legion_go AMD (multiple ACPI versions) > > one xplayer AMD > > onexplayer f1pro AMD (AMDT=0x02, AMDI* HIDs) > > orangepi_neo AMD > > xbox_ally_x AMD Ryzen Z1 Extreme > > > > The following do not: > > ayaneo_airplus > > ayaneo_kun > > ayaneo_slide > > gpd_win4 > > gpd_win4_2024 > > gpd_win4_8840u > > gpd_winmini > > legion_go_s > > loki_max > > onexpalyer_fly > > onexplayer x1 > > onexplayer x1 mini > > onexplayer_mini_pro > > rog_ally > > win_max_2_hx370 > > xbox_ally > > > >> Thanks, > >> Armin Wolf > >> > >>>> Same approach for Windows and Linux. > >>>> > >>>> More comments below. > >>>> > >>>> On 3/3/26 12:17 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: > >>>>> STAPM limit, fast/slow PPT limits, skin-temperature TDP limit, slow/STAPM > >>>>> time constants, and the thermal control target. > >>>>> > >>>>> Until now userspace has reached this interface through the acpi_call out- > >>>>> of-tree module or ryzenadj, which carry no ABI guarantees and no per-device > >>>>> safety limits. This driver replaces that with a proper in-kernel > >>>>> implementation that: > >>>>> > >>>>> * Exposes all seven parameters through the firmware-attributes sysfs ABI, > >>>>> so that standard tools (fwupd, systemd-bios-vendor, etc.) can enumerate > >>>> What is systemd-bios-vendor? I guess I'm not familiar with this and a > >>>> quick web search didn't turn anything obvious up. > >>> I used some AI assistance to compile this from my userspace > >>> implementation and the ASEGA pdf from the AMD site. I need to go > >>> through _everything_ before this moves to non-RFC. Same with > >>> copyright year. I focused on the implementation doing the things I > >>> want it to do for now. > >>> > >>>>> and modify them without device-specific knowledge. > >>>>> > >>>>> * Enforces tiered per-device and per-SoC limits. The default "device" > >>>>> mode restricts writes to a curated safe range (smin..smax) derived from > >>>>> the device's thermal design. > >>>> Can you please elaborate where you got all these numbers from? I don't > >>>> know if they're accurate or not. Someone would probably need to cross > >>>> reference them to be sure. > >>> Trial and error, research, references from Windows, etc. All of the > >>> devices in this driver have been tested with a userspace > >>> implementation using the same limits for ppt/sppt/fppt. Nobody has > >>> complained about them. To be honest, I usually do not set tctl slow > >>> and fast time limits, so those are referenced from the Legion Go and > >>> for tctl I go lower than what manufacturers usually set. Users like > >>> tctl because some of them like their device to stay cooler. > >>> > >>> The big idea for this driver is to allow locking /dev/mem and ACPI. > >>> Other than the legion go fan curves, and the Zotac Zone driver which > >>> needs a cleanup, everything else is handled in the kernel now. > >>> > >>> But this approach works fine for the Zotac Zone as they seem to be > >>> using a very simple WMI shim. > >>> > >>>>> An "expanded" mode exposes the full > >>>>> hardware-validated range. An optional CONFIG_AMD_DPTC_EXTENDED Kconfig > >>>>> adds "soc" (raw ALIB_PARAMS envelope) and "unbound" tiers for advanced > >>>>> use. The active tier is itself a firmware-attribute, switchable at > >>>>> runtime. > >>>>> > >>>>> * Stages values and commits them atomically in a single ALIB call, > >>>>> matching the protocol's intended bulk-update semantics. A save_settings > >>>>> attribute (per firmware-attributes ABI) controls whether writes commit > >>>>> immediately ("single" mode) or are held until an explicit "save". > >>>>> > >>>>> * When in "single" mode, re-applies staged values after system resume, > >>>>> so suspend/resume cycles do not silently revert to firmware defaults. > >>>> This isn't the only interface for setting power limits. How do you make > >>>> sure that the EC for example isn't stepping on toes on these designs? > >>> For the DMI matched devices it is not. For the ones that are not > >>> matched, the driver does not autoload and needs a kconfig parameter to > >>> even load. > >>> > >>> OneXPlayer is a bit more complex with their turbo button doing TDP > >>> swaps but turbo takeover in oxpec takes care of that and then you are > >>> supposed use ryzenadj. > >>> > >>>> I /guess/ it always will need to be opt-in a device by device basis. > >>>> > >>>> What happens if the vendor enables PMF in a BIOS update? How does this > >>>> avoid conflicts? > >>> Some manufacturers enable pmf without implementing the tables. From my > >>> understanding none of them implement pmf with limits. If a > >>> manufacturer wants to move to pmf, we can amend the DMI entry with a > >>> bios match. However, from my understanding, there is no TDP slider > >>> equivalent for PMF. > >>> > >>>>> 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, Z1, 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. > >>>>> > >>>>> @Mario: can you suggest a CC list for V2? Thanks. Even if not merged, this > >>>>> driver is still good for downstream use. > >>>> You should include Shyam (AMD), Denis and Derek (community). > >>> Sure. > >>> > >>>>> --- > >>>>> Usage > >>>>> ----- > >>>>> > >>>>> List all exposed attributes (read-only, no root required): > >>>>> > >>>>> $ fwupdmgr get-bios-settings > >>>>> > >>>>> This enumerates every attribute under /sys/class/firmware-attributes/, > >>>>> including current_value, default_value, min_value, max_value, and > >>>>> display_name for each DPTCi parameter. > >>>> AFAIK - fwupd doesn't understand "save_settings" today > >>> Yes, I do not expect it to even allow writing, but at least you can > >>> preview the values which is useful. And from what I saw defaults are > >>> not shown either. > >>> > >>> Antheas > >>> > >>>>> Sysfs direct usage > >>>>> ------------------ > >>>>> > >>>>> All paths are under: > >>>>> > >>>>> ATTR=/sys/class/firmware-attributes/amd_dptc/attributes > >>>>> > >>>>> Inspect a parameter (no root needed): > >>>>> > >>>>> $ cat $ATTR/stapm_limit/{display_name,min_value,max_value,default_value,current_value} > >>>>> Sustained TDP (mW) > >>>>> 4000 > >>>>> 85000 > >>>>> 25000 > >>>>> <- empty: nothing staged yet > >>>>> > >>>>> Stage values (held in memory, not yet sent to firmware): > >>>>> > >>>>> $ echo 25000 | sudo tee $ATTR/stapm_limit/current_value > >>>>> $ echo 40000 | sudo tee $ATTR/fast_limit/current_value > >>>>> $ echo 27000 | sudo tee $ATTR/slow_limit/current_value > >>>>> $ echo 25000 | sudo tee $ATTR/skin_limit/current_value > >>>>> $ echo 85 | sudo tee $ATTR/temp_target/current_value > >>>>> > >>>>> Commit all staged values in one ALIB call: > >>>>> > >>>>> $ echo save | sudo tee $ATTR/save_settings > >>>>> > >>>>> Switch to auto-commit (each write commits immediately): > >>>>> > >>>>> $ echo single | sudo tee $ATTR/save_settings > >>>>> > >>>>> Return to bulk mode: > >>>>> > >>>>> $ echo bulk | sudo tee $ATTR/save_settings > >>>>> > >>>>> Clear a staged value without committing: > >>>>> > >>>>> $ echo | sudo tee $ATTR/stapm_limit/current_value > >>>>> > >>>>> Query or change the active limit tier (device/expanded/soc/unbound): > >>>>> > >>>>> $ cat $ATTR/limit_mode/possible_values > >>>>> device;expanded;soc;unbound > >>>>> $ echo expanded | sudo tee $ATTR/limit_mode/current_value > >>>>> > >>>>> Switching tiers clears all staged values (old values may fall outside the > >>>>> new range). Stages and commits must be redone after a mode switch. > >>>>> > >>>>> Antheas Kapenekakis (2): > >>>>> Documentation: firmware-attributes: generalize save_settings entry > >>>>> platform/x86/amd: Add AMD DPTCi driver > >>>>> > >>>>> .../testing/sysfs-class-firmware-attributes | 41 +- > >>>>> MAINTAINERS | 6 + > >>>>> drivers/platform/x86/amd/Kconfig | 27 + > >>>>> drivers/platform/x86/amd/Makefile | 2 + > >>>>> drivers/platform/x86/amd/dptc.c | 1325 +++++++++++++++++ > >>>>> 5 files changed, 1386 insertions(+), 15 deletions(-) > >>>>> create mode 100644 drivers/platform/x86/amd/dptc.c > >>>>> > >>>>> > >>>>> base-commit: c89ce241c1909d2c2bdde88334c33f3000d364fb > ^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC v1 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls 2026-03-03 19:16 ` Antheas Kapenekakis 2026-03-03 19:23 ` Antheas Kapenekakis 2026-03-03 19:27 ` Armin Wolf @ 2026-03-03 19:51 ` Mario Limonciello (AMD) (kernel.org) 2026-03-03 20:04 ` Antheas Kapenekakis 2 siblings, 1 reply; 18+ messages in thread From: Mario Limonciello (AMD) (kernel.org) @ 2026-03-03 19:51 UTC (permalink / raw) To: Antheas Kapenekakis; +Cc: linux-kernel, platform-driver-x86 On 3/3/2026 1:16 PM, Antheas Kapenekakis wrote: > On Tue, 3 Mar 2026 at 19:59, Mario Limonciello <superm1@kernel.org> wrote: >> >> A high level question - why aren't these vendors implementing PMF? It's >> 1000% less work to enable PMF. All the values that match the design get >> stored in BIOS, driver pulls the information and uses it. > > From my understanding they do not implement anything and just use > ryzenadj with their windows vendor software. > >> Same approach for Windows and Linux. >> >> More comments below. >> >> On 3/3/26 12:17 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: >>> STAPM limit, fast/slow PPT limits, skin-temperature TDP limit, slow/STAPM >>> time constants, and the thermal control target. >>> >>> Until now userspace has reached this interface through the acpi_call out- >>> of-tree module or ryzenadj, which carry no ABI guarantees and no per-device >>> safety limits. This driver replaces that with a proper in-kernel >>> implementation that: >>> >>> * Exposes all seven parameters through the firmware-attributes sysfs ABI, >>> so that standard tools (fwupd, systemd-bios-vendor, etc.) can enumerate >> >> What is systemd-bios-vendor? I guess I'm not familiar with this and a >> quick web search didn't turn anything obvious up. > > I used some AI assistance to compile this from my userspace > implementation and the ASEGA pdf from the AMD site. I need to go > through _everything_ before this moves to non-RFC. Same with > copyright year. I focused on the implementation doing the things I > want it to do for now. Got it; so it's a made up tool :P > >>> and modify them without device-specific knowledge. >>> >>> * Enforces tiered per-device and per-SoC limits. The default "device" >>> mode restricts writes to a curated safe range (smin..smax) derived from >>> the device's thermal design. >> >> Can you please elaborate where you got all these numbers from? I don't >> know if they're accurate or not. Someone would probably need to cross >> reference them to be sure. > > Trial and error, research, references from Windows, etc. All of the > devices in this driver have been tested with a userspace > implementation using the same limits for ppt/sppt/fppt. Nobody has > complained about them. To be honest, I usually do not set tctl slow > and fast time limits, so those are referenced from the Legion Go and > for tctl I go lower than what manufacturers usually set. Users like > tctl because some of them like their device to stay cooler. > > The big idea for this driver is to allow locking /dev/mem and ACPI. > Other than the legion go fan curves, and the Zotac Zone driver which > needs a cleanup, everything else is handled in the kernel now. > > But this approach works fine for the Zotac Zone as they seem to be > using a very simple WMI shim. I mean at a high level I conceptually like the idea of getting rid of the need to use software that manipulates /dev/mem and especially anything that means people relying on out of tree patches. This is a strong balancing act though to ratify interfaces to things that we don't have a stable ABI contract and documentation of the implications on getting it wrong. > >>> An "expanded" mode exposes the full >>> hardware-validated range. An optional CONFIG_AMD_DPTC_EXTENDED Kconfig >>> adds "soc" (raw ALIB_PARAMS envelope) and "unbound" tiers for advanced >>> use. The active tier is itself a firmware-attribute, switchable at >>> runtime. >>> >>> * Stages values and commits them atomically in a single ALIB call, >>> matching the protocol's intended bulk-update semantics. A save_settings >>> attribute (per firmware-attributes ABI) controls whether writes commit >>> immediately ("single" mode) or are held until an explicit "save". >>> >>> * When in "single" mode, re-applies staged values after system resume, >>> so suspend/resume cycles do not silently revert to firmware defaults. >> >> This isn't the only interface for setting power limits. How do you make >> sure that the EC for example isn't stepping on toes on these designs? > > For the DMI matched devices it is not. For the ones that are not > matched, the driver does not autoload and needs a kconfig parameter to > even load. > > OneXPlayer is a bit more complex with their turbo button doing TDP > swaps but turbo takeover in oxpec takes care of that and then you are > supposed use ryzenadj. > >> I /guess/ it always will need to be opt-in a device by device basis. >> >> What happens if the vendor enables PMF in a BIOS update? How does this >> avoid conflicts? > > Some manufacturers enable pmf without implementing the tables. From my > understanding none of them implement pmf with limits. If a > manufacturer wants to move to pmf, we can amend the DMI entry with a > bios match. However, from my understanding, there is no TDP slider > equivalent for PMF. > >>> >>> 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, Z1, 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. >>> >>> @Mario: can you suggest a CC list for V2? Thanks. Even if not merged, this >>> driver is still good for downstream use. >> >> You should include Shyam (AMD), Denis and Derek (community). > > Sure. > >>> >>> --- >>> Usage >>> ----- >>> >>> List all exposed attributes (read-only, no root required): >>> >>> $ fwupdmgr get-bios-settings >>> >>> This enumerates every attribute under /sys/class/firmware-attributes/, >>> including current_value, default_value, min_value, max_value, and >>> display_name for each DPTCi parameter. >> >> AFAIK - fwupd doesn't understand "save_settings" today > > Yes, I do not expect it to even allow writing, but at least you can > preview the values which is useful. And from what I saw defaults are > not shown either. > > Antheas > >>> >>> Sysfs direct usage >>> ------------------ >>> >>> All paths are under: >>> >>> ATTR=/sys/class/firmware-attributes/amd_dptc/attributes >>> >>> Inspect a parameter (no root needed): >>> >>> $ cat $ATTR/stapm_limit/{display_name,min_value,max_value,default_value,current_value} >>> Sustained TDP (mW) >>> 4000 >>> 85000 >>> 25000 >>> <- empty: nothing staged yet >>> >>> Stage values (held in memory, not yet sent to firmware): >>> >>> $ echo 25000 | sudo tee $ATTR/stapm_limit/current_value >>> $ echo 40000 | sudo tee $ATTR/fast_limit/current_value >>> $ echo 27000 | sudo tee $ATTR/slow_limit/current_value >>> $ echo 25000 | sudo tee $ATTR/skin_limit/current_value >>> $ echo 85 | sudo tee $ATTR/temp_target/current_value >>> >>> Commit all staged values in one ALIB call: >>> >>> $ echo save | sudo tee $ATTR/save_settings >>> >>> Switch to auto-commit (each write commits immediately): >>> >>> $ echo single | sudo tee $ATTR/save_settings >>> >>> Return to bulk mode: >>> >>> $ echo bulk | sudo tee $ATTR/save_settings >>> >>> Clear a staged value without committing: >>> >>> $ echo | sudo tee $ATTR/stapm_limit/current_value >>> >>> Query or change the active limit tier (device/expanded/soc/unbound): >>> >>> $ cat $ATTR/limit_mode/possible_values >>> device;expanded;soc;unbound >>> $ echo expanded | sudo tee $ATTR/limit_mode/current_value >>> >>> Switching tiers clears all staged values (old values may fall outside the >>> new range). Stages and commits must be redone after a mode switch. >>> >>> Antheas Kapenekakis (2): >>> Documentation: firmware-attributes: generalize save_settings entry >>> platform/x86/amd: Add AMD DPTCi driver >>> >>> .../testing/sysfs-class-firmware-attributes | 41 +- >>> MAINTAINERS | 6 + >>> drivers/platform/x86/amd/Kconfig | 27 + >>> drivers/platform/x86/amd/Makefile | 2 + >>> drivers/platform/x86/amd/dptc.c | 1325 +++++++++++++++++ >>> 5 files changed, 1386 insertions(+), 15 deletions(-) >>> create mode 100644 drivers/platform/x86/amd/dptc.c >>> >>> >>> base-commit: c89ce241c1909d2c2bdde88334c33f3000d364fb >> >> > ^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [RFC v1 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls 2026-03-03 19:51 ` Mario Limonciello (AMD) (kernel.org) @ 2026-03-03 20:04 ` Antheas Kapenekakis 0 siblings, 0 replies; 18+ messages in thread From: Antheas Kapenekakis @ 2026-03-03 20:04 UTC (permalink / raw) To: Mario Limonciello (AMD) (kernel.org) Cc: linux-kernel, platform-driver-x86, Armin Wolf On Tue, 3 Mar 2026 at 20:51, Mario Limonciello (AMD) (kernel.org) <superm1@kernel.org> wrote: > > > > On 3/3/2026 1:16 PM, Antheas Kapenekakis wrote: > > On Tue, 3 Mar 2026 at 19:59, Mario Limonciello <superm1@kernel.org> wrote: > >> > >> A high level question - why aren't these vendors implementing PMF? It's > >> 1000% less work to enable PMF. All the values that match the design get > >> stored in BIOS, driver pulls the information and uses it. > > > > From my understanding they do not implement anything and just use > > ryzenadj with their windows vendor software. > > > >> Same approach for Windows and Linux. > >> > >> More comments below. > >> > >> On 3/3/26 12:17 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: > >>> STAPM limit, fast/slow PPT limits, skin-temperature TDP limit, slow/STAPM > >>> time constants, and the thermal control target. > >>> > >>> Until now userspace has reached this interface through the acpi_call out- > >>> of-tree module or ryzenadj, which carry no ABI guarantees and no per-device > >>> safety limits. This driver replaces that with a proper in-kernel > >>> implementation that: > >>> > >>> * Exposes all seven parameters through the firmware-attributes sysfs ABI, > >>> so that standard tools (fwupd, systemd-bios-vendor, etc.) can enumerate > >> > >> What is systemd-bios-vendor? I guess I'm not familiar with this and a > >> quick web search didn't turn anything obvious up. > > > > I used some AI assistance to compile this from my userspace > > implementation and the ASEGA pdf from the AMD site. I need to go > > through _everything_ before this moves to non-RFC. Same with > > copyright year. I focused on the implementation doing the things I > > want it to do for now. > > Got it; so it's a made up tool :P > > > > >>> and modify them without device-specific knowledge. > >>> > >>> * Enforces tiered per-device and per-SoC limits. The default "device" > >>> mode restricts writes to a curated safe range (smin..smax) derived from > >>> the device's thermal design. > >> > >> Can you please elaborate where you got all these numbers from? I don't > >> know if they're accurate or not. Someone would probably need to cross > >> reference them to be sure. > > > > Trial and error, research, references from Windows, etc. All of the > > devices in this driver have been tested with a userspace > > implementation using the same limits for ppt/sppt/fppt. Nobody has > > complained about them. To be honest, I usually do not set tctl slow > > and fast time limits, so those are referenced from the Legion Go and > > for tctl I go lower than what manufacturers usually set. Users like > > tctl because some of them like their device to stay cooler. > > > > The big idea for this driver is to allow locking /dev/mem and ACPI. > > Other than the legion go fan curves, and the Zotac Zone driver which > > needs a cleanup, everything else is handled in the kernel now. > > > > But this approach works fine for the Zotac Zone as they seem to be > > using a very simple WMI shim. > > I mean at a high level I conceptually like the idea of getting rid of > the need to use software that manipulates /dev/mem and especially > anything that means people relying on out of tree patches. > > This is a strong balancing act though to ratify interfaces to things > that we don't have a stable ABI contract and documentation of the > implications on getting it wrong. Yes, that's a fair concern. From a practical perspective locking /dev/mem and ACPI is required for secure boot so even if this approach bounces around it is still important for downstream usecases while OOT. This was my primary motivation for writing this. I also tried to address the limit concerns from prior series, which is why the DMI match is strong for stock kernels and the expanded range leeway requires a command line argument and the SoC and unbounded ranges require a re-compile. While the ABI of this V1 RFC is fine, it could also be made to match the lenovo driver by binding skin and stapm limits together and renaming (so it works for both STT and STAPM modes regardless). Then, it matches the convention 1-1. Although, since intel will never use this driver, the motivation here is not strong. To that end, it is still not clear to me for which CPUs the STAPM limit is dropped, which is why its enabled for all of them in this V1. I know the method got deprecated with the Z1 Extreme, but Lenovo brought it back for their Legion Go 1 at least. Antheas > > > >>> An "expanded" mode exposes the full > >>> hardware-validated range. An optional CONFIG_AMD_DPTC_EXTENDED Kconfig > >>> adds "soc" (raw ALIB_PARAMS envelope) and "unbound" tiers for advanced > >>> use. The active tier is itself a firmware-attribute, switchable at > >>> runtime. > >>> > >>> * Stages values and commits them atomically in a single ALIB call, > >>> matching the protocol's intended bulk-update semantics. A save_settings > >>> attribute (per firmware-attributes ABI) controls whether writes commit > >>> immediately ("single" mode) or are held until an explicit "save". > >>> > >>> * When in "single" mode, re-applies staged values after system resume, > >>> so suspend/resume cycles do not silently revert to firmware defaults. > >> > >> This isn't the only interface for setting power limits. How do you make > >> sure that the EC for example isn't stepping on toes on these designs? > > > > For the DMI matched devices it is not. For the ones that are not > > matched, the driver does not autoload and needs a kconfig parameter to > > even load. > > > > OneXPlayer is a bit more complex with their turbo button doing TDP > > swaps but turbo takeover in oxpec takes care of that and then you are > > supposed use ryzenadj. > > > >> I /guess/ it always will need to be opt-in a device by device basis. > >> > >> What happens if the vendor enables PMF in a BIOS update? How does this > >> avoid conflicts? > > > > Some manufacturers enable pmf without implementing the tables. From my > > understanding none of them implement pmf with limits. If a > > manufacturer wants to move to pmf, we can amend the DMI entry with a > > bios match. However, from my understanding, there is no TDP slider > > equivalent for PMF. > > > >>> > >>> 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, Z1, 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. > >>> > >>> @Mario: can you suggest a CC list for V2? Thanks. Even if not merged, this > >>> driver is still good for downstream use. > >> > >> You should include Shyam (AMD), Denis and Derek (community). > > > > Sure. > > > >>> > >>> --- > >>> Usage > >>> ----- > >>> > >>> List all exposed attributes (read-only, no root required): > >>> > >>> $ fwupdmgr get-bios-settings > >>> > >>> This enumerates every attribute under /sys/class/firmware-attributes/, > >>> including current_value, default_value, min_value, max_value, and > >>> display_name for each DPTCi parameter. > >> > >> AFAIK - fwupd doesn't understand "save_settings" today > > > > Yes, I do not expect it to even allow writing, but at least you can > > preview the values which is useful. And from what I saw defaults are > > not shown either. > > > > Antheas > > > >>> > >>> Sysfs direct usage > >>> ------------------ > >>> > >>> All paths are under: > >>> > >>> ATTR=/sys/class/firmware-attributes/amd_dptc/attributes > >>> > >>> Inspect a parameter (no root needed): > >>> > >>> $ cat $ATTR/stapm_limit/{display_name,min_value,max_value,default_value,current_value} > >>> Sustained TDP (mW) > >>> 4000 > >>> 85000 > >>> 25000 > >>> <- empty: nothing staged yet > >>> > >>> Stage values (held in memory, not yet sent to firmware): > >>> > >>> $ echo 25000 | sudo tee $ATTR/stapm_limit/current_value > >>> $ echo 40000 | sudo tee $ATTR/fast_limit/current_value > >>> $ echo 27000 | sudo tee $ATTR/slow_limit/current_value > >>> $ echo 25000 | sudo tee $ATTR/skin_limit/current_value > >>> $ echo 85 | sudo tee $ATTR/temp_target/current_value > >>> > >>> Commit all staged values in one ALIB call: > >>> > >>> $ echo save | sudo tee $ATTR/save_settings > >>> > >>> Switch to auto-commit (each write commits immediately): > >>> > >>> $ echo single | sudo tee $ATTR/save_settings > >>> > >>> Return to bulk mode: > >>> > >>> $ echo bulk | sudo tee $ATTR/save_settings > >>> > >>> Clear a staged value without committing: > >>> > >>> $ echo | sudo tee $ATTR/stapm_limit/current_value > >>> > >>> Query or change the active limit tier (device/expanded/soc/unbound): > >>> > >>> $ cat $ATTR/limit_mode/possible_values > >>> device;expanded;soc;unbound > >>> $ echo expanded | sudo tee $ATTR/limit_mode/current_value > >>> > >>> Switching tiers clears all staged values (old values may fall outside the > >>> new range). Stages and commits must be redone after a mode switch. > >>> > >>> Antheas Kapenekakis (2): > >>> Documentation: firmware-attributes: generalize save_settings entry > >>> platform/x86/amd: Add AMD DPTCi driver > >>> > >>> .../testing/sysfs-class-firmware-attributes | 41 +- > >>> MAINTAINERS | 6 + > >>> drivers/platform/x86/amd/Kconfig | 27 + > >>> drivers/platform/x86/amd/Makefile | 2 + > >>> drivers/platform/x86/amd/dptc.c | 1325 +++++++++++++++++ > >>> 5 files changed, 1386 insertions(+), 15 deletions(-) > >>> create mode 100644 drivers/platform/x86/amd/dptc.c > >>> > >>> > >>> base-commit: c89ce241c1909d2c2bdde88334c33f3000d364fb > >> > >> > > > > ^ permalink raw reply [flat|nested] 18+ messages in thread
end of thread, other threads:[~2026-03-03 23:48 UTC | newest] Thread overview: 18+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-03-03 18:17 [RFC v1 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Antheas Kapenekakis 2026-03-03 18:17 ` [RFC v1 1/2] Documentation: firmware-attributes: generalize save_settings entry Antheas Kapenekakis 2026-03-03 18:17 ` [RFC v1 2/2] platform/x86/amd: Add AMD DPTCi driver Antheas Kapenekakis 2026-03-03 20:10 ` Mario Limonciello (AMD) (kernel.org) 2026-03-03 20:40 ` Antheas Kapenekakis 2026-03-03 20:54 ` Mario Limonciello (AMD) (kernel.org) 2026-03-03 21:20 ` Antheas Kapenekakis 2026-03-03 21:44 ` Sasha Levin 2026-03-03 22:08 ` Antheas Kapenekakis 2026-03-03 18:59 ` [RFC v1 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Mario Limonciello 2026-03-03 19:16 ` Antheas Kapenekakis 2026-03-03 19:23 ` Antheas Kapenekakis 2026-03-03 19:27 ` Armin Wolf 2026-03-03 19:34 ` Antheas Kapenekakis 2026-03-03 21:50 ` Armin Wolf 2026-03-03 23:47 ` Antheas Kapenekakis 2026-03-03 19:51 ` Mario Limonciello (AMD) (kernel.org) 2026-03-03 20:04 ` 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.