* [RFC v2 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls
@ 2026-03-05 18:17 Antheas Kapenekakis
2026-03-05 18:17 ` [RFC v2 1/2] Documentation: firmware-attributes: generalize save_settings entry Antheas Kapenekakis
2026-03-05 18:17 ` [RFC v2 2/2] platform/x86/amd: Add AMD DPTCi driver Antheas Kapenekakis
0 siblings, 2 replies; 8+ messages in thread
From: Antheas Kapenekakis @ 2026-03-05 18:17 UTC (permalink / raw)
To: Mario.Limonciello
Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato,
linux-kernel, platform-driver-x86, Antheas Kapenekakis
Many AMD-based handheld PCs (GPD, AYANEO, OneXPlayer, AOKZOE, OrangePi)
ship with the AGESA ALIB method at \_SB.ALIB, which accepts Function 0x0C
(the Dynamic Power and Thermal Configuration Interface, DPTCi). This
allows software to adjust APU power and thermal parameters at runtime:
sustained power limit (SPL/STAPM + skin), slow PPT, fast PPT, and the
thermal control target.
Unlike mainstream AMD laptops, these devices do not implement vendor-
specific WMI or EC hooks for TDP control. The ones that do, use DPTCi
under the hood. For these devices, ALIB is the only viable mechanism for
the OS to adjust power limits, making a dedicated kernel driver the
correct approach rather than relying on the out-of-tree acpi_call module
or ryzenadj.
The driver provides two layers of control:
* Platform profile integration (low-power / balanced / performance /
custom), with per-device preset tunings derived from thermal envelope
data. Selecting a non-custom profile applies values immediately and
locks the individual tunables to read-only. The default profile is
"custom", leaving the device at firmware defaults until userspace
explicitly selects a profile. Certain profiles feature a "max_power"
preset when it is deemed that using the maximum values for performance
would lead to a degraded user experience (e.g. excessive fan noise)
* Four firmware-attributes tunables (ppt_pl1_spl, ppt_pl2_sppt,
ppt_pl3_fppt, cpu_temp) that become writable in "custom" mode.
Values are enforced against per-device limits (smin..smax), with an
"expanded_limits" toggle that widens the range to the full hardware-
validated envelope (min..max). A save_settings attribute controls
whether writes commit immediately ("single") or are staged for an
explicit bulk "save".
On resume, the active profile or staged values are re-applied so that
suspend/resume cycles do not silently revert to firmware defaults unless
in custom and in bulk mode. In that case, defer to userspace.
Device limits are supplied for GPD Win Mini / Win 4 / Win 5 / Win Max 2 /
Duo / Pocket 4, OrangePi NEO-01, AOKZOE A1/A2, OneXPlayer F1/2/X1/G1,
and numerous AYANEO models. The SoC table covers Ryzen 5000, 6000, 7040,
8000, 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.
I expand the CC list here to include Sasha (you might be interested,
feel free to ask to be dropped), Derek + Shyam + Denis as requested.
Responsible disclosure: The development of this driver is AI assisted.
This includes its writing, testing, and readmes. The driver was manually
reviewed line by line, but there still may small leftover quirks. This is
an RFC, not a final version. Let's push these tools to their limits and
see where it takes us.
Assisted-by: Claude:claude-opus-4-6
---
Changes in v2:
- Use a platform_device base instead of raw inits + exit, hook into devm
helpers referencing samsung-galaxybook
- Add platform_profile support (low-power / balanced / performance /
custom) with per-device energy presets; non-custom profiles lock
tunables to read-only. We default to custom to avoid writing values
- Reduce exposed parameters from seven to four (ppt_pl1_spl,
ppt_pl2_sppt, ppt_pl3_fppt, cpu_temp); drop time constants and
separate skin/STAPM limits in favour of a single SPL that sets both.
For devices where the max tdp offers thermals that are not suitable
for day to day use (e.g., excessive fan noise), MAX_POWER is added
- Remove CONFIG_AMD_DPTC_EXTENDED and the "soc"/"unbound" limit tiers;
keep only device (smin..smax) and expanded (min..max) and soc match
(certain devices ship multiple SoCs on the same motherboard/thermal
envelope, make sure we only hook into validated SoCs)
- Rename "commit" attribute to "save_settings" per firmware-attributes
ABI; rename limit_mode to expanded_limits
- Change expanded_limits attribute type from "enumeration" to "integer"
(min=0, max=1) since firmware-attributes has no bool type
- Remove all global vars, limit _dev access to init and exit
- Use u32 accessors to set values for ALIB call
- Clean up verbose comments throughout
- Add Ayn Loki Max / Tectoy Zeenix Pro
V1: https://lore.kernel.org/all/20260303181707.2920261-1-lkml@antheas.dev/
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 | 16 +
drivers/platform/x86/amd/Makefile | 2 +
drivers/platform/x86/amd/dptc.c | 1255 +++++++++++++++++
5 files changed, 1305 insertions(+), 15 deletions(-)
create mode 100644 drivers/platform/x86/amd/dptc.c
base-commit: c89ce241c1909d2c2bdde88334c33f3000d364fb
--
2.52.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [RFC v2 1/2] Documentation: firmware-attributes: generalize save_settings entry
2026-03-05 18:17 [RFC v2 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Antheas Kapenekakis
@ 2026-03-05 18:17 ` Antheas Kapenekakis
2026-03-05 18:17 ` [RFC v2 2/2] platform/x86/amd: Add AMD DPTCi driver Antheas Kapenekakis
1 sibling, 0 replies; 8+ messages in thread
From: Antheas Kapenekakis @ 2026-03-05 18:17 UTC (permalink / raw)
To: Mario.Limonciello
Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato,
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 [flat|nested] 8+ messages in thread
* [RFC v2 2/2] platform/x86/amd: Add AMD DPTCi driver
2026-03-05 18:17 [RFC v2 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Antheas Kapenekakis
2026-03-05 18:17 ` [RFC v2 1/2] Documentation: firmware-attributes: generalize save_settings entry Antheas Kapenekakis
@ 2026-03-05 18:17 ` Antheas Kapenekakis
2026-03-05 20:57 ` Mario Limonciello
2026-03-06 21:14 ` Derek John Clark
1 sibling, 2 replies; 8+ messages in thread
From: Antheas Kapenekakis @ 2026-03-05 18:17 UTC (permalink / raw)
To: Mario.Limonciello
Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato,
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 matches the ABI of asus-armoury, by exposing a platform
profile with reasonable tunings for devices depending on their max
thermal envelope, and {ppt_pl1_spl,ppt_pl2_sppt,ppt_pl3_fppt,cpu_temp}
tunables which only become writable when the profile is custom,
otherwise they are read-only and reflect the current profile.
The default profile is custom so that we do not write to the device
until userspace explicitly selects a profile, remaining at firmware
defaults.
Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
---
MAINTAINERS | 6 +
drivers/platform/x86/amd/Kconfig | 16 +
drivers/platform/x86/amd/Makefile | 2 +
drivers/platform/x86/amd/dptc.c | 1255 +++++++++++++++++++++++++++++
4 files changed, 1279 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..ee79a2c084a8 100644
--- a/drivers/platform/x86/amd/Kconfig
+++ b/drivers/platform/x86/amd/Kconfig
@@ -44,3 +44,19 @@ 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 ACPI_PLATFORM_PROFILE
+ select FIRMWARE_ATTRIBUTES_CLASS
+ help
+ Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
+ Thermal Configuration Interface (DPTCi). Exposes TDP and thermal
+ parameters for AMD APU-based handheld devices via the
+ firmware-attributes sysfs ABI, allowing userspace tools to stage
+ and atomically commit power limit settings. Requires a DMI match
+ for the device and a recognized AMD SoC.
+
+ If built as a module, the module will be called amd_dptc.
+
diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile
index f6ff0c837f34..862a609bfe38 100644
--- a/drivers/platform/x86/amd/Makefile
+++ b/drivers/platform/x86/amd/Makefile
@@ -12,3 +12,5 @@ obj-$(CONFIG_AMD_PMF) += pmf/
obj-$(CONFIG_AMD_WBRF) += wbrf.o
obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o
obj-$(CONFIG_AMD_HFI) += hfi/
+obj-$(CONFIG_AMD_DPTC) += amd_dptc.o
+amd_dptc-y := dptc.o
diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
new file mode 100644
index 000000000000..68d9d3b85cb6
--- /dev/null
+++ b/drivers/platform/x86/amd/dptc.c
@@ -0,0 +1,1255 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver
+ *
+ * Exposes AMD APU power and thermal parameters via the firmware-attributes
+ * sysfs ABI. Parameters are staged and atomically committed through the
+ * AGESA ALIB Function 0x0C (Dynamic Power and Thermal Configuration
+ * interface).
+ *
+ * Reference: AMD AGESA Publication #44065, Appendix E.5
+ * https://docs.amd.com/v/u/en-US/44065_Arch2008
+ *
+ * Copyright (C) 2026 Antheas Kapenekakis <lkml@antheas.dev>
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/acpi.h>
+#include <linux/cleanup.h>
+#include <linux/dmi.h>
+#include <linux/init.h>
+#include <linux/kobject.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/platform_device.h>
+#include <linux/platform_profile.h>
+#include <linux/processor.h>
+#include <linux/sysfs.h>
+#include <linux/unaligned.h>
+
+#include "../firmware_attributes_class.h"
+
+#define DRIVER_NAME "amd_dptc"
+
+#define ALIB_FUNC_DPTC 0x0C
+#define ALIB_PATH "\\_SB.ALIB"
+
+/* ALIB parameter IDs (AGESA spec Appendix E.5, Table E-52) */
+#define ALIB_ID_TEMP_TARGET 0x03
+#define ALIB_ID_STAPM_LIMIT 0x05
+#define ALIB_ID_FAST_LIMIT 0x06
+#define ALIB_ID_SLOW_LIMIT 0x07
+#define ALIB_ID_SKIN_LIMIT 0x2E
+
+enum dptc_param_idx {
+ DPTC_PPT_PL1_SPL, /* STAPM + skin limit (set together) */
+ DPTC_PPT_PL2_SPPT, /* slow PPT limit */
+ DPTC_PPT_PL3_FPPT, /* fast PPT limit */
+ DPTC_CPU_TEMP, /* thermal control target */
+ DPTC_NUM_PARAMS,
+};
+
+struct dptc_param_limits {
+ u32 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_profile {
+ u32 vals[DPTC_NUM_PARAMS]; /* 0 = don't set / unstage this param */
+};
+
+struct dptc_device_limits {
+ struct dptc_param_limits params[DPTC_NUM_PARAMS];
+ struct dptc_profile profiles[PLATFORM_PROFILE_LAST];
+};
+
+struct dptc_param_desc {
+ const char *name;
+ const char *display_name;
+ u8 param_id;
+ u8 param_id2; /* secondary ALIB ID, 0 if none */
+};
+
+static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = {
+ [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (mW)",
+ ALIB_ID_STAPM_LIMIT, ALIB_ID_SKIN_LIMIT },
+ [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (mW)",
+ ALIB_ID_SLOW_LIMIT },
+ [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (mW)",
+ ALIB_ID_FAST_LIMIT },
+ [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)",
+ ALIB_ID_TEMP_TARGET },
+};
+
+/* 18W class: AYANEO AIR Plus (Ryzen 5 5560U) */
+static const struct dptc_device_limits limits_18w = { .params = {
+ [DPTC_PPT_PL1_SPL] = { 0, 5000, 15000, 18000, 22000 },
+ [DPTC_PPT_PL2_SPPT] = { 0, 5000, 15000, 18000, 22000 },
+ [DPTC_PPT_PL3_FPPT] = { 0, 5000, 15000, 20000, 25000 },
+ [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
+}, .profiles = {
+ [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5000, 5000, 8000, 0 } },
+ [PLATFORM_PROFILE_BALANCED] = { .vals = { 12000, 14000, 15000, 0 } },
+ [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 18000, 18000, 20000, 0 } },
+}};
+
+/* 25W class: Ryzen 5000 handhelds (AYANEO NEXT, KUN) */
+static const struct dptc_device_limits limits_25w = { .params = {
+ [DPTC_PPT_PL1_SPL] = { 0, 4000, 15000, 25000, 32000 },
+ [DPTC_PPT_PL2_SPPT] = { 0, 4000, 20000, 27000, 35000 },
+ [DPTC_PPT_PL3_FPPT] = { 0, 4000, 25000, 30000, 37000 },
+ [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
+}, .profiles = {
+ [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8000, 8000, 12000, 0 } },
+ [PLATFORM_PROFILE_BALANCED] = { .vals = { 15000, 17000, 20000, 0 } },
+ [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25000, 27000, 30000, 0 } },
+}};
+
+/* 28W class: GPD Win series, AYANEO 2, OrangePi NEO-01 */
+static const struct dptc_device_limits limits_28w = { .params = {
+ [DPTC_PPT_PL1_SPL] = { 0, 4000, 15000, 28000, 32000 },
+ [DPTC_PPT_PL2_SPPT] = { 0, 4000, 20000, 30000, 35000 },
+ [DPTC_PPT_PL3_FPPT] = { 0, 4000, 25000, 32000, 37000 },
+ [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
+}, .profiles = {
+ [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8000, 8000, 12000, 0 } },
+ [PLATFORM_PROFILE_BALANCED] = { .vals = { 15000, 17000, 22000, 0 } },
+ [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25000, 28000, 32000, 0 } },
+ [PLATFORM_PROFILE_MAX_POWER] = { .vals = { 28000, 30000, 32000, 0 } },
+}};
+
+/* 30W class: OneXPlayer, AYANEO AIR/FLIP/GEEK/SLIDE/3, AOKZOE */
+static const struct dptc_device_limits limits_30w = { .params = {
+ [DPTC_PPT_PL1_SPL] = { 0, 4000, 15000, 30000, 40000 },
+ [DPTC_PPT_PL2_SPPT] = { 0, 4000, 20000, 32000, 43000 },
+ [DPTC_PPT_PL3_FPPT] = { 0, 4000, 25000, 41000, 50000 },
+ [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
+}, .profiles = {
+ [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8000, 8000, 12000, 0 } },
+ [PLATFORM_PROFILE_BALANCED] = { .vals = { 15000, 17000, 25000, 0 } },
+ [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25000, 28000, 41000, 0 } },
+ [PLATFORM_PROFILE_MAX_POWER] = { .vals = { 30000, 32000, 41000, 0 } },
+}};
+
+/* AI MAX Handheld class: GPD Win 5 */
+static const struct dptc_device_limits limits_maxhh = { .params = {
+ [DPTC_PPT_PL1_SPL] = { 0, 4000, 25000, 80000, 100000 },
+ [DPTC_PPT_PL2_SPPT] = { 0, 4000, 27000, 82000, 100000 },
+ [DPTC_PPT_PL3_FPPT] = { 0, 4000, 40000, 85000, 100000 },
+ [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 },
+}, .profiles = {
+ [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 15000, 15000, 25000, 0 } },
+ [PLATFORM_PROFILE_BALANCED] = { .vals = { 25000, 27000, 40000, 0 } },
+ [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 60000, 63000, 85000, 0 } },
+ [PLATFORM_PROFILE_MAX_POWER] = { .vals = { 80000, 82000, 85000, 0 } },
+}};
+
+/* Substring matches against boot_cpu_data.x86_model_id; order matters. */
+static const char * const dptc_soc_table[] = {
+ /* AI MAX */
+ "AMD RYZEN AI MAX+ 395",
+ "AMD RYZEN AI MAX+ 385",
+ "AMD RYZEN AI MAX 380",
+ /* Ryzen AI */
+ "AMD Ryzen AI 9 HX 370",
+ "AMD Ryzen AI HX 360",
+ /* Z1 - Extreme before plain Z1 */
+ "AMD Ryzen Z1 Extreme",
+ "AMD Ryzen Z1",
+ /* Ryzen 8000 */
+ "AMD Ryzen 7 8840U",
+ /* Ryzen 7040 */
+ "AMD Ryzen 7 7840U",
+ /* Ryzen 6000 */
+ "AMD Ryzen 7 6800U",
+ "AMD Ryzen 7 6600U",
+ /* Ryzen 5000 */
+ "AMD Ryzen 7 5800U",
+ "AMD Ryzen 7 5700U",
+ "AMD Ryzen 5 5560U",
+ NULL,
+};
+
+static const struct dmi_system_id dptc_dmi_table[] = {
+ /* GPD */
+ {
+ .ident = "GPD Win Mini",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
+ DMI_MATCH(DMI_PRODUCT_NAME, "G1617-01"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ {
+ .ident = "GPD Win Mini 2024",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
+ DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ {
+ .ident = "GPD Win Mini 2024",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
+ DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02-L"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ {
+ .ident = "GPD Win 4",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
+ DMI_MATCH(DMI_PRODUCT_NAME, "G1618-04"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ {
+ .ident = "GPD Win 5",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
+ DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"),
+ },
+ .driver_data = (void *)&limits_maxhh,
+ },
+ {
+ .ident = "GPD Win Max 2",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
+ DMI_MATCH(DMI_PRODUCT_NAME, "G1619-04"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ {
+ .ident = "GPD Win Max 2 2024",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
+ DMI_MATCH(DMI_PRODUCT_NAME, "G1619-05"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ {
+ .ident = "GPD Duo",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
+ DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ {
+ .ident = "GPD Duo",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
+ DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01-L"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ {
+ .ident = "GPD Pocket 4",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
+ DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ {
+ .ident = "GPD Pocket 4",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
+ DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04-L"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ /* OrangePi */
+ {
+ .ident = "OrangePi NEO-01",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ /* AYN */
+ {
+ .ident = "AYN Loki Max",
+ .matches = {
+ DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ayn"),
+ DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Loki Max"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ /* Tectoy (Zeenix Pro = Loki Max) */
+ {
+ .ident = "Zeenix Pro",
+ .matches = {
+ DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Tectoy"),
+ DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Zeenix Pro"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ /* AOKZOE */
+ {
+ .ident = "AOKZOE A1 AR07",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 AR07"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ {
+ .ident = "AOKZOE A1 Pro",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 Pro"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ {
+ .ident = "AOKZOE A1X",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1X"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ {
+ .ident = "AOKZOE A2 Pro",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A2 Pro"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ /* OneXPlayer (Intel variants filtered by SoC table) */
+ {
+ .ident = "ONEXPLAYER F1Pro",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1Pro"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ {
+ .ident = "ONEXPLAYER F1 EVA-02",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1 EVA-02"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ {
+ .ident = "ONEXPLAYER 2",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
+ DMI_MATCH(DMI_BOARD_NAME, "ONEXPLAYER 2"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ {
+ .ident = "ONEXPLAYER X1 A",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1 A"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ {
+ .ident = "ONEXPLAYER X1z",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1z"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ {
+ .ident = "ONEXPLAYER X1Pro",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Pro"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ {
+ .ident = "ONEXPLAYER G1 A",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 A"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ /* AYANEO - 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,
+ },
+ /* AYANEO - 25W */
+ {
+ .ident = "AYANEO NEXT Advance",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Advance"),
+ },
+ .driver_data = (void *)&limits_25w,
+ },
+ {
+ .ident = "AYANEO NEXT Lite",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Lite"),
+ },
+ .driver_data = (void *)&limits_25w,
+ },
+ {
+ .ident = "AYANEO NEXT Pro",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Pro"),
+ },
+ .driver_data = (void *)&limits_25w,
+ },
+ {
+ .ident = "AYANEO NEXT",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT"),
+ },
+ .driver_data = (void *)&limits_25w,
+ },
+ {
+ .ident = "AYANEO KUN",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"),
+ },
+ .driver_data = (void *)&limits_25w,
+ },
+ {
+ .ident = "AYANEO KUN",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO KUN"),
+ },
+ .driver_data = (void *)&limits_25w,
+ },
+ /* AYANEO - 28W */
+ {
+ .ident = "AYANEO 2",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_MATCH(DMI_BOARD_NAME, "AYANEO 2"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ {
+ .ident = "SuiPlay0X1",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "Mysten Labs, Inc."),
+ DMI_MATCH(DMI_PRODUCT_NAME, "SuiPlay0X1"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ /* AYANEO - 30W */
+ {
+ /* 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);
+
+struct dptc_priv;
+
+struct dptc_attr_sysfs {
+ struct dptc_priv *priv;
+ struct kobj_attribute current_value;
+ struct kobj_attribute default_value;
+ struct kobj_attribute min_value;
+ struct kobj_attribute max_value;
+ struct kobj_attribute scalar_increment;
+ struct kobj_attribute display_name;
+ struct kobj_attribute type;
+ struct attribute *attrs[8];
+ struct attribute_group group;
+ int idx;
+};
+
+struct dptc_priv {
+ struct device *fw_attr_dev;
+ struct kset *fw_attr_kset;
+
+ const struct dptc_device_limits *dev_limits;
+
+ bool expanded;
+
+ enum platform_profile_option profile;
+ struct device *ppdev;
+
+ enum dptc_save_mode { SAVE_SINGLE, SAVE_BULK } save_mode;
+
+ u32 staged[DPTC_NUM_PARAMS];
+ bool has_staged[DPTC_NUM_PARAMS];
+
+ /* Protects mutable driver state */
+ struct mutex lock;
+
+ struct dptc_attr_sysfs params[DPTC_NUM_PARAMS];
+ struct dptc_attr_sysfs expanded_attr;
+ struct kobj_attribute save_settings_attr;
+};
+
+static struct platform_device *dptc_pdev;
+
+static u32 dptc_get_min(struct dptc_priv *dptc, int idx)
+{
+ return dptc->expanded ? dptc->dev_limits->params[idx].min
+ : dptc->dev_limits->params[idx].smin;
+}
+
+static u32 dptc_get_max(struct dptc_priv *dptc, int idx)
+{
+ return dptc->expanded ? dptc->dev_limits->params[idx].max
+ : dptc->dev_limits->params[idx].smax;
+}
+
+static u32 dptc_get_default(struct dptc_priv *dptc, int idx)
+{
+ return dptc->dev_limits->params[idx].def;
+}
+
+static int dptc_alib_call(const u8 *ids, const u32 *vals, int count)
+{
+ union acpi_object in_params[2];
+ struct acpi_object_list input;
+ acpi_status status;
+ u32 buf_size;
+ int i, off;
+ u8 *buf;
+
+ if (count == 0)
+ return -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;
+
+ put_unaligned_le16(buf_size, buf);
+
+ for (i = 0; i < count; i++) {
+ off = 2 + i * 5;
+ buf[off] = ids[i];
+ put_unaligned_le32(vals[i], buf + off + 1);
+ }
+
+ in_params[0].type = ACPI_TYPE_INTEGER;
+ in_params[0].integer.value = ALIB_FUNC_DPTC;
+ in_params[1].type = ACPI_TYPE_BUFFER;
+ in_params[1].buffer.length = buf_size;
+ in_params[1].buffer.pointer = buf;
+
+ input.count = 2;
+ input.pointer = in_params;
+
+ status = acpi_evaluate_object(NULL, ALIB_PATH, &input, NULL);
+ kfree(buf);
+
+ if (ACPI_FAILURE(status)) {
+ pr_err("ALIB call failed: %s\n",
+ acpi_format_exception(status));
+ return -EIO;
+ }
+
+ pr_debug("sent %d ALIB parameter(s)\n", count);
+ return 0;
+}
+
+static int dptc_alib_send_one(int idx, u32 val)
+{
+ u32 vals[2];
+ u8 ids[2];
+ int count = 0;
+
+ ids[count] = dptc_params[idx].param_id;
+ vals[count] = val;
+ count++;
+ if (dptc_params[idx].param_id2) {
+ ids[count] = dptc_params[idx].param_id2;
+ vals[count] = val;
+ count++;
+ }
+
+ return dptc_alib_call(ids, vals, count);
+}
+
+static int dptc_alib_save(struct dptc_priv *dptc)
+{
+ u32 vals[DPTC_NUM_PARAMS * 2];
+ u8 ids[DPTC_NUM_PARAMS * 2];
+ int i, count = 0;
+
+ 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 (dptc_params[i].param_id2) {
+ ids[count] = dptc_params[i].param_id2;
+ vals[count] = dptc->staged[i];
+ count++;
+ }
+ }
+
+ if (!count)
+ return 0;
+
+ return dptc_alib_call(ids, vals, count);
+}
+
+/* Sysfs callbacks */
+
+static ssize_t dptc_current_value_show(struct kobject *kobj,
+ struct kobj_attribute *attr, char *buf)
+{
+ struct dptc_attr_sysfs *ps =
+ container_of(attr, struct dptc_attr_sysfs, current_value);
+ struct dptc_priv *dptc = ps->priv;
+
+ guard(mutex)(&dptc->lock);
+
+ if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
+ u32 val = dptc->dev_limits->profiles[dptc->profile].vals[ps->idx];
+
+ if (!val)
+ return sysfs_emit(buf, "\n");
+ return sysfs_emit(buf, "%u\n", val);
+ }
+
+ if (!dptc->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_attr_sysfs *ps =
+ container_of(attr, struct dptc_attr_sysfs, current_value);
+ struct dptc_priv *dptc = ps->priv;
+ u32 val, min, max;
+ int ret;
+
+ if (count == 1 && buf[0] == '\n') {
+ guard(mutex)(&dptc->lock);
+
+ if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
+ return -EPERM;
+ dptc->has_staged[ps->idx] = false;
+ return count;
+ }
+
+ ret = kstrtou32(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ guard(mutex)(&dptc->lock);
+
+ if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
+ return -EPERM;
+ min = dptc_get_min(dptc, ps->idx);
+ max = dptc_get_max(dptc, ps->idx);
+ if (val < min || (max && val > max))
+ return -EINVAL;
+ dptc->staged[ps->idx] = val;
+ dptc->has_staged[ps->idx] = true;
+ if (dptc->save_mode == SAVE_SINGLE)
+ ret = dptc_alib_send_one(ps->idx, val);
+
+ return ret ? ret : count;
+}
+
+static ssize_t dptc_default_value_show(struct kobject *kobj,
+ struct kobj_attribute *attr, char *buf)
+{
+ struct dptc_attr_sysfs *ps =
+ container_of(attr, struct dptc_attr_sysfs, default_value);
+
+ return sysfs_emit(buf, "%u\n", dptc_get_default(ps->priv, ps->idx));
+}
+
+static ssize_t dptc_min_value_show(struct kobject *kobj,
+ struct kobj_attribute *attr, char *buf)
+{
+ struct dptc_attr_sysfs *ps =
+ container_of(attr, struct dptc_attr_sysfs, min_value);
+ struct dptc_priv *dptc = ps->priv;
+
+ guard(mutex)(&dptc->lock);
+
+ return sysfs_emit(buf, "%u\n", dptc_get_min(dptc, ps->idx));
+}
+
+static ssize_t dptc_max_value_show(struct kobject *kobj,
+ struct kobj_attribute *attr, char *buf)
+{
+ struct dptc_attr_sysfs *ps =
+ container_of(attr, struct dptc_attr_sysfs, max_value);
+ struct dptc_priv *dptc = ps->priv;
+
+ guard(mutex)(&dptc->lock);
+
+ return sysfs_emit(buf, "%u\n", dptc_get_max(dptc, ps->idx));
+}
+
+static ssize_t dptc_scalar_increment_show(struct kobject *kobj,
+ struct kobj_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "1\n");
+}
+
+static ssize_t dptc_display_name_show(struct kobject *kobj,
+ struct kobj_attribute *attr, char *buf)
+{
+ struct dptc_attr_sysfs *ps =
+ container_of(attr, struct dptc_attr_sysfs, display_name);
+ return sysfs_emit(buf, "%s\n", dptc_params[ps->idx].display_name);
+}
+
+static ssize_t dptc_type_show(struct kobject *kobj,
+ struct kobj_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "integer\n");
+}
+
+static ssize_t dptc_save_settings_show(struct kobject *kobj,
+ struct kobj_attribute *attr, char *buf)
+{
+ struct dptc_priv *dptc =
+ container_of(attr, struct dptc_priv, save_settings_attr);
+
+ guard(mutex)(&dptc->lock);
+
+ if (dptc->save_mode == SAVE_SINGLE)
+ return sysfs_emit(buf, "single\n");
+ return sysfs_emit(buf, "bulk\n");
+}
+
+static ssize_t dptc_save_settings_store(struct kobject *kobj,
+ struct kobj_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct dptc_priv *dptc =
+ container_of(attr, struct dptc_priv, save_settings_attr);
+ int ret = 0;
+
+ guard(mutex)(&dptc->lock);
+
+ if (sysfs_streq(buf, "save"))
+ ret = dptc_alib_save(dptc);
+ else if (sysfs_streq(buf, "single"))
+ dptc->save_mode = SAVE_SINGLE;
+ else if (sysfs_streq(buf, "bulk"))
+ dptc->save_mode = SAVE_BULK;
+ else
+ return -EINVAL;
+
+ return ret ? ret : count;
+}
+
+static ssize_t dptc_expanded_current_value_show(struct kobject *kobj,
+ struct kobj_attribute *attr,
+ char *buf)
+{
+ struct dptc_attr_sysfs *ps =
+ container_of(attr, struct dptc_attr_sysfs, current_value);
+ struct dptc_priv *dptc = ps->priv;
+
+ guard(mutex)(&dptc->lock);
+
+ return sysfs_emit(buf, "%d\n", dptc->expanded);
+}
+
+static ssize_t dptc_expanded_current_value_store(struct kobject *kobj,
+ struct kobj_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct dptc_attr_sysfs *ps =
+ container_of(attr, struct dptc_attr_sysfs, current_value);
+ struct dptc_priv *dptc = ps->priv;
+ bool val;
+ int ret;
+
+ ret = kstrtobool(buf, &val);
+ if (ret)
+ return ret;
+
+ guard(mutex)(&dptc->lock);
+
+ if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
+ return -EPERM;
+ dptc->expanded = val;
+ /* Clear staged values: limits changed, old values may be out of range */
+ memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
+
+ return count;
+}
+
+static ssize_t dptc_expanded_default_value_show(struct kobject *kobj,
+ struct kobj_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "0\n");
+}
+
+static ssize_t dptc_expanded_min_value_show(struct kobject *kobj,
+ struct kobj_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "0\n");
+}
+
+static ssize_t dptc_expanded_max_value_show(struct kobject *kobj,
+ struct kobj_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "1\n");
+}
+
+static ssize_t dptc_expanded_scalar_increment_show(struct kobject *kobj,
+ struct kobj_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "1\n");
+}
+
+static ssize_t dptc_expanded_display_name_show(struct kobject *kobj,
+ struct kobj_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "Expanded Limits\n");
+}
+
+static ssize_t dptc_expanded_type_show(struct kobject *kobj,
+ struct kobj_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "integer\n");
+}
+
+/* Sysfs setup */
+
+static void dptc_setup_param_sysfs(struct dptc_priv *dptc,
+ struct dptc_attr_sysfs *ps, int idx)
+{
+ ps->priv = dptc;
+ ps->idx = idx;
+
+ sysfs_attr_init(&ps->current_value.attr);
+ ps->current_value.attr.name = "current_value";
+ ps->current_value.attr.mode = 0644;
+ ps->current_value.show = dptc_current_value_show;
+ ps->current_value.store = dptc_current_value_store;
+
+ sysfs_attr_init(&ps->default_value.attr);
+ ps->default_value.attr.name = "default_value";
+ ps->default_value.attr.mode = 0444;
+ ps->default_value.show = dptc_default_value_show;
+
+ sysfs_attr_init(&ps->min_value.attr);
+ ps->min_value.attr.name = "min_value";
+ ps->min_value.attr.mode = 0444;
+ ps->min_value.show = dptc_min_value_show;
+
+ sysfs_attr_init(&ps->max_value.attr);
+ ps->max_value.attr.name = "max_value";
+ ps->max_value.attr.mode = 0444;
+ ps->max_value.show = dptc_max_value_show;
+
+ sysfs_attr_init(&ps->scalar_increment.attr);
+ ps->scalar_increment.attr.name = "scalar_increment";
+ ps->scalar_increment.attr.mode = 0444;
+ ps->scalar_increment.show = dptc_scalar_increment_show;
+
+ sysfs_attr_init(&ps->display_name.attr);
+ ps->display_name.attr.name = "display_name";
+ ps->display_name.attr.mode = 0444;
+ ps->display_name.show = dptc_display_name_show;
+
+ sysfs_attr_init(&ps->type.attr);
+ ps->type.attr.name = "type";
+ ps->type.attr.mode = 0444;
+ ps->type.show = dptc_type_show;
+
+ ps->attrs[0] = &ps->current_value.attr;
+ ps->attrs[1] = &ps->default_value.attr;
+ ps->attrs[2] = &ps->min_value.attr;
+ ps->attrs[3] = &ps->max_value.attr;
+ ps->attrs[4] = &ps->scalar_increment.attr;
+ ps->attrs[5] = &ps->display_name.attr;
+ ps->attrs[6] = &ps->type.attr;
+ ps->attrs[7] = NULL;
+
+ ps->group.name = dptc_params[idx].name;
+ ps->group.attrs = ps->attrs;
+}
+
+static void dptc_setup_expanded_sysfs(struct dptc_priv *dptc,
+ struct dptc_attr_sysfs *ps)
+{
+ ps->priv = dptc;
+ sysfs_attr_init(&ps->current_value.attr);
+ ps->current_value.attr.name = "current_value";
+ ps->current_value.attr.mode = 0644;
+ ps->current_value.show = dptc_expanded_current_value_show;
+ ps->current_value.store = dptc_expanded_current_value_store;
+
+ sysfs_attr_init(&ps->default_value.attr);
+ ps->default_value.attr.name = "default_value";
+ ps->default_value.attr.mode = 0444;
+ ps->default_value.show = dptc_expanded_default_value_show;
+
+ sysfs_attr_init(&ps->min_value.attr);
+ ps->min_value.attr.name = "min_value";
+ ps->min_value.attr.mode = 0444;
+ ps->min_value.show = dptc_expanded_min_value_show;
+
+ sysfs_attr_init(&ps->max_value.attr);
+ ps->max_value.attr.name = "max_value";
+ ps->max_value.attr.mode = 0444;
+ ps->max_value.show = dptc_expanded_max_value_show;
+
+ sysfs_attr_init(&ps->scalar_increment.attr);
+ ps->scalar_increment.attr.name = "scalar_increment";
+ ps->scalar_increment.attr.mode = 0444;
+ ps->scalar_increment.show = dptc_expanded_scalar_increment_show;
+
+ sysfs_attr_init(&ps->display_name.attr);
+ ps->display_name.attr.name = "display_name";
+ ps->display_name.attr.mode = 0444;
+ ps->display_name.show = dptc_expanded_display_name_show;
+
+ sysfs_attr_init(&ps->type.attr);
+ ps->type.attr.name = "type";
+ ps->type.attr.mode = 0444;
+ ps->type.show = dptc_expanded_type_show;
+
+ ps->attrs[0] = &ps->current_value.attr;
+ ps->attrs[1] = &ps->default_value.attr;
+ ps->attrs[2] = &ps->min_value.attr;
+ ps->attrs[3] = &ps->max_value.attr;
+ ps->attrs[4] = &ps->scalar_increment.attr;
+ ps->attrs[5] = &ps->display_name.attr;
+ ps->attrs[6] = &ps->type.attr;
+ ps->attrs[7] = NULL;
+
+ ps->group.name = "expanded_limits";
+ ps->group.attrs = ps->attrs;
+}
+
+static void dptc_fw_dev_unregister(void *data)
+{
+ device_unregister(data);
+}
+
+static void dptc_kset_unregister(void *data)
+{
+ kset_unregister(data);
+}
+
+/* Platform profile */
+
+static void dptc_apply_profile(struct dptc_priv *dptc,
+ enum platform_profile_option profile)
+{
+ const struct dptc_profile *pp;
+ int i;
+
+ memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
+
+ if (profile == PLATFORM_PROFILE_CUSTOM)
+ return;
+
+ pp = &dptc->dev_limits->profiles[profile];
+ for (i = 0; i < DPTC_NUM_PARAMS; i++) {
+ if (!pp->vals[i])
+ continue;
+ dptc->staged[i] = pp->vals[i];
+ dptc->has_staged[i] = true;
+ }
+}
+
+static int dptc_pp_probe(void *drvdata, unsigned long *choices)
+{
+ struct dptc_priv *dptc = drvdata;
+ int i, j;
+
+ set_bit(PLATFORM_PROFILE_CUSTOM, choices);
+ for (i = 0; i < PLATFORM_PROFILE_LAST; i++) {
+ for (j = 0; j < DPTC_NUM_PARAMS; j++) {
+ if (dptc->dev_limits->profiles[i].vals[j]) {
+ set_bit(i, choices);
+ break;
+ }
+ }
+ }
+ return 0;
+}
+
+static int dptc_pp_get(struct device *dev,
+ enum platform_profile_option *profile)
+{
+ struct dptc_priv *dptc = dev_get_drvdata(dev);
+
+ guard(mutex)(&dptc->lock);
+
+ *profile = dptc->profile;
+ return 0;
+}
+
+static int dptc_pp_set(struct device *dev,
+ enum platform_profile_option profile)
+{
+ struct dptc_priv *dptc = dev_get_drvdata(dev);
+ int ret = 0;
+
+ guard(mutex)(&dptc->lock);
+
+ dptc->profile = profile;
+ dptc_apply_profile(dptc, profile);
+ if (profile != PLATFORM_PROFILE_CUSTOM)
+ ret = dptc_alib_save(dptc);
+
+ return ret;
+}
+
+static const struct platform_profile_ops dptc_pp_ops = {
+ .probe = dptc_pp_probe,
+ .profile_get = dptc_pp_get,
+ .profile_set = dptc_pp_set,
+};
+
+static int dptc_resume(struct device *dev)
+{
+ struct dptc_priv *dptc = dev_get_drvdata(dev);
+ int ret;
+
+ guard(mutex)(&dptc->lock);
+
+ if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
+ dptc_apply_profile(dptc, dptc->profile);
+ ret = dptc_alib_save(dptc);
+ } else if (dptc->save_mode == SAVE_SINGLE) {
+ ret = dptc_alib_save(dptc);
+ } else {
+ ret = 0;
+ }
+
+ if (ret)
+ dev_warn(dev, "failed to restore settings on resume: %d\n", ret);
+
+ return 0;
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(dptc_pm_ops, NULL, dptc_resume);
+
+static int dptc_probe(struct platform_device *pdev)
+{
+ const struct dmi_system_id *dmi_match = dev_get_platdata(&pdev->dev);
+ struct dptc_priv *dptc;
+ struct device *dev = &pdev->dev;
+ int i, ret;
+
+ dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL);
+ if (!dptc)
+ return -ENOMEM;
+
+ platform_set_drvdata(pdev, dptc);
+
+ ret = devm_mutex_init(dev, &dptc->lock);
+ if (ret)
+ return ret;
+
+ dptc->dev_limits = dmi_match->driver_data;
+ dev_info(dev, "%s (%s)\n", dmi_match->ident,
+ boot_cpu_data.x86_model_id);
+
+ dptc->fw_attr_dev = device_create(&firmware_attributes_class,
+ NULL, MKDEV(0, 0), NULL,
+ DRIVER_NAME);
+ if (IS_ERR(dptc->fw_attr_dev))
+ return PTR_ERR(dptc->fw_attr_dev);
+
+ ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister,
+ dptc->fw_attr_dev);
+ if (ret)
+ return ret;
+
+ dptc->fw_attr_kset = kset_create_and_add("attributes", NULL,
+ &dptc->fw_attr_dev->kobj);
+ if (!dptc->fw_attr_kset)
+ return -ENOMEM;
+
+ ret = devm_add_action_or_reset(dev, dptc_kset_unregister,
+ dptc->fw_attr_kset);
+ if (ret)
+ return ret;
+
+ for (i = 0; i < DPTC_NUM_PARAMS; i++) {
+ dptc_setup_param_sysfs(dptc, &dptc->params[i], i);
+ ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
+ &dptc->params[i].group);
+ if (ret)
+ return ret;
+ }
+
+ dptc_setup_expanded_sysfs(dptc, &dptc->expanded_attr);
+ ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
+ &dptc->expanded_attr.group);
+ if (ret)
+ return ret;
+
+ sysfs_attr_init(&dptc->save_settings_attr.attr);
+ dptc->save_settings_attr.attr.name = "save_settings";
+ dptc->save_settings_attr.attr.mode = 0644;
+ dptc->save_settings_attr.show = dptc_save_settings_show;
+ dptc->save_settings_attr.store = dptc_save_settings_store;
+ ret = sysfs_create_file(&dptc->fw_attr_kset->kobj,
+ &dptc->save_settings_attr.attr);
+ if (ret)
+ return ret;
+
+ dptc->profile = PLATFORM_PROFILE_CUSTOM;
+ dptc->ppdev = devm_platform_profile_register(dev, "amd-dptc", dptc,
+ &dptc_pp_ops);
+ if (IS_ERR(dptc->ppdev))
+ return PTR_ERR(dptc->ppdev);
+
+ return 0;
+}
+
+static struct platform_driver dptc_driver = {
+ .driver = {
+ .name = DRIVER_NAME,
+ .pm = pm_sleep_ptr(&dptc_pm_ops),
+ },
+ .probe = dptc_probe,
+};
+
+static int __init dptc_init(void)
+{
+ const struct dmi_system_id *match;
+ bool soc_found = false;
+ int i, ret;
+
+ if (!acpi_has_method(NULL, ALIB_PATH)) {
+ pr_debug("ALIB method not present\n");
+ return -ENODEV;
+ }
+
+ match = dmi_first_match(dptc_dmi_table);
+ if (!match)
+ return -ENODEV;
+
+ for (i = 0; dptc_soc_table[i]; i++) {
+ if (strstr(boot_cpu_data.x86_model_id,
+ dptc_soc_table[i])) {
+ soc_found = true;
+ break;
+ }
+ }
+ if (!soc_found) {
+ pr_warn("unrecognized SoC '%s'\n",
+ boot_cpu_data.x86_model_id);
+ return -ENODEV;
+ }
+
+ dptc_pdev = platform_device_register_data(NULL, DRIVER_NAME, -1,
+ match, sizeof(*match));
+ if (IS_ERR(dptc_pdev))
+ return PTR_ERR(dptc_pdev);
+
+ ret = platform_driver_register(&dptc_driver);
+ if (ret) {
+ platform_device_unregister(dptc_pdev);
+ return ret;
+ }
+
+ return 0;
+}
+
+static void __exit dptc_exit(void)
+{
+ platform_driver_unregister(&dptc_driver);
+ platform_device_unregister(dptc_pdev);
+}
+
+module_init(dptc_init);
+module_exit(dptc_exit);
+
+MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>");
+MODULE_DESCRIPTION("AMD DPTCi ACPI Driver");
+MODULE_LICENSE("GPL");
--
2.52.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [RFC v2 2/2] platform/x86/amd: Add AMD DPTCi driver
2026-03-05 18:17 ` [RFC v2 2/2] platform/x86/amd: Add AMD DPTCi driver Antheas Kapenekakis
@ 2026-03-05 20:57 ` Mario Limonciello
2026-03-05 21:18 ` Antheas Kapenekakis
2026-03-06 21:14 ` Derek John Clark
1 sibling, 1 reply; 8+ messages in thread
From: Mario Limonciello @ 2026-03-05 20:57 UTC (permalink / raw)
To: Antheas Kapenekakis
Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato,
linux-kernel, platform-driver-x86
On 3/5/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 matches the ABI of asus-armoury, by exposing a platform
> profile with reasonable tunings for devices depending on their max
> thermal envelope, and {ppt_pl1_spl,ppt_pl2_sppt,ppt_pl3_fppt,cpu_temp}
> tunables which only become writable when the profile is custom,
> otherwise they are read-only and reflect the current profile.
>
> The default profile is custom so that we do not write to the device
> until userspace explicitly selects a profile, remaining at firmware
> defaults.
>
> Assisted-by: Claude:claude-opus-4-6
> Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
As a general comment that will make this easier to review (than a ~1.3k
LoC file) could you split into logical patches that add functionality?
Likewise could you split some of the code into a few source files? A
logical split tha I see would be to put the quirks and associated limits
into a single file and do a lookup of values.
> ---
> MAINTAINERS | 6 +
> drivers/platform/x86/amd/Kconfig | 16 +
> drivers/platform/x86/amd/Makefile | 2 +
> drivers/platform/x86/amd/dptc.c | 1255 +++++++++++++++++++++++++++++
> 4 files changed, 1279 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..ee79a2c084a8 100644
> --- a/drivers/platform/x86/amd/Kconfig
> +++ b/drivers/platform/x86/amd/Kconfig
> @@ -44,3 +44,19 @@ 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 ACPI_PLATFORM_PROFILE
> + select FIRMWARE_ATTRIBUTES_CLASS
> + help
> + Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> + Thermal Configuration Interface (DPTCi). Exposes TDP and thermal
> + parameters for AMD APU-based handheld devices via the
> + firmware-attributes sysfs ABI, allowing userspace tools to stage
> + and atomically commit power limit settings. Requires a DMI match
> + for the device and a recognized AMD SoC.
> +
> + If built as a module, the module will be called amd_dptc.
> +
> diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile
> index f6ff0c837f34..862a609bfe38 100644
> --- a/drivers/platform/x86/amd/Makefile
> +++ b/drivers/platform/x86/amd/Makefile
> @@ -12,3 +12,5 @@ obj-$(CONFIG_AMD_PMF) += pmf/
> obj-$(CONFIG_AMD_WBRF) += wbrf.o
> obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o
> obj-$(CONFIG_AMD_HFI) += hfi/
> +obj-$(CONFIG_AMD_DPTC) += amd_dptc.o
> +amd_dptc-y := dptc.o
> diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
> new file mode 100644
> index 000000000000..68d9d3b85cb6
> --- /dev/null
> +++ b/drivers/platform/x86/amd/dptc.c
> @@ -0,0 +1,1255 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver
> + *
> + * Exposes AMD APU power and thermal parameters via the firmware-attributes
> + * sysfs ABI. Parameters are staged and atomically committed through the
> + * AGESA ALIB Function 0x0C (Dynamic Power and Thermal Configuration
> + * interface).
> + *
> + * Reference: AMD AGESA Publication #44065, Appendix E.5
> + * https://docs.amd.com/v/u/en-US/44065_Arch2008
> + *
> + * Copyright (C) 2026 Antheas Kapenekakis <lkml@antheas.dev>
> + */
> +
> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> +
> +#include <linux/acpi.h>
> +#include <linux/cleanup.h>
> +#include <linux/dmi.h>
> +#include <linux/init.h>
> +#include <linux/kobject.h>
> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/platform_device.h>
> +#include <linux/platform_profile.h>
> +#include <linux/processor.h>
> +#include <linux/sysfs.h>
> +#include <linux/unaligned.h>
> +
> +#include "../firmware_attributes_class.h"
> +
> +#define DRIVER_NAME "amd_dptc"
> +
> +#define ALIB_FUNC_DPTC 0x0C
> +#define ALIB_PATH "\\_SB.ALIB"
> +
> +/* ALIB parameter IDs (AGESA spec Appendix E.5, Table E-52) */
> +#define ALIB_ID_TEMP_TARGET 0x03
> +#define ALIB_ID_STAPM_LIMIT 0x05
> +#define ALIB_ID_FAST_LIMIT 0x06
> +#define ALIB_ID_SLOW_LIMIT 0x07
> +#define ALIB_ID_SKIN_LIMIT 0x2E
> +
> +enum dptc_param_idx {
> + DPTC_PPT_PL1_SPL, /* STAPM + skin limit (set together) */
> + DPTC_PPT_PL2_SPPT, /* slow PPT limit */
> + DPTC_PPT_PL3_FPPT, /* fast PPT limit */
> + DPTC_CPU_TEMP, /* thermal control target */
> + DPTC_NUM_PARAMS,
> +};
> +
> +struct dptc_param_limits {
> + u32 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 */
These variable names leave a little bit to be desired. I am generally
of the feeling "if you NEED to explain all the the variables in the
struct are you named them wrong".
> +};
> +
> +struct dptc_profile {
> + u32 vals[DPTC_NUM_PARAMS]; /* 0 = don't set / unstage this param */
> +};
> +
> +struct dptc_device_limits {
> + struct dptc_param_limits params[DPTC_NUM_PARAMS];
> + struct dptc_profile profiles[PLATFORM_PROFILE_LAST];
> +};
> +
> +struct dptc_param_desc {
> + const char *name;
> + const char *display_name;
> + u8 param_id;
> + u8 param_id2; /* secondary ALIB ID, 0 if none */
> +};
> +
> +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = {
> + [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (mW)",
> + ALIB_ID_STAPM_LIMIT, ALIB_ID_SKIN_LIMIT },
> + [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (mW)",
> + ALIB_ID_SLOW_LIMIT },
> + [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (mW)",
> + ALIB_ID_FAST_LIMIT },
> + [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)",
> + ALIB_ID_TEMP_TARGET },
> +};
> +
> +/* 18W class: AYANEO AIR Plus (Ryzen 5 5560U) */
> +static const struct dptc_device_limits limits_18w = { .params = {
> + [DPTC_PPT_PL1_SPL] = { 0, 5000, 15000, 18000, 22000 },
> + [DPTC_PPT_PL2_SPPT] = { 0, 5000, 15000, 18000, 22000 },
> + [DPTC_PPT_PL3_FPPT] = { 0, 5000, 15000, 20000, 25000 },
> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> +}, .profiles = {
> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5000, 5000, 8000, 0 } },
> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 12000, 14000, 15000, 0 } },
> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 18000, 18000, 20000, 0 } },
> +}};
> +
> +/* 25W class: Ryzen 5000 handhelds (AYANEO NEXT, KUN) */
> +static const struct dptc_device_limits limits_25w = { .params = {
> + [DPTC_PPT_PL1_SPL] = { 0, 4000, 15000, 25000, 32000 },
> + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 20000, 27000, 35000 },
> + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 25000, 30000, 37000 },
> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> +}, .profiles = {
> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8000, 8000, 12000, 0 } },
> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15000, 17000, 20000, 0 } },
> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25000, 27000, 30000, 0 } },
> +}};
> +
> +/* 28W class: GPD Win series, AYANEO 2, OrangePi NEO-01 */
> +static const struct dptc_device_limits limits_28w = { .params = {
> + [DPTC_PPT_PL1_SPL] = { 0, 4000, 15000, 28000, 32000 },
> + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 20000, 30000, 35000 },
> + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 25000, 32000, 37000 },
> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> +}, .profiles = {
> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8000, 8000, 12000, 0 } },
> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15000, 17000, 22000, 0 } },
> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25000, 28000, 32000, 0 } },
> + [PLATFORM_PROFILE_MAX_POWER] = { .vals = { 28000, 30000, 32000, 0 } },
> +}};
> +
> +/* 30W class: OneXPlayer, AYANEO AIR/FLIP/GEEK/SLIDE/3, AOKZOE */
> +static const struct dptc_device_limits limits_30w = { .params = {
> + [DPTC_PPT_PL1_SPL] = { 0, 4000, 15000, 30000, 40000 },
> + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 20000, 32000, 43000 },
> + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 25000, 41000, 50000 },
> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> +}, .profiles = {
> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8000, 8000, 12000, 0 } },
> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15000, 17000, 25000, 0 } },
> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25000, 28000, 41000, 0 } },
> + [PLATFORM_PROFILE_MAX_POWER] = { .vals = { 30000, 32000, 41000, 0 } },
> +}};
> +
> +/* AI MAX Handheld class: GPD Win 5 */
> +static const struct dptc_device_limits limits_maxhh = { .params = {
> + [DPTC_PPT_PL1_SPL] = { 0, 4000, 25000, 80000, 100000 },
> + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 27000, 82000, 100000 },
> + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 40000, 85000, 100000 },
> + [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 },
> +}, .profiles = {
> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 15000, 15000, 25000, 0 } },
> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 25000, 27000, 40000, 0 } },
> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 60000, 63000, 85000, 0 } },
> + [PLATFORM_PROFILE_MAX_POWER] = { .vals = { 80000, 82000, 85000, 0 } },
> +}};
> +
> +/* Substring matches against boot_cpu_data.x86_model_id; order matters. */
> +static const char * const dptc_soc_table[] = {
> + /* AI MAX */
> + "AMD RYZEN AI MAX+ 395",
> + "AMD RYZEN AI MAX+ 385",
> + "AMD RYZEN AI MAX 380",
> + /* Ryzen AI */
> + "AMD Ryzen AI 9 HX 370",
> + "AMD Ryzen AI HX 360",
> + /* Z1 - Extreme before plain Z1 */
> + "AMD Ryzen Z1 Extreme",
> + "AMD Ryzen Z1",
> + /* Ryzen 8000 */
> + "AMD Ryzen 7 8840U",
> + /* Ryzen 7040 */
> + "AMD Ryzen 7 7840U",
> + /* Ryzen 6000 */
> + "AMD Ryzen 7 6800U",
> + "AMD Ryzen 7 6600U",
> + /* Ryzen 5000 */
> + "AMD Ryzen 7 5800U",
> + "AMD Ryzen 7 5700U",
> + "AMD Ryzen 5 5560U",
> + NULL,
> +};
> +
I think this table can go. See my comments elsewhere.
> +static const struct dmi_system_id dptc_dmi_table[] = {
> + /* GPD */
> + {
> + .ident = "GPD Win Mini",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-01"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Win Mini 2024",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Win Mini 2024",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02-L"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Win 4",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-04"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Win 5",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"),
> + },
> + .driver_data = (void *)&limits_maxhh,
> + },
> + {
> + .ident = "GPD Win Max 2",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-04"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Win Max 2 2024",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-05"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Duo",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Duo",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01-L"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Pocket 4",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Pocket 4",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04-L"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + /* OrangePi */
> + {
> + .ident = "OrangePi NEO-01",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + /* AYN */
> + {
> + .ident = "AYN Loki Max",
> + .matches = {
> + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ayn"),
> + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Loki Max"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + /* Tectoy (Zeenix Pro = Loki Max) */
> + {
> + .ident = "Zeenix Pro",
> + .matches = {
> + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Tectoy"),
> + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Zeenix Pro"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + /* AOKZOE */
> + {
> + .ident = "AOKZOE A1 AR07",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 AR07"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "AOKZOE A1 Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 Pro"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "AOKZOE A1X",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1X"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "AOKZOE A2 Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A2 Pro"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + /* OneXPlayer (Intel variants filtered by SoC table) */
> + {
> + .ident = "ONEXPLAYER F1Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1Pro"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER F1 EVA-02",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1 EVA-02"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER 2",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_MATCH(DMI_BOARD_NAME, "ONEXPLAYER 2"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER X1 A",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1 A"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER X1z",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1z"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER X1Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Pro"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER G1 A",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 A"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + /* AYANEO - 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,
> + },
> + /* AYANEO - 25W */
> + {
> + .ident = "AYANEO NEXT Advance",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Advance"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + {
> + .ident = "AYANEO NEXT Lite",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Lite"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + {
> + .ident = "AYANEO NEXT Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Pro"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + {
> + .ident = "AYANEO NEXT",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + {
> + .ident = "AYANEO KUN",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + {
> + .ident = "AYANEO KUN",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO KUN"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + /* AYANEO - 28W */
> + {
> + .ident = "AYANEO 2",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 2"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "SuiPlay0X1",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "Mysten Labs, Inc."),
> + DMI_MATCH(DMI_PRODUCT_NAME, "SuiPlay0X1"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + /* AYANEO - 30W */
> + {
> + /* 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);
> +
> +struct dptc_priv;
> +
> +struct dptc_attr_sysfs {
> + struct dptc_priv *priv;
> + struct kobj_attribute current_value;
> + struct kobj_attribute default_value;
> + struct kobj_attribute min_value;
> + struct kobj_attribute max_value;
> + struct kobj_attribute scalar_increment;
> + struct kobj_attribute display_name;
> + struct kobj_attribute type;
> + struct attribute *attrs[8];
> + struct attribute_group group;
> + int idx;
> +};
> +
> +struct dptc_priv {
> + struct device *fw_attr_dev;
> + struct kset *fw_attr_kset;
> +
> + const struct dptc_device_limits *dev_limits;
> +
> + bool expanded;
> +
> + enum platform_profile_option profile;
> + struct device *ppdev;
> +
> + enum dptc_save_mode { SAVE_SINGLE, SAVE_BULK } save_mode;
> +
> + u32 staged[DPTC_NUM_PARAMS];
> + bool has_staged[DPTC_NUM_PARAMS];
> +
> + /* Protects mutable driver state */
> + struct mutex lock;
> +
> + struct dptc_attr_sysfs params[DPTC_NUM_PARAMS];
> + struct dptc_attr_sysfs expanded_attr;
> + struct kobj_attribute save_settings_attr;
> +};
> +
> +static struct platform_device *dptc_pdev;
> +
> +static u32 dptc_get_min(struct dptc_priv *dptc, int idx)
> +{
> + return dptc->expanded ? dptc->dev_limits->params[idx].min
> + : dptc->dev_limits->params[idx].smin;
> +}
> +
> +static u32 dptc_get_max(struct dptc_priv *dptc, int idx)
> +{
> + return dptc->expanded ? dptc->dev_limits->params[idx].max
> + : dptc->dev_limits->params[idx].smax;
> +}
> +
> +static u32 dptc_get_default(struct dptc_priv *dptc, int idx)
> +{
> + return dptc->dev_limits->params[idx].def;
> +}
> +
> +static int dptc_alib_call(const u8 *ids, const u32 *vals, int count)
> +{
> + union acpi_object in_params[2];
> + struct acpi_object_list input;
> + acpi_status status;
> + u32 buf_size;
> + int i, off;
> + u8 *buf;
> +
> + if (count == 0)
> + return -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;
> +
> + put_unaligned_le16(buf_size, buf);
> +
> + for (i = 0; i < count; i++) {
> + off = 2 + i * 5;
> + buf[off] = ids[i];
> + put_unaligned_le32(vals[i], buf + off + 1);
> + }
> +
> + in_params[0].type = ACPI_TYPE_INTEGER;
> + in_params[0].integer.value = ALIB_FUNC_DPTC;
> + in_params[1].type = ACPI_TYPE_BUFFER;
> + in_params[1].buffer.length = buf_size;
> + in_params[1].buffer.pointer = buf;
> +
> + input.count = 2;
> + input.pointer = in_params;
> +
> + status = acpi_evaluate_object(NULL, ALIB_PATH, &input, NULL);
> + kfree(buf);
> +
> + if (ACPI_FAILURE(status)) {
> + pr_err("ALIB call failed: %s\n",
> + acpi_format_exception(status));
> + return -EIO;
> + }
> +
> + pr_debug("sent %d ALIB parameter(s)\n", count);
> + return 0;
> +}
> +
> +static int dptc_alib_send_one(int idx, u32 val)
> +{
> + u32 vals[2];
> + u8 ids[2];
> + int count = 0;
> +
> + ids[count] = dptc_params[idx].param_id;
> + vals[count] = val;
> + count++;
> + if (dptc_params[idx].param_id2) {
> + ids[count] = dptc_params[idx].param_id2;
> + vals[count] = val;
> + count++;
> + }
> +
> + return dptc_alib_call(ids, vals, count);
> +}
> +
> +static int dptc_alib_save(struct dptc_priv *dptc)
> +{
> + u32 vals[DPTC_NUM_PARAMS * 2];
> + u8 ids[DPTC_NUM_PARAMS * 2];
> + int i, count = 0;
> +
> + 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 (dptc_params[i].param_id2) {
> + ids[count] = dptc_params[i].param_id2;
> + vals[count] = dptc->staged[i];
> + count++;
> + }
> + }
> +
> + if (!count)
> + return 0;
> +
> + return dptc_alib_call(ids, vals, count);
> +}
> +
> +/* Sysfs callbacks */
> +
> +static ssize_t dptc_current_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, current_value);
> + struct dptc_priv *dptc = ps->priv;
> +
> + guard(mutex)(&dptc->lock);
> +
> + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
> + u32 val = dptc->dev_limits->profiles[dptc->profile].vals[ps->idx];
> +
> + if (!val)
> + return sysfs_emit(buf, "\n");
> + return sysfs_emit(buf, "%u\n", val);
> + }
> +
> + if (!dptc->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_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, current_value);
> + struct dptc_priv *dptc = ps->priv;
> + u32 val, min, max;
> + int ret;
> +
> + if (count == 1 && buf[0] == '\n') {
> + guard(mutex)(&dptc->lock);
> +
> + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> + return -EPERM;
> + dptc->has_staged[ps->idx] = false;
> + return count;
> + }
> +
> + ret = kstrtou32(buf, 10, &val);
> + if (ret)
> + return ret;
> +
> + guard(mutex)(&dptc->lock);
> +
> + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> + return -EPERM;
> + min = dptc_get_min(dptc, ps->idx);
> + max = dptc_get_max(dptc, ps->idx);
> + if (val < min || (max && val > max))
> + return -EINVAL;
> + dptc->staged[ps->idx] = val;
> + dptc->has_staged[ps->idx] = true;
> + if (dptc->save_mode == SAVE_SINGLE)
> + ret = dptc_alib_send_one(ps->idx, val);
> +
> + return ret ? ret : count;
> +}
> +
> +static ssize_t dptc_default_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, default_value);
> +
> + return sysfs_emit(buf, "%u\n", dptc_get_default(ps->priv, ps->idx));
> +}
> +
> +static ssize_t dptc_min_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, min_value);
> + struct dptc_priv *dptc = ps->priv;
> +
> + guard(mutex)(&dptc->lock);
> +
> + return sysfs_emit(buf, "%u\n", dptc_get_min(dptc, ps->idx));
> +}
> +
> +static ssize_t dptc_max_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, max_value);
> + struct dptc_priv *dptc = ps->priv;
> +
> + guard(mutex)(&dptc->lock);
> +
> + return sysfs_emit(buf, "%u\n", dptc_get_max(dptc, ps->idx));
> +}
> +
> +static ssize_t dptc_scalar_increment_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + return sysfs_emit(buf, "1\n");
> +}
> +
> +static ssize_t dptc_display_name_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, display_name);
> + return sysfs_emit(buf, "%s\n", dptc_params[ps->idx].display_name);
> +}
> +
> +static ssize_t dptc_type_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + return sysfs_emit(buf, "integer\n");
> +}
> +
> +static ssize_t dptc_save_settings_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + struct dptc_priv *dptc =
> + container_of(attr, struct dptc_priv, save_settings_attr);
> +
> + guard(mutex)(&dptc->lock);
> +
> + if (dptc->save_mode == SAVE_SINGLE)
> + return sysfs_emit(buf, "single\n");
> + return sysfs_emit(buf, "bulk\n");
> +}
> +
> +static ssize_t dptc_save_settings_store(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + const char *buf, size_t count)
> +{
> + struct dptc_priv *dptc =
> + container_of(attr, struct dptc_priv, save_settings_attr);
> + int ret = 0;
> +
> + guard(mutex)(&dptc->lock);
> +
> + if (sysfs_streq(buf, "save"))
> + ret = dptc_alib_save(dptc);
> + else if (sysfs_streq(buf, "single"))
> + dptc->save_mode = SAVE_SINGLE;
> + else if (sysfs_streq(buf, "bulk"))
> + dptc->save_mode = SAVE_BULK;
> + else
> + return -EINVAL;
> +
> + return ret ? ret : count;
> +}
> +
> +static ssize_t dptc_expanded_current_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + char *buf)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, current_value);
> + struct dptc_priv *dptc = ps->priv;
> +
> + guard(mutex)(&dptc->lock);
> +
> + return sysfs_emit(buf, "%d\n", dptc->expanded);
> +}
> +
> +static ssize_t dptc_expanded_current_value_store(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + const char *buf, size_t count)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, current_value);
> + struct dptc_priv *dptc = ps->priv;
> + bool val;
> + int ret;
> +
> + ret = kstrtobool(buf, &val);
> + if (ret)
> + return ret;
> +
> + guard(mutex)(&dptc->lock);
> +
> + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> + return -EPERM;
> + dptc->expanded = val;
> + /* Clear staged values: limits changed, old values may be out of range */
> + memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
> +
> + return count;
> +}
> +
> +static ssize_t dptc_expanded_default_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + char *buf)
> +{
> + return sysfs_emit(buf, "0\n");
> +}
> +
> +static ssize_t dptc_expanded_min_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + char *buf)
> +{
> + return sysfs_emit(buf, "0\n");
> +}
> +
> +static ssize_t dptc_expanded_max_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + char *buf)
> +{
> + return sysfs_emit(buf, "1\n");
> +}
> +
> +static ssize_t dptc_expanded_scalar_increment_show(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + char *buf)
> +{
> + return sysfs_emit(buf, "1\n");
> +}
> +
> +static ssize_t dptc_expanded_display_name_show(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + char *buf)
> +{
> + return sysfs_emit(buf, "Expanded Limits\n");
> +}
> +
> +static ssize_t dptc_expanded_type_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + return sysfs_emit(buf, "integer\n");
> +}
> +
> +/* Sysfs setup */
> +
> +static void dptc_setup_param_sysfs(struct dptc_priv *dptc,
> + struct dptc_attr_sysfs *ps, int idx)
> +{
> + ps->priv = dptc;
> + ps->idx = idx;
> +
> + sysfs_attr_init(&ps->current_value.attr);
> + ps->current_value.attr.name = "current_value";
> + ps->current_value.attr.mode = 0644;
> + ps->current_value.show = dptc_current_value_show;
> + ps->current_value.store = dptc_current_value_store;
> +
> + sysfs_attr_init(&ps->default_value.attr);
> + ps->default_value.attr.name = "default_value";
> + ps->default_value.attr.mode = 0444;
> + ps->default_value.show = dptc_default_value_show;
> +
> + sysfs_attr_init(&ps->min_value.attr);
> + ps->min_value.attr.name = "min_value";
> + ps->min_value.attr.mode = 0444;
> + ps->min_value.show = dptc_min_value_show;
> +
> + sysfs_attr_init(&ps->max_value.attr);
> + ps->max_value.attr.name = "max_value";
> + ps->max_value.attr.mode = 0444;
> + ps->max_value.show = dptc_max_value_show;
> +
> + sysfs_attr_init(&ps->scalar_increment.attr);
> + ps->scalar_increment.attr.name = "scalar_increment";
> + ps->scalar_increment.attr.mode = 0444;
> + ps->scalar_increment.show = dptc_scalar_increment_show;
> +
> + sysfs_attr_init(&ps->display_name.attr);
> + ps->display_name.attr.name = "display_name";
> + ps->display_name.attr.mode = 0444;
> + ps->display_name.show = dptc_display_name_show;
> +
> + sysfs_attr_init(&ps->type.attr);
> + ps->type.attr.name = "type";
> + ps->type.attr.mode = 0444;
> + ps->type.show = dptc_type_show;
> +
> + ps->attrs[0] = &ps->current_value.attr;
> + ps->attrs[1] = &ps->default_value.attr;
> + ps->attrs[2] = &ps->min_value.attr;
> + ps->attrs[3] = &ps->max_value.attr;
> + ps->attrs[4] = &ps->scalar_increment.attr;
> + ps->attrs[5] = &ps->display_name.attr;
> + ps->attrs[6] = &ps->type.attr;
> + ps->attrs[7] = NULL;
> +
> + ps->group.name = dptc_params[idx].name;
> + ps->group.attrs = ps->attrs;
> +}
> +
> +static void dptc_setup_expanded_sysfs(struct dptc_priv *dptc,
> + struct dptc_attr_sysfs *ps)
> +{
> + ps->priv = dptc;
> + sysfs_attr_init(&ps->current_value.attr);
> + ps->current_value.attr.name = "current_value";
> + ps->current_value.attr.mode = 0644;
> + ps->current_value.show = dptc_expanded_current_value_show;
> + ps->current_value.store = dptc_expanded_current_value_store;
> +
> + sysfs_attr_init(&ps->default_value.attr);
> + ps->default_value.attr.name = "default_value";
> + ps->default_value.attr.mode = 0444;
> + ps->default_value.show = dptc_expanded_default_value_show;
> +
> + sysfs_attr_init(&ps->min_value.attr);
> + ps->min_value.attr.name = "min_value";
> + ps->min_value.attr.mode = 0444;
> + ps->min_value.show = dptc_expanded_min_value_show;
> +
> + sysfs_attr_init(&ps->max_value.attr);
> + ps->max_value.attr.name = "max_value";
> + ps->max_value.attr.mode = 0444;
> + ps->max_value.show = dptc_expanded_max_value_show;
> +
> + sysfs_attr_init(&ps->scalar_increment.attr);
> + ps->scalar_increment.attr.name = "scalar_increment";
> + ps->scalar_increment.attr.mode = 0444;
> + ps->scalar_increment.show = dptc_expanded_scalar_increment_show;
> +
> + sysfs_attr_init(&ps->display_name.attr);
> + ps->display_name.attr.name = "display_name";
> + ps->display_name.attr.mode = 0444;
> + ps->display_name.show = dptc_expanded_display_name_show;
> +
> + sysfs_attr_init(&ps->type.attr);
> + ps->type.attr.name = "type";
> + ps->type.attr.mode = 0444;
> + ps->type.show = dptc_expanded_type_show;
> +
> + ps->attrs[0] = &ps->current_value.attr;
> + ps->attrs[1] = &ps->default_value.attr;
> + ps->attrs[2] = &ps->min_value.attr;
> + ps->attrs[3] = &ps->max_value.attr;
> + ps->attrs[4] = &ps->scalar_increment.attr;
> + ps->attrs[5] = &ps->display_name.attr;
> + ps->attrs[6] = &ps->type.attr;
> + ps->attrs[7] = NULL;
> +
> + ps->group.name = "expanded_limits";
> + ps->group.attrs = ps->attrs;
> +}
> +
> +static void dptc_fw_dev_unregister(void *data)
> +{
> + device_unregister(data);
> +}
> +
> +static void dptc_kset_unregister(void *data)
> +{
> + kset_unregister(data);
> +}
> +
> +/* Platform profile */
> +
> +static void dptc_apply_profile(struct dptc_priv *dptc,
> + enum platform_profile_option profile)
> +{
> + const struct dptc_profile *pp;
> + int i;
> +
> + memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
> +
> + if (profile == PLATFORM_PROFILE_CUSTOM)
> + return;
> +
> + pp = &dptc->dev_limits->profiles[profile];
> + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> + if (!pp->vals[i])
> + continue;
> + dptc->staged[i] = pp->vals[i];
> + dptc->has_staged[i] = true;
> + }
> +}
> +
> +static int dptc_pp_probe(void *drvdata, unsigned long *choices)
> +{
> + struct dptc_priv *dptc = drvdata;
> + int i, j;
> +
> + set_bit(PLATFORM_PROFILE_CUSTOM, choices);
> + for (i = 0; i < PLATFORM_PROFILE_LAST; i++) {
> + for (j = 0; j < DPTC_NUM_PARAMS; j++) {
> + if (dptc->dev_limits->profiles[i].vals[j]) {
> + set_bit(i, choices);
> + break;
> + }
> + }
> + }
> + return 0;
> +}
> +
> +static int dptc_pp_get(struct device *dev,
> + enum platform_profile_option *profile)
> +{
> + struct dptc_priv *dptc = dev_get_drvdata(dev);
> +
> + guard(mutex)(&dptc->lock);
> +
> + *profile = dptc->profile;
> + return 0;
> +}
> +
> +static int dptc_pp_set(struct device *dev,
> + enum platform_profile_option profile)
> +{
> + struct dptc_priv *dptc = dev_get_drvdata(dev);
> + int ret = 0;
> +
> + guard(mutex)(&dptc->lock);
> +
> + dptc->profile = profile;
> + dptc_apply_profile(dptc, profile);
> + if (profile != PLATFORM_PROFILE_CUSTOM)
> + ret = dptc_alib_save(dptc);
> +
> + return ret;
> +}
> +
> +static const struct platform_profile_ops dptc_pp_ops = {
> + .probe = dptc_pp_probe,
> + .profile_get = dptc_pp_get,
> + .profile_set = dptc_pp_set,
> +};
> +
> +static int dptc_resume(struct device *dev)
> +{
> + struct dptc_priv *dptc = dev_get_drvdata(dev);
> + int ret;
> +
> + guard(mutex)(&dptc->lock);
> +
> + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
> + dptc_apply_profile(dptc, dptc->profile);
> + ret = dptc_alib_save(dptc);
> + } else if (dptc->save_mode == SAVE_SINGLE) {
> + ret = dptc_alib_save(dptc);
> + } else {
> + ret = 0;
> + }
> +
> + if (ret)
> + dev_warn(dev, "failed to restore settings on resume: %d\n", ret);
> +
> + return 0;
return ret?
> +}
> +
> +static DEFINE_SIMPLE_DEV_PM_OPS(dptc_pm_ops, NULL, dptc_resume);
> +
> +static int dptc_probe(struct platform_device *pdev)
> +{
> + const struct dmi_system_id *dmi_match = dev_get_platdata(&pdev->dev);
> + struct dptc_priv *dptc;
> + struct device *dev = &pdev->dev;
> + int i, ret;
reverse xmas tree here
> +
> + dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL);
> + if (!dptc)
> + return -ENOMEM;
> +
> + platform_set_drvdata(pdev, dptc);
> +
> + ret = devm_mutex_init(dev, &dptc->lock);
> + if (ret)
> + return ret;
> +
> + dptc->dev_limits = dmi_match->driver_data;
> + dev_info(dev, "%s (%s)\n", dmi_match->ident,
> + boot_cpu_data.x86_model_id);
> +
> + dptc->fw_attr_dev = device_create(&firmware_attributes_class,
> + NULL, MKDEV(0, 0), NULL,
> + DRIVER_NAME);
> + if (IS_ERR(dptc->fw_attr_dev))
> + return PTR_ERR(dptc->fw_attr_dev);
> +
> + ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister,
> + dptc->fw_attr_dev);
> + if (ret)
> + return ret;
> +
> + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL,
> + &dptc->fw_attr_dev->kobj);
> + if (!dptc->fw_attr_kset)
> + return -ENOMEM;
> +
> + ret = devm_add_action_or_reset(dev, dptc_kset_unregister,
> + dptc->fw_attr_kset);
> + if (ret)
> + return ret;
> +
> + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> + dptc_setup_param_sysfs(dptc, &dptc->params[i], i);
> + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> + &dptc->params[i].group);
> + if (ret)
> + return ret;
> + }
> +
> + dptc_setup_expanded_sysfs(dptc, &dptc->expanded_attr);
> + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> + &dptc->expanded_attr.group);
> + if (ret)
> + return ret;
> +
> + sysfs_attr_init(&dptc->save_settings_attr.attr);
> + dptc->save_settings_attr.attr.name = "save_settings";
> + dptc->save_settings_attr.attr.mode = 0644;
> + dptc->save_settings_attr.show = dptc_save_settings_show;
> + dptc->save_settings_attr.store = dptc_save_settings_store;
> + ret = sysfs_create_file(&dptc->fw_attr_kset->kobj,
> + &dptc->save_settings_attr.attr);
> + if (ret)
> + return ret;
Given multiple drivers have a save_settings feature now, maybe it's
worth creating a helper as part of this series.
> +
> + dptc->profile = PLATFORM_PROFILE_CUSTOM;
So it starts in custom? Wouldn't it make more sense to start in balanced?
> + dptc->ppdev = devm_platform_profile_register(dev, "amd-dptc", dptc,
> + &dptc_pp_ops);
> + if (IS_ERR(dptc->ppdev))
> + return PTR_ERR(dptc->ppdev);
> +
> + return 0;
> +}
> +
> +static struct platform_driver dptc_driver = {
> + .driver = {
> + .name = DRIVER_NAME,
> + .pm = pm_sleep_ptr(&dptc_pm_ops),
> + },
> + .probe = dptc_probe,
> +};
> +
> +static int __init dptc_init(void)
> +{
> + const struct dmi_system_id *match;
> + bool soc_found = false;
> + int i, ret;
> +
> + if (!acpi_has_method(NULL, ALIB_PATH)) {
> + pr_debug("ALIB method not present\n");
To avoid an extra debug statement on every machine not quirked - how
about you start with the quirk check?
If not that, could you do any checks that ALIB_FUNC_DPTC is supported?
Not sure if there was a method for that.
> + return -ENODEV;
> + }
I will admit I didn't look at the spec, but is there a version lookup
for this method that can be used?
> +
> + match = dmi_first_match(dptc_dmi_table);
> + if (!match)
> + return -ENODEV;
> +
> + for (i = 0; dptc_soc_table[i]; i++) {
> + if (strstr(boot_cpu_data.x86_model_id,
> + dptc_soc_table[i])) {
> + soc_found = true;
> + break;
> + }
> + }
> + if (!soc_found) {
> + pr_warn("unrecognized SoC '%s'\n",
> + boot_cpu_data.x86_model_id);
> + return -ENODEV;
> + }
Is this SOC lookup still going to be needed if you're 100% quirks table?
I would think no.
> +
> + dptc_pdev = platform_device_register_data(NULL, DRIVER_NAME, -1,
> + match, sizeof(*match));
> + if (IS_ERR(dptc_pdev))
> + return PTR_ERR(dptc_pdev);
> +
> + ret = platform_driver_register(&dptc_driver);
> + if (ret) {
> + platform_device_unregister(dptc_pdev);
> + return ret;
> + }
> +
> + return 0;
> +}
> +
> +static void __exit dptc_exit(void)
> +{
> + platform_driver_unregister(&dptc_driver);
> + platform_device_unregister(dptc_pdev);
> +}
> +
> +module_init(dptc_init);
> +module_exit(dptc_exit);
> +
> +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>");
> +MODULE_DESCRIPTION("AMD DPTCi ACPI Driver");
> +MODULE_LICENSE("GPL");
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [RFC v2 2/2] platform/x86/amd: Add AMD DPTCi driver
2026-03-05 20:57 ` Mario Limonciello
@ 2026-03-05 21:18 ` Antheas Kapenekakis
0 siblings, 0 replies; 8+ messages in thread
From: Antheas Kapenekakis @ 2026-03-05 21:18 UTC (permalink / raw)
To: Mario Limonciello
Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato,
linux-kernel, platform-driver-x86
On Thu, 5 Mar 2026 at 21:58, Mario Limonciello
<mario.limonciello@amd.com> wrote:
>
>
>
> On 3/5/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 matches the ABI of asus-armoury, by exposing a platform
> > profile with reasonable tunings for devices depending on their max
> > thermal envelope, and {ppt_pl1_spl,ppt_pl2_sppt,ppt_pl3_fppt,cpu_temp}
> > tunables which only become writable when the profile is custom,
> > otherwise they are read-only and reflect the current profile.
> >
> > The default profile is custom so that we do not write to the device
> > until userspace explicitly selects a profile, remaining at firmware
> > defaults.
> >
> > Assisted-by: Claude:claude-opus-4-6
> > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
>
> As a general comment that will make this easier to review (than a ~1.3k
> LoC file) could you split into logical patches that add functionality?
>
> Likewise could you split some of the code into a few source files? A
> logical split tha I see would be to put the quirks and associated limits
> into a single file and do a lookup of values.
As the refining gets more stable I will split it up into patches.
Perhaps V3 is this point.
There is one major change left that I figured after I sent the V2.
Other drivers use Watts and this driver uses mW. So I already queued
changing to W for V3.
>
> > ---
> > MAINTAINERS | 6 +
> > drivers/platform/x86/amd/Kconfig | 16 +
> > drivers/platform/x86/amd/Makefile | 2 +
> > drivers/platform/x86/amd/dptc.c | 1255 +++++++++++++++++++++++++++++
> > 4 files changed, 1279 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..ee79a2c084a8 100644
> > --- a/drivers/platform/x86/amd/Kconfig
> > +++ b/drivers/platform/x86/amd/Kconfig
> > @@ -44,3 +44,19 @@ 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 ACPI_PLATFORM_PROFILE
> > + select FIRMWARE_ATTRIBUTES_CLASS
> > + help
> > + Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> > + Thermal Configuration Interface (DPTCi). Exposes TDP and thermal
> > + parameters for AMD APU-based handheld devices via the
> > + firmware-attributes sysfs ABI, allowing userspace tools to stage
> > + and atomically commit power limit settings. Requires a DMI match
> > + for the device and a recognized AMD SoC.
> > +
> > + If built as a module, the module will be called amd_dptc.
> > +
> > diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile
> > index f6ff0c837f34..862a609bfe38 100644
> > --- a/drivers/platform/x86/amd/Makefile
> > +++ b/drivers/platform/x86/amd/Makefile
> > @@ -12,3 +12,5 @@ obj-$(CONFIG_AMD_PMF) += pmf/
> > obj-$(CONFIG_AMD_WBRF) += wbrf.o
> > obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o
> > obj-$(CONFIG_AMD_HFI) += hfi/
> > +obj-$(CONFIG_AMD_DPTC) += amd_dptc.o
> > +amd_dptc-y := dptc.o
> > diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
> > new file mode 100644
> > index 000000000000..68d9d3b85cb6
> > --- /dev/null
> > +++ b/drivers/platform/x86/amd/dptc.c
> > @@ -0,0 +1,1255 @@
> > +// SPDX-License-Identifier: GPL-2.0-only
> > +/*
> > + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver
> > + *
> > + * Exposes AMD APU power and thermal parameters via the firmware-attributes
> > + * sysfs ABI. Parameters are staged and atomically committed through the
> > + * AGESA ALIB Function 0x0C (Dynamic Power and Thermal Configuration
> > + * interface).
> > + *
> > + * Reference: AMD AGESA Publication #44065, Appendix E.5
> > + * https://docs.amd.com/v/u/en-US/44065_Arch2008
> > + *
> > + * Copyright (C) 2026 Antheas Kapenekakis <lkml@antheas.dev>
> > + */
> > +
> > +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> > +
> > +#include <linux/acpi.h>
> > +#include <linux/cleanup.h>
> > +#include <linux/dmi.h>
> > +#include <linux/init.h>
> > +#include <linux/kobject.h>
> > +#include <linux/module.h>
> > +#include <linux/mutex.h>
> > +#include <linux/platform_device.h>
> > +#include <linux/platform_profile.h>
> > +#include <linux/processor.h>
> > +#include <linux/sysfs.h>
> > +#include <linux/unaligned.h>
> > +
> > +#include "../firmware_attributes_class.h"
> > +
> > +#define DRIVER_NAME "amd_dptc"
> > +
> > +#define ALIB_FUNC_DPTC 0x0C
> > +#define ALIB_PATH "\\_SB.ALIB"
> > +
> > +/* ALIB parameter IDs (AGESA spec Appendix E.5, Table E-52) */
> > +#define ALIB_ID_TEMP_TARGET 0x03
> > +#define ALIB_ID_STAPM_LIMIT 0x05
> > +#define ALIB_ID_FAST_LIMIT 0x06
> > +#define ALIB_ID_SLOW_LIMIT 0x07
> > +#define ALIB_ID_SKIN_LIMIT 0x2E
> > +
> > +enum dptc_param_idx {
> > + DPTC_PPT_PL1_SPL, /* STAPM + skin limit (set together) */
> > + DPTC_PPT_PL2_SPPT, /* slow PPT limit */
> > + DPTC_PPT_PL3_FPPT, /* fast PPT limit */
> > + DPTC_CPU_TEMP, /* thermal control target */
> > + DPTC_NUM_PARAMS,
> > +};
> > +
> > +struct dptc_param_limits {
> > + u32 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 */
>
> These variable names leave a little bit to be desired. I am generally
> of the feeling "if you NEED to explain all the the variables in the
> struct are you named them wrong".
Fair, I can expand the names.
>
> > +};
> > +
> > +struct dptc_profile {
> > + u32 vals[DPTC_NUM_PARAMS]; /* 0 = don't set / unstage this param */
> > +};
> > +
> > +struct dptc_device_limits {
> > + struct dptc_param_limits params[DPTC_NUM_PARAMS];
> > + struct dptc_profile profiles[PLATFORM_PROFILE_LAST];
> > +};
> > +
> > +struct dptc_param_desc {
> > + const char *name;
> > + const char *display_name;
> > + u8 param_id;
> > + u8 param_id2; /* secondary ALIB ID, 0 if none */
> > +};
> > +
> > +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = {
> > + [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (mW)",
> > + ALIB_ID_STAPM_LIMIT, ALIB_ID_SKIN_LIMIT },
> > + [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (mW)",
> > + ALIB_ID_SLOW_LIMIT },
> > + [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (mW)",
> > + ALIB_ID_FAST_LIMIT },
> > + [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)",
> > + ALIB_ID_TEMP_TARGET },
> > +};
> > +
> > +/* 18W class: AYANEO AIR Plus (Ryzen 5 5560U) */
> > +static const struct dptc_device_limits limits_18w = { .params = {
> > + [DPTC_PPT_PL1_SPL] = { 0, 5000, 15000, 18000, 22000 },
> > + [DPTC_PPT_PL2_SPPT] = { 0, 5000, 15000, 18000, 22000 },
> > + [DPTC_PPT_PL3_FPPT] = { 0, 5000, 15000, 20000, 25000 },
> > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > +}, .profiles = {
> > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5000, 5000, 8000, 0 } },
> > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 12000, 14000, 15000, 0 } },
> > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 18000, 18000, 20000, 0 } },
> > +}};
> > +
> > +/* 25W class: Ryzen 5000 handhelds (AYANEO NEXT, KUN) */
> > +static const struct dptc_device_limits limits_25w = { .params = {
> > + [DPTC_PPT_PL1_SPL] = { 0, 4000, 15000, 25000, 32000 },
> > + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 20000, 27000, 35000 },
> > + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 25000, 30000, 37000 },
> > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > +}, .profiles = {
> > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8000, 8000, 12000, 0 } },
> > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15000, 17000, 20000, 0 } },
> > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25000, 27000, 30000, 0 } },
> > +}};
> > +
> > +/* 28W class: GPD Win series, AYANEO 2, OrangePi NEO-01 */
> > +static const struct dptc_device_limits limits_28w = { .params = {
> > + [DPTC_PPT_PL1_SPL] = { 0, 4000, 15000, 28000, 32000 },
> > + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 20000, 30000, 35000 },
> > + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 25000, 32000, 37000 },
> > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > +}, .profiles = {
> > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8000, 8000, 12000, 0 } },
> > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15000, 17000, 22000, 0 } },
> > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25000, 28000, 32000, 0 } },
> > + [PLATFORM_PROFILE_MAX_POWER] = { .vals = { 28000, 30000, 32000, 0 } },
> > +}};
> > +
> > +/* 30W class: OneXPlayer, AYANEO AIR/FLIP/GEEK/SLIDE/3, AOKZOE */
> > +static const struct dptc_device_limits limits_30w = { .params = {
> > + [DPTC_PPT_PL1_SPL] = { 0, 4000, 15000, 30000, 40000 },
> > + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 20000, 32000, 43000 },
> > + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 25000, 41000, 50000 },
> > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > +}, .profiles = {
> > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8000, 8000, 12000, 0 } },
> > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15000, 17000, 25000, 0 } },
> > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25000, 28000, 41000, 0 } },
> > + [PLATFORM_PROFILE_MAX_POWER] = { .vals = { 30000, 32000, 41000, 0 } },
> > +}};
> > +
> > +/* AI MAX Handheld class: GPD Win 5 */
> > +static const struct dptc_device_limits limits_maxhh = { .params = {
> > + [DPTC_PPT_PL1_SPL] = { 0, 4000, 25000, 80000, 100000 },
> > + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 27000, 82000, 100000 },
> > + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 40000, 85000, 100000 },
> > + [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 },
> > +}, .profiles = {
> > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 15000, 15000, 25000, 0 } },
> > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 25000, 27000, 40000, 0 } },
> > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 60000, 63000, 85000, 0 } },
> > + [PLATFORM_PROFILE_MAX_POWER] = { .vals = { 80000, 82000, 85000, 0 } },
> > +}};
> > +
> > +/* Substring matches against boot_cpu_data.x86_model_id; order matters. */
> > +static const char * const dptc_soc_table[] = {
> > + /* AI MAX */
> > + "AMD RYZEN AI MAX+ 395",
> > + "AMD RYZEN AI MAX+ 385",
> > + "AMD RYZEN AI MAX 380",
> > + /* Ryzen AI */
> > + "AMD Ryzen AI 9 HX 370",
> > + "AMD Ryzen AI HX 360",
> > + /* Z1 - Extreme before plain Z1 */
> > + "AMD Ryzen Z1 Extreme",
> > + "AMD Ryzen Z1",
> > + /* Ryzen 8000 */
> > + "AMD Ryzen 7 8840U",
> > + /* Ryzen 7040 */
> > + "AMD Ryzen 7 7840U",
> > + /* Ryzen 6000 */
> > + "AMD Ryzen 7 6800U",
> > + "AMD Ryzen 7 6600U",
> > + /* Ryzen 5000 */
> > + "AMD Ryzen 7 5800U",
> > + "AMD Ryzen 7 5700U",
> > + "AMD Ryzen 5 5560U",
> > + NULL,
> > +};
> > +
>
> I think this table can go. See my comments elsewhere.
>
> > +static const struct dmi_system_id dptc_dmi_table[] = {
> > + /* GPD */
> > + {
> > + .ident = "GPD Win Mini",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-01"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Win Mini 2024",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Win Mini 2024",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02-L"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Win 4",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-04"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Win 5",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"),
> > + },
> > + .driver_data = (void *)&limits_maxhh,
> > + },
> > + {
> > + .ident = "GPD Win Max 2",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-04"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Win Max 2 2024",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-05"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Duo",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Duo",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01-L"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Pocket 4",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Pocket 4",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04-L"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + /* OrangePi */
> > + {
> > + .ident = "OrangePi NEO-01",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + /* AYN */
> > + {
> > + .ident = "AYN Loki Max",
> > + .matches = {
> > + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ayn"),
> > + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Loki Max"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + /* Tectoy (Zeenix Pro = Loki Max) */
> > + {
> > + .ident = "Zeenix Pro",
> > + .matches = {
> > + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Tectoy"),
> > + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Zeenix Pro"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + /* AOKZOE */
> > + {
> > + .ident = "AOKZOE A1 AR07",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 AR07"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "AOKZOE A1 Pro",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 Pro"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "AOKZOE A1X",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1X"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "AOKZOE A2 Pro",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A2 Pro"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + /* OneXPlayer (Intel variants filtered by SoC table) */
> > + {
> > + .ident = "ONEXPLAYER F1Pro",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1Pro"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "ONEXPLAYER F1 EVA-02",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1 EVA-02"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "ONEXPLAYER 2",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > + DMI_MATCH(DMI_BOARD_NAME, "ONEXPLAYER 2"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "ONEXPLAYER X1 A",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1 A"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "ONEXPLAYER X1z",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1z"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "ONEXPLAYER X1Pro",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Pro"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "ONEXPLAYER G1 A",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 A"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + /* AYANEO - 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,
> > + },
> > + /* AYANEO - 25W */
> > + {
> > + .ident = "AYANEO NEXT Advance",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Advance"),
> > + },
> > + .driver_data = (void *)&limits_25w,
> > + },
> > + {
> > + .ident = "AYANEO NEXT Lite",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Lite"),
> > + },
> > + .driver_data = (void *)&limits_25w,
> > + },
> > + {
> > + .ident = "AYANEO NEXT Pro",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Pro"),
> > + },
> > + .driver_data = (void *)&limits_25w,
> > + },
> > + {
> > + .ident = "AYANEO NEXT",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT"),
> > + },
> > + .driver_data = (void *)&limits_25w,
> > + },
> > + {
> > + .ident = "AYANEO KUN",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"),
> > + },
> > + .driver_data = (void *)&limits_25w,
> > + },
> > + {
> > + .ident = "AYANEO KUN",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO KUN"),
> > + },
> > + .driver_data = (void *)&limits_25w,
> > + },
> > + /* AYANEO - 28W */
> > + {
> > + .ident = "AYANEO 2",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 2"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "SuiPlay0X1",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "Mysten Labs, Inc."),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "SuiPlay0X1"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + /* AYANEO - 30W */
> > + {
> > + /* 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);
> > +
> > +struct dptc_priv;
> > +
> > +struct dptc_attr_sysfs {
> > + struct dptc_priv *priv;
> > + struct kobj_attribute current_value;
> > + struct kobj_attribute default_value;
> > + struct kobj_attribute min_value;
> > + struct kobj_attribute max_value;
> > + struct kobj_attribute scalar_increment;
> > + struct kobj_attribute display_name;
> > + struct kobj_attribute type;
> > + struct attribute *attrs[8];
> > + struct attribute_group group;
> > + int idx;
> > +};
> > +
> > +struct dptc_priv {
> > + struct device *fw_attr_dev;
> > + struct kset *fw_attr_kset;
> > +
> > + const struct dptc_device_limits *dev_limits;
> > +
> > + bool expanded;
> > +
> > + enum platform_profile_option profile;
> > + struct device *ppdev;
> > +
> > + enum dptc_save_mode { SAVE_SINGLE, SAVE_BULK } save_mode;
> > +
> > + u32 staged[DPTC_NUM_PARAMS];
> > + bool has_staged[DPTC_NUM_PARAMS];
> > +
> > + /* Protects mutable driver state */
> > + struct mutex lock;
> > +
> > + struct dptc_attr_sysfs params[DPTC_NUM_PARAMS];
> > + struct dptc_attr_sysfs expanded_attr;
> > + struct kobj_attribute save_settings_attr;
> > +};
> > +
> > +static struct platform_device *dptc_pdev;
> > +
> > +static u32 dptc_get_min(struct dptc_priv *dptc, int idx)
> > +{
> > + return dptc->expanded ? dptc->dev_limits->params[idx].min
> > + : dptc->dev_limits->params[idx].smin;
> > +}
> > +
> > +static u32 dptc_get_max(struct dptc_priv *dptc, int idx)
> > +{
> > + return dptc->expanded ? dptc->dev_limits->params[idx].max
> > + : dptc->dev_limits->params[idx].smax;
> > +}
> > +
> > +static u32 dptc_get_default(struct dptc_priv *dptc, int idx)
> > +{
> > + return dptc->dev_limits->params[idx].def;
> > +}
> > +
> > +static int dptc_alib_call(const u8 *ids, const u32 *vals, int count)
> > +{
> > + union acpi_object in_params[2];
> > + struct acpi_object_list input;
> > + acpi_status status;
> > + u32 buf_size;
> > + int i, off;
> > + u8 *buf;
> > +
> > + if (count == 0)
> > + return -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;
> > +
> > + put_unaligned_le16(buf_size, buf);
> > +
> > + for (i = 0; i < count; i++) {
> > + off = 2 + i * 5;
> > + buf[off] = ids[i];
> > + put_unaligned_le32(vals[i], buf + off + 1);
> > + }
> > +
> > + in_params[0].type = ACPI_TYPE_INTEGER;
> > + in_params[0].integer.value = ALIB_FUNC_DPTC;
> > + in_params[1].type = ACPI_TYPE_BUFFER;
> > + in_params[1].buffer.length = buf_size;
> > + in_params[1].buffer.pointer = buf;
> > +
> > + input.count = 2;
> > + input.pointer = in_params;
> > +
> > + status = acpi_evaluate_object(NULL, ALIB_PATH, &input, NULL);
> > + kfree(buf);
> > +
> > + if (ACPI_FAILURE(status)) {
> > + pr_err("ALIB call failed: %s\n",
> > + acpi_format_exception(status));
> > + return -EIO;
> > + }
> > +
> > + pr_debug("sent %d ALIB parameter(s)\n", count);
> > + return 0;
> > +}
> > +
> > +static int dptc_alib_send_one(int idx, u32 val)
> > +{
> > + u32 vals[2];
> > + u8 ids[2];
> > + int count = 0;
> > +
> > + ids[count] = dptc_params[idx].param_id;
> > + vals[count] = val;
> > + count++;
> > + if (dptc_params[idx].param_id2) {
> > + ids[count] = dptc_params[idx].param_id2;
> > + vals[count] = val;
> > + count++;
> > + }
> > +
> > + return dptc_alib_call(ids, vals, count);
> > +}
> > +
> > +static int dptc_alib_save(struct dptc_priv *dptc)
> > +{
> > + u32 vals[DPTC_NUM_PARAMS * 2];
> > + u8 ids[DPTC_NUM_PARAMS * 2];
> > + int i, count = 0;
> > +
> > + 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 (dptc_params[i].param_id2) {
> > + ids[count] = dptc_params[i].param_id2;
> > + vals[count] = dptc->staged[i];
> > + count++;
> > + }
> > + }
> > +
> > + if (!count)
> > + return 0;
> > +
> > + return dptc_alib_call(ids, vals, count);
> > +}
> > +
> > +/* Sysfs callbacks */
> > +
> > +static ssize_t dptc_current_value_show(struct kobject *kobj,
> > + struct kobj_attribute *attr, char *buf)
> > +{
> > + struct dptc_attr_sysfs *ps =
> > + container_of(attr, struct dptc_attr_sysfs, current_value);
> > + struct dptc_priv *dptc = ps->priv;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
> > + u32 val = dptc->dev_limits->profiles[dptc->profile].vals[ps->idx];
> > +
> > + if (!val)
> > + return sysfs_emit(buf, "\n");
> > + return sysfs_emit(buf, "%u\n", val);
> > + }
> > +
> > + if (!dptc->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_attr_sysfs *ps =
> > + container_of(attr, struct dptc_attr_sysfs, current_value);
> > + struct dptc_priv *dptc = ps->priv;
> > + u32 val, min, max;
> > + int ret;
> > +
> > + if (count == 1 && buf[0] == '\n') {
> > + guard(mutex)(&dptc->lock);
> > +
> > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> > + return -EPERM;
> > + dptc->has_staged[ps->idx] = false;
> > + return count;
> > + }
> > +
> > + ret = kstrtou32(buf, 10, &val);
> > + if (ret)
> > + return ret;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> > + return -EPERM;
> > + min = dptc_get_min(dptc, ps->idx);
> > + max = dptc_get_max(dptc, ps->idx);
> > + if (val < min || (max && val > max))
> > + return -EINVAL;
> > + dptc->staged[ps->idx] = val;
> > + dptc->has_staged[ps->idx] = true;
> > + if (dptc->save_mode == SAVE_SINGLE)
> > + ret = dptc_alib_send_one(ps->idx, val);
> > +
> > + return ret ? ret : count;
> > +}
> > +
> > +static ssize_t dptc_default_value_show(struct kobject *kobj,
> > + struct kobj_attribute *attr, char *buf)
> > +{
> > + struct dptc_attr_sysfs *ps =
> > + container_of(attr, struct dptc_attr_sysfs, default_value);
> > +
> > + return sysfs_emit(buf, "%u\n", dptc_get_default(ps->priv, ps->idx));
> > +}
> > +
> > +static ssize_t dptc_min_value_show(struct kobject *kobj,
> > + struct kobj_attribute *attr, char *buf)
> > +{
> > + struct dptc_attr_sysfs *ps =
> > + container_of(attr, struct dptc_attr_sysfs, min_value);
> > + struct dptc_priv *dptc = ps->priv;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + return sysfs_emit(buf, "%u\n", dptc_get_min(dptc, ps->idx));
> > +}
> > +
> > +static ssize_t dptc_max_value_show(struct kobject *kobj,
> > + struct kobj_attribute *attr, char *buf)
> > +{
> > + struct dptc_attr_sysfs *ps =
> > + container_of(attr, struct dptc_attr_sysfs, max_value);
> > + struct dptc_priv *dptc = ps->priv;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + return sysfs_emit(buf, "%u\n", dptc_get_max(dptc, ps->idx));
> > +}
> > +
> > +static ssize_t dptc_scalar_increment_show(struct kobject *kobj,
> > + struct kobj_attribute *attr, char *buf)
> > +{
> > + return sysfs_emit(buf, "1\n");
> > +}
> > +
> > +static ssize_t dptc_display_name_show(struct kobject *kobj,
> > + struct kobj_attribute *attr, char *buf)
> > +{
> > + struct dptc_attr_sysfs *ps =
> > + container_of(attr, struct dptc_attr_sysfs, display_name);
> > + return sysfs_emit(buf, "%s\n", dptc_params[ps->idx].display_name);
> > +}
> > +
> > +static ssize_t dptc_type_show(struct kobject *kobj,
> > + struct kobj_attribute *attr, char *buf)
> > +{
> > + return sysfs_emit(buf, "integer\n");
> > +}
> > +
> > +static ssize_t dptc_save_settings_show(struct kobject *kobj,
> > + struct kobj_attribute *attr, char *buf)
> > +{
> > + struct dptc_priv *dptc =
> > + container_of(attr, struct dptc_priv, save_settings_attr);
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + if (dptc->save_mode == SAVE_SINGLE)
> > + return sysfs_emit(buf, "single\n");
> > + return sysfs_emit(buf, "bulk\n");
> > +}
> > +
> > +static ssize_t dptc_save_settings_store(struct kobject *kobj,
> > + struct kobj_attribute *attr,
> > + const char *buf, size_t count)
> > +{
> > + struct dptc_priv *dptc =
> > + container_of(attr, struct dptc_priv, save_settings_attr);
> > + int ret = 0;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + if (sysfs_streq(buf, "save"))
> > + ret = dptc_alib_save(dptc);
> > + else if (sysfs_streq(buf, "single"))
> > + dptc->save_mode = SAVE_SINGLE;
> > + else if (sysfs_streq(buf, "bulk"))
> > + dptc->save_mode = SAVE_BULK;
> > + else
> > + return -EINVAL;
> > +
> > + return ret ? ret : count;
> > +}
> > +
> > +static ssize_t dptc_expanded_current_value_show(struct kobject *kobj,
> > + struct kobj_attribute *attr,
> > + char *buf)
> > +{
> > + struct dptc_attr_sysfs *ps =
> > + container_of(attr, struct dptc_attr_sysfs, current_value);
> > + struct dptc_priv *dptc = ps->priv;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + return sysfs_emit(buf, "%d\n", dptc->expanded);
> > +}
> > +
> > +static ssize_t dptc_expanded_current_value_store(struct kobject *kobj,
> > + struct kobj_attribute *attr,
> > + const char *buf, size_t count)
> > +{
> > + struct dptc_attr_sysfs *ps =
> > + container_of(attr, struct dptc_attr_sysfs, current_value);
> > + struct dptc_priv *dptc = ps->priv;
> > + bool val;
> > + int ret;
> > +
> > + ret = kstrtobool(buf, &val);
> > + if (ret)
> > + return ret;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> > + return -EPERM;
> > + dptc->expanded = val;
> > + /* Clear staged values: limits changed, old values may be out of range */
> > + memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
> > +
> > + return count;
> > +}
> > +
> > +static ssize_t dptc_expanded_default_value_show(struct kobject *kobj,
> > + struct kobj_attribute *attr,
> > + char *buf)
> > +{
> > + return sysfs_emit(buf, "0\n");
> > +}
> > +
> > +static ssize_t dptc_expanded_min_value_show(struct kobject *kobj,
> > + struct kobj_attribute *attr,
> > + char *buf)
> > +{
> > + return sysfs_emit(buf, "0\n");
> > +}
> > +
> > +static ssize_t dptc_expanded_max_value_show(struct kobject *kobj,
> > + struct kobj_attribute *attr,
> > + char *buf)
> > +{
> > + return sysfs_emit(buf, "1\n");
> > +}
> > +
> > +static ssize_t dptc_expanded_scalar_increment_show(struct kobject *kobj,
> > + struct kobj_attribute *attr,
> > + char *buf)
> > +{
> > + return sysfs_emit(buf, "1\n");
> > +}
> > +
> > +static ssize_t dptc_expanded_display_name_show(struct kobject *kobj,
> > + struct kobj_attribute *attr,
> > + char *buf)
> > +{
> > + return sysfs_emit(buf, "Expanded Limits\n");
> > +}
> > +
> > +static ssize_t dptc_expanded_type_show(struct kobject *kobj,
> > + struct kobj_attribute *attr, char *buf)
> > +{
> > + return sysfs_emit(buf, "integer\n");
> > +}
> > +
> > +/* Sysfs setup */
> > +
> > +static void dptc_setup_param_sysfs(struct dptc_priv *dptc,
> > + struct dptc_attr_sysfs *ps, int idx)
> > +{
> > + ps->priv = dptc;
> > + ps->idx = idx;
> > +
> > + sysfs_attr_init(&ps->current_value.attr);
> > + ps->current_value.attr.name = "current_value";
> > + ps->current_value.attr.mode = 0644;
> > + ps->current_value.show = dptc_current_value_show;
> > + ps->current_value.store = dptc_current_value_store;
> > +
> > + sysfs_attr_init(&ps->default_value.attr);
> > + ps->default_value.attr.name = "default_value";
> > + ps->default_value.attr.mode = 0444;
> > + ps->default_value.show = dptc_default_value_show;
> > +
> > + sysfs_attr_init(&ps->min_value.attr);
> > + ps->min_value.attr.name = "min_value";
> > + ps->min_value.attr.mode = 0444;
> > + ps->min_value.show = dptc_min_value_show;
> > +
> > + sysfs_attr_init(&ps->max_value.attr);
> > + ps->max_value.attr.name = "max_value";
> > + ps->max_value.attr.mode = 0444;
> > + ps->max_value.show = dptc_max_value_show;
> > +
> > + sysfs_attr_init(&ps->scalar_increment.attr);
> > + ps->scalar_increment.attr.name = "scalar_increment";
> > + ps->scalar_increment.attr.mode = 0444;
> > + ps->scalar_increment.show = dptc_scalar_increment_show;
> > +
> > + sysfs_attr_init(&ps->display_name.attr);
> > + ps->display_name.attr.name = "display_name";
> > + ps->display_name.attr.mode = 0444;
> > + ps->display_name.show = dptc_display_name_show;
> > +
> > + sysfs_attr_init(&ps->type.attr);
> > + ps->type.attr.name = "type";
> > + ps->type.attr.mode = 0444;
> > + ps->type.show = dptc_type_show;
> > +
> > + ps->attrs[0] = &ps->current_value.attr;
> > + ps->attrs[1] = &ps->default_value.attr;
> > + ps->attrs[2] = &ps->min_value.attr;
> > + ps->attrs[3] = &ps->max_value.attr;
> > + ps->attrs[4] = &ps->scalar_increment.attr;
> > + ps->attrs[5] = &ps->display_name.attr;
> > + ps->attrs[6] = &ps->type.attr;
> > + ps->attrs[7] = NULL;
> > +
> > + ps->group.name = dptc_params[idx].name;
> > + ps->group.attrs = ps->attrs;
> > +}
> > +
> > +static void dptc_setup_expanded_sysfs(struct dptc_priv *dptc,
> > + struct dptc_attr_sysfs *ps)
> > +{
> > + ps->priv = dptc;
> > + sysfs_attr_init(&ps->current_value.attr);
> > + ps->current_value.attr.name = "current_value";
> > + ps->current_value.attr.mode = 0644;
> > + ps->current_value.show = dptc_expanded_current_value_show;
> > + ps->current_value.store = dptc_expanded_current_value_store;
> > +
> > + sysfs_attr_init(&ps->default_value.attr);
> > + ps->default_value.attr.name = "default_value";
> > + ps->default_value.attr.mode = 0444;
> > + ps->default_value.show = dptc_expanded_default_value_show;
> > +
> > + sysfs_attr_init(&ps->min_value.attr);
> > + ps->min_value.attr.name = "min_value";
> > + ps->min_value.attr.mode = 0444;
> > + ps->min_value.show = dptc_expanded_min_value_show;
> > +
> > + sysfs_attr_init(&ps->max_value.attr);
> > + ps->max_value.attr.name = "max_value";
> > + ps->max_value.attr.mode = 0444;
> > + ps->max_value.show = dptc_expanded_max_value_show;
> > +
> > + sysfs_attr_init(&ps->scalar_increment.attr);
> > + ps->scalar_increment.attr.name = "scalar_increment";
> > + ps->scalar_increment.attr.mode = 0444;
> > + ps->scalar_increment.show = dptc_expanded_scalar_increment_show;
> > +
> > + sysfs_attr_init(&ps->display_name.attr);
> > + ps->display_name.attr.name = "display_name";
> > + ps->display_name.attr.mode = 0444;
> > + ps->display_name.show = dptc_expanded_display_name_show;
> > +
> > + sysfs_attr_init(&ps->type.attr);
> > + ps->type.attr.name = "type";
> > + ps->type.attr.mode = 0444;
> > + ps->type.show = dptc_expanded_type_show;
> > +
> > + ps->attrs[0] = &ps->current_value.attr;
> > + ps->attrs[1] = &ps->default_value.attr;
> > + ps->attrs[2] = &ps->min_value.attr;
> > + ps->attrs[3] = &ps->max_value.attr;
> > + ps->attrs[4] = &ps->scalar_increment.attr;
> > + ps->attrs[5] = &ps->display_name.attr;
> > + ps->attrs[6] = &ps->type.attr;
> > + ps->attrs[7] = NULL;
> > +
> > + ps->group.name = "expanded_limits";
> > + ps->group.attrs = ps->attrs;
> > +}
> > +
> > +static void dptc_fw_dev_unregister(void *data)
> > +{
> > + device_unregister(data);
> > +}
> > +
> > +static void dptc_kset_unregister(void *data)
> > +{
> > + kset_unregister(data);
> > +}
> > +
> > +/* Platform profile */
> > +
> > +static void dptc_apply_profile(struct dptc_priv *dptc,
> > + enum platform_profile_option profile)
> > +{
> > + const struct dptc_profile *pp;
> > + int i;
> > +
> > + memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
> > +
> > + if (profile == PLATFORM_PROFILE_CUSTOM)
> > + return;
> > +
> > + pp = &dptc->dev_limits->profiles[profile];
> > + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> > + if (!pp->vals[i])
> > + continue;
> > + dptc->staged[i] = pp->vals[i];
> > + dptc->has_staged[i] = true;
> > + }
> > +}
> > +
> > +static int dptc_pp_probe(void *drvdata, unsigned long *choices)
> > +{
> > + struct dptc_priv *dptc = drvdata;
> > + int i, j;
> > +
> > + set_bit(PLATFORM_PROFILE_CUSTOM, choices);
> > + for (i = 0; i < PLATFORM_PROFILE_LAST; i++) {
> > + for (j = 0; j < DPTC_NUM_PARAMS; j++) {
> > + if (dptc->dev_limits->profiles[i].vals[j]) {
> > + set_bit(i, choices);
> > + break;
> > + }
> > + }
> > + }
> > + return 0;
> > +}
> > +
> > +static int dptc_pp_get(struct device *dev,
> > + enum platform_profile_option *profile)
> > +{
> > + struct dptc_priv *dptc = dev_get_drvdata(dev);
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + *profile = dptc->profile;
> > + return 0;
> > +}
> > +
> > +static int dptc_pp_set(struct device *dev,
> > + enum platform_profile_option profile)
> > +{
> > + struct dptc_priv *dptc = dev_get_drvdata(dev);
> > + int ret = 0;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + dptc->profile = profile;
> > + dptc_apply_profile(dptc, profile);
> > + if (profile != PLATFORM_PROFILE_CUSTOM)
> > + ret = dptc_alib_save(dptc);
> > +
> > + return ret;
> > +}
> > +
> > +static const struct platform_profile_ops dptc_pp_ops = {
> > + .probe = dptc_pp_probe,
> > + .profile_get = dptc_pp_get,
> > + .profile_set = dptc_pp_set,
> > +};
> > +
> > +static int dptc_resume(struct device *dev)
> > +{
> > + struct dptc_priv *dptc = dev_get_drvdata(dev);
> > + int ret;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
> > + dptc_apply_profile(dptc, dptc->profile);
> > + ret = dptc_alib_save(dptc);
> > + } else if (dptc->save_mode == SAVE_SINGLE) {
> > + ret = dptc_alib_save(dptc);
> > + } else {
> > + ret = 0;
> > + }
> > +
> > + if (ret)
> > + dev_warn(dev, "failed to restore settings on resume: %d\n", ret);
> > +
> > + return 0;
>
> return ret?
I am not sure of the effects of erroring resume or whether this should
classify as a failure.
> > +}
> > +
> > +static DEFINE_SIMPLE_DEV_PM_OPS(dptc_pm_ops, NULL, dptc_resume);
> > +
> > +static int dptc_probe(struct platform_device *pdev)
> > +{
> > + const struct dmi_system_id *dmi_match = dev_get_platdata(&pdev->dev);
> > + struct dptc_priv *dptc;
> > + struct device *dev = &pdev->dev;
> > + int i, ret;
>
> reverse xmas tree here
Missed this one, noted.
> > +
> > + dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL);
> > + if (!dptc)
> > + return -ENOMEM;
> > +
> > + platform_set_drvdata(pdev, dptc);
> > +
> > + ret = devm_mutex_init(dev, &dptc->lock);
> > + if (ret)
> > + return ret;
> > +
> > + dptc->dev_limits = dmi_match->driver_data;
> > + dev_info(dev, "%s (%s)\n", dmi_match->ident,
> > + boot_cpu_data.x86_model_id);
> > +
> > + dptc->fw_attr_dev = device_create(&firmware_attributes_class,
> > + NULL, MKDEV(0, 0), NULL,
> > + DRIVER_NAME);
> > + if (IS_ERR(dptc->fw_attr_dev))
> > + return PTR_ERR(dptc->fw_attr_dev);
> > +
> > + ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister,
> > + dptc->fw_attr_dev);
> > + if (ret)
> > + return ret;
> > +
> > + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL,
> > + &dptc->fw_attr_dev->kobj);
> > + if (!dptc->fw_attr_kset)
> > + return -ENOMEM;
> > +
> > + ret = devm_add_action_or_reset(dev, dptc_kset_unregister,
> > + dptc->fw_attr_kset);
> > + if (ret)
> > + return ret;
> > +
> > + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> > + dptc_setup_param_sysfs(dptc, &dptc->params[i], i);
> > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> > + &dptc->params[i].group);
> > + if (ret)
> > + return ret;
> > + }
> > +
> > + dptc_setup_expanded_sysfs(dptc, &dptc->expanded_attr);
> > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> > + &dptc->expanded_attr.group);
> > + if (ret)
> > + return ret;
> > +
> > + sysfs_attr_init(&dptc->save_settings_attr.attr);
> > + dptc->save_settings_attr.attr.name = "save_settings";
> > + dptc->save_settings_attr.attr.mode = 0644;
> > + dptc->save_settings_attr.show = dptc_save_settings_show;
> > + dptc->save_settings_attr.store = dptc_save_settings_store;
> > + ret = sysfs_create_file(&dptc->fw_attr_kset->kobj,
> > + &dptc->save_settings_attr.attr);
> > + if (ret)
> > + return ret;
>
> Given multiple drivers have a save_settings feature now, maybe it's
> worth creating a helper as part of this series.
I will consider it. It would be good as a follow-up in any case. I did
not manage to find attributes helpers. I am basing on 6.19.5 for now.
> > +
> > + dptc->profile = PLATFORM_PROFILE_CUSTOM;
>
> So it starts in custom? Wouldn't it make more sense to start in balanced?
As noted in the cover letter, the driver starts in custom to avoid
changing the BIOS defaults. If a power utility is installed such as
ppd, the mode is set to balanced automatically.
E.g., the GPD Win 5 starts at 45W for ppt/sppt/fppt. Balanced would
set it to 25W. I need to review the values for the GPD Win 5 closer, I
have not had a lot of user feedback about it yet. Perhaps the balanced
profile for it should be higher.
> > + dptc->ppdev = devm_platform_profile_register(dev, "amd-dptc", dptc,
> > + &dptc_pp_ops);
> > + if (IS_ERR(dptc->ppdev))
> > + return PTR_ERR(dptc->ppdev);
> > +
> > + return 0;
> > +}
> > +
> > +static struct platform_driver dptc_driver = {
> > + .driver = {
> > + .name = DRIVER_NAME,
> > + .pm = pm_sleep_ptr(&dptc_pm_ops),
> > + },
> > + .probe = dptc_probe,
> > +};
> > +
> > +static int __init dptc_init(void)
> > +{
> > + const struct dmi_system_id *match;
> > + bool soc_found = false;
> > + int i, ret;
> > +
> > + if (!acpi_has_method(NULL, ALIB_PATH)) {
> > + pr_debug("ALIB method not present\n");
>
> To avoid an extra debug statement on every machine not quirked - how
> about you start with the quirk check?
The driver only loads for DMI matches so that will not happen.
Beforehand I had a parameter to override DMI match.
However, since it is gone now, I will move it below the DMI match and
switch to pr_warn to match the SoC error.
> If not that, could you do any checks that ALIB_FUNC_DPTC is supported?
> Not sure if there was a method for that.
>
>
> > + return -ENODEV;
> > + }
>
> I will admit I didn't look at the spec, but is there a version lookup
> for this method that can be used?
I do not think there is. The spec is very dated and only includes
parameters up to 0x04. The others were pulled from the Legion Go DSDT.
I will nix the comment referencing the spec for the parameters.
Perhaps you or Shyam could help in this. However, since the interface
is meant to be used for BIOS instrumentation perhaps there is no
autodiscovery.
> > +
> > + match = dmi_first_match(dptc_dmi_table);
> > + if (!match)
> > + return -ENODEV;
> > +
> > + for (i = 0; dptc_soc_table[i]; i++) {
> > + if (strstr(boot_cpu_data.x86_model_id,
> > + dptc_soc_table[i])) {
> > + soc_found = true;
> > + break;
> > + }
> > + }
> > + if (!soc_found) {
> > + pr_warn("unrecognized SoC '%s'\n",
> > + boot_cpu_data.x86_model_id);
> > + return -ENODEV;
> > + }
>
> Is this SOC lookup still going to be needed if you're 100% quirks table?
> I would think no.
It is an additional safety feature. It can be dropped.
Although as noted in the cover letter, the same motherboard is used
for different SoCs for multiple models including the GPD Win 5. At
minimum I'd have to drop all non-exact DMI matches.
Antheas
> > +
> > + dptc_pdev = platform_device_register_data(NULL, DRIVER_NAME, -1,
> > + match, sizeof(*match));
> > + if (IS_ERR(dptc_pdev))
> > + return PTR_ERR(dptc_pdev);
> > +
> > + ret = platform_driver_register(&dptc_driver);
> > + if (ret) {
> > + platform_device_unregister(dptc_pdev);
> > + return ret;
> > + }
> > +
> > + return 0;
> > +}
> > +
> > +static void __exit dptc_exit(void)
> > +{
> > + platform_driver_unregister(&dptc_driver);
> > + platform_device_unregister(dptc_pdev);
> > +}
> > +
> > +module_init(dptc_init);
> > +module_exit(dptc_exit);
> > +
> > +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>");
> > +MODULE_DESCRIPTION("AMD DPTCi ACPI Driver");
> > +MODULE_LICENSE("GPL");
>
>
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [RFC v2 2/2] platform/x86/amd: Add AMD DPTCi driver
2026-03-05 18:17 ` [RFC v2 2/2] platform/x86/amd: Add AMD DPTCi driver Antheas Kapenekakis
2026-03-05 20:57 ` Mario Limonciello
@ 2026-03-06 21:14 ` Derek John Clark
2026-03-07 11:17 ` Antheas Kapenekakis
1 sibling, 1 reply; 8+ messages in thread
From: Derek John Clark @ 2026-03-06 21:14 UTC (permalink / raw)
To: Antheas Kapenekakis
Cc: Mario.Limonciello, W_Armin, sashal, Shyam-Sundar.S-k,
denis.benato, linux-kernel, platform-driver-x86
On Thu, Mar 5, 2026 at 10:19 AM Antheas Kapenekakis <lkml@antheas.dev> 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.
Hi Antheas,
Thanks for the CC. I've long opined the lack of having a generic
interface for these features in the kernel. With that being said, I
think very careful consideration for the interface is a necessity. We
never want to have the chance for an unsuspecting or inexperienced
user to have hardware malfunctions by misusing the interface. As a
general rule I think it would be good for each individual device added
to have a tested-by tag before it is included. That message would
preferably include some sort of metadata identifying the true hardware
limits (tech docs if available, but marketing material or OEM software
screenshots would be sufficient, we know how some of these companies
are with information)
> The driver matches the ABI of asus-armoury, by exposing a platform
> profile with reasonable tunings for devices depending on their max
> thermal envelope, and {ppt_pl1_spl,ppt_pl2_sppt,ppt_pl3_fppt,cpu_temp}
> tunables which only become writable when the profile is custom,
> otherwise they are read-only and reflect the current profile.
>
> The default profile is custom so that we do not write to the device
> until userspace explicitly selects a profile, remaining at firmware
> defaults.
I see what you're trying to do here, but I'm a bit conflicted on the
implementation. Hardware defaults are there for a reason after all...
Part of the original discussion with custom was that setting it would
taint the kernel to inform remote troubleshooters that there was a
non-standard change. TBS I don't see that in the actual code for
acpi/platform_profile.c so maybe the intention was that the drivers
themselves set it? If that's the case then I'll need to submit a patch
to add that to lenovo-wmi-gamezone so I'm compliant. I'd like Mario's
input on that first as IMO it makes more sense to do it as a blanket
in the platform class, or we don't bother as we expect drivers to
respect the TDP limits set by ODM/OEM. If we do end up doing that
though, just booting a device with this driver would automatically
taint the kernel, which is certainly not the desired behavior. I'm not
sure how to best square that circle. Do we know the BIOS defaults for
all the devices we're adding here? I'd expect that would correspond
with balanced most of the time, so perhaps making sure those match is
a good compromise.
> Assisted-by: Claude:claude-opus-4-6
> Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> ---
> MAINTAINERS | 6 +
> drivers/platform/x86/amd/Kconfig | 16 +
> drivers/platform/x86/amd/Makefile | 2 +
> drivers/platform/x86/amd/dptc.c | 1255 +++++++++++++++++++++++++++++
> 4 files changed, 1279 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..ee79a2c084a8 100644
> --- a/drivers/platform/x86/amd/Kconfig
> +++ b/drivers/platform/x86/amd/Kconfig
> @@ -44,3 +44,19 @@ 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 ACPI_PLATFORM_PROFILE
> + select FIRMWARE_ATTRIBUTES_CLASS
> + help
> + Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> + Thermal Configuration Interface (DPTCi). Exposes TDP and thermal
> + parameters for AMD APU-based handheld devices via the
> + firmware-attributes sysfs ABI, allowing userspace tools to stage
> + and atomically commit power limit settings. Requires a DMI match
> + for the device and a recognized AMD SoC.
> +
> + If built as a module, the module will be called amd_dptc.
> +
> diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile
> index f6ff0c837f34..862a609bfe38 100644
> --- a/drivers/platform/x86/amd/Makefile
> +++ b/drivers/platform/x86/amd/Makefile
> @@ -12,3 +12,5 @@ obj-$(CONFIG_AMD_PMF) += pmf/
> obj-$(CONFIG_AMD_WBRF) += wbrf.o
> obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o
> obj-$(CONFIG_AMD_HFI) += hfi/
> +obj-$(CONFIG_AMD_DPTC) += amd_dptc.o
> +amd_dptc-y := dptc.o
> diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
> new file mode 100644
> index 000000000000..68d9d3b85cb6
> --- /dev/null
> +++ b/drivers/platform/x86/amd/dptc.c
> @@ -0,0 +1,1255 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver
> + *
> + * Exposes AMD APU power and thermal parameters via the firmware-attributes
> + * sysfs ABI. Parameters are staged and atomically committed through the
> + * AGESA ALIB Function 0x0C (Dynamic Power and Thermal Configuration
> + * interface).
> + *
> + * Reference: AMD AGESA Publication #44065, Appendix E.5
> + * https://docs.amd.com/v/u/en-US/44065_Arch2008
> + *
> + * Copyright (C) 2026 Antheas Kapenekakis <lkml@antheas.dev>
> + */
> +
> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> +
> +#include <linux/acpi.h>
> +#include <linux/cleanup.h>
> +#include <linux/dmi.h>
> +#include <linux/init.h>
> +#include <linux/kobject.h>
> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/platform_device.h>
> +#include <linux/platform_profile.h>
> +#include <linux/processor.h>
> +#include <linux/sysfs.h>
> +#include <linux/unaligned.h>
> +
> +#include "../firmware_attributes_class.h"
> +
> +#define DRIVER_NAME "amd_dptc"
> +
> +#define ALIB_FUNC_DPTC 0x0C
> +#define ALIB_PATH "\\_SB.ALIB"
> +
> +/* ALIB parameter IDs (AGESA spec Appendix E.5, Table E-52) */
> +#define ALIB_ID_TEMP_TARGET 0x03
> +#define ALIB_ID_STAPM_LIMIT 0x05
> +#define ALIB_ID_FAST_LIMIT 0x06
> +#define ALIB_ID_SLOW_LIMIT 0x07
> +#define ALIB_ID_SKIN_LIMIT 0x2E
> +
> +enum dptc_param_idx {
> + DPTC_PPT_PL1_SPL, /* STAPM + skin limit (set together) */
> + DPTC_PPT_PL2_SPPT, /* slow PPT limit */
> + DPTC_PPT_PL3_FPPT, /* fast PPT limit */
> + DPTC_CPU_TEMP, /* thermal control target */
> + DPTC_NUM_PARAMS,
> +};
> +
> +struct dptc_param_limits {
> + u32 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_profile {
> + u32 vals[DPTC_NUM_PARAMS]; /* 0 = don't set / unstage this param */
> +};
> +
> +struct dptc_device_limits {
> + struct dptc_param_limits params[DPTC_NUM_PARAMS];
> + struct dptc_profile profiles[PLATFORM_PROFILE_LAST];
> +};
> +
> +struct dptc_param_desc {
> + const char *name;
> + const char *display_name;
> + u8 param_id;
> + u8 param_id2; /* secondary ALIB ID, 0 if none */
> +};
> +
> +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = {
> + [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (mW)",
> + ALIB_ID_STAPM_LIMIT, ALIB_ID_SKIN_LIMIT },
> + [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (mW)",
> + ALIB_ID_SLOW_LIMIT },
> + [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (mW)",
> + ALIB_ID_FAST_LIMIT },
> + [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)",
> + ALIB_ID_TEMP_TARGET },
> +};
You already mentioned this elsewhere, but I agree that using W over mW
would make sense to keep the interfaces consistent.
> +/* 18W class: AYANEO AIR Plus (Ryzen 5 5560U) */
> +static const struct dptc_device_limits limits_18w = { .params = {
> + [DPTC_PPT_PL1_SPL] = { 0, 5000, 15000, 18000, 22000 },
> + [DPTC_PPT_PL2_SPPT] = { 0, 5000, 15000, 18000, 22000 },
> + [DPTC_PPT_PL3_FPPT] = { 0, 5000, 15000, 20000, 25000 },
> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> +}, .profiles = {
> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5000, 5000, 8000, 0 } },
> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 12000, 14000, 15000, 0 } },
> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 18000, 18000, 20000, 0 } },
> +}};
> +
I would personally prefer these be set directly in the DMI match per
device. Its more code in the end, but its more cautious, as each
manufacturer has a bespoke cooling solution. Assuming they're all
equivalent is not best practice IMO. I have quite a bit of historical
data on this so I can help fill in the blanks where it is missing.
> +/* 25W class: Ryzen 5000 handhelds (AYANEO NEXT, KUN) */
> +static const struct dptc_device_limits limits_25w = { .params = {
> + [DPTC_PPT_PL1_SPL] = { 0, 4000, 15000, 25000, 32000 },
> + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 20000, 27000, 35000 },
> + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 25000, 30000, 37000 },
> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> +}, .profiles = {
> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8000, 8000, 12000, 0 } },
> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15000, 17000, 20000, 0 } },
> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25000, 27000, 30000, 0 } },
> +}};
>
> +/* 28W class: GPD Win series, AYANEO 2, OrangePi NEO-01 */
> +static const struct dptc_device_limits limits_28w = { .params = {
> + [DPTC_PPT_PL1_SPL] = { 0, 4000, 15000, 28000, 32000 },
> + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 20000, 30000, 35000 },
> + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 25000, 32000, 37000 },
> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> +}, .profiles = {
> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8000, 8000, 12000, 0 } },
> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15000, 17000, 22000, 0 } },
> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25000, 28000, 32000, 0 } },
> + [PLATFORM_PROFILE_MAX_POWER] = { .vals = { 28000, 30000, 32000, 0 } },
> +}};
> +
> +/* 30W class: OneXPlayer, AYANEO AIR/FLIP/GEEK/SLIDE/3, AOKZOE */
> +static const struct dptc_device_limits limits_30w = { .params = {
> + [DPTC_PPT_PL1_SPL] = { 0, 4000, 15000, 30000, 40000 },
> + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 20000, 32000, 43000 },
> + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 25000, 41000, 50000 },
> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> +}, .profiles = {
> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8000, 8000, 12000, 0 } },
> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15000, 17000, 25000, 0 } },
> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25000, 28000, 41000, 0 } },
> + [PLATFORM_PROFILE_MAX_POWER] = { .vals = { 30000, 32000, 41000, 0 } },
> +}};
> +
> +/* AI MAX Handheld class: GPD Win 5 */
> +static const struct dptc_device_limits limits_maxhh = { .params = {
This struct name seems arbitrary compared to the others. If this is
the eventual pattern this should just be called _80w
> + [DPTC_PPT_PL1_SPL] = { 0, 4000, 25000, 80000, 100000 },
> + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 27000, 82000, 100000 },
> + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 40000, 85000, 100000 },
> + [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 },
> +}, .profiles = {
> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 15000, 15000, 25000, 0 } },
> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 25000, 27000, 40000, 0 } },
> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 60000, 63000, 85000, 0 } },
> + [PLATFORM_PROFILE_MAX_POWER] = { .vals = { 80000, 82000, 85000, 0 } },
> +}};
One other thing to consider is AC vs DC power draw. Thermal load is
important, but we also want to avoid over-stressing the battery on
these devices. I would restrict MAX_POWER to when AC is plugged ad
return an error if on DC, and ensure PERFORMANCE is at or just below
peak DC power draw. I didn't do this in gamezone on Lenovo because
those limits are BIOS enforced and MAX_POWER will just match
PERFORMANCE on DC.
> +/* Substring matches against boot_cpu_data.x86_model_id; order matters. */
> +static const char * const dptc_soc_table[] = {
> + /* AI MAX */
> + "AMD RYZEN AI MAX+ 395",
> + "AMD RYZEN AI MAX+ 385",
> + "AMD RYZEN AI MAX 380",
> + /* Ryzen AI */
> + "AMD Ryzen AI 9 HX 370",
> + "AMD Ryzen AI HX 360",
> + /* Z1 - Extreme before plain Z1 */
> + "AMD Ryzen Z1 Extreme",
> + "AMD Ryzen Z1",
> + /* Ryzen 8000 */
> + "AMD Ryzen 7 8840U",
> + /* Ryzen 7040 */
> + "AMD Ryzen 7 7840U",
> + /* Ryzen 6000 */
> + "AMD Ryzen 7 6800U",
> + "AMD Ryzen 7 6600U",
> + /* Ryzen 5000 */
> + "AMD Ryzen 7 5800U",
> + "AMD Ryzen 7 5700U",
> + "AMD Ryzen 5 5560U",
> + NULL,
> +};
I have some data on 4800/4800U and AMD Athelon Silver APU devices as
well. When I return from my conference I can post some DSDT dumps to
validate the interface is present on them as well.
> +static const struct dmi_system_id dptc_dmi_table[] = {
> + /* GPD */
> + {
> + .ident = "GPD Win Mini",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-01"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Win Mini 2024",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Win Mini 2024",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02-L"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Win 4",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-04"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Win 5",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"),
> + },
> + .driver_data = (void *)&limits_maxhh,
> + },
> + {
> + .ident = "GPD Win Max 2",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-04"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Win Max 2 2024",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-05"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Duo",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Duo",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01-L"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Pocket 4",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Pocket 4",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04-L"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + /* OrangePi */
> + {
> + .ident = "OrangePi NEO-01",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + /* AYN */
> + {
> + .ident = "AYN Loki Max",
> + .matches = {
> + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ayn"),
> + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Loki Max"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + /* Tectoy (Zeenix Pro = Loki Max) */
> + {
> + .ident = "Zeenix Pro",
> + .matches = {
> + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Tectoy"),
> + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Zeenix Pro"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + /* AOKZOE */
> + {
> + .ident = "AOKZOE A1 AR07",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 AR07"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "AOKZOE A1 Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 Pro"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "AOKZOE A1X",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1X"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "AOKZOE A2 Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A2 Pro"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + /* OneXPlayer (Intel variants filtered by SoC table) */
> + {
> + .ident = "ONEXPLAYER F1Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1Pro"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER F1 EVA-02",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1 EVA-02"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER 2",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_MATCH(DMI_BOARD_NAME, "ONEXPLAYER 2"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER X1 A",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1 A"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER X1z",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1z"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER X1Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Pro"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER G1 A",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 A"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + /* AYANEO - 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,
> + },
> + /* AYANEO - 25W */
> + {
> + .ident = "AYANEO NEXT Advance",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Advance"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
AYANEO went through a strange DMI phase, many of the first run of NEXT
devices are prefaced with AYANEO in the board name as well
> + {
> + .ident = "AYANEO NEXT Lite",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Lite"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + {
> + .ident = "AYANEO NEXT Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Pro"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + {
> + .ident = "AYANEO NEXT",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + {
> + .ident = "AYANEO KUN",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + {
> + .ident = "AYANEO KUN",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO KUN"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + /* AYANEO - 28W */
> + {
> + .ident = "AYANEO 2",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 2"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "SuiPlay0X1",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "Mysten Labs, Inc."),
> + DMI_MATCH(DMI_PRODUCT_NAME, "SuiPlay0X1"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + /* AYANEO - 30W */
> + {
> + /* 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,
> + },
The original air is max 15w, air pro is 18w. I didn't scrutinize the
rest as closely, but these will need to be validated per device.
(Hence my reccomendation above)
> + {
> + /* 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);
> +
> +struct dptc_priv;
> +
> +struct dptc_attr_sysfs {
> + struct dptc_priv *priv;
> + struct kobj_attribute current_value;
> + struct kobj_attribute default_value;
> + struct kobj_attribute min_value;
> + struct kobj_attribute max_value;
> + struct kobj_attribute scalar_increment;
> + struct kobj_attribute display_name;
> + struct kobj_attribute type;
> + struct attribute *attrs[8];
> + struct attribute_group group;
> + int idx;
> +};
> +
> +struct dptc_priv {
> + struct device *fw_attr_dev;
> + struct kset *fw_attr_kset;
> +
> + const struct dptc_device_limits *dev_limits;
> +
> + bool expanded;
> +
> + enum platform_profile_option profile;
> + struct device *ppdev;
> +
> + enum dptc_save_mode { SAVE_SINGLE, SAVE_BULK } save_mode;
> +
> + u32 staged[DPTC_NUM_PARAMS];
> + bool has_staged[DPTC_NUM_PARAMS];
> +
> + /* Protects mutable driver state */
> + struct mutex lock;
> +
> + struct dptc_attr_sysfs params[DPTC_NUM_PARAMS];
> + struct dptc_attr_sysfs expanded_attr;
> + struct kobj_attribute save_settings_attr;
> +};
> +
> +static struct platform_device *dptc_pdev;
> +
> +static u32 dptc_get_min(struct dptc_priv *dptc, int idx)
> +{
> + return dptc->expanded ? dptc->dev_limits->params[idx].min
> + : dptc->dev_limits->params[idx].smin;
> +}
> +
> +static u32 dptc_get_max(struct dptc_priv *dptc, int idx)
> +{
> + return dptc->expanded ? dptc->dev_limits->params[idx].max
> + : dptc->dev_limits->params[idx].smax;
> +}
> +
> +static u32 dptc_get_default(struct dptc_priv *dptc, int idx)
> +{
> + return dptc->dev_limits->params[idx].def;
> +}
> +
> +static int dptc_alib_call(const u8 *ids, const u32 *vals, int count)
> +{
> + union acpi_object in_params[2];
> + struct acpi_object_list input;
> + acpi_status status;
> + u32 buf_size;
> + int i, off;
> + u8 *buf;
> +
> + if (count == 0)
> + return -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;
> +
> + put_unaligned_le16(buf_size, buf);
> +
> + for (i = 0; i < count; i++) {
> + off = 2 + i * 5;
> + buf[off] = ids[i];
> + put_unaligned_le32(vals[i], buf + off + 1);
> + }
> +
> + in_params[0].type = ACPI_TYPE_INTEGER;
> + in_params[0].integer.value = ALIB_FUNC_DPTC;
> + in_params[1].type = ACPI_TYPE_BUFFER;
> + in_params[1].buffer.length = buf_size;
> + in_params[1].buffer.pointer = buf;
> +
> + input.count = 2;
> + input.pointer = in_params;
> +
> + status = acpi_evaluate_object(NULL, ALIB_PATH, &input, NULL);
> + kfree(buf);
> +
> + if (ACPI_FAILURE(status)) {
> + pr_err("ALIB call failed: %s\n",
> + acpi_format_exception(status));
> + return -EIO;
> + }
> +
> + pr_debug("sent %d ALIB parameter(s)\n", count);
> + return 0;
> +}
> +
> +static int dptc_alib_send_one(int idx, u32 val)
> +{
> + u32 vals[2];
> + u8 ids[2];
> + int count = 0;
> +
> + ids[count] = dptc_params[idx].param_id;
> + vals[count] = val;
> + count++;
> + if (dptc_params[idx].param_id2) {
> + ids[count] = dptc_params[idx].param_id2;
> + vals[count] = val;
> + count++;
> + }
> +
> + return dptc_alib_call(ids, vals, count);
> +}
> +
> +static int dptc_alib_save(struct dptc_priv *dptc)
> +{
> + u32 vals[DPTC_NUM_PARAMS * 2];
> + u8 ids[DPTC_NUM_PARAMS * 2];
> + int i, count = 0;
> +
> + 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 (dptc_params[i].param_id2) {
> + ids[count] = dptc_params[i].param_id2;
> + vals[count] = dptc->staged[i];
> + count++;
> + }
> + }
> +
> + if (!count)
> + return 0;
> +
> + return dptc_alib_call(ids, vals, count);
> +}
> +
> +/* Sysfs callbacks */
> +
> +static ssize_t dptc_current_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, current_value);
I think this can be one line.
> + struct dptc_priv *dptc = ps->priv;
> +
> + guard(mutex)(&dptc->lock);
> +
> + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
> + u32 val = dptc->dev_limits->profiles[dptc->profile].vals[ps->idx];
Is the interface RO? If we can read the value from hardware that would
be preferable, in case separate userspace or PMF changes this behind
the scenes. If we can avoid the kernel getting out of sync with the
hardware that would be better.
> + if (!val)
> + return sysfs_emit(buf, "\n");
> + return sysfs_emit(buf, "%u\n", val);
> + }
> +
> + 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_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, current_value);
> + struct dptc_priv *dptc = ps->priv;
> + u32 val, min, max;
> + int ret;
> +
> + if (count == 1 && buf[0] == '\n') {
> + guard(mutex)(&dptc->lock);
> +
> + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> + return -EPERM;
Note for myself, this is a better error code than what I'm doing IIRC.
> + dptc->has_staged[ps->idx] = false;
> + return count;
> + }
> +
> + ret = kstrtou32(buf, 10, &val);
> + if (ret)
> + return ret;
> +
> + guard(mutex)(&dptc->lock);
> +
> + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> + return -EPERM;
Why do this twice? You can check this first and then use a scoped guard.
> + min = dptc_get_min(dptc, ps->idx);
> + max = dptc_get_max(dptc, ps->idx);
> + if (val < min || (max && val > max))
> + return -EINVAL;
> + dptc->staged[ps->idx] = val;
> + dptc->has_staged[ps->idx] = true;
> + if (dptc->save_mode == SAVE_SINGLE)
> + ret = dptc_alib_send_one(ps->idx, val);
> +
> + return ret ? ret : count;
> +}
> +
> +static ssize_t dptc_default_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, default_value);
> +
Another should be one liner, there are a few more but I'm not going to
tag each one. Additional note, what is ps supposed to stand for?
Verbose variable names improve readability.
> + return sysfs_emit(buf, "%u\n", dptc_get_default(ps->priv, ps->idx));
> +}
> +
> +static ssize_t dptc_min_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, min_value);
> + struct dptc_priv *dptc = ps->priv;
> +
> + guard(mutex)(&dptc->lock);
> +
> + return sysfs_emit(buf, "%u\n", dptc_get_min(dptc, ps->idx));
> +}
> +
> +static ssize_t dptc_max_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, max_value);
> + struct dptc_priv *dptc = ps->priv;
> +
> + guard(mutex)(&dptc->lock);
> +
> + return sysfs_emit(buf, "%u\n", dptc_get_max(dptc, ps->idx));
> +}
> +
> +static ssize_t dptc_scalar_increment_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + return sysfs_emit(buf, "1\n");
> +}
> +
> +static ssize_t dptc_display_name_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, display_name);
> + return sysfs_emit(buf, "%s\n", dptc_params[ps->idx].display_name);
> +}
> +
> +static ssize_t dptc_type_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + return sysfs_emit(buf, "integer\n");
> +}
> +
> +static ssize_t dptc_save_settings_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + struct dptc_priv *dptc =
> + container_of(attr, struct dptc_priv, save_settings_attr);
> +
> + guard(mutex)(&dptc->lock);
> +
> + if (dptc->save_mode == SAVE_SINGLE)
> + return sysfs_emit(buf, "single\n");
> + return sysfs_emit(buf, "bulk\n");
> +}
> +
> +static ssize_t dptc_save_settings_store(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + const char *buf, size_t count)
> +{
> + struct dptc_priv *dptc =
> + container_of(attr, struct dptc_priv, save_settings_attr);
> + int ret = 0;
> +
> + guard(mutex)(&dptc->lock);
> +
> + if (sysfs_streq(buf, "save"))
> + ret = dptc_alib_save(dptc);
> + else if (sysfs_streq(buf, "single"))
> + dptc->save_mode = SAVE_SINGLE;
> + else if (sysfs_streq(buf, "bulk"))
> + dptc->save_mode = SAVE_BULK;
> + else
> + return -EINVAL;
> +
> + return ret ? ret : count;
> +}
> +
> +static ssize_t dptc_expanded_current_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + char *buf)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, current_value);
> + struct dptc_priv *dptc = ps->priv;
> +
> + guard(mutex)(&dptc->lock);
> +
> + return sysfs_emit(buf, "%d\n", dptc->expanded);
> +}
> +
What is the actual purpose of these expanded attrs? IMO we should only
load automatically within a validated range per device. TBS, there is
a valid need for R&D loading, so either a debugfs as Mario suggested
or a module param can be used to load the driver without limits. In
that case, I would just set max to u16 max and min to 0 and offer a
very stern dev_warn in dmesg, then you can avoid duplicated
_show/_store functions.
> +static ssize_t dptc_expanded_current_value_store(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + const char *buf, size_t count)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, current_value);
> + struct dptc_priv *dptc = ps->priv;
> + bool val;
> + int ret;
> +
> + ret = kstrtobool(buf, &val);
> + if (ret)
> + return ret;
> +
> + guard(mutex)(&dptc->lock);
> +
> + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> + return -EPERM;
> + dptc->expanded = val;
> + /* Clear staged values: limits changed, old values may be out of range */
> + memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
> +
> + return count;
> +}
> +
> +static ssize_t dptc_expanded_default_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + char *buf)
> +{
> + return sysfs_emit(buf, "0\n");
> +}
> +
> +static ssize_t dptc_expanded_min_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + char *buf)
> +{
> + return sysfs_emit(buf, "0\n");
> +}
> +
> +static ssize_t dptc_expanded_max_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + char *buf)
> +{
> + return sysfs_emit(buf, "1\n");
> +}
> +
> +static ssize_t dptc_expanded_scalar_increment_show(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + char *buf)
> +{
> + return sysfs_emit(buf, "1\n");
> +}
> +
> +static ssize_t dptc_expanded_display_name_show(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + char *buf)
> +{
> + return sysfs_emit(buf, "Expanded Limits\n");
> +}
> +
> +static ssize_t dptc_expanded_type_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + return sysfs_emit(buf, "integer\n");
> +}
> +
> +/* Sysfs setup */
> +
> +static void dptc_setup_param_sysfs(struct dptc_priv *dptc,
> + struct dptc_attr_sysfs *ps, int idx)
> +{
> + ps->priv = dptc;
> + ps->idx = idx;
> +
> + sysfs_attr_init(&ps->current_value.attr);
> + ps->current_value.attr.name = "current_value";
> + ps->current_value.attr.mode = 0644;
> + ps->current_value.show = dptc_current_value_show;
> + ps->current_value.store = dptc_current_value_store;
> +
> + sysfs_attr_init(&ps->default_value.attr);
> + ps->default_value.attr.name = "default_value";
> + ps->default_value.attr.mode = 0444;
> + ps->default_value.show = dptc_default_value_show;
> +
> + sysfs_attr_init(&ps->min_value.attr);
> + ps->min_value.attr.name = "min_value";
> + ps->min_value.attr.mode = 0444;
> + ps->min_value.show = dptc_min_value_show;
> +
> + sysfs_attr_init(&ps->max_value.attr);
> + ps->max_value.attr.name = "max_value";
> + ps->max_value.attr.mode = 0444;
> + ps->max_value.show = dptc_max_value_show;
> +
> + sysfs_attr_init(&ps->scalar_increment.attr);
> + ps->scalar_increment.attr.name = "scalar_increment";
> + ps->scalar_increment.attr.mode = 0444;
> + ps->scalar_increment.show = dptc_scalar_increment_show;
> +
> + sysfs_attr_init(&ps->display_name.attr);
> + ps->display_name.attr.name = "display_name";
> + ps->display_name.attr.mode = 0444;
> + ps->display_name.show = dptc_display_name_show;
> +
> + sysfs_attr_init(&ps->type.attr);
> + ps->type.attr.name = "type";
> + ps->type.attr.mode = 0444;
> + ps->type.show = dptc_type_show;
> +
> + ps->attrs[0] = &ps->current_value.attr;
> + ps->attrs[1] = &ps->default_value.attr;
> + ps->attrs[2] = &ps->min_value.attr;
> + ps->attrs[3] = &ps->max_value.attr;
> + ps->attrs[4] = &ps->scalar_increment.attr;
> + ps->attrs[5] = &ps->display_name.attr;
> + ps->attrs[6] = &ps->type.attr;
> + ps->attrs[7] = NULL;
> +
> + ps->group.name = dptc_params[idx].name;
> + ps->group.attrs = ps->attrs;
> +}
> +
Some "macro soup" could clean this up a lot.
> +static void dptc_setup_expanded_sysfs(struct dptc_priv *dptc,
> + struct dptc_attr_sysfs *ps)
> +{
> + ps->priv = dptc;
> + sysfs_attr_init(&ps->current_value.attr);
> + ps->current_value.attr.name = "current_value";
> + ps->current_value.attr.mode = 0644;
> + ps->current_value.show = dptc_expanded_current_value_show;
> + ps->current_value.store = dptc_expanded_current_value_store;
> +
> + sysfs_attr_init(&ps->default_value.attr);
> + ps->default_value.attr.name = "default_value";
> + ps->default_value.attr.mode = 0444;
> + ps->default_value.show = dptc_expanded_default_value_show;
> +
> + sysfs_attr_init(&ps->min_value.attr);
> + ps->min_value.attr.name = "min_value";
> + ps->min_value.attr.mode = 0444;
> + ps->min_value.show = dptc_expanded_min_value_show;
> +
> + sysfs_attr_init(&ps->max_value.attr);
> + ps->max_value.attr.name = "max_value";
> + ps->max_value.attr.mode = 0444;
> + ps->max_value.show = dptc_expanded_max_value_show;
> +
> + sysfs_attr_init(&ps->scalar_increment.attr);
> + ps->scalar_increment.attr.name = "scalar_increment";
> + ps->scalar_increment.attr.mode = 0444;
> + ps->scalar_increment.show = dptc_expanded_scalar_increment_show;
> +
> + sysfs_attr_init(&ps->display_name.attr);
> + ps->display_name.attr.name = "display_name";
> + ps->display_name.attr.mode = 0444;
> + ps->display_name.show = dptc_expanded_display_name_show;
> +
> + sysfs_attr_init(&ps->type.attr);
> + ps->type.attr.name = "type";
> + ps->type.attr.mode = 0444;
> + ps->type.show = dptc_expanded_type_show;
> +
> + ps->attrs[0] = &ps->current_value.attr;
> + ps->attrs[1] = &ps->default_value.attr;
> + ps->attrs[2] = &ps->min_value.attr;
> + ps->attrs[3] = &ps->max_value.attr;
> + ps->attrs[4] = &ps->scalar_increment.attr;
> + ps->attrs[5] = &ps->display_name.attr;
> + ps->attrs[6] = &ps->type.attr;
> + ps->attrs[7] = NULL;
> +
> + ps->group.name = "expanded_limits";
> + ps->group.attrs = ps->attrs;
> +}
> +
> +static void dptc_fw_dev_unregister(void *data)
> +{
> + device_unregister(data);
> +}
> +
> +static void dptc_kset_unregister(void *data)
> +{
> + kset_unregister(data);
> +}
> +
> +/* Platform profile */
> +
> +static void dptc_apply_profile(struct dptc_priv *dptc,
> + enum platform_profile_option profile)
> +{
> + const struct dptc_profile *pp;
> + int i;
> +
> + memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
> +
> + if (profile == PLATFORM_PROFILE_CUSTOM)
> + return;
> +
> + pp = &dptc->dev_limits->profiles[profile];
> + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> + if (!pp->vals[i])
> + continue;
> + dptc->staged[i] = pp->vals[i];
> + dptc->has_staged[i] = true;
> + }
> +}
> +
> +static int dptc_pp_probe(void *drvdata, unsigned long *choices)
> +{
> + struct dptc_priv *dptc = drvdata;
> + int i, j;
> +
> + set_bit(PLATFORM_PROFILE_CUSTOM, choices);
> + for (i = 0; i < PLATFORM_PROFILE_LAST; i++) {
> + for (j = 0; j < DPTC_NUM_PARAMS; j++) {
> + if (dptc->dev_limits->profiles[i].vals[j]) {
> + set_bit(i, choices);
> + break;
> + }
> + }
> + }
> + return 0;
> +}
> +
> +static int dptc_pp_get(struct device *dev,
> + enum platform_profile_option *profile)
> +{
> + struct dptc_priv *dptc = dev_get_drvdata(dev);
> +
> + guard(mutex)(&dptc->lock);
> +
> + *profile = dptc->profile;
> + return 0;
> +}
> +
> +static int dptc_pp_set(struct device *dev,
> + enum platform_profile_option profile)
> +{
> + struct dptc_priv *dptc = dev_get_drvdata(dev);
> + int ret = 0;
> +
> + guard(mutex)(&dptc->lock);
> +
> + dptc->profile = profile;
> + dptc_apply_profile(dptc, profile);
> + if (profile != PLATFORM_PROFILE_CUSTOM)
> + ret = dptc_alib_save(dptc);
> +
> + return ret;
> +}
> +
> +static const struct platform_profile_ops dptc_pp_ops = {
> + .probe = dptc_pp_probe,
> + .profile_get = dptc_pp_get,
> + .profile_set = dptc_pp_set,
> +};
> +
> +static int dptc_resume(struct device *dev)
> +{
> + struct dptc_priv *dptc = dev_get_drvdata(dev);
> + int ret;
> +
> + guard(mutex)(&dptc->lock);
> +
> + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
> + dptc_apply_profile(dptc, dptc->profile);
> + ret = dptc_alib_save(dptc);
> + } else if (dptc->save_mode == SAVE_SINGLE) {
> + ret = dptc_alib_save(dptc);
> + } else {
> + ret = 0;
> + }
> +
> + if (ret)
> + dev_warn(dev, "failed to restore settings on resume: %d\n", ret);
> +
> + return 0;
> +}
> +
> +static DEFINE_SIMPLE_DEV_PM_OPS(dptc_pm_ops, NULL, dptc_resume);
> +
> +static int dptc_probe(struct platform_device *pdev)
> +{
> + const struct dmi_system_id *dmi_match = dev_get_platdata(&pdev->dev);
> + struct dptc_priv *dptc;
> + struct device *dev = &pdev->dev;
> + int i, ret;
> +
> + dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL);
> + if (!dptc)
> + return -ENOMEM;
> +
> + platform_set_drvdata(pdev, dptc);
> +
> + ret = devm_mutex_init(dev, &dptc->lock);
> + if (ret)
> + return ret;
> +
> + dptc->dev_limits = dmi_match->driver_data;
> + dev_info(dev, "%s (%s)\n", dmi_match->ident,
> + boot_cpu_data.x86_model_id);
> +
> + dptc->fw_attr_dev = device_create(&firmware_attributes_class,
> + NULL, MKDEV(0, 0), NULL,
> + DRIVER_NAME);
I think DRIVER_NAME can safely fit one line up.
I probably missed a bunch of other nitpicky stuff. I'll look more
closely once this is a more readable series.
Thanks,
Derek
> + if (IS_ERR(dptc->fw_attr_dev))
> + return PTR_ERR(dptc->fw_attr_dev);
> +
> + ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister,
> + dptc->fw_attr_dev);
> + if (ret)
> + return ret;
> +
> + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL,
> + &dptc->fw_attr_dev->kobj);
> + if (!dptc->fw_attr_kset)
> + return -ENOMEM;
> +
> + ret = devm_add_action_or_reset(dev, dptc_kset_unregister,
> + dptc->fw_attr_kset);
> + if (ret)
> + return ret;
> +
> + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> + dptc_setup_param_sysfs(dptc, &dptc->params[i], i);
> + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> + &dptc->params[i].group);
> + if (ret)
> + return ret;
> + }
> +
> + dptc_setup_expanded_sysfs(dptc, &dptc->expanded_attr);
> + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> + &dptc->expanded_attr.group);
> + if (ret)
> + return ret;
> +
> + sysfs_attr_init(&dptc->save_settings_attr.attr);
> + dptc->save_settings_attr.attr.name = "save_settings";
> + dptc->save_settings_attr.attr.mode = 0644;
> + dptc->save_settings_attr.show = dptc_save_settings_show;
> + dptc->save_settings_attr.store = dptc_save_settings_store;
> + ret = sysfs_create_file(&dptc->fw_attr_kset->kobj,
> + &dptc->save_settings_attr.attr);
> + if (ret)
> + return ret;
> +
> + dptc->profile = PLATFORM_PROFILE_CUSTOM;
> + dptc->ppdev = devm_platform_profile_register(dev, "amd-dptc", dptc,
> + &dptc_pp_ops);
> + if (IS_ERR(dptc->ppdev))
> + return PTR_ERR(dptc->ppdev);
> +
> + return 0;
> +}
> +
> +static struct platform_driver dptc_driver = {
> + .driver = {
> + .name = DRIVER_NAME,
> + .pm = pm_sleep_ptr(&dptc_pm_ops),
> + },
> + .probe = dptc_probe,
> +};
> +
> +static int __init dptc_init(void)
> +{
> + const struct dmi_system_id *match;
> + bool soc_found = false;
> + int i, ret;
> +
> + if (!acpi_has_method(NULL, ALIB_PATH)) {
> + pr_debug("ALIB method not present\n");
> + return -ENODEV;
> + }
> +
> + match = dmi_first_match(dptc_dmi_table);
> + if (!match)
> + return -ENODEV;
> +
> + for (i = 0; dptc_soc_table[i]; i++) {
> + if (strstr(boot_cpu_data.x86_model_id,
> + dptc_soc_table[i])) {
> + soc_found = true;
> + break;
> + }
> + }
> + if (!soc_found) {
> + pr_warn("unrecognized SoC '%s'\n",
> + boot_cpu_data.x86_model_id);
> + return -ENODEV;
> + }
> +
> + dptc_pdev = platform_device_register_data(NULL, DRIVER_NAME, -1,
> + match, sizeof(*match));
> + if (IS_ERR(dptc_pdev))
> + return PTR_ERR(dptc_pdev);
> +
> + ret = platform_driver_register(&dptc_driver);
> + if (ret) {
> + platform_device_unregister(dptc_pdev);
> + return ret;
> + }
> +
> + return 0;
> +}
> +
> +static void __exit dptc_exit(void)
> +{
> + platform_driver_unregister(&dptc_driver);
> + platform_device_unregister(dptc_pdev);
> +}
> +
> +module_init(dptc_init);
> +module_exit(dptc_exit);
> +
> +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>");
> +MODULE_DESCRIPTION("AMD DPTCi ACPI Driver");
> +MODULE_LICENSE("GPL");
> --
> 2.52.0
>
>
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [RFC v2 2/2] platform/x86/amd: Add AMD DPTCi driver
2026-03-06 21:14 ` Derek John Clark
@ 2026-03-07 11:17 ` Antheas Kapenekakis
2026-03-07 11:25 ` Antheas Kapenekakis
0 siblings, 1 reply; 8+ messages in thread
From: Antheas Kapenekakis @ 2026-03-07 11:17 UTC (permalink / raw)
To: Derek John Clark
Cc: Mario.Limonciello, W_Armin, sashal, Shyam-Sundar.S-k,
denis.benato, linux-kernel, platform-driver-x86
On Fri, 6 Mar 2026 at 22:14, Derek John Clark <derekjohn.clark@gmail.com> wrote:
>
> On Thu, Mar 5, 2026 at 10:19 AM Antheas Kapenekakis <lkml@antheas.dev> 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.
>
> Hi Antheas,
>
> Thanks for the CC. I've long opined the lack of having a generic
> interface for these features in the kernel. With that being said, I
> think very careful consideration for the interface is a necessity. We
> never want to have the chance for an unsuspecting or inexperienced
> user to have hardware malfunctions by misusing the interface. As a
> general rule I think it would be good for each individual device added
> to have a tested-by tag before it is included. That message would
> preferably include some sort of metadata identifying the true hardware
> limits (tech docs if available, but marketing material or OEM software
> screenshots would be sufficient, we know how some of these companies
> are with information)
>
> > The driver matches the ABI of asus-armoury, by exposing a platform
> > profile with reasonable tunings for devices depending on their max
> > thermal envelope, and {ppt_pl1_spl,ppt_pl2_sppt,ppt_pl3_fppt,cpu_temp}
> > tunables which only become writable when the profile is custom,
> > otherwise they are read-only and reflect the current profile.
> >
> > The default profile is custom so that we do not write to the device
> > until userspace explicitly selects a profile, remaining at firmware
> > defaults.
>
> I see what you're trying to do here, but I'm a bit conflicted on the
> implementation. Hardware defaults are there for a reason after all...
> Part of the original discussion with custom was that setting it would
> taint the kernel to inform remote troubleshooters that there was a
> non-standard change. TBS I don't see that in the actual code for
> acpi/platform_profile.c so maybe the intention was that the drivers
> themselves set it? If that's the case then I'll need to submit a patch
> to add that to lenovo-wmi-gamezone so I'm compliant. I'd like Mario's
> input on that first as IMO it makes more sense to do it as a blanket
> in the platform class, or we don't bother as we expect drivers to
> respect the TDP limits set by ODM/OEM. If we do end up doing that
> though, just booting a device with this driver would automatically
> taint the kernel, which is certainly not the desired behavior. I'm not
> sure how to best square that circle. Do we know the BIOS defaults for
> all the devices we're adding here? I'd expect that would correspond
> with balanced most of the time, so perhaps making sure those match is
> a good compromise.
>
>
> > Assisted-by: Claude:claude-opus-4-6
> > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> > ---
> > MAINTAINERS | 6 +
> > drivers/platform/x86/amd/Kconfig | 16 +
> > drivers/platform/x86/amd/Makefile | 2 +
> > drivers/platform/x86/amd/dptc.c | 1255 +++++++++++++++++++++++++++++
> > 4 files changed, 1279 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..ee79a2c084a8 100644
> > --- a/drivers/platform/x86/amd/Kconfig
> > +++ b/drivers/platform/x86/amd/Kconfig
> > @@ -44,3 +44,19 @@ 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 ACPI_PLATFORM_PROFILE
> > + select FIRMWARE_ATTRIBUTES_CLASS
> > + help
> > + Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> > + Thermal Configuration Interface (DPTCi). Exposes TDP and thermal
> > + parameters for AMD APU-based handheld devices via the
> > + firmware-attributes sysfs ABI, allowing userspace tools to stage
> > + and atomically commit power limit settings. Requires a DMI match
> > + for the device and a recognized AMD SoC.
> > +
> > + If built as a module, the module will be called amd_dptc.
> > +
> > diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile
> > index f6ff0c837f34..862a609bfe38 100644
> > --- a/drivers/platform/x86/amd/Makefile
> > +++ b/drivers/platform/x86/amd/Makefile
> > @@ -12,3 +12,5 @@ obj-$(CONFIG_AMD_PMF) += pmf/
> > obj-$(CONFIG_AMD_WBRF) += wbrf.o
> > obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o
> > obj-$(CONFIG_AMD_HFI) += hfi/
> > +obj-$(CONFIG_AMD_DPTC) += amd_dptc.o
> > +amd_dptc-y := dptc.o
> > diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
> > new file mode 100644
> > index 000000000000..68d9d3b85cb6
> > --- /dev/null
> > +++ b/drivers/platform/x86/amd/dptc.c
> > @@ -0,0 +1,1255 @@
> > +// SPDX-License-Identifier: GPL-2.0-only
> > +/*
> > + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver
> > + *
> > + * Exposes AMD APU power and thermal parameters via the firmware-attributes
> > + * sysfs ABI. Parameters are staged and atomically committed through the
> > + * AGESA ALIB Function 0x0C (Dynamic Power and Thermal Configuration
> > + * interface).
> > + *
> > + * Reference: AMD AGESA Publication #44065, Appendix E.5
> > + * https://docs.amd.com/v/u/en-US/44065_Arch2008
> > + *
> > + * Copyright (C) 2026 Antheas Kapenekakis <lkml@antheas.dev>
> > + */
> > +
> > +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> > +
> > +#include <linux/acpi.h>
> > +#include <linux/cleanup.h>
> > +#include <linux/dmi.h>
> > +#include <linux/init.h>
> > +#include <linux/kobject.h>
> > +#include <linux/module.h>
> > +#include <linux/mutex.h>
> > +#include <linux/platform_device.h>
> > +#include <linux/platform_profile.h>
> > +#include <linux/processor.h>
> > +#include <linux/sysfs.h>
> > +#include <linux/unaligned.h>
> > +
> > +#include "../firmware_attributes_class.h"
> > +
> > +#define DRIVER_NAME "amd_dptc"
> > +
> > +#define ALIB_FUNC_DPTC 0x0C
> > +#define ALIB_PATH "\\_SB.ALIB"
> > +
> > +/* ALIB parameter IDs (AGESA spec Appendix E.5, Table E-52) */
> > +#define ALIB_ID_TEMP_TARGET 0x03
> > +#define ALIB_ID_STAPM_LIMIT 0x05
> > +#define ALIB_ID_FAST_LIMIT 0x06
> > +#define ALIB_ID_SLOW_LIMIT 0x07
> > +#define ALIB_ID_SKIN_LIMIT 0x2E
> > +
> > +enum dptc_param_idx {
> > + DPTC_PPT_PL1_SPL, /* STAPM + skin limit (set together) */
> > + DPTC_PPT_PL2_SPPT, /* slow PPT limit */
> > + DPTC_PPT_PL3_FPPT, /* fast PPT limit */
> > + DPTC_CPU_TEMP, /* thermal control target */
> > + DPTC_NUM_PARAMS,
> > +};
> > +
> > +struct dptc_param_limits {
> > + u32 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_profile {
> > + u32 vals[DPTC_NUM_PARAMS]; /* 0 = don't set / unstage this param */
> > +};
> > +
> > +struct dptc_device_limits {
> > + struct dptc_param_limits params[DPTC_NUM_PARAMS];
> > + struct dptc_profile profiles[PLATFORM_PROFILE_LAST];
> > +};
> > +
> > +struct dptc_param_desc {
> > + const char *name;
> > + const char *display_name;
> > + u8 param_id;
> > + u8 param_id2; /* secondary ALIB ID, 0 if none */
> > +};
> > +
> > +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = {
> > + [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (mW)",
> > + ALIB_ID_STAPM_LIMIT, ALIB_ID_SKIN_LIMIT },
> > + [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (mW)",
> > + ALIB_ID_SLOW_LIMIT },
> > + [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (mW)",
> > + ALIB_ID_FAST_LIMIT },
> > + [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)",
> > + ALIB_ID_TEMP_TARGET },
> > +};
>
> You already mentioned this elsewhere, but I agree that using W over mW
> would make sense to keep the interfaces consistent.
>
> > +/* 18W class: AYANEO AIR Plus (Ryzen 5 5560U) */
> > +static const struct dptc_device_limits limits_18w = { .params = {
> > + [DPTC_PPT_PL1_SPL] = { 0, 5000, 15000, 18000, 22000 },
> > + [DPTC_PPT_PL2_SPPT] = { 0, 5000, 15000, 18000, 22000 },
> > + [DPTC_PPT_PL3_FPPT] = { 0, 5000, 15000, 20000, 25000 },
> > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > +}, .profiles = {
> > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5000, 5000, 8000, 0 } },
> > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 12000, 14000, 15000, 0 } },
> > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 18000, 18000, 20000, 0 } },
> > +}};
> > +
>
> I would personally prefer these be set directly in the DMI match per
> device. Its more code in the end, but its more cautious, as each
> manufacturer has a bespoke cooling solution. Assuming they're all
> equivalent is not best practice IMO. I have quite a bit of historical
> data on this so I can help fill in the blanks where it is missing.
I would rather keep it like this and expand for specific devices when
there is a reason. This makes the match section 1/3 the size. The
chips are the same, they behave the same performance-wise. And for
lower TDPs (e.g., lower_power, balanced) where the device is
guaranteed to handle it the behavior is exact.
> > +/* 25W class: Ryzen 5000 handhelds (AYANEO NEXT, KUN) */
> > +static const struct dptc_device_limits limits_25w = { .params = {
> > + [DPTC_PPT_PL1_SPL] = { 0, 4000, 15000, 25000, 32000 },
> > + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 20000, 27000, 35000 },
> > + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 25000, 30000, 37000 },
> > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > +}, .profiles = {
> > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8000, 8000, 12000, 0 } },
> > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15000, 17000, 20000, 0 } },
> > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25000, 27000, 30000, 0 } },
> > +}};
> >
> > +/* 28W class: GPD Win series, AYANEO 2, OrangePi NEO-01 */
> > +static const struct dptc_device_limits limits_28w = { .params = {
> > + [DPTC_PPT_PL1_SPL] = { 0, 4000, 15000, 28000, 32000 },
> > + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 20000, 30000, 35000 },
> > + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 25000, 32000, 37000 },
> > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > +}, .profiles = {
> > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8000, 8000, 12000, 0 } },
> > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15000, 17000, 22000, 0 } },
> > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25000, 28000, 32000, 0 } },
> > + [PLATFORM_PROFILE_MAX_POWER] = { .vals = { 28000, 30000, 32000, 0 } },
> > +}};
> > +
> > +/* 30W class: OneXPlayer, AYANEO AIR/FLIP/GEEK/SLIDE/3, AOKZOE */
> > +static const struct dptc_device_limits limits_30w = { .params = {
> > + [DPTC_PPT_PL1_SPL] = { 0, 4000, 15000, 30000, 40000 },
> > + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 20000, 32000, 43000 },
> > + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 25000, 41000, 50000 },
> > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > +}, .profiles = {
> > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8000, 8000, 12000, 0 } },
> > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15000, 17000, 25000, 0 } },
> > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25000, 28000, 41000, 0 } },
> > + [PLATFORM_PROFILE_MAX_POWER] = { .vals = { 30000, 32000, 41000, 0 } },
> > +}};
> > +
> > +/* AI MAX Handheld class: GPD Win 5 */
> > +static const struct dptc_device_limits limits_maxhh = { .params = {
> This struct name seems arbitrary compared to the others. If this is
> the eventual pattern this should just be called _80w
I am still not sure what the correct range is for this handheld, which
is why it is called MAXHandHeld. The TDP range is too broad to put an
80 on it. I am talking with users, it seems the sweet spot is 45W
> > + [DPTC_PPT_PL1_SPL] = { 0, 4000, 25000, 80000, 100000 },
> > + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 27000, 82000, 100000 },
> > + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 40000, 85000, 100000 },
> > + [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 },
> > +}, .profiles = {
> > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 15000, 15000, 25000, 0 } },
> > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 25000, 27000, 40000, 0 } },
> > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 60000, 63000, 85000, 0 } },
> > + [PLATFORM_PROFILE_MAX_POWER] = { .vals = { 80000, 82000, 85000, 0 } },
> > +}};
>
> One other thing to consider is AC vs DC power draw. Thermal load is
> important, but we also want to avoid over-stressing the battery on
> these devices. I would restrict MAX_POWER to when AC is plugged ad
> return an error if on DC, and ensure PERFORMANCE is at or just below
> peak DC power draw. I didn't do this in gamezone on Lenovo because
> those limits are BIOS enforced and MAX_POWER will just match
> PERFORMANCE on DC.
This is true. However I would like to defer AC/DC wrangling in this
driver. The values are already conservative for the profiles except
max power. So I think it would be best to drop MAX_POWER for now.
> > +/* Substring matches against boot_cpu_data.x86_model_id; order matters. */
> > +static const char * const dptc_soc_table[] = {
> > + /* AI MAX */
> > + "AMD RYZEN AI MAX+ 395",
> > + "AMD RYZEN AI MAX+ 385",
> > + "AMD RYZEN AI MAX 380",
> > + /* Ryzen AI */
> > + "AMD Ryzen AI 9 HX 370",
> > + "AMD Ryzen AI HX 360",
> > + /* Z1 - Extreme before plain Z1 */
> > + "AMD Ryzen Z1 Extreme",
> > + "AMD Ryzen Z1",
> > + /* Ryzen 8000 */
> > + "AMD Ryzen 7 8840U",
> > + /* Ryzen 7040 */
> > + "AMD Ryzen 7 7840U",
> > + /* Ryzen 6000 */
> > + "AMD Ryzen 7 6800U",
> > + "AMD Ryzen 7 6600U",
> > + /* Ryzen 5000 */
> > + "AMD Ryzen 7 5800U",
> > + "AMD Ryzen 7 5700U",
> > + "AMD Ryzen 5 5560U",
> > + NULL,
> > +};
>
> I have some data on 4800/4800U and AMD Athelon Silver APU devices as
> well. When I return from my conference I can post some DSDT dumps to
> validate the interface is present on them as well.
I think I considered adding support for the Athelon Silver and that it
did have some support. But in the end the chip is too old to make TDP
control viable for it.
> > +static const struct dmi_system_id dptc_dmi_table[] = {
> > + /* GPD */
> > + {
> > + .ident = "GPD Win Mini",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-01"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Win Mini 2024",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Win Mini 2024",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02-L"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Win 4",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-04"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Win 5",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"),
> > + },
> > + .driver_data = (void *)&limits_maxhh,
> > + },
> > + {
> > + .ident = "GPD Win Max 2",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-04"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Win Max 2 2024",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-05"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Duo",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Duo",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01-L"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Pocket 4",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Pocket 4",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04-L"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + /* OrangePi */
> > + {
> > + .ident = "OrangePi NEO-01",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + /* AYN */
> > + {
> > + .ident = "AYN Loki Max",
> > + .matches = {
> > + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ayn"),
> > + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Loki Max"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + /* Tectoy (Zeenix Pro = Loki Max) */
> > + {
> > + .ident = "Zeenix Pro",
> > + .matches = {
> > + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Tectoy"),
> > + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Zeenix Pro"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + /* AOKZOE */
> > + {
> > + .ident = "AOKZOE A1 AR07",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 AR07"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "AOKZOE A1 Pro",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 Pro"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "AOKZOE A1X",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1X"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "AOKZOE A2 Pro",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A2 Pro"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + /* OneXPlayer (Intel variants filtered by SoC table) */
> > + {
> > + .ident = "ONEXPLAYER F1Pro",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1Pro"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "ONEXPLAYER F1 EVA-02",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1 EVA-02"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "ONEXPLAYER 2",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > + DMI_MATCH(DMI_BOARD_NAME, "ONEXPLAYER 2"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "ONEXPLAYER X1 A",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1 A"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "ONEXPLAYER X1z",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1z"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "ONEXPLAYER X1Pro",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Pro"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "ONEXPLAYER G1 A",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 A"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + /* AYANEO - 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,
> > + },
> > + /* AYANEO - 25W */
> > + {
> > + .ident = "AYANEO NEXT Advance",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Advance"),
> > + },
> > + .driver_data = (void *)&limits_25w,
> > + },
>
> AYANEO went through a strange DMI phase, many of the first run of NEXT
> devices are prefaced with AYANEO in the board name as well
If you can pull up the exact name that would be good. We can dupe it.
> > + {
> > + .ident = "AYANEO NEXT Lite",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Lite"),
> > + },
> > + .driver_data = (void *)&limits_25w,
> > + },
> > + {
> > + .ident = "AYANEO NEXT Pro",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Pro"),
> > + },
> > + .driver_data = (void *)&limits_25w,
> > + },
> > + {
> > + .ident = "AYANEO NEXT",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT"),
> > + },
> > + .driver_data = (void *)&limits_25w,
> > + },
> > + {
> > + .ident = "AYANEO KUN",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"),
> > + },
> > + .driver_data = (void *)&limits_25w,
> > + },
> > + {
> > + .ident = "AYANEO KUN",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO KUN"),
> > + },
> > + .driver_data = (void *)&limits_25w,
> > + },
> > + /* AYANEO - 28W */
> > + {
> > + .ident = "AYANEO 2",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 2"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "SuiPlay0X1",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "Mysten Labs, Inc."),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "SuiPlay0X1"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + /* AYANEO - 30W */
> > + {
> > + /* 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,
> > + },
>
> The original air is max 15w, air pro is 18w. I didn't scrutinize the
> rest as closely, but these will need to be validated per device.
> (Hence my reccomendation above)
These Air devices from ~2023 and before will need a closer look.
Everything from 2024 onwards should be correct.
> > + {
> > + /* 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);
> > +
> > +struct dptc_priv;
> > +
> > +struct dptc_attr_sysfs {
> > + struct dptc_priv *priv;
> > + struct kobj_attribute current_value;
> > + struct kobj_attribute default_value;
> > + struct kobj_attribute min_value;
> > + struct kobj_attribute max_value;
> > + struct kobj_attribute scalar_increment;
> > + struct kobj_attribute display_name;
> > + struct kobj_attribute type;
> > + struct attribute *attrs[8];
> > + struct attribute_group group;
> > + int idx;
> > +};
> > +
> > +struct dptc_priv {
> > + struct device *fw_attr_dev;
> > + struct kset *fw_attr_kset;
> > +
> > + const struct dptc_device_limits *dev_limits;
> > +
> > + bool expanded;
> > +
> > + enum platform_profile_option profile;
> > + struct device *ppdev;
> > +
> > + enum dptc_save_mode { SAVE_SINGLE, SAVE_BULK } save_mode;
> > +
> > + u32 staged[DPTC_NUM_PARAMS];
> > + bool has_staged[DPTC_NUM_PARAMS];
> > +
> > + /* Protects mutable driver state */
> > + struct mutex lock;
> > +
> > + struct dptc_attr_sysfs params[DPTC_NUM_PARAMS];
> > + struct dptc_attr_sysfs expanded_attr;
> > + struct kobj_attribute save_settings_attr;
> > +};
> > +
> > +static struct platform_device *dptc_pdev;
> > +
> > +static u32 dptc_get_min(struct dptc_priv *dptc, int idx)
> > +{
> > + return dptc->expanded ? dptc->dev_limits->params[idx].min
> > + : dptc->dev_limits->params[idx].smin;
> > +}
> > +
> > +static u32 dptc_get_max(struct dptc_priv *dptc, int idx)
> > +{
> > + return dptc->expanded ? dptc->dev_limits->params[idx].max
> > + : dptc->dev_limits->params[idx].smax;
> > +}
> > +
> > +static u32 dptc_get_default(struct dptc_priv *dptc, int idx)
> > +{
> > + return dptc->dev_limits->params[idx].def;
> > +}
> > +
> > +static int dptc_alib_call(const u8 *ids, const u32 *vals, int count)
> > +{
> > + union acpi_object in_params[2];
> > + struct acpi_object_list input;
> > + acpi_status status;
> > + u32 buf_size;
> > + int i, off;
> > + u8 *buf;
> > +
> > + if (count == 0)
> > + return -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;
> > +
> > + put_unaligned_le16(buf_size, buf);
> > +
> > + for (i = 0; i < count; i++) {
> > + off = 2 + i * 5;
> > + buf[off] = ids[i];
> > + put_unaligned_le32(vals[i], buf + off + 1);
> > + }
> > +
> > + in_params[0].type = ACPI_TYPE_INTEGER;
> > + in_params[0].integer.value = ALIB_FUNC_DPTC;
> > + in_params[1].type = ACPI_TYPE_BUFFER;
> > + in_params[1].buffer.length = buf_size;
> > + in_params[1].buffer.pointer = buf;
> > +
> > + input.count = 2;
> > + input.pointer = in_params;
> > +
> > + status = acpi_evaluate_object(NULL, ALIB_PATH, &input, NULL);
> > + kfree(buf);
> > +
> > + if (ACPI_FAILURE(status)) {
> > + pr_err("ALIB call failed: %s\n",
> > + acpi_format_exception(status));
> > + return -EIO;
> > + }
> > +
> > + pr_debug("sent %d ALIB parameter(s)\n", count);
> > + return 0;
> > +}
> > +
> > +static int dptc_alib_send_one(int idx, u32 val)
> > +{
> > + u32 vals[2];
> > + u8 ids[2];
> > + int count = 0;
> > +
> > + ids[count] = dptc_params[idx].param_id;
> > + vals[count] = val;
> > + count++;
> > + if (dptc_params[idx].param_id2) {
> > + ids[count] = dptc_params[idx].param_id2;
> > + vals[count] = val;
> > + count++;
> > + }
> > +
> > + return dptc_alib_call(ids, vals, count);
> > +}
> > +
> > +static int dptc_alib_save(struct dptc_priv *dptc)
> > +{
> > + u32 vals[DPTC_NUM_PARAMS * 2];
> > + u8 ids[DPTC_NUM_PARAMS * 2];
> > + int i, count = 0;
> > +
> > + 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 (dptc_params[i].param_id2) {
> > + ids[count] = dptc_params[i].param_id2;
> > + vals[count] = dptc->staged[i];
> > + count++;
> > + }
> > + }
> > +
> > + if (!count)
> > + return 0;
> > +
> > + return dptc_alib_call(ids, vals, count);
> > +}
> > +
> > +/* Sysfs callbacks */
> > +
> > +static ssize_t dptc_current_value_show(struct kobject *kobj,
> > + struct kobj_attribute *attr, char *buf)
> > +{
> > + struct dptc_attr_sysfs *ps =
> > + container_of(attr, struct dptc_attr_sysfs, current_value);
>
> I think this can be one line.
It overflows.
>
> > + struct dptc_priv *dptc = ps->priv;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
> > + u32 val = dptc->dev_limits->profiles[dptc->profile].vals[ps->idx];
>
> Is the interface RO? If we can read the value from hardware that would
> be preferable, in case separate userspace or PMF changes this behind
> the scenes. If we can avoid the kernel getting out of sync with the
> hardware that would be better.
The dptc interface is strictly read only. The underlying SMU mailbox
provides a debug table that contains these values, but that is a debug
table and should not be relied upon by userspace. This is what
ryzenadj -i prints
The function that provides the memory address of this table is not
implemented by ALIB afaik.
> > + if (!val)
> > + return sysfs_emit(buf, "\n");
> > + return sysfs_emit(buf, "%u\n", val);
> > + }
> > +
> > + 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_attr_sysfs *ps =
> > + container_of(attr, struct dptc_attr_sysfs, current_value);
> > + struct dptc_priv *dptc = ps->priv;
> > + u32 val, min, max;
> > + int ret;
> > +
> > + if (count == 1 && buf[0] == '\n') {
> > + guard(mutex)(&dptc->lock);
> > +
> > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> > + return -EPERM;
>
> Note for myself, this is a better error code than what I'm doing IIRC.
>
> > + dptc->has_staged[ps->idx] = false;
> > + return count;
> > + }
> > +
> > + ret = kstrtou32(buf, 10, &val);
> > + if (ret)
> > + return ret;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> > + return -EPERM;
>
> Why do this twice? You can check this first and then use a scoped guard.
Noted. This can be moved up.
> > + min = dptc_get_min(dptc, ps->idx);
> > + max = dptc_get_max(dptc, ps->idx);
> > + if (val < min || (max && val > max))
> > + return -EINVAL;
> > + dptc->staged[ps->idx] = val;
> > + dptc->has_staged[ps->idx] = true;
> > + if (dptc->save_mode == SAVE_SINGLE)
> > + ret = dptc_alib_send_one(ps->idx, val);
> > +
> > + return ret ? ret : count;
> > +}
> > +
> > +static ssize_t dptc_default_value_show(struct kobject *kobj,
> > + struct kobj_attribute *attr, char *buf)
> > +{
> > + struct dptc_attr_sysfs *ps =
> > + container_of(attr, struct dptc_attr_sysfs, default_value);
> > +
>
> Another should be one liner, there are a few more but I'm not going to
> tag each one. Additional note, what is ps supposed to stand for?
> Verbose variable names improve readability.
ps -> Dptc_attr_Sysfs. No, in this case the driver would read worse.
It is a holder val. If it was verbose it would break reflowing in most
of the driver.
> > + return sysfs_emit(buf, "%u\n", dptc_get_default(ps->priv, ps->idx));
> > +}
> > +
> > +static ssize_t dptc_min_value_show(struct kobject *kobj,
> > + struct kobj_attribute *attr, char *buf)
> > +{
> > + struct dptc_attr_sysfs *ps =
> > + container_of(attr, struct dptc_attr_sysfs, min_value);
> > + struct dptc_priv *dptc = ps->priv;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + return sysfs_emit(buf, "%u\n", dptc_get_min(dptc, ps->idx));
> > +}
> > +
> > +static ssize_t dptc_max_value_show(struct kobject *kobj,
> > + struct kobj_attribute *attr, char *buf)
> > +{
> > + struct dptc_attr_sysfs *ps =
> > + container_of(attr, struct dptc_attr_sysfs, max_value);
> > + struct dptc_priv *dptc = ps->priv;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + return sysfs_emit(buf, "%u\n", dptc_get_max(dptc, ps->idx));
> > +}
> > +
> > +static ssize_t dptc_scalar_increment_show(struct kobject *kobj,
> > + struct kobj_attribute *attr, char *buf)
> > +{
> > + return sysfs_emit(buf, "1\n");
> > +}
> > +
> > +static ssize_t dptc_display_name_show(struct kobject *kobj,
> > + struct kobj_attribute *attr, char *buf)
> > +{
> > + struct dptc_attr_sysfs *ps =
> > + container_of(attr, struct dptc_attr_sysfs, display_name);
> > + return sysfs_emit(buf, "%s\n", dptc_params[ps->idx].display_name);
> > +}
> > +
> > +static ssize_t dptc_type_show(struct kobject *kobj,
> > + struct kobj_attribute *attr, char *buf)
> > +{
> > + return sysfs_emit(buf, "integer\n");
> > +}
> > +
> > +static ssize_t dptc_save_settings_show(struct kobject *kobj,
> > + struct kobj_attribute *attr, char *buf)
> > +{
> > + struct dptc_priv *dptc =
> > + container_of(attr, struct dptc_priv, save_settings_attr);
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + if (dptc->save_mode == SAVE_SINGLE)
> > + return sysfs_emit(buf, "single\n");
> > + return sysfs_emit(buf, "bulk\n");
> > +}
> > +
> > +static ssize_t dptc_save_settings_store(struct kobject *kobj,
> > + struct kobj_attribute *attr,
> > + const char *buf, size_t count)
> > +{
> > + struct dptc_priv *dptc =
> > + container_of(attr, struct dptc_priv, save_settings_attr);
> > + int ret = 0;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + if (sysfs_streq(buf, "save"))
> > + ret = dptc_alib_save(dptc);
> > + else if (sysfs_streq(buf, "single"))
> > + dptc->save_mode = SAVE_SINGLE;
> > + else if (sysfs_streq(buf, "bulk"))
> > + dptc->save_mode = SAVE_BULK;
> > + else
> > + return -EINVAL;
> > +
> > + return ret ? ret : count;
> > +}
> > +
> > +static ssize_t dptc_expanded_current_value_show(struct kobject *kobj,
> > + struct kobj_attribute *attr,
> > + char *buf)
> > +{
> > + struct dptc_attr_sysfs *ps =
> > + container_of(attr, struct dptc_attr_sysfs, current_value);
> > + struct dptc_priv *dptc = ps->priv;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + return sysfs_emit(buf, "%d\n", dptc->expanded);
> > +}
> > +
>
> What is the actual purpose of these expanded attrs? IMO we should only
> load automatically within a validated range per device. TBS, there is
> a valid need for R&D loading, so either a debugfs as Mario suggested
> or a module param can be used to load the driver without limits. In
> that case, I would just set max to u16 max and min to 0 and offer a
> very stern dev_warn in dmesg, then you can avoid duplicated
> _show/_store functions.
Some devices like the GPD Win ones can do 30W. However their casing
can become too hot which is why they're limited to 28. This toggle is
an escape hatch that gives them some predefined wiggle room so they do
not resort to more extreme measures.
> > +static ssize_t dptc_expanded_current_value_store(struct kobject *kobj,
> > + struct kobj_attribute *attr,
> > + const char *buf, size_t count)
> > +{
> > + struct dptc_attr_sysfs *ps =
> > + container_of(attr, struct dptc_attr_sysfs, current_value);
> > + struct dptc_priv *dptc = ps->priv;
> > + bool val;
> > + int ret;
> > +
> > + ret = kstrtobool(buf, &val);
> > + if (ret)
> > + return ret;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> > + return -EPERM;
> > + dptc->expanded = val;
> > + /* Clear staged values: limits changed, old values may be out of range */
> > + memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
> > +
> > + return count;
> > +}
> > +
> > +static ssize_t dptc_expanded_default_value_show(struct kobject *kobj,
> > + struct kobj_attribute *attr,
> > + char *buf)
> > +{
> > + return sysfs_emit(buf, "0\n");
> > +}
> > +
> > +static ssize_t dptc_expanded_min_value_show(struct kobject *kobj,
> > + struct kobj_attribute *attr,
> > + char *buf)
> > +{
> > + return sysfs_emit(buf, "0\n");
> > +}
> > +
> > +static ssize_t dptc_expanded_max_value_show(struct kobject *kobj,
> > + struct kobj_attribute *attr,
> > + char *buf)
> > +{
> > + return sysfs_emit(buf, "1\n");
> > +}
> > +
> > +static ssize_t dptc_expanded_scalar_increment_show(struct kobject *kobj,
> > + struct kobj_attribute *attr,
> > + char *buf)
> > +{
> > + return sysfs_emit(buf, "1\n");
> > +}
> > +
> > +static ssize_t dptc_expanded_display_name_show(struct kobject *kobj,
> > + struct kobj_attribute *attr,
> > + char *buf)
> > +{
> > + return sysfs_emit(buf, "Expanded Limits\n");
> > +}
> > +
> > +static ssize_t dptc_expanded_type_show(struct kobject *kobj,
> > + struct kobj_attribute *attr, char *buf)
> > +{
> > + return sysfs_emit(buf, "integer\n");
> > +}
> > +
> > +/* Sysfs setup */
> > +
> > +static void dptc_setup_param_sysfs(struct dptc_priv *dptc,
> > + struct dptc_attr_sysfs *ps, int idx)
> > +{
> > + ps->priv = dptc;
> > + ps->idx = idx;
> > +
> > + sysfs_attr_init(&ps->current_value.attr);
> > + ps->current_value.attr.name = "current_value";
> > + ps->current_value.attr.mode = 0644;
> > + ps->current_value.show = dptc_current_value_show;
> > + ps->current_value.store = dptc_current_value_store;
> > +
> > + sysfs_attr_init(&ps->default_value.attr);
> > + ps->default_value.attr.name = "default_value";
> > + ps->default_value.attr.mode = 0444;
> > + ps->default_value.show = dptc_default_value_show;
> > +
> > + sysfs_attr_init(&ps->min_value.attr);
> > + ps->min_value.attr.name = "min_value";
> > + ps->min_value.attr.mode = 0444;
> > + ps->min_value.show = dptc_min_value_show;
> > +
> > + sysfs_attr_init(&ps->max_value.attr);
> > + ps->max_value.attr.name = "max_value";
> > + ps->max_value.attr.mode = 0444;
> > + ps->max_value.show = dptc_max_value_show;
> > +
> > + sysfs_attr_init(&ps->scalar_increment.attr);
> > + ps->scalar_increment.attr.name = "scalar_increment";
> > + ps->scalar_increment.attr.mode = 0444;
> > + ps->scalar_increment.show = dptc_scalar_increment_show;
> > +
> > + sysfs_attr_init(&ps->display_name.attr);
> > + ps->display_name.attr.name = "display_name";
> > + ps->display_name.attr.mode = 0444;
> > + ps->display_name.show = dptc_display_name_show;
> > +
> > + sysfs_attr_init(&ps->type.attr);
> > + ps->type.attr.name = "type";
> > + ps->type.attr.mode = 0444;
> > + ps->type.show = dptc_type_show;
> > +
> > + ps->attrs[0] = &ps->current_value.attr;
> > + ps->attrs[1] = &ps->default_value.attr;
> > + ps->attrs[2] = &ps->min_value.attr;
> > + ps->attrs[3] = &ps->max_value.attr;
> > + ps->attrs[4] = &ps->scalar_increment.attr;
> > + ps->attrs[5] = &ps->display_name.attr;
> > + ps->attrs[6] = &ps->type.attr;
> > + ps->attrs[7] = NULL;
> > +
> > + ps->group.name = dptc_params[idx].name;
> > + ps->group.attrs = ps->attrs;
> > +}
> > +
>
> Some "macro soup" could clean this up a lot.
Agree to disagree, this driver is very readable and in the end that is
what matters. The value types are different so it is not clear if the
same macro would work for both anyway.
> > +static void dptc_setup_expanded_sysfs(struct dptc_priv *dptc,
> > + struct dptc_attr_sysfs *ps)
> > +{
> > + ps->priv = dptc;
> > + sysfs_attr_init(&ps->current_value.attr);
> > + ps->current_value.attr.name = "current_value";
> > + ps->current_value.attr.mode = 0644;
> > + ps->current_value.show = dptc_expanded_current_value_show;
> > + ps->current_value.store = dptc_expanded_current_value_store;
> > +
> > + sysfs_attr_init(&ps->default_value.attr);
> > + ps->default_value.attr.name = "default_value";
> > + ps->default_value.attr.mode = 0444;
> > + ps->default_value.show = dptc_expanded_default_value_show;
> > +
> > + sysfs_attr_init(&ps->min_value.attr);
> > + ps->min_value.attr.name = "min_value";
> > + ps->min_value.attr.mode = 0444;
> > + ps->min_value.show = dptc_expanded_min_value_show;
> > +
> > + sysfs_attr_init(&ps->max_value.attr);
> > + ps->max_value.attr.name = "max_value";
> > + ps->max_value.attr.mode = 0444;
> > + ps->max_value.show = dptc_expanded_max_value_show;
> > +
> > + sysfs_attr_init(&ps->scalar_increment.attr);
> > + ps->scalar_increment.attr.name = "scalar_increment";
> > + ps->scalar_increment.attr.mode = 0444;
> > + ps->scalar_increment.show = dptc_expanded_scalar_increment_show;
> > +
> > + sysfs_attr_init(&ps->display_name.attr);
> > + ps->display_name.attr.name = "display_name";
> > + ps->display_name.attr.mode = 0444;
> > + ps->display_name.show = dptc_expanded_display_name_show;
> > +
> > + sysfs_attr_init(&ps->type.attr);
> > + ps->type.attr.name = "type";
> > + ps->type.attr.mode = 0444;
> > + ps->type.show = dptc_expanded_type_show;
> > +
> > + ps->attrs[0] = &ps->current_value.attr;
> > + ps->attrs[1] = &ps->default_value.attr;
> > + ps->attrs[2] = &ps->min_value.attr;
> > + ps->attrs[3] = &ps->max_value.attr;
> > + ps->attrs[4] = &ps->scalar_increment.attr;
> > + ps->attrs[5] = &ps->display_name.attr;
> > + ps->attrs[6] = &ps->type.attr;
> > + ps->attrs[7] = NULL;
> > +
> > + ps->group.name = "expanded_limits";
> > + ps->group.attrs = ps->attrs;
> > +}
> > +
> > +static void dptc_fw_dev_unregister(void *data)
> > +{
> > + device_unregister(data);
> > +}
> > +
> > +static void dptc_kset_unregister(void *data)
> > +{
> > + kset_unregister(data);
> > +}
> > +
> > +/* Platform profile */
> > +
> > +static void dptc_apply_profile(struct dptc_priv *dptc,
> > + enum platform_profile_option profile)
> > +{
> > + const struct dptc_profile *pp;
> > + int i;
> > +
> > + memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
> > +
> > + if (profile == PLATFORM_PROFILE_CUSTOM)
> > + return;
> > +
> > + pp = &dptc->dev_limits->profiles[profile];
> > + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> > + if (!pp->vals[i])
> > + continue;
> > + dptc->staged[i] = pp->vals[i];
> > + dptc->has_staged[i] = true;
> > + }
> > +}
> > +
> > +static int dptc_pp_probe(void *drvdata, unsigned long *choices)
> > +{
> > + struct dptc_priv *dptc = drvdata;
> > + int i, j;
> > +
> > + set_bit(PLATFORM_PROFILE_CUSTOM, choices);
> > + for (i = 0; i < PLATFORM_PROFILE_LAST; i++) {
> > + for (j = 0; j < DPTC_NUM_PARAMS; j++) {
> > + if (dptc->dev_limits->profiles[i].vals[j]) {
> > + set_bit(i, choices);
> > + break;
> > + }
> > + }
> > + }
> > + return 0;
> > +}
> > +
> > +static int dptc_pp_get(struct device *dev,
> > + enum platform_profile_option *profile)
> > +{
> > + struct dptc_priv *dptc = dev_get_drvdata(dev);
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + *profile = dptc->profile;
> > + return 0;
> > +}
> > +
> > +static int dptc_pp_set(struct device *dev,
> > + enum platform_profile_option profile)
> > +{
> > + struct dptc_priv *dptc = dev_get_drvdata(dev);
> > + int ret = 0;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + dptc->profile = profile;
> > + dptc_apply_profile(dptc, profile);
> > + if (profile != PLATFORM_PROFILE_CUSTOM)
> > + ret = dptc_alib_save(dptc);
> > +
> > + return ret;
> > +}
> > +
> > +static const struct platform_profile_ops dptc_pp_ops = {
> > + .probe = dptc_pp_probe,
> > + .profile_get = dptc_pp_get,
> > + .profile_set = dptc_pp_set,
> > +};
> > +
> > +static int dptc_resume(struct device *dev)
> > +{
> > + struct dptc_priv *dptc = dev_get_drvdata(dev);
> > + int ret;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
> > + dptc_apply_profile(dptc, dptc->profile);
> > + ret = dptc_alib_save(dptc);
> > + } else if (dptc->save_mode == SAVE_SINGLE) {
> > + ret = dptc_alib_save(dptc);
> > + } else {
> > + ret = 0;
> > + }
> > +
> > + if (ret)
> > + dev_warn(dev, "failed to restore settings on resume: %d\n", ret);
> > +
> > + return 0;
> > +}
> > +
> > +static DEFINE_SIMPLE_DEV_PM_OPS(dptc_pm_ops, NULL, dptc_resume);
> > +
> > +static int dptc_probe(struct platform_device *pdev)
> > +{
> > + const struct dmi_system_id *dmi_match = dev_get_platdata(&pdev->dev);
> > + struct dptc_priv *dptc;
> > + struct device *dev = &pdev->dev;
> > + int i, ret;
> > +
> > + dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL);
> > + if (!dptc)
> > + return -ENOMEM;
> > +
> > + platform_set_drvdata(pdev, dptc);
> > +
> > + ret = devm_mutex_init(dev, &dptc->lock);
> > + if (ret)
> > + return ret;
> > +
> > + dptc->dev_limits = dmi_match->driver_data;
> > + dev_info(dev, "%s (%s)\n", dmi_match->ident,
> > + boot_cpu_data.x86_model_id);
> > +
> > + dptc->fw_attr_dev = device_create(&firmware_attributes_class,
> > + NULL, MKDEV(0, 0), NULL,
> > + DRIVER_NAME);
>
> I think DRIVER_NAME can safely fit one line up.
>
> I probably missed a bunch of other nitpicky stuff. I'll look more
> closely once this is a more readable series.
>
> Thanks,
> Derek
>
> > + if (IS_ERR(dptc->fw_attr_dev))
> > + return PTR_ERR(dptc->fw_attr_dev);
> > +
> > + ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister,
> > + dptc->fw_attr_dev);
> > + if (ret)
> > + return ret;
> > +
> > + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL,
> > + &dptc->fw_attr_dev->kobj);
> > + if (!dptc->fw_attr_kset)
> > + return -ENOMEM;
> > +
> > + ret = devm_add_action_or_reset(dev, dptc_kset_unregister,
> > + dptc->fw_attr_kset);
> > + if (ret)
> > + return ret;
> > +
> > + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> > + dptc_setup_param_sysfs(dptc, &dptc->params[i], i);
> > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> > + &dptc->params[i].group);
> > + if (ret)
> > + return ret;
> > + }
> > +
> > + dptc_setup_expanded_sysfs(dptc, &dptc->expanded_attr);
> > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> > + &dptc->expanded_attr.group);
> > + if (ret)
> > + return ret;
> > +
> > + sysfs_attr_init(&dptc->save_settings_attr.attr);
> > + dptc->save_settings_attr.attr.name = "save_settings";
> > + dptc->save_settings_attr.attr.mode = 0644;
> > + dptc->save_settings_attr.show = dptc_save_settings_show;
> > + dptc->save_settings_attr.store = dptc_save_settings_store;
> > + ret = sysfs_create_file(&dptc->fw_attr_kset->kobj,
> > + &dptc->save_settings_attr.attr);
> > + if (ret)
> > + return ret;
> > +
> > + dptc->profile = PLATFORM_PROFILE_CUSTOM;
> > + dptc->ppdev = devm_platform_profile_register(dev, "amd-dptc", dptc,
> > + &dptc_pp_ops);
> > + if (IS_ERR(dptc->ppdev))
> > + return PTR_ERR(dptc->ppdev);
> > +
> > + return 0;
> > +}
> > +
> > +static struct platform_driver dptc_driver = {
> > + .driver = {
> > + .name = DRIVER_NAME,
> > + .pm = pm_sleep_ptr(&dptc_pm_ops),
> > + },
> > + .probe = dptc_probe,
> > +};
> > +
> > +static int __init dptc_init(void)
> > +{
> > + const struct dmi_system_id *match;
> > + bool soc_found = false;
> > + int i, ret;
> > +
> > + if (!acpi_has_method(NULL, ALIB_PATH)) {
> > + pr_debug("ALIB method not present\n");
> > + return -ENODEV;
> > + }
> > +
> > + match = dmi_first_match(dptc_dmi_table);
> > + if (!match)
> > + return -ENODEV;
> > +
> > + for (i = 0; dptc_soc_table[i]; i++) {
> > + if (strstr(boot_cpu_data.x86_model_id,
> > + dptc_soc_table[i])) {
> > + soc_found = true;
> > + break;
> > + }
> > + }
> > + if (!soc_found) {
> > + pr_warn("unrecognized SoC '%s'\n",
> > + boot_cpu_data.x86_model_id);
> > + return -ENODEV;
> > + }
> > +
> > + dptc_pdev = platform_device_register_data(NULL, DRIVER_NAME, -1,
> > + match, sizeof(*match));
> > + if (IS_ERR(dptc_pdev))
> > + return PTR_ERR(dptc_pdev);
> > +
> > + ret = platform_driver_register(&dptc_driver);
> > + if (ret) {
> > + platform_device_unregister(dptc_pdev);
> > + return ret;
> > + }
> > +
> > + return 0;
> > +}
> > +
> > +static void __exit dptc_exit(void)
> > +{
> > + platform_driver_unregister(&dptc_driver);
> > + platform_device_unregister(dptc_pdev);
> > +}
> > +
> > +module_init(dptc_init);
> > +module_exit(dptc_exit);
> > +
> > +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>");
> > +MODULE_DESCRIPTION("AMD DPTCi ACPI Driver");
> > +MODULE_LICENSE("GPL");
> > --
> > 2.52.0
> >
> >
>
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [RFC v2 2/2] platform/x86/amd: Add AMD DPTCi driver
2026-03-07 11:17 ` Antheas Kapenekakis
@ 2026-03-07 11:25 ` Antheas Kapenekakis
0 siblings, 0 replies; 8+ messages in thread
From: Antheas Kapenekakis @ 2026-03-07 11:25 UTC (permalink / raw)
To: Derek John Clark
Cc: Mario.Limonciello, W_Armin, sashal, Shyam-Sundar.S-k,
denis.benato, linux-kernel, platform-driver-x86
On Sat, 7 Mar 2026 at 12:17, Antheas Kapenekakis <lkml@antheas.dev> wrote:
>
> On Fri, 6 Mar 2026 at 22:14, Derek John Clark <derekjohn.clark@gmail.com> wrote:
> >
> > On Thu, Mar 5, 2026 at 10:19 AM Antheas Kapenekakis <lkml@antheas.dev> 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.
> >
> > Hi Antheas,
> >
> > Thanks for the CC. I've long opined the lack of having a generic
> > interface for these features in the kernel. With that being said, I
> > think very careful consideration for the interface is a necessity. We
> > never want to have the chance for an unsuspecting or inexperienced
> > user to have hardware malfunctions by misusing the interface. As a
> > general rule I think it would be good for each individual device added
> > to have a tested-by tag before it is included. That message would
> > preferably include some sort of metadata identifying the true hardware
> > limits (tech docs if available, but marketing material or OEM software
> > screenshots would be sufficient, we know how some of these companies
> > are with information)
> >
> > > The driver matches the ABI of asus-armoury, by exposing a platform
> > > profile with reasonable tunings for devices depending on their max
> > > thermal envelope, and {ppt_pl1_spl,ppt_pl2_sppt,ppt_pl3_fppt,cpu_temp}
> > > tunables which only become writable when the profile is custom,
> > > otherwise they are read-only and reflect the current profile.
> > >
> > > The default profile is custom so that we do not write to the device
> > > until userspace explicitly selects a profile, remaining at firmware
> > > defaults.
> >
> > I see what you're trying to do here, but I'm a bit conflicted on the
> > implementation. Hardware defaults are there for a reason after all...
> > Part of the original discussion with custom was that setting it would
> > taint the kernel to inform remote troubleshooters that there was a
> > non-standard change. TBS I don't see that in the actual code for
> > acpi/platform_profile.c so maybe the intention was that the drivers
> > themselves set it? If that's the case then I'll need to submit a patch
> > to add that to lenovo-wmi-gamezone so I'm compliant. I'd like Mario's
> > input on that first as IMO it makes more sense to do it as a blanket
> > in the platform class, or we don't bother as we expect drivers to
> > respect the TDP limits set by ODM/OEM. If we do end up doing that
> > though, just booting a device with this driver would automatically
> > taint the kernel, which is certainly not the desired behavior. I'm not
> > sure how to best square that circle. Do we know the BIOS defaults for
> > all the devices we're adding here? I'd expect that would correspond
> > with balanced most of the time, so perhaps making sure those match is
> > a good compromise.
> >
> >
> > > Assisted-by: Claude:claude-opus-4-6
> > > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> > > ---
> > > MAINTAINERS | 6 +
> > > drivers/platform/x86/amd/Kconfig | 16 +
> > > drivers/platform/x86/amd/Makefile | 2 +
> > > drivers/platform/x86/amd/dptc.c | 1255 +++++++++++++++++++++++++++++
> > > 4 files changed, 1279 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..ee79a2c084a8 100644
> > > --- a/drivers/platform/x86/amd/Kconfig
> > > +++ b/drivers/platform/x86/amd/Kconfig
> > > @@ -44,3 +44,19 @@ 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 ACPI_PLATFORM_PROFILE
> > > + select FIRMWARE_ATTRIBUTES_CLASS
> > > + help
> > > + Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> > > + Thermal Configuration Interface (DPTCi). Exposes TDP and thermal
> > > + parameters for AMD APU-based handheld devices via the
> > > + firmware-attributes sysfs ABI, allowing userspace tools to stage
> > > + and atomically commit power limit settings. Requires a DMI match
> > > + for the device and a recognized AMD SoC.
> > > +
> > > + If built as a module, the module will be called amd_dptc.
> > > +
> > > diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile
> > > index f6ff0c837f34..862a609bfe38 100644
> > > --- a/drivers/platform/x86/amd/Makefile
> > > +++ b/drivers/platform/x86/amd/Makefile
> > > @@ -12,3 +12,5 @@ obj-$(CONFIG_AMD_PMF) += pmf/
> > > obj-$(CONFIG_AMD_WBRF) += wbrf.o
> > > obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o
> > > obj-$(CONFIG_AMD_HFI) += hfi/
> > > +obj-$(CONFIG_AMD_DPTC) += amd_dptc.o
> > > +amd_dptc-y := dptc.o
> > > diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
> > > new file mode 100644
> > > index 000000000000..68d9d3b85cb6
> > > --- /dev/null
> > > +++ b/drivers/platform/x86/amd/dptc.c
> > > @@ -0,0 +1,1255 @@
> > > +// SPDX-License-Identifier: GPL-2.0-only
> > > +/*
> > > + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver
> > > + *
> > > + * Exposes AMD APU power and thermal parameters via the firmware-attributes
> > > + * sysfs ABI. Parameters are staged and atomically committed through the
> > > + * AGESA ALIB Function 0x0C (Dynamic Power and Thermal Configuration
> > > + * interface).
> > > + *
> > > + * Reference: AMD AGESA Publication #44065, Appendix E.5
> > > + * https://docs.amd.com/v/u/en-US/44065_Arch2008
> > > + *
> > > + * Copyright (C) 2026 Antheas Kapenekakis <lkml@antheas.dev>
> > > + */
> > > +
> > > +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> > > +
> > > +#include <linux/acpi.h>
> > > +#include <linux/cleanup.h>
> > > +#include <linux/dmi.h>
> > > +#include <linux/init.h>
> > > +#include <linux/kobject.h>
> > > +#include <linux/module.h>
> > > +#include <linux/mutex.h>
> > > +#include <linux/platform_device.h>
> > > +#include <linux/platform_profile.h>
> > > +#include <linux/processor.h>
> > > +#include <linux/sysfs.h>
> > > +#include <linux/unaligned.h>
> > > +
> > > +#include "../firmware_attributes_class.h"
> > > +
> > > +#define DRIVER_NAME "amd_dptc"
> > > +
> > > +#define ALIB_FUNC_DPTC 0x0C
> > > +#define ALIB_PATH "\\_SB.ALIB"
> > > +
> > > +/* ALIB parameter IDs (AGESA spec Appendix E.5, Table E-52) */
> > > +#define ALIB_ID_TEMP_TARGET 0x03
> > > +#define ALIB_ID_STAPM_LIMIT 0x05
> > > +#define ALIB_ID_FAST_LIMIT 0x06
> > > +#define ALIB_ID_SLOW_LIMIT 0x07
> > > +#define ALIB_ID_SKIN_LIMIT 0x2E
> > > +
> > > +enum dptc_param_idx {
> > > + DPTC_PPT_PL1_SPL, /* STAPM + skin limit (set together) */
> > > + DPTC_PPT_PL2_SPPT, /* slow PPT limit */
> > > + DPTC_PPT_PL3_FPPT, /* fast PPT limit */
> > > + DPTC_CPU_TEMP, /* thermal control target */
> > > + DPTC_NUM_PARAMS,
> > > +};
> > > +
> > > +struct dptc_param_limits {
> > > + u32 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_profile {
> > > + u32 vals[DPTC_NUM_PARAMS]; /* 0 = don't set / unstage this param */
> > > +};
> > > +
> > > +struct dptc_device_limits {
> > > + struct dptc_param_limits params[DPTC_NUM_PARAMS];
> > > + struct dptc_profile profiles[PLATFORM_PROFILE_LAST];
> > > +};
> > > +
> > > +struct dptc_param_desc {
> > > + const char *name;
> > > + const char *display_name;
> > > + u8 param_id;
> > > + u8 param_id2; /* secondary ALIB ID, 0 if none */
> > > +};
> > > +
> > > +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = {
> > > + [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (mW)",
> > > + ALIB_ID_STAPM_LIMIT, ALIB_ID_SKIN_LIMIT },
> > > + [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (mW)",
> > > + ALIB_ID_SLOW_LIMIT },
> > > + [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (mW)",
> > > + ALIB_ID_FAST_LIMIT },
> > > + [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)",
> > > + ALIB_ID_TEMP_TARGET },
> > > +};
> >
> > You already mentioned this elsewhere, but I agree that using W over mW
> > would make sense to keep the interfaces consistent.
> >
> > > +/* 18W class: AYANEO AIR Plus (Ryzen 5 5560U) */
> > > +static const struct dptc_device_limits limits_18w = { .params = {
> > > + [DPTC_PPT_PL1_SPL] = { 0, 5000, 15000, 18000, 22000 },
> > > + [DPTC_PPT_PL2_SPPT] = { 0, 5000, 15000, 18000, 22000 },
> > > + [DPTC_PPT_PL3_FPPT] = { 0, 5000, 15000, 20000, 25000 },
> > > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > > +}, .profiles = {
> > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5000, 5000, 8000, 0 } },
> > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 12000, 14000, 15000, 0 } },
> > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 18000, 18000, 20000, 0 } },
> > > +}};
> > > +
> >
> > I would personally prefer these be set directly in the DMI match per
> > device. Its more code in the end, but its more cautious, as each
> > manufacturer has a bespoke cooling solution. Assuming they're all
> > equivalent is not best practice IMO. I have quite a bit of historical
> > data on this so I can help fill in the blanks where it is missing.
>
> I would rather keep it like this and expand for specific devices when
> there is a reason. This makes the match section 1/3 the size. The
> chips are the same, they behave the same performance-wise. And for
> lower TDPs (e.g., lower_power, balanced) where the device is
> guaranteed to handle it the behavior is exact.
>
> > > +/* 25W class: Ryzen 5000 handhelds (AYANEO NEXT, KUN) */
> > > +static const struct dptc_device_limits limits_25w = { .params = {
> > > + [DPTC_PPT_PL1_SPL] = { 0, 4000, 15000, 25000, 32000 },
> > > + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 20000, 27000, 35000 },
> > > + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 25000, 30000, 37000 },
> > > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > > +}, .profiles = {
> > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8000, 8000, 12000, 0 } },
> > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15000, 17000, 20000, 0 } },
> > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25000, 27000, 30000, 0 } },
> > > +}};
> > >
> > > +/* 28W class: GPD Win series, AYANEO 2, OrangePi NEO-01 */
> > > +static const struct dptc_device_limits limits_28w = { .params = {
> > > + [DPTC_PPT_PL1_SPL] = { 0, 4000, 15000, 28000, 32000 },
> > > + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 20000, 30000, 35000 },
> > > + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 25000, 32000, 37000 },
> > > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > > +}, .profiles = {
> > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8000, 8000, 12000, 0 } },
> > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15000, 17000, 22000, 0 } },
> > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25000, 28000, 32000, 0 } },
> > > + [PLATFORM_PROFILE_MAX_POWER] = { .vals = { 28000, 30000, 32000, 0 } },
> > > +}};
> > > +
> > > +/* 30W class: OneXPlayer, AYANEO AIR/FLIP/GEEK/SLIDE/3, AOKZOE */
> > > +static const struct dptc_device_limits limits_30w = { .params = {
> > > + [DPTC_PPT_PL1_SPL] = { 0, 4000, 15000, 30000, 40000 },
> > > + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 20000, 32000, 43000 },
> > > + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 25000, 41000, 50000 },
> > > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > > +}, .profiles = {
> > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8000, 8000, 12000, 0 } },
> > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15000, 17000, 25000, 0 } },
> > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25000, 28000, 41000, 0 } },
> > > + [PLATFORM_PROFILE_MAX_POWER] = { .vals = { 30000, 32000, 41000, 0 } },
> > > +}};
> > > +
> > > +/* AI MAX Handheld class: GPD Win 5 */
> > > +static const struct dptc_device_limits limits_maxhh = { .params = {
> > This struct name seems arbitrary compared to the others. If this is
> > the eventual pattern this should just be called _80w
>
> I am still not sure what the correct range is for this handheld, which
> is why it is called MAXHandHeld. The TDP range is too broad to put an
> 80 on it. I am talking with users, it seems the sweet spot is 45W
>
> > > + [DPTC_PPT_PL1_SPL] = { 0, 4000, 25000, 80000, 100000 },
> > > + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 27000, 82000, 100000 },
> > > + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 40000, 85000, 100000 },
> > > + [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 },
> > > +}, .profiles = {
> > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 15000, 15000, 25000, 0 } },
> > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 25000, 27000, 40000, 0 } },
> > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 60000, 63000, 85000, 0 } },
> > > + [PLATFORM_PROFILE_MAX_POWER] = { .vals = { 80000, 82000, 85000, 0 } },
> > > +}};
> >
> > One other thing to consider is AC vs DC power draw. Thermal load is
> > important, but we also want to avoid over-stressing the battery on
> > these devices. I would restrict MAX_POWER to when AC is plugged ad
> > return an error if on DC, and ensure PERFORMANCE is at or just below
> > peak DC power draw. I didn't do this in gamezone on Lenovo because
> > those limits are BIOS enforced and MAX_POWER will just match
> > PERFORMANCE on DC.
>
> This is true. However I would like to defer AC/DC wrangling in this
> driver. The values are already conservative for the profiles except
> max power. So I think it would be best to drop MAX_POWER for now.
>
> > > +/* Substring matches against boot_cpu_data.x86_model_id; order matters. */
> > > +static const char * const dptc_soc_table[] = {
> > > + /* AI MAX */
> > > + "AMD RYZEN AI MAX+ 395",
> > > + "AMD RYZEN AI MAX+ 385",
> > > + "AMD RYZEN AI MAX 380",
> > > + /* Ryzen AI */
> > > + "AMD Ryzen AI 9 HX 370",
> > > + "AMD Ryzen AI HX 360",
> > > + /* Z1 - Extreme before plain Z1 */
> > > + "AMD Ryzen Z1 Extreme",
> > > + "AMD Ryzen Z1",
> > > + /* Ryzen 8000 */
> > > + "AMD Ryzen 7 8840U",
> > > + /* Ryzen 7040 */
> > > + "AMD Ryzen 7 7840U",
> > > + /* Ryzen 6000 */
> > > + "AMD Ryzen 7 6800U",
> > > + "AMD Ryzen 7 6600U",
> > > + /* Ryzen 5000 */
> > > + "AMD Ryzen 7 5800U",
> > > + "AMD Ryzen 7 5700U",
> > > + "AMD Ryzen 5 5560U",
> > > + NULL,
> > > +};
> >
> > I have some data on 4800/4800U and AMD Athelon Silver APU devices as
> > well. When I return from my conference I can post some DSDT dumps to
> > validate the interface is present on them as well.
>
> I think I considered adding support for the Athelon Silver and that it
> did have some support. But in the end the chip is too old to make TDP
> control viable for it.
>
> > > +static const struct dmi_system_id dptc_dmi_table[] = {
> > > + /* GPD */
> > > + {
> > > + .ident = "GPD Win Mini",
> > > + .matches = {
> > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-01"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + {
> > > + .ident = "GPD Win Mini 2024",
> > > + .matches = {
> > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + {
> > > + .ident = "GPD Win Mini 2024",
> > > + .matches = {
> > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02-L"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + {
> > > + .ident = "GPD Win 4",
> > > + .matches = {
> > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-04"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + {
> > > + .ident = "GPD Win 5",
> > > + .matches = {
> > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"),
> > > + },
> > > + .driver_data = (void *)&limits_maxhh,
> > > + },
> > > + {
> > > + .ident = "GPD Win Max 2",
> > > + .matches = {
> > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-04"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + {
> > > + .ident = "GPD Win Max 2 2024",
> > > + .matches = {
> > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-05"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + {
> > > + .ident = "GPD Duo",
> > > + .matches = {
> > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + {
> > > + .ident = "GPD Duo",
> > > + .matches = {
> > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01-L"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + {
> > > + .ident = "GPD Pocket 4",
> > > + .matches = {
> > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + {
> > > + .ident = "GPD Pocket 4",
> > > + .matches = {
> > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04-L"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + /* OrangePi */
> > > + {
> > > + .ident = "OrangePi NEO-01",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + /* AYN */
> > > + {
> > > + .ident = "AYN Loki Max",
> > > + .matches = {
> > > + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ayn"),
> > > + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Loki Max"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + /* Tectoy (Zeenix Pro = Loki Max) */
> > > + {
> > > + .ident = "Zeenix Pro",
> > > + .matches = {
> > > + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Tectoy"),
> > > + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Zeenix Pro"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + /* AOKZOE */
> > > + {
> > > + .ident = "AOKZOE A1 AR07",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 AR07"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + {
> > > + .ident = "AOKZOE A1 Pro",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 Pro"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + {
> > > + .ident = "AOKZOE A1X",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1X"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + {
> > > + .ident = "AOKZOE A2 Pro",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A2 Pro"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + /* OneXPlayer (Intel variants filtered by SoC table) */
> > > + {
> > > + .ident = "ONEXPLAYER F1Pro",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1Pro"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + {
> > > + .ident = "ONEXPLAYER F1 EVA-02",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1 EVA-02"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + {
> > > + .ident = "ONEXPLAYER 2",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > > + DMI_MATCH(DMI_BOARD_NAME, "ONEXPLAYER 2"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + {
> > > + .ident = "ONEXPLAYER X1 A",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1 A"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + {
> > > + .ident = "ONEXPLAYER X1z",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1z"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + {
> > > + .ident = "ONEXPLAYER X1Pro",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Pro"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + {
> > > + .ident = "ONEXPLAYER G1 A",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 A"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + /* AYANEO - 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,
> > > + },
> > > + /* AYANEO - 25W */
> > > + {
> > > + .ident = "AYANEO NEXT Advance",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Advance"),
> > > + },
> > > + .driver_data = (void *)&limits_25w,
> > > + },
> >
> > AYANEO went through a strange DMI phase, many of the first run of NEXT
> > devices are prefaced with AYANEO in the board name as well
>
> If you can pull up the exact name that would be good. We can dupe it.
>
> > > + {
> > > + .ident = "AYANEO NEXT Lite",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Lite"),
> > > + },
> > > + .driver_data = (void *)&limits_25w,
> > > + },
> > > + {
> > > + .ident = "AYANEO NEXT Pro",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Pro"),
> > > + },
> > > + .driver_data = (void *)&limits_25w,
> > > + },
> > > + {
> > > + .ident = "AYANEO NEXT",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT"),
> > > + },
> > > + .driver_data = (void *)&limits_25w,
> > > + },
> > > + {
> > > + .ident = "AYANEO KUN",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"),
> > > + },
> > > + .driver_data = (void *)&limits_25w,
> > > + },
> > > + {
> > > + .ident = "AYANEO KUN",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO KUN"),
> > > + },
> > > + .driver_data = (void *)&limits_25w,
> > > + },
> > > + /* AYANEO - 28W */
> > > + {
> > > + .ident = "AYANEO 2",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 2"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + {
> > > + .ident = "SuiPlay0X1",
> > > + .matches = {
> > > + DMI_MATCH(DMI_SYS_VENDOR, "Mysten Labs, Inc."),
> > > + DMI_MATCH(DMI_PRODUCT_NAME, "SuiPlay0X1"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + /* AYANEO - 30W */
> > > + {
> > > + /* 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,
> > > + },
> >
> > The original air is max 15w, air pro is 18w. I didn't scrutinize the
> > rest as closely, but these will need to be validated per device.
> > (Hence my reccomendation above)
>
> These Air devices from ~2023 and before will need a closer look.
> Everything from 2024 onwards should be correct.
>
> > > + {
> > > + /* 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);
> > > +
> > > +struct dptc_priv;
> > > +
> > > +struct dptc_attr_sysfs {
> > > + struct dptc_priv *priv;
> > > + struct kobj_attribute current_value;
> > > + struct kobj_attribute default_value;
> > > + struct kobj_attribute min_value;
> > > + struct kobj_attribute max_value;
> > > + struct kobj_attribute scalar_increment;
> > > + struct kobj_attribute display_name;
> > > + struct kobj_attribute type;
> > > + struct attribute *attrs[8];
> > > + struct attribute_group group;
> > > + int idx;
> > > +};
> > > +
> > > +struct dptc_priv {
> > > + struct device *fw_attr_dev;
> > > + struct kset *fw_attr_kset;
> > > +
> > > + const struct dptc_device_limits *dev_limits;
> > > +
> > > + bool expanded;
> > > +
> > > + enum platform_profile_option profile;
> > > + struct device *ppdev;
> > > +
> > > + enum dptc_save_mode { SAVE_SINGLE, SAVE_BULK } save_mode;
> > > +
> > > + u32 staged[DPTC_NUM_PARAMS];
> > > + bool has_staged[DPTC_NUM_PARAMS];
> > > +
> > > + /* Protects mutable driver state */
> > > + struct mutex lock;
> > > +
> > > + struct dptc_attr_sysfs params[DPTC_NUM_PARAMS];
> > > + struct dptc_attr_sysfs expanded_attr;
> > > + struct kobj_attribute save_settings_attr;
> > > +};
> > > +
> > > +static struct platform_device *dptc_pdev;
> > > +
> > > +static u32 dptc_get_min(struct dptc_priv *dptc, int idx)
> > > +{
> > > + return dptc->expanded ? dptc->dev_limits->params[idx].min
> > > + : dptc->dev_limits->params[idx].smin;
> > > +}
> > > +
> > > +static u32 dptc_get_max(struct dptc_priv *dptc, int idx)
> > > +{
> > > + return dptc->expanded ? dptc->dev_limits->params[idx].max
> > > + : dptc->dev_limits->params[idx].smax;
> > > +}
> > > +
> > > +static u32 dptc_get_default(struct dptc_priv *dptc, int idx)
> > > +{
> > > + return dptc->dev_limits->params[idx].def;
> > > +}
> > > +
> > > +static int dptc_alib_call(const u8 *ids, const u32 *vals, int count)
> > > +{
> > > + union acpi_object in_params[2];
> > > + struct acpi_object_list input;
> > > + acpi_status status;
> > > + u32 buf_size;
> > > + int i, off;
> > > + u8 *buf;
> > > +
> > > + if (count == 0)
> > > + return -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;
> > > +
> > > + put_unaligned_le16(buf_size, buf);
> > > +
> > > + for (i = 0; i < count; i++) {
> > > + off = 2 + i * 5;
> > > + buf[off] = ids[i];
> > > + put_unaligned_le32(vals[i], buf + off + 1);
> > > + }
> > > +
> > > + in_params[0].type = ACPI_TYPE_INTEGER;
> > > + in_params[0].integer.value = ALIB_FUNC_DPTC;
> > > + in_params[1].type = ACPI_TYPE_BUFFER;
> > > + in_params[1].buffer.length = buf_size;
> > > + in_params[1].buffer.pointer = buf;
> > > +
> > > + input.count = 2;
> > > + input.pointer = in_params;
> > > +
> > > + status = acpi_evaluate_object(NULL, ALIB_PATH, &input, NULL);
> > > + kfree(buf);
> > > +
> > > + if (ACPI_FAILURE(status)) {
> > > + pr_err("ALIB call failed: %s\n",
> > > + acpi_format_exception(status));
> > > + return -EIO;
> > > + }
> > > +
> > > + pr_debug("sent %d ALIB parameter(s)\n", count);
> > > + return 0;
> > > +}
> > > +
> > > +static int dptc_alib_send_one(int idx, u32 val)
> > > +{
> > > + u32 vals[2];
> > > + u8 ids[2];
> > > + int count = 0;
> > > +
> > > + ids[count] = dptc_params[idx].param_id;
> > > + vals[count] = val;
> > > + count++;
> > > + if (dptc_params[idx].param_id2) {
> > > + ids[count] = dptc_params[idx].param_id2;
> > > + vals[count] = val;
> > > + count++;
> > > + }
> > > +
> > > + return dptc_alib_call(ids, vals, count);
> > > +}
> > > +
> > > +static int dptc_alib_save(struct dptc_priv *dptc)
> > > +{
> > > + u32 vals[DPTC_NUM_PARAMS * 2];
> > > + u8 ids[DPTC_NUM_PARAMS * 2];
> > > + int i, count = 0;
> > > +
> > > + 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 (dptc_params[i].param_id2) {
> > > + ids[count] = dptc_params[i].param_id2;
> > > + vals[count] = dptc->staged[i];
> > > + count++;
> > > + }
> > > + }
> > > +
> > > + if (!count)
> > > + return 0;
> > > +
> > > + return dptc_alib_call(ids, vals, count);
> > > +}
> > > +
> > > +/* Sysfs callbacks */
> > > +
> > > +static ssize_t dptc_current_value_show(struct kobject *kobj,
> > > + struct kobj_attribute *attr, char *buf)
> > > +{
> > > + struct dptc_attr_sysfs *ps =
> > > + container_of(attr, struct dptc_attr_sysfs, current_value);
> >
> > I think this can be one line.
>
> It overflows.
>
> >
> > > + struct dptc_priv *dptc = ps->priv;
> > > +
> > > + guard(mutex)(&dptc->lock);
> > > +
> > > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
> > > + u32 val = dptc->dev_limits->profiles[dptc->profile].vals[ps->idx];
> >
> > Is the interface RO? If we can read the value from hardware that would
> > be preferable, in case separate userspace or PMF changes this behind
> > the scenes. If we can avoid the kernel getting out of sync with the
> > hardware that would be better.
>
> The dptc interface is strictly read only. The underlying SMU mailbox
> provides a debug table that contains these values, but that is a debug
> table and should not be relied upon by userspace. This is what
> ryzenadj -i prints
>
> The function that provides the memory address of this table is not
> implemented by ALIB afaik.
>
> > > + if (!val)
> > > + return sysfs_emit(buf, "\n");
> > > + return sysfs_emit(buf, "%u\n", val);
> > > + }
> > > +
> > > + 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_attr_sysfs *ps =
> > > + container_of(attr, struct dptc_attr_sysfs, current_value);
> > > + struct dptc_priv *dptc = ps->priv;
> > > + u32 val, min, max;
> > > + int ret;
> > > +
> > > + if (count == 1 && buf[0] == '\n') {
> > > + guard(mutex)(&dptc->lock);
> > > +
> > > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> > > + return -EPERM;
> >
> > Note for myself, this is a better error code than what I'm doing IIRC.
> >
> > > + dptc->has_staged[ps->idx] = false;
> > > + return count;
> > > + }
> > > +
> > > + ret = kstrtou32(buf, 10, &val);
> > > + if (ret)
> > > + return ret;
> > > +
> > > + guard(mutex)(&dptc->lock);
> > > +
> > > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> > > + return -EPERM;
> >
> > Why do this twice? You can check this first and then use a scoped guard.
>
> Noted. This can be moved up.
Turns out that it technically can be moved up but it would be probably
be flagged. Moving it up would place the kstrtou32 under the mutex
which is not necessary.
Antheas
> > > + min = dptc_get_min(dptc, ps->idx);
> > > + max = dptc_get_max(dptc, ps->idx);
> > > + if (val < min || (max && val > max))
> > > + return -EINVAL;
> > > + dptc->staged[ps->idx] = val;
> > > + dptc->has_staged[ps->idx] = true;
> > > + if (dptc->save_mode == SAVE_SINGLE)
> > > + ret = dptc_alib_send_one(ps->idx, val);
> > > +
> > > + return ret ? ret : count;
> > > +}
> > > +
> > > +static ssize_t dptc_default_value_show(struct kobject *kobj,
> > > + struct kobj_attribute *attr, char *buf)
> > > +{
> > > + struct dptc_attr_sysfs *ps =
> > > + container_of(attr, struct dptc_attr_sysfs, default_value);
> > > +
> >
> > Another should be one liner, there are a few more but I'm not going to
> > tag each one. Additional note, what is ps supposed to stand for?
> > Verbose variable names improve readability.
>
> ps -> Dptc_attr_Sysfs. No, in this case the driver would read worse.
> It is a holder val. If it was verbose it would break reflowing in most
> of the driver.
>
> > > + return sysfs_emit(buf, "%u\n", dptc_get_default(ps->priv, ps->idx));
> > > +}
> > > +
> > > +static ssize_t dptc_min_value_show(struct kobject *kobj,
> > > + struct kobj_attribute *attr, char *buf)
> > > +{
> > > + struct dptc_attr_sysfs *ps =
> > > + container_of(attr, struct dptc_attr_sysfs, min_value);
> > > + struct dptc_priv *dptc = ps->priv;
> > > +
> > > + guard(mutex)(&dptc->lock);
> > > +
> > > + return sysfs_emit(buf, "%u\n", dptc_get_min(dptc, ps->idx));
> > > +}
> > > +
> > > +static ssize_t dptc_max_value_show(struct kobject *kobj,
> > > + struct kobj_attribute *attr, char *buf)
> > > +{
> > > + struct dptc_attr_sysfs *ps =
> > > + container_of(attr, struct dptc_attr_sysfs, max_value);
> > > + struct dptc_priv *dptc = ps->priv;
> > > +
> > > + guard(mutex)(&dptc->lock);
> > > +
> > > + return sysfs_emit(buf, "%u\n", dptc_get_max(dptc, ps->idx));
> > > +}
> > > +
> > > +static ssize_t dptc_scalar_increment_show(struct kobject *kobj,
> > > + struct kobj_attribute *attr, char *buf)
> > > +{
> > > + return sysfs_emit(buf, "1\n");
> > > +}
> > > +
> > > +static ssize_t dptc_display_name_show(struct kobject *kobj,
> > > + struct kobj_attribute *attr, char *buf)
> > > +{
> > > + struct dptc_attr_sysfs *ps =
> > > + container_of(attr, struct dptc_attr_sysfs, display_name);
> > > + return sysfs_emit(buf, "%s\n", dptc_params[ps->idx].display_name);
> > > +}
> > > +
> > > +static ssize_t dptc_type_show(struct kobject *kobj,
> > > + struct kobj_attribute *attr, char *buf)
> > > +{
> > > + return sysfs_emit(buf, "integer\n");
> > > +}
> > > +
> > > +static ssize_t dptc_save_settings_show(struct kobject *kobj,
> > > + struct kobj_attribute *attr, char *buf)
> > > +{
> > > + struct dptc_priv *dptc =
> > > + container_of(attr, struct dptc_priv, save_settings_attr);
> > > +
> > > + guard(mutex)(&dptc->lock);
> > > +
> > > + if (dptc->save_mode == SAVE_SINGLE)
> > > + return sysfs_emit(buf, "single\n");
> > > + return sysfs_emit(buf, "bulk\n");
> > > +}
> > > +
> > > +static ssize_t dptc_save_settings_store(struct kobject *kobj,
> > > + struct kobj_attribute *attr,
> > > + const char *buf, size_t count)
> > > +{
> > > + struct dptc_priv *dptc =
> > > + container_of(attr, struct dptc_priv, save_settings_attr);
> > > + int ret = 0;
> > > +
> > > + guard(mutex)(&dptc->lock);
> > > +
> > > + if (sysfs_streq(buf, "save"))
> > > + ret = dptc_alib_save(dptc);
> > > + else if (sysfs_streq(buf, "single"))
> > > + dptc->save_mode = SAVE_SINGLE;
> > > + else if (sysfs_streq(buf, "bulk"))
> > > + dptc->save_mode = SAVE_BULK;
> > > + else
> > > + return -EINVAL;
> > > +
> > > + return ret ? ret : count;
> > > +}
> > > +
> > > +static ssize_t dptc_expanded_current_value_show(struct kobject *kobj,
> > > + struct kobj_attribute *attr,
> > > + char *buf)
> > > +{
> > > + struct dptc_attr_sysfs *ps =
> > > + container_of(attr, struct dptc_attr_sysfs, current_value);
> > > + struct dptc_priv *dptc = ps->priv;
> > > +
> > > + guard(mutex)(&dptc->lock);
> > > +
> > > + return sysfs_emit(buf, "%d\n", dptc->expanded);
> > > +}
> > > +
> >
> > What is the actual purpose of these expanded attrs? IMO we should only
> > load automatically within a validated range per device. TBS, there is
> > a valid need for R&D loading, so either a debugfs as Mario suggested
> > or a module param can be used to load the driver without limits. In
> > that case, I would just set max to u16 max and min to 0 and offer a
> > very stern dev_warn in dmesg, then you can avoid duplicated
> > _show/_store functions.
>
> Some devices like the GPD Win ones can do 30W. However their casing
> can become too hot which is why they're limited to 28. This toggle is
> an escape hatch that gives them some predefined wiggle room so they do
> not resort to more extreme measures.
>
> > > +static ssize_t dptc_expanded_current_value_store(struct kobject *kobj,
> > > + struct kobj_attribute *attr,
> > > + const char *buf, size_t count)
> > > +{
> > > + struct dptc_attr_sysfs *ps =
> > > + container_of(attr, struct dptc_attr_sysfs, current_value);
> > > + struct dptc_priv *dptc = ps->priv;
> > > + bool val;
> > > + int ret;
> > > +
> > > + ret = kstrtobool(buf, &val);
> > > + if (ret)
> > > + return ret;
> > > +
> > > + guard(mutex)(&dptc->lock);
> > > +
> > > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> > > + return -EPERM;
> > > + dptc->expanded = val;
> > > + /* Clear staged values: limits changed, old values may be out of range */
> > > + memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
> > > +
> > > + return count;
> > > +}
> > > +
> > > +static ssize_t dptc_expanded_default_value_show(struct kobject *kobj,
> > > + struct kobj_attribute *attr,
> > > + char *buf)
> > > +{
> > > + return sysfs_emit(buf, "0\n");
> > > +}
> > > +
> > > +static ssize_t dptc_expanded_min_value_show(struct kobject *kobj,
> > > + struct kobj_attribute *attr,
> > > + char *buf)
> > > +{
> > > + return sysfs_emit(buf, "0\n");
> > > +}
> > > +
> > > +static ssize_t dptc_expanded_max_value_show(struct kobject *kobj,
> > > + struct kobj_attribute *attr,
> > > + char *buf)
> > > +{
> > > + return sysfs_emit(buf, "1\n");
> > > +}
> > > +
> > > +static ssize_t dptc_expanded_scalar_increment_show(struct kobject *kobj,
> > > + struct kobj_attribute *attr,
> > > + char *buf)
> > > +{
> > > + return sysfs_emit(buf, "1\n");
> > > +}
> > > +
> > > +static ssize_t dptc_expanded_display_name_show(struct kobject *kobj,
> > > + struct kobj_attribute *attr,
> > > + char *buf)
> > > +{
> > > + return sysfs_emit(buf, "Expanded Limits\n");
> > > +}
> > > +
> > > +static ssize_t dptc_expanded_type_show(struct kobject *kobj,
> > > + struct kobj_attribute *attr, char *buf)
> > > +{
> > > + return sysfs_emit(buf, "integer\n");
> > > +}
> > > +
> > > +/* Sysfs setup */
> > > +
> > > +static void dptc_setup_param_sysfs(struct dptc_priv *dptc,
> > > + struct dptc_attr_sysfs *ps, int idx)
> > > +{
> > > + ps->priv = dptc;
> > > + ps->idx = idx;
> > > +
> > > + sysfs_attr_init(&ps->current_value.attr);
> > > + ps->current_value.attr.name = "current_value";
> > > + ps->current_value.attr.mode = 0644;
> > > + ps->current_value.show = dptc_current_value_show;
> > > + ps->current_value.store = dptc_current_value_store;
> > > +
> > > + sysfs_attr_init(&ps->default_value.attr);
> > > + ps->default_value.attr.name = "default_value";
> > > + ps->default_value.attr.mode = 0444;
> > > + ps->default_value.show = dptc_default_value_show;
> > > +
> > > + sysfs_attr_init(&ps->min_value.attr);
> > > + ps->min_value.attr.name = "min_value";
> > > + ps->min_value.attr.mode = 0444;
> > > + ps->min_value.show = dptc_min_value_show;
> > > +
> > > + sysfs_attr_init(&ps->max_value.attr);
> > > + ps->max_value.attr.name = "max_value";
> > > + ps->max_value.attr.mode = 0444;
> > > + ps->max_value.show = dptc_max_value_show;
> > > +
> > > + sysfs_attr_init(&ps->scalar_increment.attr);
> > > + ps->scalar_increment.attr.name = "scalar_increment";
> > > + ps->scalar_increment.attr.mode = 0444;
> > > + ps->scalar_increment.show = dptc_scalar_increment_show;
> > > +
> > > + sysfs_attr_init(&ps->display_name.attr);
> > > + ps->display_name.attr.name = "display_name";
> > > + ps->display_name.attr.mode = 0444;
> > > + ps->display_name.show = dptc_display_name_show;
> > > +
> > > + sysfs_attr_init(&ps->type.attr);
> > > + ps->type.attr.name = "type";
> > > + ps->type.attr.mode = 0444;
> > > + ps->type.show = dptc_type_show;
> > > +
> > > + ps->attrs[0] = &ps->current_value.attr;
> > > + ps->attrs[1] = &ps->default_value.attr;
> > > + ps->attrs[2] = &ps->min_value.attr;
> > > + ps->attrs[3] = &ps->max_value.attr;
> > > + ps->attrs[4] = &ps->scalar_increment.attr;
> > > + ps->attrs[5] = &ps->display_name.attr;
> > > + ps->attrs[6] = &ps->type.attr;
> > > + ps->attrs[7] = NULL;
> > > +
> > > + ps->group.name = dptc_params[idx].name;
> > > + ps->group.attrs = ps->attrs;
> > > +}
> > > +
> >
> > Some "macro soup" could clean this up a lot.
>
> Agree to disagree, this driver is very readable and in the end that is
> what matters. The value types are different so it is not clear if the
> same macro would work for both anyway.
>
> > > +static void dptc_setup_expanded_sysfs(struct dptc_priv *dptc,
> > > + struct dptc_attr_sysfs *ps)
> > > +{
> > > + ps->priv = dptc;
> > > + sysfs_attr_init(&ps->current_value.attr);
> > > + ps->current_value.attr.name = "current_value";
> > > + ps->current_value.attr.mode = 0644;
> > > + ps->current_value.show = dptc_expanded_current_value_show;
> > > + ps->current_value.store = dptc_expanded_current_value_store;
> > > +
> > > + sysfs_attr_init(&ps->default_value.attr);
> > > + ps->default_value.attr.name = "default_value";
> > > + ps->default_value.attr.mode = 0444;
> > > + ps->default_value.show = dptc_expanded_default_value_show;
> > > +
> > > + sysfs_attr_init(&ps->min_value.attr);
> > > + ps->min_value.attr.name = "min_value";
> > > + ps->min_value.attr.mode = 0444;
> > > + ps->min_value.show = dptc_expanded_min_value_show;
> > > +
> > > + sysfs_attr_init(&ps->max_value.attr);
> > > + ps->max_value.attr.name = "max_value";
> > > + ps->max_value.attr.mode = 0444;
> > > + ps->max_value.show = dptc_expanded_max_value_show;
> > > +
> > > + sysfs_attr_init(&ps->scalar_increment.attr);
> > > + ps->scalar_increment.attr.name = "scalar_increment";
> > > + ps->scalar_increment.attr.mode = 0444;
> > > + ps->scalar_increment.show = dptc_expanded_scalar_increment_show;
> > > +
> > > + sysfs_attr_init(&ps->display_name.attr);
> > > + ps->display_name.attr.name = "display_name";
> > > + ps->display_name.attr.mode = 0444;
> > > + ps->display_name.show = dptc_expanded_display_name_show;
> > > +
> > > + sysfs_attr_init(&ps->type.attr);
> > > + ps->type.attr.name = "type";
> > > + ps->type.attr.mode = 0444;
> > > + ps->type.show = dptc_expanded_type_show;
> > > +
> > > + ps->attrs[0] = &ps->current_value.attr;
> > > + ps->attrs[1] = &ps->default_value.attr;
> > > + ps->attrs[2] = &ps->min_value.attr;
> > > + ps->attrs[3] = &ps->max_value.attr;
> > > + ps->attrs[4] = &ps->scalar_increment.attr;
> > > + ps->attrs[5] = &ps->display_name.attr;
> > > + ps->attrs[6] = &ps->type.attr;
> > > + ps->attrs[7] = NULL;
> > > +
> > > + ps->group.name = "expanded_limits";
> > > + ps->group.attrs = ps->attrs;
> > > +}
> > > +
> > > +static void dptc_fw_dev_unregister(void *data)
> > > +{
> > > + device_unregister(data);
> > > +}
> > > +
> > > +static void dptc_kset_unregister(void *data)
> > > +{
> > > + kset_unregister(data);
> > > +}
> > > +
> > > +/* Platform profile */
> > > +
> > > +static void dptc_apply_profile(struct dptc_priv *dptc,
> > > + enum platform_profile_option profile)
> > > +{
> > > + const struct dptc_profile *pp;
> > > + int i;
> > > +
> > > + memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
> > > +
> > > + if (profile == PLATFORM_PROFILE_CUSTOM)
> > > + return;
> > > +
> > > + pp = &dptc->dev_limits->profiles[profile];
> > > + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> > > + if (!pp->vals[i])
> > > + continue;
> > > + dptc->staged[i] = pp->vals[i];
> > > + dptc->has_staged[i] = true;
> > > + }
> > > +}
> > > +
> > > +static int dptc_pp_probe(void *drvdata, unsigned long *choices)
> > > +{
> > > + struct dptc_priv *dptc = drvdata;
> > > + int i, j;
> > > +
> > > + set_bit(PLATFORM_PROFILE_CUSTOM, choices);
> > > + for (i = 0; i < PLATFORM_PROFILE_LAST; i++) {
> > > + for (j = 0; j < DPTC_NUM_PARAMS; j++) {
> > > + if (dptc->dev_limits->profiles[i].vals[j]) {
> > > + set_bit(i, choices);
> > > + break;
> > > + }
> > > + }
> > > + }
> > > + return 0;
> > > +}
> > > +
> > > +static int dptc_pp_get(struct device *dev,
> > > + enum platform_profile_option *profile)
> > > +{
> > > + struct dptc_priv *dptc = dev_get_drvdata(dev);
> > > +
> > > + guard(mutex)(&dptc->lock);
> > > +
> > > + *profile = dptc->profile;
> > > + return 0;
> > > +}
> > > +
> > > +static int dptc_pp_set(struct device *dev,
> > > + enum platform_profile_option profile)
> > > +{
> > > + struct dptc_priv *dptc = dev_get_drvdata(dev);
> > > + int ret = 0;
> > > +
> > > + guard(mutex)(&dptc->lock);
> > > +
> > > + dptc->profile = profile;
> > > + dptc_apply_profile(dptc, profile);
> > > + if (profile != PLATFORM_PROFILE_CUSTOM)
> > > + ret = dptc_alib_save(dptc);
> > > +
> > > + return ret;
> > > +}
> > > +
> > > +static const struct platform_profile_ops dptc_pp_ops = {
> > > + .probe = dptc_pp_probe,
> > > + .profile_get = dptc_pp_get,
> > > + .profile_set = dptc_pp_set,
> > > +};
> > > +
> > > +static int dptc_resume(struct device *dev)
> > > +{
> > > + struct dptc_priv *dptc = dev_get_drvdata(dev);
> > > + int ret;
> > > +
> > > + guard(mutex)(&dptc->lock);
> > > +
> > > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
> > > + dptc_apply_profile(dptc, dptc->profile);
> > > + ret = dptc_alib_save(dptc);
> > > + } else if (dptc->save_mode == SAVE_SINGLE) {
> > > + ret = dptc_alib_save(dptc);
> > > + } else {
> > > + ret = 0;
> > > + }
> > > +
> > > + if (ret)
> > > + dev_warn(dev, "failed to restore settings on resume: %d\n", ret);
> > > +
> > > + return 0;
> > > +}
> > > +
> > > +static DEFINE_SIMPLE_DEV_PM_OPS(dptc_pm_ops, NULL, dptc_resume);
> > > +
> > > +static int dptc_probe(struct platform_device *pdev)
> > > +{
> > > + const struct dmi_system_id *dmi_match = dev_get_platdata(&pdev->dev);
> > > + struct dptc_priv *dptc;
> > > + struct device *dev = &pdev->dev;
> > > + int i, ret;
> > > +
> > > + dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL);
> > > + if (!dptc)
> > > + return -ENOMEM;
> > > +
> > > + platform_set_drvdata(pdev, dptc);
> > > +
> > > + ret = devm_mutex_init(dev, &dptc->lock);
> > > + if (ret)
> > > + return ret;
> > > +
> > > + dptc->dev_limits = dmi_match->driver_data;
> > > + dev_info(dev, "%s (%s)\n", dmi_match->ident,
> > > + boot_cpu_data.x86_model_id);
> > > +
> > > + dptc->fw_attr_dev = device_create(&firmware_attributes_class,
> > > + NULL, MKDEV(0, 0), NULL,
> > > + DRIVER_NAME);
> >
> > I think DRIVER_NAME can safely fit one line up.
> >
> > I probably missed a bunch of other nitpicky stuff. I'll look more
> > closely once this is a more readable series.
> >
> > Thanks,
> > Derek
> >
> > > + if (IS_ERR(dptc->fw_attr_dev))
> > > + return PTR_ERR(dptc->fw_attr_dev);
> > > +
> > > + ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister,
> > > + dptc->fw_attr_dev);
> > > + if (ret)
> > > + return ret;
> > > +
> > > + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL,
> > > + &dptc->fw_attr_dev->kobj);
> > > + if (!dptc->fw_attr_kset)
> > > + return -ENOMEM;
> > > +
> > > + ret = devm_add_action_or_reset(dev, dptc_kset_unregister,
> > > + dptc->fw_attr_kset);
> > > + if (ret)
> > > + return ret;
> > > +
> > > + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> > > + dptc_setup_param_sysfs(dptc, &dptc->params[i], i);
> > > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> > > + &dptc->params[i].group);
> > > + if (ret)
> > > + return ret;
> > > + }
> > > +
> > > + dptc_setup_expanded_sysfs(dptc, &dptc->expanded_attr);
> > > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> > > + &dptc->expanded_attr.group);
> > > + if (ret)
> > > + return ret;
> > > +
> > > + sysfs_attr_init(&dptc->save_settings_attr.attr);
> > > + dptc->save_settings_attr.attr.name = "save_settings";
> > > + dptc->save_settings_attr.attr.mode = 0644;
> > > + dptc->save_settings_attr.show = dptc_save_settings_show;
> > > + dptc->save_settings_attr.store = dptc_save_settings_store;
> > > + ret = sysfs_create_file(&dptc->fw_attr_kset->kobj,
> > > + &dptc->save_settings_attr.attr);
> > > + if (ret)
> > > + return ret;
> > > +
> > > + dptc->profile = PLATFORM_PROFILE_CUSTOM;
> > > + dptc->ppdev = devm_platform_profile_register(dev, "amd-dptc", dptc,
> > > + &dptc_pp_ops);
> > > + if (IS_ERR(dptc->ppdev))
> > > + return PTR_ERR(dptc->ppdev);
> > > +
> > > + return 0;
> > > +}
> > > +
> > > +static struct platform_driver dptc_driver = {
> > > + .driver = {
> > > + .name = DRIVER_NAME,
> > > + .pm = pm_sleep_ptr(&dptc_pm_ops),
> > > + },
> > > + .probe = dptc_probe,
> > > +};
> > > +
> > > +static int __init dptc_init(void)
> > > +{
> > > + const struct dmi_system_id *match;
> > > + bool soc_found = false;
> > > + int i, ret;
> > > +
> > > + if (!acpi_has_method(NULL, ALIB_PATH)) {
> > > + pr_debug("ALIB method not present\n");
> > > + return -ENODEV;
> > > + }
> > > +
> > > + match = dmi_first_match(dptc_dmi_table);
> > > + if (!match)
> > > + return -ENODEV;
> > > +
> > > + for (i = 0; dptc_soc_table[i]; i++) {
> > > + if (strstr(boot_cpu_data.x86_model_id,
> > > + dptc_soc_table[i])) {
> > > + soc_found = true;
> > > + break;
> > > + }
> > > + }
> > > + if (!soc_found) {
> > > + pr_warn("unrecognized SoC '%s'\n",
> > > + boot_cpu_data.x86_model_id);
> > > + return -ENODEV;
> > > + }
> > > +
> > > + dptc_pdev = platform_device_register_data(NULL, DRIVER_NAME, -1,
> > > + match, sizeof(*match));
> > > + if (IS_ERR(dptc_pdev))
> > > + return PTR_ERR(dptc_pdev);
> > > +
> > > + ret = platform_driver_register(&dptc_driver);
> > > + if (ret) {
> > > + platform_device_unregister(dptc_pdev);
> > > + return ret;
> > > + }
> > > +
> > > + return 0;
> > > +}
> > > +
> > > +static void __exit dptc_exit(void)
> > > +{
> > > + platform_driver_unregister(&dptc_driver);
> > > + platform_device_unregister(dptc_pdev);
> > > +}
> > > +
> > > +module_init(dptc_init);
> > > +module_exit(dptc_exit);
> > > +
> > > +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>");
> > > +MODULE_DESCRIPTION("AMD DPTCi ACPI Driver");
> > > +MODULE_LICENSE("GPL");
> > > --
> > > 2.52.0
> > >
> > >
> >
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-03-07 11:25 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-03-05 18:17 [RFC v2 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Antheas Kapenekakis
2026-03-05 18:17 ` [RFC v2 1/2] Documentation: firmware-attributes: generalize save_settings entry Antheas Kapenekakis
2026-03-05 18:17 ` [RFC v2 2/2] platform/x86/amd: Add AMD DPTCi driver Antheas Kapenekakis
2026-03-05 20:57 ` Mario Limonciello
2026-03-05 21:18 ` Antheas Kapenekakis
2026-03-06 21:14 ` Derek John Clark
2026-03-07 11:17 ` Antheas Kapenekakis
2026-03-07 11:25 ` Antheas Kapenekakis
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®