From: Mario Limonciello <mario.limonciello@amd.com>
To: Antheas Kapenekakis <lkml@antheas.dev>
Cc: W_Armin@gmx.de, sashal@kernel.org, Shyam-Sundar.S-k@amd.com,
derekjohn.clark@gmail.com, denis.benato@linux.dev,
linux-kernel@vger.kernel.org,
platform-driver-x86@vger.kernel.org
Subject: Re: [RFC v2 2/2] platform/x86/amd: Add AMD DPTCi driver
Date: Thu, 5 Mar 2026 14:57:59 -0600 [thread overview]
Message-ID: <54ca62dc-d590-44a3-8310-f7da59c2a42e@amd.com> (raw)
In-Reply-To: <20260305181751.3642846-3-lkml@antheas.dev>
On 3/5/2026 12:17 PM, Antheas Kapenekakis wrote:
> Implement a driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power
> and Thermal Configuration Interface (DPTCi). This function allows
> userspace to configure APU power and thermal parameters at runtime by
> calling the \_SB.ALIB ACPI method with a packed parameter buffer.
>
> Unlike mainstream AMD laptops, the handheld devices targeted by this
> driver do not implement vendor-specific WMI or EC hooks for TDP control.
> The ones that do, use DPTCi under the hood. For these devices, exposing
> the ALIB interface is the only viable mechanism for the OS to adjust
> power limits, making a dedicated kernel driver the correct approach
> rather than relying on unrestricted access to /dev/mem or ACPI method
> invocation from userspace.
>
> The driver matches the ABI of asus-armoury, by exposing a platform
> profile with reasonable tunings for devices depending on their max
> thermal envelope, and {ppt_pl1_spl,ppt_pl2_sppt,ppt_pl3_fppt,cpu_temp}
> tunables which only become writable when the profile is custom,
> otherwise they are read-only and reflect the current profile.
>
> The default profile is custom so that we do not write to the device
> until userspace explicitly selects a profile, remaining at firmware
> defaults.
>
> Assisted-by: Claude:claude-opus-4-6
> Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
As a general comment that will make this easier to review (than a ~1.3k
LoC file) could you split into logical patches that add functionality?
Likewise could you split some of the code into a few source files? A
logical split tha I see would be to put the quirks and associated limits
into a single file and do a lookup of values.
> ---
> MAINTAINERS | 6 +
> drivers/platform/x86/amd/Kconfig | 16 +
> drivers/platform/x86/amd/Makefile | 2 +
> drivers/platform/x86/amd/dptc.c | 1255 +++++++++++++++++++++++++++++
> 4 files changed, 1279 insertions(+)
> create mode 100644 drivers/platform/x86/amd/dptc.c
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index e08767323763..915293594641 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -1096,6 +1096,12 @@ S: Supported
> F: drivers/gpu/drm/amd/display/dc/dml/
> F: drivers/gpu/drm/amd/display/dc/dml2_0/
>
> +AMD DPTC DRIVER
> +M: Antheas Kapenekakis <lkml@antheas.dev>
> +L: platform-driver-x86@vger.kernel.org
> +S: Maintained
> +F: drivers/platform/x86/amd/dptc.c
> +
> AMD FAM15H PROCESSOR POWER MONITORING DRIVER
> M: Huang Rui <ray.huang@amd.com>
> L: linux-hwmon@vger.kernel.org
> diff --git a/drivers/platform/x86/amd/Kconfig b/drivers/platform/x86/amd/Kconfig
> index b813f9265368..ee79a2c084a8 100644
> --- a/drivers/platform/x86/amd/Kconfig
> +++ b/drivers/platform/x86/amd/Kconfig
> @@ -44,3 +44,19 @@ config AMD_ISP_PLATFORM
>
> This driver can also be built as a module. If so, the module
> will be called amd_isp4.
> +
> +config AMD_DPTC
> + tristate "AMD Dynamic Power and Thermal Configuration Interface (DPTCi)"
> + depends on X86_64 && ACPI && DMI
> + select ACPI_PLATFORM_PROFILE
> + select FIRMWARE_ATTRIBUTES_CLASS
> + help
> + Driver for AMD AGESA ALIB Function 0x0C, the Dynamic Power and
> + Thermal Configuration Interface (DPTCi). Exposes TDP and thermal
> + parameters for AMD APU-based handheld devices via the
> + firmware-attributes sysfs ABI, allowing userspace tools to stage
> + and atomically commit power limit settings. Requires a DMI match
> + for the device and a recognized AMD SoC.
> +
> + If built as a module, the module will be called amd_dptc.
> +
> diff --git a/drivers/platform/x86/amd/Makefile b/drivers/platform/x86/amd/Makefile
> index f6ff0c837f34..862a609bfe38 100644
> --- a/drivers/platform/x86/amd/Makefile
> +++ b/drivers/platform/x86/amd/Makefile
> @@ -12,3 +12,5 @@ obj-$(CONFIG_AMD_PMF) += pmf/
> obj-$(CONFIG_AMD_WBRF) += wbrf.o
> obj-$(CONFIG_AMD_ISP_PLATFORM) += amd_isp4.o
> obj-$(CONFIG_AMD_HFI) += hfi/
> +obj-$(CONFIG_AMD_DPTC) += amd_dptc.o
> +amd_dptc-y := dptc.o
> diff --git a/drivers/platform/x86/amd/dptc.c b/drivers/platform/x86/amd/dptc.c
> new file mode 100644
> index 000000000000..68d9d3b85cb6
> --- /dev/null
> +++ b/drivers/platform/x86/amd/dptc.c
> @@ -0,0 +1,1255 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * AMD Dynamic Power and Thermal Configuration Interface (DPTCi) driver
> + *
> + * Exposes AMD APU power and thermal parameters via the firmware-attributes
> + * sysfs ABI. Parameters are staged and atomically committed through the
> + * AGESA ALIB Function 0x0C (Dynamic Power and Thermal Configuration
> + * interface).
> + *
> + * Reference: AMD AGESA Publication #44065, Appendix E.5
> + * https://docs.amd.com/v/u/en-US/44065_Arch2008
> + *
> + * Copyright (C) 2026 Antheas Kapenekakis <lkml@antheas.dev>
> + */
> +
> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> +
> +#include <linux/acpi.h>
> +#include <linux/cleanup.h>
> +#include <linux/dmi.h>
> +#include <linux/init.h>
> +#include <linux/kobject.h>
> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/platform_device.h>
> +#include <linux/platform_profile.h>
> +#include <linux/processor.h>
> +#include <linux/sysfs.h>
> +#include <linux/unaligned.h>
> +
> +#include "../firmware_attributes_class.h"
> +
> +#define DRIVER_NAME "amd_dptc"
> +
> +#define ALIB_FUNC_DPTC 0x0C
> +#define ALIB_PATH "\\_SB.ALIB"
> +
> +/* ALIB parameter IDs (AGESA spec Appendix E.5, Table E-52) */
> +#define ALIB_ID_TEMP_TARGET 0x03
> +#define ALIB_ID_STAPM_LIMIT 0x05
> +#define ALIB_ID_FAST_LIMIT 0x06
> +#define ALIB_ID_SLOW_LIMIT 0x07
> +#define ALIB_ID_SKIN_LIMIT 0x2E
> +
> +enum dptc_param_idx {
> + DPTC_PPT_PL1_SPL, /* STAPM + skin limit (set together) */
> + DPTC_PPT_PL2_SPPT, /* slow PPT limit */
> + DPTC_PPT_PL3_FPPT, /* fast PPT limit */
> + DPTC_CPU_TEMP, /* thermal control target */
> + DPTC_NUM_PARAMS,
> +};
> +
> +struct dptc_param_limits {
> + u32 min; /* expanded floor: widest safe hardware minimum */
> + u32 smin; /* device floor: safe operating minimum */
> + u32 def; /* default hint for userspace */
> + u32 smax; /* device ceiling: safe operating maximum */
> + u32 max; /* expanded ceiling: widest safe hardware maximum */
These variable names leave a little bit to be desired. I am generally
of the feeling "if you NEED to explain all the the variables in the
struct are you named them wrong".
> +};
> +
> +struct dptc_profile {
> + u32 vals[DPTC_NUM_PARAMS]; /* 0 = don't set / unstage this param */
> +};
> +
> +struct dptc_device_limits {
> + struct dptc_param_limits params[DPTC_NUM_PARAMS];
> + struct dptc_profile profiles[PLATFORM_PROFILE_LAST];
> +};
> +
> +struct dptc_param_desc {
> + const char *name;
> + const char *display_name;
> + u8 param_id;
> + u8 param_id2; /* secondary ALIB ID, 0 if none */
> +};
> +
> +static const struct dptc_param_desc dptc_params[DPTC_NUM_PARAMS] = {
> + [DPTC_PPT_PL1_SPL] = { "ppt_pl1_spl", "Sustained power limit (mW)",
> + ALIB_ID_STAPM_LIMIT, ALIB_ID_SKIN_LIMIT },
> + [DPTC_PPT_PL2_SPPT] = { "ppt_pl2_sppt", "Slow PPT limit (mW)",
> + ALIB_ID_SLOW_LIMIT },
> + [DPTC_PPT_PL3_FPPT] = { "ppt_pl3_fppt", "Fast PPT limit (mW)",
> + ALIB_ID_FAST_LIMIT },
> + [DPTC_CPU_TEMP] = { "cpu_temp", "Thermal control limit (C)",
> + ALIB_ID_TEMP_TARGET },
> +};
> +
> +/* 18W class: AYANEO AIR Plus (Ryzen 5 5560U) */
> +static const struct dptc_device_limits limits_18w = { .params = {
> + [DPTC_PPT_PL1_SPL] = { 0, 5000, 15000, 18000, 22000 },
> + [DPTC_PPT_PL2_SPPT] = { 0, 5000, 15000, 18000, 22000 },
> + [DPTC_PPT_PL3_FPPT] = { 0, 5000, 15000, 20000, 25000 },
> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> +}, .profiles = {
> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 5000, 5000, 8000, 0 } },
> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 12000, 14000, 15000, 0 } },
> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 18000, 18000, 20000, 0 } },
> +}};
> +
> +/* 25W class: Ryzen 5000 handhelds (AYANEO NEXT, KUN) */
> +static const struct dptc_device_limits limits_25w = { .params = {
> + [DPTC_PPT_PL1_SPL] = { 0, 4000, 15000, 25000, 32000 },
> + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 20000, 27000, 35000 },
> + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 25000, 30000, 37000 },
> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> +}, .profiles = {
> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8000, 8000, 12000, 0 } },
> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15000, 17000, 20000, 0 } },
> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25000, 27000, 30000, 0 } },
> +}};
> +
> +/* 28W class: GPD Win series, AYANEO 2, OrangePi NEO-01 */
> +static const struct dptc_device_limits limits_28w = { .params = {
> + [DPTC_PPT_PL1_SPL] = { 0, 4000, 15000, 28000, 32000 },
> + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 20000, 30000, 35000 },
> + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 25000, 32000, 37000 },
> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> +}, .profiles = {
> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8000, 8000, 12000, 0 } },
> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15000, 17000, 22000, 0 } },
> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25000, 28000, 32000, 0 } },
> + [PLATFORM_PROFILE_MAX_POWER] = { .vals = { 28000, 30000, 32000, 0 } },
> +}};
> +
> +/* 30W class: OneXPlayer, AYANEO AIR/FLIP/GEEK/SLIDE/3, AOKZOE */
> +static const struct dptc_device_limits limits_30w = { .params = {
> + [DPTC_PPT_PL1_SPL] = { 0, 4000, 15000, 30000, 40000 },
> + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 20000, 32000, 43000 },
> + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 25000, 41000, 50000 },
> + [DPTC_CPU_TEMP] = { 60, 70, 85, 90, 100 },
> +}, .profiles = {
> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 8000, 8000, 12000, 0 } },
> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 15000, 17000, 25000, 0 } },
> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 25000, 28000, 41000, 0 } },
> + [PLATFORM_PROFILE_MAX_POWER] = { .vals = { 30000, 32000, 41000, 0 } },
> +}};
> +
> +/* AI MAX Handheld class: GPD Win 5 */
> +static const struct dptc_device_limits limits_maxhh = { .params = {
> + [DPTC_PPT_PL1_SPL] = { 0, 4000, 25000, 80000, 100000 },
> + [DPTC_PPT_PL2_SPPT] = { 0, 4000, 27000, 82000, 100000 },
> + [DPTC_PPT_PL3_FPPT] = { 0, 4000, 40000, 85000, 100000 },
> + [DPTC_CPU_TEMP] = { 60, 70, 95, 95, 100 },
> +}, .profiles = {
> + [PLATFORM_PROFILE_LOW_POWER] = { .vals = { 15000, 15000, 25000, 0 } },
> + [PLATFORM_PROFILE_BALANCED] = { .vals = { 25000, 27000, 40000, 0 } },
> + [PLATFORM_PROFILE_PERFORMANCE] = { .vals = { 60000, 63000, 85000, 0 } },
> + [PLATFORM_PROFILE_MAX_POWER] = { .vals = { 80000, 82000, 85000, 0 } },
> +}};
> +
> +/* Substring matches against boot_cpu_data.x86_model_id; order matters. */
> +static const char * const dptc_soc_table[] = {
> + /* AI MAX */
> + "AMD RYZEN AI MAX+ 395",
> + "AMD RYZEN AI MAX+ 385",
> + "AMD RYZEN AI MAX 380",
> + /* Ryzen AI */
> + "AMD Ryzen AI 9 HX 370",
> + "AMD Ryzen AI HX 360",
> + /* Z1 - Extreme before plain Z1 */
> + "AMD Ryzen Z1 Extreme",
> + "AMD Ryzen Z1",
> + /* Ryzen 8000 */
> + "AMD Ryzen 7 8840U",
> + /* Ryzen 7040 */
> + "AMD Ryzen 7 7840U",
> + /* Ryzen 6000 */
> + "AMD Ryzen 7 6800U",
> + "AMD Ryzen 7 6600U",
> + /* Ryzen 5000 */
> + "AMD Ryzen 7 5800U",
> + "AMD Ryzen 7 5700U",
> + "AMD Ryzen 5 5560U",
> + NULL,
> +};
> +
I think this table can go. See my comments elsewhere.
> +static const struct dmi_system_id dptc_dmi_table[] = {
> + /* GPD */
> + {
> + .ident = "GPD Win Mini",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-01"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Win Mini 2024",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Win Mini 2024",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1617-02-L"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Win 4",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-04"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Win 5",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1618-05"),
> + },
> + .driver_data = (void *)&limits_maxhh,
> + },
> + {
> + .ident = "GPD Win Max 2",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-04"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Win Max 2 2024",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1619-05"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Duo",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Duo",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1622-01-L"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Pocket 4",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "GPD Pocket 4",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "GPD"),
> + DMI_MATCH(DMI_PRODUCT_NAME, "G1628-04-L"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + /* OrangePi */
> + {
> + .ident = "OrangePi NEO-01",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "OrangePi"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEO-01"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + /* AYN */
> + {
> + .ident = "AYN Loki Max",
> + .matches = {
> + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "ayn"),
> + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Loki Max"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + /* Tectoy (Zeenix Pro = Loki Max) */
> + {
> + .ident = "Zeenix Pro",
> + .matches = {
> + DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Tectoy"),
> + DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Zeenix Pro"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + /* AOKZOE */
> + {
> + .ident = "AOKZOE A1 AR07",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 AR07"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "AOKZOE A1 Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1 Pro"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "AOKZOE A1X",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A1X"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "AOKZOE A2 Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AOKZOE"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AOKZOE A2 Pro"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + /* OneXPlayer (Intel variants filtered by SoC table) */
> + {
> + .ident = "ONEXPLAYER F1Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1Pro"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER F1 EVA-02",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER F1 EVA-02"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER 2",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_MATCH(DMI_BOARD_NAME, "ONEXPLAYER 2"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER X1 A",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1 A"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER X1z",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1z"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER X1Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER X1Pro"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "ONEXPLAYER G1 A",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "ONE-NETBOOK"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "ONEXPLAYER G1 A"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + /* AYANEO - 18W */
> + {
> + .ident = "AYANEO AIR Plus",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Plus"),
> + },
> + .driver_data = (void *)&limits_18w,
> + },
> + /* AYANEO - 25W */
> + {
> + .ident = "AYANEO NEXT Advance",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Advance"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + {
> + .ident = "AYANEO NEXT Lite",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Lite"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + {
> + .ident = "AYANEO NEXT Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT Pro"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + {
> + .ident = "AYANEO NEXT",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "NEXT"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + {
> + .ident = "AYANEO KUN",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "KUN"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + {
> + .ident = "AYANEO KUN",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO KUN"),
> + },
> + .driver_data = (void *)&limits_25w,
> + },
> + /* AYANEO - 28W */
> + {
> + .ident = "AYANEO 2",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_MATCH(DMI_BOARD_NAME, "AYANEO 2"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + {
> + .ident = "SuiPlay0X1",
> + .matches = {
> + DMI_MATCH(DMI_SYS_VENDOR, "Mysten Labs, Inc."),
> + DMI_MATCH(DMI_PRODUCT_NAME, "SuiPlay0X1"),
> + },
> + .driver_data = (void *)&limits_28w,
> + },
> + /* AYANEO - 30W */
> + {
> + /* Must come before the shorter "AIR" match */
> + .ident = "AYANEO AIR 1S",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_MATCH(DMI_BOARD_NAME, "AIR 1S"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "AYANEO AIR Pro",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR Pro"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "AYANEO AIR",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AIR"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + /* DMI_MATCH catches all FLIP variants (DS, KB, 1S DS, 1S KB) */
> + .ident = "AYANEO FLIP",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_MATCH(DMI_BOARD_NAME, "FLIP"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + /* DMI_MATCH catches GEEK and GEEK 1S */
> + .ident = "AYANEO GEEK",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_MATCH(DMI_BOARD_NAME, "GEEK"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "AYANEO SLIDE",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "SLIDE"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + {
> + .ident = "AYANEO 3",
> + .matches = {
> + DMI_MATCH(DMI_BOARD_VENDOR, "AYANEO"),
> + DMI_EXACT_MATCH(DMI_BOARD_NAME, "AYANEO 3"),
> + },
> + .driver_data = (void *)&limits_30w,
> + },
> + { }
> +};
> +MODULE_DEVICE_TABLE(dmi, dptc_dmi_table);
> +
> +struct dptc_priv;
> +
> +struct dptc_attr_sysfs {
> + struct dptc_priv *priv;
> + struct kobj_attribute current_value;
> + struct kobj_attribute default_value;
> + struct kobj_attribute min_value;
> + struct kobj_attribute max_value;
> + struct kobj_attribute scalar_increment;
> + struct kobj_attribute display_name;
> + struct kobj_attribute type;
> + struct attribute *attrs[8];
> + struct attribute_group group;
> + int idx;
> +};
> +
> +struct dptc_priv {
> + struct device *fw_attr_dev;
> + struct kset *fw_attr_kset;
> +
> + const struct dptc_device_limits *dev_limits;
> +
> + bool expanded;
> +
> + enum platform_profile_option profile;
> + struct device *ppdev;
> +
> + enum dptc_save_mode { SAVE_SINGLE, SAVE_BULK } save_mode;
> +
> + u32 staged[DPTC_NUM_PARAMS];
> + bool has_staged[DPTC_NUM_PARAMS];
> +
> + /* Protects mutable driver state */
> + struct mutex lock;
> +
> + struct dptc_attr_sysfs params[DPTC_NUM_PARAMS];
> + struct dptc_attr_sysfs expanded_attr;
> + struct kobj_attribute save_settings_attr;
> +};
> +
> +static struct platform_device *dptc_pdev;
> +
> +static u32 dptc_get_min(struct dptc_priv *dptc, int idx)
> +{
> + return dptc->expanded ? dptc->dev_limits->params[idx].min
> + : dptc->dev_limits->params[idx].smin;
> +}
> +
> +static u32 dptc_get_max(struct dptc_priv *dptc, int idx)
> +{
> + return dptc->expanded ? dptc->dev_limits->params[idx].max
> + : dptc->dev_limits->params[idx].smax;
> +}
> +
> +static u32 dptc_get_default(struct dptc_priv *dptc, int idx)
> +{
> + return dptc->dev_limits->params[idx].def;
> +}
> +
> +static int dptc_alib_call(const u8 *ids, const u32 *vals, int count)
> +{
> + union acpi_object in_params[2];
> + struct acpi_object_list input;
> + acpi_status status;
> + u32 buf_size;
> + int i, off;
> + u8 *buf;
> +
> + if (count == 0)
> + return -EINVAL;
> +
> + /* Buffer layout: WORD total_size + count * (BYTE id + DWORD value) */
> + buf_size = 2 + count * 5;
> + buf = kzalloc(buf_size, GFP_KERNEL);
> + if (!buf)
> + return -ENOMEM;
> +
> + put_unaligned_le16(buf_size, buf);
> +
> + for (i = 0; i < count; i++) {
> + off = 2 + i * 5;
> + buf[off] = ids[i];
> + put_unaligned_le32(vals[i], buf + off + 1);
> + }
> +
> + in_params[0].type = ACPI_TYPE_INTEGER;
> + in_params[0].integer.value = ALIB_FUNC_DPTC;
> + in_params[1].type = ACPI_TYPE_BUFFER;
> + in_params[1].buffer.length = buf_size;
> + in_params[1].buffer.pointer = buf;
> +
> + input.count = 2;
> + input.pointer = in_params;
> +
> + status = acpi_evaluate_object(NULL, ALIB_PATH, &input, NULL);
> + kfree(buf);
> +
> + if (ACPI_FAILURE(status)) {
> + pr_err("ALIB call failed: %s\n",
> + acpi_format_exception(status));
> + return -EIO;
> + }
> +
> + pr_debug("sent %d ALIB parameter(s)\n", count);
> + return 0;
> +}
> +
> +static int dptc_alib_send_one(int idx, u32 val)
> +{
> + u32 vals[2];
> + u8 ids[2];
> + int count = 0;
> +
> + ids[count] = dptc_params[idx].param_id;
> + vals[count] = val;
> + count++;
> + if (dptc_params[idx].param_id2) {
> + ids[count] = dptc_params[idx].param_id2;
> + vals[count] = val;
> + count++;
> + }
> +
> + return dptc_alib_call(ids, vals, count);
> +}
> +
> +static int dptc_alib_save(struct dptc_priv *dptc)
> +{
> + u32 vals[DPTC_NUM_PARAMS * 2];
> + u8 ids[DPTC_NUM_PARAMS * 2];
> + int i, count = 0;
> +
> + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> + if (!dptc->has_staged[i])
> + continue;
> + ids[count] = dptc_params[i].param_id;
> + vals[count] = dptc->staged[i];
> + count++;
> + if (dptc_params[i].param_id2) {
> + ids[count] = dptc_params[i].param_id2;
> + vals[count] = dptc->staged[i];
> + count++;
> + }
> + }
> +
> + if (!count)
> + return 0;
> +
> + return dptc_alib_call(ids, vals, count);
> +}
> +
> +/* Sysfs callbacks */
> +
> +static ssize_t dptc_current_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, current_value);
> + struct dptc_priv *dptc = ps->priv;
> +
> + guard(mutex)(&dptc->lock);
> +
> + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
> + u32 val = dptc->dev_limits->profiles[dptc->profile].vals[ps->idx];
> +
> + if (!val)
> + return sysfs_emit(buf, "\n");
> + return sysfs_emit(buf, "%u\n", val);
> + }
> +
> + if (!dptc->has_staged[ps->idx])
> + return sysfs_emit(buf, "\n");
> + return sysfs_emit(buf, "%u\n", dptc->staged[ps->idx]);
> +}
> +
> +static ssize_t dptc_current_value_store(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + const char *buf, size_t count)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, current_value);
> + struct dptc_priv *dptc = ps->priv;
> + u32 val, min, max;
> + int ret;
> +
> + if (count == 1 && buf[0] == '\n') {
> + guard(mutex)(&dptc->lock);
> +
> + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> + return -EPERM;
> + dptc->has_staged[ps->idx] = false;
> + return count;
> + }
> +
> + ret = kstrtou32(buf, 10, &val);
> + if (ret)
> + return ret;
> +
> + guard(mutex)(&dptc->lock);
> +
> + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> + return -EPERM;
> + min = dptc_get_min(dptc, ps->idx);
> + max = dptc_get_max(dptc, ps->idx);
> + if (val < min || (max && val > max))
> + return -EINVAL;
> + dptc->staged[ps->idx] = val;
> + dptc->has_staged[ps->idx] = true;
> + if (dptc->save_mode == SAVE_SINGLE)
> + ret = dptc_alib_send_one(ps->idx, val);
> +
> + return ret ? ret : count;
> +}
> +
> +static ssize_t dptc_default_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, default_value);
> +
> + return sysfs_emit(buf, "%u\n", dptc_get_default(ps->priv, ps->idx));
> +}
> +
> +static ssize_t dptc_min_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, min_value);
> + struct dptc_priv *dptc = ps->priv;
> +
> + guard(mutex)(&dptc->lock);
> +
> + return sysfs_emit(buf, "%u\n", dptc_get_min(dptc, ps->idx));
> +}
> +
> +static ssize_t dptc_max_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, max_value);
> + struct dptc_priv *dptc = ps->priv;
> +
> + guard(mutex)(&dptc->lock);
> +
> + return sysfs_emit(buf, "%u\n", dptc_get_max(dptc, ps->idx));
> +}
> +
> +static ssize_t dptc_scalar_increment_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + return sysfs_emit(buf, "1\n");
> +}
> +
> +static ssize_t dptc_display_name_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, display_name);
> + return sysfs_emit(buf, "%s\n", dptc_params[ps->idx].display_name);
> +}
> +
> +static ssize_t dptc_type_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + return sysfs_emit(buf, "integer\n");
> +}
> +
> +static ssize_t dptc_save_settings_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + struct dptc_priv *dptc =
> + container_of(attr, struct dptc_priv, save_settings_attr);
> +
> + guard(mutex)(&dptc->lock);
> +
> + if (dptc->save_mode == SAVE_SINGLE)
> + return sysfs_emit(buf, "single\n");
> + return sysfs_emit(buf, "bulk\n");
> +}
> +
> +static ssize_t dptc_save_settings_store(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + const char *buf, size_t count)
> +{
> + struct dptc_priv *dptc =
> + container_of(attr, struct dptc_priv, save_settings_attr);
> + int ret = 0;
> +
> + guard(mutex)(&dptc->lock);
> +
> + if (sysfs_streq(buf, "save"))
> + ret = dptc_alib_save(dptc);
> + else if (sysfs_streq(buf, "single"))
> + dptc->save_mode = SAVE_SINGLE;
> + else if (sysfs_streq(buf, "bulk"))
> + dptc->save_mode = SAVE_BULK;
> + else
> + return -EINVAL;
> +
> + return ret ? ret : count;
> +}
> +
> +static ssize_t dptc_expanded_current_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + char *buf)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, current_value);
> + struct dptc_priv *dptc = ps->priv;
> +
> + guard(mutex)(&dptc->lock);
> +
> + return sysfs_emit(buf, "%d\n", dptc->expanded);
> +}
> +
> +static ssize_t dptc_expanded_current_value_store(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + const char *buf, size_t count)
> +{
> + struct dptc_attr_sysfs *ps =
> + container_of(attr, struct dptc_attr_sysfs, current_value);
> + struct dptc_priv *dptc = ps->priv;
> + bool val;
> + int ret;
> +
> + ret = kstrtobool(buf, &val);
> + if (ret)
> + return ret;
> +
> + guard(mutex)(&dptc->lock);
> +
> + if (dptc->profile != PLATFORM_PROFILE_CUSTOM)
> + return -EPERM;
> + dptc->expanded = val;
> + /* Clear staged values: limits changed, old values may be out of range */
> + memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
> +
> + return count;
> +}
> +
> +static ssize_t dptc_expanded_default_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + char *buf)
> +{
> + return sysfs_emit(buf, "0\n");
> +}
> +
> +static ssize_t dptc_expanded_min_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + char *buf)
> +{
> + return sysfs_emit(buf, "0\n");
> +}
> +
> +static ssize_t dptc_expanded_max_value_show(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + char *buf)
> +{
> + return sysfs_emit(buf, "1\n");
> +}
> +
> +static ssize_t dptc_expanded_scalar_increment_show(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + char *buf)
> +{
> + return sysfs_emit(buf, "1\n");
> +}
> +
> +static ssize_t dptc_expanded_display_name_show(struct kobject *kobj,
> + struct kobj_attribute *attr,
> + char *buf)
> +{
> + return sysfs_emit(buf, "Expanded Limits\n");
> +}
> +
> +static ssize_t dptc_expanded_type_show(struct kobject *kobj,
> + struct kobj_attribute *attr, char *buf)
> +{
> + return sysfs_emit(buf, "integer\n");
> +}
> +
> +/* Sysfs setup */
> +
> +static void dptc_setup_param_sysfs(struct dptc_priv *dptc,
> + struct dptc_attr_sysfs *ps, int idx)
> +{
> + ps->priv = dptc;
> + ps->idx = idx;
> +
> + sysfs_attr_init(&ps->current_value.attr);
> + ps->current_value.attr.name = "current_value";
> + ps->current_value.attr.mode = 0644;
> + ps->current_value.show = dptc_current_value_show;
> + ps->current_value.store = dptc_current_value_store;
> +
> + sysfs_attr_init(&ps->default_value.attr);
> + ps->default_value.attr.name = "default_value";
> + ps->default_value.attr.mode = 0444;
> + ps->default_value.show = dptc_default_value_show;
> +
> + sysfs_attr_init(&ps->min_value.attr);
> + ps->min_value.attr.name = "min_value";
> + ps->min_value.attr.mode = 0444;
> + ps->min_value.show = dptc_min_value_show;
> +
> + sysfs_attr_init(&ps->max_value.attr);
> + ps->max_value.attr.name = "max_value";
> + ps->max_value.attr.mode = 0444;
> + ps->max_value.show = dptc_max_value_show;
> +
> + sysfs_attr_init(&ps->scalar_increment.attr);
> + ps->scalar_increment.attr.name = "scalar_increment";
> + ps->scalar_increment.attr.mode = 0444;
> + ps->scalar_increment.show = dptc_scalar_increment_show;
> +
> + sysfs_attr_init(&ps->display_name.attr);
> + ps->display_name.attr.name = "display_name";
> + ps->display_name.attr.mode = 0444;
> + ps->display_name.show = dptc_display_name_show;
> +
> + sysfs_attr_init(&ps->type.attr);
> + ps->type.attr.name = "type";
> + ps->type.attr.mode = 0444;
> + ps->type.show = dptc_type_show;
> +
> + ps->attrs[0] = &ps->current_value.attr;
> + ps->attrs[1] = &ps->default_value.attr;
> + ps->attrs[2] = &ps->min_value.attr;
> + ps->attrs[3] = &ps->max_value.attr;
> + ps->attrs[4] = &ps->scalar_increment.attr;
> + ps->attrs[5] = &ps->display_name.attr;
> + ps->attrs[6] = &ps->type.attr;
> + ps->attrs[7] = NULL;
> +
> + ps->group.name = dptc_params[idx].name;
> + ps->group.attrs = ps->attrs;
> +}
> +
> +static void dptc_setup_expanded_sysfs(struct dptc_priv *dptc,
> + struct dptc_attr_sysfs *ps)
> +{
> + ps->priv = dptc;
> + sysfs_attr_init(&ps->current_value.attr);
> + ps->current_value.attr.name = "current_value";
> + ps->current_value.attr.mode = 0644;
> + ps->current_value.show = dptc_expanded_current_value_show;
> + ps->current_value.store = dptc_expanded_current_value_store;
> +
> + sysfs_attr_init(&ps->default_value.attr);
> + ps->default_value.attr.name = "default_value";
> + ps->default_value.attr.mode = 0444;
> + ps->default_value.show = dptc_expanded_default_value_show;
> +
> + sysfs_attr_init(&ps->min_value.attr);
> + ps->min_value.attr.name = "min_value";
> + ps->min_value.attr.mode = 0444;
> + ps->min_value.show = dptc_expanded_min_value_show;
> +
> + sysfs_attr_init(&ps->max_value.attr);
> + ps->max_value.attr.name = "max_value";
> + ps->max_value.attr.mode = 0444;
> + ps->max_value.show = dptc_expanded_max_value_show;
> +
> + sysfs_attr_init(&ps->scalar_increment.attr);
> + ps->scalar_increment.attr.name = "scalar_increment";
> + ps->scalar_increment.attr.mode = 0444;
> + ps->scalar_increment.show = dptc_expanded_scalar_increment_show;
> +
> + sysfs_attr_init(&ps->display_name.attr);
> + ps->display_name.attr.name = "display_name";
> + ps->display_name.attr.mode = 0444;
> + ps->display_name.show = dptc_expanded_display_name_show;
> +
> + sysfs_attr_init(&ps->type.attr);
> + ps->type.attr.name = "type";
> + ps->type.attr.mode = 0444;
> + ps->type.show = dptc_expanded_type_show;
> +
> + ps->attrs[0] = &ps->current_value.attr;
> + ps->attrs[1] = &ps->default_value.attr;
> + ps->attrs[2] = &ps->min_value.attr;
> + ps->attrs[3] = &ps->max_value.attr;
> + ps->attrs[4] = &ps->scalar_increment.attr;
> + ps->attrs[5] = &ps->display_name.attr;
> + ps->attrs[6] = &ps->type.attr;
> + ps->attrs[7] = NULL;
> +
> + ps->group.name = "expanded_limits";
> + ps->group.attrs = ps->attrs;
> +}
> +
> +static void dptc_fw_dev_unregister(void *data)
> +{
> + device_unregister(data);
> +}
> +
> +static void dptc_kset_unregister(void *data)
> +{
> + kset_unregister(data);
> +}
> +
> +/* Platform profile */
> +
> +static void dptc_apply_profile(struct dptc_priv *dptc,
> + enum platform_profile_option profile)
> +{
> + const struct dptc_profile *pp;
> + int i;
> +
> + memset(dptc->has_staged, 0, sizeof(dptc->has_staged));
> +
> + if (profile == PLATFORM_PROFILE_CUSTOM)
> + return;
> +
> + pp = &dptc->dev_limits->profiles[profile];
> + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> + if (!pp->vals[i])
> + continue;
> + dptc->staged[i] = pp->vals[i];
> + dptc->has_staged[i] = true;
> + }
> +}
> +
> +static int dptc_pp_probe(void *drvdata, unsigned long *choices)
> +{
> + struct dptc_priv *dptc = drvdata;
> + int i, j;
> +
> + set_bit(PLATFORM_PROFILE_CUSTOM, choices);
> + for (i = 0; i < PLATFORM_PROFILE_LAST; i++) {
> + for (j = 0; j < DPTC_NUM_PARAMS; j++) {
> + if (dptc->dev_limits->profiles[i].vals[j]) {
> + set_bit(i, choices);
> + break;
> + }
> + }
> + }
> + return 0;
> +}
> +
> +static int dptc_pp_get(struct device *dev,
> + enum platform_profile_option *profile)
> +{
> + struct dptc_priv *dptc = dev_get_drvdata(dev);
> +
> + guard(mutex)(&dptc->lock);
> +
> + *profile = dptc->profile;
> + return 0;
> +}
> +
> +static int dptc_pp_set(struct device *dev,
> + enum platform_profile_option profile)
> +{
> + struct dptc_priv *dptc = dev_get_drvdata(dev);
> + int ret = 0;
> +
> + guard(mutex)(&dptc->lock);
> +
> + dptc->profile = profile;
> + dptc_apply_profile(dptc, profile);
> + if (profile != PLATFORM_PROFILE_CUSTOM)
> + ret = dptc_alib_save(dptc);
> +
> + return ret;
> +}
> +
> +static const struct platform_profile_ops dptc_pp_ops = {
> + .probe = dptc_pp_probe,
> + .profile_get = dptc_pp_get,
> + .profile_set = dptc_pp_set,
> +};
> +
> +static int dptc_resume(struct device *dev)
> +{
> + struct dptc_priv *dptc = dev_get_drvdata(dev);
> + int ret;
> +
> + guard(mutex)(&dptc->lock);
> +
> + if (dptc->profile != PLATFORM_PROFILE_CUSTOM) {
> + dptc_apply_profile(dptc, dptc->profile);
> + ret = dptc_alib_save(dptc);
> + } else if (dptc->save_mode == SAVE_SINGLE) {
> + ret = dptc_alib_save(dptc);
> + } else {
> + ret = 0;
> + }
> +
> + if (ret)
> + dev_warn(dev, "failed to restore settings on resume: %d\n", ret);
> +
> + return 0;
return ret?
> +}
> +
> +static DEFINE_SIMPLE_DEV_PM_OPS(dptc_pm_ops, NULL, dptc_resume);
> +
> +static int dptc_probe(struct platform_device *pdev)
> +{
> + const struct dmi_system_id *dmi_match = dev_get_platdata(&pdev->dev);
> + struct dptc_priv *dptc;
> + struct device *dev = &pdev->dev;
> + int i, ret;
reverse xmas tree here
> +
> + dptc = devm_kzalloc(dev, sizeof(*dptc), GFP_KERNEL);
> + if (!dptc)
> + return -ENOMEM;
> +
> + platform_set_drvdata(pdev, dptc);
> +
> + ret = devm_mutex_init(dev, &dptc->lock);
> + if (ret)
> + return ret;
> +
> + dptc->dev_limits = dmi_match->driver_data;
> + dev_info(dev, "%s (%s)\n", dmi_match->ident,
> + boot_cpu_data.x86_model_id);
> +
> + dptc->fw_attr_dev = device_create(&firmware_attributes_class,
> + NULL, MKDEV(0, 0), NULL,
> + DRIVER_NAME);
> + if (IS_ERR(dptc->fw_attr_dev))
> + return PTR_ERR(dptc->fw_attr_dev);
> +
> + ret = devm_add_action_or_reset(dev, dptc_fw_dev_unregister,
> + dptc->fw_attr_dev);
> + if (ret)
> + return ret;
> +
> + dptc->fw_attr_kset = kset_create_and_add("attributes", NULL,
> + &dptc->fw_attr_dev->kobj);
> + if (!dptc->fw_attr_kset)
> + return -ENOMEM;
> +
> + ret = devm_add_action_or_reset(dev, dptc_kset_unregister,
> + dptc->fw_attr_kset);
> + if (ret)
> + return ret;
> +
> + for (i = 0; i < DPTC_NUM_PARAMS; i++) {
> + dptc_setup_param_sysfs(dptc, &dptc->params[i], i);
> + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> + &dptc->params[i].group);
> + if (ret)
> + return ret;
> + }
> +
> + dptc_setup_expanded_sysfs(dptc, &dptc->expanded_attr);
> + ret = sysfs_create_group(&dptc->fw_attr_kset->kobj,
> + &dptc->expanded_attr.group);
> + if (ret)
> + return ret;
> +
> + sysfs_attr_init(&dptc->save_settings_attr.attr);
> + dptc->save_settings_attr.attr.name = "save_settings";
> + dptc->save_settings_attr.attr.mode = 0644;
> + dptc->save_settings_attr.show = dptc_save_settings_show;
> + dptc->save_settings_attr.store = dptc_save_settings_store;
> + ret = sysfs_create_file(&dptc->fw_attr_kset->kobj,
> + &dptc->save_settings_attr.attr);
> + if (ret)
> + return ret;
Given multiple drivers have a save_settings feature now, maybe it's
worth creating a helper as part of this series.
> +
> + dptc->profile = PLATFORM_PROFILE_CUSTOM;
So it starts in custom? Wouldn't it make more sense to start in balanced?
> + dptc->ppdev = devm_platform_profile_register(dev, "amd-dptc", dptc,
> + &dptc_pp_ops);
> + if (IS_ERR(dptc->ppdev))
> + return PTR_ERR(dptc->ppdev);
> +
> + return 0;
> +}
> +
> +static struct platform_driver dptc_driver = {
> + .driver = {
> + .name = DRIVER_NAME,
> + .pm = pm_sleep_ptr(&dptc_pm_ops),
> + },
> + .probe = dptc_probe,
> +};
> +
> +static int __init dptc_init(void)
> +{
> + const struct dmi_system_id *match;
> + bool soc_found = false;
> + int i, ret;
> +
> + if (!acpi_has_method(NULL, ALIB_PATH)) {
> + pr_debug("ALIB method not present\n");
To avoid an extra debug statement on every machine not quirked - how
about you start with the quirk check?
If not that, could you do any checks that ALIB_FUNC_DPTC is supported?
Not sure if there was a method for that.
> + return -ENODEV;
> + }
I will admit I didn't look at the spec, but is there a version lookup
for this method that can be used?
> +
> + match = dmi_first_match(dptc_dmi_table);
> + if (!match)
> + return -ENODEV;
> +
> + for (i = 0; dptc_soc_table[i]; i++) {
> + if (strstr(boot_cpu_data.x86_model_id,
> + dptc_soc_table[i])) {
> + soc_found = true;
> + break;
> + }
> + }
> + if (!soc_found) {
> + pr_warn("unrecognized SoC '%s'\n",
> + boot_cpu_data.x86_model_id);
> + return -ENODEV;
> + }
Is this SOC lookup still going to be needed if you're 100% quirks table?
I would think no.
> +
> + dptc_pdev = platform_device_register_data(NULL, DRIVER_NAME, -1,
> + match, sizeof(*match));
> + if (IS_ERR(dptc_pdev))
> + return PTR_ERR(dptc_pdev);
> +
> + ret = platform_driver_register(&dptc_driver);
> + if (ret) {
> + platform_device_unregister(dptc_pdev);
> + return ret;
> + }
> +
> + return 0;
> +}
> +
> +static void __exit dptc_exit(void)
> +{
> + platform_driver_unregister(&dptc_driver);
> + platform_device_unregister(dptc_pdev);
> +}
> +
> +module_init(dptc_init);
> +module_exit(dptc_exit);
> +
> +MODULE_AUTHOR("Antheas Kapenekakis <lkml@antheas.dev>");
> +MODULE_DESCRIPTION("AMD DPTCi ACPI Driver");
> +MODULE_LICENSE("GPL");
next prev parent reply other threads:[~2026-03-05 20:58 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-05 18:17 [RFC v2 0/2] platform/x86/amd: Add AMD DPTCi driver for TDP control in devices without vendor-specific controls Antheas Kapenekakis
2026-03-05 18:17 ` [RFC v2 1/2] Documentation: firmware-attributes: generalize save_settings entry Antheas Kapenekakis
2026-03-05 18:17 ` [RFC v2 2/2] platform/x86/amd: Add AMD DPTCi driver Antheas Kapenekakis
2026-03-05 20:57 ` Mario Limonciello [this message]
2026-03-05 21:18 ` Antheas Kapenekakis
2026-03-06 21:14 ` Derek John Clark
2026-03-07 11:17 ` Antheas Kapenekakis
2026-03-07 11:25 ` Antheas Kapenekakis
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=54ca62dc-d590-44a3-8310-f7da59c2a42e@amd.com \
--to=mario.limonciello@amd.com \
--cc=Shyam-Sundar.S-k@amd.com \
--cc=W_Armin@gmx.de \
--cc=denis.benato@linux.dev \
--cc=derekjohn.clark@gmail.com \
--cc=linux-kernel@vger.kernel.org \
--cc=lkml@antheas.dev \
--cc=platform-driver-x86@vger.kernel.org \
--cc=sashal@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is 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®