* [RFC v3 0/4] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls
@ 2026-03-07 11:55 Antheas Kapenekakis
2026-03-07 11:55 ` [RFC v3 1/4] Documentation: firmware-attributes: generalize save_settings entry Antheas Kapenekakis
` (3 more replies)
0 siblings, 4 replies; 19+ messages in thread
From: Antheas Kapenekakis @ 2026-03-07 11:55 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.
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 v3:
- Split single driver patch into 3: core driver, platform profile,
device entries
- Rename DRIVER_NAME from "amd_dptc" to "amd-dptc" (match subsystem
convention for platform drivers and firmware-attributes devices)
- Add scale field to dptc_param_desc: sysfs values in user units (W, C),
driver multiplies by scale (1000 for mW) before sending to ALIB
- Rename struct fields: min/smin/smax/max ->
expanded_min/device_min/device_max/expanded_max
- Remove comment "ALIB parameter IDs (AGESA spec Appendix E.5, Table
E-52)"
- Move ALIB method check after SoC/DMI validation, change pr_debug to
pr_warn
- Reorder local variable declaration in dptc_init (dptc after other vars)
- Add commit subject prefix "dptc:" to all driver patches
- Remove early-return for empty save in dptc_alib_save; let
dptc_alib_call return -EINVAL (matches documentation)
- Remove max_power as we do not do DC/AC validation in the driver
- Fix Ayaneo AIR device matches to reflect their wattage, add 15W profile
for original AIR. Cheers to the AIR Plus user who had helped tune the
AIR Plus profile so it was correct.
Changes in v2:
- Use a platform_device base instead of raw inits + exit, hook into devm
helpers referencing samsung-galaxybook
- Add platform_profile support (low-power / balanced / performance /
custom) with per-device energy presets; non-custom profiles lock
tunables to read-only. We default to custom to avoid writing values
- Reduce exposed parameters from seven to four (ppt_pl1_spl,
ppt_pl2_sppt, ppt_pl3_fppt, cpu_temp); drop time constants and
separate skin/STAPM limits in favour of a single SPL that sets both.
For devices where the max tdp offers thermals that are not suitable
for day to day use (e.g., excessive fan noise), MAX_POWER is added
- Remove CONFIG_AMD_DPTC_EXTENDED and the "soc"/"unbound" limit tiers;
keep only device (smin..smax) and expanded (min..max) and soc match
(certain devices ship multiple SoCs on the same motherboard/thermal
envelope, make sure we only hook into validated SoCs)
- Rename "commit" attribute to "save_settings" per firmware-attributes
ABI; rename limit_mode to expanded_limits
- Change expanded_limits attribute type from "enumeration" to "integer"
(min=0, max=1) since firmware-attributes has no bool type
- Remove all global vars, limit _dev access to init and exit
- Use u32 accessors to set values for ALIB call
- Clean up verbose comments throughout
- Add Ayn Loki Max / Tectoy Zeenix Pro
V2: https://lore.kernel.org/all/20260305181751.3642846-1-lkml@antheas.dev/
V1: https://lore.kernel.org/all/20260303181707.2920261-1-lkml@antheas.dev/
Antheas Kapenekakis (4):
Documentation: firmware-attributes: generalize save_settings entry
platform/x86/amd: dptc: Add AMD DPTCi driver
platform/x86/amd: dptc: Add platform profile support
platform/x86/amd: dptc: Add device entries for handheld PCs
.../testing/sysfs-class-firmware-attributes | 41 +-
MAINTAINERS | 6 +
drivers/platform/x86/amd/Kconfig | 15 +
drivers/platform/x86/amd/Makefile | 2 +
drivers/platform/x86/amd/dptc.c | 1264 +++++++++++++++++
5 files changed, 1313 insertions(+), 15 deletions(-)
create mode 100644 drivers/platform/x86/amd/dptc.c
base-commit: 298ec5fe3c6a91f61dd971f9aa7e22e79c9ec024
--
2.52.0
^ permalink raw reply [flat|nested] 19+ messages in thread
* [RFC v3 1/4] Documentation: firmware-attributes: generalize save_settings entry
2026-03-07 11:55 [RFC v3 0/4] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Antheas Kapenekakis
@ 2026-03-07 11:55 ` Antheas Kapenekakis
2026-03-07 11:55 ` [RFC v3 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver Antheas Kapenekakis
` (2 subsequent siblings)
3 siblings, 0 replies; 19+ messages in thread
From: Antheas Kapenekakis @ 2026-03-07 11:55 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] 19+ messages in thread
* [RFC v3 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver
2026-03-07 11:55 [RFC v3 0/4] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Antheas Kapenekakis
2026-03-07 11:55 ` [RFC v3 1/4] Documentation: firmware-attributes: generalize save_settings entry Antheas Kapenekakis
@ 2026-03-07 11:55 ` Antheas Kapenekakis
2026-03-07 21:50 ` Rong Zhang
2026-03-07 11:55 ` [RFC v3 3/4] platform/x86/amd: dptc: Add platform profile support Antheas Kapenekakis
2026-03-07 11:55 ` [RFC v3 4/4] platform/x86/amd: dptc: Add device entries for handheld PCs Antheas Kapenekakis
3 siblings, 1 reply; 19+ messages in thread
From: Antheas Kapenekakis @ 2026-03-07 11:55 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
Add a driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
Thermal Configuration interface (DPTCi). This exposes TDP and thermal
parameters for AMD APU-based handheld devices via the
firmware-attributes sysfs ABI.
Parameters are staged and atomically committed through ALIB. The driver
supports two save modes: "single" (apply immediately on write) and
"bulk" (stage values, then commit with "save"). An "expanded_limits"
toggle widens the allowed parameter ranges beyond device defaults.
Initial device support: GPD Win 5 (AMD Ryzen AI MAX).
Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
---
MAINTAINERS | 6 +
drivers/platform/x86/amd/Kconfig | 14 +
drivers/platform/x86/amd/Makefile | 2 +
drivers/platform/x86/amd/dptc.c | 747 ++++++++++++++++++++++++++++++
4 files changed, 769 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..d610092467fc 100644
--- a/drivers/platform/x86/amd/Kconfig
+++ b/drivers/platform/x86/amd/Kconfig
@@ -44,3 +44,17 @@ config AMD_ISP_PLATFORM
This driver can also be built as a module. If so, the module
will be called amd_isp4.
+
+config AMD_DPTC
+ tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)"
+ depends on X86_64 && ACPI && DMI
+ select FIRMWARE_ATTRIBUTES_CLASS
+ help
+ Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
+ Thermal Configuration Interface (DPTCi). Exposes TDP and thermal
+ parameters for AMD APU-based handheld devices via the
+ firmware-attributes sysfs ABI, allowing userspace tools to stage
+ and atomically commit power limit settings. Requires a DMI match
+ for the device and a recognized AMD SoC.
+
+ If built as a module, the module will be called amd_dptc.
diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile
index f6ff0c837f34..862a609bfe38 100644
--- a/drivers/platform/x86/amd/Makefile
+++ b/drivers/platform/x86/amd/Makefile
@@ -12,3 +12,5 @@ obj-$(CONFIG_AMD_PMF) += pmf/
obj-$(CONFIG_AMD_WBRF) += wbrf.o
obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o
obj-$(CONFIG_AMD_HFI) += hfi/
+obj-$(CONFIG_AMD_DPTC) += amd_dptc.o
+amd_dptc-y := dptc.o
diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
new file mode 100644
index 000000000000..acfe9cc01bab
--- /dev/null
+++ b/drivers/platform/x86/amd/dptc.c
@@ -0,0 +1,747 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver
+ *
+ * Exposes AMD APU power and thermal parameters via the firmware-attributes
+ * sysfs ABI. Parameters are staged and atomically committed through the
+ * AGESA ALIB Function 0x0C (Dynamic Power and Thermal Configuration
+ * interface).
+ *
+ * Reference: AMD AGESA Publication #44065, Appendix E.5
+ * https://docs.amd.com/v/u/en-US/44065_Arch2008
+ *
+ * Copyright (C) 2026 Antheas Kapenekakis <lkml@antheas.dev>
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/acpi.h>
+#include <linux/cleanup.h>
+#include <linux/dmi.h>
+#include <linux/init.h>
+#include <linux/kobject.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/platform_device.h>
+#include <linux/processor.h>
+#include <linux/sysfs.h>
+#include <linux/unaligned.h>
+
+#include "../firmware_attributes_class.h"
+
+#define DRIVER_NAME "amd-dptc"
+
+#define ALIB_FUNC_DPTC 0x0C
+#define ALIB_PATH "\\_SB.ALIB"
+
+#define ALIB_ID_TEMP_TARGET 0x03
+#define ALIB_ID_STAPM_LIMIT 0x05
+#define ALIB_ID_FAST_LIMIT 0x06
+#define ALIB_ID_SLOW_LIMIT 0x07
+#define ALIB_ID_SKIN_LIMIT 0x2E
+
+enum dptc_param_idx {
+ DPTC_PPT_PL1_SPL, /* STAPM + skin limit (set together) */
+ DPTC_PPT_PL2_SPPT, /* slow PPT limit */
+ DPTC_PPT_PL3_FPPT, /* fast PPT limit */
+ DPTC_CPU_TEMP, /* thermal control target */
+ DPTC_NUM_PARAMS,
+};
+
+struct dptc_param_limits {
+ u32 expanded_min;
+ u32 device_min;
+ u32 def;
+ u32 device_max;
+ u32 expanded_max;
+};
+
+struct dptc_device_limits {
+ struct dptc_param_limits params[DPTC_NUM_PARAMS];
+};
+
+struct dptc_param_desc {
+ const char *name;
+ const char *display_name;
+ u16 scale; /* sysfs-to-ALIB multiplier (e.g. 1000 for W->mW) */
+ u8 param_id;
+ u8 param_id2; /* secondary ALIB ID, 0 if none */
+};
+
+static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = {
+ [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (W)",
+ 1000, ALIB_ID_STAPM_LIMIT, ALIB_ID_SKIN_LIMIT },
+ [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (W)",
+ 1000, ALIB_ID_SLOW_LIMIT },
+ [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (W)",
+ 1000, ALIB_ID_FAST_LIMIT },
+ [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)",
+ 1, ALIB_ID_TEMP_TARGET },
+};
+
+/* AI MAX Handheld class: GPD Win 5 */
+static const struct dptc_device_limits limits_maxhh = { .params = {
+ [DPTC_PPT_PL1_SPL] = { 0, 4, 25, 80, 100 },
+ [DPTC_PPT_PL2_SPPT] = { 0, 4, 27, 82, 100 },
+ [DPTC_PPT_PL3_FPPT] = { 0, 4, 40, 85, 100 },
+ [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 },
+}};
+
+/* Substring matches against boot_cpu_data.x86_model_id; order matters. */
+static const char * const dptc_soc_table[] = {
+ /* AI MAX */
+ "AMD RYZEN AI MAX+ 395",
+ "AMD RYZEN AI MAX+ 385",
+ "AMD RYZEN AI MAX 380",
+ NULL,
+};
+
+static const struct dmi_system_id dptc_dmi_table[] = {
+ /* GPD */
+ {
+ .ident = "GPD Win 5",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
+ DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"),
+ },
+ .driver_data = (void *)&limits_maxhh,
+ },
+ { }
+};
+MODULE_DEVICE_TABLE(dmi, dptc_dmi_table);
+
+struct dptc_priv;
+
+struct dptc_attr_sysfs {
+ struct dptc_priv *priv;
+ struct kobj_attribute current_value;
+ struct kobj_attribute default_value;
+ struct kobj_attribute min_value;
+ struct kobj_attribute max_value;
+ struct kobj_attribute scalar_increment;
+ struct kobj_attribute display_name;
+ struct kobj_attribute type;
+ struct attribute *attrs[8];
+ struct attribute_group group;
+ int idx;
+};
+
+struct dptc_priv {
+ struct device *fw_attr_dev;
+ struct kset *fw_attr_kset;
+
+ const struct dptc_device_limits *dev_limits;
+
+ bool expanded;
+
+ enum dptc_save_mode { SAVE_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].expanded_min
+ : dptc->dev_limits->params[idx].device_min;
+}
+
+static u32 dptc_get_max(struct dptc_priv *dptc, int idx)
+{
+ return dptc->expanded ? dptc->dev_limits->params[idx].expanded_max
+ : dptc->dev_limits->params[idx].device_max;
+}
+
+static u32 dptc_get_default(struct dptc_priv *dptc, int idx)
+{
+ return dptc->dev_limits->params[idx].def;
+}
+
+static int dptc_alib_call(const u8 *ids, const u32 *vals, int count)
+{
+ union acpi_object in_params[2];
+ struct acpi_object_list input;
+ acpi_status status;
+ u32 buf_size;
+ int i, off;
+ u8 *buf;
+
+ if (count == 0)
+ return -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 hw_val = val * dptc_params[idx].scale;
+ u32 vals[2];
+ u8 ids[2];
+ int count = 0;
+
+ ids[count] = dptc_params[idx].param_id;
+ vals[count] = hw_val;
+ count++;
+ if (dptc_params[idx].param_id2) {
+ ids[count] = dptc_params[idx].param_id2;
+ vals[count] = hw_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;
+ u32 hw_val;
+
+ for (i = 0; i < DPTC_NUM_PARAMS; i++) {
+ if (!dptc->has_staged[i])
+ continue;
+
+ hw_val = dptc->staged[i] * dptc_params[i].scale;
+ ids[count] = dptc_params[i].param_id;
+ vals[count] = hw_val;
+ count++;
+ if (dptc_params[i].param_id2) {
+ ids[count] = dptc_params[i].param_id2;
+ vals[count] = hw_val;
+ count++;
+ }
+ }
+
+ 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->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);
+
+ dptc->has_staged[ps->idx] = false;
+ return count;
+ }
+
+ ret = kstrtou32(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ guard(mutex)(&dptc->lock);
+
+ 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);
+
+ 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);
+}
+
+static int dptc_resume(struct device *dev)
+{
+ struct dptc_priv *dptc = dev_get_drvdata(dev);
+ int ret;
+
+ guard(mutex)(&dptc->lock);
+
+ 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 device *dev = &pdev->dev;
+ struct dptc_priv *dptc;
+ int i, ret;
+
+ dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL);
+ if (!dptc)
+ return -ENOMEM;
+
+ platform_set_drvdata(pdev, dptc);
+
+ ret = devm_mutex_init(dev, &dptc->lock);
+ if (ret)
+ return ret;
+
+ dptc->dev_limits = dmi_match->driver_data;
+ dev_info(dev, "%s (%s)\n", dmi_match->ident,
+ boot_cpu_data.x86_model_id);
+
+ dptc->fw_attr_dev = device_create(&firmware_attributes_class,
+ NULL, MKDEV(0, 0), NULL,
+ DRIVER_NAME);
+ if (IS_ERR(dptc->fw_attr_dev))
+ return PTR_ERR(dptc->fw_attr_dev);
+
+ ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister,
+ dptc->fw_attr_dev);
+ if (ret)
+ return ret;
+
+ dptc->fw_attr_kset = kset_create_and_add("attributes", NULL,
+ &dptc->fw_attr_dev->kobj);
+ if (!dptc->fw_attr_kset)
+ return -ENOMEM;
+
+ ret = devm_add_action_or_reset(dev, dptc_kset_unregister,
+ dptc->fw_attr_kset);
+ if (ret)
+ return ret;
+
+ for (i = 0; i < DPTC_NUM_PARAMS; i++) {
+ dptc_setup_param_sysfs(dptc, &dptc->params[i], i);
+ ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
+ &dptc->params[i].group);
+ if (ret)
+ return ret;
+ }
+
+ dptc_setup_expanded_sysfs(dptc, &dptc->expanded_attr);
+ ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
+ &dptc->expanded_attr.group);
+ if (ret)
+ return ret;
+
+ sysfs_attr_init(&dptc->save_settings_attr.attr);
+ dptc->save_settings_attr.attr.name = "save_settings";
+ dptc->save_settings_attr.attr.mode = 0644;
+ dptc->save_settings_attr.show = dptc_save_settings_show;
+ dptc->save_settings_attr.store = dptc_save_settings_store;
+ ret = sysfs_create_file(&dptc->fw_attr_kset->kobj,
+ &dptc->save_settings_attr.attr);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static struct platform_driver dptc_driver = {
+ .driver = {
+ .name = DRIVER_NAME,
+ .pm = pm_sleep_ptr(&dptc_pm_ops),
+ },
+ .probe = dptc_probe,
+};
+
+static int __init dptc_init(void)
+{
+ const struct dmi_system_id *match;
+ bool soc_found = false;
+ int i, ret;
+
+ match = dmi_first_match(dptc_dmi_table);
+ if (!match)
+ return -ENODEV;
+
+ if (!acpi_has_method(NULL, ALIB_PATH)) {
+ pr_warn("ALIB method not present\n");
+ return -ENODEV;
+ }
+
+ for (i = 0; dptc_soc_table[i]; i++) {
+ if (strstr(boot_cpu_data.x86_model_id,
+ dptc_soc_table[i])) {
+ soc_found = true;
+ break;
+ }
+ }
+ if (!soc_found) {
+ pr_warn("unrecognized SoC '%s'\n",
+ boot_cpu_data.x86_model_id);
+ return -ENODEV;
+ }
+
+ dptc_pdev = platform_device_register_data(NULL, DRIVER_NAME, -1,
+ match, sizeof(*match));
+ if (IS_ERR(dptc_pdev))
+ return PTR_ERR(dptc_pdev);
+
+ ret = platform_driver_register(&dptc_driver);
+ if (ret) {
+ platform_device_unregister(dptc_pdev);
+ return ret;
+ }
+
+ return 0;
+}
+
+static void __exit dptc_exit(void)
+{
+ platform_driver_unregister(&dptc_driver);
+ platform_device_unregister(dptc_pdev);
+}
+
+module_init(dptc_init);
+module_exit(dptc_exit);
+
+MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>");
+MODULE_DESCRIPTION("AMD DPTCi ACPI Driver");
+MODULE_LICENSE("GPL");
--
2.52.0
^ permalink raw reply [flat|nested] 19+ messages in thread
* [RFC v3 3/4] platform/x86/amd: dptc: Add platform profile support
2026-03-07 11:55 [RFC v3 0/4] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Antheas Kapenekakis
2026-03-07 11:55 ` [RFC v3 1/4] Documentation: firmware-attributes: generalize save_settings entry Antheas Kapenekakis
2026-03-07 11:55 ` [RFC v3 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver Antheas Kapenekakis
@ 2026-03-07 11:55 ` Antheas Kapenekakis
2026-03-07 21:51 ` Rong Zhang
2026-03-07 11:55 ` [RFC v3 4/4] platform/x86/amd: dptc: Add device entries for handheld PCs Antheas Kapenekakis
3 siblings, 1 reply; 19+ messages in thread
From: Antheas Kapenekakis @ 2026-03-07 11:55 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
Register a platform_profile handler so the driver exposes standard
power profiles (low-power, balanced, performance, max-power) alongside
the manual tunable interface. max-power is only supplied when the
performance profile's preset values are below the tunable limits.
When a non-custom profile is active, parameter writes are blocked
(-EPERM) and current_value reflects the profile's preset values.
Selecting the "custom" profile returns control to the user for manual
staging and committing. On resume, the active profile is automatically
re-applied.
Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
---
drivers/platform/x86/amd/Kconfig | 1 +
drivers/platform/x86/amd/dptc.c | 116 +++++++++++++++++++++++++++++--
2 files changed, 113 insertions(+), 4 deletions(-)
diff --git a/drivers/platform/x86/amd/Kconfig b/drivers/platform/x86/amd/Kconfig
index d610092467fc..41ffbd722524 100644
--- a/drivers/platform/x86/amd/Kconfig
+++ b/drivers/platform/x86/amd/Kconfig
@@ -48,6 +48,7 @@ config AMD_ISP_PLATFORM
config AMD_DPTC
tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)"
depends on X86_64 && ACPI && DMI
+ select ACPI_PLATFORM_PROFILE
select FIRMWARE_ATTRIBUTES_CLASS
help
Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
index acfe9cc01bab..1f07e2e6f278 100644
--- a/drivers/platform/x86/amd/dptc.c
+++ b/drivers/platform/x86/amd/dptc.c
@@ -23,6 +23,7 @@
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/platform_device.h>
+#include <linux/platform_profile.h>
#include <linux/processor.h>
#include <linux/sysfs.h>
#include <linux/unaligned.h>
@@ -56,8 +57,13 @@ struct dptc_param_limits {
u32 expanded_max;
};
+struct dptc_profile {
+ u32 vals[DPTC_NUM_PARAMS]; /* 0 = don't set / unstage this param */
+};
+
struct dptc_device_limits {
struct dptc_param_limits params[DPTC_NUM_PARAMS];
+ struct dptc_profile profiles[PLATFORM_PROFILE_LAST];
};
struct dptc_param_desc {
@@ -85,6 +91,10 @@ static const struct dptc_device_limits limits_maxhh = { .params = {
[DPTC_PPT_PL2_SPPT] = { 0, 4, 27, 82, 100 },
[DPTC_PPT_PL3_FPPT] = { 0, 4, 40, 85, 100 },
[DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 },
+}, .profiles = {
+ [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 15, 15, 25, 0 } },
+ [PLATFORM_PROFILE_BALANCED] = { .vals = { 25, 27, 40, 0 } },
+ [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 60, 63, 85, 0 } },
}};
/* Substring matches against boot_cpu_data.x86_model_id; order matters. */
@@ -134,6 +144,9 @@ struct dptc_priv {
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];
@@ -269,6 +282,14 @@ static ssize_t dptc_current_value_show(struct kobject *kobj,
guard(mutex)(&dptc->lock);
+ if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
+ u32 val = dptc->dev_limits->profiles[dptc->profile].vals[ps->idx];
+
+ if (!val)
+ return sysfs_emit(buf, "\n");
+ return sysfs_emit(buf, "%u\n", val);
+ }
+
if (!dptc->has_staged[ps->idx])
return sysfs_emit(buf, "\n");
return sysfs_emit(buf, "%u\n", dptc->staged[ps->idx]);
@@ -287,6 +308,8 @@ static ssize_t dptc_current_value_store(struct kobject *kobj,
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;
}
@@ -297,6 +320,8 @@ static ssize_t dptc_current_value_store(struct kobject *kobj,
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))
@@ -426,6 +451,8 @@ static ssize_t dptc_expanded_current_value_store(struct kobject *kobj,
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));
@@ -594,6 +621,78 @@ 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);
@@ -601,10 +700,14 @@ static int dptc_resume(struct device *dev)
guard(mutex)(&dptc->lock);
- if (dptc->save_mode == SAVE_SINGLE)
+ if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
+ dptc_apply_profile(dptc, dptc->profile);
ret = dptc_alib_save(dptc);
- else
+ } 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);
@@ -636,8 +739,7 @@ static int dptc_probe(struct platform_device *pdev)
boot_cpu_data.x86_model_id);
dptc->fw_attr_dev = device_create(&firmware_attributes_class,
- NULL, MKDEV(0, 0), NULL,
- DRIVER_NAME);
+ NULL, MKDEV(0, 0), NULL, DRIVER_NAME);
if (IS_ERR(dptc->fw_attr_dev))
return PTR_ERR(dptc->fw_attr_dev);
@@ -680,6 +782,12 @@ static int dptc_probe(struct platform_device *pdev)
if (ret)
return ret;
+ dptc->profile = PLATFORM_PROFILE_CUSTOM;
+ dptc->ppdev = devm_platform_profile_register(dev, "amd-dptc", dptc,
+ &dptc_pp_ops);
+ if (IS_ERR(dptc->ppdev))
+ return PTR_ERR(dptc->ppdev);
+
return 0;
}
--
2.52.0
^ permalink raw reply [flat|nested] 19+ messages in thread
* [RFC v3 4/4] platform/x86/amd: dptc: Add device entries for handheld PCs
2026-03-07 11:55 [RFC v3 0/4] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Antheas Kapenekakis
` (2 preceding siblings ...)
2026-03-07 11:55 ` [RFC v3 3/4] platform/x86/amd: dptc: Add platform profile support Antheas Kapenekakis
@ 2026-03-07 11:55 ` Antheas Kapenekakis
2026-03-07 21:51 ` Rong Zhang
3 siblings, 1 reply; 19+ messages in thread
From: Antheas Kapenekakis @ 2026-03-07 11:55 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
Add limit tables and DMI entries for AMD APU-based handheld devices:
- 15W class: AYANEO AIR
- 18W class: AYANEO AIR Plus/Pro
- 25W class: AYANEO AIR 1S, NEXT/KUN
- 28W class: GPD Win Mini/4/Max 2, GPD Duo, GPD Pocket 4, AYANEO 2,
OrangePi NEO-01, SuiPlay0X1
- 30W class: AYANEO FLIP/GEEK/SLIDE/3, AOKZOE A1/A1X/A2,
OneXPlayer F1Pro/2/X1/G1, AYN Loki Max, Zeenix Pro
Also add SoC strings for Ryzen AI, Z1, Ryzen 8000/7040/6000/5000
families, extending support beyond the initial AI MAX series.
Assisted-by: Claude:claude-opus-4-6
Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
---
drivers/platform/x86/amd/dptc.c | 409 ++++++++++++++++++++++++++++++++
1 file changed, 409 insertions(+)
diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
index 1f07e2e6f278..a3d582e48cd3 100644
--- a/drivers/platform/x86/amd/dptc.c
+++ b/drivers/platform/x86/amd/dptc.c
@@ -85,6 +85,66 @@ static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = {
1, ALIB_ID_TEMP_TARGET },
};
+/* 15W class: AYANEO AIR (Ryzen 5 5560U) */
+static const struct dptc_device_limits limits_15w = { .params = {
+ [DPTC_PPT_PL1_SPL] = { 0, 5, 10, 15, 18 },
+ [DPTC_PPT_PL2_SPPT] = { 0, 5, 10, 15, 18 },
+ [DPTC_PPT_PL3_FPPT] = { 0, 5, 12, 18, 22 },
+ [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
+}, .profiles = {
+ [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } },
+ [PLATFORM_PROFILE_BALANCED] = { .vals = { 10, 12, 15, 0 } },
+ [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 15, 15, 18, 0 } },
+}};
+
+/* 18W class: AYANEO AIR Plus/Pro (Ryzen 5 5560U, Ryzen 7 5825U) */
+static const struct dptc_device_limits limits_18w = { .params = {
+ [DPTC_PPT_PL1_SPL] = { 0, 5, 15, 18, 22 },
+ [DPTC_PPT_PL2_SPPT] = { 0, 5, 15, 18, 22 },
+ [DPTC_PPT_PL3_FPPT] = { 0, 5, 15, 20, 25 },
+ [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
+}, .profiles = {
+ [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } },
+ [PLATFORM_PROFILE_BALANCED] = { .vals = { 12, 14, 15, 0 } },
+ [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 18, 18, 20, 0 } },
+}};
+
+/* 25W class: Ryzen 5000 handhelds (AYANEO NEXT, KUN) */
+static const struct dptc_device_limits limits_25w = { .params = {
+ [DPTC_PPT_PL1_SPL] = { 0, 4, 15, 25, 32 },
+ [DPTC_PPT_PL2_SPPT] = { 0, 4, 20, 27, 35 },
+ [DPTC_PPT_PL3_FPPT] = { 0, 4, 25, 30, 37 },
+ [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
+}, .profiles = {
+ [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } },
+ [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 20, 0 } },
+ [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 27, 30, 0 } },
+}};
+
+/* 28W class: GPD Win series, AYANEO 2, OrangePi NEO-01 */
+static const struct dptc_device_limits limits_28w = { .params = {
+ [DPTC_PPT_PL1_SPL] = { 0, 4, 15, 28, 32 },
+ [DPTC_PPT_PL2_SPPT] = { 0, 4, 20, 30, 35 },
+ [DPTC_PPT_PL3_FPPT] = { 0, 4, 25, 32, 37 },
+ [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
+}, .profiles = {
+ [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } },
+ [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 22, 0 } },
+ [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 32, 0 } },
+}};
+
+/* 30W class: OneXPlayer, AYANEO FLIP/GEEK/SLIDE/3, AOKZOE */
+static const struct dptc_device_limits limits_30w = { .params = {
+ [DPTC_PPT_PL1_SPL] = { 0, 4, 15, 30, 40 },
+ [DPTC_PPT_PL2_SPPT] = { 0, 4, 20, 32, 43 },
+ [DPTC_PPT_PL3_FPPT] = { 0, 4, 25, 41, 50 },
+ [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
+}, .profiles = {
+ [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } },
+ [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 25, 0 } },
+ [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 41, 0 } },
+}};
+
/* AI MAX Handheld class: GPD Win 5 */
static const struct dptc_device_limits limits_maxhh = { .params = {
[DPTC_PPT_PL1_SPL] = { 0, 4, 25, 80, 100 },
@@ -103,11 +163,60 @@ static const char * const dptc_soc_table[] = {
"AMD RYZEN AI MAX+ 395",
"AMD RYZEN AI MAX+ 385",
"AMD RYZEN AI MAX 380",
+ /* Ryzen AI */
+ "AMD Ryzen AI 9 HX 370",
+ "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 = {
@@ -116,6 +225,306 @@ static const struct dmi_system_id dptc_dmi_table[] = {
},
.driver_data = (void *)&limits_maxhh,
},
+ {
+ .ident = "GPD Win Max 2",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
+ DMI_MATCH(DMI_PRODUCT_NAME, "G1619-04"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ {
+ .ident = "GPD Win Max 2 2024",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
+ DMI_MATCH(DMI_PRODUCT_NAME, "G1619-05"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ {
+ .ident = "GPD Duo",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
+ DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ {
+ .ident = "GPD Duo",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
+ DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01-L"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ {
+ .ident = "GPD Pocket 4",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
+ DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ {
+ .ident = "GPD Pocket 4",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
+ DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04-L"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ /* OrangePi */
+ {
+ .ident = "OrangePi NEO-01",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ /* AYN */
+ {
+ .ident = "AYN Loki Max",
+ .matches = {
+ DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ayn"),
+ DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Loki Max"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ /* Tectoy (Zeenix Pro = Loki Max) */
+ {
+ .ident = "Zeenix Pro",
+ .matches = {
+ DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Tectoy"),
+ DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Zeenix Pro"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ /* AOKZOE */
+ {
+ .ident = "AOKZOE A1 AR07",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 AR07"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ {
+ .ident = "AOKZOE A1 Pro",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 Pro"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ {
+ .ident = "AOKZOE A1X",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1X"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ {
+ .ident = "AOKZOE A2 Pro",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A2 Pro"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ /* OneXPlayer (Intel variants filtered by SoC table) */
+ {
+ .ident = "ONEXPLAYER F1Pro",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1Pro"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ {
+ .ident = "ONEXPLAYER F1 EVA-02",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1 EVA-02"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ {
+ .ident = "ONEXPLAYER 2",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
+ DMI_MATCH(DMI_BOARD_NAME, "ONEXPLAYER 2"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ {
+ .ident = "ONEXPLAYER X1 A",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1 A"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ {
+ .ident = "ONEXPLAYER X1z",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1z"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ {
+ .ident = "ONEXPLAYER X1Pro",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Pro"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ {
+ .ident = "ONEXPLAYER G1 A",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 A"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ /* AYANEO - 15W */
+ {
+ .ident = "AYANEO AIR",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR"),
+ },
+ .driver_data = (void *)&limits_15w,
+ },
+ /* AYANEO - 18W */
+ {
+ .ident = "AYANEO AIR Plus",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Plus"),
+ },
+ .driver_data = (void *)&limits_18w,
+ },
+ {
+ .ident = "AYANEO AIR Pro",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Pro"),
+ },
+ .driver_data = (void *)&limits_18w,
+ },
+ /* AYANEO - 25W */
+ {
+ .ident = "AYANEO AIR 1S",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_MATCH(DMI_BOARD_NAME, "AIR 1S"),
+ },
+ .driver_data = (void *)&limits_25w,
+ },
+ {
+ .ident = "AYANEO NEXT Advance",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Advance"),
+ },
+ .driver_data = (void *)&limits_25w,
+ },
+ {
+ .ident = "AYANEO NEXT Lite",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Lite"),
+ },
+ .driver_data = (void *)&limits_25w,
+ },
+ {
+ .ident = "AYANEO NEXT Pro",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Pro"),
+ },
+ .driver_data = (void *)&limits_25w,
+ },
+ {
+ .ident = "AYANEO NEXT",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT"),
+ },
+ .driver_data = (void *)&limits_25w,
+ },
+ {
+ .ident = "AYANEO KUN",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"),
+ },
+ .driver_data = (void *)&limits_25w,
+ },
+ {
+ .ident = "AYANEO KUN",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO KUN"),
+ },
+ .driver_data = (void *)&limits_25w,
+ },
+ /* AYANEO - 28W */
+ {
+ .ident = "AYANEO 2",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_MATCH(DMI_BOARD_NAME, "AYANEO 2"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ {
+ .ident = "SuiPlay0X1",
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "Mysten Labs, Inc."),
+ DMI_MATCH(DMI_PRODUCT_NAME, "SuiPlay0X1"),
+ },
+ .driver_data = (void *)&limits_28w,
+ },
+ /* AYANEO - 30W */
+ {
+ /* DMI_MATCH catches all FLIP variants (DS, KB, 1S DS, 1S KB) */
+ .ident = "AYANEO FLIP",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_MATCH(DMI_BOARD_NAME, "FLIP"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ {
+ /* DMI_MATCH catches GEEK and GEEK 1S */
+ .ident = "AYANEO GEEK",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_MATCH(DMI_BOARD_NAME, "GEEK"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ {
+ .ident = "AYANEO SLIDE",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "SLIDE"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
+ {
+ .ident = "AYANEO 3",
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
+ DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO 3"),
+ },
+ .driver_data = (void *)&limits_30w,
+ },
{ }
};
MODULE_DEVICE_TABLE(dmi, dptc_dmi_table);
--
2.52.0
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [RFC v3 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver
2026-03-07 11:55 ` [RFC v3 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver Antheas Kapenekakis
@ 2026-03-07 21:50 ` Rong Zhang
2026-03-07 23:02 ` Antheas Kapenekakis
0 siblings, 1 reply; 19+ messages in thread
From: Rong Zhang @ 2026-03-07 21:50 UTC (permalink / raw)
To: Antheas Kapenekakis, Mario.Limonciello
Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato,
linux-kernel, platform-driver-x86
Hi Antheas,
Thanks for working on it.
I think this driver is useful for me and I will see if I can test it on
my devices once I find some time. They're not handhelds but I am sure
at least one of them has the ALIB method. It'd better if a module
option can be used to bypass the quirk table so that testing against a
new device will be easier. Such an option should taint the kernel ofc.
That being said, I have some review comments. Please see below and my
following replies.
On Sat, 2026-03-07 at 12:55 +0100, Antheas Kapenekakis wrote:
> Add a driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> Thermal Configuration interface (DPTCi). This exposes TDP and thermal
> parameters for AMD APU-based handheld devices via the
> firmware-attributes sysfs ABI.
>
> Parameters are staged and atomically committed through ALIB. The driver
> supports two save modes: "single" (apply immediately on write) and
> "bulk" (stage values, then commit with "save"). An "expanded_limits"
> toggle widens the allowed parameter ranges beyond device defaults.
>
> Initial device support: GPD Win 5 (AMD Ryzen AI MAX).
>
> Assisted-by: Claude:claude-opus-4-6
> Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> ---
> MAINTAINERS | 6 +
> drivers/platform/x86/amd/Kconfig | 14 +
> drivers/platform/x86/amd/Makefile | 2 +
> drivers/platform/x86/amd/dptc.c | 747 ++++++++++++++++++++++++++++++
> 4 files changed, 769 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..d610092467fc 100644
> --- a/drivers/platform/x86/amd/Kconfig
> +++ b/drivers/platform/x86/amd/Kconfig
> @@ -44,3 +44,17 @@ config AMD_ISP_PLATFORM
>
> This driver can also be built as a module. If so, the module
> will be called amd_isp4.
> +
> +config AMD_DPTC
> + tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)"
> + depends on X86_64 && ACPI && DMI
> + select FIRMWARE_ATTRIBUTES_CLASS
> + help
> + Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> + Thermal Configuration Interface (DPTCi). Exposes TDP and thermal
> + parameters for AMD APU-based handheld devices via the
> + firmware-attributes sysfs ABI, allowing userspace tools to stage
> + and atomically commit power limit settings. Requires a DMI match
> + for the device and a recognized AMD SoC.
> +
> + If built as a module, the module will be called amd_dptc.
> diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile
> index f6ff0c837f34..862a609bfe38 100644
> --- a/drivers/platform/x86/amd/Makefile
> +++ b/drivers/platform/x86/amd/Makefile
> @@ -12,3 +12,5 @@ obj-$(CONFIG_AMD_PMF) += pmf/
> obj-$(CONFIG_AMD_WBRF) += wbrf.o
> obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o
> obj-$(CONFIG_AMD_HFI) += hfi/
> +obj-$(CONFIG_AMD_DPTC) += amd_dptc.o
> +amd_dptc-y := dptc.o
> diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
> new file mode 100644
> index 000000000000..acfe9cc01bab
> --- /dev/null
> +++ b/drivers/platform/x86/amd/dptc.c
> @@ -0,0 +1,747 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver
> + *
> + * Exposes AMD APU power and thermal parameters via the firmware-attributes
> + * sysfs ABI. Parameters are staged and atomically committed through the
> + * AGESA ALIB Function 0x0C (Dynamic Power and Thermal Configuration
> + * interface).
> + *
> + * Reference: AMD AGESA Publication #44065, Appendix E.5
> + * https://docs.amd.com/v/u/en-US/44065_Arch2008
> + *
> + * Copyright (C) 2026 Antheas Kapenekakis <lkml@antheas.dev>
> + */
> +
> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> +
> +#include <linux/acpi.h>
> +#include <linux/cleanup.h>
> +#include <linux/dmi.h>
> +#include <linux/init.h>
> +#include <linux/kobject.h>
> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/platform_device.h>
> +#include <linux/processor.h>
> +#include <linux/sysfs.h>
> +#include <linux/unaligned.h>
> +
> +#include "../firmware_attributes_class.h"
> +
> +#define DRIVER_NAME "amd-dptc"
> +
> +#define ALIB_FUNC_DPTC 0x0C
> +#define ALIB_PATH "\\_SB.ALIB"
> +
> +#define ALIB_ID_TEMP_TARGET 0x03
> +#define ALIB_ID_STAPM_LIMIT 0x05
> +#define ALIB_ID_FAST_LIMIT 0x06
> +#define ALIB_ID_SLOW_LIMIT 0x07
> +#define ALIB_ID_SKIN_LIMIT 0x2E
> +
> +enum dptc_param_idx {
> + DPTC_PPT_PL1_SPL, /* STAPM + skin limit (set together) */
> + DPTC_PPT_PL2_SPPT, /* slow PPT limit */
> + DPTC_PPT_PL3_FPPT, /* fast PPT limit */
> + DPTC_CPU_TEMP, /* thermal control target */
> + DPTC_NUM_PARAMS,
> +};
> +
> +struct dptc_param_limits {
> + u32 expanded_min;
> + u32 device_min;
> + u32 def;
> + u32 device_max;
> + u32 expanded_max;
> +};
> +
> +struct dptc_device_limits {
> + struct dptc_param_limits params[DPTC_NUM_PARAMS];
> +};
> +
> +struct dptc_param_desc {
> + const char *name;
> + const char *display_name;
> + u16 scale; /* sysfs-to-ALIB multiplier (e.g. 1000 for W->mW) */
> + u8 param_id;
> + u8 param_id2; /* secondary ALIB ID, 0 if none */
> +};
> +
> +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = {
> + [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (W)",
> + 1000, ALIB_ID_STAPM_LIMIT, ALIB_ID_SKIN_LIMIT },
> + [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (W)",
> + 1000, ALIB_ID_SLOW_LIMIT },
> + [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (W)",
> + 1000, ALIB_ID_FAST_LIMIT },
> + [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)",
> + 1, ALIB_ID_TEMP_TARGET },
> +};
The indent here just made me crazy :-/
Align the contents after line breaks to the left braces.
> +
> +/* AI MAX Handheld class: GPD Win 5 */
> +static const struct dptc_device_limits limits_maxhh = { .params = {
Please don't save a line by zipping braces like this. It's hard to read
and likely to be missed when glancing the code (that's what happened to
me and it really confused me).
I found you did the same in [PATCH v3 3/4] for `.profiles'. That should
be avoided too.
> + [DPTC_PPT_PL1_SPL] = { 0, 4, 25, 80, 100 },
> + [DPTC_PPT_PL2_SPPT] = { 0, 4, 27, 82, 100 },
> + [DPTC_PPT_PL3_FPPT] = { 0, 4, 40, 85, 100 },
> + [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 },
> +}};
> +
> +/* Substring matches against boot_cpu_data.x86_model_id; order matters. */
> +static const char * const dptc_soc_table[] = {
> + /* AI MAX */
> + "AMD RYZEN AI MAX+ 395",
> + "AMD RYZEN AI MAX+ 385",
> + "AMD RYZEN AI MAX 380",
> + NULL,
> +};
> +
> +static const struct dmi_system_id dptc_dmi_table[] = {
> + /* GPD */
> + {
> + .ident = "GPD Win 5",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"),
> + },
> + .driver_data = (void *)&limits_maxhh,
> + },
> + { }
> +};
> +MODULE_DEVICE_TABLE(dmi, dptc_dmi_table);
> +
> +struct dptc_priv;
> +
> +struct dptc_attr_sysfs {
> + struct dptc_priv *priv;
> + struct kobj_attribute current_value;
> + struct kobj_attribute default_value;
> + struct kobj_attribute min_value;
> + struct kobj_attribute max_value;
> + struct kobj_attribute scalar_increment;
> + struct kobj_attribute display_name;
> + struct kobj_attribute type;
> + struct attribute *attrs[8];
> + struct attribute_group group;
> + int idx;
> +};
> +
> +struct dptc_priv {
> + struct device *fw_attr_dev;
> + struct kset *fw_attr_kset;
> +
> + const struct dptc_device_limits *dev_limits;
> +
> + bool expanded;
> +
> + enum dptc_save_mode { SAVE_SINGLE, SAVE_BULK } save_mode;
Defining the enumeration separately will improve readability.
> +
> + u32 staged[DPTC_NUM_PARAMS];
> + bool has_staged[DPTC_NUM_PARAMS];
> +
> + /* Protects mutable driver state */
The comment is nonsense. To make it helpful, it should be "Protects
members above/below" with properly arranged structure layout.
> + struct mutex lock;
> +
> + struct dptc_attr_sysfs params[DPTC_NUM_PARAMS];
> + struct dptc_attr_sysfs expanded_attr;
> + struct kobj_attribute save_settings_attr;
> +};
> +
> +static struct platform_device *dptc_pdev;
> +
> +static u32 dptc_get_min(struct dptc_priv *dptc, int idx)
> +{
> + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_min
> + : dptc->dev_limits->params[idx].device_min;
> +}
> +
> +static u32 dptc_get_max(struct dptc_priv *dptc, int idx)
> +{
> + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_max
> + : dptc->dev_limits->params[idx].device_max;
> +}
> +
> +static u32 dptc_get_default(struct dptc_priv *dptc, int idx)
> +{
> + return dptc->dev_limits->params[idx].def;
> +}
> +
> +static int dptc_alib_call(const u8 *ids, const u32 *vals, int count)
> +{
> + union acpi_object in_params[2];
> + struct acpi_object_list input;
> + acpi_status status;
> + u32 buf_size;
> + int i, off;
> + u8 *buf;
> +
> + if (count == 0)
> + return -EINVAL;
Return -ENOENT as think-lmi does (see its save_settings_store()) and
update the documentation accordingly.
> +
> + /* 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);
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;
Misaligned `='.
> +
> + 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 hw_val = val * dptc_params[idx].scale;
> + u32 vals[2];
> + u8 ids[2];
> + int count = 0;
> +
> + ids[count] = dptc_params[idx].param_id;
> + vals[count] = hw_val;
> + count++;
> + if (dptc_params[idx].param_id2) {
> + ids[count] = dptc_params[idx].param_id2;
> + vals[count] = hw_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;
> + u32 hw_val;
> +
> + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> + if (!dptc->has_staged[i])
> + continue;
> +
> + hw_val = dptc->staged[i] * dptc_params[i].scale;
> + ids[count] = dptc_params[i].param_id;
> + vals[count] = hw_val;
> + count++;
> + if (dptc_params[i].param_id2) {
> + ids[count] = dptc_params[i].param_id2;
> + vals[count] = hw_val;
> + count++;
> + }
I could imagine a helper function to prevent replicating the same code
patterns twice:
static size_t dptc_alib_fill_param(u8 *ids, u32 *vals, size_t offset,
enum dptc_param_idx param, u32 val)
{
u32 hw_val = val * dptc_params[param].scale;
ids[offset] = dptc_params[param].param_id;
vals[offset++] = hw_val;
if (dptc_params[param].param_id2) {
ids[offset] = dptc_params[param].param_id2;
vals[offset++] = hw_val;
}
return offset;
}
> + }
> +
> + 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->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);
> +
> + dptc->has_staged[ps->idx] = false;
> + return count;
> + }
> +
> + ret = kstrtou32(buf, 10, &val);
> + if (ret)
> + return ret;
> +
> + guard(mutex)(&dptc->lock);
> +
> + 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"))
Please don't rely on the `count == 0' check in dptc_alib_call(). `dptc-
>save_mode == SAVE_SINGLE' should result in -EOPNOTSUPP to maintain
consistency with think-lmi (update the documentation accordingly).
> + ret = dptc_alib_save(dptc);
> + else if (sysfs_streq(buf, "single"))
> + dptc->save_mode = SAVE_SINGLE;
> + else if (sysfs_streq(buf, "bulk"))
> + dptc->save_mode = SAVE_BULK;
> + else
> + return -EINVAL;
> +
> + return ret ? ret : count;
> +}
> +
> +static ssize_t dptc_expanded_current_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + char *buf)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, current_value);
> + struct dptc_priv *dptc = ps->priv;
> +
> + guard(mutex)(&dptc->lock);
> +
> + return sysfs_emit(buf, "%d\n", dptc->expanded);
> +}
> +
> +static ssize_t dptc_expanded_current_value_store(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + const char *buf, size_t count)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, current_value);
> + struct dptc_priv *dptc = ps->priv;
> + bool val;
> + int ret;
> +
> + ret = kstrtobool(buf, &val);
> + if (ret)
> + return ret;
> +
> + guard(mutex)(&dptc->lock);
> +
> + dptc->expanded = val;
> + /* Clear staged values: limits changed, old values may be out of range */
> + memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
I'd prefer using a bitmap so that you don't need to use memset() to
clear them. It also saves some memory if more parameters are added in
the future.
> +
> + 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;
> +}
As Derek has said in v2, use macros to rearrange them.
It's painful to read all these things as I'll need to scroll the screen
quite a lot just to read something with a low information density.
That's why there are numerous macros defined in include/linux/sysfs.h.
I could imagine defining macros like DPTC{,_EXPANDED}_ATTR_{RO,RW}.
Meanwhile, I am not very sure why you would have to dynamically
initialize all these attributes. I haven't notice a blocker of defining
them statically at the module-level (see how lenovo-wmi-other did). Am
I missing anything?
> +
> +static void dptc_fw_dev_unregister(void *data)
> +{
> + device_unregister(data);
> +}
> +
> +static void dptc_kset_unregister(void *data)
> +{
> + kset_unregister(data);
> +}
> +
> +static int dptc_resume(struct device *dev)
> +{
> + struct dptc_priv *dptc = dev_get_drvdata(dev);
> + int ret;
> +
> + guard(mutex)(&dptc->lock);
> +
> + if (dptc->save_mode == SAVE_SINGLE)
> + ret = dptc_alib_save(dptc);
I don't see why bulk mode and single mode should have divergence on
resuming behavior.
Thanks,
Rong
> + 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 device *dev = &pdev->dev;
> + struct dptc_priv *dptc;
> + int i, ret;
> +
> + dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL);
> + if (!dptc)
> + return -ENOMEM;
> +
> + platform_set_drvdata(pdev, dptc);
> +
> + ret = devm_mutex_init(dev, &dptc->lock);
> + if (ret)
> + return ret;
> +
> + dptc->dev_limits = dmi_match->driver_data;
> + dev_info(dev, "%s (%s)\n", dmi_match->ident,
> + boot_cpu_data.x86_model_id);
> +
> + dptc->fw_attr_dev = device_create(&firmware_attributes_class,
> + NULL, MKDEV(0, 0), NULL,
> + DRIVER_NAME);
> + if (IS_ERR(dptc->fw_attr_dev))
> + return PTR_ERR(dptc->fw_attr_dev);
> +
> + ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister,
> + dptc->fw_attr_dev);
> + if (ret)
> + return ret;
> +
> + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL,
> + &dptc->fw_attr_dev->kobj);
> + if (!dptc->fw_attr_kset)
> + return -ENOMEM;
> +
> + ret = devm_add_action_or_reset(dev, dptc_kset_unregister,
> + dptc->fw_attr_kset);
> + if (ret)
> + return ret;
> +
> + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> + dptc_setup_param_sysfs(dptc, &dptc->params[i], i);
> + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> + &dptc->params[i].group);
> + if (ret)
> + return ret;
> + }
> +
> + dptc_setup_expanded_sysfs(dptc, &dptc->expanded_attr);
> + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> + &dptc->expanded_attr.group);
> + if (ret)
> + return ret;
> +
> + sysfs_attr_init(&dptc->save_settings_attr.attr);
> + dptc->save_settings_attr.attr.name = "save_settings";
> + dptc->save_settings_attr.attr.mode = 0644;
> + dptc->save_settings_attr.show = dptc_save_settings_show;
> + dptc->save_settings_attr.store = dptc_save_settings_store;
> + ret = sysfs_create_file(&dptc->fw_attr_kset->kobj,
> + &dptc->save_settings_attr.attr);
> + if (ret)
> + return ret;
> +
> + return 0;
> +}
> +
> +static struct platform_driver dptc_driver = {
> + .driver = {
> + .name = DRIVER_NAME,
> + .pm = pm_sleep_ptr(&dptc_pm_ops),
> + },
> + .probe = dptc_probe,
> +};
> +
> +static int __init dptc_init(void)
> +{
> + const struct dmi_system_id *match;
> + bool soc_found = false;
> + int i, ret;
> +
> + match = dmi_first_match(dptc_dmi_table);
> + if (!match)
> + return -ENODEV;
> +
> + if (!acpi_has_method(NULL, ALIB_PATH)) {
> + pr_warn("ALIB method not present\n");
> + return -ENODEV;
> + }
> +
> + for (i = 0; dptc_soc_table[i]; i++) {
> + if (strstr(boot_cpu_data.x86_model_id,
> + dptc_soc_table[i])) {
> + soc_found = true;
> + break;
> + }
> + }
> + if (!soc_found) {
> + pr_warn("unrecognized SoC '%s'\n",
> + boot_cpu_data.x86_model_id);
> + return -ENODEV;
> + }
> +
> + dptc_pdev = platform_device_register_data(NULL, DRIVER_NAME, -1,
> + match, sizeof(*match));
> + if (IS_ERR(dptc_pdev))
> + return PTR_ERR(dptc_pdev);
> +
> + ret = platform_driver_register(&dptc_driver);
> + if (ret) {
> + platform_device_unregister(dptc_pdev);
> + return ret;
> + }
> +
> + return 0;
> +}
> +
> +static void __exit dptc_exit(void)
> +{
> + platform_driver_unregister(&dptc_driver);
> + platform_device_unregister(dptc_pdev);
> +}
> +
> +module_init(dptc_init);
> +module_exit(dptc_exit);
> +
> +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>");
> +MODULE_DESCRIPTION("AMD DPTCi ACPI Driver");
> +MODULE_LICENSE("GPL");
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [RFC v3 3/4] platform/x86/amd: dptc: Add platform profile support
2026-03-07 11:55 ` [RFC v3 3/4] platform/x86/amd: dptc: Add platform profile support Antheas Kapenekakis
@ 2026-03-07 21:51 ` Rong Zhang
2026-03-07 22:45 ` Antheas Kapenekakis
0 siblings, 1 reply; 19+ messages in thread
From: Rong Zhang @ 2026-03-07 21:51 UTC (permalink / raw)
To: Antheas Kapenekakis, Mario.Limonciello
Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato,
linux-kernel, platform-driver-x86
Hi Antheas,
On Sat, 2026-03-07 at 12:55 +0100, Antheas Kapenekakis wrote:
> Register a platform_profile handler so the driver exposes standard
> power profiles (low-power, balanced, performance, max-power) alongside
> the manual tunable interface. max-power is only supplied when the
> performance profile's preset values are below the tunable limits.
>
> When a non-custom profile is active, parameter writes are blocked
> (-EPERM) and current_value reflects the profile's preset values.
> Selecting the "custom" profile returns control to the user for manual
> staging and committing. On resume, the active profile is automatically
> re-applied.
>
> Assisted-by: Claude:claude-opus-4-6
> Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> ---
> drivers/platform/x86/amd/Kconfig | 1 +
> drivers/platform/x86/amd/dptc.c | 116 +++++++++++++++++++++++++++++--
> 2 files changed, 113 insertions(+), 4 deletions(-)
>
> diff --git a/drivers/platform/x86/amd/Kconfig b/drivers/platform/x86/amd/Kconfig
> index d610092467fc..41ffbd722524 100644
> --- a/drivers/platform/x86/amd/Kconfig
> +++ b/drivers/platform/x86/amd/Kconfig
> @@ -48,6 +48,7 @@ config AMD_ISP_PLATFORM
> config AMD_DPTC
> tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)"
> depends on X86_64 && ACPI && DMI
> + select ACPI_PLATFORM_PROFILE
> select FIRMWARE_ATTRIBUTES_CLASS
> help
> Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
> index acfe9cc01bab..1f07e2e6f278 100644
> --- a/drivers/platform/x86/amd/dptc.c
> +++ b/drivers/platform/x86/amd/dptc.c
> @@ -23,6 +23,7 @@
> #include <linux/module.h>
> #include <linux/mutex.h>
> #include <linux/platform_device.h>
> +#include <linux/platform_profile.h>
> #include <linux/processor.h>
> #include <linux/sysfs.h>
> #include <linux/unaligned.h>
> @@ -56,8 +57,13 @@ struct dptc_param_limits {
> u32 expanded_max;
> };
>
> +struct dptc_profile {
> + u32 vals[DPTC_NUM_PARAMS]; /* 0 = don't set / unstage this param */
> +};
> +
> struct dptc_device_limits {
> struct dptc_param_limits params[DPTC_NUM_PARAMS];
> + struct dptc_profile profiles[PLATFORM_PROFILE_LAST];
> };
>
> struct dptc_param_desc {
> @@ -85,6 +91,10 @@ static const struct dptc_device_limits limits_maxhh = { .params = {
> [DPTC_PPT_PL2_SPPT] = { 0, 4, 27, 82, 100 },
> [DPTC_PPT_PL3_FPPT] = { 0, 4, 40, 85, 100 },
> [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 },
> +}, .profiles = {
> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 15, 15, 25, 0 } },
> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 25, 27, 40, 0 } },
> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 60, 63, 85, 0 } },
> }};
>
> /* Substring matches against boot_cpu_data.x86_model_id; order matters. */
> @@ -134,6 +144,9 @@ struct dptc_priv {
>
> 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];
> @@ -269,6 +282,14 @@ static ssize_t dptc_current_value_show(struct kobject *kobj,
>
> guard(mutex)(&dptc->lock);
>
> + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
> + u32 val = dptc->dev_limits->profiles[dptc->profile].vals[ps->idx];
> +
> + if (!val)
> + return sysfs_emit(buf, "\n");
> + return sysfs_emit(buf, "%u\n", val);
> + }
> +
> if (!dptc->has_staged[ps->idx])
> return sysfs_emit(buf, "\n");
> return sysfs_emit(buf, "%u\n", dptc->staged[ps->idx]);
> @@ -287,6 +308,8 @@ static ssize_t dptc_current_value_store(struct kobject *kobj,
> if (count == 1 && buf[0] == '\n') {
> guard(mutex)(&dptc->lock);
>
> + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> + return -EPERM;
It has nothing to do with permissions. I'd prefer -EBUSY.
To maintain readability, usually a blank line should be added after
returning early.
> dptc->has_staged[ps->idx] = false;
> return count;
> }
> @@ -297,6 +320,8 @@ static ssize_t dptc_current_value_store(struct kobject *kobj,
>
> 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))
> @@ -426,6 +451,8 @@ static ssize_t dptc_expanded_current_value_store(struct kobject *kobj,
>
> guard(mutex)(&dptc->lock);
>
> + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> + return -EPERM;
IMO kstrtou32() isn't really a blocker of acquiring the mutex earlier
to prevent duplicating the same check. After all, firmware-attributes
is not a performance critical interface, and dptc_save_settings_store()
also calls sysfs_eq() with the mutex held.
> dptc->expanded = val;
> /* Clear staged values: limits changed, old values may be out of range */
> memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
> @@ -594,6 +621,78 @@ 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);
I'd prefer simply calling dptc_alib_save() in dptc_apply_profile()
since the latter already checks the profile.
> +
> + 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);
> @@ -601,10 +700,14 @@ static int dptc_resume(struct device *dev)
>
> guard(mutex)(&dptc->lock);
>
> - if (dptc->save_mode == SAVE_SINGLE)
> + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
> + dptc_apply_profile(dptc, dptc->profile);
> ret = dptc_alib_save(dptc);
> - else
> + } 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);
> @@ -636,8 +739,7 @@ static int dptc_probe(struct platform_device *pdev)
> boot_cpu_data.x86_model_id);
>
> dptc->fw_attr_dev = device_create(&firmware_attributes_class,
> - NULL, MKDEV(0, 0), NULL,
> - DRIVER_NAME);
> + NULL, MKDEV(0, 0), NULL, DRIVER_NAME);
Squash the change into the previous patch.
Thanks,
Rong
> if (IS_ERR(dptc->fw_attr_dev))
> return PTR_ERR(dptc->fw_attr_dev);
>
> @@ -680,6 +782,12 @@ static int dptc_probe(struct platform_device *pdev)
> if (ret)
> return ret;
>
> + dptc->profile = PLATFORM_PROFILE_CUSTOM;
> + dptc->ppdev = devm_platform_profile_register(dev, "amd-dptc", dptc,
> + &dptc_pp_ops);
> + if (IS_ERR(dptc->ppdev))
> + return PTR_ERR(dptc->ppdev);
> +
> return 0;
> }
>
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [RFC v3 4/4] platform/x86/amd: dptc: Add device entries for handheld PCs
2026-03-07 11:55 ` [RFC v3 4/4] platform/x86/amd: dptc: Add device entries for handheld PCs Antheas Kapenekakis
@ 2026-03-07 21:51 ` Rong Zhang
2026-03-07 22:40 ` Antheas Kapenekakis
0 siblings, 1 reply; 19+ messages in thread
From: Rong Zhang @ 2026-03-07 21:51 UTC (permalink / raw)
To: Antheas Kapenekakis, Mario.Limonciello
Cc: W_Armin, sashal, Shyam-Sundar.S-k, derekjohn.clark, denis.benato,
linux-kernel, platform-driver-x86
Hi Antheas,
On Sat, 2026-03-07 at 12:55 +0100, Antheas Kapenekakis wrote:
> Add limit tables and DMI entries for AMD APU-based handheld devices:
>
> - 15W class: AYANEO AIR
> - 18W class: AYANEO AIR Plus/Pro
> - 25W class: AYANEO AIR 1S, NEXT/KUN
> - 28W class: GPD Win Mini/4/Max 2, GPD Duo, GPD Pocket 4, AYANEO 2,
> OrangePi NEO-01, SuiPlay0X1
> - 30W class: AYANEO FLIP/GEEK/SLIDE/3, AOKZOE A1/A1X/A2,
> OneXPlayer F1Pro/2/X1/G1, AYN Loki Max, Zeenix Pro
>
> Also add SoC strings for Ryzen AI, Z1, Ryzen 8000/7040/6000/5000
> families, extending support beyond the initial AI MAX series.
>
> Assisted-by: Claude:claude-opus-4-6
> Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> ---
> drivers/platform/x86/amd/dptc.c | 409 ++++++++++++++++++++++++++++++++
> 1 file changed, 409 insertions(+)
>
> diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
> index 1f07e2e6f278..a3d582e48cd3 100644
> --- a/drivers/platform/x86/amd/dptc.c
> +++ b/drivers/platform/x86/amd/dptc.c
> @@ -85,6 +85,66 @@ static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = {
> 1, ALIB_ID_TEMP_TARGET },
> };
>
> +/* 15W class: AYANEO AIR (Ryzen 5 5560U) */
> +static const struct dptc_device_limits limits_15w = { .params = {
> + [DPTC_PPT_PL1_SPL] = { 0, 5, 10, 15, 18 },
> + [DPTC_PPT_PL2_SPPT] = { 0, 5, 10, 15, 18 },
> + [DPTC_PPT_PL3_FPPT] = { 0, 5, 12, 18, 22 },
> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> +}, .profiles = {
> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } },
> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 10, 12, 15, 0 } },
> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 15, 15, 18, 0 } },
> +}};
> +
> +/* 18W class: AYANEO AIR Plus/Pro (Ryzen 5 5560U, Ryzen 7 5825U) */
> +static const struct dptc_device_limits limits_18w = { .params = {
> + [DPTC_PPT_PL1_SPL] = { 0, 5, 15, 18, 22 },
> + [DPTC_PPT_PL2_SPPT] = { 0, 5, 15, 18, 22 },
> + [DPTC_PPT_PL3_FPPT] = { 0, 5, 15, 20, 25 },
> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> +}, .profiles = {
> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } },
> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 12, 14, 15, 0 } },
> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 18, 18, 20, 0 } },
> +}};
> +
> +/* 25W class: Ryzen 5000 handhelds (AYANEO NEXT, KUN) */
> +static const struct dptc_device_limits limits_25w = { .params = {
> + [DPTC_PPT_PL1_SPL] = { 0, 4, 15, 25, 32 },
> + [DPTC_PPT_PL2_SPPT] = { 0, 4, 20, 27, 35 },
> + [DPTC_PPT_PL3_FPPT] = { 0, 4, 25, 30, 37 },
> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> +}, .profiles = {
> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } },
> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 20, 0 } },
> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 27, 30, 0 } },
> +}};
> +
> +/* 28W class: GPD Win series, AYANEO 2, OrangePi NEO-01 */
> +static const struct dptc_device_limits limits_28w = { .params = {
> + [DPTC_PPT_PL1_SPL] = { 0, 4, 15, 28, 32 },
> + [DPTC_PPT_PL2_SPPT] = { 0, 4, 20, 30, 35 },
> + [DPTC_PPT_PL3_FPPT] = { 0, 4, 25, 32, 37 },
> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> +}, .profiles = {
> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } },
> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 22, 0 } },
> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 32, 0 } },
> +}};
> +
> +/* 30W class: OneXPlayer, AYANEO FLIP/GEEK/SLIDE/3, AOKZOE */
> +static const struct dptc_device_limits limits_30w = { .params = {
> + [DPTC_PPT_PL1_SPL] = { 0, 4, 15, 30, 40 },
> + [DPTC_PPT_PL2_SPPT] = { 0, 4, 20, 32, 43 },
> + [DPTC_PPT_PL3_FPPT] = { 0, 4, 25, 41, 50 },
> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> +}, .profiles = {
> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } },
> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 25, 0 } },
> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 41, 0 } },
> +}};
> +
> /* AI MAX Handheld class: GPD Win 5 */
> static const struct dptc_device_limits limits_maxhh = { .params = {
> [DPTC_PPT_PL1_SPL] = { 0, 4, 25, 80, 100 },
> @@ -103,11 +163,60 @@ static const char * const dptc_soc_table[] = {
> "AMD RYZEN AI MAX+ 395",
> "AMD RYZEN AI MAX+ 385",
> "AMD RYZEN AI MAX 380",
> + /* Ryzen AI */
> + "AMD Ryzen AI 9 HX 370",
> + "AMD Ryzen AI HX 360",
What's "AMD Ryzen AI HX 360"? Did you mean "AMD Ryzen AI 9 H 365"
(Chinese-specific model id) and "AMD Ryzen AI 9 365" (other regions)?
> + /* Z1 - Extreme before plain Z1 */
> + "AMD Ryzen Z1 Extreme",
> + "AMD Ryzen Z1",
You don't need both since strstr("AMD Ryzen Z1") matches "AMD Ryzen Z1
Extreme".
Thanks,
Rong
> + /* 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 = {
> @@ -116,6 +225,306 @@ static const struct dmi_system_id dptc_dmi_table[] = {
> },
> .driver_data = (void *)&limits_maxhh,
> },
> + {
> + .ident = "GPD Win Max 2",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-04"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Win Max 2 2024",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-05"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Duo",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Duo",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01-L"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Pocket 4",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Pocket 4",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04-L"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + /* OrangePi */
> + {
> + .ident = "OrangePi NEO-01",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + /* AYN */
> + {
> + .ident = "AYN Loki Max",
> + .matches = {
> + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ayn"),
> + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Loki Max"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + /* Tectoy (Zeenix Pro = Loki Max) */
> + {
> + .ident = "Zeenix Pro",
> + .matches = {
> + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Tectoy"),
> + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Zeenix Pro"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + /* AOKZOE */
> + {
> + .ident = "AOKZOE A1 AR07",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 AR07"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "AOKZOE A1 Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 Pro"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "AOKZOE A1X",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1X"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "AOKZOE A2 Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A2 Pro"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + /* OneXPlayer (Intel variants filtered by SoC table) */
> + {
> + .ident = "ONEXPLAYER F1Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1Pro"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER F1 EVA-02",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1 EVA-02"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER 2",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_MATCH(DMI_BOARD_NAME, "ONEXPLAYER 2"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER X1 A",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1 A"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER X1z",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1z"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER X1Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Pro"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER G1 A",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 A"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + /* AYANEO - 15W */
> + {
> + .ident = "AYANEO AIR",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR"),
> + },
> + .driver_data = (void *)&limits_15w,
> + },
> + /* AYANEO - 18W */
> + {
> + .ident = "AYANEO AIR Plus",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Plus"),
> + },
> + .driver_data = (void *)&limits_18w,
> + },
> + {
> + .ident = "AYANEO AIR Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Pro"),
> + },
> + .driver_data = (void *)&limits_18w,
> + },
> + /* AYANEO - 25W */
> + {
> + .ident = "AYANEO AIR 1S",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_MATCH(DMI_BOARD_NAME, "AIR 1S"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + {
> + .ident = "AYANEO NEXT Advance",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Advance"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + {
> + .ident = "AYANEO NEXT Lite",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Lite"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + {
> + .ident = "AYANEO NEXT Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Pro"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + {
> + .ident = "AYANEO NEXT",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + {
> + .ident = "AYANEO KUN",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + {
> + .ident = "AYANEO KUN",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO KUN"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + /* AYANEO - 28W */
> + {
> + .ident = "AYANEO 2",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 2"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "SuiPlay0X1",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "Mysten Labs, Inc."),
> + DMI_MATCH(DMI_PRODUCT_NAME, "SuiPlay0X1"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + /* AYANEO - 30W */
> + {
> + /* DMI_MATCH catches all FLIP variants (DS, KB, 1S DS, 1S KB) */
> + .ident = "AYANEO FLIP",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_MATCH(DMI_BOARD_NAME, "FLIP"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + /* DMI_MATCH catches GEEK and GEEK 1S */
> + .ident = "AYANEO GEEK",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_MATCH(DMI_BOARD_NAME, "GEEK"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "AYANEO SLIDE",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "SLIDE"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "AYANEO 3",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO 3"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> { }
> };
> MODULE_DEVICE_TABLE(dmi, dptc_dmi_table);
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [RFC v3 4/4] platform/x86/amd: dptc: Add device entries for handheld PCs
2026-03-07 21:51 ` Rong Zhang
@ 2026-03-07 22:40 ` Antheas Kapenekakis
2026-03-08 9:23 ` Rong Zhang
0 siblings, 1 reply; 19+ messages in thread
From: Antheas Kapenekakis @ 2026-03-07 22:40 UTC (permalink / raw)
To: Rong Zhang
Cc: Mario.Limonciello, W_Armin, sashal, Shyam-Sundar.S-k,
derekjohn.clark, denis.benato, linux-kernel, platform-driver-x86
On Sat, 7 Mar 2026 at 22:57, Rong Zhang <i@rong.moe> wrote:
>
> Hi Antheas,
>
> On Sat, 2026-03-07 at 12:55 +0100, Antheas Kapenekakis wrote:
> > Add limit tables and DMI entries for AMD APU-based handheld devices:
> >
> > - 15W class: AYANEO AIR
> > - 18W class: AYANEO AIR Plus/Pro
> > - 25W class: AYANEO AIR 1S, NEXT/KUN
> > - 28W class: GPD Win Mini/4/Max 2, GPD Duo, GPD Pocket 4, AYANEO 2,
> > OrangePi NEO-01, SuiPlay0X1
> > - 30W class: AYANEO FLIP/GEEK/SLIDE/3, AOKZOE A1/A1X/A2,
> > OneXPlayer F1Pro/2/X1/G1, AYN Loki Max, Zeenix Pro
> >
> > Also add SoC strings for Ryzen AI, Z1, Ryzen 8000/7040/6000/5000
> > families, extending support beyond the initial AI MAX series.
> >
> > Assisted-by: Claude:claude-opus-4-6
> > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> > ---
> > drivers/platform/x86/amd/dptc.c | 409 ++++++++++++++++++++++++++++++++
> > 1 file changed, 409 insertions(+)
> >
> > diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
> > index 1f07e2e6f278..a3d582e48cd3 100644
> > --- a/drivers/platform/x86/amd/dptc.c
> > +++ b/drivers/platform/x86/amd/dptc.c
> > @@ -85,6 +85,66 @@ static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = {
> > 1, ALIB_ID_TEMP_TARGET },
> > };
> >
> > +/* 15W class: AYANEO AIR (Ryzen 5 5560U) */
> > +static const struct dptc_device_limits limits_15w = { .params = {
> > + [DPTC_PPT_PL1_SPL] = { 0, 5, 10, 15, 18 },
> > + [DPTC_PPT_PL2_SPPT] = { 0, 5, 10, 15, 18 },
> > + [DPTC_PPT_PL3_FPPT] = { 0, 5, 12, 18, 22 },
> > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > +}, .profiles = {
> > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } },
> > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 10, 12, 15, 0 } },
> > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 15, 15, 18, 0 } },
> > +}};
> > +
> > +/* 18W class: AYANEO AIR Plus/Pro (Ryzen 5 5560U, Ryzen 7 5825U) */
> > +static const struct dptc_device_limits limits_18w = { .params = {
> > + [DPTC_PPT_PL1_SPL] = { 0, 5, 15, 18, 22 },
> > + [DPTC_PPT_PL2_SPPT] = { 0, 5, 15, 18, 22 },
> > + [DPTC_PPT_PL3_FPPT] = { 0, 5, 15, 20, 25 },
> > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > +}, .profiles = {
> > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } },
> > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 12, 14, 15, 0 } },
> > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 18, 18, 20, 0 } },
> > +}};
> > +
> > +/* 25W class: Ryzen 5000 handhelds (AYANEO NEXT, KUN) */
> > +static const struct dptc_device_limits limits_25w = { .params = {
> > + [DPTC_PPT_PL1_SPL] = { 0, 4, 15, 25, 32 },
> > + [DPTC_PPT_PL2_SPPT] = { 0, 4, 20, 27, 35 },
> > + [DPTC_PPT_PL3_FPPT] = { 0, 4, 25, 30, 37 },
> > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > +}, .profiles = {
> > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } },
> > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 20, 0 } },
> > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 27, 30, 0 } },
> > +}};
> > +
> > +/* 28W class: GPD Win series, AYANEO 2, OrangePi NEO-01 */
> > +static const struct dptc_device_limits limits_28w = { .params = {
> > + [DPTC_PPT_PL1_SPL] = { 0, 4, 15, 28, 32 },
> > + [DPTC_PPT_PL2_SPPT] = { 0, 4, 20, 30, 35 },
> > + [DPTC_PPT_PL3_FPPT] = { 0, 4, 25, 32, 37 },
> > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > +}, .profiles = {
> > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } },
> > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 22, 0 } },
> > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 32, 0 } },
> > +}};
> > +
> > +/* 30W class: OneXPlayer, AYANEO FLIP/GEEK/SLIDE/3, AOKZOE */
> > +static const struct dptc_device_limits limits_30w = { .params = {
> > + [DPTC_PPT_PL1_SPL] = { 0, 4, 15, 30, 40 },
> > + [DPTC_PPT_PL2_SPPT] = { 0, 4, 20, 32, 43 },
> > + [DPTC_PPT_PL3_FPPT] = { 0, 4, 25, 41, 50 },
> > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > +}, .profiles = {
> > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } },
> > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 25, 0 } },
> > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 41, 0 } },
> > +}};
> > +
> > /* AI MAX Handheld class: GPD Win 5 */
> > static const struct dptc_device_limits limits_maxhh = { .params = {
> > [DPTC_PPT_PL1_SPL] = { 0, 4, 25, 80, 100 },
> > @@ -103,11 +163,60 @@ static const char * const dptc_soc_table[] = {
> > "AMD RYZEN AI MAX+ 395",
> > "AMD RYZEN AI MAX+ 385",
> > "AMD RYZEN AI MAX 380",
> > + /* Ryzen AI */
> > + "AMD Ryzen AI 9 HX 370",
> > + "AMD Ryzen AI HX 360",
>
> What's "AMD Ryzen AI HX 360"? Did you mean "AMD Ryzen AI 9 H 365"
> (Chinese-specific model id) and "AMD Ryzen AI 9 365" (other regions)?
Hm, goot point. I seem to have added this preemptively looking at my
commit logs earlier this year.
It might not be the exact name. Since we do SoC + DMI in this driver
it might cause some devices with the chip to fail to load.
Some HX360 handhelds did ship I think, but I never got bug reports
about it missing. I want to say 360 is ryzen 7, at least looking
online. Are you sure it's 9?
Antheas
> > + /* Z1 - Extreme before plain Z1 */
> > + "AMD Ryzen Z1 Extreme",
> > + "AMD Ryzen Z1",
>
> You don't need both since strstr("AMD Ryzen Z1") matches "AMD Ryzen Z1
> Extreme".
>
> Thanks,
> Rong
>
> > + /* 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 = {
> > @@ -116,6 +225,306 @@ static const struct dmi_system_id dptc_dmi_table[] = {
> > },
> > .driver_data = (void *)&limits_maxhh,
> > },
> > + {
> > + .ident = "GPD Win Max 2",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-04"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Win Max 2 2024",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-05"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Duo",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Duo",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01-L"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Pocket 4",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "GPD Pocket 4",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04-L"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + /* OrangePi */
> > + {
> > + .ident = "OrangePi NEO-01",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + /* AYN */
> > + {
> > + .ident = "AYN Loki Max",
> > + .matches = {
> > + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ayn"),
> > + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Loki Max"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + /* Tectoy (Zeenix Pro = Loki Max) */
> > + {
> > + .ident = "Zeenix Pro",
> > + .matches = {
> > + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Tectoy"),
> > + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Zeenix Pro"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + /* AOKZOE */
> > + {
> > + .ident = "AOKZOE A1 AR07",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 AR07"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "AOKZOE A1 Pro",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 Pro"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "AOKZOE A1X",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1X"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "AOKZOE A2 Pro",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A2 Pro"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + /* OneXPlayer (Intel variants filtered by SoC table) */
> > + {
> > + .ident = "ONEXPLAYER F1Pro",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1Pro"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "ONEXPLAYER F1 EVA-02",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1 EVA-02"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "ONEXPLAYER 2",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > + DMI_MATCH(DMI_BOARD_NAME, "ONEXPLAYER 2"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "ONEXPLAYER X1 A",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1 A"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "ONEXPLAYER X1z",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1z"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "ONEXPLAYER X1Pro",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Pro"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "ONEXPLAYER G1 A",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 A"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + /* AYANEO - 15W */
> > + {
> > + .ident = "AYANEO AIR",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR"),
> > + },
> > + .driver_data = (void *)&limits_15w,
> > + },
> > + /* AYANEO - 18W */
> > + {
> > + .ident = "AYANEO AIR Plus",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Plus"),
> > + },
> > + .driver_data = (void *)&limits_18w,
> > + },
> > + {
> > + .ident = "AYANEO AIR Pro",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Pro"),
> > + },
> > + .driver_data = (void *)&limits_18w,
> > + },
> > + /* AYANEO - 25W */
> > + {
> > + .ident = "AYANEO AIR 1S",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_MATCH(DMI_BOARD_NAME, "AIR 1S"),
> > + },
> > + .driver_data = (void *)&limits_25w,
> > + },
> > + {
> > + .ident = "AYANEO NEXT Advance",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Advance"),
> > + },
> > + .driver_data = (void *)&limits_25w,
> > + },
> > + {
> > + .ident = "AYANEO NEXT Lite",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Lite"),
> > + },
> > + .driver_data = (void *)&limits_25w,
> > + },
> > + {
> > + .ident = "AYANEO NEXT Pro",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Pro"),
> > + },
> > + .driver_data = (void *)&limits_25w,
> > + },
> > + {
> > + .ident = "AYANEO NEXT",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT"),
> > + },
> > + .driver_data = (void *)&limits_25w,
> > + },
> > + {
> > + .ident = "AYANEO KUN",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"),
> > + },
> > + .driver_data = (void *)&limits_25w,
> > + },
> > + {
> > + .ident = "AYANEO KUN",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO KUN"),
> > + },
> > + .driver_data = (void *)&limits_25w,
> > + },
> > + /* AYANEO - 28W */
> > + {
> > + .ident = "AYANEO 2",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 2"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + {
> > + .ident = "SuiPlay0X1",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "Mysten Labs, Inc."),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "SuiPlay0X1"),
> > + },
> > + .driver_data = (void *)&limits_28w,
> > + },
> > + /* AYANEO - 30W */
> > + {
> > + /* DMI_MATCH catches all FLIP variants (DS, KB, 1S DS, 1S KB) */
> > + .ident = "AYANEO FLIP",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_MATCH(DMI_BOARD_NAME, "FLIP"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + /* DMI_MATCH catches GEEK and GEEK 1S */
> > + .ident = "AYANEO GEEK",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_MATCH(DMI_BOARD_NAME, "GEEK"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "AYANEO SLIDE",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "SLIDE"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > + {
> > + .ident = "AYANEO 3",
> > + .matches = {
> > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO 3"),
> > + },
> > + .driver_data = (void *)&limits_30w,
> > + },
> > { }
> > };
> > MODULE_DEVICE_TABLE(dmi, dptc_dmi_table);
>
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [RFC v3 3/4] platform/x86/amd: dptc: Add platform profile support
2026-03-07 21:51 ` Rong Zhang
@ 2026-03-07 22:45 ` Antheas Kapenekakis
2026-03-08 9:30 ` Rong Zhang
0 siblings, 1 reply; 19+ messages in thread
From: Antheas Kapenekakis @ 2026-03-07 22:45 UTC (permalink / raw)
To: Rong Zhang
Cc: Mario.Limonciello, W_Armin, sashal, Shyam-Sundar.S-k,
derekjohn.clark, denis.benato, linux-kernel, platform-driver-x86
On Sat, 7 Mar 2026 at 22:57, Rong Zhang <i@rong.moe> wrote:
>
> Hi Antheas,
>
> On Sat, 2026-03-07 at 12:55 +0100, Antheas Kapenekakis wrote:
> > Register a platform_profile handler so the driver exposes standard
> > power profiles (low-power, balanced, performance, max-power) alongside
> > the manual tunable interface. max-power is only supplied when the
> > performance profile's preset values are below the tunable limits.
> >
> > When a non-custom profile is active, parameter writes are blocked
> > (-EPERM) and current_value reflects the profile's preset values.
> > Selecting the "custom" profile returns control to the user for manual
> > staging and committing. On resume, the active profile is automatically
> > re-applied.
> >
> > Assisted-by: Claude:claude-opus-4-6
> > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> > ---
> > drivers/platform/x86/amd/Kconfig | 1 +
> > drivers/platform/x86/amd/dptc.c | 116 +++++++++++++++++++++++++++++--
> > 2 files changed, 113 insertions(+), 4 deletions(-)
> >
> > diff --git a/drivers/platform/x86/amd/Kconfig b/drivers/platform/x86/amd/Kconfig
> > index d610092467fc..41ffbd722524 100644
> > --- a/drivers/platform/x86/amd/Kconfig
> > +++ b/drivers/platform/x86/amd/Kconfig
> > @@ -48,6 +48,7 @@ config AMD_ISP_PLATFORM
> > config AMD_DPTC
> > tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)"
> > depends on X86_64 && ACPI && DMI
> > + select ACPI_PLATFORM_PROFILE
> > select FIRMWARE_ATTRIBUTES_CLASS
> > help
> > Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> > diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
> > index acfe9cc01bab..1f07e2e6f278 100644
> > --- a/drivers/platform/x86/amd/dptc.c
> > +++ b/drivers/platform/x86/amd/dptc.c
> > @@ -23,6 +23,7 @@
> > #include <linux/module.h>
> > #include <linux/mutex.h>
> > #include <linux/platform_device.h>
> > +#include <linux/platform_profile.h>
> > #include <linux/processor.h>
> > #include <linux/sysfs.h>
> > #include <linux/unaligned.h>
> > @@ -56,8 +57,13 @@ struct dptc_param_limits {
> > u32 expanded_max;
> > };
> >
> > +struct dptc_profile {
> > + u32 vals[DPTC_NUM_PARAMS]; /* 0 = don't set / unstage this param */
> > +};
> > +
> > struct dptc_device_limits {
> > struct dptc_param_limits params[DPTC_NUM_PARAMS];
> > + struct dptc_profile profiles[PLATFORM_PROFILE_LAST];
> > };
> >
> > struct dptc_param_desc {
> > @@ -85,6 +91,10 @@ static const struct dptc_device_limits limits_maxhh = { .params = {
> > [DPTC_PPT_PL2_SPPT] = { 0, 4, 27, 82, 100 },
> > [DPTC_PPT_PL3_FPPT] = { 0, 4, 40, 85, 100 },
> > [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 },
> > +}, .profiles = {
> > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 15, 15, 25, 0 } },
> > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 25, 27, 40, 0 } },
> > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 60, 63, 85, 0 } },
> > }};
> >
> > /* Substring matches against boot_cpu_data.x86_model_id; order matters. */
> > @@ -134,6 +144,9 @@ struct dptc_priv {
> >
> > 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];
> > @@ -269,6 +282,14 @@ static ssize_t dptc_current_value_show(struct kobject *kobj,
> >
> > guard(mutex)(&dptc->lock);
> >
> > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
> > + u32 val = dptc->dev_limits->profiles[dptc->profile].vals[ps->idx];
> > +
> > + if (!val)
> > + return sysfs_emit(buf, "\n");
> > + return sysfs_emit(buf, "%u\n", val);
> > + }
> > +
> > if (!dptc->has_staged[ps->idx])
> > return sysfs_emit(buf, "\n");
> > return sysfs_emit(buf, "%u\n", dptc->staged[ps->idx]);
> > @@ -287,6 +308,8 @@ static ssize_t dptc_current_value_store(struct kobject *kobj,
> > if (count == 1 && buf[0] == '\n') {
> > guard(mutex)(&dptc->lock);
> >
> > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> > + return -EPERM;
>
> It has nothing to do with permissions. I'd prefer -EBUSY.
I will defer to other drivers and update if necessary based on their choices.
> To maintain readability, usually a blank line should be added after
> returning early.
>
> > dptc->has_staged[ps->idx] = false;
> > return count;
> > }
> > @@ -297,6 +320,8 @@ static ssize_t dptc_current_value_store(struct kobject *kobj,
> >
> > 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))
> > @@ -426,6 +451,8 @@ static ssize_t dptc_expanded_current_value_store(struct kobject *kobj,
> >
> > guard(mutex)(&dptc->lock);
> >
> > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> > + return -EPERM;
>
> IMO kstrtou32() isn't really a blocker of acquiring the mutex earlier
> to prevent duplicating the same check. After all, firmware-attributes
> is not a performance critical interface, and dptc_save_settings_store()
> also calls sysfs_eq() with the mutex held.
>
> > dptc->expanded = val;
> > /* Clear staged values: limits changed, old values may be out of range */
> > memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
> > @@ -594,6 +621,78 @@ 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);
>
> I'd prefer simply calling dptc_alib_save() in dptc_apply_profile()
> since the latter already checks the profile.
This way you will propagate an error because custom unstages all values.
Antheas
> > +
> > + 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);
> > @@ -601,10 +700,14 @@ static int dptc_resume(struct device *dev)
> >
> > guard(mutex)(&dptc->lock);
> >
> > - if (dptc->save_mode == SAVE_SINGLE)
> > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
> > + dptc_apply_profile(dptc, dptc->profile);
> > ret = dptc_alib_save(dptc);
> > - else
> > + } 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);
> > @@ -636,8 +739,7 @@ static int dptc_probe(struct platform_device *pdev)
> > boot_cpu_data.x86_model_id);
> >
> > dptc->fw_attr_dev = device_create(&firmware_attributes_class,
> > - NULL, MKDEV(0, 0), NULL,
> > - DRIVER_NAME);
> > + NULL, MKDEV(0, 0), NULL, DRIVER_NAME);
>
> Squash the change into the previous patch.
>
> Thanks,
> Rong
>
> > if (IS_ERR(dptc->fw_attr_dev))
> > return PTR_ERR(dptc->fw_attr_dev);
> >
> > @@ -680,6 +782,12 @@ static int dptc_probe(struct platform_device *pdev)
> > if (ret)
> > return ret;
> >
> > + dptc->profile = PLATFORM_PROFILE_CUSTOM;
> > + dptc->ppdev = devm_platform_profile_register(dev, "amd-dptc", dptc,
> > + &dptc_pp_ops);
> > + if (IS_ERR(dptc->ppdev))
> > + return PTR_ERR(dptc->ppdev);
> > +
> > return 0;
> > }
> >
>
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [RFC v3 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver
2026-03-07 21:50 ` Rong Zhang
@ 2026-03-07 23:02 ` Antheas Kapenekakis
2026-03-07 23:08 ` Antheas Kapenekakis
0 siblings, 1 reply; 19+ messages in thread
From: Antheas Kapenekakis @ 2026-03-07 23:02 UTC (permalink / raw)
To: Rong Zhang
Cc: Mario.Limonciello, W_Armin, sashal, Shyam-Sundar.S-k,
derekjohn.clark, denis.benato, linux-kernel, platform-driver-x86
On Sat, 7 Mar 2026 at 22:56, Rong Zhang <i@rong.moe> wrote:
>
> Hi Antheas,
>
> Thanks for working on it.
>
> I think this driver is useful for me and I will see if I can test it on
> my devices once I find some time. They're not handhelds but I am sure
> at least one of them has the ALIB method. It'd better if a module
> option can be used to bypass the quirk table so that testing against a
> new device will be easier. Such an option should taint the kernel ofc.
I will defer to Mario for this one. Previous iterations of this series
had an override. Before I posted V2 I had a module param that allowed
matching by ident.
> That being said, I have some review comments. Please see below and my
> following replies.
>
> On Sat, 2026-03-07 at 12:55 +0100, Antheas Kapenekakis wrote:
> > Add a driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> > Thermal Configuration interface (DPTCi). This exposes TDP and thermal
> > parameters for AMD APU-based handheld devices via the
> > firmware-attributes sysfs ABI.
> >
> > Parameters are staged and atomically committed through ALIB. The driver
> > supports two save modes: "single" (apply immediately on write) and
> > "bulk" (stage values, then commit with "save"). An "expanded_limits"
> > toggle widens the allowed parameter ranges beyond device defaults.
> >
> > Initial device support: GPD Win 5 (AMD Ryzen AI MAX).
> >
> > Assisted-by: Claude:claude-opus-4-6
> > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> > ---
> > MAINTAINERS | 6 +
> > drivers/platform/x86/amd/Kconfig | 14 +
> > drivers/platform/x86/amd/Makefile | 2 +
> > drivers/platform/x86/amd/dptc.c | 747 ++++++++++++++++++++++++++++++
> > 4 files changed, 769 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..d610092467fc 100644
> > --- a/drivers/platform/x86/amd/Kconfig
> > +++ b/drivers/platform/x86/amd/Kconfig
> > @@ -44,3 +44,17 @@ config AMD_ISP_PLATFORM
> >
> > This driver can also be built as a module. If so, the module
> > will be called amd_isp4.
> > +
> > +config AMD_DPTC
> > + tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)"
> > + depends on X86_64 && ACPI && DMI
> > + select FIRMWARE_ATTRIBUTES_CLASS
> > + help
> > + Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> > + Thermal Configuration Interface (DPTCi). Exposes TDP and thermal
> > + parameters for AMD APU-based handheld devices via the
> > + firmware-attributes sysfs ABI, allowing userspace tools to stage
> > + and atomically commit power limit settings. Requires a DMI match
> > + for the device and a recognized AMD SoC.
> > +
> > + If built as a module, the module will be called amd_dptc.
> > diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile
> > index f6ff0c837f34..862a609bfe38 100644
> > --- a/drivers/platform/x86/amd/Makefile
> > +++ b/drivers/platform/x86/amd/Makefile
> > @@ -12,3 +12,5 @@ obj-$(CONFIG_AMD_PMF) += pmf/
> > obj-$(CONFIG_AMD_WBRF) += wbrf.o
> > obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o
> > obj-$(CONFIG_AMD_HFI) += hfi/
> > +obj-$(CONFIG_AMD_DPTC) += amd_dptc.o
> > +amd_dptc-y := dptc.o
> > diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
> > new file mode 100644
> > index 000000000000..acfe9cc01bab
> > --- /dev/null
> > +++ b/drivers/platform/x86/amd/dptc.c
> > @@ -0,0 +1,747 @@
> > +// SPDX-License-Identifier: GPL-2.0-only
> > +/*
> > + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver
> > + *
> > + * Exposes AMD APU power and thermal parameters via the firmware-attributes
> > + * sysfs ABI. Parameters are staged and atomically committed through the
> > + * AGESA ALIB Function 0x0C (Dynamic Power and Thermal Configuration
> > + * interface).
> > + *
> > + * Reference: AMD AGESA Publication #44065, Appendix E.5
> > + * https://docs.amd.com/v/u/en-US/44065_Arch2008
> > + *
> > + * Copyright (C) 2026 Antheas Kapenekakis <lkml@antheas.dev>
> > + */
> > +
> > +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> > +
> > +#include <linux/acpi.h>
> > +#include <linux/cleanup.h>
> > +#include <linux/dmi.h>
> > +#include <linux/init.h>
> > +#include <linux/kobject.h>
> > +#include <linux/module.h>
> > +#include <linux/mutex.h>
> > +#include <linux/platform_device.h>
> > +#include <linux/processor.h>
> > +#include <linux/sysfs.h>
> > +#include <linux/unaligned.h>
> > +
> > +#include "../firmware_attributes_class.h"
> > +
> > +#define DRIVER_NAME "amd-dptc"
> > +
> > +#define ALIB_FUNC_DPTC 0x0C
> > +#define ALIB_PATH "\\_SB.ALIB"
> > +
> > +#define ALIB_ID_TEMP_TARGET 0x03
> > +#define ALIB_ID_STAPM_LIMIT 0x05
> > +#define ALIB_ID_FAST_LIMIT 0x06
> > +#define ALIB_ID_SLOW_LIMIT 0x07
> > +#define ALIB_ID_SKIN_LIMIT 0x2E
> > +
> > +enum dptc_param_idx {
> > + DPTC_PPT_PL1_SPL, /* STAPM + skin limit (set together) */
> > + DPTC_PPT_PL2_SPPT, /* slow PPT limit */
> > + DPTC_PPT_PL3_FPPT, /* fast PPT limit */
> > + DPTC_CPU_TEMP, /* thermal control target */
> > + DPTC_NUM_PARAMS,
> > +};
> > +
> > +struct dptc_param_limits {
> > + u32 expanded_min;
> > + u32 device_min;
> > + u32 def;
> > + u32 device_max;
> > + u32 expanded_max;
> > +};
> > +
> > +struct dptc_device_limits {
> > + struct dptc_param_limits params[DPTC_NUM_PARAMS];
> > +};
> > +
> > +struct dptc_param_desc {
> > + const char *name;
> > + const char *display_name;
> > + u16 scale; /* sysfs-to-ALIB multiplier (e.g. 1000 for W->mW) */
> > + u8 param_id;
> > + u8 param_id2; /* secondary ALIB ID, 0 if none */
> > +};
> > +
> > +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = {
> > + [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (W)",
> > + 1000, ALIB_ID_STAPM_LIMIT, ALIB_ID_SKIN_LIMIT },
> > + [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (W)",
> > + 1000, ALIB_ID_SLOW_LIMIT },
> > + [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (W)",
> > + 1000, ALIB_ID_FAST_LIMIT },
> > + [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)",
> > + 1, ALIB_ID_TEMP_TARGET },
> > +};
>
> The indent here just made me crazy :-/
>
> Align the contents after line breaks to the left braces.
I will review how to do the alignment, although I'm not happy with how
aligning to spaces will look. PL1 will overflow.
> > +
> > +/* AI MAX Handheld class: GPD Win 5 */
> > +static const struct dptc_device_limits limits_maxhh = { .params = {
>
> Please don't save a line by zipping braces like this. It's hard to read
> and likely to be missed when glancing the code (that's what happened to
> me and it really confused me).
>
> I found you did the same in [PATCH v3 3/4] for `.profiles'. That should
> be avoided too.
This is a leftover from before adding profiles. I guess it does not
make much sense now. I can reflow.
> > + [DPTC_PPT_PL1_SPL] = { 0, 4, 25, 80, 100 },
> > + [DPTC_PPT_PL2_SPPT] = { 0, 4, 27, 82, 100 },
> > + [DPTC_PPT_PL3_FPPT] = { 0, 4, 40, 85, 100 },
> > + [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 },
> > +}};
> > +
> > +/* Substring matches against boot_cpu_data.x86_model_id; order matters. */
> > +static const char * const dptc_soc_table[] = {
> > + /* AI MAX */
> > + "AMD RYZEN AI MAX+ 395",
> > + "AMD RYZEN AI MAX+ 385",
> > + "AMD RYZEN AI MAX 380",
> > + NULL,
> > +};
> > +
> > +static const struct dmi_system_id dptc_dmi_table[] = {
> > + /* GPD */
> > + {
> > + .ident = "GPD Win 5",
> > + .matches = {
> > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"),
> > + },
> > + .driver_data = (void *)&limits_maxhh,
> > + },
> > + { }
> > +};
> > +MODULE_DEVICE_TABLE(dmi, dptc_dmi_table);
> > +
> > +struct dptc_priv;
> > +
> > +struct dptc_attr_sysfs {
> > + struct dptc_priv *priv;
> > + struct kobj_attribute current_value;
> > + struct kobj_attribute default_value;
> > + struct kobj_attribute min_value;
> > + struct kobj_attribute max_value;
> > + struct kobj_attribute scalar_increment;
> > + struct kobj_attribute display_name;
> > + struct kobj_attribute type;
> > + struct attribute *attrs[8];
> > + struct attribute_group group;
> > + int idx;
> > +};
> > +
> > +struct dptc_priv {
> > + struct device *fw_attr_dev;
> > + struct kset *fw_attr_kset;
> > +
> > + const struct dptc_device_limits *dev_limits;
> > +
> > + bool expanded;
> > +
> > + enum dptc_save_mode { SAVE_SINGLE, SAVE_BULK } save_mode;
>
> Defining the enumeration separately will improve readability.
>
> > +
> > + u32 staged[DPTC_NUM_PARAMS];
> > + bool has_staged[DPTC_NUM_PARAMS];
> > +
> > + /* Protects mutable driver state */
>
> The comment is nonsense. To make it helpful, it should be "Protects
> members above/below" with properly arranged structure layout.
>
> > + struct mutex lock;
> > +
> > + struct dptc_attr_sysfs params[DPTC_NUM_PARAMS];
> > + struct dptc_attr_sysfs expanded_attr;
> > + struct kobj_attribute save_settings_attr;
> > +};
> > +
> > +static struct platform_device *dptc_pdev;
> > +
> > +static u32 dptc_get_min(struct dptc_priv *dptc, int idx)
> > +{
> > + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_min
> > + : dptc->dev_limits->params[idx].device_min;
> > +}
> > +
> > +static u32 dptc_get_max(struct dptc_priv *dptc, int idx)
> > +{
> > + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_max
> > + : dptc->dev_limits->params[idx].device_max;
> > +}
> > +
> > +static u32 dptc_get_default(struct dptc_priv *dptc, int idx)
> > +{
> > + return dptc->dev_limits->params[idx].def;
> > +}
> > +
> > +static int dptc_alib_call(const u8 *ids, const u32 *vals, int count)
> > +{
> > + union acpi_object in_params[2];
> > + struct acpi_object_list input;
> > + acpi_status status;
> > + u32 buf_size;
> > + int i, off;
> > + u8 *buf;
> > +
> > + if (count == 0)
> > + return -EINVAL;
>
> Return -ENOENT as think-lmi does (see its save_settings_store()) and
> update the documentation accordingly.
>
> > +
> > + /* 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);
>
> 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;
>
> Misaligned `='.
>
> > +
> > + 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 hw_val = val * dptc_params[idx].scale;
> > + u32 vals[2];
> > + u8 ids[2];
> > + int count = 0;
> > +
> > + ids[count] = dptc_params[idx].param_id;
> > + vals[count] = hw_val;
> > + count++;
> > + if (dptc_params[idx].param_id2) {
> > + ids[count] = dptc_params[idx].param_id2;
> > + vals[count] = hw_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;
> > + u32 hw_val;
> > +
> > + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> > + if (!dptc->has_staged[i])
> > + continue;
> > +
> > + hw_val = dptc->staged[i] * dptc_params[i].scale;
> > + ids[count] = dptc_params[i].param_id;
> > + vals[count] = hw_val;
> > + count++;
> > + if (dptc_params[i].param_id2) {
> > + ids[count] = dptc_params[i].param_id2;
> > + vals[count] = hw_val;
> > + count++;
> > + }
>
> I could imagine a helper function to prevent replicating the same code
> patterns twice:
>
> static size_t dptc_alib_fill_param(u8 *ids, u32 *vals, size_t offset,
> enum dptc_param_idx param, u32 val)
> {
> u32 hw_val = val * dptc_params[param].scale;
>
> ids[offset] = dptc_params[param].param_id;
> vals[offset++] = hw_val;
>
> if (dptc_params[param].param_id2) {
> ids[offset] = dptc_params[param].param_id2;
> vals[offset++] = hw_val;
> }
>
> return offset;
> }
>
> > + }
> > +
> > + 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->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);
> > +
> > + dptc->has_staged[ps->idx] = false;
> > + return count;
> > + }
> > +
> > + ret = kstrtou32(buf, 10, &val);
> > + if (ret)
> > + return ret;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + 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"))
>
> Please don't rely on the `count == 0' check in dptc_alib_call(). `dptc-
> >save_mode == SAVE_SINGLE' should result in -EOPNOTSUPP to maintain
> consistency with think-lmi (update the documentation accordingly).
I will consider it. But this is subtractive behavior. We are still ABI
compliant even if the write goes through and it does not affect
functionality.
> > + ret = dptc_alib_save(dptc);
> > + else if (sysfs_streq(buf, "single"))
> > + dptc->save_mode = SAVE_SINGLE;
> > + else if (sysfs_streq(buf, "bulk"))
> > + dptc->save_mode = SAVE_BULK;
> > + else
> > + return -EINVAL;
> > +
> > + return ret ? ret : count;
> > +}
> > +
> > +static ssize_t dptc_expanded_current_value_show(struct kobject *kobj,
> > + struct kobj_attribute *attr,
> > + char *buf)
> > +{
> > + struct dptc_attr_sysfs *ps =
> > + container_of(attr, struct dptc_attr_sysfs, current_value);
> > + struct dptc_priv *dptc = ps->priv;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + return sysfs_emit(buf, "%d\n", dptc->expanded);
> > +}
> > +
> > +static ssize_t dptc_expanded_current_value_store(struct kobject *kobj,
> > + struct kobj_attribute *attr,
> > + const char *buf, size_t count)
> > +{
> > + struct dptc_attr_sysfs *ps =
> > + container_of(attr, struct dptc_attr_sysfs, current_value);
> > + struct dptc_priv *dptc = ps->priv;
> > + bool val;
> > + int ret;
> > +
> > + ret = kstrtobool(buf, &val);
> > + if (ret)
> > + return ret;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + dptc->expanded = val;
> > + /* Clear staged values: limits changed, old values may be out of range */
> > + memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
>
> I'd prefer using a bitmap so that you don't need to use memset() to
> clear them. It also saves some memory if more parameters are added in
> the future.
I will review if has_staged needs to exist. Values cannot be 0 anyway.
A bit map has other readability issues.
> > +
> > + 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;
> > +}
>
> As Derek has said in v2, use macros to rearrange them.
>
> It's painful to read all these things as I'll need to scroll the screen
> quite a lot just to read something with a low information density.
> That's why there are numerous macros defined in include/linux/sysfs.h.
> I could imagine defining macros like DPTC{,_EXPANDED}_ATTR_{RO,RW}.
This driver consists of two instantiation blocks. It is not possible
to dedup the initialization functions and save space because the
function holders and structs that would be needed undo that.
The blocks in lenovo other are significantly less readable and harder
to write. Perhaps you save around 20 lines. But it is not worth it
imo.
> Meanwhile, I am not very sure why you would have to dynamically
> initialize all these attributes. I haven't notice a blocker of defining
> them statically at the module-level (see how lenovo-wmi-other did). Am
> I missing anything?
You are not missing something. There is no benefit to dynamic
allocation. Static allocation needs macros.
> > +
> > +static void dptc_fw_dev_unregister(void *data)
> > +{
> > + device_unregister(data);
> > +}
> > +
> > +static void dptc_kset_unregister(void *data)
> > +{
> > + kset_unregister(data);
> > +}
> > +
> > +static int dptc_resume(struct device *dev)
> > +{
> > + struct dptc_priv *dptc = dev_get_drvdata(dev);
> > + int ret;
> > +
> > + guard(mutex)(&dptc->lock);
> > +
> > + if (dptc->save_mode == SAVE_SINGLE)
> > + ret = dptc_alib_save(dptc);
>
> I don't see why bulk mode and single mode should have divergence on
> resuming behavior.
This is personal preference. I'd rather bulk mode completely disengage
the driver handling. It also gives it a better reason to exist. After
all, ALIB runs a for loop inside it, there is no difference calling it
3 times or 1 as far as the GPU is concerned. It is only ACPI overhead.
The original name was commit_mode, with manual/auto. But while working
on the ABI entry I found save_settings, and it was an ABI match so I
opted for that instead of making a new entry.
Unfortunately, naming does not match precisely.
Thanks for the review! Ack. on everything I did not comment on.
Best,
Antheas
> Thanks,
> Rong
>
> > + 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 device *dev = &pdev->dev;
> > + struct dptc_priv *dptc;
> > + int i, ret;
> > +
> > + dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL);
> > + if (!dptc)
> > + return -ENOMEM;
> > +
> > + platform_set_drvdata(pdev, dptc);
> > +
> > + ret = devm_mutex_init(dev, &dptc->lock);
> > + if (ret)
> > + return ret;
> > +
> > + dptc->dev_limits = dmi_match->driver_data;
> > + dev_info(dev, "%s (%s)\n", dmi_match->ident,
> > + boot_cpu_data.x86_model_id);
> > +
> > + dptc->fw_attr_dev = device_create(&firmware_attributes_class,
> > + NULL, MKDEV(0, 0), NULL,
> > + DRIVER_NAME);
> > + if (IS_ERR(dptc->fw_attr_dev))
> > + return PTR_ERR(dptc->fw_attr_dev);
> > +
> > + ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister,
> > + dptc->fw_attr_dev);
> > + if (ret)
> > + return ret;
> > +
> > + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL,
> > + &dptc->fw_attr_dev->kobj);
> > + if (!dptc->fw_attr_kset)
> > + return -ENOMEM;
> > +
> > + ret = devm_add_action_or_reset(dev, dptc_kset_unregister,
> > + dptc->fw_attr_kset);
> > + if (ret)
> > + return ret;
> > +
> > + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> > + dptc_setup_param_sysfs(dptc, &dptc->params[i], i);
> > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> > + &dptc->params[i].group);
> > + if (ret)
> > + return ret;
> > + }
> > +
> > + dptc_setup_expanded_sysfs(dptc, &dptc->expanded_attr);
> > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> > + &dptc->expanded_attr.group);
> > + if (ret)
> > + return ret;
> > +
> > + sysfs_attr_init(&dptc->save_settings_attr.attr);
> > + dptc->save_settings_attr.attr.name = "save_settings";
> > + dptc->save_settings_attr.attr.mode = 0644;
> > + dptc->save_settings_attr.show = dptc_save_settings_show;
> > + dptc->save_settings_attr.store = dptc_save_settings_store;
> > + ret = sysfs_create_file(&dptc->fw_attr_kset->kobj,
> > + &dptc->save_settings_attr.attr);
> > + if (ret)
> > + return ret;
> > +
> > + return 0;
> > +}
> > +
> > +static struct platform_driver dptc_driver = {
> > + .driver = {
> > + .name = DRIVER_NAME,
> > + .pm = pm_sleep_ptr(&dptc_pm_ops),
> > + },
> > + .probe = dptc_probe,
> > +};
> > +
> > +static int __init dptc_init(void)
> > +{
> > + const struct dmi_system_id *match;
> > + bool soc_found = false;
> > + int i, ret;
> > +
> > + match = dmi_first_match(dptc_dmi_table);
> > + if (!match)
> > + return -ENODEV;
> > +
> > + if (!acpi_has_method(NULL, ALIB_PATH)) {
> > + pr_warn("ALIB method not present\n");
> > + return -ENODEV;
> > + }
> > +
> > + for (i = 0; dptc_soc_table[i]; i++) {
> > + if (strstr(boot_cpu_data.x86_model_id,
> > + dptc_soc_table[i])) {
> > + soc_found = true;
> > + break;
> > + }
> > + }
> > + if (!soc_found) {
> > + pr_warn("unrecognized SoC '%s'\n",
> > + boot_cpu_data.x86_model_id);
> > + return -ENODEV;
> > + }
> > +
> > + dptc_pdev = platform_device_register_data(NULL, DRIVER_NAME, -1,
> > + match, sizeof(*match));
> > + if (IS_ERR(dptc_pdev))
> > + return PTR_ERR(dptc_pdev);
> > +
> > + ret = platform_driver_register(&dptc_driver);
> > + if (ret) {
> > + platform_device_unregister(dptc_pdev);
> > + return ret;
> > + }
> > +
> > + return 0;
> > +}
> > +
> > +static void __exit dptc_exit(void)
> > +{
> > + platform_driver_unregister(&dptc_driver);
> > + platform_device_unregister(dptc_pdev);
> > +}
> > +
> > +module_init(dptc_init);
> > +module_exit(dptc_exit);
> > +
> > +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>");
> > +MODULE_DESCRIPTION("AMD DPTCi ACPI Driver");
> > +MODULE_LICENSE("GPL");
>
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [RFC v3 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver
2026-03-07 23:02 ` Antheas Kapenekakis
@ 2026-03-07 23:08 ` Antheas Kapenekakis
2026-03-08 20:00 ` Rong Zhang
0 siblings, 1 reply; 19+ messages in thread
From: Antheas Kapenekakis @ 2026-03-07 23:08 UTC (permalink / raw)
To: Rong Zhang
Cc: Mario.Limonciello, W_Armin, sashal, Shyam-Sundar.S-k,
derekjohn.clark, denis.benato, linux-kernel, platform-driver-x86
On Sun, 8 Mar 2026 at 00:02, Antheas Kapenekakis <lkml@antheas.dev> wrote:
>
> On Sat, 7 Mar 2026 at 22:56, Rong Zhang <i@rong.moe> wrote:
> >
> > Hi Antheas,
> >
> > Thanks for working on it.
> >
> > I think this driver is useful for me and I will see if I can test it on
> > my devices once I find some time. They're not handhelds but I am sure
> > at least one of them has the ALIB method. It'd better if a module
> > option can be used to bypass the quirk table so that testing against a
> > new device will be easier. Such an option should taint the kernel ofc.
>
> I will defer to Mario for this one. Previous iterations of this series
> had an override. Before I posted V2 I had a module param that allowed
> matching by ident.
Hi, 2 small follow-ups. Entries to this driver will only be added for
devices without EC/amd_pmf handling. Because those interfere. So it
does not apply to lenovo devices AFAIK.
> > That being said, I have some review comments. Please see below and my
> > following replies.
> >
> > On Sat, 2026-03-07 at 12:55 +0100, Antheas Kapenekakis wrote:
> > > Add a driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> > > Thermal Configuration interface (DPTCi). This exposes TDP and thermal
> > > parameters for AMD APU-based handheld devices via the
> > > firmware-attributes sysfs ABI.
> > >
> > > Parameters are staged and atomically committed through ALIB. The driver
> > > supports two save modes: "single" (apply immediately on write) and
> > > "bulk" (stage values, then commit with "save"). An "expanded_limits"
> > > toggle widens the allowed parameter ranges beyond device defaults.
> > >
> > > Initial device support: GPD Win 5 (AMD Ryzen AI MAX).
> > >
> > > Assisted-by: Claude:claude-opus-4-6
> > > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> > > ---
> > > MAINTAINERS | 6 +
> > > drivers/platform/x86/amd/Kconfig | 14 +
> > > drivers/platform/x86/amd/Makefile | 2 +
> > > drivers/platform/x86/amd/dptc.c | 747 ++++++++++++++++++++++++++++++
> > > 4 files changed, 769 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..d610092467fc 100644
> > > --- a/drivers/platform/x86/amd/Kconfig
> > > +++ b/drivers/platform/x86/amd/Kconfig
> > > @@ -44,3 +44,17 @@ config AMD_ISP_PLATFORM
> > >
> > > This driver can also be built as a module. If so, the module
> > > will be called amd_isp4.
> > > +
> > > +config AMD_DPTC
> > > + tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)"
> > > + depends on X86_64 && ACPI && DMI
> > > + select FIRMWARE_ATTRIBUTES_CLASS
> > > + help
> > > + Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> > > + Thermal Configuration Interface (DPTCi). Exposes TDP and thermal
> > > + parameters for AMD APU-based handheld devices via the
> > > + firmware-attributes sysfs ABI, allowing userspace tools to stage
> > > + and atomically commit power limit settings. Requires a DMI match
> > > + for the device and a recognized AMD SoC.
> > > +
> > > + If built as a module, the module will be called amd_dptc.
> > > diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile
> > > index f6ff0c837f34..862a609bfe38 100644
> > > --- a/drivers/platform/x86/amd/Makefile
> > > +++ b/drivers/platform/x86/amd/Makefile
> > > @@ -12,3 +12,5 @@ obj-$(CONFIG_AMD_PMF) += pmf/
> > > obj-$(CONFIG_AMD_WBRF) += wbrf.o
> > > obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o
> > > obj-$(CONFIG_AMD_HFI) += hfi/
> > > +obj-$(CONFIG_AMD_DPTC) += amd_dptc.o
> > > +amd_dptc-y := dptc.o
> > > diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
> > > new file mode 100644
> > > index 000000000000..acfe9cc01bab
> > > --- /dev/null
> > > +++ b/drivers/platform/x86/amd/dptc.c
> > > @@ -0,0 +1,747 @@
> > > +// SPDX-License-Identifier: GPL-2.0-only
> > > +/*
> > > + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver
> > > + *
> > > + * Exposes AMD APU power and thermal parameters via the firmware-attributes
> > > + * sysfs ABI. Parameters are staged and atomically committed through the
> > > + * AGESA ALIB Function 0x0C (Dynamic Power and Thermal Configuration
> > > + * interface).
> > > + *
> > > + * Reference: AMD AGESA Publication #44065, Appendix E.5
> > > + * https://docs.amd.com/v/u/en-US/44065_Arch2008
> > > + *
> > > + * Copyright (C) 2026 Antheas Kapenekakis <lkml@antheas.dev>
> > > + */
> > > +
> > > +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> > > +
> > > +#include <linux/acpi.h>
> > > +#include <linux/cleanup.h>
> > > +#include <linux/dmi.h>
> > > +#include <linux/init.h>
> > > +#include <linux/kobject.h>
> > > +#include <linux/module.h>
> > > +#include <linux/mutex.h>
> > > +#include <linux/platform_device.h>
> > > +#include <linux/processor.h>
> > > +#include <linux/sysfs.h>
> > > +#include <linux/unaligned.h>
> > > +
> > > +#include "../firmware_attributes_class.h"
> > > +
> > > +#define DRIVER_NAME "amd-dptc"
> > > +
> > > +#define ALIB_FUNC_DPTC 0x0C
> > > +#define ALIB_PATH "\\_SB.ALIB"
> > > +
> > > +#define ALIB_ID_TEMP_TARGET 0x03
> > > +#define ALIB_ID_STAPM_LIMIT 0x05
> > > +#define ALIB_ID_FAST_LIMIT 0x06
> > > +#define ALIB_ID_SLOW_LIMIT 0x07
> > > +#define ALIB_ID_SKIN_LIMIT 0x2E
> > > +
> > > +enum dptc_param_idx {
> > > + DPTC_PPT_PL1_SPL, /* STAPM + skin limit (set together) */
> > > + DPTC_PPT_PL2_SPPT, /* slow PPT limit */
> > > + DPTC_PPT_PL3_FPPT, /* fast PPT limit */
> > > + DPTC_CPU_TEMP, /* thermal control target */
> > > + DPTC_NUM_PARAMS,
> > > +};
> > > +
> > > +struct dptc_param_limits {
> > > + u32 expanded_min;
> > > + u32 device_min;
> > > + u32 def;
> > > + u32 device_max;
> > > + u32 expanded_max;
> > > +};
> > > +
> > > +struct dptc_device_limits {
> > > + struct dptc_param_limits params[DPTC_NUM_PARAMS];
> > > +};
> > > +
> > > +struct dptc_param_desc {
> > > + const char *name;
> > > + const char *display_name;
> > > + u16 scale; /* sysfs-to-ALIB multiplier (e.g. 1000 for W->mW) */
> > > + u8 param_id;
> > > + u8 param_id2; /* secondary ALIB ID, 0 if none */
> > > +};
> > > +
> > > +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = {
> > > + [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (W)",
> > > + 1000, ALIB_ID_STAPM_LIMIT, ALIB_ID_SKIN_LIMIT },
> > > + [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (W)",
> > > + 1000, ALIB_ID_SLOW_LIMIT },
> > > + [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (W)",
> > > + 1000, ALIB_ID_FAST_LIMIT },
> > > + [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)",
> > > + 1, ALIB_ID_TEMP_TARGET },
> > > +};
> >
> > The indent here just made me crazy :-/
> >
> > Align the contents after line breaks to the left braces.
>
> I will review how to do the alignment, although I'm not happy with how
> aligning to spaces will look. PL1 will overflow.
>
> > > +
> > > +/* AI MAX Handheld class: GPD Win 5 */
> > > +static const struct dptc_device_limits limits_maxhh = { .params = {
> >
> > Please don't save a line by zipping braces like this. It's hard to read
> > and likely to be missed when glancing the code (that's what happened to
> > me and it really confused me).
> >
> > I found you did the same in [PATCH v3 3/4] for `.profiles'. That should
> > be avoided too.
>
> This is a leftover from before adding profiles. I guess it does not
> make much sense now. I can reflow.
>
> > > + [DPTC_PPT_PL1_SPL] = { 0, 4, 25, 80, 100 },
> > > + [DPTC_PPT_PL2_SPPT] = { 0, 4, 27, 82, 100 },
> > > + [DPTC_PPT_PL3_FPPT] = { 0, 4, 40, 85, 100 },
> > > + [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 },
> > > +}};
> > > +
> > > +/* Substring matches against boot_cpu_data.x86_model_id; order matters. */
> > > +static const char * const dptc_soc_table[] = {
> > > + /* AI MAX */
> > > + "AMD RYZEN AI MAX+ 395",
> > > + "AMD RYZEN AI MAX+ 385",
> > > + "AMD RYZEN AI MAX 380",
> > > + NULL,
> > > +};
> > > +
> > > +static const struct dmi_system_id dptc_dmi_table[] = {
> > > + /* GPD */
> > > + {
> > > + .ident = "GPD Win 5",
> > > + .matches = {
> > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"),
> > > + },
> > > + .driver_data = (void *)&limits_maxhh,
> > > + },
> > > + { }
> > > +};
> > > +MODULE_DEVICE_TABLE(dmi, dptc_dmi_table);
> > > +
> > > +struct dptc_priv;
> > > +
> > > +struct dptc_attr_sysfs {
> > > + struct dptc_priv *priv;
> > > + struct kobj_attribute current_value;
> > > + struct kobj_attribute default_value;
> > > + struct kobj_attribute min_value;
> > > + struct kobj_attribute max_value;
> > > + struct kobj_attribute scalar_increment;
> > > + struct kobj_attribute display_name;
> > > + struct kobj_attribute type;
> > > + struct attribute *attrs[8];
> > > + struct attribute_group group;
> > > + int idx;
> > > +};
> > > +
> > > +struct dptc_priv {
> > > + struct device *fw_attr_dev;
> > > + struct kset *fw_attr_kset;
> > > +
> > > + const struct dptc_device_limits *dev_limits;
> > > +
> > > + bool expanded;
> > > +
> > > + enum dptc_save_mode { SAVE_SINGLE, SAVE_BULK } save_mode;
> >
> > Defining the enumeration separately will improve readability.
> >
> > > +
> > > + u32 staged[DPTC_NUM_PARAMS];
> > > + bool has_staged[DPTC_NUM_PARAMS];
> > > +
> > > + /* Protects mutable driver state */
> >
> > The comment is nonsense. To make it helpful, it should be "Protects
> > members above/below" with properly arranged structure layout.
> >
> > > + struct mutex lock;
> > > +
> > > + struct dptc_attr_sysfs params[DPTC_NUM_PARAMS];
> > > + struct dptc_attr_sysfs expanded_attr;
> > > + struct kobj_attribute save_settings_attr;
> > > +};
> > > +
> > > +static struct platform_device *dptc_pdev;
> > > +
> > > +static u32 dptc_get_min(struct dptc_priv *dptc, int idx)
> > > +{
> > > + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_min
> > > + : dptc->dev_limits->params[idx].device_min;
> > > +}
> > > +
> > > +static u32 dptc_get_max(struct dptc_priv *dptc, int idx)
> > > +{
> > > + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_max
> > > + : dptc->dev_limits->params[idx].device_max;
> > > +}
> > > +
> > > +static u32 dptc_get_default(struct dptc_priv *dptc, int idx)
> > > +{
> > > + return dptc->dev_limits->params[idx].def;
> > > +}
> > > +
> > > +static int dptc_alib_call(const u8 *ids, const u32 *vals, int count)
> > > +{
> > > + union acpi_object in_params[2];
> > > + struct acpi_object_list input;
> > > + acpi_status status;
> > > + u32 buf_size;
> > > + int i, off;
> > > + u8 *buf;
> > > +
> > > + if (count == 0)
> > > + return -EINVAL;
> >
> > Return -ENOENT as think-lmi does (see its save_settings_store()) and
> > update the documentation accordingly.
> >
> > > +
> > > + /* 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);
> >
> > 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;
> >
> > Misaligned `='.
> >
> > > +
> > > + 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 hw_val = val * dptc_params[idx].scale;
> > > + u32 vals[2];
> > > + u8 ids[2];
> > > + int count = 0;
> > > +
> > > + ids[count] = dptc_params[idx].param_id;
> > > + vals[count] = hw_val;
> > > + count++;
> > > + if (dptc_params[idx].param_id2) {
> > > + ids[count] = dptc_params[idx].param_id2;
> > > + vals[count] = hw_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;
> > > + u32 hw_val;
> > > +
> > > + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> > > + if (!dptc->has_staged[i])
> > > + continue;
> > > +
> > > + hw_val = dptc->staged[i] * dptc_params[i].scale;
> > > + ids[count] = dptc_params[i].param_id;
> > > + vals[count] = hw_val;
> > > + count++;
> > > + if (dptc_params[i].param_id2) {
> > > + ids[count] = dptc_params[i].param_id2;
> > > + vals[count] = hw_val;
> > > + count++;
> > > + }
> >
> > I could imagine a helper function to prevent replicating the same code
> > patterns twice:
> >
> > static size_t dptc_alib_fill_param(u8 *ids, u32 *vals, size_t offset,
> > enum dptc_param_idx param, u32 val)
> > {
> > u32 hw_val = val * dptc_params[param].scale;
> >
> > ids[offset] = dptc_params[param].param_id;
> > vals[offset++] = hw_val;
> >
> > if (dptc_params[param].param_id2) {
> > ids[offset] = dptc_params[param].param_id2;
> > vals[offset++] = hw_val;
> > }
> >
> > return offset;
> > }
> >
> > > + }
> > > +
> > > + 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->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);
> > > +
> > > + dptc->has_staged[ps->idx] = false;
> > > + return count;
> > > + }
> > > +
> > > + ret = kstrtou32(buf, 10, &val);
> > > + if (ret)
> > > + return ret;
> > > +
> > > + guard(mutex)(&dptc->lock);
> > > +
> > > + 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"))
> >
> > Please don't rely on the `count == 0' check in dptc_alib_call(). `dptc-
> > >save_mode == SAVE_SINGLE' should result in -EOPNOTSUPP to maintain
> > consistency with think-lmi (update the documentation accordingly).
>
> I will consider it. But this is subtractive behavior. We are still ABI
> compliant even if the write goes through and it does not affect
> functionality.
>
> > > + ret = dptc_alib_save(dptc);
> > > + else if (sysfs_streq(buf, "single"))
> > > + dptc->save_mode = SAVE_SINGLE;
> > > + else if (sysfs_streq(buf, "bulk"))
> > > + dptc->save_mode = SAVE_BULK;
> > > + else
> > > + return -EINVAL;
> > > +
> > > + return ret ? ret : count;
> > > +}
> > > +
> > > +static ssize_t dptc_expanded_current_value_show(struct kobject *kobj,
> > > + struct kobj_attribute *attr,
> > > + char *buf)
> > > +{
> > > + struct dptc_attr_sysfs *ps =
> > > + container_of(attr, struct dptc_attr_sysfs, current_value);
> > > + struct dptc_priv *dptc = ps->priv;
> > > +
> > > + guard(mutex)(&dptc->lock);
> > > +
> > > + return sysfs_emit(buf, "%d\n", dptc->expanded);
> > > +}
> > > +
> > > +static ssize_t dptc_expanded_current_value_store(struct kobject *kobj,
> > > + struct kobj_attribute *attr,
> > > + const char *buf, size_t count)
> > > +{
> > > + struct dptc_attr_sysfs *ps =
> > > + container_of(attr, struct dptc_attr_sysfs, current_value);
> > > + struct dptc_priv *dptc = ps->priv;
> > > + bool val;
> > > + int ret;
> > > +
> > > + ret = kstrtobool(buf, &val);
> > > + if (ret)
> > > + return ret;
> > > +
> > > + guard(mutex)(&dptc->lock);
> > > +
> > > + dptc->expanded = val;
> > > + /* Clear staged values: limits changed, old values may be out of range */
> > > + memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
> >
> > I'd prefer using a bitmap so that you don't need to use memset() to
> > clear them. It also saves some memory if more parameters are added in
> > the future.
>
> I will review if has_staged needs to exist. Values cannot be 0 anyway.
> A bit map has other readability issues.
>
> > > +
> > > + 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;
> > > +}
> >
> > As Derek has said in v2, use macros to rearrange them.
> >
> > It's painful to read all these things as I'll need to scroll the screen
> > quite a lot just to read something with a low information density.
> > That's why there are numerous macros defined in include/linux/sysfs.h.
> > I could imagine defining macros like DPTC{,_EXPANDED}_ATTR_{RO,RW}.
>
> This driver consists of two instantiation blocks. It is not possible
> to dedup the initialization functions and save space because the
> function holders and structs that would be needed undo that.
>
> The blocks in lenovo other are significantly less readable and harder
> to write. Perhaps you save around 20 lines. But it is not worth it
> imo.
I would consider adding them if the helper macros are centralized, but
I am not adding macros to this driver.
Antheas
> > Meanwhile, I am not very sure why you would have to dynamically
> > initialize all these attributes. I haven't notice a blocker of defining
> > them statically at the module-level (see how lenovo-wmi-other did). Am
> > I missing anything?
>
> You are not missing something. There is no benefit to dynamic
> allocation. Static allocation needs macros.
>
> > > +
> > > +static void dptc_fw_dev_unregister(void *data)
> > > +{
> > > + device_unregister(data);
> > > +}
> > > +
> > > +static void dptc_kset_unregister(void *data)
> > > +{
> > > + kset_unregister(data);
> > > +}
> > > +
> > > +static int dptc_resume(struct device *dev)
> > > +{
> > > + struct dptc_priv *dptc = dev_get_drvdata(dev);
> > > + int ret;
> > > +
> > > + guard(mutex)(&dptc->lock);
> > > +
> > > + if (dptc->save_mode == SAVE_SINGLE)
> > > + ret = dptc_alib_save(dptc);
> >
> > I don't see why bulk mode and single mode should have divergence on
> > resuming behavior.
>
> This is personal preference. I'd rather bulk mode completely disengage
> the driver handling. It also gives it a better reason to exist. After
> all, ALIB runs a for loop inside it, there is no difference calling it
> 3 times or 1 as far as the GPU is concerned. It is only ACPI overhead.
> The original name was commit_mode, with manual/auto. But while working
> on the ABI entry I found save_settings, and it was an ABI match so I
> opted for that instead of making a new entry.
>
> Unfortunately, naming does not match precisely.
>
> Thanks for the review! Ack. on everything I did not comment on.
>
> Best,
> Antheas
>
> > Thanks,
> > Rong
> >
> > > + 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 device *dev = &pdev->dev;
> > > + struct dptc_priv *dptc;
> > > + int i, ret;
> > > +
> > > + dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL);
> > > + if (!dptc)
> > > + return -ENOMEM;
> > > +
> > > + platform_set_drvdata(pdev, dptc);
> > > +
> > > + ret = devm_mutex_init(dev, &dptc->lock);
> > > + if (ret)
> > > + return ret;
> > > +
> > > + dptc->dev_limits = dmi_match->driver_data;
> > > + dev_info(dev, "%s (%s)\n", dmi_match->ident,
> > > + boot_cpu_data.x86_model_id);
> > > +
> > > + dptc->fw_attr_dev = device_create(&firmware_attributes_class,
> > > + NULL, MKDEV(0, 0), NULL,
> > > + DRIVER_NAME);
> > > + if (IS_ERR(dptc->fw_attr_dev))
> > > + return PTR_ERR(dptc->fw_attr_dev);
> > > +
> > > + ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister,
> > > + dptc->fw_attr_dev);
> > > + if (ret)
> > > + return ret;
> > > +
> > > + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL,
> > > + &dptc->fw_attr_dev->kobj);
> > > + if (!dptc->fw_attr_kset)
> > > + return -ENOMEM;
> > > +
> > > + ret = devm_add_action_or_reset(dev, dptc_kset_unregister,
> > > + dptc->fw_attr_kset);
> > > + if (ret)
> > > + return ret;
> > > +
> > > + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> > > + dptc_setup_param_sysfs(dptc, &dptc->params[i], i);
> > > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> > > + &dptc->params[i].group);
> > > + if (ret)
> > > + return ret;
> > > + }
> > > +
> > > + dptc_setup_expanded_sysfs(dptc, &dptc->expanded_attr);
> > > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> > > + &dptc->expanded_attr.group);
> > > + if (ret)
> > > + return ret;
> > > +
> > > + sysfs_attr_init(&dptc->save_settings_attr.attr);
> > > + dptc->save_settings_attr.attr.name = "save_settings";
> > > + dptc->save_settings_attr.attr.mode = 0644;
> > > + dptc->save_settings_attr.show = dptc_save_settings_show;
> > > + dptc->save_settings_attr.store = dptc_save_settings_store;
> > > + ret = sysfs_create_file(&dptc->fw_attr_kset->kobj,
> > > + &dptc->save_settings_attr.attr);
> > > + if (ret)
> > > + return ret;
> > > +
> > > + return 0;
> > > +}
> > > +
> > > +static struct platform_driver dptc_driver = {
> > > + .driver = {
> > > + .name = DRIVER_NAME,
> > > + .pm = pm_sleep_ptr(&dptc_pm_ops),
> > > + },
> > > + .probe = dptc_probe,
> > > +};
> > > +
> > > +static int __init dptc_init(void)
> > > +{
> > > + const struct dmi_system_id *match;
> > > + bool soc_found = false;
> > > + int i, ret;
> > > +
> > > + match = dmi_first_match(dptc_dmi_table);
> > > + if (!match)
> > > + return -ENODEV;
> > > +
> > > + if (!acpi_has_method(NULL, ALIB_PATH)) {
> > > + pr_warn("ALIB method not present\n");
> > > + return -ENODEV;
> > > + }
> > > +
> > > + for (i = 0; dptc_soc_table[i]; i++) {
> > > + if (strstr(boot_cpu_data.x86_model_id,
> > > + dptc_soc_table[i])) {
> > > + soc_found = true;
> > > + break;
> > > + }
> > > + }
> > > + if (!soc_found) {
> > > + pr_warn("unrecognized SoC '%s'\n",
> > > + boot_cpu_data.x86_model_id);
> > > + return -ENODEV;
> > > + }
> > > +
> > > + dptc_pdev = platform_device_register_data(NULL, DRIVER_NAME, -1,
> > > + match, sizeof(*match));
> > > + if (IS_ERR(dptc_pdev))
> > > + return PTR_ERR(dptc_pdev);
> > > +
> > > + ret = platform_driver_register(&dptc_driver);
> > > + if (ret) {
> > > + platform_device_unregister(dptc_pdev);
> > > + return ret;
> > > + }
> > > +
> > > + return 0;
> > > +}
> > > +
> > > +static void __exit dptc_exit(void)
> > > +{
> > > + platform_driver_unregister(&dptc_driver);
> > > + platform_device_unregister(dptc_pdev);
> > > +}
> > > +
> > > +module_init(dptc_init);
> > > +module_exit(dptc_exit);
> > > +
> > > +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>");
> > > +MODULE_DESCRIPTION("AMD DPTCi ACPI Driver");
> > > +MODULE_LICENSE("GPL");
> >
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [RFC v3 4/4] platform/x86/amd: dptc: Add device entries for handheld PCs
2026-03-07 22:40 ` Antheas Kapenekakis
@ 2026-03-08 9:23 ` Rong Zhang
2026-03-08 9:42 ` Antheas Kapenekakis
0 siblings, 1 reply; 19+ messages in thread
From: Rong Zhang @ 2026-03-08 9:23 UTC (permalink / raw)
To: Antheas Kapenekakis
Cc: Mario.Limonciello, W_Armin, sashal, Shyam-Sundar.S-k,
derekjohn.clark, denis.benato, linux-kernel, platform-driver-x86
Hi Antheas,
On Sat, 2026-03-07 at 23:40 +0100, Antheas Kapenekakis wrote:
> On Sat, 7 Mar 2026 at 22:57, Rong Zhang <i@rong.moe> wrote:
> >
> > Hi Antheas,
> >
> > On Sat, 2026-03-07 at 12:55 +0100, Antheas Kapenekakis wrote:
> > > Add limit tables and DMI entries for AMD APU-based handheld devices:
> > >
> > > - 15W class: AYANEO AIR
> > > - 18W class: AYANEO AIR Plus/Pro
> > > - 25W class: AYANEO AIR 1S, NEXT/KUN
> > > - 28W class: GPD Win Mini/4/Max 2, GPD Duo, GPD Pocket 4, AYANEO 2,
> > > OrangePi NEO-01, SuiPlay0X1
> > > - 30W class: AYANEO FLIP/GEEK/SLIDE/3, AOKZOE A1/A1X/A2,
> > > OneXPlayer F1Pro/2/X1/G1, AYN Loki Max, Zeenix Pro
> > >
> > > Also add SoC strings for Ryzen AI, Z1, Ryzen 8000/7040/6000/5000
> > > families, extending support beyond the initial AI MAX series.
> > >
> > > Assisted-by: Claude:claude-opus-4-6
> > > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> > > ---
> > > drivers/platform/x86/amd/dptc.c | 409 ++++++++++++++++++++++++++++++++
> > > 1 file changed, 409 insertions(+)
> > >
> > > diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
> > > index 1f07e2e6f278..a3d582e48cd3 100644
> > > --- a/drivers/platform/x86/amd/dptc.c
> > > +++ b/drivers/platform/x86/amd/dptc.c
> > > @@ -85,6 +85,66 @@ static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = {
> > > 1, ALIB_ID_TEMP_TARGET },
> > > };
> > >
> > > +/* 15W class: AYANEO AIR (Ryzen 5 5560U) */
> > > +static const struct dptc_device_limits limits_15w = { .params = {
> > > + [DPTC_PPT_PL1_SPL] = { 0, 5, 10, 15, 18 },
> > > + [DPTC_PPT_PL2_SPPT] = { 0, 5, 10, 15, 18 },
> > > + [DPTC_PPT_PL3_FPPT] = { 0, 5, 12, 18, 22 },
> > > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > > +}, .profiles = {
> > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } },
> > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 10, 12, 15, 0 } },
> > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 15, 15, 18, 0 } },
> > > +}};
> > > +
> > > +/* 18W class: AYANEO AIR Plus/Pro (Ryzen 5 5560U, Ryzen 7 5825U) */
> > > +static const struct dptc_device_limits limits_18w = { .params = {
> > > + [DPTC_PPT_PL1_SPL] = { 0, 5, 15, 18, 22 },
> > > + [DPTC_PPT_PL2_SPPT] = { 0, 5, 15, 18, 22 },
> > > + [DPTC_PPT_PL3_FPPT] = { 0, 5, 15, 20, 25 },
> > > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > > +}, .profiles = {
> > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } },
> > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 12, 14, 15, 0 } },
> > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 18, 18, 20, 0 } },
> > > +}};
> > > +
> > > +/* 25W class: Ryzen 5000 handhelds (AYANEO NEXT, KUN) */
> > > +static const struct dptc_device_limits limits_25w = { .params = {
> > > + [DPTC_PPT_PL1_SPL] = { 0, 4, 15, 25, 32 },
> > > + [DPTC_PPT_PL2_SPPT] = { 0, 4, 20, 27, 35 },
> > > + [DPTC_PPT_PL3_FPPT] = { 0, 4, 25, 30, 37 },
> > > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > > +}, .profiles = {
> > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } },
> > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 20, 0 } },
> > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 27, 30, 0 } },
> > > +}};
> > > +
> > > +/* 28W class: GPD Win series, AYANEO 2, OrangePi NEO-01 */
> > > +static const struct dptc_device_limits limits_28w = { .params = {
> > > + [DPTC_PPT_PL1_SPL] = { 0, 4, 15, 28, 32 },
> > > + [DPTC_PPT_PL2_SPPT] = { 0, 4, 20, 30, 35 },
> > > + [DPTC_PPT_PL3_FPPT] = { 0, 4, 25, 32, 37 },
> > > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > > +}, .profiles = {
> > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } },
> > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 22, 0 } },
> > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 32, 0 } },
> > > +}};
> > > +
> > > +/* 30W class: OneXPlayer, AYANEO FLIP/GEEK/SLIDE/3, AOKZOE */
> > > +static const struct dptc_device_limits limits_30w = { .params = {
> > > + [DPTC_PPT_PL1_SPL] = { 0, 4, 15, 30, 40 },
> > > + [DPTC_PPT_PL2_SPPT] = { 0, 4, 20, 32, 43 },
> > > + [DPTC_PPT_PL3_FPPT] = { 0, 4, 25, 41, 50 },
> > > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > > +}, .profiles = {
> > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } },
> > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 25, 0 } },
> > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 41, 0 } },
> > > +}};
> > > +
> > > /* AI MAX Handheld class: GPD Win 5 */
> > > static const struct dptc_device_limits limits_maxhh = { .params = {
> > > [DPTC_PPT_PL1_SPL] = { 0, 4, 25, 80, 100 },
> > > @@ -103,11 +163,60 @@ static const char * const dptc_soc_table[] = {
> > > "AMD RYZEN AI MAX+ 395",
> > > "AMD RYZEN AI MAX+ 385",
> > > "AMD RYZEN AI MAX 380",
> > > + /* Ryzen AI */
> > > + "AMD Ryzen AI 9 HX 370",
> > > + "AMD Ryzen AI HX 360",
> >
> > What's "AMD Ryzen AI HX 360"? Did you mean "AMD Ryzen AI 9 H 365"
> > (Chinese-specific model id) and "AMD Ryzen AI 9 365" (other regions)?
>
> Hm, goot point. I seem to have added this preemptively looking at my
> commit logs earlier this year.
>
> It might not be the exact name. Since we do SoC + DMI in this driver
> it might cause some devices with the chip to fail to load.
>
> Some HX360 handhelds did ship I think, but I never got bug reports
> about it missing. I want to say 360 is ryzen 7, at least looking
> online. Are you sure it's 9?
Did you mean asking your AI when you said "looking online"? There's no
*HX* 360. If you want `Ryzen AI * H* 3*', online search gives you
"Ryzen AI 9 HX 370" and "Ryzen AI 9/7/5 H 365/350/340/330" ("H" is
Chinese-specific). If you want `Ryzen AI * 360', online search gives
you "Ryzen AI 7 PRO 360" -- it's "PRO" instead of "HX".
Thanks,
Rong
>
> Antheas
>
> > > + /* Z1 - Extreme before plain Z1 */
> > > + "AMD Ryzen Z1 Extreme",
> > > + "AMD Ryzen Z1",
> >
> > You don't need both since strstr("AMD Ryzen Z1") matches "AMD Ryzen Z1
> > Extreme".
> >
> > Thanks,
> > Rong
> >
> > > + /* 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 = {
> > > @@ -116,6 +225,306 @@ static const struct dmi_system_id dptc_dmi_table[] = {
> > > },
> > > .driver_data = (void *)&limits_maxhh,
> > > },
> > > + {
> > > + .ident = "GPD Win Max 2",
> > > + .matches = {
> > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-04"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + {
> > > + .ident = "GPD Win Max 2 2024",
> > > + .matches = {
> > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-05"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + {
> > > + .ident = "GPD Duo",
> > > + .matches = {
> > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + {
> > > + .ident = "GPD Duo",
> > > + .matches = {
> > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01-L"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + {
> > > + .ident = "GPD Pocket 4",
> > > + .matches = {
> > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + {
> > > + .ident = "GPD Pocket 4",
> > > + .matches = {
> > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04-L"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + /* OrangePi */
> > > + {
> > > + .ident = "OrangePi NEO-01",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + /* AYN */
> > > + {
> > > + .ident = "AYN Loki Max",
> > > + .matches = {
> > > + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ayn"),
> > > + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Loki Max"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + /* Tectoy (Zeenix Pro = Loki Max) */
> > > + {
> > > + .ident = "Zeenix Pro",
> > > + .matches = {
> > > + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Tectoy"),
> > > + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Zeenix Pro"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + /* AOKZOE */
> > > + {
> > > + .ident = "AOKZOE A1 AR07",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 AR07"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + {
> > > + .ident = "AOKZOE A1 Pro",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 Pro"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + {
> > > + .ident = "AOKZOE A1X",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1X"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + {
> > > + .ident = "AOKZOE A2 Pro",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A2 Pro"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + /* OneXPlayer (Intel variants filtered by SoC table) */
> > > + {
> > > + .ident = "ONEXPLAYER F1Pro",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1Pro"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + {
> > > + .ident = "ONEXPLAYER F1 EVA-02",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1 EVA-02"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + {
> > > + .ident = "ONEXPLAYER 2",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > > + DMI_MATCH(DMI_BOARD_NAME, "ONEXPLAYER 2"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + {
> > > + .ident = "ONEXPLAYER X1 A",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1 A"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + {
> > > + .ident = "ONEXPLAYER X1z",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1z"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + {
> > > + .ident = "ONEXPLAYER X1Pro",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Pro"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + {
> > > + .ident = "ONEXPLAYER G1 A",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 A"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + /* AYANEO - 15W */
> > > + {
> > > + .ident = "AYANEO AIR",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR"),
> > > + },
> > > + .driver_data = (void *)&limits_15w,
> > > + },
> > > + /* AYANEO - 18W */
> > > + {
> > > + .ident = "AYANEO AIR Plus",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Plus"),
> > > + },
> > > + .driver_data = (void *)&limits_18w,
> > > + },
> > > + {
> > > + .ident = "AYANEO AIR Pro",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Pro"),
> > > + },
> > > + .driver_data = (void *)&limits_18w,
> > > + },
> > > + /* AYANEO - 25W */
> > > + {
> > > + .ident = "AYANEO AIR 1S",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_MATCH(DMI_BOARD_NAME, "AIR 1S"),
> > > + },
> > > + .driver_data = (void *)&limits_25w,
> > > + },
> > > + {
> > > + .ident = "AYANEO NEXT Advance",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Advance"),
> > > + },
> > > + .driver_data = (void *)&limits_25w,
> > > + },
> > > + {
> > > + .ident = "AYANEO NEXT Lite",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Lite"),
> > > + },
> > > + .driver_data = (void *)&limits_25w,
> > > + },
> > > + {
> > > + .ident = "AYANEO NEXT Pro",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Pro"),
> > > + },
> > > + .driver_data = (void *)&limits_25w,
> > > + },
> > > + {
> > > + .ident = "AYANEO NEXT",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT"),
> > > + },
> > > + .driver_data = (void *)&limits_25w,
> > > + },
> > > + {
> > > + .ident = "AYANEO KUN",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"),
> > > + },
> > > + .driver_data = (void *)&limits_25w,
> > > + },
> > > + {
> > > + .ident = "AYANEO KUN",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO KUN"),
> > > + },
> > > + .driver_data = (void *)&limits_25w,
> > > + },
> > > + /* AYANEO - 28W */
> > > + {
> > > + .ident = "AYANEO 2",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 2"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + {
> > > + .ident = "SuiPlay0X1",
> > > + .matches = {
> > > + DMI_MATCH(DMI_SYS_VENDOR, "Mysten Labs, Inc."),
> > > + DMI_MATCH(DMI_PRODUCT_NAME, "SuiPlay0X1"),
> > > + },
> > > + .driver_data = (void *)&limits_28w,
> > > + },
> > > + /* AYANEO - 30W */
> > > + {
> > > + /* DMI_MATCH catches all FLIP variants (DS, KB, 1S DS, 1S KB) */
> > > + .ident = "AYANEO FLIP",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_MATCH(DMI_BOARD_NAME, "FLIP"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + {
> > > + /* DMI_MATCH catches GEEK and GEEK 1S */
> > > + .ident = "AYANEO GEEK",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_MATCH(DMI_BOARD_NAME, "GEEK"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + {
> > > + .ident = "AYANEO SLIDE",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "SLIDE"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > + {
> > > + .ident = "AYANEO 3",
> > > + .matches = {
> > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO 3"),
> > > + },
> > > + .driver_data = (void *)&limits_30w,
> > > + },
> > > { }
> > > };
> > > MODULE_DEVICE_TABLE(dmi, dptc_dmi_table);
> >
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [RFC v3 3/4] platform/x86/amd: dptc: Add platform profile support
2026-03-07 22:45 ` Antheas Kapenekakis
@ 2026-03-08 9:30 ` Rong Zhang
2026-03-08 9:45 ` Antheas Kapenekakis
0 siblings, 1 reply; 19+ messages in thread
From: Rong Zhang @ 2026-03-08 9:30 UTC (permalink / raw)
To: Antheas Kapenekakis
Cc: Mario.Limonciello, W_Armin, sashal, Shyam-Sundar.S-k,
derekjohn.clark, denis.benato, linux-kernel, platform-driver-x86
Hi Antheas,
On Sat, 2026-03-07 at 23:45 +0100, Antheas Kapenekakis wrote:
> On Sat, 7 Mar 2026 at 22:57, Rong Zhang <i@rong.moe> wrote:
> >
> > Hi Antheas,
> >
> > On Sat, 2026-03-07 at 12:55 +0100, Antheas Kapenekakis wrote:
> > > Register a platform_profile handler so the driver exposes standard
> > > power profiles (low-power, balanced, performance, max-power) alongside
> > > the manual tunable interface. max-power is only supplied when the
> > > performance profile's preset values are below the tunable limits.
> > >
> > > When a non-custom profile is active, parameter writes are blocked
> > > (-EPERM) and current_value reflects the profile's preset values.
> > > Selecting the "custom" profile returns control to the user for manual
> > > staging and committing. On resume, the active profile is automatically
> > > re-applied.
> > >
> > > Assisted-by: Claude:claude-opus-4-6
> > > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> > > ---
> > > drivers/platform/x86/amd/Kconfig | 1 +
> > > drivers/platform/x86/amd/dptc.c | 116 +++++++++++++++++++++++++++++--
> > > 2 files changed, 113 insertions(+), 4 deletions(-)
> > >
> > > diff --git a/drivers/platform/x86/amd/Kconfig b/drivers/platform/x86/amd/Kconfig
> > > index d610092467fc..41ffbd722524 100644
> > > --- a/drivers/platform/x86/amd/Kconfig
> > > +++ b/drivers/platform/x86/amd/Kconfig
> > > @@ -48,6 +48,7 @@ config AMD_ISP_PLATFORM
> > > config AMD_DPTC
> > > tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)"
> > > depends on X86_64 && ACPI && DMI
> > > + select ACPI_PLATFORM_PROFILE
> > > select FIRMWARE_ATTRIBUTES_CLASS
> > > help
> > > Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> > > diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
> > > index acfe9cc01bab..1f07e2e6f278 100644
> > > --- a/drivers/platform/x86/amd/dptc.c
> > > +++ b/drivers/platform/x86/amd/dptc.c
> > > @@ -23,6 +23,7 @@
> > > #include <linux/module.h>
> > > #include <linux/mutex.h>
> > > #include <linux/platform_device.h>
> > > +#include <linux/platform_profile.h>
> > > #include <linux/processor.h>
> > > #include <linux/sysfs.h>
> > > #include <linux/unaligned.h>
> > > @@ -56,8 +57,13 @@ struct dptc_param_limits {
> > > u32 expanded_max;
> > > };
> > >
> > > +struct dptc_profile {
> > > + u32 vals[DPTC_NUM_PARAMS]; /* 0 = don't set / unstage this param */
> > > +};
> > > +
> > > struct dptc_device_limits {
> > > struct dptc_param_limits params[DPTC_NUM_PARAMS];
> > > + struct dptc_profile profiles[PLATFORM_PROFILE_LAST];
> > > };
> > >
> > > struct dptc_param_desc {
> > > @@ -85,6 +91,10 @@ static const struct dptc_device_limits limits_maxhh = { .params = {
> > > [DPTC_PPT_PL2_SPPT] = { 0, 4, 27, 82, 100 },
> > > [DPTC_PPT_PL3_FPPT] = { 0, 4, 40, 85, 100 },
> > > [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 },
> > > +}, .profiles = {
> > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 15, 15, 25, 0 } },
> > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 25, 27, 40, 0 } },
> > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 60, 63, 85, 0 } },
> > > }};
> > >
> > > /* Substring matches against boot_cpu_data.x86_model_id; order matters. */
> > > @@ -134,6 +144,9 @@ struct dptc_priv {
> > >
> > > 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];
> > > @@ -269,6 +282,14 @@ static ssize_t dptc_current_value_show(struct kobject *kobj,
> > >
> > > guard(mutex)(&dptc->lock);
> > >
> > > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
> > > + u32 val = dptc->dev_limits->profiles[dptc->profile].vals[ps->idx];
> > > +
> > > + if (!val)
> > > + return sysfs_emit(buf, "\n");
> > > + return sysfs_emit(buf, "%u\n", val);
> > > + }
> > > +
> > > if (!dptc->has_staged[ps->idx])
> > > return sysfs_emit(buf, "\n");
> > > return sysfs_emit(buf, "%u\n", dptc->staged[ps->idx]);
> > > @@ -287,6 +308,8 @@ static ssize_t dptc_current_value_store(struct kobject *kobj,
> > > if (count == 1 && buf[0] == '\n') {
> > > guard(mutex)(&dptc->lock);
> > >
> > > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> > > + return -EPERM;
> >
> > It has nothing to do with permissions. I'd prefer -EBUSY.
>
> I will defer to other drivers and update if necessary based on their choices.
Here it isn't about consistency, but correctness. If something has
nothing to do with permissions, -EPERM is confusing. -EBUSY tells users
that the interface is under some other thing's (i.e., platform profile)
control.
>
> > To maintain readability, usually a blank line should be added after
> > returning early.
> >
> > > dptc->has_staged[ps->idx] = false;
> > > return count;
> > > }
> > > @@ -297,6 +320,8 @@ static ssize_t dptc_current_value_store(struct kobject *kobj,
> > >
> > > 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))
> > > @@ -426,6 +451,8 @@ static ssize_t dptc_expanded_current_value_store(struct kobject *kobj,
> > >
> > > guard(mutex)(&dptc->lock);
> > >
> > > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> > > + return -EPERM;
> >
> > IMO kstrtou32() isn't really a blocker of acquiring the mutex earlier
> > to prevent duplicating the same check. After all, firmware-attributes
> > is not a performance critical interface, and dptc_save_settings_store()
> > also calls sysfs_eq() with the mutex held.
> >
> > > dptc->expanded = val;
> > > /* Clear staged values: limits changed, old values may be out of range */
> > > memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
> > > @@ -594,6 +621,78 @@ 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);
> >
> > I'd prefer simply calling dptc_alib_save() in dptc_apply_profile()
> > since the latter already checks the profile.
>
> This way you will propagate an error because custom unstages all values.
I meant:
static int 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 0; /* You've checked this and returned early!
*/
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;
}
return dptc_alib_save(dptc);
}
How can an error being propagated?
>
> Antheas
>
> > > +
> > > + 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);
> > > @@ -601,10 +700,14 @@ static int dptc_resume(struct device *dev)
> > >
> > > guard(mutex)(&dptc->lock);
> > >
> > > - if (dptc->save_mode == SAVE_SINGLE)
> > > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
> > > + dptc_apply_profile(dptc, dptc->profile);
> > > ret = dptc_alib_save(dptc);
> > > - else
> > > + } 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);
> > > @@ -636,8 +739,7 @@ static int dptc_probe(struct platform_device *pdev)
> > > boot_cpu_data.x86_model_id);
> > >
> > > dptc->fw_attr_dev = device_create(&firmware_attributes_class,
> > > - NULL, MKDEV(0, 0), NULL,
> > > - DRIVER_NAME);
> > > + NULL, MKDEV(0, 0), NULL, DRIVER_NAME);
> >
> > Squash the change into the previous patch.
> >
> > Thanks,
> > Rong
> >
> > > if (IS_ERR(dptc->fw_attr_dev))
> > > return PTR_ERR(dptc->fw_attr_dev);
> > >
> > > @@ -680,6 +782,12 @@ static int dptc_probe(struct platform_device *pdev)
> > > if (ret)
> > > return ret;
> > >
> > > + dptc->profile = PLATFORM_PROFILE_CUSTOM;
> > > + dptc->ppdev = devm_platform_profile_register(dev, "amd-dptc", dptc,
> > > + &dptc_pp_ops);
> > > + if (IS_ERR(dptc->ppdev))
> > > + return PTR_ERR(dptc->ppdev);
> > > +
> > > return 0;
> > > }
> > >
> >
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [RFC v3 4/4] platform/x86/amd: dptc: Add device entries for handheld PCs
2026-03-08 9:23 ` Rong Zhang
@ 2026-03-08 9:42 ` Antheas Kapenekakis
0 siblings, 0 replies; 19+ messages in thread
From: Antheas Kapenekakis @ 2026-03-08 9:42 UTC (permalink / raw)
To: Rong Zhang
Cc: Mario.Limonciello, W_Armin, sashal, Shyam-Sundar.S-k,
derekjohn.clark, denis.benato, linux-kernel, platform-driver-x86
On Sun, 8 Mar 2026 at 10:29, Rong Zhang <i@rong.moe> wrote:
>
> Hi Antheas,
>
> On Sat, 2026-03-07 at 23:40 +0100, Antheas Kapenekakis wrote:
> > On Sat, 7 Mar 2026 at 22:57, Rong Zhang <i@rong.moe> wrote:
> > >
> > > Hi Antheas,
> > >
> > > On Sat, 2026-03-07 at 12:55 +0100, Antheas Kapenekakis wrote:
> > > > Add limit tables and DMI entries for AMD APU-based handheld devices:
> > > >
> > > > - 15W class: AYANEO AIR
> > > > - 18W class: AYANEO AIR Plus/Pro
> > > > - 25W class: AYANEO AIR 1S, NEXT/KUN
> > > > - 28W class: GPD Win Mini/4/Max 2, GPD Duo, GPD Pocket 4, AYANEO 2,
> > > > OrangePi NEO-01, SuiPlay0X1
> > > > - 30W class: AYANEO FLIP/GEEK/SLIDE/3, AOKZOE A1/A1X/A2,
> > > > OneXPlayer F1Pro/2/X1/G1, AYN Loki Max, Zeenix Pro
> > > >
> > > > Also add SoC strings for Ryzen AI, Z1, Ryzen 8000/7040/6000/5000
> > > > families, extending support beyond the initial AI MAX series.
> > > >
> > > > Assisted-by: Claude:claude-opus-4-6
> > > > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> > > > ---
> > > > drivers/platform/x86/amd/dptc.c | 409 ++++++++++++++++++++++++++++++++
> > > > 1 file changed, 409 insertions(+)
> > > >
> > > > diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
> > > > index 1f07e2e6f278..a3d582e48cd3 100644
> > > > --- a/drivers/platform/x86/amd/dptc.c
> > > > +++ b/drivers/platform/x86/amd/dptc.c
> > > > @@ -85,6 +85,66 @@ static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = {
> > > > 1, ALIB_ID_TEMP_TARGET },
> > > > };
> > > >
> > > > +/* 15W class: AYANEO AIR (Ryzen 5 5560U) */
> > > > +static const struct dptc_device_limits limits_15w = { .params = {
> > > > + [DPTC_PPT_PL1_SPL] = { 0, 5, 10, 15, 18 },
> > > > + [DPTC_PPT_PL2_SPPT] = { 0, 5, 10, 15, 18 },
> > > > + [DPTC_PPT_PL3_FPPT] = { 0, 5, 12, 18, 22 },
> > > > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > > > +}, .profiles = {
> > > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } },
> > > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 10, 12, 15, 0 } },
> > > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 15, 15, 18, 0 } },
> > > > +}};
> > > > +
> > > > +/* 18W class: AYANEO AIR Plus/Pro (Ryzen 5 5560U, Ryzen 7 5825U) */
> > > > +static const struct dptc_device_limits limits_18w = { .params = {
> > > > + [DPTC_PPT_PL1_SPL] = { 0, 5, 15, 18, 22 },
> > > > + [DPTC_PPT_PL2_SPPT] = { 0, 5, 15, 18, 22 },
> > > > + [DPTC_PPT_PL3_FPPT] = { 0, 5, 15, 20, 25 },
> > > > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > > > +}, .profiles = {
> > > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5, 5, 8, 0 } },
> > > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 12, 14, 15, 0 } },
> > > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 18, 18, 20, 0 } },
> > > > +}};
> > > > +
> > > > +/* 25W class: Ryzen 5000 handhelds (AYANEO NEXT, KUN) */
> > > > +static const struct dptc_device_limits limits_25w = { .params = {
> > > > + [DPTC_PPT_PL1_SPL] = { 0, 4, 15, 25, 32 },
> > > > + [DPTC_PPT_PL2_SPPT] = { 0, 4, 20, 27, 35 },
> > > > + [DPTC_PPT_PL3_FPPT] = { 0, 4, 25, 30, 37 },
> > > > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > > > +}, .profiles = {
> > > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } },
> > > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 20, 0 } },
> > > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 27, 30, 0 } },
> > > > +}};
> > > > +
> > > > +/* 28W class: GPD Win series, AYANEO 2, OrangePi NEO-01 */
> > > > +static const struct dptc_device_limits limits_28w = { .params = {
> > > > + [DPTC_PPT_PL1_SPL] = { 0, 4, 15, 28, 32 },
> > > > + [DPTC_PPT_PL2_SPPT] = { 0, 4, 20, 30, 35 },
> > > > + [DPTC_PPT_PL3_FPPT] = { 0, 4, 25, 32, 37 },
> > > > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > > > +}, .profiles = {
> > > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } },
> > > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 22, 0 } },
> > > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 32, 0 } },
> > > > +}};
> > > > +
> > > > +/* 30W class: OneXPlayer, AYANEO FLIP/GEEK/SLIDE/3, AOKZOE */
> > > > +static const struct dptc_device_limits limits_30w = { .params = {
> > > > + [DPTC_PPT_PL1_SPL] = { 0, 4, 15, 30, 40 },
> > > > + [DPTC_PPT_PL2_SPPT] = { 0, 4, 20, 32, 43 },
> > > > + [DPTC_PPT_PL3_FPPT] = { 0, 4, 25, 41, 50 },
> > > > + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> > > > +}, .profiles = {
> > > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8, 8, 12, 0 } },
> > > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15, 17, 25, 0 } },
> > > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25, 28, 41, 0 } },
> > > > +}};
> > > > +
> > > > /* AI MAX Handheld class: GPD Win 5 */
> > > > static const struct dptc_device_limits limits_maxhh = { .params = {
> > > > [DPTC_PPT_PL1_SPL] = { 0, 4, 25, 80, 100 },
> > > > @@ -103,11 +163,60 @@ static const char * const dptc_soc_table[] = {
> > > > "AMD RYZEN AI MAX+ 395",
> > > > "AMD RYZEN AI MAX+ 385",
> > > > "AMD RYZEN AI MAX 380",
> > > > + /* Ryzen AI */
> > > > + "AMD Ryzen AI 9 HX 370",
> > > > + "AMD Ryzen AI HX 360",
> > >
> > > What's "AMD Ryzen AI HX 360"? Did you mean "AMD Ryzen AI 9 H 365"
> > > (Chinese-specific model id) and "AMD Ryzen AI 9 365" (other regions)?
> >
> > Hm, goot point. I seem to have added this preemptively looking at my
> > commit logs earlier this year.
> >
> > It might not be the exact name. Since we do SoC + DMI in this driver
> > it might cause some devices with the chip to fail to load.
> >
> > Some HX360 handhelds did ship I think, but I never got bug reports
> > about it missing. I want to say 360 is ryzen 7, at least looking
> > online. Are you sure it's 9?
>
> Did you mean asking your AI when you said "looking online"? There's no
> *HX* 360. If you want `Ryzen AI * H* 3*', online search gives you
> "Ryzen AI 9 HX 370" and "Ryzen AI 9/7/5 H 365/350/340/330" ("H" is
> Chinese-specific). If you want `Ryzen AI * 360', online search gives
> you "Ryzen AI 7 PRO 360" -- it's "PRO" instead of "HX".
Yes, that was what I found. I have a stale entry in my userspace
software from last february [1]. However, no users complained about
missing TDP controls, so I am not sure what to make of it.
Marketing name can be different from the code name in the CPU.
Since my software did hybrid SoC or DMI matching, I cannot
disambiguate them now.
This driver does SoC AND DMI for safety (most devices did SoC since as
you can see in this driver, there are only 4 profiles and CPU matches
are correct for devices other than GPD), so there is a chance we will
propagate errors and some devices will miss TDP controls initially. I
am unsure about 36X series, most users went with H370 or 8840U for
that gen. It is very rare to see those in the wild.
Antheas
[1] https://github.com/hhd-dev/hhd/commit/c2f3fdbd1488a19deef67846bf30c33d6dcd94c8
> Thanks,
> Rong
>
> >
> > Antheas
> >
> > > > + /* Z1 - Extreme before plain Z1 */
> > > > + "AMD Ryzen Z1 Extreme",
> > > > + "AMD Ryzen Z1",
> > >
> > > You don't need both since strstr("AMD Ryzen Z1") matches "AMD Ryzen Z1
> > > Extreme".
> > >
> > > Thanks,
> > > Rong
> > >
> > > > + /* 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 = {
> > > > @@ -116,6 +225,306 @@ static const struct dmi_system_id dptc_dmi_table[] = {
> > > > },
> > > > .driver_data = (void *)&limits_maxhh,
> > > > },
> > > > + {
> > > > + .ident = "GPD Win Max 2",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-04"),
> > > > + },
> > > > + .driver_data = (void *)&limits_28w,
> > > > + },
> > > > + {
> > > > + .ident = "GPD Win Max 2 2024",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-05"),
> > > > + },
> > > > + .driver_data = (void *)&limits_28w,
> > > > + },
> > > > + {
> > > > + .ident = "GPD Duo",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01"),
> > > > + },
> > > > + .driver_data = (void *)&limits_28w,
> > > > + },
> > > > + {
> > > > + .ident = "GPD Duo",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01-L"),
> > > > + },
> > > > + .driver_data = (void *)&limits_28w,
> > > > + },
> > > > + {
> > > > + .ident = "GPD Pocket 4",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04"),
> > > > + },
> > > > + .driver_data = (void *)&limits_28w,
> > > > + },
> > > > + {
> > > > + .ident = "GPD Pocket 4",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04-L"),
> > > > + },
> > > > + .driver_data = (void *)&limits_28w,
> > > > + },
> > > > + /* OrangePi */
> > > > + {
> > > > + .ident = "OrangePi NEO-01",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"),
> > > > + },
> > > > + .driver_data = (void *)&limits_28w,
> > > > + },
> > > > + /* AYN */
> > > > + {
> > > > + .ident = "AYN Loki Max",
> > > > + .matches = {
> > > > + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ayn"),
> > > > + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Loki Max"),
> > > > + },
> > > > + .driver_data = (void *)&limits_30w,
> > > > + },
> > > > + /* Tectoy (Zeenix Pro = Loki Max) */
> > > > + {
> > > > + .ident = "Zeenix Pro",
> > > > + .matches = {
> > > > + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Tectoy"),
> > > > + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Zeenix Pro"),
> > > > + },
> > > > + .driver_data = (void *)&limits_30w,
> > > > + },
> > > > + /* AOKZOE */
> > > > + {
> > > > + .ident = "AOKZOE A1 AR07",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 AR07"),
> > > > + },
> > > > + .driver_data = (void *)&limits_30w,
> > > > + },
> > > > + {
> > > > + .ident = "AOKZOE A1 Pro",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 Pro"),
> > > > + },
> > > > + .driver_data = (void *)&limits_30w,
> > > > + },
> > > > + {
> > > > + .ident = "AOKZOE A1X",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1X"),
> > > > + },
> > > > + .driver_data = (void *)&limits_30w,
> > > > + },
> > > > + {
> > > > + .ident = "AOKZOE A2 Pro",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A2 Pro"),
> > > > + },
> > > > + .driver_data = (void *)&limits_30w,
> > > > + },
> > > > + /* OneXPlayer (Intel variants filtered by SoC table) */
> > > > + {
> > > > + .ident = "ONEXPLAYER F1Pro",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1Pro"),
> > > > + },
> > > > + .driver_data = (void *)&limits_30w,
> > > > + },
> > > > + {
> > > > + .ident = "ONEXPLAYER F1 EVA-02",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1 EVA-02"),
> > > > + },
> > > > + .driver_data = (void *)&limits_30w,
> > > > + },
> > > > + {
> > > > + .ident = "ONEXPLAYER 2",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > > > + DMI_MATCH(DMI_BOARD_NAME, "ONEXPLAYER 2"),
> > > > + },
> > > > + .driver_data = (void *)&limits_30w,
> > > > + },
> > > > + {
> > > > + .ident = "ONEXPLAYER X1 A",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1 A"),
> > > > + },
> > > > + .driver_data = (void *)&limits_30w,
> > > > + },
> > > > + {
> > > > + .ident = "ONEXPLAYER X1z",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1z"),
> > > > + },
> > > > + .driver_data = (void *)&limits_30w,
> > > > + },
> > > > + {
> > > > + .ident = "ONEXPLAYER X1Pro",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Pro"),
> > > > + },
> > > > + .driver_data = (void *)&limits_30w,
> > > > + },
> > > > + {
> > > > + .ident = "ONEXPLAYER G1 A",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 A"),
> > > > + },
> > > > + .driver_data = (void *)&limits_30w,
> > > > + },
> > > > + /* AYANEO - 15W */
> > > > + {
> > > > + .ident = "AYANEO AIR",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR"),
> > > > + },
> > > > + .driver_data = (void *)&limits_15w,
> > > > + },
> > > > + /* AYANEO - 18W */
> > > > + {
> > > > + .ident = "AYANEO AIR Plus",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Plus"),
> > > > + },
> > > > + .driver_data = (void *)&limits_18w,
> > > > + },
> > > > + {
> > > > + .ident = "AYANEO AIR Pro",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Pro"),
> > > > + },
> > > > + .driver_data = (void *)&limits_18w,
> > > > + },
> > > > + /* AYANEO - 25W */
> > > > + {
> > > > + .ident = "AYANEO AIR 1S",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > > + DMI_MATCH(DMI_BOARD_NAME, "AIR 1S"),
> > > > + },
> > > > + .driver_data = (void *)&limits_25w,
> > > > + },
> > > > + {
> > > > + .ident = "AYANEO NEXT Advance",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Advance"),
> > > > + },
> > > > + .driver_data = (void *)&limits_25w,
> > > > + },
> > > > + {
> > > > + .ident = "AYANEO NEXT Lite",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Lite"),
> > > > + },
> > > > + .driver_data = (void *)&limits_25w,
> > > > + },
> > > > + {
> > > > + .ident = "AYANEO NEXT Pro",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Pro"),
> > > > + },
> > > > + .driver_data = (void *)&limits_25w,
> > > > + },
> > > > + {
> > > > + .ident = "AYANEO NEXT",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT"),
> > > > + },
> > > > + .driver_data = (void *)&limits_25w,
> > > > + },
> > > > + {
> > > > + .ident = "AYANEO KUN",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"),
> > > > + },
> > > > + .driver_data = (void *)&limits_25w,
> > > > + },
> > > > + {
> > > > + .ident = "AYANEO KUN",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO KUN"),
> > > > + },
> > > > + .driver_data = (void *)&limits_25w,
> > > > + },
> > > > + /* AYANEO - 28W */
> > > > + {
> > > > + .ident = "AYANEO 2",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > > + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 2"),
> > > > + },
> > > > + .driver_data = (void *)&limits_28w,
> > > > + },
> > > > + {
> > > > + .ident = "SuiPlay0X1",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_SYS_VENDOR, "Mysten Labs, Inc."),
> > > > + DMI_MATCH(DMI_PRODUCT_NAME, "SuiPlay0X1"),
> > > > + },
> > > > + .driver_data = (void *)&limits_28w,
> > > > + },
> > > > + /* AYANEO - 30W */
> > > > + {
> > > > + /* DMI_MATCH catches all FLIP variants (DS, KB, 1S DS, 1S KB) */
> > > > + .ident = "AYANEO FLIP",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > > + DMI_MATCH(DMI_BOARD_NAME, "FLIP"),
> > > > + },
> > > > + .driver_data = (void *)&limits_30w,
> > > > + },
> > > > + {
> > > > + /* DMI_MATCH catches GEEK and GEEK 1S */
> > > > + .ident = "AYANEO GEEK",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > > + DMI_MATCH(DMI_BOARD_NAME, "GEEK"),
> > > > + },
> > > > + .driver_data = (void *)&limits_30w,
> > > > + },
> > > > + {
> > > > + .ident = "AYANEO SLIDE",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "SLIDE"),
> > > > + },
> > > > + .driver_data = (void *)&limits_30w,
> > > > + },
> > > > + {
> > > > + .ident = "AYANEO 3",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> > > > + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO 3"),
> > > > + },
> > > > + .driver_data = (void *)&limits_30w,
> > > > + },
> > > > { }
> > > > };
> > > > MODULE_DEVICE_TABLE(dmi, dptc_dmi_table);
> > >
>
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [RFC v3 3/4] platform/x86/amd: dptc: Add platform profile support
2026-03-08 9:30 ` Rong Zhang
@ 2026-03-08 9:45 ` Antheas Kapenekakis
0 siblings, 0 replies; 19+ messages in thread
From: Antheas Kapenekakis @ 2026-03-08 9:45 UTC (permalink / raw)
To: Rong Zhang
Cc: Mario.Limonciello, W_Armin, sashal, Shyam-Sundar.S-k,
derekjohn.clark, denis.benato, linux-kernel, platform-driver-x86
On Sun, 8 Mar 2026 at 10:36, Rong Zhang <i@rong.moe> wrote:
>
> Hi Antheas,
>
> On Sat, 2026-03-07 at 23:45 +0100, Antheas Kapenekakis wrote:
> > On Sat, 7 Mar 2026 at 22:57, Rong Zhang <i@rong.moe> wrote:
> > >
> > > Hi Antheas,
> > >
> > > On Sat, 2026-03-07 at 12:55 +0100, Antheas Kapenekakis wrote:
> > > > Register a platform_profile handler so the driver exposes standard
> > > > power profiles (low-power, balanced, performance, max-power) alongside
> > > > the manual tunable interface. max-power is only supplied when the
> > > > performance profile's preset values are below the tunable limits.
> > > >
> > > > When a non-custom profile is active, parameter writes are blocked
> > > > (-EPERM) and current_value reflects the profile's preset values.
> > > > Selecting the "custom" profile returns control to the user for manual
> > > > staging and committing. On resume, the active profile is automatically
> > > > re-applied.
> > > >
> > > > Assisted-by: Claude:claude-opus-4-6
> > > > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> > > > ---
> > > > drivers/platform/x86/amd/Kconfig | 1 +
> > > > drivers/platform/x86/amd/dptc.c | 116 +++++++++++++++++++++++++++++--
> > > > 2 files changed, 113 insertions(+), 4 deletions(-)
> > > >
> > > > diff --git a/drivers/platform/x86/amd/Kconfig b/drivers/platform/x86/amd/Kconfig
> > > > index d610092467fc..41ffbd722524 100644
> > > > --- a/drivers/platform/x86/amd/Kconfig
> > > > +++ b/drivers/platform/x86/amd/Kconfig
> > > > @@ -48,6 +48,7 @@ config AMD_ISP_PLATFORM
> > > > config AMD_DPTC
> > > > tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)"
> > > > depends on X86_64 && ACPI && DMI
> > > > + select ACPI_PLATFORM_PROFILE
> > > > select FIRMWARE_ATTRIBUTES_CLASS
> > > > help
> > > > Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> > > > diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
> > > > index acfe9cc01bab..1f07e2e6f278 100644
> > > > --- a/drivers/platform/x86/amd/dptc.c
> > > > +++ b/drivers/platform/x86/amd/dptc.c
> > > > @@ -23,6 +23,7 @@
> > > > #include <linux/module.h>
> > > > #include <linux/mutex.h>
> > > > #include <linux/platform_device.h>
> > > > +#include <linux/platform_profile.h>
> > > > #include <linux/processor.h>
> > > > #include <linux/sysfs.h>
> > > > #include <linux/unaligned.h>
> > > > @@ -56,8 +57,13 @@ struct dptc_param_limits {
> > > > u32 expanded_max;
> > > > };
> > > >
> > > > +struct dptc_profile {
> > > > + u32 vals[DPTC_NUM_PARAMS]; /* 0 = don't set / unstage this param */
> > > > +};
> > > > +
> > > > struct dptc_device_limits {
> > > > struct dptc_param_limits params[DPTC_NUM_PARAMS];
> > > > + struct dptc_profile profiles[PLATFORM_PROFILE_LAST];
> > > > };
> > > >
> > > > struct dptc_param_desc {
> > > > @@ -85,6 +91,10 @@ static const struct dptc_device_limits limits_maxhh = { .params = {
> > > > [DPTC_PPT_PL2_SPPT] = { 0, 4, 27, 82, 100 },
> > > > [DPTC_PPT_PL3_FPPT] = { 0, 4, 40, 85, 100 },
> > > > [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 },
> > > > +}, .profiles = {
> > > > + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 15, 15, 25, 0 } },
> > > > + [PLATFORM_PROFILE_BALANCED] = { .vals = { 25, 27, 40, 0 } },
> > > > + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 60, 63, 85, 0 } },
> > > > }};
> > > >
> > > > /* Substring matches against boot_cpu_data.x86_model_id; order matters. */
> > > > @@ -134,6 +144,9 @@ struct dptc_priv {
> > > >
> > > > 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];
> > > > @@ -269,6 +282,14 @@ static ssize_t dptc_current_value_show(struct kobject *kobj,
> > > >
> > > > guard(mutex)(&dptc->lock);
> > > >
> > > > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
> > > > + u32 val = dptc->dev_limits->profiles[dptc->profile].vals[ps->idx];
> > > > +
> > > > + if (!val)
> > > > + return sysfs_emit(buf, "\n");
> > > > + return sysfs_emit(buf, "%u\n", val);
> > > > + }
> > > > +
> > > > if (!dptc->has_staged[ps->idx])
> > > > return sysfs_emit(buf, "\n");
> > > > return sysfs_emit(buf, "%u\n", dptc->staged[ps->idx]);
> > > > @@ -287,6 +308,8 @@ static ssize_t dptc_current_value_store(struct kobject *kobj,
> > > > if (count == 1 && buf[0] == '\n') {
> > > > guard(mutex)(&dptc->lock);
> > > >
> > > > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> > > > + return -EPERM;
> > >
> > > It has nothing to do with permissions. I'd prefer -EBUSY.
> >
> > I will defer to other drivers and update if necessary based on their choices.
>
> Here it isn't about consistency, but correctness. If something has
> nothing to do with permissions, -EPERM is confusing. -EBUSY tells users
> that the interface is under some other thing's (i.e., platform profile)
> control.
Both EBUSY and EPERM are non-ideal. I will edge towards EBUSY or
implement the error of the other drivers if it is different as it is
precedent.
> >
> > > To maintain readability, usually a blank line should be added after
> > > returning early.
> > >
> > > > dptc->has_staged[ps->idx] = false;
> > > > return count;
> > > > }
> > > > @@ -297,6 +320,8 @@ static ssize_t dptc_current_value_store(struct kobject *kobj,
> > > >
> > > > 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))
> > > > @@ -426,6 +451,8 @@ static ssize_t dptc_expanded_current_value_store(struct kobject *kobj,
> > > >
> > > > guard(mutex)(&dptc->lock);
> > > >
> > > > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> > > > + return -EPERM;
> > >
> > > IMO kstrtou32() isn't really a blocker of acquiring the mutex earlier
> > > to prevent duplicating the same check. After all, firmware-attributes
> > > is not a performance critical interface, and dptc_save_settings_store()
> > > also calls sysfs_eq() with the mutex held.
> > >
> > > > dptc->expanded = val;
> > > > /* Clear staged values: limits changed, old values may be out of range */
> > > > memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
> > > > @@ -594,6 +621,78 @@ 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);
> > >
> > > I'd prefer simply calling dptc_alib_save() in dptc_apply_profile()
> > > since the latter already checks the profile.
> >
> > This way you will propagate an error because custom unstages all values.
>
> I meant:
>
> static int 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 0; /* You've checked this and returned early!
> */
>
> 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;
> }
>
> return dptc_alib_save(dptc);
> }
>
> How can an error being propagated?
I missed that. I inferred your comment as suggesting to remove the `
if (profile != PLATFORM_PROFILE_CUSTOM)` block, which would propagate
an error. I will consider consolidating apply_profile.
Antheas
> >
> > Antheas
> >
> > > > +
> > > > + 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);
> > > > @@ -601,10 +700,14 @@ static int dptc_resume(struct device *dev)
> > > >
> > > > guard(mutex)(&dptc->lock);
> > > >
> > > > - if (dptc->save_mode == SAVE_SINGLE)
> > > > + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
> > > > + dptc_apply_profile(dptc, dptc->profile);
> > > > ret = dptc_alib_save(dptc);
> > > > - else
> > > > + } 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);
> > > > @@ -636,8 +739,7 @@ static int dptc_probe(struct platform_device *pdev)
> > > > boot_cpu_data.x86_model_id);
> > > >
> > > > dptc->fw_attr_dev = device_create(&firmware_attributes_class,
> > > > - NULL, MKDEV(0, 0), NULL,
> > > > - DRIVER_NAME);
> > > > + NULL, MKDEV(0, 0), NULL, DRIVER_NAME);
> > >
> > > Squash the change into the previous patch.
> > >
> > > Thanks,
> > > Rong
> > >
> > > > if (IS_ERR(dptc->fw_attr_dev))
> > > > return PTR_ERR(dptc->fw_attr_dev);
> > > >
> > > > @@ -680,6 +782,12 @@ static int dptc_probe(struct platform_device *pdev)
> > > > if (ret)
> > > > return ret;
> > > >
> > > > + dptc->profile = PLATFORM_PROFILE_CUSTOM;
> > > > + dptc->ppdev = devm_platform_profile_register(dev, "amd-dptc", dptc,
> > > > + &dptc_pp_ops);
> > > > + if (IS_ERR(dptc->ppdev))
> > > > + return PTR_ERR(dptc->ppdev);
> > > > +
> > > > return 0;
> > > > }
> > > >
> > >
>
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [RFC v3 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver
2026-03-07 23:08 ` Antheas Kapenekakis
@ 2026-03-08 20:00 ` Rong Zhang
2026-03-08 22:15 ` Antheas Kapenekakis
0 siblings, 1 reply; 19+ messages in thread
From: Rong Zhang @ 2026-03-08 20:00 UTC (permalink / raw)
To: Antheas Kapenekakis
Cc: Mario.Limonciello, W_Armin, sashal, Shyam-Sundar.S-k,
derekjohn.clark, denis.benato, linux-kernel, platform-driver-x86
[-- Attachment #1: Type: text/plain, Size: 42348 bytes --]
Hi Antheas,
On Sun, 2026-03-08 at 00:08 +0100, Antheas Kapenekakis wrote:
> On Sun, 8 Mar 2026 at 00:02, Antheas Kapenekakis <lkml@antheas.dev> wrote:
> >
> > On Sat, 7 Mar 2026 at 22:56, Rong Zhang <i@rong.moe> wrote:
> > >
> > > Hi Antheas,
> > >
> > > Thanks for working on it.
> > >
> > > I think this driver is useful for me and I will see if I can test it on
> > > my devices once I find some time. They're not handhelds but I am sure
> > > at least one of them has the ALIB method. It'd better if a module
> > > option can be used to bypass the quirk table so that testing against a
> > > new device will be easier. Such an option should taint the kernel ofc.
> >
> > I will defer to Mario for this one. Previous iterations of this series
> > had an override. Before I posted V2 I had a module param that allowed
> > matching by ident.
Sure. That's not really a problem for me as I can easily patch and
compile it myself. However, it makes a lot of sense if we want user's
test results on new devices. Here's my two cents on such a module
option :-P
>
> Hi, 2 small follow-ups. Entries to this driver will only be added for
> devices without EC/amd_pmf handling. Because those interfere. So it
> does not apply to lenovo devices AFAIK.
Yeah I knew that. I have a non-Lenovo device with ALIB and no EC/PMF.
>
> > > That being said, I have some review comments. Please see below and my
> > > following replies.
> > >
> > > On Sat, 2026-03-07 at 12:55 +0100, Antheas Kapenekakis wrote:
> > > > Add a driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> > > > Thermal Configuration interface (DPTCi). This exposes TDP and thermal
> > > > parameters for AMD APU-based handheld devices via the
> > > > firmware-attributes sysfs ABI.
> > > >
> > > > Parameters are staged and atomically committed through ALIB. The driver
> > > > supports two save modes: "single" (apply immediately on write) and
> > > > "bulk" (stage values, then commit with "save"). An "expanded_limits"
> > > > toggle widens the allowed parameter ranges beyond device defaults.
> > > >
> > > > Initial device support: GPD Win 5 (AMD Ryzen AI MAX).
> > > >
> > > > Assisted-by: Claude:claude-opus-4-6
> > > > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> > > > ---
> > > > MAINTAINERS | 6 +
> > > > drivers/platform/x86/amd/Kconfig | 14 +
> > > > drivers/platform/x86/amd/Makefile | 2 +
> > > > drivers/platform/x86/amd/dptc.c | 747 ++++++++++++++++++++++++++++++
> > > > 4 files changed, 769 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..d610092467fc 100644
> > > > --- a/drivers/platform/x86/amd/Kconfig
> > > > +++ b/drivers/platform/x86/amd/Kconfig
> > > > @@ -44,3 +44,17 @@ config AMD_ISP_PLATFORM
> > > >
> > > > This driver can also be built as a module. If so, the module
> > > > will be called amd_isp4.
> > > > +
> > > > +config AMD_DPTC
> > > > + tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)"
> > > > + depends on X86_64 && ACPI && DMI
> > > > + select FIRMWARE_ATTRIBUTES_CLASS
> > > > + help
> > > > + Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> > > > + Thermal Configuration Interface (DPTCi). Exposes TDP and thermal
> > > > + parameters for AMD APU-based handheld devices via the
> > > > + firmware-attributes sysfs ABI, allowing userspace tools to stage
> > > > + and atomically commit power limit settings. Requires a DMI match
> > > > + for the device and a recognized AMD SoC.
> > > > +
> > > > + If built as a module, the module will be called amd_dptc.
> > > > diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile
> > > > index f6ff0c837f34..862a609bfe38 100644
> > > > --- a/drivers/platform/x86/amd/Makefile
> > > > +++ b/drivers/platform/x86/amd/Makefile
> > > > @@ -12,3 +12,5 @@ obj-$(CONFIG_AMD_PMF) += pmf/
> > > > obj-$(CONFIG_AMD_WBRF) += wbrf.o
> > > > obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o
> > > > obj-$(CONFIG_AMD_HFI) += hfi/
> > > > +obj-$(CONFIG_AMD_DPTC) += amd_dptc.o
> > > > +amd_dptc-y := dptc.o
> > > > diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
> > > > new file mode 100644
> > > > index 000000000000..acfe9cc01bab
> > > > --- /dev/null
> > > > +++ b/drivers/platform/x86/amd/dptc.c
> > > > @@ -0,0 +1,747 @@
> > > > +// SPDX-License-Identifier: GPL-2.0-only
> > > > +/*
> > > > + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver
> > > > + *
> > > > + * Exposes AMD APU power and thermal parameters via the firmware-attributes
> > > > + * sysfs ABI. Parameters are staged and atomically committed through the
> > > > + * AGESA ALIB Function 0x0C (Dynamic Power and Thermal Configuration
> > > > + * interface).
> > > > + *
> > > > + * Reference: AMD AGESA Publication #44065, Appendix E.5
> > > > + * https://docs.amd.com/v/u/en-US/44065_Arch2008
> > > > + *
> > > > + * Copyright (C) 2026 Antheas Kapenekakis <lkml@antheas.dev>
> > > > + */
> > > > +
> > > > +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> > > > +
> > > > +#include <linux/acpi.h>
> > > > +#include <linux/cleanup.h>
> > > > +#include <linux/dmi.h>
> > > > +#include <linux/init.h>
> > > > +#include <linux/kobject.h>
> > > > +#include <linux/module.h>
> > > > +#include <linux/mutex.h>
> > > > +#include <linux/platform_device.h>
> > > > +#include <linux/processor.h>
> > > > +#include <linux/sysfs.h>
> > > > +#include <linux/unaligned.h>
> > > > +
> > > > +#include "../firmware_attributes_class.h"
> > > > +
> > > > +#define DRIVER_NAME "amd-dptc"
> > > > +
> > > > +#define ALIB_FUNC_DPTC 0x0C
> > > > +#define ALIB_PATH "\\_SB.ALIB"
> > > > +
> > > > +#define ALIB_ID_TEMP_TARGET 0x03
> > > > +#define ALIB_ID_STAPM_LIMIT 0x05
> > > > +#define ALIB_ID_FAST_LIMIT 0x06
> > > > +#define ALIB_ID_SLOW_LIMIT 0x07
> > > > +#define ALIB_ID_SKIN_LIMIT 0x2E
> > > > +
> > > > +enum dptc_param_idx {
> > > > + DPTC_PPT_PL1_SPL, /* STAPM + skin limit (set together) */
> > > > + DPTC_PPT_PL2_SPPT, /* slow PPT limit */
> > > > + DPTC_PPT_PL3_FPPT, /* fast PPT limit */
> > > > + DPTC_CPU_TEMP, /* thermal control target */
> > > > + DPTC_NUM_PARAMS,
> > > > +};
> > > > +
> > > > +struct dptc_param_limits {
> > > > + u32 expanded_min;
> > > > + u32 device_min;
> > > > + u32 def;
> > > > + u32 device_max;
> > > > + u32 expanded_max;
> > > > +};
> > > > +
> > > > +struct dptc_device_limits {
> > > > + struct dptc_param_limits params[DPTC_NUM_PARAMS];
> > > > +};
> > > > +
> > > > +struct dptc_param_desc {
> > > > + const char *name;
> > > > + const char *display_name;
> > > > + u16 scale; /* sysfs-to-ALIB multiplier (e.g. 1000 for W->mW) */
> > > > + u8 param_id;
> > > > + u8 param_id2; /* secondary ALIB ID, 0 if none */
> > > > +};
> > > > +
> > > > +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = {
> > > > + [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (W)",
> > > > + 1000, ALIB_ID_STAPM_LIMIT, ALIB_ID_SKIN_LIMIT },
> > > > + [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (W)",
> > > > + 1000, ALIB_ID_SLOW_LIMIT },
> > > > + [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (W)",
> > > > + 1000, ALIB_ID_FAST_LIMIT },
> > > > + [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)",
> > > > + 1, ALIB_ID_TEMP_TARGET },
> > > > +};
> > >
> > > The indent here just made me crazy :-/
> > >
> > > Align the contents after line breaks to the left braces.
> >
> > I will review how to do the alignment, although I'm not happy with how
> > aligning to spaces will look. PL1 will overflow.
No, it won't. Having four tabs before `1000' gives you a line length of
80 characters (editors will tell that you are at column 81). And
nowadays the maximum line length allowed is 100 (that's why Derek
suggested you should avoid some unnecessary wraps).
> >
> > > > +
> > > > +/* AI MAX Handheld class: GPD Win 5 */
> > > > +static const struct dptc_device_limits limits_maxhh = { .params = {
> > >
> > > Please don't save a line by zipping braces like this. It's hard to read
> > > and likely to be missed when glancing the code (that's what happened to
> > > me and it really confused me).
> > >
> > > I found you did the same in [PATCH v3 3/4] for `.profiles'. That should
> > > be avoided too.
> >
> > This is a leftover from before adding profiles. I guess it does not
> > make much sense now. I can reflow.
> >
> > > > + [DPTC_PPT_PL1_SPL] = { 0, 4, 25, 80, 100 },
> > > > + [DPTC_PPT_PL2_SPPT] = { 0, 4, 27, 82, 100 },
> > > > + [DPTC_PPT_PL3_FPPT] = { 0, 4, 40, 85, 100 },
> > > > + [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 },
> > > > +}};
> > > > +
> > > > +/* Substring matches against boot_cpu_data.x86_model_id; order matters. */
> > > > +static const char * const dptc_soc_table[] = {
> > > > + /* AI MAX */
> > > > + "AMD RYZEN AI MAX+ 395",
> > > > + "AMD RYZEN AI MAX+ 385",
> > > > + "AMD RYZEN AI MAX 380",
> > > > + NULL,
> > > > +};
> > > > +
> > > > +static const struct dmi_system_id dptc_dmi_table[] = {
> > > > + /* GPD */
> > > > + {
> > > > + .ident = "GPD Win 5",
> > > > + .matches = {
> > > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"),
> > > > + },
> > > > + .driver_data = (void *)&limits_maxhh,
> > > > + },
> > > > + { }
> > > > +};
> > > > +MODULE_DEVICE_TABLE(dmi, dptc_dmi_table);
> > > > +
> > > > +struct dptc_priv;
> > > > +
> > > > +struct dptc_attr_sysfs {
> > > > + struct dptc_priv *priv;
> > > > + struct kobj_attribute current_value;
> > > > + struct kobj_attribute default_value;
> > > > + struct kobj_attribute min_value;
> > > > + struct kobj_attribute max_value;
> > > > + struct kobj_attribute scalar_increment;
> > > > + struct kobj_attribute display_name;
> > > > + struct kobj_attribute type;
> > > > + struct attribute *attrs[8];
> > > > + struct attribute_group group;
> > > > + int idx;
> > > > +};
> > > > +
> > > > +struct dptc_priv {
> > > > + struct device *fw_attr_dev;
> > > > + struct kset *fw_attr_kset;
> > > > +
> > > > + const struct dptc_device_limits *dev_limits;
> > > > +
> > > > + bool expanded;
> > > > +
> > > > + enum dptc_save_mode { SAVE_SINGLE, SAVE_BULK } save_mode;
> > >
> > > Defining the enumeration separately will improve readability.
> > >
> > > > +
> > > > + u32 staged[DPTC_NUM_PARAMS];
> > > > + bool has_staged[DPTC_NUM_PARAMS];
> > > > +
> > > > + /* Protects mutable driver state */
> > >
> > > The comment is nonsense. To make it helpful, it should be "Protects
> > > members above/below" with properly arranged structure layout.
> > >
> > > > + struct mutex lock;
> > > > +
> > > > + struct dptc_attr_sysfs params[DPTC_NUM_PARAMS];
> > > > + struct dptc_attr_sysfs expanded_attr;
> > > > + struct kobj_attribute save_settings_attr;
> > > > +};
> > > > +
> > > > +static struct platform_device *dptc_pdev;
> > > > +
> > > > +static u32 dptc_get_min(struct dptc_priv *dptc, int idx)
> > > > +{
> > > > + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_min
> > > > + : dptc->dev_limits->params[idx].device_min;
> > > > +}
> > > > +
> > > > +static u32 dptc_get_max(struct dptc_priv *dptc, int idx)
> > > > +{
> > > > + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_max
> > > > + : dptc->dev_limits->params[idx].device_max;
> > > > +}
> > > > +
> > > > +static u32 dptc_get_default(struct dptc_priv *dptc, int idx)
> > > > +{
> > > > + return dptc->dev_limits->params[idx].def;
> > > > +}
> > > > +
> > > > +static int dptc_alib_call(const u8 *ids, const u32 *vals, int count)
> > > > +{
> > > > + union acpi_object in_params[2];
> > > > + struct acpi_object_list input;
> > > > + acpi_status status;
> > > > + u32 buf_size;
> > > > + int i, off;
> > > > + u8 *buf;
> > > > +
> > > > + if (count == 0)
> > > > + return -EINVAL;
> > >
> > > Return -ENOENT as think-lmi does (see its save_settings_store()) and
> > > update the documentation accordingly.
> > >
> > > > +
> > > > + /* 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);
> > >
> > > 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;
> > >
> > > Misaligned `='.
> > >
> > > > +
> > > > + 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 hw_val = val * dptc_params[idx].scale;
> > > > + u32 vals[2];
> > > > + u8 ids[2];
> > > > + int count = 0;
> > > > +
> > > > + ids[count] = dptc_params[idx].param_id;
> > > > + vals[count] = hw_val;
> > > > + count++;
> > > > + if (dptc_params[idx].param_id2) {
> > > > + ids[count] = dptc_params[idx].param_id2;
> > > > + vals[count] = hw_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;
> > > > + u32 hw_val;
> > > > +
> > > > + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> > > > + if (!dptc->has_staged[i])
> > > > + continue;
> > > > +
> > > > + hw_val = dptc->staged[i] * dptc_params[i].scale;
> > > > + ids[count] = dptc_params[i].param_id;
> > > > + vals[count] = hw_val;
> > > > + count++;
> > > > + if (dptc_params[i].param_id2) {
> > > > + ids[count] = dptc_params[i].param_id2;
> > > > + vals[count] = hw_val;
> > > > + count++;
> > > > + }
> > >
> > > I could imagine a helper function to prevent replicating the same code
> > > patterns twice:
> > >
> > > static size_t dptc_alib_fill_param(u8 *ids, u32 *vals, size_t offset,
> > > enum dptc_param_idx param, u32 val)
> > > {
> > > u32 hw_val = val * dptc_params[param].scale;
> > >
> > > ids[offset] = dptc_params[param].param_id;
> > > vals[offset++] = hw_val;
> > >
> > > if (dptc_params[param].param_id2) {
> > > ids[offset] = dptc_params[param].param_id2;
> > > vals[offset++] = hw_val;
> > > }
> > >
> > > return offset;
> > > }
> > >
> > > > + }
> > > > +
> > > > + 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->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);
> > > > +
> > > > + dptc->has_staged[ps->idx] = false;
> > > > + return count;
> > > > + }
> > > > +
> > > > + ret = kstrtou32(buf, 10, &val);
> > > > + if (ret)
> > > > + return ret;
> > > > +
> > > > + guard(mutex)(&dptc->lock);
> > > > +
> > > > + 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"))
> > >
> > > Please don't rely on the `count == 0' check in dptc_alib_call(). `dptc-
> > > > save_mode == SAVE_SINGLE' should result in -EOPNOTSUPP to maintain
> > > consistency with think-lmi (update the documentation accordingly).
> >
> > I will consider it. But this is subtractive behavior. We are still ABI
> > compliant even if the write goes through and it does not affect
> > functionality.
If you return something different in a certain case, it's neither API
compatible nor ABI compatible.
Also the issue here is more about you shouldn't rely on the `count ==
0' check in dptc_alib_call() to prevent running into unexpected state
when `dptc->save_mode == SAVE_SINGLE' but "save" is written. Explicit
checks are better than implicit ones.
> >
> > > > + ret = dptc_alib_save(dptc);
> > > > + else if (sysfs_streq(buf, "single"))
> > > > + dptc->save_mode = SAVE_SINGLE;
> > > > + else if (sysfs_streq(buf, "bulk"))
> > > > + dptc->save_mode = SAVE_BULK;
> > > > + else
> > > > + return -EINVAL;
> > > > +
> > > > + return ret ? ret : count;
> > > > +}
> > > > +
> > > > +static ssize_t dptc_expanded_current_value_show(struct kobject *kobj,
> > > > + struct kobj_attribute *attr,
> > > > + char *buf)
> > > > +{
> > > > + struct dptc_attr_sysfs *ps =
> > > > + container_of(attr, struct dptc_attr_sysfs, current_value);
> > > > + struct dptc_priv *dptc = ps->priv;
> > > > +
> > > > + guard(mutex)(&dptc->lock);
> > > > +
> > > > + return sysfs_emit(buf, "%d\n", dptc->expanded);
> > > > +}
> > > > +
> > > > +static ssize_t dptc_expanded_current_value_store(struct kobject *kobj,
> > > > + struct kobj_attribute *attr,
> > > > + const char *buf, size_t count)
> > > > +{
> > > > + struct dptc_attr_sysfs *ps =
> > > > + container_of(attr, struct dptc_attr_sysfs, current_value);
> > > > + struct dptc_priv *dptc = ps->priv;
> > > > + bool val;
> > > > + int ret;
> > > > +
> > > > + ret = kstrtobool(buf, &val);
> > > > + if (ret)
> > > > + return ret;
> > > > +
> > > > + guard(mutex)(&dptc->lock);
> > > > +
> > > > + dptc->expanded = val;
> > > > + /* Clear staged values: limits changed, old values may be out of range */
> > > > + memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
> > >
> > > I'd prefer using a bitmap so that you don't need to use memset() to
> > > clear them. It also saves some memory if more parameters are added in
> > > the future.
> >
> > I will review if has_staged needs to exist. Values cannot be 0 anyway.
> > A bit map has other readability issues.
Just a reminder: you may also need to increase expanded_min to 1.
> >
> > > > +
> > > > + 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;
> > > > +}
> > >
> > > As Derek has said in v2, use macros to rearrange them.
> > >
> > > It's painful to read all these things as I'll need to scroll the screen
> > > quite a lot just to read something with a low information density.
> > > That's why there are numerous macros defined in include/linux/sysfs.h.
> > > I could imagine defining macros like DPTC{,_EXPANDED}_ATTR_{RO,RW}.
> >
> > This driver consists of two instantiation blocks. It is not possible
> > to dedup the initialization functions and save space because the
> > function holders and structs that would be needed undo that.
Undo what?
> >
> > The blocks in lenovo other are significantly less readable and harder
> > to write. Perhaps you save around 20 lines. But it is not worth it
> > imo.
>
> I would consider adding them if the helper macros are centralized, but
> I am not adding macros to this driver.
The macros in lenovo-wmi-other are completely readable and are just
some common attribute techniques. Just search for `##_attr|attr_##',
and you'll see a lot of similar patterns.
Also my PoC (see attachment) showed that using macros to allocate them
statically saves 77 lines.
>
> Antheas
>
> > > Meanwhile, I am not very sure why you would have to dynamically
> > > initialize all these attributes. I haven't notice a blocker of defining
> > > them statically at the module-level (see how lenovo-wmi-other did). Am
> > > I missing anything?
> >
> > You are not missing something. There is no benefit to dynamic
> > allocation. Static allocation needs macros.
If dynamic allocation of attributes isn't required, it should be
avoided at all. Having unnecessary dynamic allocated attributes is an
anti-pattern, especially when there are (4 + 1) * 7 + 1 = 36
attributes. It slows down the probe sequence and adds more pressure to
kzalloc().
IMHO unwillingness to add macros is not a valid excuse of allocating
and initializing attributes dynamically.
Anyway, I've quickly figured out a PoC patch (based on v3 4/4) to
demonstrate how to properly define readable macros to allocate all
attributes statically. See the attachment.
You may add
Co-developed-by: Rong Zhang <i@rong.moe>
Signed-off-by: Rong Zhang <i@rong.moe>
to v4 if you decide to adopt my patch.
> >
> > > > +
> > > > +static void dptc_fw_dev_unregister(void *data)
> > > > +{
> > > > + device_unregister(data);
> > > > +}
> > > > +
> > > > +static void dptc_kset_unregister(void *data)
> > > > +{
> > > > + kset_unregister(data);
> > > > +}
> > > > +
> > > > +static int dptc_resume(struct device *dev)
> > > > +{
> > > > + struct dptc_priv *dptc = dev_get_drvdata(dev);
> > > > + int ret;
> > > > +
> > > > + guard(mutex)(&dptc->lock);
> > > > +
> > > > + if (dptc->save_mode == SAVE_SINGLE)
> > > > + ret = dptc_alib_save(dptc);
> > >
> > > I don't see why bulk mode and single mode should have divergence on
> > > resuming behavior.
> >
> > This is personal preference.
Then add comments to the code with your explanation.
Thanks,
Rong
> > I'd rather bulk mode completely disengage
> > the driver handling. It also gives it a better reason to exist. After
> > all, ALIB runs a for loop inside it, there is no difference calling it
> > 3 times or 1 as far as the GPU is concerned. It is only ACPI overhead.
> > The original name was commit_mode, with manual/auto. But while working
> > on the ABI entry I found save_settings, and it was an ABI match so I
> > opted for that instead of making a new entry.
> >
> > Unfortunately, naming does not match precisely.
> >
> > Thanks for the review! Ack. on everything I did not comment on.
> >
> > Best,
> > Antheas
> >
> > > Thanks,
> > > Rong
> > >
> > > > + 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 device *dev = &pdev->dev;
> > > > + struct dptc_priv *dptc;
> > > > + int i, ret;
> > > > +
> > > > + dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL);
> > > > + if (!dptc)
> > > > + return -ENOMEM;
> > > > +
> > > > + platform_set_drvdata(pdev, dptc);
> > > > +
> > > > + ret = devm_mutex_init(dev, &dptc->lock);
> > > > + if (ret)
> > > > + return ret;
> > > > +
> > > > + dptc->dev_limits = dmi_match->driver_data;
> > > > + dev_info(dev, "%s (%s)\n", dmi_match->ident,
> > > > + boot_cpu_data.x86_model_id);
> > > > +
> > > > + dptc->fw_attr_dev = device_create(&firmware_attributes_class,
> > > > + NULL, MKDEV(0, 0), NULL,
> > > > + DRIVER_NAME);
> > > > + if (IS_ERR(dptc->fw_attr_dev))
> > > > + return PTR_ERR(dptc->fw_attr_dev);
> > > > +
> > > > + ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister,
> > > > + dptc->fw_attr_dev);
> > > > + if (ret)
> > > > + return ret;
> > > > +
> > > > + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL,
> > > > + &dptc->fw_attr_dev->kobj);
> > > > + if (!dptc->fw_attr_kset)
> > > > + return -ENOMEM;
> > > > +
> > > > + ret = devm_add_action_or_reset(dev, dptc_kset_unregister,
> > > > + dptc->fw_attr_kset);
> > > > + if (ret)
> > > > + return ret;
> > > > +
> > > > + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> > > > + dptc_setup_param_sysfs(dptc, &dptc->params[i], i);
> > > > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> > > > + &dptc->params[i].group);
> > > > + if (ret)
> > > > + return ret;
> > > > + }
> > > > +
> > > > + dptc_setup_expanded_sysfs(dptc, &dptc->expanded_attr);
> > > > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> > > > + &dptc->expanded_attr.group);
> > > > + if (ret)
> > > > + return ret;
> > > > +
> > > > + sysfs_attr_init(&dptc->save_settings_attr.attr);
> > > > + dptc->save_settings_attr.attr.name = "save_settings";
> > > > + dptc->save_settings_attr.attr.mode = 0644;
> > > > + dptc->save_settings_attr.show = dptc_save_settings_show;
> > > > + dptc->save_settings_attr.store = dptc_save_settings_store;
> > > > + ret = sysfs_create_file(&dptc->fw_attr_kset->kobj,
> > > > + &dptc->save_settings_attr.attr);
> > > > + if (ret)
> > > > + return ret;
> > > > +
> > > > + return 0;
> > > > +}
> > > > +
> > > > +static struct platform_driver dptc_driver = {
> > > > + .driver = {
> > > > + .name = DRIVER_NAME,
> > > > + .pm = pm_sleep_ptr(&dptc_pm_ops),
> > > > + },
> > > > + .probe = dptc_probe,
> > > > +};
> > > > +
> > > > +static int __init dptc_init(void)
> > > > +{
> > > > + const struct dmi_system_id *match;
> > > > + bool soc_found = false;
> > > > + int i, ret;
> > > > +
> > > > + match = dmi_first_match(dptc_dmi_table);
> > > > + if (!match)
> > > > + return -ENODEV;
> > > > +
> > > > + if (!acpi_has_method(NULL, ALIB_PATH)) {
> > > > + pr_warn("ALIB method not present\n");
> > > > + return -ENODEV;
> > > > + }
> > > > +
> > > > + for (i = 0; dptc_soc_table[i]; i++) {
> > > > + if (strstr(boot_cpu_data.x86_model_id,
> > > > + dptc_soc_table[i])) {
> > > > + soc_found = true;
> > > > + break;
> > > > + }
> > > > + }
> > > > + if (!soc_found) {
> > > > + pr_warn("unrecognized SoC '%s'\n",
> > > > + boot_cpu_data.x86_model_id);
> > > > + return -ENODEV;
> > > > + }
> > > > +
> > > > + dptc_pdev = platform_device_register_data(NULL, DRIVER_NAME, -1,
> > > > + match, sizeof(*match));
> > > > + if (IS_ERR(dptc_pdev))
> > > > + return PTR_ERR(dptc_pdev);
> > > > +
> > > > + ret = platform_driver_register(&dptc_driver);
> > > > + if (ret) {
> > > > + platform_device_unregister(dptc_pdev);
> > > > + return ret;
> > > > + }
> > > > +
> > > > + return 0;
> > > > +}
> > > > +
> > > > +static void __exit dptc_exit(void)
> > > > +{
> > > > + platform_driver_unregister(&dptc_driver);
> > > > + platform_device_unregister(dptc_pdev);
> > > > +}
> > > > +
> > > > +module_init(dptc_init);
> > > > +module_exit(dptc_exit);
> > > > +
> > > > +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>");
> > > > +MODULE_DESCRIPTION("AMD DPTCi ACPI Driver");
> > > > +MODULE_LICENSE("GPL");
> > >
[-- Attachment #2: static_attr.patch --]
[-- Type: text/x-patch, Size: 14033 bytes --]
drivers/platform/x86/amd/dptc.c | 259 ++++++++++++++--------------------------
1 file changed, 91 insertions(+), 168 deletions(-)
diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
index a3d582e48cd3..f54f4fca63f6 100644
--- a/drivers/platform/x86/amd/dptc.c
+++ b/drivers/platform/x86/amd/dptc.c
@@ -18,6 +18,7 @@
#include <linux/acpi.h>
#include <linux/cleanup.h>
#include <linux/dmi.h>
+#include <linux/device.h>
#include <linux/init.h>
#include <linux/kobject.h>
#include <linux/module.h>
@@ -26,6 +27,7 @@
#include <linux/platform_profile.h>
#include <linux/processor.h>
#include <linux/sysfs.h>
+#include <linux/stringify.h>
#include <linux/unaligned.h>
#include "../firmware_attributes_class.h"
@@ -67,7 +69,6 @@ struct dptc_device_limits {
};
struct dptc_param_desc {
- const char *name;
const char *display_name;
u16 scale; /* sysfs-to-ALIB multiplier (e.g. 1000 for W->mW) */
u8 param_id;
@@ -75,14 +76,14 @@ struct dptc_param_desc {
};
static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = {
- [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (W)",
- 1000, ALIB_ID_STAPM_LIMIT, ALIB_ID_SKIN_LIMIT },
- [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (W)",
- 1000, ALIB_ID_SLOW_LIMIT },
- [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (W)",
- 1000, ALIB_ID_FAST_LIMIT },
- [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)",
- 1, ALIB_ID_TEMP_TARGET },
+ [DPTC_PPT_PL1_SPL] = { "Sustained power limit (W)", 1000,
+ ALIB_ID_STAPM_LIMIT, ALIB_ID_SKIN_LIMIT },
+ [DPTC_PPT_PL2_SPPT] = { "Slow PPT limit (W)", 1000,
+ ALIB_ID_SLOW_LIMIT },
+ [DPTC_PPT_PL3_FPPT] = { "Fast PPT limit (W)", 1000,
+ ALIB_ID_FAST_LIMIT },
+ [DPTC_CPU_TEMP] = { "Thermal control limit (C)", 1,
+ ALIB_ID_TEMP_TARGET },
};
/* 15W class: AYANEO AIR (Ryzen 5 5560U) */
@@ -529,22 +530,6 @@ static const struct dmi_system_id dptc_dmi_table[] = {
};
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;
@@ -563,10 +548,6 @@ struct dptc_priv {
/* 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;
@@ -682,12 +663,23 @@ static int dptc_alib_save(struct dptc_priv *dptc)
/* Sysfs callbacks */
+struct dptc_attr_sysfs {
+ struct kobj_attribute current_value;
+ struct kobj_attribute default_value;
+ struct kobj_attribute min_value;
+ struct kobj_attribute max_value;
+ struct kobj_attribute scalar_increment;
+ struct kobj_attribute display_name;
+ struct kobj_attribute type;
+ int idx;
+};
+
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;
+ struct dptc_priv *dptc = dev_get_drvdata(kobj_to_dev(kobj->parent));
guard(mutex)(&dptc->lock);
@@ -710,7 +702,7 @@ static ssize_t dptc_current_value_store(struct kobject *kobj,
{
struct dptc_attr_sysfs *ps =
container_of(attr, struct dptc_attr_sysfs, current_value);
- struct dptc_priv *dptc = ps->priv;
+ struct dptc_priv *dptc = dev_get_drvdata(kobj_to_dev(kobj->parent));
u32 val, min, max;
int ret;
@@ -748,8 +740,9 @@ static ssize_t dptc_default_value_show(struct kobject *kobj,
{
struct dptc_attr_sysfs *ps =
container_of(attr, struct dptc_attr_sysfs, default_value);
+ struct dptc_priv *dptc = dev_get_drvdata(kobj_to_dev(kobj->parent));
- return sysfs_emit(buf, "%u\n", dptc_get_default(ps->priv, ps->idx));
+ return sysfs_emit(buf, "%u\n", dptc_get_default(dptc, ps->idx));
}
static ssize_t dptc_min_value_show(struct kobject *kobj,
@@ -757,7 +750,7 @@ static ssize_t dptc_min_value_show(struct kobject *kobj,
{
struct dptc_attr_sysfs *ps =
container_of(attr, struct dptc_attr_sysfs, min_value);
- struct dptc_priv *dptc = ps->priv;
+ struct dptc_priv *dptc = dev_get_drvdata(kobj_to_dev(kobj->parent));
guard(mutex)(&dptc->lock);
@@ -769,7 +762,7 @@ static ssize_t dptc_max_value_show(struct kobject *kobj,
{
struct dptc_attr_sysfs *ps =
container_of(attr, struct dptc_attr_sysfs, max_value);
- struct dptc_priv *dptc = ps->priv;
+ struct dptc_priv *dptc = dev_get_drvdata(kobj_to_dev(kobj->parent));
guard(mutex)(&dptc->lock);
@@ -799,8 +792,7 @@ static ssize_t dptc_type_show(struct kobject *kobj,
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);
+ struct dptc_priv *dptc = dev_get_drvdata(kobj_to_dev(kobj->parent));
guard(mutex)(&dptc->lock);
@@ -813,8 +805,7 @@ 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);
+ struct dptc_priv *dptc = dev_get_drvdata(kobj_to_dev(kobj->parent));
int ret = 0;
guard(mutex)(&dptc->lock);
@@ -835,9 +826,7 @@ 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;
+ struct dptc_priv *dptc = dev_get_drvdata(kobj_to_dev(kobj->parent));
guard(mutex)(&dptc->lock);
@@ -848,9 +837,7 @@ 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;
+ struct dptc_priv *dptc = dev_get_drvdata(kobj_to_dev(kobj->parent));
bool val;
int ret;
@@ -912,113 +899,62 @@ static ssize_t dptc_expanded_type_show(struct kobject *kobj,
/* 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;
-}
+#define __DPTC_ATTR_RO(_func, _name) \
+ __ATTR(_name, 0444, _func##_##_name##_show, NULL)
+
+#define __DPTC_ATTR_RW(_func, _name) \
+ __ATTR(_name, 0644, _func##_##_name##_show, _func##_##_name##_store)
+
+#define DPTC_ATTR_RO(_func, _name) \
+ ._name = __DPTC_ATTR_RO(_func, _name)
+
+#define DPTC_ATTR_RW(_func, _name) \
+ ._name = __DPTC_ATTR_RW(_func, _name)
+
+#define DPTC_ATTR_GROUP(_func, _idx, _grpname) \
+ static struct dptc_attr_sysfs dptc_##_grpname##_sysfs = { \
+ DPTC_ATTR_RW(_func, current_value), \
+ DPTC_ATTR_RO(_func, default_value), \
+ DPTC_ATTR_RO(_func, min_value), \
+ DPTC_ATTR_RO(_func, max_value), \
+ DPTC_ATTR_RO(_func, scalar_increment), \
+ DPTC_ATTR_RO(_func, display_name), \
+ DPTC_ATTR_RO(_func, type), \
+ .idx = _idx, \
+ }; \
+ static struct attribute *dptc_##_grpname##_attrs[] = { \
+ &dptc_##_grpname##_sysfs.current_value.attr, \
+ &dptc_##_grpname##_sysfs.default_value.attr, \
+ &dptc_##_grpname##_sysfs.min_value.attr, \
+ &dptc_##_grpname##_sysfs.max_value.attr, \
+ &dptc_##_grpname##_sysfs.scalar_increment.attr, \
+ &dptc_##_grpname##_sysfs.display_name.attr, \
+ &dptc_##_grpname##_sysfs.type.attr, \
+ NULL, \
+ }; \
+ static const struct attribute_group dptc_##_grpname##_group = { \
+ .name = __stringify(_grpname), \
+ .attrs = dptc_##_grpname##_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;
-}
+DPTC_ATTR_GROUP(dptc, DPTC_PPT_PL1_SPL, ppt_pl1_spl);
+DPTC_ATTR_GROUP(dptc, DPTC_PPT_PL2_SPPT, ppt_pl2_sppt);
+DPTC_ATTR_GROUP(dptc, DPTC_PPT_PL3_FPPT, ppt_pl3_fppt);
+DPTC_ATTR_GROUP(dptc, DPTC_CPU_TEMP, cpu_temp);
+
+DPTC_ATTR_GROUP(dptc_expanded, 0, expanded);
+
+static const struct attribute_group *dptc_attr_groups[] = {
+ &dptc_ppt_pl1_spl_group,
+ &dptc_ppt_pl2_sppt_group,
+ &dptc_ppt_pl3_fppt_group,
+ &dptc_cpu_temp_group,
+ &dptc_expanded_group,
+ NULL,
+};
+
+static const struct kobj_attribute save_settings_attr =
+ __DPTC_ATTR_RW(dptc, save_settings);
static void dptc_fw_dev_unregister(void *data)
{
@@ -1131,7 +1067,7 @@ static int dptc_probe(struct platform_device *pdev)
const struct dmi_system_id *dmi_match = dev_get_platdata(&pdev->dev);
struct device *dev = &pdev->dev;
struct dptc_priv *dptc;
- int i, ret;
+ int ret;
dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL);
if (!dptc)
@@ -1152,6 +1088,8 @@ static int dptc_probe(struct platform_device *pdev)
if (IS_ERR(dptc->fw_attr_dev))
return PTR_ERR(dptc->fw_attr_dev);
+ dev_set_drvdata(dptc->fw_attr_dev, dptc);
+
ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister,
dptc->fw_attr_dev);
if (ret)
@@ -1167,27 +1105,12 @@ static int dptc_probe(struct platform_device *pdev)
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);
+ ret = sysfs_create_groups(&dptc->fw_attr_kset->kobj, dptc_attr_groups);
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);
+ &save_settings_attr.attr);
if (ret)
return ret;
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [RFC v3 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver
2026-03-08 20:00 ` Rong Zhang
@ 2026-03-08 22:15 ` Antheas Kapenekakis
2026-03-08 22:18 ` Antheas Kapenekakis
0 siblings, 1 reply; 19+ messages in thread
From: Antheas Kapenekakis @ 2026-03-08 22:15 UTC (permalink / raw)
To: Rong Zhang
Cc: Mario.Limonciello, W_Armin, sashal, Shyam-Sundar.S-k,
derekjohn.clark, denis.benato, linux-kernel, platform-driver-x86
On Sun, 8 Mar 2026 at 21:06, Rong Zhang <i@rong.moe> wrote:
>
> Hi Antheas,
>
> On Sun, 2026-03-08 at 00:08 +0100, Antheas Kapenekakis wrote:
> > On Sun, 8 Mar 2026 at 00:02, Antheas Kapenekakis <lkml@antheas.dev> wrote:
> > >
> > > On Sat, 7 Mar 2026 at 22:56, Rong Zhang <i@rong.moe> wrote:
> > > >
> > > > Hi Antheas,
> > > >
> > > > Thanks for working on it.
> > > >
> > > > I think this driver is useful for me and I will see if I can test it on
> > > > my devices once I find some time. They're not handhelds but I am sure
> > > > at least one of them has the ALIB method. It'd better if a module
> > > > option can be used to bypass the quirk table so that testing against a
> > > > new device will be easier. Such an option should taint the kernel ofc.
> > >
> > > I will defer to Mario for this one. Previous iterations of this series
> > > had an override. Before I posted V2 I had a module param that allowed
> > > matching by ident.
>
> Sure. That's not really a problem for me as I can easily patch and
> compile it myself. However, it makes a lot of sense if we want user's
> test results on new devices. Here's my two cents on such a module
> option :-P
>
> >
> > Hi, 2 small follow-ups. Entries to this driver will only be added for
> > devices without EC/amd_pmf handling. Because those interfere. So it
> > does not apply to lenovo devices AFAIK.
>
> Yeah I knew that. I have a non-Lenovo device with ALIB and no EC/PMF.
>
> >
> > > > That being said, I have some review comments. Please see below and my
> > > > following replies.
> > > >
> > > > On Sat, 2026-03-07 at 12:55 +0100, Antheas Kapenekakis wrote:
> > > > > Add a driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> > > > > Thermal Configuration interface (DPTCi). This exposes TDP and thermal
> > > > > parameters for AMD APU-based handheld devices via the
> > > > > firmware-attributes sysfs ABI.
> > > > >
> > > > > Parameters are staged and atomically committed through ALIB. The driver
> > > > > supports two save modes: "single" (apply immediately on write) and
> > > > > "bulk" (stage values, then commit with "save"). An "expanded_limits"
> > > > > toggle widens the allowed parameter ranges beyond device defaults.
> > > > >
> > > > > Initial device support: GPD Win 5 (AMD Ryzen AI MAX).
> > > > >
> > > > > Assisted-by: Claude:claude-opus-4-6
> > > > > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> > > > > ---
> > > > > MAINTAINERS | 6 +
> > > > > drivers/platform/x86/amd/Kconfig | 14 +
> > > > > drivers/platform/x86/amd/Makefile | 2 +
> > > > > drivers/platform/x86/amd/dptc.c | 747 ++++++++++++++++++++++++++++++
> > > > > 4 files changed, 769 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..d610092467fc 100644
> > > > > --- a/drivers/platform/x86/amd/Kconfig
> > > > > +++ b/drivers/platform/x86/amd/Kconfig
> > > > > @@ -44,3 +44,17 @@ config AMD_ISP_PLATFORM
> > > > >
> > > > > This driver can also be built as a module. If so, the module
> > > > > will be called amd_isp4.
> > > > > +
> > > > > +config AMD_DPTC
> > > > > + tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)"
> > > > > + depends on X86_64 && ACPI && DMI
> > > > > + select FIRMWARE_ATTRIBUTES_CLASS
> > > > > + help
> > > > > + Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> > > > > + Thermal Configuration Interface (DPTCi). Exposes TDP and thermal
> > > > > + parameters for AMD APU-based handheld devices via the
> > > > > + firmware-attributes sysfs ABI, allowing userspace tools to stage
> > > > > + and atomically commit power limit settings. Requires a DMI match
> > > > > + for the device and a recognized AMD SoC.
> > > > > +
> > > > > + If built as a module, the module will be called amd_dptc.
> > > > > diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile
> > > > > index f6ff0c837f34..862a609bfe38 100644
> > > > > --- a/drivers/platform/x86/amd/Makefile
> > > > > +++ b/drivers/platform/x86/amd/Makefile
> > > > > @@ -12,3 +12,5 @@ obj-$(CONFIG_AMD_PMF) += pmf/
> > > > > obj-$(CONFIG_AMD_WBRF) += wbrf.o
> > > > > obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o
> > > > > obj-$(CONFIG_AMD_HFI) += hfi/
> > > > > +obj-$(CONFIG_AMD_DPTC) += amd_dptc.o
> > > > > +amd_dptc-y := dptc.o
> > > > > diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
> > > > > new file mode 100644
> > > > > index 000000000000..acfe9cc01bab
> > > > > --- /dev/null
> > > > > +++ b/drivers/platform/x86/amd/dptc.c
> > > > > @@ -0,0 +1,747 @@
> > > > > +// SPDX-License-Identifier: GPL-2.0-only
> > > > > +/*
> > > > > + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver
> > > > > + *
> > > > > + * Exposes AMD APU power and thermal parameters via the firmware-attributes
> > > > > + * sysfs ABI. Parameters are staged and atomically committed through the
> > > > > + * AGESA ALIB Function 0x0C (Dynamic Power and Thermal Configuration
> > > > > + * interface).
> > > > > + *
> > > > > + * Reference: AMD AGESA Publication #44065, Appendix E.5
> > > > > + * https://docs.amd.com/v/u/en-US/44065_Arch2008
> > > > > + *
> > > > > + * Copyright (C) 2026 Antheas Kapenekakis <lkml@antheas.dev>
> > > > > + */
> > > > > +
> > > > > +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> > > > > +
> > > > > +#include <linux/acpi.h>
> > > > > +#include <linux/cleanup.h>
> > > > > +#include <linux/dmi.h>
> > > > > +#include <linux/init.h>
> > > > > +#include <linux/kobject.h>
> > > > > +#include <linux/module.h>
> > > > > +#include <linux/mutex.h>
> > > > > +#include <linux/platform_device.h>
> > > > > +#include <linux/processor.h>
> > > > > +#include <linux/sysfs.h>
> > > > > +#include <linux/unaligned.h>
> > > > > +
> > > > > +#include "../firmware_attributes_class.h"
> > > > > +
> > > > > +#define DRIVER_NAME "amd-dptc"
> > > > > +
> > > > > +#define ALIB_FUNC_DPTC 0x0C
> > > > > +#define ALIB_PATH "\\_SB.ALIB"
> > > > > +
> > > > > +#define ALIB_ID_TEMP_TARGET 0x03
> > > > > +#define ALIB_ID_STAPM_LIMIT 0x05
> > > > > +#define ALIB_ID_FAST_LIMIT 0x06
> > > > > +#define ALIB_ID_SLOW_LIMIT 0x07
> > > > > +#define ALIB_ID_SKIN_LIMIT 0x2E
> > > > > +
> > > > > +enum dptc_param_idx {
> > > > > + DPTC_PPT_PL1_SPL, /* STAPM + skin limit (set together) */
> > > > > + DPTC_PPT_PL2_SPPT, /* slow PPT limit */
> > > > > + DPTC_PPT_PL3_FPPT, /* fast PPT limit */
> > > > > + DPTC_CPU_TEMP, /* thermal control target */
> > > > > + DPTC_NUM_PARAMS,
> > > > > +};
> > > > > +
> > > > > +struct dptc_param_limits {
> > > > > + u32 expanded_min;
> > > > > + u32 device_min;
> > > > > + u32 def;
> > > > > + u32 device_max;
> > > > > + u32 expanded_max;
> > > > > +};
> > > > > +
> > > > > +struct dptc_device_limits {
> > > > > + struct dptc_param_limits params[DPTC_NUM_PARAMS];
> > > > > +};
> > > > > +
> > > > > +struct dptc_param_desc {
> > > > > + const char *name;
> > > > > + const char *display_name;
> > > > > + u16 scale; /* sysfs-to-ALIB multiplier (e.g. 1000 for W->mW) */
> > > > > + u8 param_id;
> > > > > + u8 param_id2; /* secondary ALIB ID, 0 if none */
> > > > > +};
> > > > > +
> > > > > +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = {
> > > > > + [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (W)",
> > > > > + 1000, ALIB_ID_STAPM_LIMIT, ALIB_ID_SKIN_LIMIT },
> > > > > + [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (W)",
> > > > > + 1000, ALIB_ID_SLOW_LIMIT },
> > > > > + [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (W)",
> > > > > + 1000, ALIB_ID_FAST_LIMIT },
> > > > > + [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)",
> > > > > + 1, ALIB_ID_TEMP_TARGET },
> > > > > +};
> > > >
> > > > The indent here just made me crazy :-/
> > > >
> > > > Align the contents after line breaks to the left braces.
> > >
> > > I will review how to do the alignment, although I'm not happy with how
> > > aligning to spaces will look. PL1 will overflow.
>
> No, it won't. Having four tabs before `1000' gives you a line length of
> 80 characters (editors will tell that you are at column 81). And
> nowadays the maximum line length allowed is 100 (that's why Derek
> suggested you should avoid some unnecessary wraps).
I double wrapped the first line and it is fine. I have already staged
the fixes for v4
> > >
> > > > > +
> > > > > +/* AI MAX Handheld class: GPD Win 5 */
> > > > > +static const struct dptc_device_limits limits_maxhh = { .params = {
> > > >
> > > > Please don't save a line by zipping braces like this. It's hard to read
> > > > and likely to be missed when glancing the code (that's what happened to
> > > > me and it really confused me).
> > > >
> > > > I found you did the same in [PATCH v3 3/4] for `.profiles'. That should
> > > > be avoided too.
> > >
> > > This is a leftover from before adding profiles. I guess it does not
> > > make much sense now. I can reflow.
> > >
> > > > > + [DPTC_PPT_PL1_SPL] = { 0, 4, 25, 80, 100 },
> > > > > + [DPTC_PPT_PL2_SPPT] = { 0, 4, 27, 82, 100 },
> > > > > + [DPTC_PPT_PL3_FPPT] = { 0, 4, 40, 85, 100 },
> > > > > + [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 },
> > > > > +}};
> > > > > +
> > > > > +/* Substring matches against boot_cpu_data.x86_model_id; order matters. */
> > > > > +static const char * const dptc_soc_table[] = {
> > > > > + /* AI MAX */
> > > > > + "AMD RYZEN AI MAX+ 395",
> > > > > + "AMD RYZEN AI MAX+ 385",
> > > > > + "AMD RYZEN AI MAX 380",
> > > > > + NULL,
> > > > > +};
> > > > > +
> > > > > +static const struct dmi_system_id dptc_dmi_table[] = {
> > > > > + /* GPD */
> > > > > + {
> > > > > + .ident = "GPD Win 5",
> > > > > + .matches = {
> > > > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"),
> > > > > + },
> > > > > + .driver_data = (void *)&limits_maxhh,
> > > > > + },
> > > > > + { }
> > > > > +};
> > > > > +MODULE_DEVICE_TABLE(dmi, dptc_dmi_table);
> > > > > +
> > > > > +struct dptc_priv;
> > > > > +
> > > > > +struct dptc_attr_sysfs {
> > > > > + struct dptc_priv *priv;
> > > > > + struct kobj_attribute current_value;
> > > > > + struct kobj_attribute default_value;
> > > > > + struct kobj_attribute min_value;
> > > > > + struct kobj_attribute max_value;
> > > > > + struct kobj_attribute scalar_increment;
> > > > > + struct kobj_attribute display_name;
> > > > > + struct kobj_attribute type;
> > > > > + struct attribute *attrs[8];
> > > > > + struct attribute_group group;
> > > > > + int idx;
> > > > > +};
> > > > > +
> > > > > +struct dptc_priv {
> > > > > + struct device *fw_attr_dev;
> > > > > + struct kset *fw_attr_kset;
> > > > > +
> > > > > + const struct dptc_device_limits *dev_limits;
> > > > > +
> > > > > + bool expanded;
> > > > > +
> > > > > + enum dptc_save_mode { SAVE_SINGLE, SAVE_BULK } save_mode;
> > > >
> > > > Defining the enumeration separately will improve readability.
> > > >
> > > > > +
> > > > > + u32 staged[DPTC_NUM_PARAMS];
> > > > > + bool has_staged[DPTC_NUM_PARAMS];
> > > > > +
> > > > > + /* Protects mutable driver state */
> > > >
> > > > The comment is nonsense. To make it helpful, it should be "Protects
> > > > members above/below" with properly arranged structure layout.
> > > >
> > > > > + struct mutex lock;
> > > > > +
> > > > > + struct dptc_attr_sysfs params[DPTC_NUM_PARAMS];
> > > > > + struct dptc_attr_sysfs expanded_attr;
> > > > > + struct kobj_attribute save_settings_attr;
> > > > > +};
> > > > > +
> > > > > +static struct platform_device *dptc_pdev;
> > > > > +
> > > > > +static u32 dptc_get_min(struct dptc_priv *dptc, int idx)
> > > > > +{
> > > > > + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_min
> > > > > + : dptc->dev_limits->params[idx].device_min;
> > > > > +}
> > > > > +
> > > > > +static u32 dptc_get_max(struct dptc_priv *dptc, int idx)
> > > > > +{
> > > > > + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_max
> > > > > + : dptc->dev_limits->params[idx].device_max;
> > > > > +}
> > > > > +
> > > > > +static u32 dptc_get_default(struct dptc_priv *dptc, int idx)
> > > > > +{
> > > > > + return dptc->dev_limits->params[idx].def;
> > > > > +}
> > > > > +
> > > > > +static int dptc_alib_call(const u8 *ids, const u32 *vals, int count)
> > > > > +{
> > > > > + union acpi_object in_params[2];
> > > > > + struct acpi_object_list input;
> > > > > + acpi_status status;
> > > > > + u32 buf_size;
> > > > > + int i, off;
> > > > > + u8 *buf;
> > > > > +
> > > > > + if (count == 0)
> > > > > + return -EINVAL;
> > > >
> > > > Return -ENOENT as think-lmi does (see its save_settings_store()) and
> > > > update the documentation accordingly.
> > > >
> > > > > +
> > > > > + /* 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);
> > > >
> > > > 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;
> > > >
> > > > Misaligned `='.
> > > >
> > > > > +
> > > > > + 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 hw_val = val * dptc_params[idx].scale;
> > > > > + u32 vals[2];
> > > > > + u8 ids[2];
> > > > > + int count = 0;
> > > > > +
> > > > > + ids[count] = dptc_params[idx].param_id;
> > > > > + vals[count] = hw_val;
> > > > > + count++;
> > > > > + if (dptc_params[idx].param_id2) {
> > > > > + ids[count] = dptc_params[idx].param_id2;
> > > > > + vals[count] = hw_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;
> > > > > + u32 hw_val;
> > > > > +
> > > > > + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> > > > > + if (!dptc->has_staged[i])
> > > > > + continue;
> > > > > +
> > > > > + hw_val = dptc->staged[i] * dptc_params[i].scale;
> > > > > + ids[count] = dptc_params[i].param_id;
> > > > > + vals[count] = hw_val;
> > > > > + count++;
> > > > > + if (dptc_params[i].param_id2) {
> > > > > + ids[count] = dptc_params[i].param_id2;
> > > > > + vals[count] = hw_val;
> > > > > + count++;
> > > > > + }
> > > >
> > > > I could imagine a helper function to prevent replicating the same code
> > > > patterns twice:
> > > >
> > > > static size_t dptc_alib_fill_param(u8 *ids, u32 *vals, size_t offset,
> > > > enum dptc_param_idx param, u32 val)
> > > > {
> > > > u32 hw_val = val * dptc_params[param].scale;
> > > >
> > > > ids[offset] = dptc_params[param].param_id;
> > > > vals[offset++] = hw_val;
> > > >
> > > > if (dptc_params[param].param_id2) {
> > > > ids[offset] = dptc_params[param].param_id2;
> > > > vals[offset++] = hw_val;
> > > > }
> > > >
> > > > return offset;
> > > > }
> > > >
> > > > > + }
> > > > > +
> > > > > + 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->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);
> > > > > +
> > > > > + dptc->has_staged[ps->idx] = false;
> > > > > + return count;
> > > > > + }
> > > > > +
> > > > > + ret = kstrtou32(buf, 10, &val);
> > > > > + if (ret)
> > > > > + return ret;
> > > > > +
> > > > > + guard(mutex)(&dptc->lock);
> > > > > +
> > > > > + 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"))
> > > >
> > > > Please don't rely on the `count == 0' check in dptc_alib_call(). `dptc-
> > > > > save_mode == SAVE_SINGLE' should result in -EOPNOTSUPP to maintain
> > > > consistency with think-lmi (update the documentation accordingly).
> > >
> > > I will consider it. But this is subtractive behavior. We are still ABI
> > > compliant even if the write goes through and it does not affect
> > > functionality.
>
> If you return something different in a certain case, it's neither API
> compatible nor ABI compatible.
>
> Also the issue here is more about you shouldn't rely on the `count ==
> 0' check in dptc_alib_call() to prevent running into unexpected state
> when `dptc->save_mode == SAVE_SINGLE' but "save" is written. Explicit
> checks are better than implicit ones.
I fixed te code to return ENOENT when count is 0. Not returning an
error and accepting the write instead of sending an error is a
superset of the previous API so it is not a problem. It is fine to
write the params with "single"
> > >
> > > > > + ret = dptc_alib_save(dptc);
> > > > > + else if (sysfs_streq(buf, "single"))
> > > > > + dptc->save_mode = SAVE_SINGLE;
> > > > > + else if (sysfs_streq(buf, "bulk"))
> > > > > + dptc->save_mode = SAVE_BULK;
> > > > > + else
> > > > > + return -EINVAL;
> > > > > +
> > > > > + return ret ? ret : count;
> > > > > +}
> > > > > +
> > > > > +static ssize_t dptc_expanded_current_value_show(struct kobject *kobj,
> > > > > + struct kobj_attribute *attr,
> > > > > + char *buf)
> > > > > +{
> > > > > + struct dptc_attr_sysfs *ps =
> > > > > + container_of(attr, struct dptc_attr_sysfs, current_value);
> > > > > + struct dptc_priv *dptc = ps->priv;
> > > > > +
> > > > > + guard(mutex)(&dptc->lock);
> > > > > +
> > > > > + return sysfs_emit(buf, "%d\n", dptc->expanded);
> > > > > +}
> > > > > +
> > > > > +static ssize_t dptc_expanded_current_value_store(struct kobject *kobj,
> > > > > + struct kobj_attribute *attr,
> > > > > + const char *buf, size_t count)
> > > > > +{
> > > > > + struct dptc_attr_sysfs *ps =
> > > > > + container_of(attr, struct dptc_attr_sysfs, current_value);
> > > > > + struct dptc_priv *dptc = ps->priv;
> > > > > + bool val;
> > > > > + int ret;
> > > > > +
> > > > > + ret = kstrtobool(buf, &val);
> > > > > + if (ret)
> > > > > + return ret;
> > > > > +
> > > > > + guard(mutex)(&dptc->lock);
> > > > > +
> > > > > + dptc->expanded = val;
> > > > > + /* Clear staged values: limits changed, old values may be out of range */
> > > > > + memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
> > > >
> > > > I'd prefer using a bitmap so that you don't need to use memset() to
> > > > clear them. It also saves some memory if more parameters are added in
> > > > the future.
> > >
> > > I will review if has_staged needs to exist. Values cannot be 0 anyway.
> > > A bit map has other readability issues.
>
> Just a reminder: you may also need to increase expanded_min to 1.
I did.
> > >
> > > > > +
> > > > > + 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;
> > > > > +}
> > > >
> > > > As Derek has said in v2, use macros to rearrange them.
> > > >
> > > > It's painful to read all these things as I'll need to scroll the screen
> > > > quite a lot just to read something with a low information density.
> > > > That's why there are numerous macros defined in include/linux/sysfs.h.
> > > > I could imagine defining macros like DPTC{,_EXPANDED}_ATTR_{RO,RW}.
> > >
> > > This driver consists of two instantiation blocks. It is not possible
> > > to dedup the initialization functions and save space because the
> > > function holders and structs that would be needed undo that.
>
> Undo what?
The space savings. I tried to merge the functions
dptc_setup_expanded_sysfs and dptc_setup_param_sysfs and this ended up
having around the same lines and being overly complicated with
additional structs.
> > >
> > > The blocks in lenovo other are significantly less readable and harder
> > > to write. Perhaps you save around 20 lines. But it is not worth it
> > > imo.
> >
> > I would consider adding them if the helper macros are centralized, but
> > I am not adding macros to this driver.
>
> The macros in lenovo-wmi-other are completely readable and are just
> some common attribute techniques. Just search for `##_attr|attr_##',
> and you'll see a lot of similar patterns.
>
> Also my PoC (see attachment) showed that using macros to allocate them
> statically saves 77 lines.
But the driver is 800 lines already in this patch. If you are willing
to experiment and want some patches under your belt, I'd suggest you
try to create some centralized macros and introduce them for
armoury/lenovo-other first.
I am willing to use centralized macros instead of dynamic
instantiation in this driver as a follow-up. But I am not willing to
introduce macros.
As one of the issues of the macros, I will point to your review of
Derek's patch in lenovo-other recently where he missed an alignment
and a tendency to avoid these types of comments.
> >
> > Antheas
> >
> > > > Meanwhile, I am not very sure why you would have to dynamically
> > > > initialize all these attributes. I haven't notice a blocker of defining
> > > > them statically at the module-level (see how lenovo-wmi-other did). Am
> > > > I missing anything?
> > >
> > > You are not missing something. There is no benefit to dynamic
> > > allocation. Static allocation needs macros.
>
> If dynamic allocation of attributes isn't required, it should be
> avoided at all. Having unnecessary dynamic allocated attributes is an
> anti-pattern, especially when there are (4 + 1) * 7 + 1 = 36
> attributes. It slows down the probe sequence and adds more pressure to
> kzalloc().
>
> IMHO unwillingness to add macros is not a valid excuse of allocating
> and initializing attributes dynamically.
It is an accepted pattern in upstream drivers. It is a valid design choice.
> Anyway, I've quickly figured out a PoC patch (based on v3 4/4) to
> demonstrate how to properly define readable macros to allocate all
> attributes statically. See the attachment.
>
> You may add
>
> Co-developed-by: Rong Zhang <i@rong.moe>
> Signed-off-by: Rong Zhang <i@rong.moe>
>
> to v4 if you decide to adopt my patch.
I reviewed your attachment. It mirrors the pattern in lenovo-other. I
would rather you redirect this energy to centralizing the macros
instead, so that I can just use the definitions. This way, we avoid
duplicating code.
If this driver ends up merging before you introduce such a series,
feel free to refactor this driver as part of it as well and I will ack
it.
Other maintainers have expressed a need for centralized macros for
firmware attributes, so it would be a good opportunity for you.
Best,
Antheas
> > >
> > > > > +
> > > > > +static void dptc_fw_dev_unregister(void *data)
> > > > > +{
> > > > > + device_unregister(data);
> > > > > +}
> > > > > +
> > > > > +static void dptc_kset_unregister(void *data)
> > > > > +{
> > > > > + kset_unregister(data);
> > > > > +}
> > > > > +
> > > > > +static int dptc_resume(struct device *dev)
> > > > > +{
> > > > > + struct dptc_priv *dptc = dev_get_drvdata(dev);
> > > > > + int ret;
> > > > > +
> > > > > + guard(mutex)(&dptc->lock);
> > > > > +
> > > > > + if (dptc->save_mode == SAVE_SINGLE)
> > > > > + ret = dptc_alib_save(dptc);
> > > >
> > > > I don't see why bulk mode and single mode should have divergence on
> > > > resuming behavior.
> > >
> > > This is personal preference.
>
> Then add comments to the code with your explanation.
Ack. It is already in the sysfs doc but I can add it here too.
> Thanks,
> Rong
>
> > > I'd rather bulk mode completely disengage
> > > the driver handling. It also gives it a better reason to exist. After
> > > all, ALIB runs a for loop inside it, there is no difference calling it
> > > 3 times or 1 as far as the GPU is concerned. It is only ACPI overhead.
> > > The original name was commit_mode, with manual/auto. But while working
> > > on the ABI entry I found save_settings, and it was an ABI match so I
> > > opted for that instead of making a new entry.
> > >
> > > Unfortunately, naming does not match precisely.
> > >
> > > Thanks for the review! Ack. on everything I did not comment on.
> > >
> > > Best,
> > > Antheas
> > >
> > > > Thanks,
> > > > Rong
> > > >
> > > > > + 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 device *dev = &pdev->dev;
> > > > > + struct dptc_priv *dptc;
> > > > > + int i, ret;
> > > > > +
> > > > > + dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL);
> > > > > + if (!dptc)
> > > > > + return -ENOMEM;
> > > > > +
> > > > > + platform_set_drvdata(pdev, dptc);
> > > > > +
> > > > > + ret = devm_mutex_init(dev, &dptc->lock);
> > > > > + if (ret)
> > > > > + return ret;
> > > > > +
> > > > > + dptc->dev_limits = dmi_match->driver_data;
> > > > > + dev_info(dev, "%s (%s)\n", dmi_match->ident,
> > > > > + boot_cpu_data.x86_model_id);
> > > > > +
> > > > > + dptc->fw_attr_dev = device_create(&firmware_attributes_class,
> > > > > + NULL, MKDEV(0, 0), NULL,
> > > > > + DRIVER_NAME);
> > > > > + if (IS_ERR(dptc->fw_attr_dev))
> > > > > + return PTR_ERR(dptc->fw_attr_dev);
> > > > > +
> > > > > + ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister,
> > > > > + dptc->fw_attr_dev);
> > > > > + if (ret)
> > > > > + return ret;
> > > > > +
> > > > > + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL,
> > > > > + &dptc->fw_attr_dev->kobj);
> > > > > + if (!dptc->fw_attr_kset)
> > > > > + return -ENOMEM;
> > > > > +
> > > > > + ret = devm_add_action_or_reset(dev, dptc_kset_unregister,
> > > > > + dptc->fw_attr_kset);
> > > > > + if (ret)
> > > > > + return ret;
> > > > > +
> > > > > + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> > > > > + dptc_setup_param_sysfs(dptc, &dptc->params[i], i);
> > > > > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> > > > > + &dptc->params[i].group);
> > > > > + if (ret)
> > > > > + return ret;
> > > > > + }
> > > > > +
> > > > > + dptc_setup_expanded_sysfs(dptc, &dptc->expanded_attr);
> > > > > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> > > > > + &dptc->expanded_attr.group);
> > > > > + if (ret)
> > > > > + return ret;
> > > > > +
> > > > > + sysfs_attr_init(&dptc->save_settings_attr.attr);
> > > > > + dptc->save_settings_attr.attr.name = "save_settings";
> > > > > + dptc->save_settings_attr.attr.mode = 0644;
> > > > > + dptc->save_settings_attr.show = dptc_save_settings_show;
> > > > > + dptc->save_settings_attr.store = dptc_save_settings_store;
> > > > > + ret = sysfs_create_file(&dptc->fw_attr_kset->kobj,
> > > > > + &dptc->save_settings_attr.attr);
> > > > > + if (ret)
> > > > > + return ret;
> > > > > +
> > > > > + return 0;
> > > > > +}
> > > > > +
> > > > > +static struct platform_driver dptc_driver = {
> > > > > + .driver = {
> > > > > + .name = DRIVER_NAME,
> > > > > + .pm = pm_sleep_ptr(&dptc_pm_ops),
> > > > > + },
> > > > > + .probe = dptc_probe,
> > > > > +};
> > > > > +
> > > > > +static int __init dptc_init(void)
> > > > > +{
> > > > > + const struct dmi_system_id *match;
> > > > > + bool soc_found = false;
> > > > > + int i, ret;
> > > > > +
> > > > > + match = dmi_first_match(dptc_dmi_table);
> > > > > + if (!match)
> > > > > + return -ENODEV;
> > > > > +
> > > > > + if (!acpi_has_method(NULL, ALIB_PATH)) {
> > > > > + pr_warn("ALIB method not present\n");
> > > > > + return -ENODEV;
> > > > > + }
> > > > > +
> > > > > + for (i = 0; dptc_soc_table[i]; i++) {
> > > > > + if (strstr(boot_cpu_data.x86_model_id,
> > > > > + dptc_soc_table[i])) {
> > > > > + soc_found = true;
> > > > > + break;
> > > > > + }
> > > > > + }
> > > > > + if (!soc_found) {
> > > > > + pr_warn("unrecognized SoC '%s'\n",
> > > > > + boot_cpu_data.x86_model_id);
> > > > > + return -ENODEV;
> > > > > + }
> > > > > +
> > > > > + dptc_pdev = platform_device_register_data(NULL, DRIVER_NAME, -1,
> > > > > + match, sizeof(*match));
> > > > > + if (IS_ERR(dptc_pdev))
> > > > > + return PTR_ERR(dptc_pdev);
> > > > > +
> > > > > + ret = platform_driver_register(&dptc_driver);
> > > > > + if (ret) {
> > > > > + platform_device_unregister(dptc_pdev);
> > > > > + return ret;
> > > > > + }
> > > > > +
> > > > > + return 0;
> > > > > +}
> > > > > +
> > > > > +static void __exit dptc_exit(void)
> > > > > +{
> > > > > + platform_driver_unregister(&dptc_driver);
> > > > > + platform_device_unregister(dptc_pdev);
> > > > > +}
> > > > > +
> > > > > +module_init(dptc_init);
> > > > > +module_exit(dptc_exit);
> > > > > +
> > > > > +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>");
> > > > > +MODULE_DESCRIPTION("AMD DPTCi ACPI Driver");
> > > > > +MODULE_LICENSE("GPL");
> > > >
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [RFC v3 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver
2026-03-08 22:15 ` Antheas Kapenekakis
@ 2026-03-08 22:18 ` Antheas Kapenekakis
0 siblings, 0 replies; 19+ messages in thread
From: Antheas Kapenekakis @ 2026-03-08 22:18 UTC (permalink / raw)
To: Rong Zhang
Cc: Mario.Limonciello, W_Armin, sashal, Shyam-Sundar.S-k,
derekjohn.clark, denis.benato, linux-kernel, platform-driver-x86
On Sun, 8 Mar 2026 at 23:15, Antheas Kapenekakis <lkml@antheas.dev> wrote:
>
> On Sun, 8 Mar 2026 at 21:06, Rong Zhang <i@rong.moe> wrote:
> >
> > Hi Antheas,
> >
> > On Sun, 2026-03-08 at 00:08 +0100, Antheas Kapenekakis wrote:
> > > On Sun, 8 Mar 2026 at 00:02, Antheas Kapenekakis <lkml@antheas.dev> wrote:
> > > >
> > > > On Sat, 7 Mar 2026 at 22:56, Rong Zhang <i@rong.moe> wrote:
> > > > >
> > > > > Hi Antheas,
> > > > >
> > > > > Thanks for working on it.
> > > > >
> > > > > I think this driver is useful for me and I will see if I can test it on
> > > > > my devices once I find some time. They're not handhelds but I am sure
> > > > > at least one of them has the ALIB method. It'd better if a module
> > > > > option can be used to bypass the quirk table so that testing against a
> > > > > new device will be easier. Such an option should taint the kernel ofc.
> > > >
> > > > I will defer to Mario for this one. Previous iterations of this series
> > > > had an override. Before I posted V2 I had a module param that allowed
> > > > matching by ident.
> >
> > Sure. That's not really a problem for me as I can easily patch and
> > compile it myself. However, it makes a lot of sense if we want user's
> > test results on new devices. Here's my two cents on such a module
> > option :-P
> >
> > >
> > > Hi, 2 small follow-ups. Entries to this driver will only be added for
> > > devices without EC/amd_pmf handling. Because those interfere. So it
> > > does not apply to lenovo devices AFAIK.
> >
> > Yeah I knew that. I have a non-Lenovo device with ALIB and no EC/PMF.
> >
> > >
> > > > > That being said, I have some review comments. Please see below and my
> > > > > following replies.
> > > > >
> > > > > On Sat, 2026-03-07 at 12:55 +0100, Antheas Kapenekakis wrote:
> > > > > > Add a driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> > > > > > Thermal Configuration interface (DPTCi). This exposes TDP and thermal
> > > > > > parameters for AMD APU-based handheld devices via the
> > > > > > firmware-attributes sysfs ABI.
> > > > > >
> > > > > > Parameters are staged and atomically committed through ALIB. The driver
> > > > > > supports two save modes: "single" (apply immediately on write) and
> > > > > > "bulk" (stage values, then commit with "save"). An "expanded_limits"
> > > > > > toggle widens the allowed parameter ranges beyond device defaults.
> > > > > >
> > > > > > Initial device support: GPD Win 5 (AMD Ryzen AI MAX).
> > > > > >
> > > > > > Assisted-by: Claude:claude-opus-4-6
> > > > > > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> > > > > > ---
> > > > > > MAINTAINERS | 6 +
> > > > > > drivers/platform/x86/amd/Kconfig | 14 +
> > > > > > drivers/platform/x86/amd/Makefile | 2 +
> > > > > > drivers/platform/x86/amd/dptc.c | 747 ++++++++++++++++++++++++++++++
> > > > > > 4 files changed, 769 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..d610092467fc 100644
> > > > > > --- a/drivers/platform/x86/amd/Kconfig
> > > > > > +++ b/drivers/platform/x86/amd/Kconfig
> > > > > > @@ -44,3 +44,17 @@ config AMD_ISP_PLATFORM
> > > > > >
> > > > > > This driver can also be built as a module. If so, the module
> > > > > > will be called amd_isp4.
> > > > > > +
> > > > > > +config AMD_DPTC
> > > > > > + tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)"
> > > > > > + depends on X86_64 && ACPI && DMI
> > > > > > + select FIRMWARE_ATTRIBUTES_CLASS
> > > > > > + help
> > > > > > + Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> > > > > > + Thermal Configuration Interface (DPTCi). Exposes TDP and thermal
> > > > > > + parameters for AMD APU-based handheld devices via the
> > > > > > + firmware-attributes sysfs ABI, allowing userspace tools to stage
> > > > > > + and atomically commit power limit settings. Requires a DMI match
> > > > > > + for the device and a recognized AMD SoC.
> > > > > > +
> > > > > > + If built as a module, the module will be called amd_dptc.
> > > > > > diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile
> > > > > > index f6ff0c837f34..862a609bfe38 100644
> > > > > > --- a/drivers/platform/x86/amd/Makefile
> > > > > > +++ b/drivers/platform/x86/amd/Makefile
> > > > > > @@ -12,3 +12,5 @@ obj-$(CONFIG_AMD_PMF) += pmf/
> > > > > > obj-$(CONFIG_AMD_WBRF) += wbrf.o
> > > > > > obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o
> > > > > > obj-$(CONFIG_AMD_HFI) += hfi/
> > > > > > +obj-$(CONFIG_AMD_DPTC) += amd_dptc.o
> > > > > > +amd_dptc-y := dptc.o
> > > > > > diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
> > > > > > new file mode 100644
> > > > > > index 000000000000..acfe9cc01bab
> > > > > > --- /dev/null
> > > > > > +++ b/drivers/platform/x86/amd/dptc.c
> > > > > > @@ -0,0 +1,747 @@
> > > > > > +// SPDX-License-Identifier: GPL-2.0-only
> > > > > > +/*
> > > > > > + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver
> > > > > > + *
> > > > > > + * Exposes AMD APU power and thermal parameters via the firmware-attributes
> > > > > > + * sysfs ABI. Parameters are staged and atomically committed through the
> > > > > > + * AGESA ALIB Function 0x0C (Dynamic Power and Thermal Configuration
> > > > > > + * interface).
> > > > > > + *
> > > > > > + * Reference: AMD AGESA Publication #44065, Appendix E.5
> > > > > > + * https://docs.amd.com/v/u/en-US/44065_Arch2008
> > > > > > + *
> > > > > > + * Copyright (C) 2026 Antheas Kapenekakis <lkml@antheas.dev>
> > > > > > + */
> > > > > > +
> > > > > > +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> > > > > > +
> > > > > > +#include <linux/acpi.h>
> > > > > > +#include <linux/cleanup.h>
> > > > > > +#include <linux/dmi.h>
> > > > > > +#include <linux/init.h>
> > > > > > +#include <linux/kobject.h>
> > > > > > +#include <linux/module.h>
> > > > > > +#include <linux/mutex.h>
> > > > > > +#include <linux/platform_device.h>
> > > > > > +#include <linux/processor.h>
> > > > > > +#include <linux/sysfs.h>
> > > > > > +#include <linux/unaligned.h>
> > > > > > +
> > > > > > +#include "../firmware_attributes_class.h"
> > > > > > +
> > > > > > +#define DRIVER_NAME "amd-dptc"
> > > > > > +
> > > > > > +#define ALIB_FUNC_DPTC 0x0C
> > > > > > +#define ALIB_PATH "\\_SB.ALIB"
> > > > > > +
> > > > > > +#define ALIB_ID_TEMP_TARGET 0x03
> > > > > > +#define ALIB_ID_STAPM_LIMIT 0x05
> > > > > > +#define ALIB_ID_FAST_LIMIT 0x06
> > > > > > +#define ALIB_ID_SLOW_LIMIT 0x07
> > > > > > +#define ALIB_ID_SKIN_LIMIT 0x2E
> > > > > > +
> > > > > > +enum dptc_param_idx {
> > > > > > + DPTC_PPT_PL1_SPL, /* STAPM + skin limit (set together) */
> > > > > > + DPTC_PPT_PL2_SPPT, /* slow PPT limit */
> > > > > > + DPTC_PPT_PL3_FPPT, /* fast PPT limit */
> > > > > > + DPTC_CPU_TEMP, /* thermal control target */
> > > > > > + DPTC_NUM_PARAMS,
> > > > > > +};
> > > > > > +
> > > > > > +struct dptc_param_limits {
> > > > > > + u32 expanded_min;
> > > > > > + u32 device_min;
> > > > > > + u32 def;
> > > > > > + u32 device_max;
> > > > > > + u32 expanded_max;
> > > > > > +};
> > > > > > +
> > > > > > +struct dptc_device_limits {
> > > > > > + struct dptc_param_limits params[DPTC_NUM_PARAMS];
> > > > > > +};
> > > > > > +
> > > > > > +struct dptc_param_desc {
> > > > > > + const char *name;
> > > > > > + const char *display_name;
> > > > > > + u16 scale; /* sysfs-to-ALIB multiplier (e.g. 1000 for W->mW) */
> > > > > > + u8 param_id;
> > > > > > + u8 param_id2; /* secondary ALIB ID, 0 if none */
> > > > > > +};
> > > > > > +
> > > > > > +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = {
> > > > > > + [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (W)",
> > > > > > + 1000, ALIB_ID_STAPM_LIMIT, ALIB_ID_SKIN_LIMIT },
> > > > > > + [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (W)",
> > > > > > + 1000, ALIB_ID_SLOW_LIMIT },
> > > > > > + [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (W)",
> > > > > > + 1000, ALIB_ID_FAST_LIMIT },
> > > > > > + [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)",
> > > > > > + 1, ALIB_ID_TEMP_TARGET },
> > > > > > +};
> > > > >
> > > > > The indent here just made me crazy :-/
> > > > >
> > > > > Align the contents after line breaks to the left braces.
> > > >
> > > > I will review how to do the alignment, although I'm not happy with how
> > > > aligning to spaces will look. PL1 will overflow.
> >
> > No, it won't. Having four tabs before `1000' gives you a line length of
> > 80 characters (editors will tell that you are at column 81). And
> > nowadays the maximum line length allowed is 100 (that's why Derek
> > suggested you should avoid some unnecessary wraps).
>
> I double wrapped the first line and it is fine. I have already staged
> the fixes for v4
>
> > > >
> > > > > > +
> > > > > > +/* AI MAX Handheld class: GPD Win 5 */
> > > > > > +static const struct dptc_device_limits limits_maxhh = { .params = {
> > > > >
> > > > > Please don't save a line by zipping braces like this. It's hard to read
> > > > > and likely to be missed when glancing the code (that's what happened to
> > > > > me and it really confused me).
> > > > >
> > > > > I found you did the same in [PATCH v3 3/4] for `.profiles'. That should
> > > > > be avoided too.
> > > >
> > > > This is a leftover from before adding profiles. I guess it does not
> > > > make much sense now. I can reflow.
> > > >
> > > > > > + [DPTC_PPT_PL1_SPL] = { 0, 4, 25, 80, 100 },
> > > > > > + [DPTC_PPT_PL2_SPPT] = { 0, 4, 27, 82, 100 },
> > > > > > + [DPTC_PPT_PL3_FPPT] = { 0, 4, 40, 85, 100 },
> > > > > > + [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 },
> > > > > > +}};
> > > > > > +
> > > > > > +/* Substring matches against boot_cpu_data.x86_model_id; order matters. */
> > > > > > +static const char * const dptc_soc_table[] = {
> > > > > > + /* AI MAX */
> > > > > > + "AMD RYZEN AI MAX+ 395",
> > > > > > + "AMD RYZEN AI MAX+ 385",
> > > > > > + "AMD RYZEN AI MAX 380",
> > > > > > + NULL,
> > > > > > +};
> > > > > > +
> > > > > > +static const struct dmi_system_id dptc_dmi_table[] = {
> > > > > > + /* GPD */
> > > > > > + {
> > > > > > + .ident = "GPD Win 5",
> > > > > > + .matches = {
> > > > > > + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> > > > > > + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"),
> > > > > > + },
> > > > > > + .driver_data = (void *)&limits_maxhh,
> > > > > > + },
> > > > > > + { }
> > > > > > +};
> > > > > > +MODULE_DEVICE_TABLE(dmi, dptc_dmi_table);
> > > > > > +
> > > > > > +struct dptc_priv;
> > > > > > +
> > > > > > +struct dptc_attr_sysfs {
> > > > > > + struct dptc_priv *priv;
> > > > > > + struct kobj_attribute current_value;
> > > > > > + struct kobj_attribute default_value;
> > > > > > + struct kobj_attribute min_value;
> > > > > > + struct kobj_attribute max_value;
> > > > > > + struct kobj_attribute scalar_increment;
> > > > > > + struct kobj_attribute display_name;
> > > > > > + struct kobj_attribute type;
> > > > > > + struct attribute *attrs[8];
> > > > > > + struct attribute_group group;
> > > > > > + int idx;
> > > > > > +};
> > > > > > +
> > > > > > +struct dptc_priv {
> > > > > > + struct device *fw_attr_dev;
> > > > > > + struct kset *fw_attr_kset;
> > > > > > +
> > > > > > + const struct dptc_device_limits *dev_limits;
> > > > > > +
> > > > > > + bool expanded;
> > > > > > +
> > > > > > + enum dptc_save_mode { SAVE_SINGLE, SAVE_BULK } save_mode;
> > > > >
> > > > > Defining the enumeration separately will improve readability.
> > > > >
> > > > > > +
> > > > > > + u32 staged[DPTC_NUM_PARAMS];
> > > > > > + bool has_staged[DPTC_NUM_PARAMS];
> > > > > > +
> > > > > > + /* Protects mutable driver state */
> > > > >
> > > > > The comment is nonsense. To make it helpful, it should be "Protects
> > > > > members above/below" with properly arranged structure layout.
> > > > >
> > > > > > + struct mutex lock;
> > > > > > +
> > > > > > + struct dptc_attr_sysfs params[DPTC_NUM_PARAMS];
> > > > > > + struct dptc_attr_sysfs expanded_attr;
> > > > > > + struct kobj_attribute save_settings_attr;
> > > > > > +};
> > > > > > +
> > > > > > +static struct platform_device *dptc_pdev;
> > > > > > +
> > > > > > +static u32 dptc_get_min(struct dptc_priv *dptc, int idx)
> > > > > > +{
> > > > > > + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_min
> > > > > > + : dptc->dev_limits->params[idx].device_min;
> > > > > > +}
> > > > > > +
> > > > > > +static u32 dptc_get_max(struct dptc_priv *dptc, int idx)
> > > > > > +{
> > > > > > + return dptc->expanded ? dptc->dev_limits->params[idx].expanded_max
> > > > > > + : dptc->dev_limits->params[idx].device_max;
> > > > > > +}
> > > > > > +
> > > > > > +static u32 dptc_get_default(struct dptc_priv *dptc, int idx)
> > > > > > +{
> > > > > > + return dptc->dev_limits->params[idx].def;
> > > > > > +}
> > > > > > +
> > > > > > +static int dptc_alib_call(const u8 *ids, const u32 *vals, int count)
> > > > > > +{
> > > > > > + union acpi_object in_params[2];
> > > > > > + struct acpi_object_list input;
> > > > > > + acpi_status status;
> > > > > > + u32 buf_size;
> > > > > > + int i, off;
> > > > > > + u8 *buf;
> > > > > > +
> > > > > > + if (count == 0)
> > > > > > + return -EINVAL;
> > > > >
> > > > > Return -ENOENT as think-lmi does (see its save_settings_store()) and
> > > > > update the documentation accordingly.
> > > > >
> > > > > > +
> > > > > > + /* 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);
> > > > >
> > > > > 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;
> > > > >
> > > > > Misaligned `='.
> > > > >
> > > > > > +
> > > > > > + 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 hw_val = val * dptc_params[idx].scale;
> > > > > > + u32 vals[2];
> > > > > > + u8 ids[2];
> > > > > > + int count = 0;
> > > > > > +
> > > > > > + ids[count] = dptc_params[idx].param_id;
> > > > > > + vals[count] = hw_val;
> > > > > > + count++;
> > > > > > + if (dptc_params[idx].param_id2) {
> > > > > > + ids[count] = dptc_params[idx].param_id2;
> > > > > > + vals[count] = hw_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;
> > > > > > + u32 hw_val;
> > > > > > +
> > > > > > + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> > > > > > + if (!dptc->has_staged[i])
> > > > > > + continue;
> > > > > > +
> > > > > > + hw_val = dptc->staged[i] * dptc_params[i].scale;
> > > > > > + ids[count] = dptc_params[i].param_id;
> > > > > > + vals[count] = hw_val;
> > > > > > + count++;
> > > > > > + if (dptc_params[i].param_id2) {
> > > > > > + ids[count] = dptc_params[i].param_id2;
> > > > > > + vals[count] = hw_val;
> > > > > > + count++;
> > > > > > + }
> > > > >
> > > > > I could imagine a helper function to prevent replicating the same code
> > > > > patterns twice:
> > > > >
> > > > > static size_t dptc_alib_fill_param(u8 *ids, u32 *vals, size_t offset,
> > > > > enum dptc_param_idx param, u32 val)
> > > > > {
> > > > > u32 hw_val = val * dptc_params[param].scale;
> > > > >
> > > > > ids[offset] = dptc_params[param].param_id;
> > > > > vals[offset++] = hw_val;
> > > > >
> > > > > if (dptc_params[param].param_id2) {
> > > > > ids[offset] = dptc_params[param].param_id2;
> > > > > vals[offset++] = hw_val;
> > > > > }
> > > > >
> > > > > return offset;
> > > > > }
> > > > >
> > > > > > + }
> > > > > > +
> > > > > > + 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->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);
> > > > > > +
> > > > > > + dptc->has_staged[ps->idx] = false;
> > > > > > + return count;
> > > > > > + }
> > > > > > +
> > > > > > + ret = kstrtou32(buf, 10, &val);
> > > > > > + if (ret)
> > > > > > + return ret;
> > > > > > +
> > > > > > + guard(mutex)(&dptc->lock);
> > > > > > +
> > > > > > + 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"))
> > > > >
> > > > > Please don't rely on the `count == 0' check in dptc_alib_call(). `dptc-
> > > > > > save_mode == SAVE_SINGLE' should result in -EOPNOTSUPP to maintain
> > > > > consistency with think-lmi (update the documentation accordingly).
> > > >
> > > > I will consider it. But this is subtractive behavior. We are still ABI
> > > > compliant even if the write goes through and it does not affect
> > > > functionality.
> >
> > If you return something different in a certain case, it's neither API
> > compatible nor ABI compatible.
> >
> > Also the issue here is more about you shouldn't rely on the `count ==
> > 0' check in dptc_alib_call() to prevent running into unexpected state
> > when `dptc->save_mode == SAVE_SINGLE' but "save" is written. Explicit
> > checks are better than implicit ones.
>
> I fixed te code to return ENOENT when count is 0. Not returning an
> error and accepting the write instead of sending an error is a
> superset of the previous API so it is not a problem. It is fine to
> write the params with "single"
>
> > > >
> > > > > > + ret = dptc_alib_save(dptc);
> > > > > > + else if (sysfs_streq(buf, "single"))
> > > > > > + dptc->save_mode = SAVE_SINGLE;
> > > > > > + else if (sysfs_streq(buf, "bulk"))
> > > > > > + dptc->save_mode = SAVE_BULK;
> > > > > > + else
> > > > > > + return -EINVAL;
> > > > > > +
> > > > > > + return ret ? ret : count;
> > > > > > +}
> > > > > > +
> > > > > > +static ssize_t dptc_expanded_current_value_show(struct kobject *kobj,
> > > > > > + struct kobj_attribute *attr,
> > > > > > + char *buf)
> > > > > > +{
> > > > > > + struct dptc_attr_sysfs *ps =
> > > > > > + container_of(attr, struct dptc_attr_sysfs, current_value);
> > > > > > + struct dptc_priv *dptc = ps->priv;
> > > > > > +
> > > > > > + guard(mutex)(&dptc->lock);
> > > > > > +
> > > > > > + return sysfs_emit(buf, "%d\n", dptc->expanded);
> > > > > > +}
> > > > > > +
> > > > > > +static ssize_t dptc_expanded_current_value_store(struct kobject *kobj,
> > > > > > + struct kobj_attribute *attr,
> > > > > > + const char *buf, size_t count)
> > > > > > +{
> > > > > > + struct dptc_attr_sysfs *ps =
> > > > > > + container_of(attr, struct dptc_attr_sysfs, current_value);
> > > > > > + struct dptc_priv *dptc = ps->priv;
> > > > > > + bool val;
> > > > > > + int ret;
> > > > > > +
> > > > > > + ret = kstrtobool(buf, &val);
> > > > > > + if (ret)
> > > > > > + return ret;
> > > > > > +
> > > > > > + guard(mutex)(&dptc->lock);
> > > > > > +
> > > > > > + dptc->expanded = val;
> > > > > > + /* Clear staged values: limits changed, old values may be out of range */
> > > > > > + memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
> > > > >
> > > > > I'd prefer using a bitmap so that you don't need to use memset() to
> > > > > clear them. It also saves some memory if more parameters are added in
> > > > > the future.
> > > >
> > > > I will review if has_staged needs to exist. Values cannot be 0 anyway.
> > > > A bit map has other readability issues.
> >
> > Just a reminder: you may also need to increase expanded_min to 1.
>
> I did.
>
> > > >
> > > > > > +
> > > > > > + 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;
> > > > > > +}
> > > > >
> > > > > As Derek has said in v2, use macros to rearrange them.
> > > > >
> > > > > It's painful to read all these things as I'll need to scroll the screen
> > > > > quite a lot just to read something with a low information density.
> > > > > That's why there are numerous macros defined in include/linux/sysfs.h.
> > > > > I could imagine defining macros like DPTC{,_EXPANDED}_ATTR_{RO,RW}.
> > > >
> > > > This driver consists of two instantiation blocks. It is not possible
> > > > to dedup the initialization functions and save space because the
> > > > function holders and structs that would be needed undo that.
> >
> > Undo what?
>
> The space savings. I tried to merge the functions
> dptc_setup_expanded_sysfs and dptc_setup_param_sysfs and this ended up
> having around the same lines and being overly complicated with
> additional structs.
>
> > > >
> > > > The blocks in lenovo other are significantly less readable and harder
> > > > to write. Perhaps you save around 20 lines. But it is not worth it
> > > > imo.
> > >
> > > I would consider adding them if the helper macros are centralized, but
> > > I am not adding macros to this driver.
> >
> > The macros in lenovo-wmi-other are completely readable and are just
> > some common attribute techniques. Just search for `##_attr|attr_##',
> > and you'll see a lot of similar patterns.
> >
> > Also my PoC (see attachment) showed that using macros to allocate them
> > statically saves 77 lines.
>
> But the driver is 800 lines already in this patch. If you are willing
> to experiment and want some patches under your belt, I'd suggest you
> try to create some centralized macros and introduce them for
> armoury/lenovo-other first.
>
> I am willing to use centralized macros instead of dynamic
> instantiation in this driver as a follow-up. But I am not willing to
> introduce macros.
>
> As one of the issues of the macros, I will point to your review of
> Derek's patch in lenovo-other recently where he missed an alignment
> and a tendency to avoid these types of comments.
>
> > >
> > > Antheas
> > >
> > > > > Meanwhile, I am not very sure why you would have to dynamically
> > > > > initialize all these attributes. I haven't notice a blocker of defining
> > > > > them statically at the module-level (see how lenovo-wmi-other did). Am
> > > > > I missing anything?
> > > >
> > > > You are not missing something. There is no benefit to dynamic
> > > > allocation. Static allocation needs macros.
> >
> > If dynamic allocation of attributes isn't required, it should be
> > avoided at all. Having unnecessary dynamic allocated attributes is an
> > anti-pattern, especially when there are (4 + 1) * 7 + 1 = 36
> > attributes. It slows down the probe sequence and adds more pressure to
> > kzalloc().
> >
> > IMHO unwillingness to add macros is not a valid excuse of allocating
> > and initializing attributes dynamically.
>
> It is an accepted pattern in upstream drivers. It is a valid design choice.
>
> > Anyway, I've quickly figured out a PoC patch (based on v3 4/4) to
> > demonstrate how to properly define readable macros to allocate all
> > attributes statically. See the attachment.
> >
> > You may add
> >
> > Co-developed-by: Rong Zhang <i@rong.moe>
> > Signed-off-by: Rong Zhang <i@rong.moe>
> >
> > to v4 if you decide to adopt my patch.
>
> I reviewed your attachment. It mirrors the pattern in lenovo-other. I
> would rather you redirect this energy to centralizing the macros
> instead, so that I can just use the definitions. This way, we avoid
> duplicating code.
>
> If this driver ends up merging before you introduce such a series,
> feel free to refactor this driver as part of it as well and I will ack
> it.
>
> Other maintainers have expressed a need for centralized macros for
> firmware attributes, so it would be a good opportunity for you.
Centralized helpers for adding attributes* If they are macros, this
should be fine too.
> Best,
> Antheas
>
> > > >
> > > > > > +
> > > > > > +static void dptc_fw_dev_unregister(void *data)
> > > > > > +{
> > > > > > + device_unregister(data);
> > > > > > +}
> > > > > > +
> > > > > > +static void dptc_kset_unregister(void *data)
> > > > > > +{
> > > > > > + kset_unregister(data);
> > > > > > +}
> > > > > > +
> > > > > > +static int dptc_resume(struct device *dev)
> > > > > > +{
> > > > > > + struct dptc_priv *dptc = dev_get_drvdata(dev);
> > > > > > + int ret;
> > > > > > +
> > > > > > + guard(mutex)(&dptc->lock);
> > > > > > +
> > > > > > + if (dptc->save_mode == SAVE_SINGLE)
> > > > > > + ret = dptc_alib_save(dptc);
> > > > >
> > > > > I don't see why bulk mode and single mode should have divergence on
> > > > > resuming behavior.
> > > >
> > > > This is personal preference.
> >
> > Then add comments to the code with your explanation.
>
> Ack. It is already in the sysfs doc but I can add it here too.
>
> > Thanks,
> > Rong
> >
> > > > I'd rather bulk mode completely disengage
> > > > the driver handling. It also gives it a better reason to exist. After
> > > > all, ALIB runs a for loop inside it, there is no difference calling it
> > > > 3 times or 1 as far as the GPU is concerned. It is only ACPI overhead.
> > > > The original name was commit_mode, with manual/auto. But while working
> > > > on the ABI entry I found save_settings, and it was an ABI match so I
> > > > opted for that instead of making a new entry.
> > > >
> > > > Unfortunately, naming does not match precisely.
> > > >
> > > > Thanks for the review! Ack. on everything I did not comment on.
> > > >
> > > > Best,
> > > > Antheas
> > > >
> > > > > Thanks,
> > > > > Rong
> > > > >
> > > > > > + 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 device *dev = &pdev->dev;
> > > > > > + struct dptc_priv *dptc;
> > > > > > + int i, ret;
> > > > > > +
> > > > > > + dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL);
> > > > > > + if (!dptc)
> > > > > > + return -ENOMEM;
> > > > > > +
> > > > > > + platform_set_drvdata(pdev, dptc);
> > > > > > +
> > > > > > + ret = devm_mutex_init(dev, &dptc->lock);
> > > > > > + if (ret)
> > > > > > + return ret;
> > > > > > +
> > > > > > + dptc->dev_limits = dmi_match->driver_data;
> > > > > > + dev_info(dev, "%s (%s)\n", dmi_match->ident,
> > > > > > + boot_cpu_data.x86_model_id);
> > > > > > +
> > > > > > + dptc->fw_attr_dev = device_create(&firmware_attributes_class,
> > > > > > + NULL, MKDEV(0, 0), NULL,
> > > > > > + DRIVER_NAME);
> > > > > > + if (IS_ERR(dptc->fw_attr_dev))
> > > > > > + return PTR_ERR(dptc->fw_attr_dev);
> > > > > > +
> > > > > > + ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister,
> > > > > > + dptc->fw_attr_dev);
> > > > > > + if (ret)
> > > > > > + return ret;
> > > > > > +
> > > > > > + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL,
> > > > > > + &dptc->fw_attr_dev->kobj);
> > > > > > + if (!dptc->fw_attr_kset)
> > > > > > + return -ENOMEM;
> > > > > > +
> > > > > > + ret = devm_add_action_or_reset(dev, dptc_kset_unregister,
> > > > > > + dptc->fw_attr_kset);
> > > > > > + if (ret)
> > > > > > + return ret;
> > > > > > +
> > > > > > + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> > > > > > + dptc_setup_param_sysfs(dptc, &dptc->params[i], i);
> > > > > > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> > > > > > + &dptc->params[i].group);
> > > > > > + if (ret)
> > > > > > + return ret;
> > > > > > + }
> > > > > > +
> > > > > > + dptc_setup_expanded_sysfs(dptc, &dptc->expanded_attr);
> > > > > > + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> > > > > > + &dptc->expanded_attr.group);
> > > > > > + if (ret)
> > > > > > + return ret;
> > > > > > +
> > > > > > + sysfs_attr_init(&dptc->save_settings_attr.attr);
> > > > > > + dptc->save_settings_attr.attr.name = "save_settings";
> > > > > > + dptc->save_settings_attr.attr.mode = 0644;
> > > > > > + dptc->save_settings_attr.show = dptc_save_settings_show;
> > > > > > + dptc->save_settings_attr.store = dptc_save_settings_store;
> > > > > > + ret = sysfs_create_file(&dptc->fw_attr_kset->kobj,
> > > > > > + &dptc->save_settings_attr.attr);
> > > > > > + if (ret)
> > > > > > + return ret;
> > > > > > +
> > > > > > + return 0;
> > > > > > +}
> > > > > > +
> > > > > > +static struct platform_driver dptc_driver = {
> > > > > > + .driver = {
> > > > > > + .name = DRIVER_NAME,
> > > > > > + .pm = pm_sleep_ptr(&dptc_pm_ops),
> > > > > > + },
> > > > > > + .probe = dptc_probe,
> > > > > > +};
> > > > > > +
> > > > > > +static int __init dptc_init(void)
> > > > > > +{
> > > > > > + const struct dmi_system_id *match;
> > > > > > + bool soc_found = false;
> > > > > > + int i, ret;
> > > > > > +
> > > > > > + match = dmi_first_match(dptc_dmi_table);
> > > > > > + if (!match)
> > > > > > + return -ENODEV;
> > > > > > +
> > > > > > + if (!acpi_has_method(NULL, ALIB_PATH)) {
> > > > > > + pr_warn("ALIB method not present\n");
> > > > > > + return -ENODEV;
> > > > > > + }
> > > > > > +
> > > > > > + for (i = 0; dptc_soc_table[i]; i++) {
> > > > > > + if (strstr(boot_cpu_data.x86_model_id,
> > > > > > + dptc_soc_table[i])) {
> > > > > > + soc_found = true;
> > > > > > + break;
> > > > > > + }
> > > > > > + }
> > > > > > + if (!soc_found) {
> > > > > > + pr_warn("unrecognized SoC '%s'\n",
> > > > > > + boot_cpu_data.x86_model_id);
> > > > > > + return -ENODEV;
> > > > > > + }
> > > > > > +
> > > > > > + dptc_pdev = platform_device_register_data(NULL, DRIVER_NAME, -1,
> > > > > > + match, sizeof(*match));
> > > > > > + if (IS_ERR(dptc_pdev))
> > > > > > + return PTR_ERR(dptc_pdev);
> > > > > > +
> > > > > > + ret = platform_driver_register(&dptc_driver);
> > > > > > + if (ret) {
> > > > > > + platform_device_unregister(dptc_pdev);
> > > > > > + return ret;
> > > > > > + }
> > > > > > +
> > > > > > + return 0;
> > > > > > +}
> > > > > > +
> > > > > > +static void __exit dptc_exit(void)
> > > > > > +{
> > > > > > + platform_driver_unregister(&dptc_driver);
> > > > > > + platform_device_unregister(dptc_pdev);
> > > > > > +}
> > > > > > +
> > > > > > +module_init(dptc_init);
> > > > > > +module_exit(dptc_exit);
> > > > > > +
> > > > > > +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>");
> > > > > > +MODULE_DESCRIPTION("AMD DPTCi ACPI Driver");
> > > > > > +MODULE_LICENSE("GPL");
> > > > >
^ permalink raw reply [flat|nested] 19+ messages in thread
end of thread, other threads:[~2026-03-08 22:18 UTC | newest]
Thread overview: 19+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-03-07 11:55 [RFC v3 0/4] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Antheas Kapenekakis
2026-03-07 11:55 ` [RFC v3 1/4] Documentation: firmware-attributes: generalize save_settings entry Antheas Kapenekakis
2026-03-07 11:55 ` [RFC v3 2/4] platform/x86/amd: dptc: Add AMD DPTCi driver Antheas Kapenekakis
2026-03-07 21:50 ` Rong Zhang
2026-03-07 23:02 ` Antheas Kapenekakis
2026-03-07 23:08 ` Antheas Kapenekakis
2026-03-08 20:00 ` Rong Zhang
2026-03-08 22:15 ` Antheas Kapenekakis
2026-03-08 22:18 ` Antheas Kapenekakis
2026-03-07 11:55 ` [RFC v3 3/4] platform/x86/amd: dptc: Add platform profile support Antheas Kapenekakis
2026-03-07 21:51 ` Rong Zhang
2026-03-07 22:45 ` Antheas Kapenekakis
2026-03-08 9:30 ` Rong Zhang
2026-03-08 9:45 ` Antheas Kapenekakis
2026-03-07 11:55 ` [RFC v3 4/4] platform/x86/amd: dptc: Add device entries for handheld PCs Antheas Kapenekakis
2026-03-07 21:51 ` Rong Zhang
2026-03-07 22:40 ` Antheas Kapenekakis
2026-03-08 9:23 ` Rong Zhang
2026-03-08 9:42 ` 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®